From 74fc8f91c5ab9ad23f2d4f8e35c91a83daac99c1 Mon Sep 17 00:00:00 2001 From: rabbitstack Date: Wed, 2 Dec 2020 08:29:26 +0100 Subject: [PATCH] new generation fibratus initial commit --- .gitignore | 32 +- .landscape.yml | 4 - MANIFEST.in | 8 - appveyor.yml | 28 - build/package/LICENSE.txt | 11 + build/package/fibratus.nsi | 163 + cmd/fibratus/app/capture.go | 143 + cmd/fibratus/app/config.go | 60 + cmd/fibratus/app/control_service.go | 265 + cmd/fibratus/app/docs.go | 32 + cmd/fibratus/app/install_service.go | 72 + cmd/fibratus/app/list.go | 154 + cmd/fibratus/app/remove_service.go | 57 + cmd/fibratus/app/replay.go | 142 + cmd/fibratus/app/root.go | 65 + cmd/fibratus/app/run.go | 187 + cmd/fibratus/app/stats.go | 162 + cmd/fibratus/app/version.go | 40 + cmd/fibratus/fibratus.exe.manifest | 9 + cmd/fibratus/fibratus.rc | 38 + cmd/fibratus/main.go | 42 + cmd/fibratus/version.h | 6 + configs/fibratus.json | 55 + configs/fibratus.yml | 476 + fibratus.spec | 30 - fibratus.yml | 55 - fibratus/__init__.py | 15 - fibratus/apidefs/__init__.py | 14 - fibratus/apidefs/cdefs.py | 74 - fibratus/apidefs/declarer.py | 43 - fibratus/apidefs/etw.py | 143 - fibratus/apidefs/fs.py | 66 - fibratus/apidefs/guiddef.py | 44 - fibratus/apidefs/process.py | 115 - fibratus/apidefs/registry.py | 51 - fibratus/apidefs/sys.py | 157 - fibratus/binding/__init__.py | 14 - fibratus/binding/base.py | 24 - fibratus/binding/yar.py | 94 - fibratus/cli.py | 135 - fibratus/common.py | 92 - fibratus/config.py | 85 - fibratus/context_switch.py | 405 - fibratus/controller.py | 158 - fibratus/dll.py | 138 - fibratus/entrypoint.py | 435 - fibratus/errors.py | 54 - fibratus/filament.py | 321 - fibratus/fs.py | 223 - fibratus/handle.py | 268 - fibratus/image_meta.py | 237 - fibratus/kevent.py | 269 - fibratus/kevent_types.py | 239 - fibratus/output/aggregator.py | 49 - fibratus/output/amqp.py | 104 - fibratus/output/base.py | 30 - fibratus/output/console.py | 97 - fibratus/output/elasticsearch.py | 95 - fibratus/output/fs.py | 55 - fibratus/output/smtp.py | 82 - fibratus/registry.py | 294 - fibratus/tcpip/__init__.py | 14 - fibratus/tcpip/ports.py | 11095 --------------- fibratus/tcpip/tcpip.py | 123 - fibratus/term.py | 191 - fibratus/thread.py | 504 - fibratus/version.py | 21 - filaments/anomalous_process_netio.py | 44 - filaments/elasticsearch_indexing.py | 56 - filaments/fishy_netio.py | 73 + filaments/registry_persistence.py | 91 + filaments/registry_persistence_detection.py | 41 - filaments/top_in_packets.py | 22 +- ...top_registry_io_process.py => top_keys.py} | 25 +- filaments/top_out_packets.py | 22 +- filaments/utils/dotdict.py | 17 + .../{created_files.py => watch_files.py} | 23 +- go.mod | 36 + go.sum | 283 + kstream/__init__.py | 14 - kstream/includes/__init__.py | 14 - kstream/includes/etw.pxd | 97 - kstream/includes/python.pxd | 141 - kstream/includes/stdlib.pxd | 19 - kstream/includes/string.pxd | 41 - kstream/includes/tdh.pxd | 148 - kstream/includes/windows.pxd | 121 - kstream/kstreamc.pxd | 32 - kstream/kstreamc.pyx | 648 - kstream/ktuple.pxd | 27 - kstream/process.pxd | 57 - kstream/time.pxd | 29 - make.bat | 110 + pkg-config/python-37.pc | 11 + pkg/aggregator/aggregator.go | 187 + pkg/aggregator/aggregator_test.go | 76 + pkg/aggregator/config.go | 50 + pkg/aggregator/submitter.go | 57 + pkg/aggregator/transformers/config.go | 30 + pkg/aggregator/transformers/remove/config.go | 40 + pkg/aggregator/transformers/remove/remove.go | 52 + .../transformers/remove/remove_test.go | 53 + pkg/aggregator/transformers/rename/config.go | 44 + pkg/aggregator/transformers/rename/rename.go | 54 + .../transformers/rename/rename_test.go | 56 + pkg/aggregator/transformers/replace/config.go | 45 + .../transformers/replace/replace.go | 68 + .../transformers/replace/replace_test.go | 50 + pkg/aggregator/transformers/tags/config.go | 46 + pkg/aggregator/transformers/tags/tags.go | 68 + pkg/aggregator/transformers/tags/tags_test.go | 58 + pkg/aggregator/transformers/transformer.go | 99 + pkg/aggregator/transformers/trim/config.go | 46 + pkg/aggregator/transformers/trim/trim.go | 74 + pkg/aggregator/transformers/trim/trim_test.go | 67 + pkg/aggregator/worker.go | 70 + pkg/aggregator/worker_test.go | 108 + pkg/alertsender/alert.go | 83 + pkg/alertsender/config.go | 25 + pkg/alertsender/mail/config.go | 60 + pkg/alertsender/mail/mail.go | 61 + pkg/alertsender/sender.go | 122 + pkg/alertsender/slack/config.go | 52 + pkg/alertsender/slack/slack.go | 129 + pkg/api/handler/config.go | 33 + pkg/api/listener.go | 65 + pkg/api/server.go | 89 + pkg/config/_fixtures/fibratus.json | 53 + pkg/config/_fixtures/fibratus.yml | 230 + pkg/config/_fixtures/output.yml | 23 + pkg/config/_fixtures/transformers.yml | 29 + pkg/config/alertsender.go | 83 + pkg/config/api.go | 43 + pkg/config/config.go | 338 + pkg/config/config_test.go | 113 + pkg/config/decoder.go | 38 + pkg/config/filament.go | 41 + pkg/config/kstream.go | 94 + pkg/config/kstream_test.go | 48 + pkg/config/output.go | 137 + pkg/config/output_test.go | 49 + pkg/config/print.go | 108 + pkg/config/print_test.go | 34 + pkg/config/schema.go | 446 + pkg/config/transformer.go | 123 + pkg/config/transformer_test.go | 37 + pkg/config/validation.go | 108 + pkg/config/validation_test.go | 92 + pkg/errors/errors.go | 89 + .../filament/_fixtures/test_filter.py | 13 +- .../filament/_fixtures/test_on_next_kevent.py | 17 +- .../filament/_fixtures/top_hives_io.py | 16 +- pkg/filament/_fixtures/top_keys_io_table.py | 43 + .../cpython/_fixtures}/top_hives_io.py | 19 +- pkg/filament/cpython/api.c | 163 + pkg/filament/cpython/api.h | 58 + pkg/filament/cpython/dict.go | 55 + pkg/filament/cpython/dict_test.go | 36 + pkg/filament/cpython/errors.go | 86 + pkg/filament/cpython/gil.go | 85 + pkg/filament/cpython/gil_test.go | 42 + pkg/filament/cpython/interpreter.go | 91 + pkg/filament/cpython/interpreter_test.go | 29 + pkg/filament/cpython/ip.go | 50 + pkg/filament/cpython/module.go | 88 + pkg/filament/cpython/module_test.go | 60 + pkg/filament/cpython/object.go | 379 + pkg/filament/cpython/sequence.go | 39 + pkg/filament/cpython/string.go | 32 + pkg/filament/filament.go | 554 + pkg/filament/filament_test.go | 127 + pkg/filament/filament_unsupported.go | 38 + pkg/filament/kdict.go | 89 + pkg/filament/kdict_test.go | 117 + pkg/filament/table.go | 78 + pkg/filament/table_test.go | 36 + pkg/filament/types.go | 41 + pkg/filter/accessor.go | 543 + pkg/filter/accessor_test.go | 98 + pkg/filter/fields/fields.go | 309 + pkg/filter/fields/fields_test.go | 38 + pkg/filter/filter.go | 133 + pkg/filter/filter_test.go | 297 + pkg/filter/ql/ast.go | 611 + pkg/filter/ql/error.go | 57 + pkg/filter/ql/error_test.go | 31 + pkg/filter/ql/expr.go | 51 + pkg/filter/ql/lexer.go | 507 + pkg/filter/ql/lexer_test.go | 97 + pkg/filter/ql/literal.go | 98 + pkg/filter/ql/parser.go | 203 + pkg/filter/ql/parser_test.go | 69 + pkg/filter/ql/token.go | 157 + pkg/filter/ql/visitor.go | 49 + .../__init__.py => pkg/fs/_fixtures/.gitkeep | 0 pkg/fs/attrs.go | 64 + pkg/fs/dev.go | 67 + pkg/fs/dev_test.go | 70 + pkg/fs/file.go | 114 + pkg/fs/file_test.go | 35 + pkg/fs/types.go | 123 + pkg/handle/_fixtures/.fibratus | 0 pkg/handle/alpc.go | 35 + pkg/handle/key.go | 111 + pkg/handle/key_test.go | 55 + pkg/handle/mutant.go | 42 + pkg/handle/object.go | 249 + pkg/handle/object_test.go | 108 + pkg/handle/snapshotter.go | 414 + pkg/handle/snapshotter_mock.go | 42 + pkg/handle/snapshotter_test.go | 45 + pkg/handle/timeout.go | 122 + pkg/handle/timeout_test.go | 30 + pkg/handle/types.go | 72 + pkg/handle/types/marshaller.go | 166 + pkg/handle/types/marshaller_test.go | 72 + pkg/handle/types/types.go | 105 + pkg/kcap/_fixtures/cap.kcap | Bin 0 -> 977 bytes pkg/kcap/_fixtures/cap1.kcap | Bin 0 -> 1832 bytes pkg/kcap/config.go | 26 + pkg/kcap/header.go | 51 + pkg/kcap/reader.go | 254 + pkg/kcap/reader_test.go | 56 + pkg/kcap/reader_unsupported.go | 31 + pkg/kcap/section/section.go | 86 + pkg/kcap/section/section_test.go | 33 + pkg/kcap/types.go | 55 + pkg/kcap/version/version.go | 38 + pkg/kcap/writer.go | 277 + pkg/kcap/writer_test.go | 163 + pkg/kcap/writer_unsupported.go | 32 + pkg/kevent/README.md | 1 + pkg/kevent/batch.go | 58 + pkg/kevent/batch_test.go | 252 + pkg/kevent/doc.go | 21 + pkg/kevent/formatter.go | 230 + pkg/kevent/formatter_test.go | 137 + pkg/kevent/kevent.go | 191 + pkg/kevent/kevent_test.go | 19 + pkg/kevent/kparam.go | 554 + pkg/kevent/kparam_test.go | 62 + pkg/kevent/kparams/canonicalize.go | 191 + pkg/kevent/kparams/canonicalize_test.go | 19 + pkg/kevent/kparams/fields.go | 147 + pkg/kevent/kparams/types.go | 167 + pkg/kevent/kparams/types_test.go | 38 + pkg/kevent/ktypes/category.go | 43 + pkg/kevent/ktypes/ktypes.go | 290 + pkg/kevent/ktypes/ktypes_test.go | 69 + pkg/kevent/ktypes/metainfo.go | 149 + pkg/kevent/ktypes/metainfo_test.go | 46 + pkg/kevent/marshaller.go | 1213 ++ pkg/kevent/marshaller_test.go | 581 + pkg/kevent/sequencer.go | 128 + pkg/kevent/sequencer_test.go | 60 + pkg/kstream/README.md | 1 + .../_fixtures/snapshots/create-process.gob | Bin 0 -> 878 bytes pkg/kstream/controller.go | 347 + pkg/kstream/controller_test.go | 77 + pkg/kstream/doc.go | 21 + pkg/kstream/interceptors/chain.go | 128 + pkg/kstream/interceptors/fs.go | 450 + pkg/kstream/interceptors/fs_test.go | 211 + pkg/kstream/interceptors/handle.go | 162 + pkg/kstream/interceptors/handle_test.go | 194 + pkg/kstream/interceptors/image.go | 83 + pkg/kstream/interceptors/image_test.go | 19 + pkg/kstream/interceptors/interceptor.go | 66 + pkg/kstream/interceptors/net.go | 116 + pkg/kstream/interceptors/net_test.go | 68 + pkg/kstream/interceptors/ps.go | 171 + pkg/kstream/interceptors/ps_test.go | 92 + pkg/kstream/interceptors/registry.go | 276 + pkg/kstream/interceptors/registry_test.go | 72 + pkg/kstream/kstream_rundownc.go | 176 + pkg/kstream/kstreamc.go | 611 + pkg/kstream/kstreamc_test.go | 242 + pkg/net/types.go | 41 + pkg/outputs/amqp/_fixtures/garagemq/README.md | 1 + .../garagemq/amqp/constants_generated.go | 356 + .../garagemq/amqp/extended_constants.go | 5 + .../garagemq/amqp/methods_generated.go | 4899 +++++++ .../garagemq/amqp/readers_writers.go | 882 ++ .../amqp/_fixtures/garagemq/amqp/types.go | 198 + .../amqp/_fixtures/garagemq/auth/auth.go | 51 + .../_fixtures/garagemq/binding/binding.go | 311 + .../amqp/_fixtures/garagemq/config/config.go | 89 + .../amqp/_fixtures/garagemq/config/default.go | 47 + .../_fixtures/garagemq/consumer/consumer.go | 171 + .../_fixtures/garagemq/exchange/exchange.go | 270 + .../garagemq/interfaces/interfaces.go | 57 + .../amqp/_fixtures/garagemq/pool/pool.go | 51 + .../amqp/_fixtures/garagemq/qos/qos.go | 101 + .../amqp/_fixtures/garagemq/queue/queue.go | 529 + .../_fixtures/garagemq/safequeue/safequeue.go | 148 + .../_fixtures/garagemq/server/basicMethods.go | 130 + .../amqp/_fixtures/garagemq/server/channel.go | 638 + .../garagemq/server/channelMethods.go | 51 + .../garagemq/server/confirmMethods.go | 23 + .../_fixtures/garagemq/server/connection.go | 409 + .../garagemq/server/connectionMethods.go | 134 + .../garagemq/server/exchangeMethods.go | 97 + .../_fixtures/garagemq/server/queueMethods.go | 244 + .../amqp/_fixtures/garagemq/server/server.go | 255 + .../amqp/_fixtures/garagemq/server/vhost.go | 304 + pkg/outputs/amqp/amqp.go | 81 + pkg/outputs/amqp/amqp_test.go | 384 + pkg/outputs/amqp/client.go | 193 + pkg/outputs/amqp/config.go | 120 + pkg/outputs/client.go | 30 + pkg/outputs/config.go | 52 + pkg/outputs/console/config.go | 44 + pkg/outputs/console/console.go | 124 + pkg/outputs/elasticsearch/config.go | 101 + pkg/outputs/elasticsearch/elasticsearch.go | 201 + .../elasticsearch/elasticsearch_test.go | 355 + pkg/outputs/elasticsearch/index.go | 95 + pkg/outputs/elasticsearch/index_test.go | 50 + pkg/outputs/elasticsearch/template.go | 74 + pkg/outputs/null/config.go | 22 + pkg/outputs/null/null.go | 46 + pkg/outputs/outputs.go | 94 + pkg/pe/config.go | 71 + pkg/pe/doc.go | 21 + pkg/pe/entropy.go | 41 + pkg/pe/marshaller.go | 223 + pkg/pe/marshaller_test.go | 85 + pkg/pe/reader.go | 193 + pkg/pe/reader_test.go | 55 + pkg/pe/resource/types.go | 141 + pkg/pe/resources.go | 398 + pkg/pe/resources_test.go | 19 + pkg/pe/section.go | 157 + pkg/pe/section_test.go | 19 + pkg/pe/types.go | 94 + pkg/ps/doc.go | 20 + pkg/ps/peb.go | 148 + pkg/ps/peb_test.go | 42 + pkg/ps/snapshotter.go | 525 + pkg/ps/snapshotter_mock.go | 44 + pkg/ps/snapshotter_test.go | 379 + pkg/ps/types/marshaller.go | 334 + pkg/ps/types/marshaller_test.go | 97 + pkg/ps/types/types.go | 293 + pkg/syscall/doc.go | 20 + pkg/syscall/etw/etw.go | 159 + pkg/syscall/etw/types.go | 465 + pkg/syscall/file/file.go | 124 + pkg/syscall/file/types.go | 54 + pkg/syscall/handle/handle.go | 83 + pkg/syscall/object/alpc.go | 50 + pkg/syscall/object/event.go | 73 + pkg/syscall/object/mutant.go | 48 + pkg/syscall/object/object.go | 63 + pkg/syscall/object/types.go | 100 + pkg/syscall/process/process.go | 174 + pkg/syscall/process/types.go | 76 + pkg/syscall/registry/key.go | 52 + pkg/syscall/security/privileges.go | 158 + pkg/syscall/security/sid.go | 131 + pkg/syscall/sys/sys.go | 57 + pkg/syscall/tdh/tdh.go | 90 + pkg/syscall/tdh/types.go | 139 + pkg/syscall/thread/thread.go | 79 + pkg/syscall/utf16/string.go | 73 + pkg/syscall/ver/ver.go | 24 + pkg/syscall/winerrno/errors.go | 53 + pkg/util/bytes/bytes.go | 80 + pkg/util/fasttemplate/doc.go | 25 + pkg/util/fasttemplate/template.go | 210 + pkg/util/fasttemplate/unsafe.go | 33 + pkg/util/filetime/filetime.go | 30 + pkg/util/hostname/hostname.go | 109 + pkg/util/ip/ip.go | 46 + pkg/util/ip/ip_test.go | 34 + pkg/util/log/_fixtures/.gitkeep | 0 pkg/util/log/_fixtures/fibratus.log | 1 + pkg/util/log/config.go | 76 + pkg/util/log/logger.go | 112 + pkg/util/log/logger_test.go | 38 + pkg/util/log/rotate/rotate.go | 137 + pkg/util/multierror/multierror.go | 55 + pkg/util/ports/iana_ports.go | 11352 ++++++++++++++++ pkg/util/rest/rest.go | 125 + pkg/util/rest/rest_test.go | 79 + pkg/util/spinner/spinner.go | 33 + pkg/util/term/fb.go | 175 + pkg/util/term/term.go | 95 + pkg/util/tls/tls.go | 63 + pkg/util/typesize/typesize.go | 26 + pkg/yara/_fixtures/rules/dll.yar | 10 + pkg/yara/_fixtures/rules/notepad.yar | 21 + pkg/yara/_fixtures/yara-test.dll | Bin 0 -> 1633792 bytes pkg/yara/config/config.go | 159 + pkg/yara/scanner.go | 385 + pkg/yara/scanner_test.go | 185 + pkg/yara/scanner_unsupported.go | 32 + pkg/yara/types.go | 31 + requirements.txt | 17 - schema.yml | 132 - setup.py | 84 - tests/__init__.py | 14 - tests/fixtures/__init__.py | 15 - tests/fixtures/fibratus.yml | 54 - tests/fixtures/filaments/__init__.py | 15 - .../test_filament_no_on_next_kevent.py | 23 - .../fixtures/filaments/test_filament_nodoc.py | 23 - .../test_filament_wrong_on_next_kevent.py | 27 - tests/fixtures/schema.yml | 127 - tests/pytest.ini | 3 - tests/unit/__init__.py | 14 - tests/unit/apidefs/__init__.py | 15 - tests/unit/apidefs/declarer.py | 33 - tests/unit/binding/__init__.py | 14 - tests/unit/binding/yar.py | 81 - tests/unit/cli.py | 15 - tests/unit/config.py | 76 - tests/unit/context_switch.py | 97 - tests/unit/controller.py | 24 - tests/unit/dll.py | 26 - tests/unit/filament.py | 174 - tests/unit/fs.py | 153 - tests/unit/handle.py | 80 - tests/unit/image_meta.py | 57 - tests/unit/kevent.py | 113 - tests/unit/kevent_types.py | 44 - tests/unit/output/__init__.py | 14 - tests/unit/output/aggregator.py | 48 - tests/unit/output/amqp.py | 84 - tests/unit/output/elasticsearch.py | 137 - tests/unit/output/fs.py | 40 - tests/unit/output/smtp.py | 68 - tests/unit/registry.py | 199 - tests/unit/tcpip/__init__.py | 15 - tests/unit/tcpip/tcpip.py | 151 - tests/unit/term.py | 129 - tests/unit/thread.py | 250 - 437 files changed, 56781 insertions(+), 21102 deletions(-) delete mode 100644 .landscape.yml delete mode 100644 MANIFEST.in delete mode 100644 appveyor.yml create mode 100644 build/package/LICENSE.txt create mode 100644 build/package/fibratus.nsi create mode 100644 cmd/fibratus/app/capture.go create mode 100644 cmd/fibratus/app/config.go create mode 100644 cmd/fibratus/app/control_service.go create mode 100644 cmd/fibratus/app/docs.go create mode 100644 cmd/fibratus/app/install_service.go create mode 100644 cmd/fibratus/app/list.go create mode 100644 cmd/fibratus/app/remove_service.go create mode 100644 cmd/fibratus/app/replay.go create mode 100644 cmd/fibratus/app/root.go create mode 100644 cmd/fibratus/app/run.go create mode 100644 cmd/fibratus/app/stats.go create mode 100644 cmd/fibratus/app/version.go create mode 100644 cmd/fibratus/fibratus.exe.manifest create mode 100644 cmd/fibratus/fibratus.rc create mode 100644 cmd/fibratus/main.go create mode 100644 cmd/fibratus/version.h create mode 100644 configs/fibratus.json create mode 100644 configs/fibratus.yml delete mode 100644 fibratus.spec delete mode 100644 fibratus.yml delete mode 100644 fibratus/__init__.py delete mode 100644 fibratus/apidefs/__init__.py delete mode 100644 fibratus/apidefs/cdefs.py delete mode 100644 fibratus/apidefs/declarer.py delete mode 100644 fibratus/apidefs/etw.py delete mode 100644 fibratus/apidefs/fs.py delete mode 100644 fibratus/apidefs/guiddef.py delete mode 100644 fibratus/apidefs/process.py delete mode 100644 fibratus/apidefs/registry.py delete mode 100644 fibratus/apidefs/sys.py delete mode 100644 fibratus/binding/__init__.py delete mode 100644 fibratus/binding/base.py delete mode 100644 fibratus/binding/yar.py delete mode 100644 fibratus/cli.py delete mode 100644 fibratus/common.py delete mode 100644 fibratus/config.py delete mode 100644 fibratus/context_switch.py delete mode 100644 fibratus/controller.py delete mode 100644 fibratus/dll.py delete mode 100644 fibratus/entrypoint.py delete mode 100644 fibratus/errors.py delete mode 100644 fibratus/filament.py delete mode 100644 fibratus/fs.py delete mode 100644 fibratus/handle.py delete mode 100644 fibratus/image_meta.py delete mode 100644 fibratus/kevent.py delete mode 100644 fibratus/kevent_types.py delete mode 100644 fibratus/output/aggregator.py delete mode 100644 fibratus/output/amqp.py delete mode 100644 fibratus/output/base.py delete mode 100644 fibratus/output/console.py delete mode 100644 fibratus/output/elasticsearch.py delete mode 100644 fibratus/output/fs.py delete mode 100644 fibratus/output/smtp.py delete mode 100644 fibratus/registry.py delete mode 100644 fibratus/tcpip/__init__.py delete mode 100644 fibratus/tcpip/ports.py delete mode 100644 fibratus/tcpip/tcpip.py delete mode 100644 fibratus/term.py delete mode 100644 fibratus/thread.py delete mode 100644 fibratus/version.py delete mode 100644 filaments/anomalous_process_netio.py delete mode 100644 filaments/elasticsearch_indexing.py create mode 100644 filaments/fishy_netio.py create mode 100644 filaments/registry_persistence.py delete mode 100644 filaments/registry_persistence_detection.py rename filaments/{top_registry_io_process.py => top_keys.py} (61%) create mode 100644 filaments/utils/dotdict.py rename filaments/{created_files.py => watch_files.py} (64%) create mode 100644 go.mod create mode 100644 go.sum delete mode 100644 kstream/__init__.py delete mode 100644 kstream/includes/__init__.py delete mode 100644 kstream/includes/etw.pxd delete mode 100644 kstream/includes/python.pxd delete mode 100644 kstream/includes/stdlib.pxd delete mode 100644 kstream/includes/string.pxd delete mode 100644 kstream/includes/tdh.pxd delete mode 100644 kstream/includes/windows.pxd delete mode 100644 kstream/kstreamc.pxd delete mode 100644 kstream/kstreamc.pyx delete mode 100644 kstream/ktuple.pxd delete mode 100644 kstream/process.pxd delete mode 100644 kstream/time.pxd create mode 100644 make.bat create mode 100644 pkg-config/python-37.pc create mode 100644 pkg/aggregator/aggregator.go create mode 100644 pkg/aggregator/aggregator_test.go create mode 100644 pkg/aggregator/config.go create mode 100644 pkg/aggregator/submitter.go create mode 100644 pkg/aggregator/transformers/config.go create mode 100644 pkg/aggregator/transformers/remove/config.go create mode 100644 pkg/aggregator/transformers/remove/remove.go create mode 100644 pkg/aggregator/transformers/remove/remove_test.go create mode 100644 pkg/aggregator/transformers/rename/config.go create mode 100644 pkg/aggregator/transformers/rename/rename.go create mode 100644 pkg/aggregator/transformers/rename/rename_test.go create mode 100644 pkg/aggregator/transformers/replace/config.go create mode 100644 pkg/aggregator/transformers/replace/replace.go create mode 100644 pkg/aggregator/transformers/replace/replace_test.go create mode 100644 pkg/aggregator/transformers/tags/config.go create mode 100644 pkg/aggregator/transformers/tags/tags.go create mode 100644 pkg/aggregator/transformers/tags/tags_test.go create mode 100644 pkg/aggregator/transformers/transformer.go create mode 100644 pkg/aggregator/transformers/trim/config.go create mode 100644 pkg/aggregator/transformers/trim/trim.go create mode 100644 pkg/aggregator/transformers/trim/trim_test.go create mode 100644 pkg/aggregator/worker.go create mode 100644 pkg/aggregator/worker_test.go create mode 100644 pkg/alertsender/alert.go create mode 100644 pkg/alertsender/config.go create mode 100644 pkg/alertsender/mail/config.go create mode 100644 pkg/alertsender/mail/mail.go create mode 100644 pkg/alertsender/sender.go create mode 100644 pkg/alertsender/slack/config.go create mode 100644 pkg/alertsender/slack/slack.go create mode 100644 pkg/api/handler/config.go create mode 100644 pkg/api/listener.go create mode 100644 pkg/api/server.go create mode 100644 pkg/config/_fixtures/fibratus.json create mode 100644 pkg/config/_fixtures/fibratus.yml create mode 100644 pkg/config/_fixtures/output.yml create mode 100644 pkg/config/_fixtures/transformers.yml create mode 100644 pkg/config/alertsender.go create mode 100644 pkg/config/api.go create mode 100644 pkg/config/config.go create mode 100644 pkg/config/config_test.go create mode 100644 pkg/config/decoder.go create mode 100644 pkg/config/filament.go create mode 100644 pkg/config/kstream.go create mode 100644 pkg/config/kstream_test.go create mode 100644 pkg/config/output.go create mode 100644 pkg/config/output_test.go create mode 100644 pkg/config/print.go create mode 100644 pkg/config/print_test.go create mode 100644 pkg/config/schema.go create mode 100644 pkg/config/transformer.go create mode 100644 pkg/config/transformer_test.go create mode 100644 pkg/config/validation.go create mode 100644 pkg/config/validation_test.go create mode 100644 pkg/errors/errors.go rename tests/fixtures/filaments/test_filament_interval.py => pkg/filament/_fixtures/test_filter.py (80%) rename tests/fixtures/filaments/test_filament_invalid_interval.py => pkg/filament/_fixtures/test_on_next_kevent.py (62%) rename tests/fixtures/filaments/test_filament.py => pkg/filament/_fixtures/top_hives_io.py (76%) create mode 100644 pkg/filament/_fixtures/top_keys_io_table.py rename {filaments => pkg/filament/cpython/_fixtures}/top_hives_io.py (77%) create mode 100644 pkg/filament/cpython/api.c create mode 100644 pkg/filament/cpython/api.h create mode 100644 pkg/filament/cpython/dict.go create mode 100644 pkg/filament/cpython/dict_test.go create mode 100644 pkg/filament/cpython/errors.go create mode 100644 pkg/filament/cpython/gil.go create mode 100644 pkg/filament/cpython/gil_test.go create mode 100644 pkg/filament/cpython/interpreter.go create mode 100644 pkg/filament/cpython/interpreter_test.go create mode 100644 pkg/filament/cpython/ip.go create mode 100644 pkg/filament/cpython/module.go create mode 100644 pkg/filament/cpython/module_test.go create mode 100644 pkg/filament/cpython/object.go create mode 100644 pkg/filament/cpython/sequence.go create mode 100644 pkg/filament/cpython/string.go create mode 100644 pkg/filament/filament.go create mode 100644 pkg/filament/filament_test.go create mode 100644 pkg/filament/filament_unsupported.go create mode 100644 pkg/filament/kdict.go create mode 100644 pkg/filament/kdict_test.go create mode 100644 pkg/filament/table.go create mode 100644 pkg/filament/table_test.go create mode 100644 pkg/filament/types.go create mode 100644 pkg/filter/accessor.go create mode 100644 pkg/filter/accessor_test.go create mode 100644 pkg/filter/fields/fields.go create mode 100644 pkg/filter/fields/fields_test.go create mode 100644 pkg/filter/filter.go create mode 100644 pkg/filter/filter_test.go create mode 100644 pkg/filter/ql/ast.go create mode 100644 pkg/filter/ql/error.go create mode 100644 pkg/filter/ql/error_test.go create mode 100644 pkg/filter/ql/expr.go create mode 100644 pkg/filter/ql/lexer.go create mode 100644 pkg/filter/ql/lexer_test.go create mode 100644 pkg/filter/ql/literal.go create mode 100644 pkg/filter/ql/parser.go create mode 100644 pkg/filter/ql/parser_test.go create mode 100644 pkg/filter/ql/token.go create mode 100644 pkg/filter/ql/visitor.go rename fibratus/output/__init__.py => pkg/fs/_fixtures/.gitkeep (100%) create mode 100644 pkg/fs/attrs.go create mode 100644 pkg/fs/dev.go create mode 100644 pkg/fs/dev_test.go create mode 100644 pkg/fs/file.go create mode 100644 pkg/fs/file_test.go create mode 100644 pkg/fs/types.go create mode 100644 pkg/handle/_fixtures/.fibratus create mode 100644 pkg/handle/alpc.go create mode 100644 pkg/handle/key.go create mode 100644 pkg/handle/key_test.go create mode 100644 pkg/handle/mutant.go create mode 100644 pkg/handle/object.go create mode 100644 pkg/handle/object_test.go create mode 100644 pkg/handle/snapshotter.go create mode 100644 pkg/handle/snapshotter_mock.go create mode 100644 pkg/handle/snapshotter_test.go create mode 100644 pkg/handle/timeout.go create mode 100644 pkg/handle/timeout_test.go create mode 100644 pkg/handle/types.go create mode 100644 pkg/handle/types/marshaller.go create mode 100644 pkg/handle/types/marshaller_test.go create mode 100644 pkg/handle/types/types.go create mode 100644 pkg/kcap/_fixtures/cap.kcap create mode 100644 pkg/kcap/_fixtures/cap1.kcap create mode 100644 pkg/kcap/config.go create mode 100644 pkg/kcap/header.go create mode 100644 pkg/kcap/reader.go create mode 100644 pkg/kcap/reader_test.go create mode 100644 pkg/kcap/reader_unsupported.go create mode 100644 pkg/kcap/section/section.go create mode 100644 pkg/kcap/section/section_test.go create mode 100644 pkg/kcap/types.go create mode 100644 pkg/kcap/version/version.go create mode 100644 pkg/kcap/writer.go create mode 100644 pkg/kcap/writer_test.go create mode 100644 pkg/kcap/writer_unsupported.go create mode 100644 pkg/kevent/README.md create mode 100644 pkg/kevent/batch.go create mode 100644 pkg/kevent/batch_test.go create mode 100644 pkg/kevent/doc.go create mode 100644 pkg/kevent/formatter.go create mode 100644 pkg/kevent/formatter_test.go create mode 100644 pkg/kevent/kevent.go create mode 100644 pkg/kevent/kevent_test.go create mode 100644 pkg/kevent/kparam.go create mode 100644 pkg/kevent/kparam_test.go create mode 100644 pkg/kevent/kparams/canonicalize.go create mode 100644 pkg/kevent/kparams/canonicalize_test.go create mode 100644 pkg/kevent/kparams/fields.go create mode 100644 pkg/kevent/kparams/types.go create mode 100644 pkg/kevent/kparams/types_test.go create mode 100644 pkg/kevent/ktypes/category.go create mode 100644 pkg/kevent/ktypes/ktypes.go create mode 100644 pkg/kevent/ktypes/ktypes_test.go create mode 100644 pkg/kevent/ktypes/metainfo.go create mode 100644 pkg/kevent/ktypes/metainfo_test.go create mode 100644 pkg/kevent/marshaller.go create mode 100644 pkg/kevent/marshaller_test.go create mode 100644 pkg/kevent/sequencer.go create mode 100644 pkg/kevent/sequencer_test.go create mode 100644 pkg/kstream/README.md create mode 100644 pkg/kstream/_fixtures/snapshots/create-process.gob create mode 100644 pkg/kstream/controller.go create mode 100644 pkg/kstream/controller_test.go create mode 100644 pkg/kstream/doc.go create mode 100644 pkg/kstream/interceptors/chain.go create mode 100644 pkg/kstream/interceptors/fs.go create mode 100644 pkg/kstream/interceptors/fs_test.go create mode 100644 pkg/kstream/interceptors/handle.go create mode 100644 pkg/kstream/interceptors/handle_test.go create mode 100644 pkg/kstream/interceptors/image.go create mode 100644 pkg/kstream/interceptors/image_test.go create mode 100644 pkg/kstream/interceptors/interceptor.go create mode 100644 pkg/kstream/interceptors/net.go create mode 100644 pkg/kstream/interceptors/net_test.go create mode 100644 pkg/kstream/interceptors/ps.go create mode 100644 pkg/kstream/interceptors/ps_test.go create mode 100644 pkg/kstream/interceptors/registry.go create mode 100644 pkg/kstream/interceptors/registry_test.go create mode 100644 pkg/kstream/kstream_rundownc.go create mode 100644 pkg/kstream/kstreamc.go create mode 100644 pkg/kstream/kstreamc_test.go create mode 100644 pkg/net/types.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/README.md create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/amqp/constants_generated.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/amqp/extended_constants.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/amqp/methods_generated.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/amqp/readers_writers.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/amqp/types.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/auth/auth.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/binding/binding.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/config/config.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/config/default.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/consumer/consumer.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/exchange/exchange.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/interfaces/interfaces.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/pool/pool.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/qos/qos.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/queue/queue.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/safequeue/safequeue.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/server/basicMethods.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/server/channel.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/server/channelMethods.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/server/confirmMethods.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/server/connection.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/server/connectionMethods.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/server/exchangeMethods.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/server/queueMethods.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/server/server.go create mode 100644 pkg/outputs/amqp/_fixtures/garagemq/server/vhost.go create mode 100644 pkg/outputs/amqp/amqp.go create mode 100644 pkg/outputs/amqp/amqp_test.go create mode 100644 pkg/outputs/amqp/client.go create mode 100644 pkg/outputs/amqp/config.go create mode 100644 pkg/outputs/client.go create mode 100644 pkg/outputs/config.go create mode 100644 pkg/outputs/console/config.go create mode 100644 pkg/outputs/console/console.go create mode 100644 pkg/outputs/elasticsearch/config.go create mode 100644 pkg/outputs/elasticsearch/elasticsearch.go create mode 100644 pkg/outputs/elasticsearch/elasticsearch_test.go create mode 100644 pkg/outputs/elasticsearch/index.go create mode 100644 pkg/outputs/elasticsearch/index_test.go create mode 100644 pkg/outputs/elasticsearch/template.go create mode 100644 pkg/outputs/null/config.go create mode 100644 pkg/outputs/null/null.go create mode 100644 pkg/outputs/outputs.go create mode 100644 pkg/pe/config.go create mode 100644 pkg/pe/doc.go create mode 100644 pkg/pe/entropy.go create mode 100644 pkg/pe/marshaller.go create mode 100644 pkg/pe/marshaller_test.go create mode 100644 pkg/pe/reader.go create mode 100644 pkg/pe/reader_test.go create mode 100644 pkg/pe/resource/types.go create mode 100644 pkg/pe/resources.go create mode 100644 pkg/pe/resources_test.go create mode 100644 pkg/pe/section.go create mode 100644 pkg/pe/section_test.go create mode 100644 pkg/pe/types.go create mode 100644 pkg/ps/doc.go create mode 100644 pkg/ps/peb.go create mode 100644 pkg/ps/peb_test.go create mode 100644 pkg/ps/snapshotter.go create mode 100644 pkg/ps/snapshotter_mock.go create mode 100644 pkg/ps/snapshotter_test.go create mode 100644 pkg/ps/types/marshaller.go create mode 100644 pkg/ps/types/marshaller_test.go create mode 100644 pkg/ps/types/types.go create mode 100644 pkg/syscall/doc.go create mode 100644 pkg/syscall/etw/etw.go create mode 100644 pkg/syscall/etw/types.go create mode 100644 pkg/syscall/file/file.go create mode 100644 pkg/syscall/file/types.go create mode 100644 pkg/syscall/handle/handle.go create mode 100644 pkg/syscall/object/alpc.go create mode 100644 pkg/syscall/object/event.go create mode 100644 pkg/syscall/object/mutant.go create mode 100644 pkg/syscall/object/object.go create mode 100644 pkg/syscall/object/types.go create mode 100644 pkg/syscall/process/process.go create mode 100644 pkg/syscall/process/types.go create mode 100644 pkg/syscall/registry/key.go create mode 100644 pkg/syscall/security/privileges.go create mode 100644 pkg/syscall/security/sid.go create mode 100644 pkg/syscall/sys/sys.go create mode 100644 pkg/syscall/tdh/tdh.go create mode 100644 pkg/syscall/tdh/types.go create mode 100644 pkg/syscall/thread/thread.go create mode 100644 pkg/syscall/utf16/string.go create mode 100644 pkg/syscall/ver/ver.go create mode 100644 pkg/syscall/winerrno/errors.go create mode 100644 pkg/util/bytes/bytes.go create mode 100644 pkg/util/fasttemplate/doc.go create mode 100644 pkg/util/fasttemplate/template.go create mode 100644 pkg/util/fasttemplate/unsafe.go create mode 100644 pkg/util/filetime/filetime.go create mode 100644 pkg/util/hostname/hostname.go create mode 100644 pkg/util/ip/ip.go create mode 100644 pkg/util/ip/ip_test.go create mode 100644 pkg/util/log/_fixtures/.gitkeep create mode 100644 pkg/util/log/_fixtures/fibratus.log create mode 100644 pkg/util/log/config.go create mode 100644 pkg/util/log/logger.go create mode 100644 pkg/util/log/logger_test.go create mode 100644 pkg/util/log/rotate/rotate.go create mode 100644 pkg/util/multierror/multierror.go create mode 100644 pkg/util/ports/iana_ports.go create mode 100644 pkg/util/rest/rest.go create mode 100644 pkg/util/rest/rest_test.go create mode 100644 pkg/util/spinner/spinner.go create mode 100644 pkg/util/term/fb.go create mode 100644 pkg/util/term/term.go create mode 100644 pkg/util/tls/tls.go create mode 100644 pkg/util/typesize/typesize.go create mode 100644 pkg/yara/_fixtures/rules/dll.yar create mode 100644 pkg/yara/_fixtures/rules/notepad.yar create mode 100644 pkg/yara/_fixtures/yara-test.dll create mode 100644 pkg/yara/config/config.go create mode 100644 pkg/yara/scanner.go create mode 100644 pkg/yara/scanner_test.go create mode 100644 pkg/yara/scanner_unsupported.go create mode 100644 pkg/yara/types.go delete mode 100644 requirements.txt delete mode 100644 schema.yml delete mode 100644 setup.py delete mode 100644 tests/__init__.py delete mode 100644 tests/fixtures/__init__.py delete mode 100644 tests/fixtures/fibratus.yml delete mode 100644 tests/fixtures/filaments/__init__.py delete mode 100644 tests/fixtures/filaments/test_filament_no_on_next_kevent.py delete mode 100644 tests/fixtures/filaments/test_filament_nodoc.py delete mode 100644 tests/fixtures/filaments/test_filament_wrong_on_next_kevent.py delete mode 100644 tests/fixtures/schema.yml delete mode 100644 tests/pytest.ini delete mode 100644 tests/unit/__init__.py delete mode 100644 tests/unit/apidefs/__init__.py delete mode 100644 tests/unit/apidefs/declarer.py delete mode 100644 tests/unit/binding/__init__.py delete mode 100644 tests/unit/binding/yar.py delete mode 100644 tests/unit/cli.py delete mode 100644 tests/unit/config.py delete mode 100644 tests/unit/context_switch.py delete mode 100644 tests/unit/controller.py delete mode 100644 tests/unit/dll.py delete mode 100644 tests/unit/filament.py delete mode 100644 tests/unit/fs.py delete mode 100644 tests/unit/handle.py delete mode 100644 tests/unit/image_meta.py delete mode 100644 tests/unit/kevent.py delete mode 100644 tests/unit/kevent_types.py delete mode 100644 tests/unit/output/__init__.py delete mode 100644 tests/unit/output/aggregator.py delete mode 100644 tests/unit/output/amqp.py delete mode 100644 tests/unit/output/elasticsearch.py delete mode 100644 tests/unit/output/fs.py delete mode 100644 tests/unit/output/smtp.py delete mode 100644 tests/unit/registry.py delete mode 100644 tests/unit/tcpip/__init__.py delete mode 100644 tests/unit/tcpip/tcpip.py delete mode 100644 tests/unit/term.py delete mode 100644 tests/unit/thread.py diff --git a/.gitignore b/.gitignore index e8b82ccc1..a43f99d63 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,8 @@ -/fibratus/__pycache__ -/.cache -.coverage -coverage.xml -/.idea -/build -/kstream/build -/kstream/*.pyd -/kstream/*.c -/kstream/*.cpp -/tests/.cache -/tests/__pycache__ -/tests/*/__pycache__ -/tests/*/.cache -/tests/htmlcov -/tests/.coverage -/tests/unit/.coverage -/tests/*/htmlcov -/tests/coverage.xml -/tests/*/coverage.xml -/kstreamc.pyd -/htmlcov -dist -fibratus.egg-info \ No newline at end of file +cmd/fibratus/fibratus.exe +cmd/fibratus/fibratus.syso + +build/package/release +build/package/*.exe + +.idea +filaments/__pycache__ diff --git a/.landscape.yml b/.landscape.yml deleted file mode 100644 index e9816b1b0..000000000 --- a/.landscape.yml +++ /dev/null @@ -1,4 +0,0 @@ -ignore-paths: - - filaments -python-targets: - - 3 \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 9a961e1f1..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,8 +0,0 @@ -recursive-include filaments * -recursive-include kstream *.pxd - -include fibratus.yml -include requirements.txt -include LICENSE.MD - -recursive-exclude * __pycache__ \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 4305133b7..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,28 +0,0 @@ -environment: - - MSVS_VERSION: 2015 - - matrix: - - PYTHON: "C:\\Python34-x64" - DISTUTILS_USE_SDK: "1" - -platform: - - x64 - -install: - - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x64 - - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - - "pip install -r requirements.txt" - -build_script: - - call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x64 - - "python setup.py build_ext install" - -test_script: - - py.test tests/" - -artifacts: - - path: dist\* - -on_success: - - "codecov" diff --git a/build/package/LICENSE.txt b/build/package/LICENSE.txt new file mode 100644 index 000000000..d9eb5356b --- /dev/null +++ b/build/package/LICENSE.txt @@ -0,0 +1,11 @@ +Copyright 2019-2020 by Nedim Sabic Sabic + +All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/build/package/fibratus.nsi b/build/package/fibratus.nsi new file mode 100644 index 000000000..6d95f15cb --- /dev/null +++ b/build/package/fibratus.nsi @@ -0,0 +1,163 @@ +!define APPNAME "Fibratus" +!define COMPANYNAME "Fibratus" +!define DESCRIPTION "Fibratus is a modern tool for exploration and tracing of the Windows kernel" + + +# These will be displayed by the "Click here for support information" link in "Add/Remove Programs" +!define HELPURL "https://www.fibratus.io" # "Support Information" link +!define UPDATEURL "https://www.fibratus.io" # "Product Updates" link +!define ABOUTURL "https://www.fibratus.io" # "Publisher" link + +RequestExecutionLevel admin ;Require admin rights on NT6+ (When UAC is turned on) + +InstallDir "$PROGRAMFILES64\${COMPANYNAME}" +!define UNINSTALLDIR "Software\Microsoft\Windows\CurrentVersion\Uninstall\${COMPANYNAME}" +BrandingText " " + +# This will be in the installer/uninstaller's title bar +Name "${APPNAME}" +OutFile "fibratus-${VERSION}-amd64.exe" + +!include "LogicLib.nsh" +!include "MUI2.nsh" ; Modern UI + +!define MUI_FINISHPAGE_NOAUTOCLOSE +!define MUI_UNFINISHPAGE_NOAUTOCLOSE + +!insertmacro MUI_PAGE_WELCOME +!insertmacro MUI_PAGE_LICENSE "LICENSE.txt" +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_PAGE_FINISH + +!insertmacro MUI_UNPAGE_WELCOME +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES +!insertmacro MUI_UNPAGE_FINISH + +; Set languages (first is default language) +;!insertmacro MUI_LANGUAGE "English" +!define MUI_LANGDLL_ALLLANGUAGES +;Languages + + !insertmacro MUI_LANGUAGE "English" + !insertmacro MUI_LANGUAGE "French" + !insertmacro MUI_LANGUAGE "TradChinese" + !insertmacro MUI_LANGUAGE "Spanish" + !insertmacro MUI_LANGUAGE "Hungarian" + !insertmacro MUI_LANGUAGE "Russian" + !insertmacro MUI_LANGUAGE "German" + !insertmacro MUI_LANGUAGE "Dutch" + !insertmacro MUI_LANGUAGE "SimpChinese" + !insertmacro MUI_LANGUAGE "Italian" + !insertmacro MUI_LANGUAGE "Danish" + !insertmacro MUI_LANGUAGE "Polish" + !insertmacro MUI_LANGUAGE "Czech" + !insertmacro MUI_LANGUAGE "Slovenian" + !insertmacro MUI_LANGUAGE "Slovak" + !insertmacro MUI_LANGUAGE "Swedish" + !insertmacro MUI_LANGUAGE "Norwegian" + !insertmacro MUI_LANGUAGE "PortugueseBR" + !insertmacro MUI_LANGUAGE "Ukrainian" + !insertmacro MUI_LANGUAGE "Turkish" + !insertmacro MUI_LANGUAGE "Catalan" + !insertmacro MUI_LANGUAGE "Arabic" + !insertmacro MUI_LANGUAGE "Lithuanian" + !insertmacro MUI_LANGUAGE "Finnish" + !insertmacro MUI_LANGUAGE "Greek" + !insertmacro MUI_LANGUAGE "Korean" + !insertmacro MUI_LANGUAGE "Hebrew" + !insertmacro MUI_LANGUAGE "Portuguese" + !insertmacro MUI_LANGUAGE "Farsi" + !insertmacro MUI_LANGUAGE "Bulgarian" + !insertmacro MUI_LANGUAGE "Indonesian" + !insertmacro MUI_LANGUAGE "Japanese" + !insertmacro MUI_LANGUAGE "Croatian" + !insertmacro MUI_LANGUAGE "Serbian" + !insertmacro MUI_LANGUAGE "Thai" + !insertmacro MUI_LANGUAGE "NorwegianNynorsk" + !insertmacro MUI_LANGUAGE "Belarusian" + !insertmacro MUI_LANGUAGE "Albanian" + !insertmacro MUI_LANGUAGE "Malay" + !insertmacro MUI_LANGUAGE "Galician" + !insertmacro MUI_LANGUAGE "Basque" + !insertmacro MUI_LANGUAGE "Luxembourgish" + !insertmacro MUI_LANGUAGE "Afrikaans" + !insertmacro MUI_LANGUAGE "Uzbek" + !insertmacro MUI_LANGUAGE "Macedonian" + !insertmacro MUI_LANGUAGE "Latvian" + !insertmacro MUI_LANGUAGE "Bosnian" + !insertmacro MUI_LANGUAGE "Mongolian" + !insertmacro MUI_LANGUAGE "Estonian" + +!insertmacro MUI_RESERVEFILE_LANGDLL + +Function .onInit + + !insertmacro MUI_LANGDLL_DISPLAY + +FunctionEnd + +Section "Install" + # Files for the install directory + SetOutPath $INSTDIR + + # Create directories + CreateDirectory $INSTDIR\Logs + + # Files added here should be removed by the uninstaller + File /r "release\Bin" + File /r "release\Config" + File /r /x .idea /x __pycache__ "release\Filaments" + File /r "release\Python" + + # Uninstaller - See function un.onInit and section "uninstall" for configuration + WriteUninstaller "$INSTDIR\uninstall.exe" + + # Registry information for add/remove programs + WriteRegStr HKLM "${UNINSTALLDIR}" "DisplayName" "${APPNAME} - ${DESCRIPTION}" + WriteRegStr HKLM "${UNINSTALLDIR}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINSTALLDIR}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + WriteRegStr HKLM "${UNINSTALLDIR}" "InstallLocation" "$\"$INSTDIR$\"" + WriteRegStr HKLM "${UNINSTALLDIR}" "Publisher" "${COMPANYNAME}" + WriteRegStr HKLM "${UNINSTALLDIR}" "HelpLink" "$\"${HELPURL}$\"" + WriteRegStr HKLM "${UNINSTALLDIR}" "URLUpdateInfo" "$\"${UPDATEURL}$\"" + WriteRegStr HKLM "${UNINSTALLDIR}" "URLInfoAbout" "$\"${ABOUTURL}$\"" + WriteRegStr HKLM "${UNINSTALLDIR}" "DisplayVersion" "${VERSION}" + + # There is no option for modifying or repairing the install + WriteRegDWORD HKLM "${UNINSTALLDIR}" "NoModify" 1 + WriteRegDWORD HKLM "${UNINSTALLDIR}" "NoRepair" 1 + + # Set the INSTALLSIZE constant (!defined at the top of this script) so Add/Remove Programs can accurately report the size + WriteRegDWORD HKLM "${UNINSTALLDIR}" "EstimatedSize" ${INSTALLSIZE} + + # Add executable to PATH + EnVar::SetHKCU + EnVar::AddValue "Path" "$INSTDIR\Bin\" + + +SectionEnd + +Section "Uninstall" + + # Remove uninstalled executable from PATH + EnVar::SetHKCU + EnVar::DeleteValue "Path" "$INSTDIR\Bin\" + + # Remove files/directories + RMDir /r /REBOOTOK $INSTDIR\Bin + RMDir /r /REBOOTOK $INSTDIR\Logs + RMDir /r /REBOOTOK $INSTDIR\Config + RMDir /r /REBOOTOK $INSTDIR\Filaments + RMDir /r /REBOOTOK $INSTDIR\Python + + # Always delete uninstaller as the last action + Delete /REBOOTOK $INSTDIR\uninstall.exe + + # Try to remove the install directory - this will only happen if it is empty + RmDir /REBOOTOK $INSTDIR + + # Remove uninstaller information from the registry + DeleteRegKey HKLM "${UNINSTALLDIR}" + +SectionEnd diff --git a/cmd/fibratus/app/capture.go b/cmd/fibratus/app/capture.go new file mode 100644 index 000000000..15ab22cf9 --- /dev/null +++ b/cmd/fibratus/app/capture.go @@ -0,0 +1,143 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app + +import ( + "github.com/rabbitstack/fibratus/pkg/api" + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/filter" + "github.com/rabbitstack/fibratus/pkg/handle" + "github.com/rabbitstack/fibratus/pkg/kcap" + "github.com/rabbitstack/fibratus/pkg/kstream" + "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/syscall/security" + logger "github.com/rabbitstack/fibratus/pkg/util/log" + "github.com/rabbitstack/fibratus/pkg/util/spinner" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "os" + "os/signal" + "time" +) + +var captureCmd = &cobra.Command{ + Use: "capture [filter]", + Short: "Capture kernel event stream to the kcap file", + RunE: capture, +} + +var captureConfig = config.NewWithOpts(config.WithCapture()) + +func init() { + captureConfig.MustViperize(captureCmd) +} + +func capture(cmd *cobra.Command, args []string) error { + if err := captureConfig.TryLoadFile(captureConfig.File()); err != nil { + return err + } + if err := captureConfig.Init(); err != nil { + return err + } + if err := captureConfig.Validate(); err != nil { + return err + } + if captureConfig.DebugPrivilege { + security.SetDebugPrivilege() + } + if err := logger.InitFromConfig(captureConfig.Log); err != nil { + return err + } + + spin := spinner.Show("Snapshotting processes and handles") + // make sure to not wait more than a minute if system handle enumeration + // got stuck or taking too much time to complete. + wait := make(chan struct{}, 1) + deadline := time.AfterFunc(time.Minute, func() { + wait <- struct{}{} + }) + cb := func(total uint64, withName uint64) { + deadline.Stop() + spin.Stop() + wait <- struct{}{} + } + + // the capture will start after all system handles have been enumerated. This gives us a + // chance to build the handle state before writing the event flow + hsnap := handle.NewSnapshotter(captureConfig, cb) + psnap := ps.NewSnapshotter(hsnap, captureConfig) + + // we'll start writing to the kcap file once we receive on the wait channel + <-wait + + // initiate the kernel trace and start consuming from the event stream + ktracec := kstream.NewKtraceController(captureConfig.Kstream) + err := ktracec.StartKtrace() + if err != nil { + return err + } + defer ktracec.CloseKtrace() + + kstreamc := kstream.NewConsumer(ktracec, psnap, hsnap, captureConfig) + kfilter, err := filter.NewFromCLI(args) + if err != nil { + return err + } + if kfilter != nil { + kstreamc.SetFilter(kfilter) + } + err = kstreamc.OpenKstream() + if err != nil { + return err + } + defer kstreamc.CloseKstream() + + // bootstrap kcap writer with inbound event channel + writer, err := kcap.NewWriter(captureConfig.KcapFile, psnap, hsnap) + if err != nil { + return err + } + errsc := writer.Write(kstreamc.Events(), kstreamc.Errors()) + go func() { + for err := range errsc { + log.Warnf("fail to write event to kcap: %v", err) + } + }() + + // start rendering the spinner + spin = spinner.Show("Capturing") + + // start the HTTP server + if err := api.StartServer(captureConfig); err != nil { + return err + } + + signal.Notify(sig, os.Kill, os.Interrupt) + <-sig + spin.Stop() + + if err := writer.Close(); err != nil { + return err + } + if err := api.CloseServer(); err != nil { + return err + } + + return nil +} diff --git a/cmd/fibratus/app/config.go b/cmd/fibratus/app/config.go new file mode 100644 index 000000000..1217ada8f --- /dev/null +++ b/cmd/fibratus/app/config.go @@ -0,0 +1,60 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/util/rest" + "github.com/spf13/cobra" + "os" +) + +var configCmd = &cobra.Command{ + Use: "config", + Short: "Show runtime config", + RunE: printConfig, +} + +var c = config.NewWithOpts(config.WithStats()) + +func init() { + c.MustViperize(configCmd) +} + +func printConfig(cmd *cobra.Command, args []string) error { + if err := c.TryLoadFile(c.File()); err != nil { + return err + } + if err := c.Init(); err != nil { + return err + } + if err := c.Validate(); err != nil { + return err + } + body, err := rest.Get(rest.WithTransport(c.API.Transport), rest.WithURI("config")) + if err != nil { + return err + } + _, err = fmt.Fprintln(os.Stdout, string(body)) + if err != nil { + return err + } + return nil +} diff --git a/cmd/fibratus/app/control_service.go b/cmd/fibratus/app/control_service.go new file mode 100644 index 000000000..c66904628 --- /dev/null +++ b/cmd/fibratus/app/control_service.go @@ -0,0 +1,265 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/aggregator" + "github.com/rabbitstack/fibratus/pkg/api" + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/handle" + "github.com/rabbitstack/fibratus/pkg/kstream" + "github.com/rabbitstack/fibratus/pkg/outputs" + "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/syscall/security" + logger "github.com/rabbitstack/fibratus/pkg/util/log" + "github.com/spf13/cobra" + "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/debug" + "golang.org/x/sys/windows/svc/eventlog" + "golang.org/x/sys/windows/svc/mgr" + "time" +) + +var startSvcCmd = &cobra.Command{ + Use: "start-service", + RunE: startService, + Short: "Start fibratus service", +} + +var stopSvcCmd = &cobra.Command{ + Use: "stop-service", + RunE: stopService, + Short: "Stop fibratus service", +} + +var restartSvcCmd = &cobra.Command{ + Use: "restart-service", + RunE: restartService, + Short: "Restart fibratus service", +} + +var svcConfig = config.NewWithOpts(config.WithRun()) + +func init() { + svcConfig.MustViperize(startSvcCmd) +} + +func startService(cmd *cobra.Command, args []string) error { + h, err := windows.OpenSCManager(nil, nil, windows.SC_MANAGER_CONNECT) + if err != nil { + return fmt.Errorf("couldn't connect to Windows Service Manager: %v", err) + } + m := &mgr.Mgr{Handle: h} + defer m.Disconnect() + s, err := windows.OpenService( + m.Handle, + windows.StringToUTF16Ptr(svcName), + windows.SERVICE_START|windows.SERVICE_STOP, + ) + if err != nil { + return fmt.Errorf("could not open fibratus service: %v", err) + } + scm := &mgr.Service{Name: svcName, Handle: s} + defer scm.Close() + err = scm.Start() + if err != nil { + return fmt.Errorf("could not start fibratus service: %v", err) + } + + start := time.Now() + var status svc.Status + for time.Since(start) > 5*time.Second { + status, err = scm.Query() + if err != nil { + return fmt.Errorf("failed to get fibratus service status: %v", err) + } + + if status.State == svc.Running { + return nil + } + } + return nil +} + +func stopService(cmd *cobra.Command, args []string) error { + return stopSvc() +} + +func restartService(cmd *cobra.Command, args []string) error { + if err := stopSvc(); err != nil { + return err + } + return startService(cmd, args) +} + +func stopSvc() error { + h, err := windows.OpenSCManager(nil, nil, windows.SC_MANAGER_CONNECT) + if err != nil { + return fmt.Errorf("couldn't connect to Windows Service Manager: %v", err) + } + m := &mgr.Mgr{Handle: h} + defer m.Disconnect() + + s, err := windows.OpenService( + m.Handle, + windows.StringToUTF16Ptr(svcName), + windows.SERVICE_START|windows.SERVICE_STOP|windows.SERVICE_QUERY_STATUS, + ) + if err != nil { + return fmt.Errorf("could not open fibratus service: %v", err) + } + scm := &mgr.Service{Name: svcName, Handle: s} + defer scm.Close() + + status, err := scm.Control(svc.Stop) + if err != nil { + return fmt.Errorf("couldn't stop fibratus service: %v", err) + } + timeout := time.Now().Add(10 * time.Second) + for status.State != svc.Stopped { + if timeout.Before(time.Now()) { + return fmt.Errorf("timeout waiting for service to go to state=%d", svc.Stopped) + } + time.Sleep(300 * time.Millisecond) + status, err = scm.Query() + if err != nil { + return fmt.Errorf("could not retrieve service status: %v", err) + } + } + return nil +} + +type fsvc struct{} + +var evtlog debug.Log + +var sktracec kstream.KtraceController +var skstreamc kstream.Consumer +var sagg *aggregator.BufferedAggregator + +func (s *fsvc) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) { + const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown + changes <- svc.Status{State: svc.StartPending} + changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted} + + if err := s.run(); err != nil { + evtlog.Error(0xc000000B, err.Error()) + changes <- svc.Status{State: svc.Stopped} + return false, 1 + } + +loop: + for { + select { + case c := <-r: + switch c.Cmd { + case svc.Interrogate: + changes <- c.CurrentStatus + time.Sleep(100 * time.Millisecond) + changes <- c.CurrentStatus + case svc.Stop: + break loop + case svc.Shutdown: + break loop + } + } + } + + changes <- svc.Status{State: svc.StopPending} + if sktracec != nil { + sktracec.CloseKtrace() + } + if skstreamc != nil { + skstreamc.CloseKstream() + } + if sagg != nil { + sagg.Stop() + } + handle.CloseTimeout() + api.CloseServer() + changes <- svc.Status{State: svc.Stopped} + + return true, 0 +} + +func (s *fsvc) run() error { + if err := svcConfig.TryLoadFile(svcConfig.GetConfigFile()); err != nil { + return err + } + if err := svcConfig.Init(); err != nil { + return err + } + if err := svcConfig.Validate(); err != nil { + return err + } + // ask for debug privileges + if svcConfig.DebugPrivilege { + security.SetDebugPrivilege() + } + if err := logger.InitFromConfig(svcConfig.Log); err != nil { + return err + } + sktracec = kstream.NewKtraceController(svcConfig.Kstream) + err := sktracec.StartKtrace() + if err != nil { + return err + } + // initialize handle/process snapshotters and try to open the kernel event stream + hsnap := handle.NewSnapshotter(svcConfig, nil) + psnap := ps.NewSnapshotter(hsnap, svcConfig) + skstreamc = kstream.NewConsumer(sktracec, psnap, hsnap, svcConfig) + // open the kernel event stream, start processing events and forwarding to outputs + err = skstreamc.OpenKstream() + if err != nil { + return err + } + sagg, err = aggregator.NewBuffered( + skstreamc.Events(), + skstreamc.Errors(), + svcConfig.Aggregator, + outputs.Config{Type: svcConfig.Output.Type, Output: svcConfig.Output.Output}, + svcConfig.Transformers, + svcConfig.Alertsenders, + ) + if err != nil { + return err + } + if err := api.StartServer(svcConfig); err != nil { + return err + } + return nil +} + +// RunService runs the service handler. +func RunService() { + var err error + evtlog, err = eventlog.Open(svcName) + if err != nil { + return + } + defer evtlog.Close() + + err = svc.Run(svcName, &fsvc{}) + if err != nil { + evtlog.Error(0xc0000008, err.Error()) + return + } +} diff --git a/cmd/fibratus/app/docs.go b/cmd/fibratus/app/docs.go new file mode 100644 index 000000000..db95375ce --- /dev/null +++ b/cmd/fibratus/app/docs.go @@ -0,0 +1,32 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app + +import ( + "github.com/spf13/cobra" + "os/exec" +) + +var docsCmd = &cobra.Command{ + Use: "docs", + Short: "Open Fibratus docs in the web browser", + RunE: func(cmd *cobra.Command, args []string) error { + return exec.Command("rundll32", "url.dll,FileProtocolHandler", "https://www.fibratus.io").Start() + }, +} diff --git a/cmd/fibratus/app/install_service.go b/cmd/fibratus/app/install_service.go new file mode 100644 index 000000000..c42236aea --- /dev/null +++ b/cmd/fibratus/app/install_service.go @@ -0,0 +1,72 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app + +import ( + "errors" + "fmt" + "github.com/spf13/cobra" + "golang.org/x/sys/windows/svc/eventlog" + "golang.org/x/sys/windows/svc/mgr" + "os" +) + +const svcName = "fibratus" + +var errServiceAlreadyInstalled = errors.New("fibratus service is already installed") + +var installSvcCmd = &cobra.Command{ + Use: "install-service", + Short: "Install fibratus within the Windows service control manager", + RunE: installService, +} + +func installService(cmd *cobra.Command, args []string) error { + exe, err := os.Executable() + if err != nil { + return err + } + m, err := mgr.Connect() + if err != nil { + return err + } + defer m.Disconnect() + s, err := m.OpenService(svcName) + if err == nil { + s.Close() + return errServiceAlreadyInstalled + } + svccfg := mgr.Config{ + DisplayName: "Fibratus Service", + Description: "Exploration and tracing of the Windows kernel", + } + s, err = m.CreateService(svcName, exe, svccfg) + if err != nil { + return err + } + defer s.Close() + err = eventlog.InstallAsEventCreate(svcName, eventlog.Error|eventlog.Warning|eventlog.Info) + if err != nil { + if err := s.Delete(); err != nil { + return err + } + return fmt.Errorf("couldn't create event log record: %v", err) + } + return nil +} diff --git a/cmd/fibratus/app/list.go b/cmd/fibratus/app/list.go new file mode 100644 index 000000000..6a7a3653d --- /dev/null +++ b/cmd/fibratus/app/list.go @@ -0,0 +1,154 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app + +import ( + "bufio" + "fmt" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/filter/fields" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/spf13/cobra" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +var listCmd = &cobra.Command{ + Use: "list", + Short: "Show info about filaments, filter fields or kernel event types", +} + +var listFilamentsCmd = &cobra.Command{ + Use: "filaments", + Short: "List available filaments", + RunE: listFilaments, +} + +var listFieldsCmd = &cobra.Command{ + Use: "fields", + Short: "List available filtering fields", + Run: listFields, +} + +var listsKeventsCmd = &cobra.Command{ + Use: "kevents", + Short: "List supported kernel event types", + Run: listKevents, +} + +var listConfig = config.NewWithOpts(config.WithList()) + +func init() { + listConfig.MustViperize(listFilamentsCmd) + + listCmd.AddCommand(listFilamentsCmd) + listCmd.AddCommand(listFieldsCmd) + listCmd.AddCommand(listsKeventsCmd) + + RootCmd.AddCommand(listCmd) +} + +// listFilaments renders a table with all available filaments. +func listFilaments(cmd *cobra.Command, args []string) error { + if err := listConfig.Init(); err != nil { + return err + } + + dir := listConfig.Filament.Path + if _, err := os.Stat(dir); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("%q directory does not exist", dir) + } + return err + } + + filaments, err := ioutil.ReadDir(dir) + if err != nil { + return err + } + + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{"Name", "Description"}) + t.SetStyle(table.StyleLight) + + for _, f := range filaments { + if f.IsDir() { + continue + } + py, err := os.Open(filepath.Join(dir, f.Name())) + if err != nil { + continue + } + if filepath.Ext(f.Name()) != ".py" { + continue + } + + sn := bufio.NewScanner(py) + var docStart bool + var doc string + for sn.Scan() { + ln := sn.Text() + if docStart { + doc = ln + break + } + if ln == `"""` { + docStart = true + } + + } + _ = py.Close() + t.AppendRow(table.Row{strings.TrimSuffix(f.Name(), ".py"), doc}) + } + t.Render() + + return nil +} + +// listKevents renders a table with supported kernel event types showing the category to which their pertain and a short description. +func listKevents(cmd *cobra.Command, args []string) { + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{"Name", "Category", "Description"}) + t.SetStyle(table.StyleLight) + + for _, ktyp := range ktypes.GetKtypesMeta() { + t.AppendRow(table.Row{ktyp.Name, ktyp.Category, ktyp.Description}) + } + + t.Render() +} + +// listFields renders a table with available filtering fields containing the name, description and the example filtering expression. +func listFields(cmd *cobra.Command, args []string) { + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{"Name", "Description", "Example"}) + t.SetStyle(table.StyleLight) + + for _, field := range fields.Get() { + t.AppendRow(table.Row{field.Field, field.Desc, strings.Join(field.Examples, ",")}) + } + + t.Render() +} diff --git a/cmd/fibratus/app/remove_service.go b/cmd/fibratus/app/remove_service.go new file mode 100644 index 000000000..db6d6cf02 --- /dev/null +++ b/cmd/fibratus/app/remove_service.go @@ -0,0 +1,57 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app + +import ( + "errors" + "fmt" + "github.com/spf13/cobra" + "golang.org/x/sys/windows/svc/eventlog" + "golang.org/x/sys/windows/svc/mgr" +) + +var removeSvcCmd = &cobra.Command{ + Use: "remove-service", + Short: "Remove fibratus from the Windows service control manager", + RunE: removeService, +} + +var errServiceNotInstalled = errors.New("fibratus service is not installed") + +func removeService(cmd *cobra.Command, args []string) error { + m, err := mgr.Connect() + if err != nil { + return err + } + defer m.Disconnect() + s, err := m.OpenService(svcName) + if err != nil { + return errServiceNotInstalled + } + defer s.Close() + err = s.Delete() + if err != nil { + return err + } + err = eventlog.Remove(svcName) + if err != nil { + return fmt.Errorf("couldn't create eventlog remove record: %v", err) + } + return nil +} diff --git a/cmd/fibratus/app/replay.go b/cmd/fibratus/app/replay.go new file mode 100644 index 000000000..f35e201e2 --- /dev/null +++ b/cmd/fibratus/app/replay.go @@ -0,0 +1,142 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app + +import ( + "context" + "github.com/rabbitstack/fibratus/pkg/aggregator" + "github.com/rabbitstack/fibratus/pkg/api" + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/filament" + "github.com/rabbitstack/fibratus/pkg/filter" + "github.com/rabbitstack/fibratus/pkg/kcap" + "github.com/rabbitstack/fibratus/pkg/outputs" + logger "github.com/rabbitstack/fibratus/pkg/util/log" + "github.com/spf13/cobra" + "os" + "os/signal" +) + +var replayCmd = &cobra.Command{ + Use: "replay", + Short: "Replay kernel event flow from the kcap file", + RunE: replay, +} + +var replayConfig = config.NewWithOpts(config.WithReplay()) + +func init() { + replayConfig.MustViperize(replayCmd) +} + +func replay(cmd *cobra.Command, args []string) error { + if err := replayConfig.TryLoadFile(replayConfig.File()); err != nil { + return err + } + if err := replayConfig.Init(); err != nil { + return err + } + if err := replayConfig.Validate(); err != nil { + return err + } + if err := logger.InitFromConfig(replayConfig.Log); err != nil { + return err + } + kfilter, err := filter.NewFromCLI(args) + if err != nil { + return err + } + // initialize kcap reader and try to recover the snapshotters + // from the captured state + reader, err := kcap.NewReader(replayConfig.KcapFile, replayConfig) + if err != nil { + return err + } + hsnap, psnap, err := reader.RecoverSnapshotters() + if err != nil { + return err + } + + ctx, cancel := context.WithCancel(context.Background()) + + filamentConfig := replayConfig.Filament + filamentName := filamentConfig.Name + // we don't need the aggregator is user decided to replay the + // kcap on the filament. Otwherise, we setup the full-fledged + // buffered aggregator + var agg *aggregator.BufferedAggregator + + if filamentName != "" { + f, err := filament.New(filamentName, psnap, hsnap, filamentConfig) + if err != nil { + return err + } + if f.Filter() != nil { + kfilter = f.Filter() + } + reader.SetFilter(kfilter) + + // returns the channel where events are read from the kcap + kevents, errs := reader.Read(ctx) + + go func() { + defer f.Close() + err = f.Run(kevents, errs) + if err != nil { + sig <- os.Interrupt + } + }() + } else { + if kfilter != nil { + reader.SetFilter(kfilter) + } + + // use the channels where events are read from the kcap as aggregator source + kevents, errs := reader.Read(ctx) + + var err error + agg, err = aggregator.NewBuffered( + kevents, + errs, + replayConfig.Aggregator, + outputs.Config{Type: replayConfig.Output.Type, Output: replayConfig.Output.Output}, + replayConfig.Transformers, + replayConfig.Alertsenders, + ) + if err != nil { + return err + } + } + // start the HTTP server + if err := api.StartServer(replayConfig); err != nil { + return err + } + signal.Notify(sig, os.Kill, os.Interrupt) + <-sig + // stop reader consumer goroutines + cancel() + + if agg != nil { + if err := agg.Stop(); err != nil { + return err + } + } + + return nil +} diff --git a/cmd/fibratus/app/root.go b/cmd/fibratus/app/root.go new file mode 100644 index 000000000..821f7f48e --- /dev/null +++ b/cmd/fibratus/app/root.go @@ -0,0 +1,65 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app + +import ( + "errors" + "github.com/spf13/cobra" + "os" + "runtime" +) + +var sig = make(chan os.Signal, 2) + +var RootCmd = &cobra.Command{ + Use: "fibratus", + Short: "Modern tool for the kernel observability and exploration", + Long: ` + Fibratus is a tool for exploration and tracing of the Windows kernel. + It lets you trap system-wide events such as process life-cycle, file system I/O, + registry modifications or network requests among many other observability signals. + In a nutshell, Fibratus allows for gaining deep operational visibility into the Windows + kernel but also processes running on top of it. + `, + SilenceUsage: true, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if runtime.GOOS != "windows" { + return errors.New("fibratus can only be run on Windows operating systems") + } + if runtime.GOARCH == "386" { + return errors.New("fibratus can't be run on 32-bits Windows operating systems") + } + return nil + }, +} + +func init() { + RootCmd.AddCommand(runCmd) + RootCmd.AddCommand(captureCmd) + RootCmd.AddCommand(replayCmd) + RootCmd.AddCommand(installSvcCmd) + RootCmd.AddCommand(removeSvcCmd) + RootCmd.AddCommand(startSvcCmd) + RootCmd.AddCommand(stopSvcCmd) + RootCmd.AddCommand(restartSvcCmd) + RootCmd.AddCommand(statsCmd) + RootCmd.AddCommand(configCmd) + RootCmd.AddCommand(docsCmd) + RootCmd.AddCommand(versionCmd) +} diff --git a/cmd/fibratus/app/run.go b/cmd/fibratus/app/run.go new file mode 100644 index 000000000..6a1069aa8 --- /dev/null +++ b/cmd/fibratus/app/run.go @@ -0,0 +1,187 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app + +import ( + "github.com/rabbitstack/fibratus/pkg/aggregator" + "github.com/rabbitstack/fibratus/pkg/alertsender" + "github.com/rabbitstack/fibratus/pkg/api" + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/filament" + "github.com/rabbitstack/fibratus/pkg/filter" + "github.com/rabbitstack/fibratus/pkg/handle" + "github.com/rabbitstack/fibratus/pkg/kstream" + "github.com/rabbitstack/fibratus/pkg/outputs" + "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/syscall/security" + logger "github.com/rabbitstack/fibratus/pkg/util/log" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "os" + "os/signal" +) + +var runCmd = &cobra.Command{ + Use: "run [filter]", + Short: "Bootstrap fibratus or a filament", + Aliases: []string{"start"}, + RunE: run, + Example: ` + # Run without the filter + fibratus run + + # Run with the filter that drops all but events produced by the svchost.exe process + fibratus run ps.name = 'svchost.exe' + + # Run with the filter that traps all events that were generated by process that contains the 'svc' string and it was started by 'SYSTEM' or 'admin' users + fibratus run ps.name contains 'svc' and ps.sid in ('NT AUTHORITY\\SYSTEM', 'ARCHRABBIT\\admin') + + # Run the top_keys filament + fibratus run -f top_keys + `, +} + +var cfg = config.NewWithOpts(config.WithRun()) + +func init() { + cfg.MustViperize(runCmd) +} + +func run(cmd *cobra.Command, args []string) error { + // even though it is possible to bootstrap with default config, we'll + // return an error if for some reason the config can't be loaded from the file + if err := cfg.TryLoadFile(cfg.File()); err != nil { + return err + } + // initialize and validate the config + if err := cfg.Init(); err != nil { + return err + } + if err := cfg.Validate(); err != nil { + return err + } + // inject the debug privilege if enabled + if cfg.DebugPrivilege { + security.SetDebugPrivilege() + } + if err := logger.InitFromConfig(cfg.Log); err != nil { + return err + } + // initialize kernel trace controller and try to start the trace + ktracec := kstream.NewKtraceController(cfg.Kstream) + err := ktracec.StartKtrace() + if err != nil { + return err + } + defer ktracec.CloseKtrace() + // bootstrap essential components, including handle, process snapshotters + // and the kernel stream consumer that will actually collect all the events + hsnap := handle.NewSnapshotter(cfg, nil) + psnap := ps.NewSnapshotter(hsnap, cfg) + kstreamc := kstream.NewConsumer(ktracec, psnap, hsnap, cfg) + // build the filter from the CLI argument. If we got a valid expression the filter + // is linked to the kernel stream consumer so it can drop any events that don't match + // the filter criteria + kfilter, err := filter.NewFromCLI(args) + if err != nil { + return err + } + if kfilter != nil { + kstreamc.SetFilter(kfilter) + } + log.Infof("bootstrapping with pid %d", os.Getpid()) + // user can either instruct to bootstrap a filament or start a regular run. We'll setup + // the corresponding components accordingly to what we got from the CLI options. If a filament + // was given, we'll assign it the previous filter if it wasn't provided in the filament init function. + // Finally, we open the kernel stream flow and run the filament i.e. Python main thread in a new goroutine. + // In case of a regular run, we additionally setup the aggregator. The aggregator will grab the events + // from the queue, assemble them into batches and hand over to output sinks. + var f filament.Filament + filamentName := cfg.Filament.Name + if filamentName != "" { + f, err = filament.New(filamentName, psnap, hsnap, cfg.Filament) + if err != nil { + return err + } + if f.Filter() != nil { + kstreamc.SetFilter(f.Filter()) + } + err = kstreamc.OpenKstream() + if err != nil { + return err + } + defer kstreamc.CloseKstream() + // load alert senders so emitting alerts is possible from filaments + err = alertsender.LoadAll(cfg.Alertsenders) + if err != nil { + log.Warnf("couldn't load alertsenders: %v", err) + } + go func() { + err = f.Run(kstreamc.Events(), kstreamc.Errors()) + if err != nil { + log.Error(err) + sig <- os.Interrupt + } + }() + } else { + err = kstreamc.OpenKstream() + if err != nil { + return err + } + defer kstreamc.CloseKstream() + // setup the aggregator that forwards events to outputs + agg, err := aggregator.NewBuffered( + kstreamc.Events(), + kstreamc.Errors(), + cfg.Aggregator, + outputs.Config{Type: cfg.Output.Type, Output: cfg.Output.Output}, + cfg.Transformers, + cfg.Alertsenders, + ) + if err != nil { + return err + } + defer func() { + if err := agg.Stop(); err != nil { + log.Error(err) + } + }() + } + // start the HTTP server + if err := api.StartServer(cfg); err != nil { + return err + } + // wait for signals + signal.Notify(sig, os.Interrupt, os.Kill) + <-sig + log.Infof("shutting down...") + // shutdown everything gracefully + if f != nil { + if err := f.Close(); err != nil { + return err + } + } + if err := handle.CloseTimeout(); err != nil { + return err + } + if err := api.CloseServer(); err != nil { + return err + } + return nil +} diff --git a/cmd/fibratus/app/stats.go b/cmd/fibratus/app/stats.go new file mode 100644 index 000000000..35c2e67ff --- /dev/null +++ b/cmd/fibratus/app/stats.go @@ -0,0 +1,162 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app + +import ( + "encoding/json" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/util/rest" + "github.com/spf13/cobra" + "os" + "reflect" +) + +var statsCmd = &cobra.Command{ + Use: "stats", + Short: "Show runtime stats", + RunE: stats, +} + +var statsConfig = config.NewWithOpts(config.WithStats()) + +func init() { + statsConfig.MustViperize(statsCmd) +} + +// Stats stores runtime statistics that are retrieved from the expvar endpoint. +type Stats struct { + AggregatorBatchEvents int `json:"aggregator.batch.events"` + AggregatorFlushesCount int `json:"aggregator.flushes.count"` + AggregatorKeventErrors int `json:"aggregator.kevent.errors"` + AggregatorTransformerErrors map[string]int `json:"aggregator.transformer.errors"` + AggregatorWorkerClientPublishErrors int `json:"aggregator.worker.client.publish.errors"` + FilamentKdictErrors int `json:"filament.kdict.errors"` + FilamentKeventBatchFlushes int `json:"filament.kevent.batch.flushes"` + FilamentKeventErrors map[string]int `json:"filament.kevent.errors"` + FilamentKeventProcessErrors int `json:"filament.kevent.process.errors"` + FilterAccessorErrors map[string]int `json:"filter.accessor.errors"` + FsFileObjectHandleHits int `json:"fs.file.object.handle.hits"` + FsFileObjectMisses int `json:"fs.file.object.misses"` + FsFileReleases int `json:"fs.file.releases"` + FsTotalRundownFiles int `json:"fs.total.rundown.files"` + HandleDeferredEvictions int `json:"handle.deferred.evictions"` + HandleNameQueryFailures map[string]int `json:"handle.name.query.failures"` + HandleSnapshotCount int `json:"handle.snapshot.count"` + HandleSnapshotBytes int `json:"handle.snapshot.bytes"` + HandleTypesCount int `json:"handle.types.count"` + HandleTypeNameMisses int `json:"handle.type.name.misses"` + HandleWaitTimeouts int `json:"handle.wait.timeouts"` + HostnameErrors map[string]int `json:"hostname.errors"` + KcapDroppedKevents int `json:"kcap.dropped.kevents"` + KcapFlusherErrors map[string]int `json:"kcap.flusher.errors"` + KcapHandleWriteErrors int `json:"kcap.handle.write.errors"` + KcapKeventUnmarshalErrors int `json:"kcap.kevent.unmarshal.errors"` + KcapKeventWriteErrors int `json:"kcap.kevent.write.errors"` + KcapKstreamConsumerErrors int `json:"kcap.kstream.consumer.errors"` + KcapOverflowErrors int `json:"kcap.overflow.errors"` + KcapReadBytes int `json:"kcap.read.bytes"` + KcapReadKevents int `json:"kcap.read.kevents"` + KcapReaderDroppedByFilter int `json:"kcap.reader.dropped.by.filter"` + KcapReaderHandleUnmarshalErrors int `json:"kcap.reader.handle.unmarshal.errors"` + KeventInterceptorFailures int `json:"kevent.interceptor.failures"` + KeventSeqInitErrors map[string]int `json:"kevent.seq.init.errors"` + KeventSeqStoreErrors int `json:"kevent.seq.store.errors"` + KeventTimestampUnmarshalErrors int `json:"kevent.timestamp.unmarshal.errors"` + KstreamBlacklistDroppedKevents map[string]int `json:"kstream.blacklist.dropped.kevents"` + KstreamBlacklistDroppedProcs map[string]int `json:"kstream.blacklist.dropped.procs"` + KstreamKbuffersRead int `json:"kstream.kbuffers.read"` + KstreamKeventParamFailures int `json:"kstream.kevent.param.failures"` + KstreamKeventsEnqueued int `json:"kstream.kevents.enqueued"` + KstreamKeventsDequeued int `json:"kstream.kevents.dequeued"` + KstreamKeventsFailures map[string]int `json:"kstream.kevents.failures"` + KstreamKeventsMissingSchemaErrors map[string]int `json:"kstream.kevents.missing.schema.errors"` + KstreamUpstreamCancellations int `json:"kstream.upstream.cancellations"` + LoggerErrors map[string]int `json:"logger.errors"` + OutputAmqpChannelFailures int `json:"output.amqp.channel.failures"` + OutputAmqpConnectionFailures int `json:"output.amqp.connection.failures"` + OutputAmqpPublishErrors int `json:"output.amqp.publish.errors"` + OutputConsoleErrors int `json:"output.console.errors"` + OutputNullBlackholeEvents int `json:"output.null.blackhole.events"` + PeFailedResourceEntryReads int `json:"pe.failed.resource.entry.reads"` + PeMaxResourceEntriesExceeded int `json:"pe.max.resource.entries.exceeded"` + ProcessCount int `json:"process.count"` + ProcessModuleCount int `json:"process.module.count"` + ProcessLookupFailureCount map[int]int `json:"process.lookup.failure.count"` + ProcessPebReadErrors int `json:"process.peb.read.errors"` + ProcessReaped int `json:"process.reaped"` + ProcessThreadCount int `json:"process.thread.count"` + RegistryKcbCount int `json:"registry.kcb.count"` + RegistryKcbMisses int `json:"registry.kcb.misses"` + RegistryKeyHandleHits int `json:"registry.key.handle.hits"` + RegistryUnknownKeysCount int `json:"registry.unknown.keys.count"` + SidsCount int `json:"sids.count"` + YaraImageScans int `json:"yara.image.scans"` + YaraProcScans int `json:"yara.proc.scans"` + YaraRuleMatches int `json:"yara.rule.matches"` +} + +func stats(cmd *cobra.Command, args []string) error { + if err := statsConfig.TryLoadFile(statsConfig.File()); err != nil { + return err + } + if err := statsConfig.Init(); err != nil { + return err + } + if err := statsConfig.Validate(); err != nil { + return err + } + + c := statsConfig.API + body, err := rest.Get(rest.WithTransport(c.Transport), rest.WithURI("debug/vars")) + if err != nil { + return err + } + var stats Stats + if err := json.Unmarshal(body, &stats); err != nil { + return err + } + + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{"Name", "Value"}) + t.SetStyle(table.StyleLight) + + typ := reflect.TypeOf(stats) + val := reflect.ValueOf(stats) + + for i := 0; i < typ.NumField(); i++ { + f := typ.Field(i) + tag := f.Tag.Get("json") + + if tag == "" { + continue + } + + if !val.Field(i).CanInterface() { + continue + } + + t.AppendRow(table.Row{tag, val.Field(i).Interface()}) + } + + t.Render() + + return nil +} diff --git a/cmd/fibratus/app/version.go b/cmd/fibratus/app/version.go new file mode 100644 index 000000000..3e04e0e2f --- /dev/null +++ b/cmd/fibratus/app/version.go @@ -0,0 +1,40 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app + +import ( + "fmt" + "github.com/spf13/cobra" + "os" + "runtime" +) + +var version string +var commit string + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Show version info", + Run: func(cmd *cobra.Command, args []string) { + if version == "" { + version = "dev" + } + _, _ = fmt.Fprintln(os.Stdout, "Version:", version, "Commit:", commit, "Go compiler:", runtime.Version()) + }, +} diff --git a/cmd/fibratus/fibratus.exe.manifest b/cmd/fibratus/fibratus.exe.manifest new file mode 100644 index 000000000..c1cb48310 --- /dev/null +++ b/cmd/fibratus/fibratus.exe.manifest @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/cmd/fibratus/fibratus.rc b/cmd/fibratus/fibratus.rc new file mode 100644 index 000000000..64be5cbd6 --- /dev/null +++ b/cmd/fibratus/fibratus.rc @@ -0,0 +1,38 @@ +#include "version.h" +#define RT_MANIFEST 24 + +#define VS_VERSION_INFO 1 +VS_VERSION_INFO VERSIONINFO + FILEVERSION RC_FILE_VERSION + PRODUCTVERSION RC_FILE_VERSION + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x40004L + FILETYPE 0x0L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "Fibratus" + VALUE "FileDescription", "Kernel tracing and exploration tool" + VALUE "FileVersion", FILE_VERSION_STRING + VALUE "InternalName", "fibratus" + VALUE "LegalCopyright", "Copyright (C) 2019-2020" + VALUE "OriginalFilename", "fibratus.exe" + VALUE "ProductName", "Fibratus" + VALUE "ProductVersion", FILE_VERSION_STRING + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END + +1 RT_MANIFEST "fibratus.exe.manifest" \ No newline at end of file diff --git a/cmd/fibratus/main.go b/cmd/fibratus/main.go new file mode 100644 index 000000000..315296881 --- /dev/null +++ b/cmd/fibratus/main.go @@ -0,0 +1,42 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "fmt" + "github.com/rabbitstack/fibratus/cmd/fibratus/app" + "golang.org/x/sys/windows/svc" + "os" +) + +func main() { + // determine if we are running in an interactive session + in, err := svc.IsAnInteractiveSession() + if err != nil { + fmt.Printf("interactive session check failed: %v\n", err) + os.Exit(-1) + } + if !in { + app.RunService() + return + } + if err := app.RootCmd.Execute(); err != nil { + os.Exit(-1) + } +} diff --git a/cmd/fibratus/version.h b/cmd/fibratus/version.h new file mode 100644 index 000000000..237ddd52b --- /dev/null +++ b/cmd/fibratus/version.h @@ -0,0 +1,6 @@ +#define RC_FILE_VERSION RC_VER,0 + +#define STRINGIFY(x) #x +#define TO_STRING(x) STRINGIFY(x) + +#define FILE_VERSION_STRING TO_STRING(VER) diff --git a/configs/fibratus.json b/configs/fibratus.json new file mode 100644 index 000000000..cac66b122 --- /dev/null +++ b/configs/fibratus.json @@ -0,0 +1,55 @@ +{ + "aggregator": { + "flush-period": "500ms", + "flush-timeout": "4s" + }, + + "alertsenders": { + "mail": { + "enabled": false + }, + + "slack": { + "enabled": false + } + }, + + "api": { + "transport": "localhost:8090", + "timeout": "5s" + }, + + "debug-privilege": true, + + "filament": { + "name": "", + "flush-period": "200ms" + }, + + "handle": { + "init-snapshot": true + }, + + "kevent": { + + }, + + "kcap": { + + }, + + "kstream": {}, + "logging": {}, + + "output": { + "console": { + "enabled": true, + "format": "pretty", + "kv-delimiter": "->" + } + }, + + "pe": {}, + "transformers": {}, + "yara": {} +} \ No newline at end of file diff --git a/configs/fibratus.yml b/configs/fibratus.yml new file mode 100644 index 000000000..62e9b60c5 --- /dev/null +++ b/configs/fibratus.yml @@ -0,0 +1,476 @@ +###################### Fibratus Configuration File ##################################### + +# =============================== Aggregator ========================================== + +# Aggregator is responsible for creating kernel event batches, applying transformers to each event +# present in the batch, and forwarding those batches to the output sinks. +aggregator: + # Determines the flush period that triggers the flushing of the kernel event batches to output sinks + flush-period: 500ms + + # Represents the max time to wait before announcing failed flushing of enqueued events when fibratus + # is stopped + flush-timeout: 4s + +# =============================== Alert senders ======================================== + +# Alert senders deal with emitting alerts via different channels. +alertsenders: + # Mail sender transports the alerts via SMTP protocol. + mail: + # Enables/disables mail alert sender + enabled: false + + # Represents the host of the SMTP server + #host: + + # Represents the port of the SMTP server + #port: 25 + + # Specifies the user name when authenticating to the SMTP server + #user: + + # Specifies the password when authenticating to the SMTP server + #password: + + # Specifies the sender's address + #from: + + # Specifies all the recipients that'll receive the alert + #to: + # - "" + + # Slack sender transports the alerts to the Slack workspace. + slack: + # Enables/disables Slack alert sender + enabled: false + + # Represents the Webhook URL of the workspace where alerts will be dispatched + #url: + + # Designates the Slack workspace where alerts will be routed + #workspace: + + # Is the slack channel in which to post alerts + #channel: + + # Represents the emoji icon surrounded in ':' characters for the Slack bot + #emoji: "" + +# =============================== API ================================================== + +# Settings that influence the behaviour of the HTTP server that exposes a number of endpoints such as +# expvar metrics, internal state, and so on +api: + # Specifies the underlying transport protocol for the API HTTP server. The transport can either be the + # named pipe or TCP socket. Default is named pipe but you can override it to expose the API server on + # TCP address, e.g. 192.168.1.32:8084. + transport: localhost:8482 + + # Represents the timeout interval for the HTTP server responses. + timeout: 5s + +# =============================== General ============================================== + +# Indicates whether debug privilege is set in Fibratus process' token. Enabling this security policy allows +# Fibratus to obtain handles of protected processes for the purpose of querying the Process Environment Block +# regions. +debug-privilege: true + + +# =============================== Filament ============================================= + +# Filaments are lightweight Python scriplets that are executed on top of the kernel event stream. You can easily +# extend Fibratus with custom features that is encapsulated in filaments. This section controls the behaviour of +# the filament engine. +filament: + # Specifies the name of the filament that is executed by the run command + name: "" + + # The directory where all filaments are located. By default, filaments are stored in the ${PROGRAMFILES}/fibratus/filaments directory. + #path: ${PROGRAMFILES}/fibratus/filaments + + # Determines how often event batches are propagated to the filament callback function + #flush-period: 200ms + +# =============================== Handle =============================================== + +# Indicates whether initial handle snapshot is built. The snapshot contains the state of system handles. +handle: + init-snapshot: true + +# =============================== Kevent =============================================== + +# The following settings control the state of the kernel event. +kevent: + # Indicates if threads are serialized as part of the process state + serialize-threads: false + + # Indicates if modules such as Dynamic Linked Libraries are serialized as part of the process state + serialize-images: false + + # Indicates if handles are serialized as part of the process state + serialize-handles: false + + # Indicates if PE (Portable Executable) metadata are serialized as part of the process state + serialize-pe: false + + # Indicates if environment variables are serialized as part of the process state + serialize-envs: false + +# =============================== Kcap ================================================= + +# Contains the settings that dictate the behaviour of the kernel event captures. + +kcap: + # Specifies the name of the output kcap file. If not empty, capture files are always stored + # to this file by overwriting any existing capture file + file: "" + +# =============================== Kstream ============================================== + +# Tweaks for controlling the behaviour of the kernel stream consumer. +kstream: + # Determines the maximum number of buffers allocated for the event tracing session's buffer pool + #max-buffers: + + # Determines the minimum number of buffers allocated for the event tracing session's buffer pool + #min-buffers: + + # Specifies how often the trace buffers are forcibly flushed + #flush-interval: 1s + + # Represents the amount of memory allocated for each event tracing session buffer, in kilobytes. + # The buffer size affects the rate at which buffers fill and must be flushed (small buffer size requires + # less memory but it increases the rate at which buffers must be flushed) + #buffer-size: + + # Determines whether thread kernel events are collected by Kernel Logger provider + #enable-thread: true + + # Determines whether registry kernel events are collected by Kernel Logger provider + #enable-registry: true + + # Determines whether network kernel events are collected by Kernel Logger provider + #enable-net: true + + # Determines whether file kernel events are collected by Kernel Logger provider + #enable-fileio: true + + # Determines whether image kernel events are collected by Kernel Logger provider + #enable-image: true + + # Determines whether object manager kernel events (handle creation/destruction) are + # collected by Kernel Logger provider + #enable-handle: false + + # Determines which events are dropped either by the event name or the process' image + # name that triggered the event. + blacklist: + # Contains a list of kernel event names that are dropped from the event stream + events: + - CloseFile + # Contains a list of case-insensitive process image names including the extension. + # Any event originated by the image specified in this list is dropped from the event stream + images: + - System + + +# =============================== Logging ================================================ + +# Contains the tweaks for fine-tuning the behaviour of the log files produced by Fibratus. +logging: + # Specifies the minimum allowed log level. Anything logged below this log level will + # not get dumped to a file or stdout stream + level: info + + # Represents the maximum number of days to retain old log files based on the timestamp + # encoded in their filename. By default, all log files are retained + # max-age: 0 + + # Specifies the maximum number of old log files to retain + #max-backups: 15 + + # Specifies the maximum size in megabytes of the log file before it gets rotated + #max-size: 100 + + # Represents the log file format. By default, Fibratus will dump the logs in JSON format + #formatter: json + + # Represents the alternative paths for storing the logs. Logs are usually stored in the + # same directory where Fibratus was installed + #path: + + # Indicates whether log lines are written to standard output in addition to writing them to log files + #log-stdout: false + + +# =============================== Output ================================================ + +# Outputs transport the event flowing through kernel event stream to its final destination. Only one output +# can be active at the time. The following section contains available outputs and their preferences. +output: + # Console output writes the event to standard output stream. + console: + # Indicates whether the console output is active + enabled: true + + # Specifies the console output format. The "pretty" format dictates that formatting is accomplished + # by replacing the specifiers in the template. The "json" format outputs the event as a raw JSON string + format: pretty + + # Template that's feed into event formatter. The default event formatter template is: + # + # {{ .Seq }} {{ .Timestamp }} - {{ .CPU }} {{ .Process }} ({{ .Pid }}) - {{ .Type }} ({{ .Kparams }}) + # + #template: + + # Specifies the separator that's rendered between the event parameter's key and its value. + #kv-delimiter: + + # Elasticsearch output indexes event bulks into Elasticsearch clusters. + elasticsearch: + # Indicates whether the Elasticsearch output is enabled + enabled: false + + # Defines the URL endpoints of the Elasticsearch nodes + #servers: + # - http://localhost:9200 + + # Represents the initial HTTP connection timeout + #timeout: 5s + + # Specifies when to flush the bulk at the end of the given interval + #flush-period: 1s + + # Determines the number of workers that commit docs to Elasticsearch + #bulk-workers: 1 + + # Enables/disables nodes health checking + #healthcheck: true + + # Specifies the interval for checking if the Elasticsearch nodes are available + #healthcheck-interval: 10s + + # Specifies the timeout for periodic health checks + #healthcheck-timeout: 5s + + # Identifies the user name for the basic HTTP authentication + #username: + + # Identifies the password for the basic HTTP authentication + #password: + + # Enables the discovery of all Elasticsearch nodes in the cluster. This avoids populating the list + # of available Elasticsearch nodes + #sniff: false + + # Determines if the Elasticsearch trace log is enabled. Useful for troubleshooting + #trace-log: false + + # Specifies if gzip compression is enabled + #gzip-compression: false + + # Specifies the name of the index template + #template-name: fibratus + + # Represents the target index for kernel events. It allows time specifiers to create indices per time frame. + # For example, fibratus-%Y-%m generates the index name with current year and and month time specifiers + #index-name: fibratus + + # Contains the full JSON body of the index template. For more information refer to + # https://www.elastic.co/guide/en/elasticsearch/reference/current/index-templates.html + #template-config: + + # Path to the public/private key file + #tls-key: + + # Path to certificate file + #tls-cert: + + # Represents the path of the certificate file that is associated with the Certification Authority (CA) + #tls-ca: + + # Indicates if the chain and host verification stage is skipped + #tls-insecure-skip-verify: false + + # Amqp output emits event batches to RabbitMQ brokers. + amqp: + # Indicates if the AMQP output is enabled + enabled: false + + # Represents the AMQP connection string + #url: amqp://localhost:5672 + + # Specifies the AMQP connection timeout + #timeout: 5s + + # Specifies target exchange name that receives inbound kernel events + #exchange: fibratus + + # Represents the AMQP exchange type. Available exchange type include common types are "direct", "fanout", + # "topic", "header", and "x-consistent-hash" + #exchange-type: topic + + # Represents the static routing key to link exchanges with queues. + #routing-key: fibratus + + # Represents the virtual host name + #vhost: / + + # Indicates if the exchange is marked as durable. Durable exchanges can survive server restarts + #durable: false + + # Indicates if the server checks whether the exchange already exists and raises an error if it doesn't exist + #passive: false + + # Determines if a published message is persistent or transient + #delivery-mode: transient + + # The username for the plain authentication method + #username: + # The password for the plain authentication method + #password: + + # Designates static headers that are added to each published message + #headers: + # env: dev + + # Path to the public/private key file + #tls-key: + + # Path to certificate file + #tls-cert: + + # Represents the path of the certificate file that is associated with the Certification Authority (CA) + #tls-ca: + + # Indicates if the chain and host verification stage is skipped + #tls-insecure-skip-verify: false + +# =============================== Portable Executable (PE) ============================= + +# Tweaks for controlling the fetching of the PE (Portable Executable) metadata from the process' binary image. +pe: + # Designates whether inspecting PE metadata is allowed. + enabled: false + + # Contains a list of image names that are excluded from PE parsing + excluded-images: + - svchost.exe + + # Determines if resources are read from the PE resource directory + #read-resources: false + + # Indicates if symbols are read from the PE headers + #read-symbols: false + + # Indicates if full section inspection is allowed. When se to true, section's individual bytes are + # consulted for computing section hashes, calculating the entropy, and so on + #read-sections: false + +# =============================== Transformers ========================================= + +# Transformers are responsible for augmenting, parsing or enriching kernel events. +transformers: + # Remove transformer deletes provided event parameters. + remove: + # Indicates if the remove transformer is enabled + enabled: false + + # Represents the list of parameters that are removed from the event + #kparams: + # - irp + + # Rename transformer renames parameter from old to new name. + rename: + # Indicates if the rename transformer is enabled + enabled: false + + # Contains the list of old/new mappings. Old represents the original + # parameter name, while new is the new parameter name + #kparams: + # - old: + # new: + + # Replace transformer replaces all non-overlapping instances of old parameter's value with the new one. + replace: + # Indicates if the replace transformer is enabled + enabled: false + + # Contains the list of parameter replacements. For each target event parameter, the old represent the substring + # that gets replaced by the new string. + #replacements: + # - kparam: + # old: + # new: + + # Tags transformer appends custom key/value pairs to event metadata. + tags: + # Indicates if the tags transformer is enabled + enabled: false + + # Contains the list of tags that are appended to event metadata. Values can be fetched from environment + # variables by enclosing them in % symbols + #tags: + # - key: + # value: + + # Trim transformer removes prefixes/suffixes from event parameter values. + trim: + # # Indicates if the trim transformer is enabled + enabled: false + + # Contains the list of parameters associated with the prefix that is trimmed from the parameter's value + #prefixes: + # - kparam: + # trim: + + # Contains the list of parameters associated with the suffix that is trimmed from the parameter's value + #suffixes: + # - kparam: + # trim: + +# =============================== YARA ================================================= + +# Tweaks that influence the behaviour of the YARA scanner. +yara: + # Indicates if the YARA scanner is enabled. When enabled, each newly created process is scanned for pattern matches. + enabled: false + + # Contains rule paths and rule definition information + rule: + # Represents the paths within the file system along with the YARA namespace identifier + paths: + - path: "" + namespace: "" + + # Represents the string with the rule definition along with the YARA namespace identifier + strings: + - string: + namespace: + + # Indicates which sender is used to transport the alert generated by scanner + #alert-via: mail + + # Specifies templates for the alert title and text in Go templating language (https://golang.org/pkg/text/template) + #alert-template: + # title: + # text: + + # Determines when multiple matches of the same string can be avoided when not necessary + #fastscan: true + + # Specifies the timeout for the scanner. If the timeout is reached, the scan operation is cancelled + #scan-timeout: 20s + + # Indicates whether file scanning is disabled. This affects the scan triggered by the image loading events. + #skip-files: true + + # Contains the list of file names that shouldn't be scanned + #excluded-files: + # - kernel32.dll + + # Contains the list of the process' image names that shouldn't be scanned + #excluded-procs: + # - System diff --git a/fibratus.spec b/fibratus.spec deleted file mode 100644 index d9180136b..000000000 --- a/fibratus.spec +++ /dev/null @@ -1,30 +0,0 @@ -# -*- mode: python -*- - -block_cipher = None - - -a = Analysis(['fibratus\\cli.py'], - pathex=[], - binaries=[], - datas=[('schema.yml', '.')], - hiddenimports=[], - hookspath=[], - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher) -pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) -exe = EXE(pyz, - a.scripts, - a.binaries + [('msvcp140.dll', 'C:\\Windows\\System32\\msvcp140.dll', 'BINARY'), - ('vcruntime140.dll', 'C:\\Windows\\System32\\vcruntime140.dll', 'BINARY')], - a.zipfiles, - a.datas, - name='fibratus', - debug=False, - strip=False, - upx=True, - console=True, - icon=None) diff --git a/fibratus.yml b/fibratus.yml deleted file mode 100644 index f6dc466a7..000000000 --- a/fibratus.yml +++ /dev/null @@ -1,55 +0,0 @@ -image_meta: - enabled: false - imports: false - file_info: false - -skips: - images: - - svchost.exe - - smss.exe - - services.exe - - taskmgr.exe - - dwm.exe - - vprot.exe - - lsass.exe - - sihost.exe - - system - -output: - - console: - format: pretty -# - amqp: -# host: 127.0.0.1 -# port: 5672 -# username: guest -# password: guest -# vhost: / -# exchange: amq.direct -# routingkey: fibratus -# - smtp: -# host: smtp.gmail.com -# port: 587 -# from: info@github.io -# password: secret -# to: -# - fibratus@github.io -# - netmutatus@github.io -# - elasticsearch: -# hosts: -# - localhost:9200 -# index: kernelstream -# index_type: daily -# daily_index_format: %Y.%m.%d -# document: threads -# bulk: False -# username: elastic -# password: changeme -# ssl: True -# - fs: -# path: D:\\ -# mode: a -# format: json - -#binding: -# - yara: -# path: D:\yara-rules diff --git a/fibratus/__init__.py b/fibratus/__init__.py deleted file mode 100644 index 1a28ab226..000000000 --- a/fibratus/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. diff --git a/fibratus/apidefs/__init__.py b/fibratus/apidefs/__init__.py deleted file mode 100644 index 23b74303e..000000000 --- a/fibratus/apidefs/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. \ No newline at end of file diff --git a/fibratus/apidefs/cdefs.py b/fibratus/apidefs/cdefs.py deleted file mode 100644 index 79f3cc230..000000000 --- a/fibratus/apidefs/cdefs.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from ctypes import Structure -from ctypes import c_void_p, c_ubyte, c_ushort, c_ulong, c_size_t, c_wchar_p -import re -import ctypes - -# undefined ctypes wintypes -LPVOID = c_void_p -PVOID = c_void_p -UCHAR = c_ubyte -SIZE_T = c_size_t -LPTSTR = c_wchar_p - -# status codes -STATUS_INFO_LENGTH_MISMATCH = 0xc0000004 -STATUS_SUCCESS = 0 - -# error codes -ERROR_SUCCESS = 0x0 -ERROR_ACCESS_DENIED = 0x5 -ERROR_BAD_LENGTH = 0x18 -ERROR_INVALID_PARAMETER = 0x57 -ERROR_ALREADY_EXISTS = 0xB7 - - -def get_last_error(): - return ctypes.GetLastError() - - -class UNICODE_STRING(Structure): - _fields_ = [('length', c_ushort), - ('maximum_length', c_ushort), - ('buffer', c_void_p)] - - -class GUID(Structure): - _fields_ = [("Data1", c_ulong), - ("Data2", c_ushort), - ("Data3", c_ushort), - ("Data4", c_ubyte * 8)] - _GUID_REGEX = re.compile('{([0-9A-F]{8})-([0-9A-F]{4})-([0-9A-F]{4})-([0-9A-F]{2})([0-9A-F]{2})-' - '([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})' - '([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})}', re.I) - - def __init__(self, gs=None): - if gs: - match = self._GUID_REGEX.match(gs) - g = [int(i, 16) for i in match.groups()] - self.Data1 = g[0] - self.Data2 = g[1] - self.Data3 = g[2] - for i in range(8): - self.Data4[i] = g[3 + i] - - def __str__(self): - return "{%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}" % \ - (self.Data1, self.Data2, self.Data3, - self.Data4[0], self.Data4[1], - self.Data4[2], self.Data4[3], self.Data4[4], - self.Data4[5], self.Data4[6], self.Data4[7]) diff --git a/fibratus/apidefs/declarer.py b/fibratus/apidefs/declarer.py deleted file mode 100644 index d6ed2bc54..000000000 --- a/fibratus/apidefs/declarer.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from ctypes import windll, CDLL - - -ADVAPI = 0 -KERNEL = 1 -NT = 2 -C = 3 -USER = 4 - -__LIBS__ = {ADVAPI: windll.advapi32, - KERNEL: windll.kernel32, - NT: windll.ntdll, - C: CDLL('msvcrt'), - USER: windll.user32} - - -def declare(lib_name, function_name, args, restype): - if lib_name in __LIBS__: - lib = __LIBS__[lib_name] - function = getattr(lib, function_name) - if function: - if len(args) > 0: - function.argtypes = args - if restype: - function.restype = restype - return function - else: - raise AttributeError('The library %s cannot be loaded' % lib_name) \ No newline at end of file diff --git a/fibratus/apidefs/etw.py b/fibratus/apidefs/etw.py deleted file mode 100644 index bd193293d..000000000 --- a/fibratus/apidefs/etw.py +++ /dev/null @@ -1,143 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from ctypes import Structure, POINTER -from ctypes import c_uint64, c_ulong, c_long, c_ulonglong, c_wchar_p, c_ubyte -from ctypes.wintypes import LARGE_INTEGER, HANDLE - -from fibratus.apidefs.guiddef import GUID -import fibratus.apidefs.declarer as declarer - - -TRACEHANDLE = c_uint64 - -WNODE_FLAG_TRACED_GUID = 0x00020000 -PROCESS_TRACE_MODE_REAL_TIME = 0x00000100 - - -KERNEL_TRACE_CONTROL_GUID = GUID('{9e814aad-3204-11d2-9a82-006008a86939}') -KERNEL_LOGGER_NAME = "NT Kernel Logger" - - -# enable flags for kernel events -EVENT_TRACE_FLAG_PROCESS = 0x00000001 -EVENT_TRACE_FLAG_THREAD = 0x00000002 -EVENT_TRACE_FLAG_IMAGE_LOAD = 0x00000004 - -EVENT_TRACE_FLAG_DISK_IO = 0x00000100 -EVENT_TRACE_FLAG_DISK_FILE_IO = 0x00000200 - -EVENT_TRACE_FLAG_MEMORY_PAGE_FAULTS = 0x00001000 -EVENT_TRACE_FLAG_MEMORY_HARD_FAULTS = 0x00002000 - -EVENT_TRACE_FLAG_NETWORK_TCPIP = 0x00010000 - -EVENT_TRACE_FLAG_REGISTRY = 0x00020000 -EVENT_TRACE_FLAG_DBGPRINT = 0x00040000 - -EVENT_TRACE_FLAG_PROCESS_COUNTERS = 0x00000008 -EVENT_TRACE_FLAG_CSWITCH = 0x00000010 -EVENT_TRACE_FLAG_DPC = 0x00000020 -EVENT_TRACE_FLAG_INTERRUPT = 0x00000040 -EVENT_TRACE_FLAG_SYSTEMCALL = 0x00000080 - -EVENT_TRACE_FLAG_DISK_IO_INIT = 0x00000400 - -EVENT_TRACE_FLAG_ALPC = 0x00100000 -EVENT_TRACE_FLAG_SPLIT_IO = 0x00200000 - -EVENT_TRACE_FLAG_DRIVER = 0x00800000 -EVENT_TRACE_FLAG_PROFILE = 0x01000000 -EVENT_TRACE_FLAG_FILE_IO = 0x02000000 -EVENT_TRACE_FLAG_FILE_IO_INIT = 0x04000000 - - -EVENT_TRACE_FLAG_DISPATCHER = 0x00000800 -EVENT_TRACE_FLAG_VIRTUAL_ALLOC = 0x00004000 - - -EVENT_TRACE_CONTROL_QUERY = 0 -EVENT_TRACE_CONTROL_STOP = 1 -EVENT_TRACE_CONTROL_UPDATE = 2 - - -EVENT_CONTROL_CODE_DISABLE_PROVIDER = 0 -EVENT_CONTROL_CODE_ENABLE_PROVIDER = 1 -EVENT_CONTROL_CODE_CAPTURE_STATE = 2 - - -class WNODE_HEADER(Structure): - _fields_ = [('buffer_size', c_ulong), - ('provider_id', c_ulong), - ('historical_context', c_uint64), - ('timestamp', LARGE_INTEGER), - ('guid', GUID), - ('client_context', c_ulong), - ('flags', c_ulong)] - - -class EVENT_TRACE_PROPERTIES(Structure): - _fields_ = [('wnode', WNODE_HEADER), - ('buffer_size', c_ulong), - ('minimum_buffers', c_ulong), - ('maximum_buffers', c_ulong), - ('maximum_file_size', c_ulong), - ('log_file_mode', c_ulong), - ('flush_timer', c_ulong), - ('enable_flags', c_ulong), - ('age_limit', c_long), - ('number_of_buffers', c_ulong), - ('free_buffers', c_ulong), - ('events_lost', c_ulong), - ('buffers_written', c_ulong), - ('log_buffers_lost', c_ulong), - ('real_time_buffer_lost', c_ulong), - ('logger_thread_id', HANDLE), - ('log_file_name_offset', c_ulong), - ('logger_name_offset', c_ulong)] - - -class TRACE_GUID_REGISTRATION(Structure): - _fields_ = [('guid', POINTER(GUID)), - ('reg_handle', HANDLE)] - - -class EVENT_FILTER_DESCRIPTOR(Structure): - _fields_ = [('Ptr', c_ulonglong), - ('Size', c_ulong), - ('Type', c_ulong)] - - -class ENABLE_TRACE_PARAMETERS(Structure): - _fields_ = [('Version', c_ulong), - ('EnableProperty', c_ulong), - ('ControlFlags', c_ulong), - ('SourceId', GUID), - ('EnableFilterDesc', POINTER(EVENT_FILTER_DESCRIPTOR)), - ('FilterDescCount', c_ulong)] - - -start_trace = declarer.declare(declarer.ADVAPI, 'StartTraceW', - [POINTER(TRACEHANDLE), c_wchar_p, POINTER(EVENT_TRACE_PROPERTIES)], - c_ulong) - - -control_trace = declarer.declare(declarer.ADVAPI, 'ControlTraceW', - [TRACEHANDLE, c_wchar_p, POINTER(EVENT_TRACE_PROPERTIES), c_ulong], - c_ulong) - -enable_trace_ex = declarer.declare(declarer.ADVAPI, 'EnableTraceEx2', - [TRACEHANDLE, POINTER(GUID), c_ulong, c_ubyte, c_ulonglong, - c_ulonglong, c_ulong, POINTER(ENABLE_TRACE_PARAMETERS)], - c_ulong) \ No newline at end of file diff --git a/fibratus/apidefs/fs.py b/fibratus/apidefs/fs.py deleted file mode 100644 index bdce04440..000000000 --- a/fibratus/apidefs/fs.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from ctypes import Structure -from ctypes.wintypes import HANDLE, DWORD, BOOL, WCHAR, LONG - -import fibratus.apidefs.declarer as declarer -from fibratus.apidefs.cdefs import LPVOID, LPTSTR - - -FILE_SHARE_READ = 0x00000001 -FILE_SHARE_WRITE = 0x00000002 -FILE_SHARE_DELETE = 0x00000004 - -# if the file already exists, replace it with the given file. If it does not, create the given file. -FILE_SUPERSEDE = 0x00000000 -# if the file already exists, open it instead of creating a new file. -# If it does not, fail the request and do not create a new file. -FILE_OPEN = 0x00000001 -# if the file already exists, fail the request and do not create or open the given file. -# If it does not, create the given file. -FILE_CREATE = 0x00000002 -# If the file already exists, open it. If it does not, create the given file. -FILE_OPEN_IF = 0x00000003 -# If the file already exists, open it and overwrite it. If it does not, fail the request. -FILE_OVERWRITE = 0x00000004 -# If the file already exists, open it and overwrite it. If it does not, create the given file. -FILE_OVERWRITE_IF = 0x00000005 - -# the file being created or opened is a directory file -FILE_DIRECTORY_FILE = 0x00000001 -# open a file with a reparse point and bypass normal reparse point processing for the file -FILE_OPEN_REPARSE_POINT = 0x00200000 - - -class FILE_NAME_INFO(Structure): - _fields_ = [('file_name_length', DWORD), - ('filename', WCHAR * 1)] - - -get_file_info_by_handle = declarer.declare(declarer.KERNEL, 'GetFileInformationByHandleEx', - [HANDLE, DWORD, LPVOID, DWORD], - BOOL) -query_dos_device = declarer.declare(declarer.KERNEL, 'QueryDosDeviceW', - [LPTSTR, LPTSTR, DWORD], - DWORD) - -_get_osfhandle = declarer.declare(declarer.C, '_get_osfhandle', - [DWORD], - LONG) - -get_file_type = declarer.declare(declarer.KERNEL, 'GetFileType', - [HANDLE], - DWORD) \ No newline at end of file diff --git a/fibratus/apidefs/guiddef.py b/fibratus/apidefs/guiddef.py deleted file mode 100644 index a383f78f8..000000000 --- a/fibratus/apidefs/guiddef.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -import ctypes -import re - - -class GUID(ctypes.Structure): - _DECOMPOSE_RE = re.compile('{([0-9A-F]{8})-([0-9A-F]{4})-([0-9A-F]{4})-([0-9A-F]{2})([0-9A-F]{2})-' - '([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})' - '([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})}', re.I) - - def __init__(self, guid_as_str=None): - if guid_as_str: - m = self._DECOMPOSE_RE.match(guid_as_str) - g = [int(i, 16) for i in m.groups()] - self.Data1 = g[0] - self.Data2 = g[1] - self.Data3 = g[2] - for i in range(8): - self.Data4[i] = g[3 + i] - - _fields_ = [("Data1", ctypes.c_ulong), - ("Data2", ctypes.c_ushort), - ("Data3", ctypes.c_ushort), - ("Data4", ctypes.c_ubyte * 8)] - - def __str__(self): - return "{%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}" % \ - (self.Data1, self.Data2, self.Data3, - self.Data4[0], self.Data4[1], - self.Data4[2], self.Data4[3], self.Data4[4], - self.Data4[5], self.Data4[6], self.Data4[7]) diff --git a/fibratus/apidefs/process.py b/fibratus/apidefs/process.py deleted file mode 100644 index d02e01fe4..000000000 --- a/fibratus/apidefs/process.py +++ /dev/null @@ -1,115 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from ctypes import POINTER -from ctypes.wintypes import DWORD, BOOL, HANDLE, ULONG, PULONG, BYTE, PDWORD - -from fibratus.apidefs.cdefs import * -from fibratus.apidefs.sys import malloc, free -import fibratus.apidefs.declarer as declarer - - -# process access rights -PROCESS_VM_READ = 0x0010 -PROCESS_DUP_HANDLE = 0x0040 -PROCESS_QUERY_INFORMATION = 0x0400 -PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 - -# thread access rights -THREAD_QUERY_INFORMATION = 0x0040 - -# ZwQueryInformationProcess constants -PROCESS_BASIC_INFO = 0 -PROCESS_IMAGE_FILENAME = 27 - - -# PEB (Process Environment Block) structures -class LIST_ENTRY(Structure): - pass -LIST_ENTRY._fields_ = [('flink', POINTER(LIST_ENTRY)), ('blink', POINTER(LIST_ENTRY))] - - -class PEB_LDR_DATA(Structure): - _fields_ = [('reserved1', BYTE * 8), - ('reserved2', BYTE * 3), - ('in_memory_order_module_list', LIST_ENTRY)] - - -class RTL_USER_PROCESS_PARAMETERS(Structure): - _fields_ = [('reserved1', BYTE * 16), - ('reserved2', PVOID * 10), - ('image_path_name', UNICODE_STRING), - ('command_line', UNICODE_STRING)] - - -class PEB(Structure): - _fields_ = [('reserved1', BYTE * 2), - ('being_debugged', BYTE), - ('reserved2', BYTE * 21), - ('ldr', POINTER(PEB_LDR_DATA)), - ('process_parameters', POINTER(RTL_USER_PROCESS_PARAMETERS)), - ('reserved3', BYTE * 520), - ('post_process_init_routine', PVOID), - ('reserved4', BYTE * 136), - ('session_id', ULONG)] - - -class PROCESS_BASIC_INFORMATION(Structure): - _fields_ = [('reserved1', PVOID), - ('peb_base_address', POINTER(PEB)), - ('reserved2', PVOID * 2), - ('unique_process_id', PULONG), - ('inherited_from_unique_process_id', ULONG)] - -open_process = declarer.declare(declarer.KERNEL, 'OpenProcess', - [DWORD, BOOL, DWORD], - HANDLE) - -open_thread = declarer.declare(declarer.KERNEL, 'OpenThread', - [DWORD, BOOL, DWORD], - HANDLE) - -_read_process_memory = declarer.declare(declarer.KERNEL, 'ReadProcessMemory', - [HANDLE, LPVOID, LPVOID, SIZE_T, POINTER(SIZE_T)], - BOOL) - -zw_query_information_process = declarer.declare(declarer.NT, 'ZwQueryInformationProcess', - [HANDLE, DWORD, PVOID, ULONG, PULONG], - DWORD) -query_full_process_image_name = declarer.declare(declarer.KERNEL, 'QueryFullProcessImageNameW', - [HANDLE, DWORD, LPTSTR, PDWORD], - BOOL) - -get_current_process = declarer.declare(declarer.KERNEL, 'GetCurrentProcess', - [], - HANDLE) -get_process_id_of_thread = declarer.declare(declarer.KERNEL, 'GetProcessIdOfThread', - [HANDLE], - DWORD) - - -def read_process_memory(process, chunk, size): - """Reads a memory block from the process address space. - """ - buff = malloc(size) - status = _read_process_memory(process, - chunk, - buff, - size, - None) - if status != ERROR_SUCCESS: - return buff - else: - free(buff) diff --git a/fibratus/apidefs/registry.py b/fibratus/apidefs/registry.py deleted file mode 100644 index c253b419f..000000000 --- a/fibratus/apidefs/registry.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from ctypes.wintypes import HKEY, DWORD, LPDWORD, LONG, LPCWSTR -from enum import Enum - -from fibratus.apidefs.cdefs import * -import fibratus.apidefs.declarer as declarer - - -# query type flags -RRF_RT_ANY = 0x0000ffff - -# reserved key handles -HKEY_CLASSES_ROOT = HKEY(0x80000000) -HKEY_CURRENT_USER = HKEY(0x80000001) -HKEY_LOCAL_MACHINE = HKEY(0x80000002) -HKEY_USERS = HKEY(0x80000003) - -MAX_BUFFER_SIZE = 4096 -reg_get_value = declarer.declare(declarer.ADVAPI, 'RegGetValueW', - [HKEY, LPCWSTR, LPCWSTR, - DWORD, LPDWORD, PVOID, LPDWORD], - LONG) - - -class ValueType(Enum): - REG_NONE = 0 - REG_SZ = 1 - REG_EXPAND_SZ = 2 - REG_BINARY = 3 - REG_DWORD = 4 - REG_DWORD_BIG_ENDIAN = 5 - REG_LINK = 6 - REG_MULTI_SZ = 7 - REG_RESOURCE_LIST = 8 - REG_FULL_RESOURCE_DESCRIPTOR = 9 - REG_RESOURCE_REQUIREMENTS_LIST = 10 - REG_QWORD = 11 diff --git a/fibratus/apidefs/sys.py b/fibratus/apidefs/sys.py deleted file mode 100644 index 3686ee21a..000000000 --- a/fibratus/apidefs/sys.py +++ /dev/null @@ -1,157 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from ctypes import POINTER -from ctypes import c_int, c_byte, WINFUNCTYPE -from ctypes.wintypes import DWORD, ULONG, PULONG, USHORT, HANDLE, BOOL, SHORT, WCHAR, CHAR, WORD, LPDWORD - -from fibratus.apidefs.cdefs import * -import fibratus.apidefs.declarer as declarer - - -SYSTEM_HANDLE_INFORMATION_CLASS = 16 - -PUBLIC_OBJECT_BASIC_INFORMATION = 0 -PUBLIC_OBJECT_NAME_INFORMATION = 1 -PUBLIC_OBJECT_TYPE_INFORMATION = 2 - -# this constants may vary from -# one Windows version to another -# for Win 7/8 they should have -# the following values -FILE_OBJECT_TYPE_INDEX = 28 - - -STD_OUTPUT_HANDLE = -11 -INVALID_HANDLE_VALUE = -1 - -CONSOLE_TEXTMODE_BUFFER = 1 - -GENERIC_READ = 0x80000000 -GENERIC_WRITE = 0x40000000 - -FILE_SHARE_READ = 0x00000001 -FILE_SHARE_WRITE = 0x00000002 - - -# console structures -class CURSOR_INFO(ctypes.Structure): - _fields_ = [("size", c_int), - ("visible", c_byte)] - - -class COORD(ctypes.Structure): - _fields_ = [("x", SHORT), ("y", SHORT)] - - -class SMALL_RECT(ctypes.Structure): - _fields_ = [("left", SHORT), - ("top", SHORT), - ("right", SHORT), - ("bottom", SHORT)] - - -class CHAR_INFOU(ctypes.Union): - _fields_ = [("unicode_char", WCHAR), ("ascii_char", CHAR)] - - -class CHAR_INFO(ctypes.Structure): - _anonymous_ = ("char",) - _fields_ = [("char", CHAR_INFOU), ("attributes", WORD)] - - -class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure): - _fields_ = [('size', COORD), - ('cursor_position', COORD), - ('attributes', WORD), - ('window', SMALL_RECT), - ('maximum_window_size', COORD)] - - -class SYSTEM_HANDLE(Structure): - _fields_ = [('process_id', ULONG), - ('object_type_number', UCHAR), - ('flags', UCHAR), - ('handle', USHORT), - ('object', PVOID), - ('access_mask', DWORD)] - - -class SYSTEM_HANDLE_INFORMATION(Structure): - _fields_ = [('number_of_handles', ULONG), - ('handles', SYSTEM_HANDLE * 1)] - - -class OBJECT_TYPE_INFORMATION(Structure): - _fields_ = [('type_name', UNICODE_STRING), - ('reserved', ULONG * 22)] - - -# retrieves the specified system information -zw_query_system_information = declarer.declare(declarer.NT, 'ZwQuerySystemInformation', - [DWORD, PVOID, ULONG, PULONG], - DWORD) - -# memory alloc/free functions -malloc = declarer.declare(declarer.C, 'malloc', [c_size_t], c_void_p) -realloc = declarer.declare(declarer.C, 'realloc', [c_void_p, c_size_t], c_void_p) -free = declarer.declare(declarer.C, 'free', [c_void_p], None) - -# object handle cleanup -close_handle = declarer.declare(declarer.KERNEL, 'CloseHandle', [HANDLE], BOOL) -# duplicate object handle -duplicate_handle = declarer.declare(declarer.KERNEL, 'DuplicateHandle', - [HANDLE, HANDLE, HANDLE, POINTER(HANDLE), DWORD, ULONG, ULONG], - DWORD) - -# query object name / type -nt_query_object = declarer.declare(declarer.NT, 'NtQueryObject', - [HANDLE, ULONG, PVOID, ULONG, PULONG], - DWORD) - -# low level console api -get_std_handle = declarer.declare(declarer.KERNEL, 'GetStdHandle', [DWORD], HANDLE) -set_console_active_screen_buffer = declarer.declare(declarer.KERNEL, 'SetConsoleActiveScreenBuffer', [HANDLE], BOOL) - -create_console_screen_buffer = declarer.declare(declarer.KERNEL, 'CreateConsoleScreenBuffer', - [DWORD, DWORD, c_void_p, DWORD, LPVOID], HANDLE) -get_console_screen_buffer_info = declarer.declare(declarer.KERNEL, 'GetConsoleScreenBufferInfo', - [HANDLE, POINTER(CONSOLE_SCREEN_BUFFER_INFO)], BOOL) - -write_console_output = declarer.declare(declarer.KERNEL, 'WriteConsoleOutputW', - [HANDLE, POINTER(CHAR_INFO), COORD, COORD, POINTER(SMALL_RECT)], BOOL) - -set_console_cursor_position = declarer.declare(declarer.KERNEL, 'SetConsoleCursorPosition', - [HANDLE, COORD], BOOL) - -get_console_cursor_info = declarer.declare(declarer.KERNEL, 'GetConsoleCursorInfo', - [HANDLE, POINTER(CURSOR_INFO)], BOOL) - -set_console_cursor_info = declarer.declare(declarer.KERNEL, 'SetConsoleCursorInfo', - [HANDLE, POINTER(CURSOR_INFO)], BOOL) - -write_console_unicode = declarer.declare(declarer.KERNEL, 'WriteConsoleW', - [HANDLE, c_void_p, DWORD, LPDWORD, LPVOID], BOOL) - - -PHANDLER_ROUTINE = WINFUNCTYPE(BOOL, DWORD) -set_console_ctrl_handler = declarer.declare(declarer.KERNEL, 'SetConsoleCtrlHandler', - [PHANDLER_ROUTINE, BOOL], BOOL) - - -# event objects -create_event = declarer.declare(declarer.KERNEL, 'CreateEventW', [c_void_p, BOOL, BOOL, LPTSTR], HANDLE) - - diff --git a/fibratus/binding/__init__.py b/fibratus/binding/__init__.py deleted file mode 100644 index d7ce39087..000000000 --- a/fibratus/binding/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright 2017 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. \ No newline at end of file diff --git a/fibratus/binding/base.py b/fibratus/binding/base.py deleted file mode 100644 index f4a8913cf..000000000 --- a/fibratus/binding/base.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2017 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -class BaseBinding(object): - - def __init__(self, outputs, logger): - self.outputs = outputs - self.logger = logger - - def run(self, **kwargs): - raise NotImplementedError() diff --git a/fibratus/binding/yar.py b/fibratus/binding/yar.py deleted file mode 100644 index 57db325e6..000000000 --- a/fibratus/binding/yar.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright 2017 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from fibratus.binding.base import BaseBinding -from fibratus.errors import BindingError - -import os -import glob - -try: - import yara -except ImportError: - yara = None - - -class YaraBinding(BaseBinding): - - def __init__(self, outputs, logger, **config): - """Creates an instance of the YARA binding. - - This binding integrates with YARA tool to provide real time classification and pattern matching of the - process's binary images. The image path is extracted from the `ThreadInfo` instance after `CreateProcess` - kernel event has been captured. - - :param dict outputs: declared output adapters - :param logbook.Logger logger: reference to the logger implementation - :param dict config: configuration for this binding - """ - - BaseBinding.__init__(self, outputs, logger) - self._path = config.pop('path', None) - self._rules = None - if not yara: - raise BindingError('yara-python package is not installed') - if not os.path.exists(self._path) or not os.path.isdir(self._path): - raise BindingError('%s rules path does not exist' % - self._path) - try: - for file in glob.glob(os.path.join(self._path, '*.yar')): - self._rules = yara.compile(os.path.join(self._path, file)) - except yara.SyntaxError as e: - raise BindingError("rule compilation error %s" % e) - - def run(self, **kwargs): - """Apply the YARA rule set to process's image path. - - If a rule match occurs, the data with rule information, matching strings, process name, etc. is transported - over provided output implementation. If output type is not specified, the console output stream is used. - - :param dict kwargs: parameters for the binding context - """ - thread_info = kwargs.pop('thread_info', None) - kevent = kwargs.pop('kevent', None) - if thread_info: - def yara_callback(data): - matches = data['matches'] - if matches: - rule_context = { - 'rule_info': { - 'meta': data['meta'], - 'tags': data['tags'], - 'namespace': data['namespace'], - 'rule': data['rule'], - 'strings': [self.__string_meta(string) for string in data['strings']] - } - } - kevent.params.update(rule_context) - return yara.CALLBACK_CONTINUE - self._rules.match(thread_info.exe, callback=yara_callback) - - def __string_meta(self, string): - """Unpacks the tuple with matching string data and transforms it to a dictionary. - - :param tuple string: the tuple with matching string data - :return: dict: - """ - offset, ident, data = string - return { - 'offset': offset, - 'identifier': ident, - 'data': data.decode('utf-8') - } diff --git a/fibratus/cli.py b/fibratus/cli.py deleted file mode 100644 index 07a4cd373..000000000 --- a/fibratus/cli.py +++ /dev/null @@ -1,135 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Usage: - fibratus run ([--filament=] | [--filters ...]) - [--pid= | --image=] [--no-enum-handles] [--cswitch] - fibratus list-kevents - fibratus list-filaments - fibratus -h | --help - fibratus --version - -Options: - -h --help Show this screen. - --filament= Specify the filament to execute. - --no-enum-handles Avoids enumerating the system handles. - --pid= Spy on a specific process identifier. - --image= Spy on a specific image name. - --cswitch Enables context switch kernel events. - --version Show version. -""" -import sys - -from docopt import docopt - -from fibratus.apidefs.sys import set_console_ctrl_handler, PHANDLER_ROUTINE -from fibratus.errors import FilamentError -from fibratus.entrypoint import Fibratus -from fibratus.filament import Filament -from fibratus.kevent import KEvents -from fibratus.version import VERSION -from fibratus.common import panic, Tabular - -args = docopt(__doc__, version=VERSION) - -kevent_filters = args[''] -filament_name = args['--filament'] if args['--filament'] else None - - -def _check_kevent(kevent): - if kevent not in KEvents.all(): - panic('fibratus run: ERROR - %s is not a valid kernel event. Run list-kevents to see ' - 'the available kernel events' % kevent) - - -def main(): - if args['run']: - if len(kevent_filters) > 0 and not filament_name: - for kfilter in kevent_filters: - _check_kevent(kfilter) - - enum_handles = False if args['--no-enum-handles'] else True - cswitch = True if args['--cswitch'] else False - - filament = None - filament_filters = [] - - if filament_name: - if not Filament.exists(filament_name): - panic('fibratus run: ERROR - %s filament does not exist. Run list-filaments to see ' - 'the available filaments' % filament_name) - filament = Filament() - try: - filament.load_filament(filament_name) - except FilamentError as e: - panic('fibratus run: ERROR - %s' % e) - - filament_filters = filament.filters - - if len(filament_filters) > 0: - for kfilter in filament_filters: - _check_kevent(kfilter) - - filament.render_tabular() - - try: - fibratus = Fibratus(filament, enum_handles=enum_handles, cswitch=cswitch) - except KeyboardInterrupt: - # the user has stopped command execution - # before opening the kernel event stream - sys.exit(0) - - @PHANDLER_ROUTINE - def handle_ctrl_c(event): - if event == 0: - fibratus.stop_ktrace() - return 0 - set_console_ctrl_handler(handle_ctrl_c, True) - - # add specific filters - filters = dict() - filters['pid'] = args['--pid'] if args['--pid'] else None - filters['image'] = args['--image'] if args['--image'] else None - - if not filament: - if len(kevent_filters) > 0: - fibratus.add_filters(kevent_filters, **filters) - else: - fibratus.add_filters([], **filters) - else: - if len(filament_filters) > 0: - fibratus.add_filters(filament_filters, **filters) - else: - fibratus.add_filters([], **filters) - try: - fibratus.run() - except KeyboardInterrupt: - set_console_ctrl_handler(handle_ctrl_c, False) - - elif args['list-filaments']: - filaments = Tabular(['Filament', 'Description'], 'Description', - sort_by='Filament') - for filament, desc in Filament.list_filaments().items(): - filaments.add_row([filament, desc]) - filaments.draw() - - elif args['list-kevents']: - kevents = Tabular(['KEvent', 'Category', 'Description'], 'Description', - sort_by='Category') - for kevent, meta in KEvents.meta_info().items(): - kevents.add_row([kevent, meta[0].name, meta[1]]) - kevents.draw() diff --git a/fibratus/common.py b/fibratus/common.py deleted file mode 100644 index d7e7f830e..000000000 --- a/fibratus/common.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import sys -import re -from prettytable import PrettyTable - -__underscore_regex__ = re.compile('((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))') - - -NA = '' - - -def panic(msg): - """Write the message on the console and terminates the process. - - Parameters - ---------- - msg: str - the message to be written on the standard output stream - """ - print(msg) - sys.exit() - - -def underscore_dict_keys(in_dict): - if type(in_dict) is dict: - out_dict = {} - for key, item in in_dict.items(): - out_dict[__underscore_regex__.sub(r'_\1', key).lower()] = underscore_dict_keys(item) - return out_dict - elif type(in_dict) is list: - return [__underscore_regex__.sub(r'_\1', obj).lower() for obj in in_dict] - else: - return in_dict - - -class Tabular(PrettyTable): - - def __init__(self, columns, align_col=None, align_type='l', sort_by=None): - PrettyTable.__init__(self, columns) - if align_col: - self.align[align_col] = align_type - if sort_by: - self.sortby = sort_by - - def draw(self): - print(self.get_string()) - - -class DotD(dict): - """This code is borrowed from easydict - Credits to: - - https://github.com/makinacorpus/easydict/blob/master/easydict/__init__.py - """ - def __init__(self, d=None, **kwargs): - if d is None: - d = {} - if kwargs: - d.update(**kwargs) - for k, v in d.items(): - setattr(self, k, v) - # class attributes - for k in self.__class__.__dict__.keys(): - if not (k.startswith('__') and k.endswith('__')): - setattr(self, k, getattr(self, k)) - - def __setattr__(self, name, value): - if isinstance(value, (list, tuple)): - value = [self.__class__(x) - if isinstance(x, dict) else x for x in value] - else: - value = self.__class__(value) if isinstance(value, dict) else value - super(DotD, self).__setattr__(name, value) - super(DotD, self).__setitem__(name, value) - - __setitem__ = __setattr__ - diff --git a/fibratus/config.py b/fibratus/config.py deleted file mode 100644 index 40b819133..000000000 --- a/fibratus/config.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os -import sys -import anyconfig -from fibratus.common import panic, DotD as ddict -from pykwalify.core import Core - - -class YamlConfig(object): - """YAML based configuration reader. - - Reads the configuration from YAML file, and ensures the content satisfies the structure - as defined in the schema file. - """ - - def __init__(self, config_path=None): - self._default_config_path = os.path.join(os.path.expanduser('~'), '.fibratus', 'fibratus.yml') - self._default_schema_path = os.path.join(os.path.expanduser('~'), '.fibratus', 'schema.yml') - self.path = config_path or os.getenv('FIBRATUS_CONFIG_PATH', self._default_config_path) - self._yaml = None - - def load(self, validate=True): - schema_file = os.path.join(sys._MEIPASS, 'schema.yml') \ - if hasattr(sys, '_MEIPASS') else self._default_schema_path - try: - self._yaml = anyconfig.load(self.path, ignore_missing=False) - except FileNotFoundError: - panic('ERROR - %s configuration file does not exist' % self.path) - if validate: - validator = Core(source_file=self.path, schema_files=[schema_file]) - validator.validate(raise_exception=True) - - @property - def image_meta(self): - return ddict(self._yaml.pop('image_meta', {})) - - @property - def skips(self): - return ddict(self._yaml.pop('skips', {})) - - @property - def outputs(self): - return self._yaml.pop('output', None) - - @property - def bindings(self): - return self._yaml.pop('binding', None) - - @property - def yaml(self): - return self._yaml - - @property - def default_config_path(self): - return self._default_config_path - - @default_config_path.setter - def default_config_path(self, path): - self._default_config_path = path - - @property - def default_schema_path(self): - return self._default_schema_path - - @default_schema_path.setter - def default_schema_path(self, path): - self._default_schema_path = path - - @property - def config_path(self): - return self.path \ No newline at end of file diff --git a/fibratus/context_switch.py b/fibratus/context_switch.py deleted file mode 100644 index eb8be4a28..000000000 --- a/fibratus/context_switch.py +++ /dev/null @@ -1,405 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -import ctypes -import os -from ctypes.wintypes import MAX_PATH, DWORD -from enum import Enum - -from fibratus.apidefs.process import open_thread, get_process_id_of_thread, \ - THREAD_QUERY_INFORMATION, open_process, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ, \ - query_full_process_image_name -from fibratus.apidefs.sys import close_handle -from fibratus.common import NA - - -class ContextSwitchRegistry(object): - """Keeps the state of the context switches ocurring on the system. - - Once the CPU scheduler selects a new thread to execute, the context switch - registry tracks down a plethora of attributes like the new thread priority, - the old thread state, the wait reason, etc. It also keeps a counter on how many - context switches has been made for a particular thread and the logical cpu. - - """ - - def __init__(self, thread_registry, kevent): - self._css = {} - self._thread_registry = thread_registry - self._kevent = kevent - - def next_cswitch(self, cpu, ts, kcs, on_context_switch=None): - """Parses the context switch kernel events. - - Parameters - ---------- - cpu: int - the logical cpu where the context switch occurs - ts: str - the timestamp of the context switch - kcs: dict - the context switch info as forwarded - from the kstream collector - on_context_switch: callable - the callback to execute after the parsing stage - - """ - new_thread_id = int(kcs.new_thread_id, 16) - old_thread_id = int(kcs.old_thread_id, 16) - new_thread_wait_time = int(kcs.new_thread_wait_time, 16) - thread_cs = (cpu, new_thread_id,) - next_thread = self._thread_registry.get_thread(new_thread_id) - prev_thread = self._thread_registry.get_thread(old_thread_id) - - next_pid = next_thread.pid if next_thread else None - next_proc_name = next_thread.name if next_thread \ - else self._get_proc(new_thread_id) - prev_proc_name = prev_thread.name if prev_thread \ - else self._get_proc(old_thread_id) - - if thread_cs in self._css: - # if the thread has been previously scheduled - # on the same logical cpu, we can update its - # context switch info - cs = self._css[thread_cs] - cs.timestamp = ts - cs.prev_thread = prev_proc_name or NA - cs.next_thread_prio = kcs.new_thread_priority - cs.next_thread_wait_time = new_thread_wait_time - cs.prev_thread_prio = kcs.old_thread_priority - cs.prev_thread_state = ContextSwitchRegistry._human_thread_state(kcs.old_thread_state) - cs.prev_thread_wait_mode = ContextSwitchRegistry._human_wait_mode(kcs.old_thread_wait_mode) - cs.prev_thread_wait_reason = ContextSwitchRegistry._human_wait_reason(kcs.old_thread_wait_reason) - cs.increment_count() - else: - # the new thread has been scheduled - # add it to the registry of context - # switches - cs = CSwitch(ts, - next_proc_name or NA, - prev_proc_name or NA, - kcs.new_thread_priority, - new_thread_wait_time, - kcs.old_thread_priority, - ContextSwitchRegistry._human_thread_state(kcs.old_thread_state), - ContextSwitchRegistry._human_wait_mode(kcs.old_thread_wait_mode), - ContextSwitchRegistry._human_wait_reason(kcs.old_thread_wait_reason)) - cs.increment_count() - self._css[thread_cs] = cs - - if on_context_switch: - if next_proc_name: - on_context_switch(cpu, next_proc_name) - else: - on_context_switch(cpu, kcs.new_thread_id) - - self._kevent.tid = new_thread_id - self._kevent.pid = next_pid - params = { - 'next_proc_name': cs.next_proc_name, - 'prev_proc_name': cs.prev_proc_name, - 'cpu': cpu, - 'next_thread_id': new_thread_id, - 'prev_thread_id': old_thread_id, - 'next_thread_prio': cs.next_thread_prio, - 'prev_thread_prio': cs.prev_thread_prio, - 'prev_thread_state': cs.prev_thread_state.name if cs.prev_thread_state else NA, - 'next_thread_wait_time': cs.next_thread_wait_time, - 'prev_thread_wait_mode': cs.prev_thread_wait_mode.name if cs.prev_thread_wait_mode else NA, - 'prev_thread_wait_reason': cs.prev_thread_wait_reason.name if cs.prev_thread_wait_reason else NA - } - self._kevent.params = params - - def context_switches(self): - """Returns a dictionary of context switches. - """ - return self._css - - def _get_proc(self, thread_id): - handle = open_thread(THREAD_QUERY_INFORMATION, - False, - thread_id) - - if handle: - # if it was possible to get the process id - # which is the parent of the thread, we can - # try to get the process name from its pid - pid = get_process_id_of_thread(handle) - close_handle(handle) - handle = open_process(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, - False, - pid) - if handle: - exe = ctypes.create_unicode_buffer(MAX_PATH) - status = query_full_process_image_name(handle, 0, - exe, DWORD(MAX_PATH)) - close_handle(handle) - if status: - return os.path.basename(exe.value) - - @classmethod - def _human_thread_state(cls, thread_state): - if thread_state == ThreadState.INITIALIZED.value: - return ThreadState.INITIALIZED - elif thread_state == ThreadState.READY.value: - return ThreadState.READY - elif thread_state == ThreadState.RUNNING.value: - return ThreadState.RUNNING - elif thread_state == ThreadState.STANDBY.value: - return ThreadState.STANDBY - elif thread_state == ThreadState.TERMINATED.value: - return ThreadState.TERMINATED - elif thread_state == ThreadState.WAITING.value: - return ThreadState.WAITING - elif thread_state == ThreadState.TRANSITION.value: - return ThreadState.TRANSITION - elif thread_state == ThreadState.DEFERRED_READY.value: - return ThreadState.DEFERRED_READY - - @classmethod - def _human_wait_reason(cls, wait_reason): - if wait_reason == WaitReason.EXECUTIVE.value or wait_reason == WaitReason.EXECUTIVE.value + 7: - return WaitReason.EXECUTIVE - elif wait_reason == WaitReason.FREE_PAGE.value or wait_reason == WaitReason.FREE_PAGE.value + 7: - return WaitReason.FREE_PAGE - elif wait_reason == WaitReason.PAGE_IN.value or wait_reason == WaitReason.PAGE_IN.value + 7: - return WaitReason.PAGE_IN - elif wait_reason == WaitReason.POOL_ALLOCATION.value or wait_reason == WaitReason.POOL_ALLOCATION.value + 7: - return WaitReason.POOL_ALLOCATION - elif wait_reason == WaitReason.DELAY_EXECUTION.value or wait_reason == WaitReason.DELAY_EXECUTION.value + 7: - return WaitReason.DELAY_EXECUTION - elif wait_reason == WaitReason.SUSPENDED.value or wait_reason == WaitReason.SUSPENDED.value + 7: - return WaitReason.SUSPENDED - elif wait_reason == WaitReason.USER_REQUEST or wait_reason == WaitReason.USER_REQUEST.value + 7: - return WaitReason.USER_REQUEST - elif wait_reason == WaitReason.EVENT_PAIR.value: - return WaitReason.EVENT_PAIR - elif wait_reason == WaitReason.QUEUE.value: - return WaitReason.QUEUE - elif wait_reason == WaitReason.LPC_RECEIVE.value: - return WaitReason.LPC_RECEIVE - elif wait_reason == WaitReason.LPC_REPLY.value: - return WaitReason.LPC_REPLY - elif wait_reason == WaitReason.VIRTUAL_MEMORY.value: - return WaitReason.VIRTUAL_MEMORY - elif wait_reason == WaitReason.PAGE_OUT.value: - return WaitReason.PAGE_OUT - elif wait_reason == WaitReason.RENDEZVOUS.value: - return WaitReason.RENDEZVOUS - elif wait_reason == WaitReason.KEYED_EVENT.value: - return WaitReason.KEYED_EVENT - elif wait_reason == WaitReason.TERMINATED.value: - return WaitReason.TERMINATED - elif wait_reason == WaitReason.PROCESS_IN_SWAP.value: - return WaitReason.PROCESS_IN_SWAP - elif wait_reason == WaitReason.CPU_WAIT_CONTROL.value: - return WaitReason.CPU_WAIT_CONTROL - elif wait_reason == WaitReason.CALLOUT_STACK.value: - return WaitReason.CALLOUT_STACK - elif wait_reason == WaitReason.KERNEL.value: - return WaitReason.KERNEL - elif wait_reason == WaitReason.RESOURCE.value: - return WaitReason.RESOURCE - elif wait_reason == WaitReason.PUSH_LOCK.value: - return WaitReason.PUSH_LOCK - elif wait_reason == WaitReason.MUTEX.value: - return WaitReason.MUTEX - elif wait_reason == WaitReason.QUANTUM_END.value: - return WaitReason.QUANTUM_END - elif wait_reason == WaitReason.DISPATCH_INT.value: - return WaitReason.DISPATCH_INT - elif wait_reason == WaitReason.PREEMPTED.value: - return WaitReason.PREEMPTED - elif wait_reason == WaitReason.YIELD_EXECUTION.value: - return WaitReason.YIELD_EXECUTION - elif wait_reason == WaitReason.FAST_MUTEX.value: - return WaitReason.FAST_MUTEX - elif wait_reason == WaitReason.GUARDED_MUTEX.value: - return WaitReason.GUARDED_MUTEX - elif wait_reason == WaitReason.RUNDOWN.value: - return WaitReason.RUNDOWN - elif wait_reason == WaitReason.MAXIMUM_WAIT_REASON.value: - return WaitReason.MAXIMUM_WAIT_REASON - - @classmethod - def _human_wait_mode(cls, wait_mode): - if wait_mode == WaitMode.KERNEL.value: - return WaitMode.KERNEL - elif wait_mode == WaitMode.USER.value: - return WaitMode.USER - - -class CSwitch(object): - - def __init__(self, ts, next_proc_name, prev_proc_name, next_thread_prio, - next_thread_wait_time, prev_thread_prio, prev_thread_state, - prev_thread_wait_mode, - prev_thread_wait_reason): - """Context switch state info. - - Parameters - ---------- - ts: str - the timestamp of the context switch - next_proc_name: str - process name of the thread which is about to be scheduled - prev_proc_name: str - process name right before the context switch - next_thread_prio: int - the priority of the new thread - next_thread_wait_time: int - wait time for the new thread - prev_thread_prio: int - the priority of the old thread - prev_thread_state: Enum - state of the previous thread - prev_thread_wait_mode: Enum - the wait mode of the old thread - prev_thread_wait_reason: Enum - the wait reason of the previous thread - - """ - self._ts = ts - self._next_proc_name = next_proc_name - self._prev_proc_name = prev_proc_name - self._next_thread_prio = next_thread_prio - self._next_thread_wait_time = next_thread_wait_time - self._prev_thread_prio = prev_thread_prio - self._prev_thread_state = prev_thread_state - self._prev_thread_wait_mode = prev_thread_wait_mode - self._prev_thread_wait_reason = prev_thread_wait_reason - self._count = 0 - - @property - def timestamp(self): - return self._ts - - @timestamp.setter - def timestamp(self, ts): - self._ts = ts - - @property - def next_proc_name(self): - return self._next_proc_name - - @property - def prev_proc_name(self): - return self._prev_proc_name - - @property - def next_thread_prio(self): - return self._next_thread_prio - - @next_thread_prio.setter - def next_thread_prio(self, next_thread_prio): - self._next_thread_prio = next_thread_prio - - @property - def next_thread_wait_time(self): - return self._next_thread_wait_time - - @next_thread_wait_time.setter - def next_thread_wait_time(self, next_thread_wait_time): - self._next_thread_wait_time = next_thread_wait_time - - @property - def prev_thread_prio(self): - return self._prev_thread_prio - - @prev_thread_prio.setter - def prev_thread_prio(self, prev_thread_prio): - self._prev_thread_prio = prev_thread_prio - - @property - def prev_thread_state(self): - return self._prev_thread_state - - @prev_thread_state.setter - def prev_thread_state(self, prev_thread_state): - self._prev_thread_state = prev_thread_state - - @property - def prev_thread_wait_mode(self): - return self._prev_thread_wait_mode - - @prev_thread_wait_mode.setter - def prev_thread_wait_mode(self, prev_thread_wait_mode): - self._prev_thread_wait_mode = prev_thread_wait_mode - - @property - def prev_thread_wait_reason(self): - return self._prev_thread_wait_reason - - @prev_thread_wait_reason.setter - def prev_thread_wait_reason(self, prev_thread_wait_reason): - self._prev_thread_wait_reason = prev_thread_wait_reason - - @property - def count(self): - return self._count - - def increment_count(self): - self._count += 1 - - -class ThreadState(Enum): - """Possible thread states. - """ - INITIALIZED = 0 - READY = 1 - RUNNING = 2 - STANDBY = 3 - TERMINATED = 4 - WAITING = 5 - TRANSITION = 6 - DEFERRED_READY = 7 - - -class WaitMode(Enum): - KERNEL = 0 - USER = 1 - - -class WaitReason(Enum): - EXECUTIVE = 0 - FREE_PAGE = 1 - PAGE_IN = 2 - POOL_ALLOCATION = 3 - DELAY_EXECUTION = 4 - SUSPENDED = 5 - USER_REQUEST = 6 - EVENT_PAIR = 14 - QUEUE = 15 - LPC_RECEIVE = 16 - LPC_REPLY = 17 - VIRTUAL_MEMORY = 18 - PAGE_OUT = 19 - RENDEZVOUS = 20 - KEYED_EVENT = 21 - TERMINATED = 22 - PROCESS_IN_SWAP = 23 - CPU_WAIT_CONTROL = 24 - CALLOUT_STACK = 25 - KERNEL = 26 - RESOURCE = 27 - PUSH_LOCK = 28 - MUTEX = 29 - QUANTUM_END = 30 - DISPATCH_INT = 31 - PREEMPTED = 32 - YIELD_EXECUTION = 33 - FAST_MUTEX = 34 - GUARDED_MUTEX = 35 - RUNDOWN = 36 - MAXIMUM_WAIT_REASON = 37 diff --git a/fibratus/controller.py b/fibratus/controller.py deleted file mode 100644 index 48da194fc..000000000 --- a/fibratus/controller.py +++ /dev/null @@ -1,158 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from ctypes import addressof, byref, cast, memmove, sizeof, c_char, c_wchar -from ctypes import ArgumentError, pointer - -from fibratus.apidefs.cdefs import ERROR_ALREADY_EXISTS, ERROR_ACCESS_DENIED, ERROR_BAD_LENGTH, \ - ERROR_INVALID_PARAMETER, ERROR_SUCCESS -from fibratus.apidefs.etw import * -from fibratus.errors import FibratusError -from fibratus.common import panic - -class KTraceProps(object): - - def __init__(self, buffer_size=1024): - """Builds the tracing session properties. - - Parameters - --------- - - buffer_size: int - the amount of memory allocated for each trace buffer - """ - - # allocate buffer for the trace - self.max_string_len = 1024 - self.buff_size = sizeof(EVENT_TRACE_PROPERTIES) + 2 * sizeof(c_wchar) * self.max_string_len - - self._buff = (c_char * self.buff_size)() - self._props = cast(pointer(self._buff), POINTER(EVENT_TRACE_PROPERTIES)) - - # set trace properties - self._props.contents.wnode.buffer_size = self.buff_size - self._props.contents.wnode.guid = KERNEL_TRACE_CONTROL_GUID - self._props.contents.wnode.flags = WNODE_FLAG_TRACED_GUID - self._props.contents.logger_name_offset = sizeof(EVENT_TRACE_PROPERTIES) - self._props.contents.log_file_name_offset = 0 - self._props.contents.log_file_mode = PROCESS_TRACE_MODE_REAL_TIME - self._props.contents.buffer_size = buffer_size - - def enable_kflags(self, syscall=False, cswitch=False): - # enable the basic set of flags - # for the kernel events - self._props.contents.enable_flags = (EVENT_TRACE_FLAG_PROCESS | - EVENT_TRACE_FLAG_REGISTRY | - EVENT_TRACE_FLAG_THREAD | - EVENT_TRACE_FLAG_DISK_IO | - EVENT_TRACE_FLAG_DISK_FILE_IO | - EVENT_TRACE_FLAG_FILE_IO | - EVENT_TRACE_FLAG_FILE_IO_INIT | - EVENT_TRACE_FLAG_IMAGE_LOAD | - EVENT_TRACE_FLAG_NETWORK_TCPIP) - - # syscall / cswitch flags generate a LOT of kevents - # and they are disabled by default - if syscall: - self._props.contents.enable_flags |= (EVENT_TRACE_FLAG_SYSTEMCALL | EVENT_TRACE_FLAG_CSWITCH) - if cswitch: - self._props.contents.enable_flags |= EVENT_TRACE_FLAG_CSWITCH - - def get(self): - return self._props - - @property - def logger_name(self): - return c_wchar_p(addressof(self._props.contents) + - self._props.contents.logger_name_offset) - - @logger_name.setter - def logger_name(self, logger_name): - name_len = len(logger_name) + 1 - if self.max_string_len < name_len: - raise ArgumentError("Logger name %s is too long" % logger_name) - props = self._props - logger = c_wchar_p(addressof(props.contents) + props.contents.logger_name_offset) - memmove(logger, c_wchar_p(logger_name), sizeof(c_wchar) * name_len) - - -class KTraceController(object): - """Controls the life cycle of the kernel traces. - - """ - - def __init__(self): - self._handle = TRACEHANDLE() - self._trace_name = None - - def __del__(self): - if self._handle: - self.stop_ktrace() - - def start_ktrace(self, name, kprops): - """Starts a new trace. - - Parameters - --------- - - name: str - the name for the trace session - kprops: KTraceProps - an instance of the kernel trace properties - """ - self._trace_name = name - handle = TRACEHANDLE() - kp = kprops.get() - status = start_trace(byref(handle), - self._trace_name, - kp) - self._handle = handle - if status == ERROR_ALREADY_EXISTS: - # the kernel logger trace session - # is already running. Restart the trace. - self.stop_ktrace() - status = start_trace(byref(handle), - self._trace_name, - kp) - if status != ERROR_SUCCESS: - raise FibratusError('Unable to start fibratus') - self._handle = handle - elif status == ERROR_ACCESS_DENIED: - # insufficient privileges - panic("You don't have administrative privileges. Stopping fibratus...") - elif status == ERROR_BAD_LENGTH: - raise FibratusError('Incorrect buffer size for the trace buffer') - elif status == ERROR_INVALID_PARAMETER: - raise FibratusError('Invalid trace handle or provider GUID') - elif status != ERROR_SUCCESS: - raise FibratusError('Unable to start fibratus') - - def stop_ktrace(self, kprops=None): - """Stops the current running trace. - - Parameters - --------- - kprops: KTraceProps - an instance of the kernel trace properties - """ - kprops = kprops or KTraceProps() - - handle = self._handle - self._handle = TRACEHANDLE() - control_trace(handle, - self._trace_name, - kprops.get(), - EVENT_TRACE_CONTROL_STOP) diff --git a/fibratus/dll.py b/fibratus/dll.py deleted file mode 100644 index 7fe42afa0..000000000 --- a/fibratus/dll.py +++ /dev/null @@ -1,138 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os - - -class DllRepository(object): - - def __init__(self, kevent): - self.dlls = {} - self._kevent = kevent - - def register_dll(self, kdll): - """Registers a loaded image. - - Registers an image when - the latter is loaded into the address space - of the process. - - Parameters - ---------- - - kdll: dict - Image load event payload as forwarded - from the kernel event stream collector - """ - pid = kdll.process_id - path = kdll.file_name - image = os.path.basename(path) - size = kdll.image_size - checksum = kdll.image_checksum - base = kdll.image_base - - self._kevent.pid = pid - - dll = Dll(pid, path, - image, - size, - checksum, - base) - - if pid in self.dlls: - # append a new image to - # the associated process - self.dlls[pid].append(dll) - else: - self.dlls[pid] = [dll] - - self._kevent.params = dict(image=image, - pid=pid, - path=path, - size=dll.size, - checksum=checksum, - base=hex(base)) - - def unregister_dll(self, kdll): - """Unregisters a loaded image. - - Removes the loaded image from - the repository for a given process. - - Parameters - ---------- - - kdll: dict - Image unload event payload as forwarded - from the kernel event stream collector - """ - pid = kdll.process_id - path = kdll.file_name - image = os.path.basename(path) - size = kdll.image_size / 1024 - checksum = kdll.image_checksum - base = kdll.image_base - - self._kevent.pid = pid - - if pid in self.dlls: - dlls = self.dlls[pid] - for dll in dlls: - if dll.image == image: - dlls.remove(dll) - self._kevent.params = dict(image=image, - pid=pid, - path=path, - size=size, - checksum=checksum, - base=hex(base)) - - def dlls_for_process(self, pid): - return self.dlls[pid] if pid in self.dlls else [] - - -class Dll(object): - - def __init__(self, pid, path, image, size, checksum, base): - self._pid = pid - self._path = path - self._size = size - self._checksum = checksum - self._base = base - self._image = image - - @property - def pid(self): - return self._pid - - @property - def path(self): - return self._path - - @property - def image(self): - return self._image - - @property - def size(self): - return self._size - - @property - def base(self): - return hex(self._base) - - @property - def checksum(self): - return self._checksum diff --git a/fibratus/entrypoint.py b/fibratus/entrypoint.py deleted file mode 100644 index 737593f6f..000000000 --- a/fibratus/entrypoint.py +++ /dev/null @@ -1,435 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# http://rabbitstack.github.io -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import atexit -import os -import sys -from datetime import datetime - -from kstreamc import KEventStreamCollector -from pykwalify.errors import SchemaError - -from fibratus.binding.yar import YaraBinding -from fibratus.errors import BindingError -from fibratus.image_meta import ImageMetaRegistry -from fibratus.output.aggregator import OutputAggregator -from fibratus.output.console import ConsoleOutput -from fibratus.output.elasticsearch import ElasticsearchOutput -from fibratus.output.fs import FsOutput -from fibratus.output.smtp import SmtpOutput -from logbook import Logger, FileHandler, StreamHandler - -import fibratus.apidefs.etw as etw -from fibratus.common import DotD as ddict, panic -from fibratus.config import YamlConfig -from fibratus.context_switch import ContextSwitchRegistry -from fibratus.controller import KTraceController, KTraceProps -from fibratus.dll import DllRepository -from fibratus.fs import FsIO -from fibratus.handle import HandleRepository -from fibratus.kevent import KEvent -from fibratus.kevent_types import * -from fibratus.tcpip.tcpip import TcpIpParser -from fibratus.output.amqp import AmqpOutput -from fibratus.registry import HiveParser -from fibratus.thread import ThreadRegistry - - -class Fibratus(object): - - """Fibratus entrypoint. - - Setup the core components including the kernel - event stream collector and the tracing controller. - At this point the system handles are also being - enumerated. - - """ - def __init__(self, filament, **kwargs): - - self._start = datetime.now() - try: - log_path = os.path.join(os.path.expanduser('~'), '.fibratus', 'fibratus.log') - FileHandler(log_path, mode='w+').push_application() - StreamHandler(sys.stdout, bubble=True).push_application() - except PermissionError: - panic("ERROR - Unable to open log file for writing due to permission error") - - self.logger = Logger(Fibratus.__name__) - self.logger.info('Starting Fibratus...') - - self._config = YamlConfig() - self.logger.info('Loading configuration from [%s]' % self._config.config_path) - try: - self._config.load() - except SchemaError as e: - panic('Invalid configuration file. %s' % e.msg) - - enable_cswitch = kwargs.pop('cswitch', False) - - self.kcontroller = KTraceController() - self.ktrace_props = KTraceProps() - self.ktrace_props.enable_kflags(cswitch=enable_cswitch) - self.ktrace_props.logger_name = etw.KERNEL_LOGGER_NAME - - enum_handles = kwargs.pop('enum_handles', True) - - self.handle_repository = HandleRepository() - self._handles = [] - # query for handles on the - # start of the kernel trace - if enum_handles: - self.logger.info('Enumerating system handles...') - self._handles = self.handle_repository.query_handles() - self.logger.info('%s handles found' % len(self._handles)) - self.handle_repository.free_buffers() - - image_meta_config = self._config.image_meta - self.image_meta_registry = ImageMetaRegistry(image_meta_config.enabled, image_meta_config.imports, - image_meta_config.file_info) - - self.thread_registry = ThreadRegistry(self.handle_repository, self._handles, - self.image_meta_registry) - - self.kevt_streamc = KEventStreamCollector(etw.KERNEL_LOGGER_NAME.encode()) - skips = self._config.skips - image_skips = skips.images if 'images' in skips else [] - if len(image_skips) > 0: - self.logger.info("Adding skips for images %s" % image_skips) - for skip in image_skips: - self.kevt_streamc.add_skip(skip) - - self.kevent = KEvent(self.thread_registry) - - self._output_classes = dict(console=ConsoleOutput, - amqp=AmqpOutput, - smtp=SmtpOutput, - elasticsearch=ElasticsearchOutput, - fs=FsOutput) - self._outputs = self._construct_outputs() - self.output_aggregator = OutputAggregator(self._outputs) - - self._binding_classes = dict(yara=YaraBinding) - self._bindings = self._construct_bindings() - - if filament: - filament.logger = self.logger - filament.do_output_accessors(self._outputs) - self._filament = filament - - self.fsio = FsIO(self.kevent, self._handles) - self.hive_parser = HiveParser(self.kevent, self.thread_registry) - self.tcpip_parser = TcpIpParser(self.kevent) - self.dll_repository = DllRepository(self.kevent) - self.context_switch_registry = ContextSwitchRegistry(self.thread_registry, self.kevent) - - self.output_kevents = {} - self.filters_count = 0 - - def run(self): - - @atexit.register - def _exit(): - self.stop_ktrace() - - self.kcontroller.start_ktrace(etw.KERNEL_LOGGER_NAME, self.ktrace_props) - - def on_kstream_open(): - if self._filament is None: - delta = datetime.now() - self._start - self.logger.info('Started in %sm:%02ds.%s' % (int(delta.total_seconds() / 60), delta.seconds, - int(delta.total_seconds() * 1000))) - else: - self.logger.info('Running [%s] filament...' % self._filament.name) - self.kevt_streamc.set_kstream_open_callback(on_kstream_open) - self._open_kstream() - - def _open_kstream(self): - try: - self.kevt_streamc.open_kstream(self._on_next_kevent) - except Exception as e: - self.logger.error(e) - except KeyboardInterrupt: - self.stop_ktrace() - - def _construct_outputs(self): - """Instantiates output classes. - - Builds the dictionary with instances - of the output classes. - """ - outputs = {} - output_configs = self._config.outputs - if not output_configs: - return outputs - for output in output_configs: - name = next(iter(list(output.keys())), None) - if name and \ - name in self._output_classes.keys(): - # get the output configuration - # and instantiate its class - output_config = output[name] - self.logger.info("Deploying [%s] output - [%s]" - % (name, {k: v for k, v in output_config.items() - if 'password' not in k})) - output_class = self._output_classes[name] - outputs[name] = output_class(**output_config) - return outputs - - def _construct_bindings(self): - """Builds binding classes. - - :return: dict: dictionary with instances of the binding classes - """ - bindings = {} - binding_configs = self._config.bindings - if not binding_configs: - return bindings - for b in binding_configs: - name = next(iter(list(b.keys())), None) - if name and \ - name in self._binding_classes.keys(): - binding_config = b[name] - self.logger.info("Starting [%s] binding - [%s]" % - (name, binding_config)) - binding_class = self._binding_classes[name] - try: - binding = binding_class(self._outputs, self.logger, - **binding_config) - bindings[name] = binding - except BindingError as e: - self.logger.error("Couldn't start [%s] binding. Reason: %s" % - (name, e)) - return bindings - - def __find_binding(self, name): - return self._bindings[name] if name in self._bindings else None - - def stop_ktrace(self): - self.logger.info('Stopping fibratus...') - if self._filament: - self._filament.close() - self.kcontroller.stop_ktrace(self.ktrace_props) - self.kevt_streamc.close_kstream() - - def add_filters(self, kevent_filters, **kwargs): - self.kevt_streamc.add_pid_filter(kwargs.pop('pid', None)) - self.kevt_streamc.add_image_filter(kwargs.pop('image', None)) - if len(kevent_filters) > 0: - self.filters_count = len(kevent_filters) - # include the basic filters - # that are essential to the - # rest of kernel events - self.kevt_streamc.add_ktuple_filter(ENUM_PROCESS) - self.kevt_streamc.add_ktuple_filter(ENUM_THREAD) - self.kevt_streamc.add_ktuple_filter(ENUM_IMAGE) - self.kevt_streamc.add_ktuple_filter(REG_CREATE_KCB) - self.kevt_streamc.add_ktuple_filter(REG_DELETE_KCB) - - # these kevents are necessary for consistent state - # of the trace. If the user doesn't include them - # in a filter list, then we do the job but set the - # kernel event type as not eligible for rendering - if KEvents.CREATE_PROCESS not in kevent_filters: - self.kevt_streamc.add_ktuple_filter(CREATE_PROCESS) - self.output_kevents[CREATE_PROCESS] = False - else: - self.output_kevents[CREATE_PROCESS] = True - - if KEvents.CREATE_THREAD not in kevent_filters: - self.kevt_streamc.add_ktuple_filter(CREATE_THREAD) - self.output_kevents[CREATE_THREAD] = False - else: - self.output_kevents[CREATE_THREAD] = True - - if KEvents.TERMINATE_PROCESS not in kevent_filters: - self.kevt_streamc.add_ktuple_filter(TERMINATE_PROCESS) - self.output_kevents[TERMINATE_PROCESS] = False - else: - self.output_kevents[TERMINATE_PROCESS] = True - - if KEvents.TERMINATE_THREAD not in kevent_filters: - self.kevt_streamc.add_ktuple_filter(TERMINATE_THREAD) - self.output_kevents[TERMINATE_THREAD] = False - else: - self.output_kevents[TERMINATE_THREAD] = True - - for kevent_filter in kevent_filters: - ktuple = kname_to_tuple(kevent_filter) - if isinstance(ktuple, list): - for kt in ktuple: - self.kevt_streamc.add_ktuple_filter(kt) - if kt not in self.output_kevents: - self.output_kevents[kt] = True - else: - self.kevt_streamc.add_ktuple_filter(ktuple) - if ktuple not in self.output_kevents: - self.output_kevents[ktuple] = True - - def _on_next_kevent(self, ktype, cpuid, ts, kparams): - """Callback which fires when new kernel event arrives. - - This callback is invoked for every new kernel event - forwarded from the kernel stream collector. - - Parameters - ---------- - - ktype: tuple - Kernel event type. - cpuid: int - Indentifies the CPU core where the event - has been captured. - ts: str - Temporal reference of the kernel event. - kparams: dict - Kernel event's parameters. - """ - - # initialize kernel event properties - self.kevent.ts = ts - self.kevent.cpuid = cpuid - self.kevent.name = ktuple_to_name(ktype) - kparams = ddict(kparams) - - # thread / process kernel events - if ktype in [CREATE_PROCESS, - CREATE_THREAD, - ENUM_PROCESS, - ENUM_THREAD]: - self.thread_registry.add_thread(ktype, kparams) - if ktype in [CREATE_PROCESS, CREATE_THREAD]: - self.thread_registry.init_thread_kevent(self.kevent, - ktype, - kparams) - # apply yara binding by matching against the process's image path - if ktype == CREATE_PROCESS: - yara_binding = self.__find_binding('yara') - pid = int(kparams.process_id, 16) - thread = self.thread_registry.get_thread(pid) - if thread and yara_binding: - yara_binding.run(thread_info=thread, - kevent=self.kevent) - self._aggregate(ktype) - - elif ktype in [TERMINATE_PROCESS, TERMINATE_THREAD]: - self.thread_registry.init_thread_kevent(self.kevent, - ktype, - kparams) - self._aggregate(ktype) - self.thread_registry.remove_thread(ktype, kparams) - - # file system/disk kernel events - elif ktype in [CREATE_FILE, - DELETE_FILE, - CLOSE_FILE, - READ_FILE, - WRITE_FILE, - RENAME_FILE, - SET_FILE_INFORMATION]: - self.fsio.parse_fsio(ktype, kparams) - self._aggregate(ktype) - - # dll kernel events - elif ktype in [LOAD_IMAGE, ENUM_IMAGE]: - self.dll_repository.register_dll(kparams) - if ktype == LOAD_IMAGE: - self._aggregate(ktype) - elif ktype == UNLOAD_IMAGE: - self.dll_repository.unregister_dll(kparams) - self._aggregate(ktype) - # - # # registry kernel events - elif ktype == REG_CREATE_KCB: - self.hive_parser.add_kcb(kparams) - elif ktype == REG_DELETE_KCB: - self.hive_parser.remove_kcb(kparams.key_handle) - - elif ktype in [REG_CREATE_KEY, - REG_DELETE_KEY, - REG_OPEN_KEY, - REG_QUERY_KEY, - REG_SET_VALUE, - REG_DELETE_VALUE, - REG_QUERY_VALUE]: - self.hive_parser.parse_hive(ktype, kparams) - self._aggregate(ktype) - - # network kernel events - elif ktype in [SEND_SOCKET_TCPV4, - SEND_SOCKET_UDPV4, - RECV_SOCKET_TCPV4, - RECV_SOCKET_UDPV4, - ACCEPT_SOCKET_TCPV4, - CONNECT_SOCKET_TCPV4, - DISCONNECT_SOCKET_TCPV4, - RECONNECT_SOCKET_TCPV4]: - self.tcpip_parser.parse_tcpip(ktype, kparams) - self._aggregate(ktype) - - # context switch events - elif ktype == CONTEXT_SWITCH: - self.context_switch_registry.next_cswitch(cpuid, ts, kparams) - self._aggregate(ktype) - - if self._filament: - if ktype not in [ENUM_PROCESS, - ENUM_THREAD, - ENUM_IMAGE, - REG_CREATE_KCB, - REG_DELETE_KCB]: - ok = self.output_kevents[ktype] if ktype in self.output_kevents \ - else False - if self.kevent.name and ok: - thread = self.kevent.thread - kevent = { - 'params': self.kevent.params, - 'name': self.kevent.name, - 'pid': self.kevent.pid, - 'tid': self.kevent.tid, - 'timestamp': self.kevent.ts, - 'cpuid': self.kevent.cpuid, - 'category': self.kevent.category - } - if thread: - kevent.update({ - 'thread': { - 'name': thread.name, - 'exe': thread.exe, - 'comm': thread.comm, - 'pid': thread.pid, - 'ppid': thread.ppid - } - }) - self._filament.on_next_kevent(kevent) - - def _aggregate(self, ktype): - """Aggregates the kernel event to the output sink. - - Parameters - ---------- - - ktype: tuple - Identifier of the kernel event - """ - if not self._filament: - if ktype in self.output_kevents: - if self.output_kevents[ktype]: - self.kevent.inc_kid() - self.output_aggregator.aggregate(self.kevent) - elif self.filters_count == 0: - self.kevent.inc_kid() - self.output_aggregator.aggregate(self.kevent) \ No newline at end of file diff --git a/fibratus/errors.py b/fibratus/errors.py deleted file mode 100644 index 1e2349b32..000000000 --- a/fibratus/errors.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -class KTraceError(Exception): - pass - - -class FibratusError(Exception): - pass - - -class FilamentError(Exception): - pass - - -class TermInitializationError(Exception): - pass - - -class UnknownKeventTypeError(Exception): - - def __init__(self, kevent): - Exception.__init__(self, '%s cannot be recognized as a valid kernel event' - % kevent) - - -class HandleEnumError(Exception): - - def __init__(self, status): - Exception.__init__(self, 'Unable to enumerate handles. Error code %s' - % status) - - -class InvalidPayloadError(Exception): - pass - - -class BindingError(Exception): - pass - diff --git a/fibratus/filament.py b/fibratus/filament.py deleted file mode 100644 index 9d214f556..000000000 --- a/fibratus/filament.py +++ /dev/null @@ -1,321 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import inspect -import traceback -import os -import sys -from importlib.machinery import SourceFileLoader - -from apscheduler.executors.pool import ThreadPoolExecutor -from apscheduler.schedulers.background import BackgroundScheduler -from apscheduler.triggers.interval import IntervalTrigger - -from fibratus.common import DotD as ddict, Tabular -from fibratus.common import panic -from fibratus.errors import FilamentError, TermInitializationError -from fibratus.term import AnsiTerm - - -FILAMENTS_DIR = os.getenv('FILAMENTS_PATH', os.path.join(os.path.expanduser('~'), '.fibratus', 'filaments')) - - -class OutputAccessor(object): - """An accessor for the output meta variable. - - It represents an output accessor which is injected into - every filament module. - """ - def __init__(self, output): - self._output = output - - def emit(self, body, **kwargs): - self._output.emit(body, **kwargs) - - -class Filament(object): - """Filament initialization and execution engine. - - Filaments are lightweight Python modules which run - on top of Fibratus. They are often used to enrich/extend the - functionality of Fibratus by performing any type of logic - (aggregations, groupings, filters, counters, etc) on the - kernel event stream. - - """ - def __init__(self): - """Builds a new instance of the filament. - - Attributes: - ---------- - - filament_module: module - module which contains the filament logic - """ - self._filament_module = None - self._name = None - self._filters = [] - self._cols = [] - self._tabular = None - self._limit = 10 - self._interval = 1 - self._sort_by = None - self._sort_desc = True - self._logger = None - self._ansi_term = AnsiTerm() - self.scheduler = BackgroundScheduler() - - def load_filament(self, name): - """Loads the filament module. - - Finds and loads the python module which - holds the filament logic. It also looks up for - some essential filament methods and raises an error - if they can't be found. - - Parameters - ---------- - name: str - name of the filament to load - - """ - self._name = name - Filament._assert_root_dir() - filament_path = self._find_filament_path(name) - if filament_path: - loader = SourceFileLoader(name, filament_path) - self._filament_module = loader.load_module() - sys.path.append(FILAMENTS_DIR) - doc = inspect.getdoc(self._filament_module) - if not doc: - raise FilamentError('Please provide a short ' - 'description for the filament') - - on_next_kevent = self._find_filament_func('on_next_kevent') - if on_next_kevent: - if self._num_args(on_next_kevent) != 1: - raise FilamentError('Missing one argument on_next_kevent ' - 'method on filament') - self._initialize_funcs() - else: - raise FilamentError('Missing required on_next_kevent ' - 'method on filament') - else: - raise FilamentError('%s filament not found' % name) - - def _initialize_funcs(self): - """Setup the filament modules functions. - - Functions - --------- - - set_filter: func - accepts the comma separated list of kernel events - for whose the filter should be applied - set_interval: func - establishes the fixed repeating interval in seconds - columns: func - configure the column set for the table - add_row: func - adds a new row to the table - sort_by: func - sorts the table by specific column - """ - - def set_filter(*args): - self._filters = args - self._filament_module.set_filter = set_filter - - def set_interval(interval): - if not type(interval) is int: - raise FilamentError('Interval must be an integer value') - self._interval = interval - self._filament_module.set_interval = set_interval - - def columns(cols): - if not isinstance(cols, list): - raise FilamentError('Columns must be a list, ' - '%s found' % type(cols)) - self._cols = cols - self._tabular = Tabular(self._cols) - self._tabular.padding_width = 10 - self._tabular.junction_char = '|' - - def add_row(row): - if not isinstance(row, list): - raise FilamentError('Expected list type for the row, found %s' - % type(row)) - self._tabular.add_row(row) - - def sort_by(col, sort_desc=True): - if len(self._cols) == 0: - raise FilamentError('Expected at least 1 column but 0 found') - if col not in self._cols: - raise FilamentError('%s column does not exist' % col) - self._sort_by = col - self._sort_desc = sort_desc - - def limit(l): - if len(self._cols) == 0: - raise FilamentError('Expected at least 1 column but 0 found') - if not type(l) is int: - raise FilamentError('Limit must be an integer value') - self._limit = l - - def title(text): - self._tabular.title = text - - self._filament_module.columns = columns - self._filament_module.title = title - self._filament_module.sort_by = sort_by - self._filament_module.limit = limit - self._filament_module.add_row = add_row - self._filament_module.render_tabular = self.render_tabular - - on_init = self._find_filament_func('on_init') - if on_init and self._zero_args(on_init): - self._filament_module.on_init() - if self._find_filament_func('on_interval'): - self.scheduler.add_executor(ThreadPoolExecutor(max_workers=4)) - self.scheduler.start() - - def on_interval(): - try: - self._filament_module.on_interval() - except Exception: - self._logger.error('Unexpected error on interval elapsed %s' - % traceback.format_exc()) - self.scheduler.add_job(on_interval, - IntervalTrigger(), - seconds=self._interval, - max_instances=4, - misfire_grace_time=60) - if len(self._cols) > 0: - try: - self._ansi_term.setup_console() - except TermInitializationError: - panic('fibratus run: ERROR - console initialization failed') - - def do_output_accessors(self, outputs): - """Creates the filament's output accessors. - - Parameters - ---------- - - outputs: dict - outputs initialized from the configuration - descriptor - """ - for name, output in outputs.items(): - setattr(self._filament_module, name, OutputAccessor(output)) - - def on_next_kevent(self, kevent): - try: - self._filament_module.on_next_kevent(ddict(kevent)) - except Exception as e: - self._logger.error('Unexpected filament error %s' % e) - - def render_tabular(self): - """Renders the table on the console. - """ - if len(self._cols) > 0: - tabular = self._tabular.get_string(start=1, end=self._limit) - if self._sort_by: - tabular = self._tabular.get_string(start=1, end=self._limit, - sortby=self._sort_by, - reversesort=self._sort_desc) - self._tabular.clear_rows() - self._ansi_term.write_output(tabular) - - def close(self): - on_stop = self._find_filament_func('on_stop') - if on_stop and self._zero_args(on_stop): - self._filament_module.on_stop() - if self.scheduler.running: - self.scheduler.shutdown() - self._ansi_term.restore_console() - - @classmethod - def exists(cls, filament): - Filament._assert_root_dir() - return os.path.exists(os.path.join(FILAMENTS_DIR, '%s.py' % filament)) - - @classmethod - def list_filaments(cls): - Filament._assert_root_dir() - filaments = {} - paths = [os.path.join(FILAMENTS_DIR, path) for path in os.listdir(FILAMENTS_DIR) - if path.endswith('.py')] - for path in paths: - filament_name = os.path.basename(path)[:-3] - loader = SourceFileLoader(filament_name, path) - filament = loader.load_module() - filaments[filament_name] = inspect.getdoc(filament) - return filaments - - @classmethod - def _assert_root_dir(cls): - if not os.path.exists(FILAMENTS_DIR): - panic('fibratus run: ERROR - %s path does not exist.' % FILAMENTS_DIR) - - @property - def filters(self): - return self._filters - - @property - def logger(self): - return self._logger - - @logger.setter - def logger(self, logger): - self._logger = logger - - @property - def filament_module(self): - return self._filament_module - - @property - def name(self): - return self._name - - def _find_filament_func(self, func_name): - """Finds the function in the filament module. - - Parameters - ---------- - - func_name: str - the name of the function - """ - functions = inspect.getmembers(self._filament_module, predicate=inspect.isfunction) - return next(iter([func for name, func in functions if name == func_name]), None) - - def _find_filament_path(self, filament_name): - """Resolves the filament full path from the name - - Parameters - ---------- - - filament_name: str - the name of the filament whose path if about to be resolved - """ - return next(iter([os.path.join(FILAMENTS_DIR, filament) for filament in os.listdir(FILAMENTS_DIR) - if filament.endswith('.py') and filament_name == filament[:-3]]), None) - - def _num_args(self, func): - return len(inspect.getargspec(func).args) - - def _zero_args(self, func): - return self._num_args(func) == 0 diff --git a/fibratus/fs.py b/fibratus/fs.py deleted file mode 100644 index 841805328..000000000 --- a/fibratus/fs.py +++ /dev/null @@ -1,223 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from enum import Enum - -from fibratus.common import NA -from fibratus.handle import HandleType -from fibratus.kevent_types import * -from fibratus.apidefs.fs import * - - -class FileOps(Enum): - - # if the file already exists, - # replace it with the given file - # otherwise create the given file - SUPERSEDE = 0 - # if the file already exists, - # open it instead of creating a new file - OPEN = 1 - # if the file already exists, - # fail the request otherwise create the file - CREATE = 2 - # if the file already exists, - # open it, otherwise create the given file - OPEN_IF = 3 - # if the file already exists, - # open it and overwrite it, - # otherwise fail the request - OVERWRITE = 4 - # if the file already exists, - # open it and overwrite it, - # otherwise create the given file - OVERWRITE_IF = 5 - - -class FileType(Enum): - - FILE = 0 - DIRECTORY = 1 - REPARSE_POINT = 2 - UNKNOWN = 3 - - -class FsIO(object): - - def __init__(self, kevent, handles): - self._kevent = kevent - self.file_pool = {} - self.file_handles = {handle.obj: (handle.name, handle.handle_type, handle.handle) - for handle in handles - if handle.handle_type in [HandleType.FILE, HandleType.DIRECTORY]} - - def parse_fsio(self, ketype, kfsio): - """Parses the file system related kevents. - - Parameters - ---------- - - ketype: tuple - kevent type - kfsio: dict - kevent payload as forwarded from - """ - - # thread which is perfoming the op - tid = kfsio.ttid - pid = kfsio.process_id - obj = kfsio.file_object - self._kevent.tid = tid - self._kevent.pid = pid - # creates or opens a file or the I/O device. - # The device can be a file, file stream, directory, - # physical disk, volume, console buffer, tape drive, - # communications resource, mailslot, or pipe. - if ketype == CREATE_FILE: - file = kfsio.open_path - # the high 8 bits correspond to the value of the - # `CreateDisposition` parameter and the low 24 bits - # are the value of the `CreateOptions` parameter - # of the `NtCreateFile` system call - co = kfsio.create_options - - # extract the most significat 8 bits - flags = (co >> 24) & ((1 << 8) - 1) - - op = FileOps.OPEN - if flags == FILE_SUPERSEDE: - op = FileOps.SUPERSEDE - elif flags == FILE_OPEN: - op = FileOps.OPEN - elif flags == FILE_CREATE: - op = FileOps.CREATE - elif flags == FILE_OPEN_IF: - op = FileOps.OPEN_IF - elif flags == FILE_OVERWRITE: - op = FileOps.OVERWRITE - elif flags == FILE_OVERWRITE_IF: - op = FileOps.OVERWRITE_IF - - # determine file descriptor type - file_type = FileType.FILE - if (co & FILE_DIRECTORY_FILE) == FILE_DIRECTORY_FILE: - file_type = FileType.DIRECTORY - elif (co & FILE_OPEN_REPARSE_POINT) == FILE_OPEN_REPARSE_POINT: - file_type = FileType.REPARSE_POINT - - share_mask = self._resolve_share_mask(kfsio.share_access) - params = { - 'file': file, - 'file_type': file_type.name, - 'file_object': obj, - 'tid': tid, - 'pid': pid, - 'operation': op.name, - 'share_mask': share_mask - } - self._kevent.params = params - - # index by file object pointer - # so we can query the pool - # to resolve the file name - self.file_pool[obj] = file - - elif ketype == DELETE_FILE or ketype == CLOSE_FILE: - file = self._query_file_name(obj, True) - params = { - 'file': file, - 'file_object': obj, - 'pid': pid, - 'tid': tid - } - self._kevent.params = params - elif ketype == WRITE_FILE or ketype == READ_FILE: - # the number of kb read/written - io_size = kfsio.io_size / 1024 - file = self._query_file_name(obj) - params = { - 'file': file, - 'file_object': obj, - 'pid': pid, - 'tid': tid, - 'io_size': io_size - } - self._kevent.params = params - elif ketype == RENAME_FILE: - file = self._query_file_name(obj) - params = { - 'file': file, - 'file_object': obj, - 'pid': pid, - 'tid': tid - } - self._kevent.params = params - if NA not in file: - self.file_pool[obj] = file - elif ketype == SET_FILE_INFORMATION: - file = self._query_file_name(obj) - params = { - 'file': file, - 'file_object': obj, - 'pid': pid, - 'tid': tid, - 'info_class': kfsio.info_class - } - self._kevent.params = params - - def _query_file_name(self, fobj, remove=False): - if fobj in self.file_pool: - return self.file_pool.pop(fobj) if remove \ - else self.file_pool[fobj] - else: - # couldn't find the file in the file pool, - # query the file handles - if fobj in self.file_handles: - file, _, _ = self.file_handles.pop(fobj) - if file and not remove: - self.file_pool[fobj] = file - return file if file else NA - else: - return NA - - def _resolve_share_mask(self, share_access): - """Resolves the share mask. - - Resolves the type of share access that - the caller would like to use in the file. - - For example, `FILE_SHARE_READ` would allow other - threads to open the file for read access. - - :param str share_access: the value of the share access - :return: str: resolved share mask - """ - - if share_access == FILE_SHARE_READ: - return 'r--' - elif share_access == FILE_SHARE_WRITE: - return '-w-' - elif share_access == FILE_SHARE_DELETE: - return '--d' - elif share_access == (FILE_SHARE_READ | FILE_SHARE_WRITE): - return 'rw-' - elif share_access == (FILE_SHARE_READ | FILE_SHARE_DELETE): - return 'r-d' - elif share_access == (FILE_SHARE_WRITE | FILE_SHARE_DELETE): - return '-wd' - elif share_access == (FILE_SHARE_READ | FILE_SHARE_WRITE | - FILE_SHARE_DELETE): - return 'rwd' - else: - return '---' diff --git a/fibratus/handle.py b/fibratus/handle.py deleted file mode 100644 index adf496e33..000000000 --- a/fibratus/handle.py +++ /dev/null @@ -1,268 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from _ctypes import POINTER, byref, addressof -from ctypes import cast, c_ulong, c_wchar_p -from ctypes.wintypes import HANDLE, ULONG -from enum import Enum - -from fibratus.common import DotD as ddict -from fibratus.apidefs.cdefs import STATUS_INFO_LENGTH_MISMATCH, STATUS_SUCCESS, ERROR_SUCCESS, \ - UNICODE_STRING -from fibratus.apidefs.process import open_process, PROCESS_DUP_HANDLE, get_current_process -from fibratus.apidefs.registry import MAX_BUFFER_SIZE -from fibratus.apidefs.sys import zw_query_system_information, SYSTEM_HANDLE_INFORMATION_CLASS, \ - SYSTEM_HANDLE_INFORMATION, free, realloc, SYSTEM_HANDLE, malloc, duplicate_handle, nt_query_object, \ - PUBLIC_OBJECT_TYPE_INFORMATION, OBJECT_TYPE_INFORMATION, PUBLIC_OBJECT_NAME_INFORMATION, close_handle -from fibratus.errors import HandleEnumError - - -class HandleType(Enum): - FILE = 0 - DIRECTORY = 1 - KEY = 2 - ALPC_PORT = 3 - SECTION = 4 - MUTANT = 5 - EVENT = 6 - DESKTOP = 7 - SEMAPHORE = 8 - TIMER = 9 - TOKEN = 10 - JOB = 11 - - -class HandleRepository(object): - """Stores open handle objects. - """ - - def __init__(self): - self._object_buff_size = 0x1000 - self._object_types = {} - # the object handles with these - # masks shouldn't be queried, - # otherwise the call could hang - # the main thread - self._nasty_access_masks = [0x120189, - 0x0012019f, - 0x1A019F] - - self._handle_types = [name for name, _ in HandleType.__members__.items()] - self._buffers = [] - - def query_handles(self, pid=None): - raw_handles = self._enum_handles(pid) - current_ps = HANDLE(get_current_process()) - handles = [] - # find the object handles for the process - for _, handle in raw_handles.items(): - ps_handle = open_process(PROCESS_DUP_HANDLE, - False, - handle.pid) - if ps_handle: - handle_copy = HANDLE() - # to query the object handle - # we need to duplicate it in - # the address space of the current process - status = duplicate_handle(ps_handle, - handle.handle, - current_ps, - byref(handle_copy), - 0, 0, 0) - if status != ERROR_SUCCESS: - # get the object type - handle_type = self._query_handle(handle_copy, - PUBLIC_OBJECT_TYPE_INFORMATION, - OBJECT_TYPE_INFORMATION) - if handle_type: - handle_type = cast(handle_type.contents.type_name.buffer, c_wchar_p) \ - .value \ - .upper().replace(' ', '_') - # query for object name - # (file names, registry keys, - # sections, ALPC ports, etc) - # check the access mask to make - # sure `NtQueryObject` won't hang - if handle_type in self._handle_types and \ - handle.access_mask not in self._nasty_access_masks: - handle_name = self._query_handle(handle_copy, - PUBLIC_OBJECT_NAME_INFORMATION, - UNICODE_STRING) - if handle_name: - handle_name = cast(handle_name.contents.buffer, c_wchar_p).value - handle_info = HandleInfo(handle.handle, - handle.obj, - HandleType(HandleType.__getattr__(handle_type)), - handle_name, - handle.pid) - handles.append(handle_info) - - close_handle(handle_copy) - close_handle(ps_handle) - return handles - - def free_buffers(self): - for buff in self._buffers: - free(buff) - - def _enum_handles(self, process_id=None): - """Enumerates handle information. - - Enumerates handle info on - the start of the kernel capture. - - Returns a dictionary of handle's - information including the handle id, - access mask, and the process which owns - the handle. - """ - buff_size = MAX_BUFFER_SIZE - size = c_ulong() - # allocate the initial buffer - buff = malloc(buff_size) - handles = {} - - while True: - status = zw_query_system_information(SYSTEM_HANDLE_INFORMATION_CLASS, - buff, - buff_size, - byref(size)) - if status == STATUS_INFO_LENGTH_MISMATCH: - # the buffer is too small - # increment the buffer size and try again - buff_size += MAX_BUFFER_SIZE - elif status == STATUS_SUCCESS: - # cast the buffer to `SYSTEM_HANDLE_INFORMATION` struct - # which contains an array of `SYSTEM_HANDLE` structures - sys_handle_info = cast(buff, POINTER(SYSTEM_HANDLE_INFORMATION)) - sys_handle_info = sys_handle_info.contents - handle_count = sys_handle_info.number_of_handles - - # resize the array size to the - # actual number of file handles - sys_handles = (SYSTEM_HANDLE * buff_size).from_address(addressof(sys_handle_info.handles)) - - for i in range(handle_count): - sys_handle = sys_handles[i] - pid = sys_handle.process_id - handle = sys_handle.handle - obj = sys_handle.object - obj_type_index = sys_handle.object_type_number - access_mask = sys_handle.access_mask - if process_id and process_id == pid: - handles[obj] = ddict(pid=process_id, - handle=handle, - obj=obj, - access_mask=access_mask, - obj_type_index=obj_type_index) - elif process_id is None: - handles[obj] = ddict(pid=pid, - handle=handle, - obj=obj, - access_mask=access_mask, - obj_type_index=obj_type_index) - break - else: - raise HandleEnumError(status) - # reallocate the buffer - buff = realloc(buff, buff_size) - # free the buffer memory - free(buff) - - return handles - - def _async_query_object(self): - pass - - def _query_handle(self, handle, klass, object_info_type): - """Gets the object handle info. - - Parameters - ---------- - - - handle: HANDLE - handle object - klass: int - the class of information to query - object_info_type: Structure - structure type which holds the handle info - """ - buff = malloc(self._object_buff_size) - rlen = ULONG() - status = nt_query_object(handle, - klass, - buff, - self._object_buff_size, - byref(rlen)) - if status >= 0: - info = cast(buff, POINTER(object_info_type)) - self._buffers.append(buff) - return info - else: - # reallocate the buffer size - # and try again - buff = realloc(buff, rlen.value) - status = nt_query_object(handle, - klass, - buff, - self._object_buff_size, - None) - if status >= 0: - info = cast(buff, POINTER(object_info_type)) - self._buffers.append(buff) - return info - else: - free(buff) - return None - - -class HandleInfo(): - """Saves the handle meta data. - """ - - def __init__(self, handle, obj, handle_type, name, pid): - self._handle = handle - self._obj = obj - self._handle_type = handle_type - self._name = name - self._pid = pid - - @property - def name(self): - return self._name - - @property - def handle_type(self): - return self._handle_type - - @property - def obj(self): - return self._obj - - @property - def pid(self): - return self._pid - - @property - def handle(self): - return self._handle - - def __str__(self): - return '%s type: [%s] object address: [%s] handle id: [%s] pid: [%s]' % \ - (self._name, self._handle_type, - hex(self._obj), - self._handle, - self._pid) diff --git a/fibratus/image_meta.py b/fibratus/image_meta.py deleted file mode 100644 index 4ff2e199a..000000000 --- a/fibratus/image_meta.py +++ /dev/null @@ -1,237 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -import pefile -from fibratus.common import DotD as ddict, underscore_dict_keys - - -def __decode__(value): - return value.decode('utf-8') - - -def __from_idx__(string_table, idx): - """Lookups the entry in the string table. - - Parameters - ---------- - string_table: dict - the string table - idx: int - index for the entry - """ - _, v = string_table[idx] - return __decode__(v) - - -class ImageMetaRegistry(object): - - def __init__(self, enabled=True, imports=False, file_info=False): - """Creates an instace of the image meta registry. - - Arguments - --------- - - enabled: bool - determines if image meta information should be added to the registry - imports: bool - it instructs the PE module to parse the directory entry import structure - file_info: bool - determines if file information meta data should be extracted from the PE - """ - self.image_metas = {} - self.imports = imports - self.file_info = file_info - self.enabled = enabled - self.full_loaded = False - - def add_image_meta(self, path): - """Registers image meta information. - - This method parses the PE (Portable Executable) binary format - of the the image passed in the `path` parameter. - - It then extracts some basic headers present in the PE, as well - as sections which form the binary image. - - Parameters - ---------- - - path: str - the absolute path of the image file - """ - if not self.enabled: - return None - try: - if (path.endswith('exe') or - path.endswith('dll') or - path.endswith('sys')) and \ - path not in self.image_metas: - pe = pefile.PE(path, fast_load=True) - file_header = ddict(underscore_dict_keys(pe.FILE_HEADER.dump_dict())) - # create image meta instance - image_meta = ImageMeta(file_header.machine.value, - file_header.time_date_stamp.value, - file_header.number_of_sections.value) - image_meta.sections = [dict(name=__decode__(ddict(se.dump_dict()).Name.Value), - entropy=se.get_entropy(), - md5=se.get_hash_md5(), - sha1=se.get_hash_sha1(), - sha256=se.get_hash_sha256(), - sha512=se.get_hash_sha512()) - for se in pe.sections] - # parse directory entry imports - if self.imports: - pe.full_load() - self.full_loaded = True - for module in self.__directory_entry_import__(pe): - dll = __decode__(module.dll) - imports = [__decode__(i.name) - for i in module.imports - if not i.import_by_ordinal] - image_meta.imports[dll] = imports - # parse the string table to extract - # the copyright, company, description - # and other attributes - if self.file_info: - if not self.full_loaded: - pe.full_load() - if self.__pe_has_version_info__(pe): - file_info = pe.FileInfo - if file_info and len(file_info) > 0: - file_info = file_info[0] - if self.__fi_has_string_table__(file_info): - string_table = sorted(list(file_info.StringTable[0].entries.items())) - # get file info entries from table index - image_meta.org = __from_idx__(string_table, 0) - image_meta.description = __from_idx__(string_table, 1) - image_meta.version = __from_idx__(string_table, 2) - image_meta.internal_name = __from_idx__(string_table, 3) - image_meta.copyright = __from_idx__(string_table, 4) - - self.image_metas[path] = image_meta - - return image_meta - except Exception: - # ignore the exception for now - # but consider logging it to file - # in case it can provide hints for - # troubleshooting purposes - pass - - def get_image_meta(self, path): - return self.image_metas[path] if path in self.image_metas else None - - def remove_image_meta(self, path): - return self.image_metas.pop(path, None) - - def __pe_has_version_info__(self, pe): - return hasattr(pe, 'VS_VERSIONINFO') - - def __fi_has_string_table__(self, file_info): - return len(file_info.StringTable) > 0 and hasattr(file_info, 'StringTable') - - def __directory_entry_import__(self, pe): - return getattr(pe, 'DIRECTORY_ENTRY_IMPORT') if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT') else [] - - -class ImageMeta(object): - """Container for a plethora of metadata extracted from the PE headers. - - Attributes - ---------- - arch: str - identifies the target architecture for which this image is compiled - timestamp: str - the date and time the image was created by the linker - num_sections: int - indicates the size of the section table - sections: list - information for every section found in the image - """ - def __init__(self, arch, timestamp, num_sections): - self._arch = 'x86-64' if arch == 34404 else 'x86' - self._timestamp = timestamp - self._num_sections = num_sections - self._sections = [] - self._org = None - self._description = None - self._version = None - self._internal_name = None - self._copyright = None - - self._imports = {} - - @property - def arch(self): - return self._arch - - @property - def timestamp(self): - return self._timestamp - - @property - def num_sections(self): - return self._num_sections - - @property - def org(self): - return self._org - - @org.setter - def org(self, org): - self._org = org - - @property - def description(self): - return self._description - - @description.setter - def description(self, description): - self._description = description - - @property - def version(self): - return self._version - - @version.setter - def version(self, version): - self._version = version - - @property - def internal_name(self): - return self._internal_name - - @internal_name.setter - def internal_name(self, internal_name): - self._internal_name = internal_name - - @property - def copyright(self): - return self._copyright - - @copyright.setter - def copyright(self, copyright): - self._copyright = copyright - - @property - def sections(self): - return self._sections - - @sections.setter - def sections(self, sections): - self._sections = sections - - @property - def imports(self): - return self._imports diff --git a/fibratus/kevent.py b/fibratus/kevent.py deleted file mode 100644 index 5f7a76766..000000000 --- a/fibratus/kevent.py +++ /dev/null @@ -1,269 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from datetime import datetime -from enum import Enum - -from fibratus.apidefs.process import open_thread, THREAD_QUERY_INFORMATION, get_process_id_of_thread -from fibratus.apidefs.sys import close_handle -from fibratus.common import DotD as ddict, NA - - -class Category(Enum): - - REGISTRY = 0 - FILE = 1 - NET = 2 - PROCESS = 3 - THREAD = 4 - MM = 5 - CSWITCH = 6 - SYSCALL = 7 - DISK_IO = 8 - DLL = 9 - OTHER = 10 - - -class KEvents(object): - """Available kernel event names. - """ - CREATE_PROCESS = 'CreateProcess' - CREATE_THREAD = 'CreateThread' - TERMINATE_PROCESS = 'TerminateProcess' - TERMINATE_THREAD = 'TerminateThread' - - REG_CREATE_KEY = 'RegCreateKey' - REG_DELETE_KEY = 'RegDeleteKey' - REG_DELETE_VALUE = 'RegDeleteValue' - REG_OPEN_KEY = 'RegOpenKey' - REG_SET_VALUE = 'RegSetValue' - REG_QUERY_VALUE = 'RegQueryValue' - REG_QUERY_KEY = 'RegQueryKey' - - CREATE_FILE = 'CreateFile' - DELETE_FILE = 'DeleteFile' - WRITE_FILE = 'WriteFile' - READ_FILE = 'ReadFile' - CLOSE_FILE = 'CloseFile' - RENAME_FILE = 'RenameFile' - SET_FILE_INFORMATION = 'SetFileInformation' - - SEND = 'Send' - RECEIVE = 'Recv' - ACCEPT = 'Accept' - CONNECT = 'Connect' - DISCONNECT = 'Disconnect' - RECONNECT = 'Reconnect' - - LOAD_IMAGE = 'LoadImage' - UNLOAD_IMAGE = 'UnloadImage' - - SYSCALL_ENTER = 'SyscallEnter' - SYSCALL_EXIT = 'SyscallExit' - - CONTEXT_SWITCH = 'ContextSwitch' - - @classmethod - def all(cls): - return [cls.CREATE_PROCESS, - cls.CREATE_THREAD, - cls.TERMINATE_PROCESS, - cls.TERMINATE_THREAD, - cls.CREATE_FILE, - cls.DELETE_FILE, - cls.READ_FILE, - cls.WRITE_FILE, - cls.CLOSE_FILE, - cls.RENAME_FILE, - cls.SET_FILE_INFORMATION, - cls.REG_QUERY_KEY, - cls.REG_QUERY_VALUE, - cls.REG_CREATE_KEY, - cls.REG_DELETE_KEY, - cls.REG_DELETE_VALUE, - cls.REG_OPEN_KEY, - cls.REG_SET_VALUE, - cls.LOAD_IMAGE, - cls.UNLOAD_IMAGE, - cls.SEND, - cls.RECEIVE, - cls.ACCEPT, - cls.CONNECT, - cls.RECONNECT, - cls.DISCONNECT, - cls.CONTEXT_SWITCH] - - @classmethod - def meta_info(cls): - kevents = { - KEvents.CREATE_PROCESS: (Category.PROCESS, 'Creates a new process and its primary thread', ), - KEvents.CREATE_THREAD: (Category.THREAD, 'Creates a thread to execute within the virtual address space' - ' of the calling process', ), - KEvents.TERMINATE_PROCESS: (Category.PROCESS, 'Terminates the process and all of its threads', ), - KEvents.TERMINATE_THREAD: (Category.THREAD, 'Terminates a thread', ), - KEvents.CREATE_FILE: (Category.FILE, 'Creates or opens a file or I/O device', ), - KEvents.DELETE_FILE: (Category.FILE, 'Deletes an existing file or directory', ), - KEvents.READ_FILE: (Category.FILE, 'Reads data from the file or I/O device', ), - KEvents.WRITE_FILE: (Category.FILE, 'Writes data to the file or I/O device', ), - KEvents.CLOSE_FILE: (Category.FILE, 'Closes the file or I/O device', ), - KEvents.SET_FILE_INFORMATION: (Category.FILE, 'Changes information for the specified file',), - KEvents.RENAME_FILE: (Category.FILE, 'Renames a file or directory', ), - KEvents.REG_QUERY_KEY: (Category.REGISTRY, 'Retrieves information about the registry key', ), - KEvents.REG_OPEN_KEY: (Category.REGISTRY, 'Opens the registry key', ), - KEvents.REG_CREATE_KEY: (Category.REGISTRY, 'Creates the registry key or open it if the key ' - 'already exists', ), - KEvents.REG_DELETE_KEY: (Category.REGISTRY, 'Deletes a subkey and its values', ), - KEvents.REG_QUERY_VALUE: (Category.REGISTRY, 'Retrieves the type and data of the value' - ' associated with an open registry key', ), - KEvents.REG_DELETE_VALUE: (Category.REGISTRY, 'Removes a value from the registry key', ), - KEvents.REG_SET_VALUE: (Category.REGISTRY, 'Sets the data and type of a value under a registry key', ), - KEvents.LOAD_IMAGE: (Category.DLL, 'Loads the module into the address space of the calling process', ), - KEvents.UNLOAD_IMAGE: (Category.DLL, 'Frees the loaded module from the address space ' - 'of the calling process', ), - KEvents.SEND: (Category.NET, 'Sends data on a connected socket', ), - KEvents.RECEIVE: (Category.NET, 'Receives data from a connected socket', ), - KEvents.ACCEPT: (Category.NET, 'Initiates the connection attempt from the remote or local TCP socket', ), - KEvents.CONNECT: (Category.NET, 'Establishes the connection to a TCP socket', ), - KEvents.RECONNECT: (Category.NET, 'Reconnects to a TCP socket', ), - KEvents.DISCONNECT: (Category.NET, 'Closes the connection to a TCP socket', ), - - KEvents.CONTEXT_SWITCH: (Category.THREAD, 'Scheduler selects a new thread to execute',)} - return kevents - -__kevents__ = KEvents.meta_info() - - -class KEvent(object): - - def __init__(self, thread_registry): - self._kid = 0 - self._ts = datetime.now() - self._cpuid = 0 - self._name = None - self._category = None - self._params = {} - self._tid = None - self._pid = None - self.thread_registry = thread_registry - - @property - def name(self): - return self._name - - @name.setter - def name(self, name): - self._name = name - if name in __kevents__: - cat, _ = __kevents__[name] - self._category = cat.name - - @property - def params(self): - return self._params - - @params.setter - def params(self, params): - self._params = ddict(params) - - @property - def ts(self): - return self._ts - - @ts.setter - def ts(self, ts): - self._ts = datetime.strptime(ts, '%Y-%m-%d %H:%M:%S.%f') - - @property - def cpuid(self): - return self._cpuid - - @cpuid.setter - def cpuid(self, cpuid): - self._cpuid = cpuid - - @property - def category(self): - return self._category - - @property - def pid(self): - return self._pid - - @pid.setter - def pid(self, pid): - self._pid = pid - - @property - def tid(self): - return self._tid - - @tid.setter - def tid(self, tid): - self._tid = tid - - @property - def kid(self): - return self._kid - - @property - def thread(self): - return self._find_thread() - - def _find_thread(self): - """Finds the current thread/process emitted by the kernel event. - """ - if self._pid: - # first lookup by process id - # if the process doesn't exist - # in the thread registry - # then query by the thread id - thread = self.thread_registry.get_thread(self._pid) - if not thread and self._tid: - thread = self.thread_registry.get_thread(self._tid) - else: - # we dont have the process id - # try to find the thread from which - # we can get the process - thread = self.thread_registry.get_thread(self._tid) - return thread - - def get_thread(self): - """Gets the thread associated with the kernel event. - """ - thread = self._find_thread() - if thread: - return thread.pid, thread.name - else: - # figure out the process id from thread - # if the process can't be found in - # the thread registry - pid = NA - if self._pid is None: - if self._tid: - # get the thread handle - handle = open_thread(THREAD_QUERY_INFORMATION, - False, - self._tid) - if handle: - pid = get_process_id_of_thread(handle) - close_handle(handle) - else: - pid = self._pid - return pid, NA - - def inc_kid(self): - self._kid += 1 - - diff --git a/fibratus/kevent_types.py b/fibratus/kevent_types.py deleted file mode 100644 index 445712c99..000000000 --- a/fibratus/kevent_types.py +++ /dev/null @@ -1,239 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# http://rabbitstack.github.io -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from fibratus.errors import UnknownKeventTypeError -from fibratus.kevent import KEvents - - -# start process event -CREATE_PROCESS = ('{3d6fa8d0-fe05-11d0-9dda-00c04fd7ba7c}', 1) -# end process event -TERMINATE_PROCESS = ('{3d6fa8d0-fe05-11d0-9dda-00c04fd7ba7c}', 2) -# enum processes event -ENUM_PROCESS = ('{3d6fa8d0-fe05-11d0-9dda-00c04fd7ba7c}', 3) -# start thread event -CREATE_THREAD = ('{3d6fa8d1-fe05-11d0-9dda-00c04fd7ba7c}', 1) -# end thread event -TERMINATE_THREAD = ('{3d6fa8d1-fe05-11d0-9dda-00c04fd7ba7c}', 2) -# enum threads event -ENUM_THREAD = ('{3d6fa8d1-fe05-11d0-9dda-00c04fd7ba7c}', 3) - -# create file event -CREATE_FILE = ('{90cbdc39-4a3e-11d1-84f4-0000f80464e3}', 64) -# delete file event -DELETE_FILE = ('{90cbdc39-4a3e-11d1-84f4-0000f80464e3}', 70) -# close file event generated when the file object is freed -CLOSE_FILE = ('{90cbdc39-4a3e-11d1-84f4-0000f80464e3}', 66) -# read file event -READ_FILE = ('{90cbdc39-4a3e-11d1-84f4-0000f80464e3}', 67) -# write file event -WRITE_FILE = ('{90cbdc39-4a3e-11d1-84f4-0000f80464e3}', 68) -# rename file event -RENAME_FILE = ('{90cbdc39-4a3e-11d1-84f4-0000f80464e3}', 71) -# enumerate directory event -ENUM_DIRECTORY = ('{90cbdc39-4a3e-11d1-84f4-0000f80464e3}', 72) -# set file information event -SET_FILE_INFORMATION = ('{90cbdc39-4a3e-11d1-84f4-0000f80464e3}', 69) - -# disk read event -DISK_IO_READ = ('{3d6fa8d4-fe05-11d0-9dda-00c04fd7ba7c}', 10) -# disk write event -DISK_IO_WRITE = ('{3d6fa8d4-fe05-11d0-9dda-00c04fd7ba7c}', 11) - -# create registry key event -REG_CREATE_KEY = ('{ae53722e-c863-11d2-8659-00c04fa321a1}', 10) -# create registry key event -REG_DELETE_KEY = ('{ae53722e-c863-11d2-8659-00c04fa321a1}', 12) -# delete registry value event -REG_DELETE_VALUE = ('{ae53722e-c863-11d2-8659-00c04fa321a1}', 15) -# registry open key -REG_OPEN_KEY = ('{ae53722e-c863-11d2-8659-00c04fa321a1}', 11) -# registry set value key event -REG_SET_VALUE = ('{ae53722e-c863-11d2-8659-00c04fa321a1}', 14) -# registry query value key event -REG_QUERY_VALUE = ('{ae53722e-c863-11d2-8659-00c04fa321a1}', 16) -# registry query value key event -REG_QUERY_KEY = ('{ae53722e-c863-11d2-8659-00c04fa321a1}', 13) -# create the key control block -REG_CREATE_KCB = ('{ae53722e-c863-11d2-8659-00c04fa321a1}', 22) -# delete the key control block -REG_DELETE_KCB = ('{ae53722e-c863-11d2-8659-00c04fa321a1}', 23) - -# image load event generated when a DLL or executable file is loaded -LOAD_IMAGE = ('{2cb15d1d-5fc1-11d2-abe1-00a0c911f518}', 10) -# generated when a DLL or executable file is unloaded -UNLOAD_IMAGE = ('{2cb15d1d-5fc1-11d2-abe1-00a0c911f518}', 2) -# enumerates all loaded images -ENUM_IMAGE = ('{2cb15d1d-5fc1-11d2-abe1-00a0c911f518}', 3) - -# virtual memory allocation event -VIRTUAL_ALLOC = ('{3d6fa8d3-fe05-11d0-9dda-00c04fd7ba7c}', 98) -# virtual memory free event -VIRTUAL_FREE = ('{3d6fa8d3-fe05-11d0-9dda-00c04fd7ba7c}', 99) - -# system call enter event -SYSCALL_ENTER = ('{ce1dbfb4-137e-4da6-87b0-3f59aa102cbc}', 51) -# system call exit event -SYSCALL_EXIT = ('{ce1dbfb4-137e-4da6-87b0-3f59aa102cbc}', 52) - -# context switch event -CONTEXT_SWITCH = ('{3d6fa8d1-fe05-11d0-9dda-00c04fd7ba7c}', 36) - -# starts an incoming connection attempt on socket -ACCEPT_SOCKET_TCPV4 = ('{9a280ac0-c8e0-11d1-84e2-00c04fb998a2}', 15) -ACCEPT_SOCKET_TCPV6 = ('{9a280ac0-c8e0-11d1-84e2-00c04fb998a2}', 31) - -# sends data on a connected socket -SEND_SOCKET_TCPV4 = ('{9a280ac0-c8e0-11d1-84e2-00c04fb998a2}', 10) -SEND_SOCKET_UDPV4 = ('{bf3a50c5-a9c9-4988-a005-2df0b7c80f80}', 10) -# establishes a connection to a specified socket -CONNECT_SOCKET_TCPV4 = ('{9a280ac0-c8e0-11d1-84e2-00c04fb998a2}', 12) -# disconnect event -DISCONNECT_SOCKET_TCPV4 = ('{9a280ac0-c8e0-11d1-84e2-00c04fb998a2}', 13) -# reconnect attempt event -RECONNECT_SOCKET_TCPV4 = ('{9a280ac0-c8e0-11d1-84e2-00c04fb998a2}', 16) -# receives data from a connected socket -RECV_SOCKET_TCPV4 = ('{9a280ac0-c8e0-11d1-84e2-00c04fb998a2}', 11) -RECV_SOCKET_UDPV4 = ('{bf3a50c5-a9c9-4988-a005-2df0b7c80f80}', 11) - - -def kname_to_tuple(name): - - if name == KEvents.CREATE_PROCESS: - return CREATE_PROCESS - elif name == KEvents.TERMINATE_PROCESS: - return TERMINATE_PROCESS - elif name == KEvents.CREATE_THREAD: - return CREATE_THREAD - elif name == KEvents.TERMINATE_THREAD: - return TERMINATE_THREAD - - elif name == KEvents.REG_CREATE_KEY: - return REG_CREATE_KEY - elif name == KEvents.REG_QUERY_KEY: - return REG_QUERY_KEY - elif name == KEvents.REG_OPEN_KEY: - return REG_OPEN_KEY - elif name == KEvents.REG_QUERY_VALUE: - return REG_QUERY_VALUE - elif name == KEvents.REG_SET_VALUE: - return REG_SET_VALUE - elif name == KEvents.REG_DELETE_KEY: - return REG_DELETE_KEY - elif name == KEvents.REG_DELETE_VALUE: - return REG_DELETE_VALUE - - elif name == KEvents.CREATE_FILE: - return CREATE_FILE - elif name == KEvents.READ_FILE: - return READ_FILE - elif name == KEvents.WRITE_FILE: - return WRITE_FILE - elif name == KEvents.CLOSE_FILE: - return CLOSE_FILE - elif name == KEvents.DELETE_FILE: - return DELETE_FILE - elif name == KEvents.RENAME_FILE: - return RENAME_FILE - elif name == KEvents.SET_FILE_INFORMATION: - return SET_FILE_INFORMATION - - elif name == KEvents.LOAD_IMAGE: - return LOAD_IMAGE - elif name == KEvents.UNLOAD_IMAGE: - return UNLOAD_IMAGE - - elif name == KEvents.CONTEXT_SWITCH: - return CONTEXT_SWITCH - - elif name == KEvents.SEND: - return [SEND_SOCKET_UDPV4, SEND_SOCKET_TCPV4] - elif name == KEvents.RECEIVE: - return [RECV_SOCKET_UDPV4, RECV_SOCKET_TCPV4] - elif name == KEvents.ACCEPT: - return [ACCEPT_SOCKET_TCPV4, ACCEPT_SOCKET_TCPV6] - elif name == KEvents.CONNECT: - return CONNECT_SOCKET_TCPV4 - elif name == KEvents.RECONNECT: - return RECONNECT_SOCKET_TCPV4 - elif name == KEvents.DISCONNECT: - return DISCONNECT_SOCKET_TCPV4 - else: - raise UnknownKeventTypeError(name) - - -def ktuple_to_name(ktuple): - - if ktuple == CREATE_PROCESS: - return KEvents.CREATE_PROCESS - elif ktuple == CREATE_THREAD: - return KEvents.CREATE_THREAD - elif ktuple == TERMINATE_PROCESS: - return KEvents.TERMINATE_PROCESS - elif ktuple == TERMINATE_THREAD: - return KEvents.TERMINATE_THREAD - - elif ktuple == REG_CREATE_KEY: - return KEvents.REG_CREATE_KEY - elif ktuple == REG_DELETE_KEY: - return KEvents.REG_DELETE_KEY - elif ktuple == REG_DELETE_VALUE: - return KEvents.REG_DELETE_VALUE - elif ktuple == REG_OPEN_KEY: - return KEvents.REG_OPEN_KEY - elif ktuple == REG_SET_VALUE: - return KEvents.REG_SET_VALUE - elif ktuple == REG_QUERY_VALUE: - return KEvents.REG_QUERY_VALUE - elif ktuple == REG_QUERY_KEY: - return KEvents.REG_QUERY_KEY - - elif ktuple == CREATE_FILE: - return KEvents.CREATE_FILE - elif ktuple == DELETE_FILE: - return KEvents.DELETE_FILE - elif ktuple == CLOSE_FILE: - return KEvents.CLOSE_FILE - elif ktuple == WRITE_FILE: - return KEvents.WRITE_FILE - elif ktuple == READ_FILE: - return KEvents.READ_FILE - elif ktuple == RENAME_FILE: - return KEvents.RENAME_FILE - elif ktuple == SET_FILE_INFORMATION: - return KEvents.SET_FILE_INFORMATION - - elif ktuple == LOAD_IMAGE: - return KEvents.LOAD_IMAGE - elif ktuple == UNLOAD_IMAGE: - return KEvents.UNLOAD_IMAGE - - elif ktuple == CONTEXT_SWITCH: - return KEvents.CONTEXT_SWITCH - - elif ktuple == SEND_SOCKET_UDPV4 or\ - ktuple == SEND_SOCKET_TCPV4: - return KEvents.SEND - elif ktuple == RECV_SOCKET_UDPV4 or\ - ktuple == RECV_SOCKET_TCPV4: - return KEvents.RECEIVE - elif ktuple == ACCEPT_SOCKET_TCPV4: - return KEvents.ACCEPT - elif ktuple == CONNECT_SOCKET_TCPV4: - return KEvents.CONNECT - elif ktuple == DISCONNECT_SOCKET_TCPV4: - return KEvents.DISCONNECT - elif ktuple == RECONNECT_SOCKET_TCPV4: - return KEvents.RECONNECT \ No newline at end of file diff --git a/fibratus/output/aggregator.py b/fibratus/output/aggregator.py deleted file mode 100644 index 2df2f9cc2..000000000 --- a/fibratus/output/aggregator.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from fibratus.output.console import ConsoleOutput - - -class OutputAggregator(object): - - def __init__(self, outputs): - self.outputs = outputs - - def aggregate(self, kevent): - """Emit the kernel stream via output sinks. - - For each output registered, invokes the `emit`` - method to send the kernel event info to the - output sink. - - Parameters - ---------- - - kevent: KEvent - an instance of the kernel event - """ - for _, output in self.outputs.items(): - if isinstance(output, ConsoleOutput): - output.emit(kevent) - else: - pid, proc = kevent.get_thread() - body = {'id': kevent.kid, - 'timestamp': kevent.ts.strftime('%Y-%m-%d %H:%M:%S.%f'), - 'cpuid': kevent.cpuid, - 'proc': proc, - 'pid': pid, - 'name': kevent.name, - 'category': kevent.category, - 'params': kevent.params} - output.emit(body) diff --git a/fibratus/output/amqp.py b/fibratus/output/amqp.py deleted file mode 100644 index 4a0edbca1..000000000 --- a/fibratus/output/amqp.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -import json - -import pika - -from fibratus.errors import InvalidPayloadError -from fibratus.output.base import Output - - -class AmqpOutput(Output): - - def __init__(self, **kwargs): - """Builds a new instance of the AMQP output adapter. - - Parameters - ---------- - - kwargs: dict - AMQP configuration - """ - Output.__init__(self) - self._username = kwargs.pop('username', 'guest') - self._password = kwargs.pop('password', 'guest') - - self._host = kwargs.pop('host', '127.0.0.1') - self._port = kwargs.pop('port', 5672) - self._vhost = kwargs.pop('vhost', '/') - self._delivery_mode = kwargs.pop('delivery_mode', 1) - - credentials = pika.PlainCredentials(self._username, self._password) - self._parameters = pika.ConnectionParameters(self._host, - self._port, - self._vhost, - credentials) - - self._exchange = kwargs.pop('exchange', None) - self._routingkey = kwargs.pop('routingkey', None) - - self._connection = None - self._channel = None - - self._basic_props = pika.BasicProperties(content_type='text/json', - delivery_mode=self._delivery_mode) - - def emit(self, body, **kwargs): - if not self._connection: - self._connection = pika.BlockingConnection(self._parameters) - self._channel = self._connection.channel() - # override the default exchange name - # and the routing key used to send - # the message to the AMQP broker - self._routingkey = kwargs.pop('routingkey', self._routingkey) - self._exchange = kwargs.pop('exchange', self._exchange) - - # the message body should be a dictionary - if not isinstance(body, dict): - raise InvalidPayloadError('invalid payload for AMQP message. ' - 'dict expected but %s found' - % type(body)) - body = json.dumps(body) - self._channel.basic_publish(self._exchange, - self._routingkey, - body, self._basic_props) - - @property - def username(self): - return self._username - - @property - def host(self): - return self._host - - @property - def port(self): - return self._port - - @property - def vhost(self): - return self._vhost - - @property - def exchange(self): - return self._exchange - - @property - def routingkey(self): - return self._routingkey - - @property - def delivery_mode(self): - return self._delivery_mode diff --git a/fibratus/output/base.py b/fibratus/output/base.py deleted file mode 100644 index 4aa4d9681..000000000 --- a/fibratus/output/base.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from logbook import Logger, StreamHandler -import sys - - -class Output(object): - - def __init__(self): - StreamHandler(sys.stdout).push_application() - self.logger = Logger() - - def emit(self, body, **kwargs): - raise NotImplementedError() - - def supports_batches(self): - return False diff --git a/fibratus/output/console.py b/fibratus/output/console.py deleted file mode 100644 index 42e2b9fe8..000000000 --- a/fibratus/output/console.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -import json -from _ctypes import byref - -from fibratus.apidefs.sys import get_std_handle, STD_OUTPUT_HANDLE, write_console_unicode, c_ulong -from fibratus.output.base import Output - -RENDER_FORMAT = '%s %s %s %s (%s) - %s %s' - - -class ConsoleOutput(Output): - - def __init__(self, **kwargs): - Output.__init__(self) - - self._fmt = kwargs.pop('format', 'pretty') - self._timestamp_pattern = kwargs.pop('timestamp_pattern', '%Y-%m-%d %H:%M:%S.%f') - self._stdout_handle = get_std_handle(STD_OUTPUT_HANDLE) - - assert self._stdout_handle, 'could not acquire the standard output stream handle' - - def emit(self, kevent, **kwargs): - """Renders the kevent to the standard output stream. - - Uses the default output format or JSON to render the - kernel event to standard output stream. - - The default output format is as follows: - - id timestamp cpu process (process id) - kevent (parameters) - -- --------- --- ------- ----------- ------- ------------ - - Example: - - 160 13:27:27.554 0 wmiprvse.exe (1012) - CloseFile (file=C:\\WINDOWS\\SYSTEM32\\RSAENH.DLL, tid=2668) - - Parameters - ---------- - - kevent: KEvent - the information regarding the kernel event - - kwargs: dict - console adapter configuration - - """ - if isinstance(kevent, dict): - kevt = json.dumps(kevent) - else: - pid, proc = kevent.get_thread() - if 'pretty' in self._fmt: - kevt = RENDER_FORMAT % (kevent.kid, - kevent.ts.time(), - kevent.cpuid, - proc, - pid, - kevent.name, - self._format_params(kevent.params)) - else: - kevt = json.dumps(dict(id=kevent.kid, - timestamp=kevent.ts.strftime(self._timestamp_pattern), - cpuid=kevent.cpuid, - proc=proc, - pid=pid, - name=kevent.name, - params=kevent.params)) - - kevt += '\n' - # write the output on the standard output stream - write_console_unicode(self._stdout_handle, kevt, - len(kevt), - byref(c_ulong()), - None) - - def _format_params(self, kparams): - """Transforms the kevent parameters. - - Apply the rendering format on the kevent payload - to transform it into more convenient structure - sorted by parameter keys. - """ - fmt = ', '.join('%s=%s' % (k, kparams[k]) for k in sorted(kparams.keys())) \ - .replace('\"', '') - return '(%s)' % fmt diff --git a/fibratus/output/elasticsearch.py b/fibratus/output/elasticsearch.py deleted file mode 100644 index 771f23445..000000000 --- a/fibratus/output/elasticsearch.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import elasticsearch -import elasticsearch.helpers - -from fibratus.errors import InvalidPayloadError -from fibratus.output.base import Output -from datetime import datetime - -class ElasticsearchOutput(Output): - - def __init__(self, **kwargs): - """Creates an instance of the Elasticsearch output adapter. - - Parameters - ---------- - - kwargs: dict - Elasticsearch cluster configuration - """ - Output.__init__(self) - - hosts = kwargs.pop('hosts', []) - self._hosts = [dict(host=host.split(':')[0], port=int(host.split(':')[1])) for host in hosts] - self._index_name = kwargs.pop('index', None) - self._index_type = kwargs.pop('index_type', 'fixed') - self._daily_index_format = kwargs.pop('daily_index_format', '%Y.%m.%d') - self._document_type = kwargs.pop('document', None) - self._bulk = kwargs.pop('bulk', False) - self._username = kwargs.pop('username', None) - self._password = kwargs.pop('password', None) - self._config = {} - if self._username and self._password: - self._config['http_auth'] = (self._username, self._password,) - self._config['use_ssl'] = kwargs.pop('ssl', False) - self._elasticsearch = None - - def emit(self, body, **kwargs): - if not self._elasticsearch: - self._elasticsearch = elasticsearch.Elasticsearch(self._hosts, **self._config) - if self._bulk: - if not isinstance(body, list): - raise InvalidPayloadError('invalid payload for bulk indexing. ' - 'list expected but %s found' - % type(body)) - else: - if not isinstance(body, dict): - raise InvalidPayloadError('invalid payload for document. ' - 'dict expected but %s found' - % type(body)) - - self._index_name = kwargs.pop('index', self._index_name) - - # build index name for daily index types - if 'daily' in self._index_type: - self._index_name = '%s-%s' % (self._index_name, datetime.now().strftime(self._daily_index_format)) - - if self._bulk: - actions = [dict(_index=self._index_name, _type=self._document_type, _source=b) for b in body] - elasticsearch.helpers.bulk(self._elasticsearch, actions) - else: - self._elasticsearch.index(self._index_name, self._document_type, body=body) - - @property - def hosts(self): - return self._hosts - - @property - def index_name(self): - return self._index_name - - @property - def index_type(self): - return self._index_type - - @property - def document_type(self): - return self._document_type - - @property - def bulk(self): - return self._bulk diff --git a/fibratus/output/fs.py b/fibratus/output/fs.py deleted file mode 100644 index 10fe507c5..000000000 --- a/fibratus/output/fs.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2017 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from fibratus.output.base import Output -import io -import os -import time -import json - - -class FsOutput(Output): - """File system output. - - Implementation of the output which writes the stream of - kernel events to a file. - """ - - def __init__(self, **kwargs): - Output.__init__(self) - self._path = kwargs.pop('path', None) - self._fmt = kwargs.pop('format', 'json') - self._mode = kwargs.pop('mode', 'a') - - filename = os.path.join(self._path, - '%s.fibra' % time.strftime('%x') - .replace('/', '-')) - self.stream = io.open(filename, self._mode) - - def emit(self, body, **kwargs): - if 'json' in self._fmt: - self.stream.write(json.dumps(body) + '\n') - - @property - def path(self): - return self._path - - @property - def format(self): - return self._fmt - - @property - def mode(self): - return self._mode \ No newline at end of file diff --git a/fibratus/output/smtp.py b/fibratus/output/smtp.py deleted file mode 100644 index 612ead8e0..000000000 --- a/fibratus/output/smtp.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os -import smtplib - -from fibratus.output.base import Output - - -class SmtpOutput(Output): - - def __init__(self, **kwargs): - """Constructs a new instance of the SMTP outbound adapter. - - Parameters - ---------- - - kwargs: dict - SMTP server and account configuration - """ - Output.__init__(self) - self._host = kwargs.pop('host', None) - self._port = kwargs.pop('port', 587) - self._from = kwargs.pop('from', None) - self._to = kwargs.pop('to', []) - self._password = kwargs.pop('password', None) or \ - os.environ.get('SMTP_PASS') - self._smtp = None - - def emit(self, body, **kwargs): - if not self._smtp: - self._smtp = smtplib.SMTP(self._host, self._port) - self._smtp.ehlo() - self._smtp.starttls() - self._smtp.ehlo() - subject = kwargs.pop('subject', '') - message = self._compose_message(subject, body) - # try to authenticate with the server - # before attempting to send the message - try: - self._smtp.login(self._from, self._password) - self._smtp.sendmail(self._from, self._to, message) - except smtplib.SMTPAuthenticationError: - self.logger.error('Invalid SMTP credentials for %s account' - % self._from) - finally: - self._smtp.quit() - - def _compose_message(self, subject, body): - return """From: %s\r\nTo: %s\r\nSubject: %s\r\n\ - - %s - """ % (self._from, ", ".join(self._to), - subject, body) - - @property - def host(self): - return self._host - - @property - def port(self): - return self._port - - @property - def sender(self): - return self._from - - @property - def to(self): - return self._to diff --git a/fibratus/registry.py b/fibratus/registry.py deleted file mode 100644 index 9815a64b0..000000000 --- a/fibratus/registry.py +++ /dev/null @@ -1,294 +0,0 @@ -# Copyright 2015/2016 by Nedim Sabic (RabbitStack) -# http://rabbitstack.github.io -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -import os -from ctypes import cast, byref - -from fibratus.common import NA -from fibratus.handle import HandleType -from fibratus.kevent_types import * -from fibratus.apidefs.registry import * -from fibratus.apidefs.sys import malloc, free - - -class HiveParser(object): - - def __init__(self, kevent, thread_registry): - self._kcblocks = {} - self._kevent = kevent - self.thread_registry = thread_registry - self.hive_regexs = [ - r'(?i)(REGISTRY\\MACHINE\\SOFTWARE)(.*)', - r'(?i)(REGISTRY\\MACHINE\\HARDWARE)(.*)', - r'(?i)(REGISTRY\\MACHINE\\SECURITY)(.*)', - r'(?i)(REGISTRY\\MACHINE\\SYSTEM)(.*)', - r'(?i)(REGISTRY\\MACHINE\\SAM)(.*)', - r'(?i)(REGISTRY\\USER\\.DEFAULT)(.*)', - r'(?i)(REGISTRY\\USER\\S-.+?)\\(.*)', - r'(?i)(REGISTRY\\USER)(.*)'] - self._reg_value_types = [v.value for v in ValueType] - - @property - def kcblocks(self): - return self._kcblocks - - def remove_kcb(self, key_handle): - if key_handle in self._kcblocks: - self._kcblocks.pop(key_handle) - - def add_kcb(self, kkcb): - """Adds a key control block (KCB). - - Parameters - ---------- - - kkcb: dict - metadata for the KCB - - """ - handle = kkcb.key_handle - # index the KCB by key handle - # we also save the process and - # thread id which created the KCB - kcb = Kcb(handle, kkcb.key_name, - kkcb.index, - kkcb.status, - kkcb.thread_id, - kkcb.process_id) - self._kcblocks[handle] = kcb - - def parse_hive(self, ketype, regkevt): - """Parses a hive from the registry kernel event. - - Hive is a logical group of keys, subkeys and values - which are commonly called as nodes. - - Parameters - ---------- - - ketype: tuple - kernel event type - regkevt: dict - kernel registry event payload as forwarded from the - event stream collector - """ - hive = NA - key = regkevt.key_name - status = regkevt.status - tid = regkevt.thread_id - pid = regkevt.process_id - index = regkevt.index - - self._kevent.tid = tid - self._kevent.pid = pid - - # if the node handle (KCB handle) is equal to 0 - # we have the full node name. Otherwise - # we have to query the key control blocks - # to found the full node name - handle = regkevt.key_handle - if handle == 0: - # find the hive by applying - # the regular expression - hive, key = self._dissect_hive(key) - else: - if handle in self._kcblocks: - # KCB found. Concatenate the - # full node path - kcb = self._kcblocks[handle] - full_path = '%s\%s' % (kcb.key, key) - hive, key = self._dissect_hive(full_path) - else: - # we missed the KCB creation - # lookup the handles - # to find the key name - thread = self.thread_registry.get_thread(pid) - if thread: - key_handles = [kh for kh in thread.handles if kh.handle_type is not None and - kh.handle_type == HandleType.KEY] - for khandle in key_handles: - if ketype in [REG_CREATE_KEY, - REG_DELETE_KEY, - REG_OPEN_KEY, - REG_QUERY_KEY]: - # try to find the match of the key name - # from registry key handle name. - # Replace the backslash to prevent - # bogus escape exceptions - khandle_name = khandle.name - f = re.findall(r"%s" % key.replace('\\', '_'), - khandle_name.replace('\\', '_')) - if len(f) > 0: - hive, key = self._dissect_hive(khandle_name) - kcb = Kcb(handle, - khandle_name, - index, - status, - tid, - pid) - self._kcblocks[handle] = kcb - break - - if hive == NA: - # set the unknown hive and - # the partial node name - key = '..\%s' % key - - if ketype in [REG_CREATE_KEY, - REG_DELETE_KEY, - REG_OPEN_KEY, - REG_QUERY_KEY]: - params = { - 'hive': hive, - 'key': key, - 'status': status, - 'tid': tid, - 'pid': pid - } - self._kevent.params = params - elif ketype in [REG_SET_VALUE, - REG_DELETE_VALUE, - REG_QUERY_VALUE]: - if ketype == REG_SET_VALUE or ketype == REG_QUERY_VALUE: - # we have the hive and the subkey - # including the registry value name - # which means we are able to query the content - # of the registry value - if hive != NA and not key.startswith('..'): - # resolve the root key name - # from the registry hive - hkey = self._hive_to_hkey(hive) - subkey, value_name = os.path.split(key) - # get the value data and value type - # from the registry - value, value_type = self._query_value(hkey, - subkey, - value_name) - self._kevent.params = dict(hive=hive, key=key, - value_type=value_type, - value=value, - status=status, - tid=tid, - pid=pid) - else: - self._kevent.params = dict(hive=hive, key=key, - value_type=NA, - value=NA, status=status, - tid=tid, pid=pid) - - else: - self._kevent.params = dict(hive=hive, key=key, - status=status, - tid=tid, - pid=pid) - - def _query_value(self, hkey, subkey, value_name): - """Get value content and value type from registry. - - Parameters - ---------- - - hkey: HKEY - handle to registry root key - subkey: str - path representing the subkey - value: - the name of the value - """ - if not hkey: - return NA, NA - value_type = c_ulong() - buff = malloc(MAX_BUFFER_SIZE) - buff_size = c_ulong(MAX_BUFFER_SIZE) - - status = reg_get_value(hkey, c_wchar_p(subkey), - c_wchar_p(value_name), - RRF_RT_ANY, - byref(value_type), - buff, byref(buff_size)) - if status == ERROR_SUCCESS: - value = cast(buff, c_wchar_p).value - value_type = value_type.value - if value_type in self._reg_value_types: - if value_type == ValueType.REG_BINARY.value: - value = '' - [value_type] = [v.name for v in ValueType if v.value == value_type] - else: - value_type = ValueType.REG_NONE.name - free(buff) - return value, value_type - else: - free(buff) - return NA, NA - - def _dissect_hive(self, key_name): - """Extracts the hive name and the subkey from the key path. - - Parameters - ---------- - - key_name: str - key path from whom the hive - can be resolved - """ - for rx in self.hive_regexs: - # for each regex match it - # against key path - m = re.search(rx, key_name) - if m and len(m.groups()) > 0: - hive = m.group(1).upper() - # hive found, now try - # to get the node path - if len(m.groups()) > 1: - node = m.group(2) - if node: - # because the hive contains the - # child node of the registry subkey - # we have to include it - _, hive_child = os.path.split(hive) - if not node.startswith('\\'): - node = '\\%s' % node - node = '%s%s' % (hive_child, node) - return hive.replace('\\', '_'), node - return hive.replace('\\', '_'), key_name - return key_name, key_name - - def _hive_to_hkey(self, hive): - if re.match(r'(?i).*MACHINE.*', hive): - return HKEY_LOCAL_MACHINE - elif re.match(r'(?i).*USER_S-.*|\.DEFAULT', hive): - return HKEY_USERS - else: - return None - - -class Kcb(object): - """The container for the Key Control Block data. - """ - def __init__(self, handle, key, index, - status, tid, pid): - self._handle = handle - self._key = key - self._index = index - self._status = status - self._thread_id = tid - self._process_id = pid - - @property - def key(self): - return self._key - - - diff --git a/fibratus/tcpip/__init__.py b/fibratus/tcpip/__init__.py deleted file mode 100644 index b4b9e2a2a..000000000 --- a/fibratus/tcpip/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. \ No newline at end of file diff --git a/fibratus/tcpip/ports.py b/fibratus/tcpip/ports.py deleted file mode 100644 index 7e5885f28..000000000 --- a/fibratus/tcpip/ports.py +++ /dev/null @@ -1,11095 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -IANA_PORTS_TCP = { - 1: "tcpmux", - 2: "compressnet", - 3: "compressnet", - 5: "rje", - 7: "echo", - 9: "discard", - 11: "systat", - 13: "daytime", - 17: "qotd", - 18: "msp", - 19: "chargen", - 20: "ftp-data", - 21: "ftp", - 22: "ssh", - 23: "telnet", - 25: "smtp", - 27: "nsw-fe", - 29: "msg-icp", - 31: "msg-auth", - 33: "dsp", - 37: "time", - 38: "rap", - 39: "rlp", - 41: "graphics", - 42: "name", - 43: "nicname", - 44: "mpm-flags", - 45: "mpm", - 46: "mpm-snd", - 47: "ni-ftp", - 48: "auditd", - 49: "tacacs", - 50: "re-mail-ck", - 52: "xns-time", - 53: "domain", - 54: "xns-ch", - 55: "isi-gl", - 56: "xns-auth", - 58: "xns-mail", - 61: "ni-mail", - 62: "acas", - 63: "whoispp", - 64: "covia", - 65: "tacacs-ds", - 66: "sql-net", - 67: "bootps", - 68: "bootpc", - 69: "tftp", - 70: "gopher", - 71: "netrjs-1", - 72: "netrjs-2", - 73: "netrjs-3", - 74: "netrjs-4", - 76: "deos", - 78: "vettcp", - 79: "finger", - 80: "http", - 82: "xfer", - 83: "mit-ml-dev", - 84: "ctf", - 85: "mit-ml-dev", - 86: "mfcobol", - 88: "kerberos", - 89: "su-mit-tg", - 90: "dnsix", - 91: "mit-dov", - 92: "npp", - 93: "dcp", - 94: "objcall", - 95: "supdup", - 96: "dixie", - 97: "swift-rvf", - 98: "tacnews", - 99: "metagram", - 101: "hostname", - 102: "iso-tsap", - 103: "gppitnp", - 104: "acr-nema", - 105: "cso", - 106: "3com-tsmux", - 107: "rtelnet", - 108: "snagas", - 109: "pop2", - 110: "pop3", - 111: "sunrpc", - 112: "mcidas", - 113: "ident", - 115: "sftp", - 116: "ansanotify", - 117: "uucp-path", - 118: "sqlserv", - 119: "nntp", - 120: "cfdptkt", - 121: "erpc", - 122: "smakynet", - 123: "ntp", - 124: "ansatrader", - 125: "locus-map", - 126: "nxedit", - 127: "locus-con", - 128: "gss-xlicen", - 129: "pwdgen", - 130: "cisco-fna", - 131: "cisco-tna", - 132: "cisco-sys", - 133: "statsrv", - 134: "ingres-net", - 135: "epmap", - 136: "profile", - 137: "netbios-ns", - 138: "netbios-dgm", - 139: "netbios-ssn", - 140: "emfis-data", - 141: "emfis-cntl", - 142: "bl-idm", - 143: "imap", - 144: "uma", - 145: "uaac", - 146: "iso-tp0", - 147: "iso-ip", - 148: "jargon", - 149: "aed-512", - 150: "sql-net", - 151: "hems", - 152: "bftp", - 153: "sgmp", - 154: "netsc-prod", - 155: "netsc-dev", - 156: "sqlsrv", - 157: "knet-cmp", - 158: "pcmail-srv", - 159: "nss-routing", - 160: "sgmp-traps", - 161: "snmp", - 162: "snmptrap", - 163: "cmip-man", - 164: "cmip-agent", - 165: "xns-courier", - 166: "s-net", - 167: "namp", - 168: "rsvd", - 169: "send", - 170: "print-srv", - 171: "multiplex", - 172: "cl-1", - 173: "xyplex-mux", - 174: "mailq", - 175: "vmnet", - 176: "genrad-mux", - 177: "xdmcp", - 178: "nextstep", - 179: "bgp", - 180: "ris", - 181: "unify", - 182: "audit", - 183: "ocbinder", - 184: "ocserver", - 185: "remote-kis", - 186: "kis", - 187: "aci", - 188: "mumps", - 189: "qft", - 190: "gacp", - 191: "prospero", - 192: "osu-nms", - 193: "srmp", - 194: "irc", - 195: "dn6-nlm-aud", - 196: "dn6-smm-red", - 197: "dls", - 198: "dls-mon", - 199: "smux", - 200: "src", - 201: "at-rtmp", - 202: "at-nbp", - 203: "at-3", - 204: "at-echo", - 205: "at-5", - 206: "at-zis", - 207: "at-7", - 208: "at-8", - 209: "qmtp", - 210: "z39-50", - 211: "914c-g", - 212: "anet", - 213: "ipx", - 214: "vmpwscs", - 215: "softpc", - 216: "CAIlic", - 217: "dbase", - 218: "mpp", - 219: "uarps", - 220: "imap3", - 221: "fln-spx", - 222: "rsh-spx", - 223: "cdc", - 224: "masqdialer", - 242: "direct", - 243: "sur-meas", - 244: "inbusiness", - 245: "link", - 246: "dsp3270", - 247: "subntbcst-tftp", - 248: "bhfhs", - 256: "rap", - 257: "set", - 259: "esro-gen", - 260: "openport", - 261: "nsiiops", - 262: "arcisdms", - 263: "hdap", - 264: "bgmp", - 265: "x-bone-ctl", - 266: "sst", - 267: "td-service", - 268: "td-replica", - 269: "manet", - 271: "pt-tls", - 280: "http-mgmt", - 281: "personal-link", - 282: "cableport-ax", - 283: "rescap", - 284: "corerjd", - 286: "fxp", - 287: "k-block", - 308: "novastorbakcup", - 309: "entrusttime", - 310: "bhmds", - 311: "asip-webadmin", - 312: "vslmp", - 313: "magenta-logic", - 314: "opalis-robot", - 315: "dpsi", - 316: "decauth", - 317: "zannet", - 318: "pkix-timestamp", - 319: "ptp-event", - 320: "ptp-general", - 321: "pip", - 322: "rtsps", - 323: "rpki-rtr", - 324: "rpki-rtr-tls", - 333: "texar", - 344: "pdap", - 345: "pawserv", - 346: "zserv", - 347: "fatserv", - 348: "csi-sgwp", - 349: "mftp", - 350: "matip-type-a", - 351: "matip-type-b", - 352: "dtag-ste-sb", - 353: "ndsauth", - 354: "bh611", - 355: "datex-asn", - 356: "cloanto-net-1", - 357: "bhevent", - 358: "shrinkwrap", - 359: "nsrmp", - 360: "scoi2odialog", - 361: "semantix", - 362: "srssend", - 363: "rsvp-tunnel", - 364: "aurora-cmgr", - 365: "dtk", - 366: "odmr", - 367: "mortgageware", - 368: "qbikgdp", - 369: "rpc2portmap", - 370: "codaauth2", - 371: "clearcase", - 372: "ulistproc", - 373: "legent-1", - 374: "legent-2", - 375: "hassle", - 376: "nip", - 377: "tnETOS", - 378: "dsETOS", - 379: "is99c", - 380: "is99s", - 381: "hp-collector", - 382: "hp-managed-node", - 383: "hp-alarm-mgr", - 384: "arns", - 385: "ibm-app", - 386: "asa", - 387: "aurp", - 388: "unidata-ldm", - 389: "ldap", - 390: "uis", - 391: "synotics-relay", - 392: "synotics-broker", - 393: "meta5", - 394: "embl-ndt", - 395: "netcp", - 396: "netware-ip", - 397: "mptn", - 398: "kryptolan", - 399: "iso-tsap-c2", - 400: "osb-sd", - 401: "ups", - 402: "genie", - 403: "decap", - 404: "nced", - 405: "ncld", - 406: "imsp", - 407: "timbuktu", - 408: "prm-sm", - 409: "prm-nm", - 410: "decladebug", - 411: "rmt", - 412: "synoptics-trap", - 413: "smsp", - 414: "infoseek", - 415: "bnet", - 416: "silverplatter", - 417: "onmux", - 418: "hyper-g", - 419: "ariel1", - 420: "smpte", - 421: "ariel2", - 422: "ariel3", - 423: "opc-job-start", - 424: "opc-job-track", - 425: "icad-el", - 426: "smartsdp", - 427: "svrloc", - 428: "ocs-cmu", - 429: "ocs-amu", - 430: "utmpsd", - 431: "utmpcd", - 432: "iasd", - 433: "nnsp", - 434: "mobileip-agent", - 435: "mobilip-mn", - 436: "dna-cml", - 437: "comscm", - 438: "dsfgw", - 439: "dasp", - 440: "sgcp", - 441: "decvms-sysmgt", - 442: "cvc-hostd", - 443: "https", - 444: "snpp", - 445: "microsoft-ds", - 446: "ddm-rdb", - 447: "ddm-dfm", - 448: "ddm-ssl", - 449: "as-servermap", - 450: "tserver", - 451: "sfs-smp-net", - 452: "sfs-config", - 453: "creativeserver", - 454: "contentserver", - 455: "creativepartnr", - 456: "macon-tcp", - 457: "scohelp", - 458: "appleqtc", - 459: "ampr-rcmd", - 460: "skronk", - 461: "datasurfsrv", - 462: "datasurfsrvsec", - 463: "alpes", - 464: "kpasswd", - 465: "urd", - 466: "digital-vrc", - 467: "mylex-mapd", - 468: "photuris", - 469: "rcp", - 470: "scx-proxy", - 471: "mondex", - 472: "ljk-login", - 473: "hybrid-pop", - 474: "tn-tl-w1", - 475: "tcpnethaspsrv", - 476: "tn-tl-fd1", - 477: "ss7ns", - 478: "spsc", - 479: "iafserver", - 480: "iafdbase", - 481: "ph", - 482: "bgs-nsi", - 483: "ulpnet", - 484: "integra-sme", - 485: "powerburst", - 486: "avian", - 487: "saft", - 488: "gss-http", - 489: "nest-protocol", - 490: "micom-pfs", - 491: "go-login", - 492: "ticf-1", - 493: "ticf-2", - 494: "pov-ray", - 495: "intecourier", - 496: "pim-rp-disc", - 497: "retrospect", - 498: "siam", - 499: "iso-ill", - 500: "isakmp", - 501: "stmf", - 502: "mbap", - 503: "intrinsa", - 504: "citadel", - 505: "mailbox-lm", - 506: "ohimsrv", - 507: "crs", - 508: "xvttp", - 509: "snare", - 510: "fcp", - 511: "passgo", - 512: "exec", - 513: "login", - 514: "shell", - 515: "printer", - 516: "videotex", - 517: "talk", - 518: "ntalk", - 519: "utime", - 520: "efs", - 521: "ripng", - 522: "ulp", - 523: "ibm-db2", - 524: "ncp", - 525: "timed", - 526: "tempo", - 527: "stx", - 528: "custix", - 529: "irc-serv", - 530: "courier", - 531: "conference", - 532: "netnews", - 533: "netwall", - 534: "windream", - 535: "iiop", - 536: "opalis-rdv", - 537: "nmsp", - 538: "gdomap", - 539: "apertus-ldp", - 540: "uucp", - 541: "uucp-rlogin", - 542: "commerce", - 543: "klogin", - 544: "kshell", - 545: "appleqtcsrvr", - 546: "dhcpv6-client", - 547: "dhcpv6-server", - 548: "afpovertcp", - 549: "idfp", - 550: "new-rwho", - 551: "cybercash", - 552: "devshr-nts", - 553: "pirp", - 554: "rtsp", - 555: "dsf", - 556: "remotefs", - 557: "openvms-sysipc", - 558: "sdnskmp", - 559: "teedtap", - 560: "rmonitor", - 561: "monitor", - 562: "chshell", - 563: "nntps", - 564: "9pfs", - 565: "whoami", - 566: "streettalk", - 567: "banyan-rpc", - 568: "ms-shuttle", - 569: "ms-rome", - 570: "meter", - 571: "meter", - 572: "sonar", - 573: "banyan-vip", - 574: "ftp-agent", - 575: "vemmi", - 576: "ipcd", - 577: "vnas", - 578: "ipdd", - 579: "decbsrv", - 580: "sntp-heartbeat", - 581: "bdp", - 582: "scc-security", - 583: "philips-vc", - 584: "keyserver", - 586: "password-chg", - 587: "submission", - 588: "cal", - 589: "eyelink", - 590: "tns-cml", - 591: "http-alt", - 592: "eudora-set", - 593: "http-rpc-epmap", - 594: "tpip", - 595: "cab-protocol", - 596: "smsd", - 597: "ptcnameservice", - 598: "sco-websrvrmg3", - 599: "acp", - 600: "ipcserver", - 601: "syslog-conn", - 602: "xmlrpc-beep", - 603: "idxp", - 604: "tunnel", - 605: "soap-beep", - 606: "urm", - 607: "nqs", - 608: "sift-uft", - 609: "npmp-trap", - 610: "npmp-local", - 611: "npmp-gui", - 612: "hmmp-ind", - 613: "hmmp-op", - 614: "sshell", - 615: "sco-inetmgr", - 616: "sco-sysmgr", - 617: "sco-dtmgr", - 618: "dei-icda", - 619: "compaq-evm", - 620: "sco-websrvrmgr", - 621: "escp-ip", - 622: "collaborator", - 623: "oob-ws-http", - 624: "cryptoadmin", - 625: "dec-dlm", - 626: "asia", - 627: "passgo-tivoli", - 628: "qmqp", - 629: "3com-amp3", - 630: "rda", - 631: "ipp", - 632: "bmpp", - 633: "servstat", - 634: "ginad", - 635: "rlzdbase", - 636: "ldaps", - 637: "lanserver", - 638: "mcns-sec", - 639: "msdp", - 640: "entrust-sps", - 641: "repcmd", - 642: "esro-emsdp", - 643: "sanity", - 644: "dwr", - 645: "pssc", - 646: "ldp", - 647: "dhcp-failover", - 648: "rrp", - 649: "cadview-3d", - 650: "obex", - 651: "ieee-mms", - 652: "hello-port", - 653: "repscmd", - 654: "aodv", - 655: "tinc", - 656: "spmp", - 657: "rmc", - 658: "tenfold", - 660: "mac-srvr-admin", - 661: "hap", - 662: "pftp", - 663: "purenoise", - 664: "oob-ws-https", - 665: "sun-dr", - 666: "mdqs", - 667: "disclose", - 668: "mecomm", - 669: "meregister", - 670: "vacdsm-sws", - 671: "vacdsm-app", - 672: "vpps-qua", - 673: "cimplex", - 674: "acap", - 675: "dctp", - 676: "vpps-via", - 677: "vpp", - 678: "ggf-ncp", - 679: "mrm", - 680: "entrust-aaas", - 681: "entrust-aams", - 682: "xfr", - 683: "corba-iiop", - 684: "corba-iiop-ssl", - 685: "mdc-portmapper", - 686: "hcp-wismar", - 687: "asipregistry", - 688: "realm-rusd", - 689: "nmap", - 690: "vatp", - 691: "msexch-routing", - 692: "hyperwave-isp", - 693: "connendp", - 694: "ha-cluster", - 695: "ieee-mms-ssl", - 696: "rushd", - 697: "uuidgen", - 698: "olsr", - 699: "accessnetwork", - 700: "epp", - 701: "lmp", - 702: "iris-beep", - 704: "elcsd", - 705: "agentx", - 706: "silc", - 707: "borland-dsj", - 709: "entrust-kmsh", - 710: "entrust-ash", - 711: "cisco-tdp", - 712: "tbrpf", - 713: "iris-xpc", - 714: "iris-xpcs", - 715: "iris-lwz", - 729: "netviewdm1", - 730: "netviewdm2", - 731: "netviewdm3", - 741: "netgw", - 742: "netrcs", - 744: "flexlm", - 747: "fujitsu-dev", - 748: "ris-cm", - 749: "kerberos-adm", - 750: "rfile", - 751: "pump", - 752: "qrh", - 753: "rrh", - 754: "tell", - 758: "nlogin", - 759: "con", - 760: "ns", - 761: "rxe", - 762: "quotad", - 763: "cycleserv", - 764: "omserv", - 765: "webster", - 767: "phonebook", - 769: "vid", - 770: "cadlock", - 771: "rtip", - 772: "cycleserv2", - 773: "submit", - 774: "rpasswd", - 775: "entomb", - 776: "wpages", - 777: "multiling-http", - 780: "wpgs", - 800: "mdbs-daemon", - 801: "device", - 802: "mbap-s", - 810: "fcp-udp", - 828: "itm-mcell-s", - 829: "pkix-3-ca-ra", - 830: "netconf-ssh", - 831: "netconf-beep", - 832: "netconfsoaphttp", - 833: "netconfsoapbeep", - 847: "dhcp-failover2", - 848: "gdoi", - 860: "iscsi", - 861: "owamp-control", - 862: "twamp-control", - 873: "rsync", - 886: "iclcnet-locate", - 887: "iclcnet-svinfo", - 888: "accessbuilder", - 900: "omginitialrefs", - 901: "smpnameres", - 902: "ideafarm-door", - 903: "ideafarm-panic", - 910: "kink", - 911: "xact-backup", - 912: "apex-mesh", - 913: "apex-edge", - 989: "ftps-data", - 990: "ftps", - 991: "nas", - 992: "telnets", - 993: "imaps", - 995: "pop3s", - 996: "vsinet", - 997: "maitrd", - 998: "busboy", - 999: "garcon", - 1000: "cadlock2", - 1010: "surf", - 1021: "exp1", - 1022: "exp2", - 1025: "blackjack", - 1026: "cap", - 1029: "solid-mux", - 1033: "netinfo-local", - 1034: "activesync", - 1035: "mxxrlogin", - 1036: "nsstp", - 1037: "ams", - 1038: "mtqp", - 1039: "sbl", - 1040: "netarx", - 1041: "danf-ak2", - 1042: "afrog", - 1043: "boinc-client", - 1044: "dcutility", - 1045: "fpitp", - 1046: "wfremotertm", - 1047: "neod1", - 1048: "neod2", - 1049: "td-postman", - 1050: "cma", - 1051: "optima-vnet", - 1052: "ddt", - 1053: "remote-as", - 1054: "brvread", - 1055: "ansyslmd", - 1056: "vfo", - 1057: "startron", - 1058: "nim", - 1059: "nimreg", - 1060: "polestar", - 1061: "kiosk", - 1062: "veracity", - 1063: "kyoceranetdev", - 1064: "jstel", - 1065: "syscomlan", - 1066: "fpo-fns", - 1067: "instl-boots", - 1068: "instl-bootc", - 1069: "cognex-insight", - 1070: "gmrupdateserv", - 1071: "bsquare-voip", - 1072: "cardax", - 1073: "bridgecontrol", - 1074: "warmspotMgmt", - 1075: "rdrmshc", - 1076: "dab-sti-c", - 1077: "imgames", - 1078: "avocent-proxy", - 1079: "asprovatalk", - 1080: "socks", - 1081: "pvuniwien", - 1082: "amt-esd-prot", - 1083: "ansoft-lm-1", - 1084: "ansoft-lm-2", - 1085: "webobjects", - 1086: "cplscrambler-lg", - 1087: "cplscrambler-in", - 1088: "cplscrambler-al", - 1089: "ff-annunc", - 1090: "ff-fms", - 1091: "ff-sm", - 1092: "obrpd", - 1093: "proofd", - 1094: "rootd", - 1095: "nicelink", - 1096: "cnrprotocol", - 1097: "sunclustermgr", - 1098: "rmiactivation", - 1099: "rmiregistry", - 1100: "mctp", - 1101: "pt2-discover", - 1102: "adobeserver-1", - 1103: "adobeserver-2", - 1104: "xrl", - 1105: "ftranhc", - 1106: "isoipsigport-1", - 1107: "isoipsigport-2", - 1108: "ratio-adp", - 1110: "webadmstart", - 1111: "lmsocialserver", - 1112: "icp", - 1113: "ltp-deepspace", - 1114: "mini-sql", - 1115: "ardus-trns", - 1116: "ardus-cntl", - 1117: "ardus-mtrns", - 1118: "sacred", - 1119: "bnetgame", - 1120: "bnetfile", - 1121: "rmpp", - 1122: "availant-mgr", - 1123: "murray", - 1124: "hpvmmcontrol", - 1125: "hpvmmagent", - 1126: "hpvmmdata", - 1127: "kwdb-commn", - 1128: "saphostctrl", - 1129: "saphostctrls", - 1130: "casp", - 1131: "caspssl", - 1132: "kvm-via-ip", - 1133: "dfn", - 1134: "aplx", - 1135: "omnivision", - 1136: "hhb-gateway", - 1137: "trim", - 1138: "encrypted-admin", - 1139: "evm", - 1140: "autonoc", - 1141: "mxomss", - 1142: "edtools", - 1143: "imyx", - 1144: "fuscript", - 1145: "x9-icue", - 1146: "audit-transfer", - 1147: "capioverlan", - 1148: "elfiq-repl", - 1149: "bvtsonar", - 1150: "blaze", - 1151: "unizensus", - 1152: "winpoplanmess", - 1153: "c1222-acse", - 1154: "resacommunity", - 1155: "nfa", - 1156: "iascontrol-oms", - 1157: "iascontrol", - 1158: "dbcontrol-oms", - 1159: "oracle-oms", - 1160: "olsv", - 1161: "health-polling", - 1162: "health-trap", - 1163: "sddp", - 1164: "qsm-proxy", - 1165: "qsm-gui", - 1166: "qsm-remote", - 1167: "cisco-ipsla", - 1168: "vchat", - 1169: "tripwire", - 1170: "atc-lm", - 1171: "atc-appserver", - 1172: "dnap", - 1173: "d-cinema-rrp", - 1174: "fnet-remote-ui", - 1175: "dossier", - 1176: "indigo-server", - 1177: "dkmessenger", - 1178: "sgi-storman", - 1179: "b2n", - 1180: "mc-client", - 1181: "3comnetman", - 1182: "accelenet", - 1183: "llsurfup-http", - 1184: "llsurfup-https", - 1185: "catchpole", - 1186: "mysql-cluster", - 1187: "alias", - 1188: "hp-webadmin", - 1189: "unet", - 1190: "commlinx-avl", - 1191: "gpfs", - 1192: "caids-sensor", - 1193: "fiveacross", - 1194: "openvpn", - 1195: "rsf-1", - 1196: "netmagic", - 1197: "carrius-rshell", - 1198: "cajo-discovery", - 1199: "dmidi", - 1200: "scol", - 1201: "nucleus-sand", - 1202: "caiccipc", - 1203: "ssslic-mgr", - 1204: "ssslog-mgr", - 1205: "accord-mgc", - 1206: "anthony-data", - 1207: "metasage", - 1208: "seagull-ais", - 1209: "ipcd3", - 1210: "eoss", - 1211: "groove-dpp", - 1212: "lupa", - 1213: "mpc-lifenet", - 1214: "kazaa", - 1215: "scanstat-1", - 1216: "etebac5", - 1217: "hpss-ndapi", - 1218: "aeroflight-ads", - 1219: "aeroflight-ret", - 1220: "qt-serveradmin", - 1221: "sweetware-apps", - 1222: "nerv", - 1223: "tgp", - 1224: "vpnz", - 1225: "slinkysearch", - 1226: "stgxfws", - 1227: "dns2go", - 1228: "florence", - 1229: "zented", - 1230: "periscope", - 1231: "menandmice-lpm", - 1232: "first-defense", - 1233: "univ-appserver", - 1234: "search-agent", - 1235: "mosaicsyssvc1", - 1236: "bvcontrol", - 1237: "tsdos390", - 1238: "hacl-qs", - 1239: "nmsd", - 1240: "instantia", - 1241: "nessus", - 1242: "nmasoverip", - 1243: "serialgateway", - 1244: "isbconference1", - 1245: "isbconference2", - 1246: "payrouter", - 1247: "visionpyramid", - 1248: "hermes", - 1249: "mesavistaco", - 1250: "swldy-sias", - 1251: "servergraph", - 1252: "bspne-pcc", - 1253: "q55-pcc", - 1254: "de-noc", - 1255: "de-cache-query", - 1256: "de-server", - 1257: "shockwave2", - 1258: "opennl", - 1259: "opennl-voice", - 1260: "ibm-ssd", - 1261: "mpshrsv", - 1262: "qnts-orb", - 1263: "dka", - 1264: "prat", - 1265: "dssiapi", - 1266: "dellpwrappks", - 1267: "epc", - 1268: "propel-msgsys", - 1269: "watilapp", - 1270: "opsmgr", - 1271: "excw", - 1272: "cspmlockmgr", - 1273: "emc-gateway", - 1274: "t1distproc", - 1275: "ivcollector", - 1277: "miva-mqs", - 1278: "dellwebadmin-1", - 1279: "dellwebadmin-2", - 1280: "pictrography", - 1281: "healthd", - 1282: "emperion", - 1283: "productinfo", - 1284: "iee-qfx", - 1285: "neoiface", - 1286: "netuitive", - 1287: "routematch", - 1288: "navbuddy", - 1289: "jwalkserver", - 1290: "winjaserver", - 1291: "seagulllms", - 1292: "dsdn", - 1293: "pkt-krb-ipsec", - 1294: "cmmdriver", - 1295: "ehtp", - 1296: "dproxy", - 1297: "sdproxy", - 1298: "lpcp", - 1299: "hp-sci", - 1300: "h323hostcallsc", - 1301: "ci3-software-1", - 1302: "ci3-software-2", - 1303: "sftsrv", - 1304: "boomerang", - 1305: "pe-mike", - 1306: "re-conn-proto", - 1307: "pacmand", - 1308: "odsi", - 1309: "jtag-server", - 1310: "husky", - 1311: "rxmon", - 1312: "sti-envision", - 1313: "bmc-patroldb", - 1314: "pdps", - 1315: "els", - 1316: "exbit-escp", - 1317: "vrts-ipcserver", - 1318: "krb5gatekeeper", - 1319: "amx-icsp", - 1320: "amx-axbnet", - 1321: "pip", - 1322: "novation", - 1323: "brcd", - 1324: "delta-mcp", - 1325: "dx-instrument", - 1326: "wimsic", - 1327: "ultrex", - 1328: "ewall", - 1329: "netdb-export", - 1330: "streetperfect", - 1331: "intersan", - 1332: "pcia-rxp-b", - 1333: "passwrd-policy", - 1334: "writesrv", - 1335: "digital-notary", - 1336: "ischat", - 1337: "menandmice-dns", - 1338: "wmc-log-svc", - 1339: "kjtsiteserver", - 1340: "naap", - 1341: "qubes", - 1342: "esbroker", - 1343: "re101", - 1344: "icap", - 1345: "vpjp", - 1346: "alta-ana-lm", - 1347: "bbn-mmc", - 1348: "bbn-mmx", - 1349: "sbook", - 1350: "editbench", - 1351: "equationbuilder", - 1352: "lotusnote", - 1353: "relief", - 1354: "XSIP-network", - 1355: "intuitive-edge", - 1356: "cuillamartin", - 1357: "pegboard", - 1358: "connlcli", - 1359: "ftsrv", - 1360: "mimer", - 1361: "linx", - 1362: "timeflies", - 1363: "ndm-requester", - 1364: "ndm-server", - 1365: "adapt-sna", - 1366: "netware-csp", - 1367: "dcs", - 1368: "screencast", - 1369: "gv-us", - 1370: "us-gv", - 1371: "fc-cli", - 1372: "fc-ser", - 1373: "chromagrafx", - 1374: "molly", - 1375: "bytex", - 1376: "ibm-pps", - 1377: "cichlid", - 1378: "elan", - 1379: "dbreporter", - 1380: "telesis-licman", - 1381: "apple-licman", - 1382: "udt-os", - 1383: "gwha", - 1384: "os-licman", - 1385: "atex-elmd", - 1386: "checksum", - 1387: "cadsi-lm", - 1388: "objective-dbc", - 1389: "iclpv-dm", - 1390: "iclpv-sc", - 1391: "iclpv-sas", - 1392: "iclpv-pm", - 1393: "iclpv-nls", - 1394: "iclpv-nlc", - 1395: "iclpv-wsm", - 1396: "dvl-activemail", - 1397: "audio-activmail", - 1398: "video-activmail", - 1399: "cadkey-licman", - 1400: "cadkey-tablet", - 1401: "goldleaf-licman", - 1402: "prm-sm-np", - 1403: "prm-nm-np", - 1404: "igi-lm", - 1405: "ibm-res", - 1406: "netlabs-lm", - 1407: "dbsa-lm", - 1408: "sophia-lm", - 1409: "here-lm", - 1410: "hiq", - 1411: "af", - 1412: "innosys", - 1413: "innosys-acl", - 1414: "ibm-mqseries", - 1415: "dbstar", - 1416: "novell-lu6-2", - 1417: "timbuktu-srv1", - 1418: "timbuktu-srv2", - 1419: "timbuktu-srv3", - 1420: "timbuktu-srv4", - 1421: "gandalf-lm", - 1422: "autodesk-lm", - 1423: "essbase", - 1424: "hybrid", - 1425: "zion-lm", - 1426: "sais", - 1427: "mloadd", - 1428: "informatik-lm", - 1429: "nms", - 1430: "tpdu", - 1431: "rgtp", - 1432: "blueberry-lm", - 1433: "ms-sql-s", - 1434: "ms-sql-m", - 1435: "ibm-cics", - 1436: "saism", - 1437: "tabula", - 1438: "eicon-server", - 1439: "eicon-x25", - 1440: "eicon-slp", - 1441: "cadis-1", - 1442: "cadis-2", - 1443: "ies-lm", - 1444: "marcam-lm", - 1445: "proxima-lm", - 1446: "ora-lm", - 1447: "apri-lm", - 1448: "oc-lm", - 1449: "peport", - 1450: "dwf", - 1451: "infoman", - 1452: "gtegsc-lm", - 1453: "genie-lm", - 1454: "interhdl-elmd", - 1455: "esl-lm", - 1456: "dca", - 1457: "valisys-lm", - 1458: "nrcabq-lm", - 1459: "proshare1", - 1460: "proshare2", - 1461: "ibm-wrless-lan", - 1462: "world-lm", - 1463: "nucleus", - 1464: "msl-lmd", - 1465: "pipes", - 1466: "oceansoft-lm", - 1467: "csdmbase", - 1468: "csdm", - 1469: "aal-lm", - 1470: "uaiact", - 1471: "csdmbase", - 1472: "csdm", - 1473: "openmath", - 1474: "telefinder", - 1475: "taligent-lm", - 1476: "clvm-cfg", - 1477: "ms-sna-server", - 1478: "ms-sna-base", - 1479: "dberegister", - 1480: "pacerforum", - 1481: "airs", - 1482: "miteksys-lm", - 1483: "afs", - 1484: "confluent", - 1485: "lansource", - 1486: "nms-topo-serv", - 1487: "localinfosrvr", - 1488: "docstor", - 1489: "dmdocbroker", - 1490: "insitu-conf", - 1492: "stone-design-1", - 1493: "netmap-lm", - 1494: "ica", - 1495: "cvc", - 1496: "liberty-lm", - 1497: "rfx-lm", - 1498: "sybase-sqlany", - 1499: "fhc", - 1500: "vlsi-lm", - 1501: "saiscm", - 1502: "shivadiscovery", - 1503: "imtc-mcs", - 1504: "evb-elm", - 1505: "funkproxy", - 1506: "utcd", - 1507: "symplex", - 1508: "diagmond", - 1509: "robcad-lm", - 1510: "mvx-lm", - 1511: "3l-l1", - 1512: "wins", - 1513: "fujitsu-dtc", - 1514: "fujitsu-dtcns", - 1515: "ifor-protocol", - 1516: "vpad", - 1517: "vpac", - 1518: "vpvd", - 1519: "vpvc", - 1520: "atm-zip-office", - 1521: "ncube-lm", - 1522: "ricardo-lm", - 1523: "cichild-lm", - 1524: "ingreslock", - 1525: "orasrv", - 1526: "pdap-np", - 1527: "tlisrv", - 1529: "coauthor", - 1530: "rap-service", - 1531: "rap-listen", - 1532: "miroconnect", - 1533: "virtual-places", - 1534: "micromuse-lm", - 1535: "ampr-info", - 1536: "ampr-inter", - 1537: "sdsc-lm", - 1538: "3ds-lm", - 1539: "intellistor-lm", - 1540: "rds", - 1541: "rds2", - 1542: "gridgen-elmd", - 1543: "simba-cs", - 1544: "aspeclmd", - 1545: "vistium-share", - 1546: "abbaccuray", - 1547: "laplink", - 1548: "axon-lm", - 1549: "shivahose", - 1550: "3m-image-lm", - 1551: "hecmtl-db", - 1552: "pciarray", - 1553: "sna-cs", - 1554: "caci-lm", - 1555: "livelan", - 1556: "veritas-pbx", - 1557: "arbortext-lm", - 1558: "xingmpeg", - 1559: "web2host", - 1560: "asci-val", - 1561: "facilityview", - 1562: "pconnectmgr", - 1563: "cadabra-lm", - 1564: "pay-per-view", - 1565: "winddlb", - 1566: "corelvideo", - 1567: "jlicelmd", - 1568: "tsspmap", - 1569: "ets", - 1570: "orbixd", - 1571: "rdb-dbs-disp", - 1572: "chip-lm", - 1573: "itscomm-ns", - 1574: "mvel-lm", - 1575: "oraclenames", - 1576: "moldflow-lm", - 1577: "hypercube-lm", - 1578: "jacobus-lm", - 1579: "ioc-sea-lm", - 1580: "tn-tl-r1", - 1581: "mil-2045-47001", - 1582: "msims", - 1583: "simbaexpress", - 1584: "tn-tl-fd2", - 1585: "intv", - 1586: "ibm-abtact", - 1587: "pra-elmd", - 1588: "triquest-lm", - 1589: "vqp", - 1590: "gemini-lm", - 1591: "ncpm-pm", - 1592: "commonspace", - 1593: "mainsoft-lm", - 1594: "sixtrak", - 1595: "radio", - 1596: "radio-sm", - 1597: "orbplus-iiop", - 1598: "picknfs", - 1599: "simbaservices", - 1600: "issd", - 1601: "aas", - 1602: "inspect", - 1603: "picodbc", - 1604: "icabrowser", - 1605: "slp", - 1606: "slm-api", - 1607: "stt", - 1608: "smart-lm", - 1609: "isysg-lm", - 1610: "taurus-wh", - 1611: "ill", - 1612: "netbill-trans", - 1613: "netbill-keyrep", - 1614: "netbill-cred", - 1615: "netbill-auth", - 1616: "netbill-prod", - 1617: "nimrod-agent", - 1618: "skytelnet", - 1619: "xs-openstorage", - 1620: "faxportwinport", - 1621: "softdataphone", - 1622: "ontime", - 1623: "jaleosnd", - 1624: "udp-sr-port", - 1625: "svs-omagent", - 1626: "shockwave", - 1627: "t128-gateway", - 1628: "lontalk-norm", - 1629: "lontalk-urgnt", - 1630: "oraclenet8cman", - 1631: "visitview", - 1632: "pammratc", - 1633: "pammrpc", - 1634: "loaprobe", - 1635: "edb-server1", - 1636: "isdc", - 1637: "islc", - 1638: "ismc", - 1639: "cert-initiator", - 1640: "cert-responder", - 1641: "invision", - 1642: "isis-am", - 1643: "isis-ambc", - 1644: "saiseh", - 1645: "sightline", - 1646: "sa-msg-port", - 1647: "rsap", - 1648: "concurrent-lm", - 1649: "kermit", - 1650: "nkd", - 1651: "shiva-confsrvr", - 1652: "xnmp", - 1653: "alphatech-lm", - 1654: "stargatealerts", - 1655: "dec-mbadmin", - 1656: "dec-mbadmin-h", - 1657: "fujitsu-mmpdc", - 1658: "sixnetudr", - 1659: "sg-lm", - 1660: "skip-mc-gikreq", - 1661: "netview-aix-1", - 1662: "netview-aix-2", - 1663: "netview-aix-3", - 1664: "netview-aix-4", - 1665: "netview-aix-5", - 1666: "netview-aix-6", - 1667: "netview-aix-7", - 1668: "netview-aix-8", - 1669: "netview-aix-9", - 1670: "netview-aix-10", - 1671: "netview-aix-11", - 1672: "netview-aix-12", - 1673: "proshare-mc-1", - 1674: "proshare-mc-2", - 1675: "pdp", - 1676: "netcomm1", - 1677: "groupwise", - 1678: "prolink", - 1679: "darcorp-lm", - 1680: "microcom-sbp", - 1681: "sd-elmd", - 1682: "lanyon-lantern", - 1683: "ncpm-hip", - 1684: "snaresecure", - 1685: "n2nremote", - 1686: "cvmon", - 1687: "nsjtp-ctrl", - 1688: "nsjtp-data", - 1689: "firefox", - 1690: "ng-umds", - 1691: "empire-empuma", - 1692: "sstsys-lm", - 1693: "rrirtr", - 1694: "rrimwm", - 1695: "rrilwm", - 1696: "rrifmm", - 1697: "rrisat", - 1698: "rsvp-encap-1", - 1699: "rsvp-encap-2", - 1700: "mps-raft", - 1701: "l2f", - 1702: "deskshare", - 1703: "hb-engine", - 1704: "bcs-broker", - 1705: "slingshot", - 1706: "jetform", - 1707: "vdmplay", - 1708: "gat-lmd", - 1709: "centra", - 1710: "impera", - 1711: "pptconference", - 1712: "registrar", - 1713: "conferencetalk", - 1714: "sesi-lm", - 1715: "houdini-lm", - 1716: "xmsg", - 1717: "fj-hdnet", - 1718: "h323gatedisc", - 1719: "h323gatestat", - 1720: "h323hostcall", - 1721: "caicci", - 1722: "hks-lm", - 1723: "pptp", - 1724: "csbphonemaster", - 1725: "iden-ralp", - 1726: "iberiagames", - 1727: "winddx", - 1728: "telindus", - 1729: "citynl", - 1730: "roketz", - 1731: "msiccp", - 1732: "proxim", - 1733: "siipat", - 1734: "cambertx-lm", - 1735: "privatechat", - 1736: "street-stream", - 1737: "ultimad", - 1738: "gamegen1", - 1739: "webaccess", - 1740: "encore", - 1741: "cisco-net-mgmt", - 1742: "3Com-nsd", - 1743: "cinegrfx-lm", - 1744: "ncpm-ft", - 1745: "remote-winsock", - 1746: "ftrapid-1", - 1747: "ftrapid-2", - 1748: "oracle-em1", - 1749: "aspen-services", - 1750: "sslp", - 1751: "swiftnet", - 1752: "lofr-lm", - 1753: "predatar-comms", - 1754: "oracle-em2", - 1755: "ms-streaming", - 1756: "capfast-lmd", - 1757: "cnhrp", - 1758: "tftp-mcast", - 1759: "spss-lm", - 1760: "www-ldap-gw", - 1761: "cft-0", - 1762: "cft-1", - 1763: "cft-2", - 1764: "cft-3", - 1765: "cft-4", - 1766: "cft-5", - 1767: "cft-6", - 1768: "cft-7", - 1769: "bmc-net-adm", - 1770: "bmc-net-svc", - 1771: "vaultbase", - 1772: "essweb-gw", - 1773: "kmscontrol", - 1774: "global-dtserv", - 1775: "vdab", - 1776: "femis", - 1777: "powerguardian", - 1778: "prodigy-intrnet", - 1779: "pharmasoft", - 1780: "dpkeyserv", - 1781: "answersoft-lm", - 1782: "hp-hcip", - 1784: "finle-lm", - 1785: "windlm", - 1786: "funk-logger", - 1787: "funk-license", - 1788: "psmond", - 1789: "hello", - 1790: "nmsp", - 1791: "ea1", - 1792: "ibm-dt-2", - 1793: "rsc-robot", - 1794: "cera-bcm", - 1795: "dpi-proxy", - 1796: "vocaltec-admin", - 1797: "uma", - 1798: "etp", - 1799: "netrisk", - 1800: "ansys-lm", - 1801: "msmq", - 1802: "concomp1", - 1803: "hp-hcip-gwy", - 1804: "enl", - 1805: "enl-name", - 1806: "musiconline", - 1807: "fhsp", - 1808: "oracle-vp2", - 1809: "oracle-vp1", - 1810: "jerand-lm", - 1811: "scientia-sdb", - 1812: "radius", - 1813: "radius-acct", - 1814: "tdp-suite", - 1815: "mmpft", - 1816: "harp", - 1817: "rkb-oscs", - 1818: "etftp", - 1819: "plato-lm", - 1820: "mcagent", - 1821: "donnyworld", - 1822: "es-elmd", - 1823: "unisys-lm", - 1824: "metrics-pas", - 1825: "direcpc-video", - 1826: "ardt", - 1827: "asi", - 1828: "itm-mcell-u", - 1829: "optika-emedia", - 1830: "net8-cman", - 1831: "myrtle", - 1832: "tht-treasure", - 1833: "udpradio", - 1834: "ardusuni", - 1835: "ardusmul", - 1836: "ste-smsc", - 1837: "csoft1", - 1838: "talnet", - 1839: "netopia-vo1", - 1840: "netopia-vo2", - 1841: "netopia-vo3", - 1842: "netopia-vo4", - 1843: "netopia-vo5", - 1844: "direcpc-dll", - 1845: "altalink", - 1846: "tunstall-pnc", - 1847: "slp-notify", - 1848: "fjdocdist", - 1849: "alpha-sms", - 1850: "gsi", - 1851: "ctcd", - 1852: "virtual-time", - 1853: "vids-avtp", - 1854: "buddy-draw", - 1855: "fiorano-rtrsvc", - 1856: "fiorano-msgsvc", - 1857: "datacaptor", - 1858: "privateark", - 1859: "gammafetchsvr", - 1860: "sunscalar-svc", - 1861: "lecroy-vicp", - 1862: "mysql-cm-agent", - 1863: "msnp", - 1864: "paradym-31port", - 1865: "entp", - 1866: "swrmi", - 1867: "udrive", - 1868: "viziblebrowser", - 1869: "transact", - 1870: "sunscalar-dns", - 1871: "canocentral0", - 1872: "canocentral1", - 1873: "fjmpjps", - 1874: "fjswapsnp", - 1875: "westell-stats", - 1876: "ewcappsrv", - 1877: "hp-webqosdb", - 1878: "drmsmc", - 1879: "nettgain-nms", - 1880: "vsat-control", - 1881: "ibm-mqseries2", - 1882: "ecsqdmn", - 1883: "ibm-mqisdp", - 1884: "idmaps", - 1885: "vrtstrapserver", - 1886: "leoip", - 1887: "filex-lport", - 1888: "ncconfig", - 1889: "unify-adapter", - 1890: "wilkenlistener", - 1891: "childkey-notif", - 1892: "childkey-ctrl", - 1893: "elad", - 1894: "o2server-port", - 1896: "b-novative-ls", - 1897: "metaagent", - 1898: "cymtec-port", - 1899: "mc2studios", - 1900: "ssdp", - 1901: "fjicl-tep-a", - 1902: "fjicl-tep-b", - 1903: "linkname", - 1904: "fjicl-tep-c", - 1905: "sugp", - 1906: "tpmd", - 1907: "intrastar", - 1908: "dawn", - 1909: "global-wlink", - 1910: "ultrabac", - 1911: "mtp", - 1912: "rhp-iibp", - 1913: "armadp", - 1914: "elm-momentum", - 1915: "facelink", - 1916: "persona", - 1917: "noagent", - 1918: "can-nds", - 1919: "can-dch", - 1920: "can-ferret", - 1921: "noadmin", - 1922: "tapestry", - 1923: "spice", - 1924: "xiip", - 1925: "discovery-port", - 1926: "egs", - 1927: "videte-cipc", - 1928: "emsd-port", - 1929: "bandwiz-system", - 1930: "driveappserver", - 1931: "amdsched", - 1932: "ctt-broker", - 1933: "xmapi", - 1934: "xaapi", - 1935: "macromedia-fcs", - 1936: "jetcmeserver", - 1937: "jwserver", - 1938: "jwclient", - 1939: "jvserver", - 1940: "jvclient", - 1941: "dic-aida", - 1942: "res", - 1943: "beeyond-media", - 1944: "close-combat", - 1945: "dialogic-elmd", - 1946: "tekpls", - 1947: "sentinelsrm", - 1948: "eye2eye", - 1949: "ismaeasdaqlive", - 1950: "ismaeasdaqtest", - 1951: "bcs-lmserver", - 1952: "mpnjsc", - 1953: "rapidbase", - 1954: "abr-api", - 1955: "abr-secure", - 1956: "vrtl-vmf-ds", - 1957: "unix-status", - 1958: "dxadmind", - 1959: "simp-all", - 1960: "nasmanager", - 1961: "bts-appserver", - 1962: "biap-mp", - 1963: "webmachine", - 1964: "solid-e-engine", - 1965: "tivoli-npm", - 1966: "slush", - 1967: "sns-quote", - 1968: "lipsinc", - 1969: "lipsinc1", - 1970: "netop-rc", - 1971: "netop-school", - 1972: "intersys-cache", - 1973: "dlsrap", - 1974: "drp", - 1975: "tcoflashagent", - 1976: "tcoregagent", - 1977: "tcoaddressbook", - 1978: "unisql", - 1979: "unisql-java", - 1980: "pearldoc-xact", - 1981: "p2pq", - 1982: "estamp", - 1983: "lhtp", - 1984: "bb", - 1985: "hsrp", - 1986: "licensedaemon", - 1987: "tr-rsrb-p1", - 1988: "tr-rsrb-p2", - 1989: "tr-rsrb-p3", - 1990: "stun-p1", - 1991: "stun-p2", - 1992: "stun-p3", - 1993: "snmp-tcp-port", - 1994: "stun-port", - 1995: "perf-port", - 1996: "tr-rsrb-port", - 1997: "gdp-port", - 1998: "x25-svc-port", - 1999: "tcp-id-port", - 2000: "cisco-sccp", - 2001: "dc", - 2002: "globe", - 2003: "brutus", - 2004: "mailbox", - 2005: "berknet", - 2006: "invokator", - 2007: "dectalk", - 2008: "conf", - 2009: "news", - 2010: "search", - 2011: "raid-cc", - 2012: "ttyinfo", - 2013: "raid-am", - 2014: "troff", - 2015: "cypress", - 2016: "bootserver", - 2017: "cypress-stat", - 2018: "terminaldb", - 2019: "whosockami", - 2020: "xinupageserver", - 2021: "servexec", - 2022: "down", - 2023: "xinuexpansion3", - 2024: "xinuexpansion4", - 2025: "ellpack", - 2026: "scrabble", - 2027: "shadowserver", - 2028: "submitserver", - 2029: "hsrpv6", - 2030: "device2", - 2031: "mobrien-chat", - 2032: "blackboard", - 2033: "glogger", - 2034: "scoremgr", - 2035: "imsldoc", - 2036: "e-dpnet", - 2037: "applus", - 2038: "objectmanager", - 2039: "prizma", - 2040: "lam", - 2041: "interbase", - 2042: "isis", - 2043: "isis-bcast", - 2044: "rimsl", - 2045: "cdfunc", - 2046: "sdfunc", - 2047: "dls", - 2048: "dls-monitor", - 2049: "shilp", - 2050: "av-emb-config", - 2051: "epnsdp", - 2052: "clearvisn", - 2053: "lot105-ds-upd", - 2054: "weblogin", - 2055: "iop", - 2056: "omnisky", - 2057: "rich-cp", - 2058: "newwavesearch", - 2059: "bmc-messaging", - 2060: "teleniumdaemon", - 2061: "netmount", - 2062: "icg-swp", - 2063: "icg-bridge", - 2064: "icg-iprelay", - 2065: "dlsrpn", - 2066: "aura", - 2067: "dlswpn", - 2068: "avauthsrvprtcl", - 2069: "event-port", - 2070: "ah-esp-encap", - 2071: "acp-port", - 2072: "msync", - 2073: "gxs-data-port", - 2074: "vrtl-vmf-sa", - 2075: "newlixengine", - 2076: "newlixconfig", - 2077: "tsrmagt", - 2078: "tpcsrvr", - 2079: "idware-router", - 2080: "autodesk-nlm", - 2081: "kme-trap-port", - 2082: "infowave", - 2083: "radsec", - 2084: "sunclustergeo", - 2085: "ada-cip", - 2086: "gnunet", - 2087: "eli", - 2088: "ip-blf", - 2089: "sep", - 2090: "lrp", - 2091: "prp", - 2092: "descent3", - 2093: "nbx-cc", - 2094: "nbx-au", - 2095: "nbx-ser", - 2096: "nbx-dir", - 2097: "jetformpreview", - 2098: "dialog-port", - 2099: "h2250-annex-g", - 2100: "amiganetfs", - 2101: "rtcm-sc104", - 2102: "zephyr-srv", - 2103: "zephyr-clt", - 2104: "zephyr-hm", - 2105: "minipay", - 2106: "mzap", - 2107: "bintec-admin", - 2108: "comcam", - 2109: "ergolight", - 2110: "umsp", - 2111: "dsatp", - 2112: "idonix-metanet", - 2113: "hsl-storm", - 2114: "newheights", - 2115: "kdm", - 2116: "ccowcmr", - 2117: "mentaclient", - 2118: "mentaserver", - 2119: "gsigatekeeper", - 2120: "qencp", - 2121: "scientia-ssdb", - 2122: "caupc-remote", - 2123: "gtp-control", - 2124: "elatelink", - 2125: "lockstep", - 2126: "pktcable-cops", - 2127: "index-pc-wb", - 2128: "net-steward", - 2129: "cs-live", - 2130: "xds", - 2131: "avantageb2b", - 2132: "solera-epmap", - 2133: "zymed-zpp", - 2134: "avenue", - 2135: "gris", - 2136: "appworxsrv", - 2137: "connect", - 2138: "unbind-cluster", - 2139: "ias-auth", - 2140: "ias-reg", - 2141: "ias-admind", - 2142: "tdmoip", - 2143: "lv-jc", - 2144: "lv-ffx", - 2145: "lv-pici", - 2146: "lv-not", - 2147: "lv-auth", - 2148: "veritas-ucl", - 2149: "acptsys", - 2150: "dynamic3d", - 2151: "docent", - 2152: "gtp-user", - 2153: "ctlptc", - 2154: "stdptc", - 2155: "brdptc", - 2156: "trp", - 2157: "xnds", - 2158: "touchnetplus", - 2159: "gdbremote", - 2160: "apc-2160", - 2161: "apc-2161", - 2162: "navisphere", - 2163: "navisphere-sec", - 2164: "ddns-v3", - 2165: "x-bone-api", - 2166: "iwserver", - 2167: "raw-serial", - 2168: "easy-soft-mux", - 2169: "brain", - 2170: "eyetv", - 2171: "msfw-storage", - 2172: "msfw-s-storage", - 2173: "msfw-replica", - 2174: "msfw-array", - 2175: "airsync", - 2176: "rapi", - 2177: "qwave", - 2178: "bitspeer", - 2179: "vmrdp", - 2180: "mc-gt-srv", - 2181: "eforward", - 2182: "cgn-stat", - 2183: "cgn-config", - 2184: "nvd", - 2185: "onbase-dds", - 2186: "gtaua", - 2187: "ssmc", - 2188: "radware-rpm", - 2189: "radware-rpm-s", - 2190: "tivoconnect", - 2191: "tvbus", - 2192: "asdis", - 2193: "drwcs", - 2197: "mnp-exchange", - 2198: "onehome-remote", - 2199: "onehome-help", - 2200: "ici", - 2201: "ats", - 2202: "imtc-map", - 2203: "b2-runtime", - 2204: "b2-license", - 2205: "jps", - 2206: "hpocbus", - 2207: "hpssd", - 2208: "hpiod", - 2209: "rimf-ps", - 2210: "noaaport", - 2211: "emwin", - 2212: "leecoposserver", - 2213: "kali", - 2214: "rpi", - 2215: "ipcore", - 2216: "vtu-comms", - 2217: "gotodevice", - 2218: "bounzza", - 2219: "netiq-ncap", - 2220: "netiq", - 2221: "rockwell-csp1", - 2222: "EtherNet-IP-1", - 2223: "rockwell-csp2", - 2224: "efi-mg", - 2225: "rcip-itu", - 2226: "di-drm", - 2227: "di-msg", - 2228: "ehome-ms", - 2229: "datalens", - 2230: "queueadm", - 2231: "wimaxasncp", - 2232: "ivs-video", - 2233: "infocrypt", - 2234: "directplay", - 2235: "sercomm-wlink", - 2236: "nani", - 2237: "optech-port1-lm", - 2238: "aviva-sna", - 2239: "imagequery", - 2240: "recipe", - 2241: "ivsd", - 2242: "foliocorp", - 2243: "magicom", - 2244: "nmsserver", - 2245: "hao", - 2246: "pc-mta-addrmap", - 2247: "antidotemgrsvr", - 2248: "ums", - 2249: "rfmp", - 2250: "remote-collab", - 2251: "dif-port", - 2252: "njenet-ssl", - 2253: "dtv-chan-req", - 2254: "seispoc", - 2255: "vrtp", - 2256: "pcc-mfp", - 2257: "simple-tx-rx", - 2258: "rcts", - 2260: "apc-2260", - 2261: "comotionmaster", - 2262: "comotionback", - 2263: "ecwcfg", - 2264: "apx500api-1", - 2265: "apx500api-2", - 2266: "mfserver", - 2267: "ontobroker", - 2268: "amt", - 2269: "mikey", - 2270: "starschool", - 2271: "mmcals", - 2272: "mmcal", - 2273: "mysql-im", - 2274: "pcttunnell", - 2275: "ibridge-data", - 2276: "ibridge-mgmt", - 2277: "bluectrlproxy", - 2278: "s3db", - 2279: "xmquery", - 2280: "lnvpoller", - 2281: "lnvconsole", - 2282: "lnvalarm", - 2283: "lnvstatus", - 2284: "lnvmaps", - 2285: "lnvmailmon", - 2286: "nas-metering", - 2287: "dna", - 2288: "netml", - 2289: "dict-lookup", - 2290: "sonus-logging", - 2291: "eapsp", - 2292: "mib-streaming", - 2293: "npdbgmngr", - 2294: "konshus-lm", - 2295: "advant-lm", - 2296: "theta-lm", - 2297: "d2k-datamover1", - 2298: "d2k-datamover2", - 2299: "pc-telecommute", - 2300: "cvmmon", - 2301: "cpq-wbem", - 2302: "binderysupport", - 2303: "proxy-gateway", - 2304: "attachmate-uts", - 2305: "mt-scaleserver", - 2306: "tappi-boxnet", - 2307: "pehelp", - 2308: "sdhelp", - 2309: "sdserver", - 2310: "sdclient", - 2311: "messageservice", - 2312: "wanscaler", - 2313: "iapp", - 2314: "cr-websystems", - 2315: "precise-sft", - 2316: "sent-lm", - 2317: "attachmate-g32", - 2318: "cadencecontrol", - 2319: "infolibria", - 2320: "siebel-ns", - 2321: "rdlap", - 2322: "ofsd", - 2323: "3d-nfsd", - 2324: "cosmocall", - 2325: "ansysli", - 2326: "idcp", - 2327: "xingcsm", - 2328: "netrix-sftm", - 2329: "nvd", - 2330: "tscchat", - 2331: "agentview", - 2332: "rcc-host", - 2333: "snapp", - 2334: "ace-client", - 2335: "ace-proxy", - 2336: "appleugcontrol", - 2337: "ideesrv", - 2338: "norton-lambert", - 2339: "3com-webview", - 2340: "wrs-registry", - 2341: "xiostatus", - 2342: "manage-exec", - 2343: "nati-logos", - 2344: "fcmsys", - 2345: "dbm", - 2346: "redstorm-join", - 2347: "redstorm-find", - 2348: "redstorm-info", - 2349: "redstorm-diag", - 2350: "psbserver", - 2351: "psrserver", - 2352: "pslserver", - 2353: "pspserver", - 2354: "psprserver", - 2355: "psdbserver", - 2356: "gxtelmd", - 2357: "unihub-server", - 2358: "futrix", - 2359: "flukeserver", - 2360: "nexstorindltd", - 2361: "tl1", - 2362: "digiman", - 2363: "mediacntrlnfsd", - 2364: "oi-2000", - 2365: "dbref", - 2366: "qip-login", - 2367: "service-ctrl", - 2368: "opentable", - 2370: "l3-hbmon", - 2371: "hp-rda", - 2372: "lanmessenger", - 2373: "remographlm", - 2374: "hydra", - 2375: "docker", - 2376: "docker-s", - 2379: "etcd-client", - 2380: "etcd-server", - 2381: "compaq-https", - 2382: "ms-olap3", - 2383: "ms-olap4", - 2384: "sd-request", - 2385: "sd-data", - 2386: "virtualtape", - 2387: "vsamredirector", - 2388: "mynahautostart", - 2389: "ovsessionmgr", - 2390: "rsmtp", - 2391: "3com-net-mgmt", - 2392: "tacticalauth", - 2393: "ms-olap1", - 2394: "ms-olap2", - 2395: "lan900-remote", - 2396: "wusage", - 2397: "ncl", - 2398: "orbiter", - 2399: "fmpro-fdal", - 2400: "opequus-server", - 2401: "cvspserver", - 2402: "taskmaster2000", - 2403: "taskmaster2000", - 2404: "iec-104", - 2405: "trc-netpoll", - 2406: "jediserver", - 2407: "orion", - 2408: "railgun-webaccl", - 2409: "sns-protocol", - 2410: "vrts-registry", - 2411: "netwave-ap-mgmt", - 2412: "cdn", - 2413: "orion-rmi-reg", - 2414: "beeyond", - 2415: "codima-rtp", - 2416: "rmtserver", - 2417: "composit-server", - 2418: "cas", - 2419: "attachmate-s2s", - 2420: "dslremote-mgmt", - 2421: "g-talk", - 2422: "crmsbits", - 2423: "rnrp", - 2424: "kofax-svr", - 2425: "fjitsuappmgr", - 2427: "mgcp-gateway", - 2428: "ott", - 2429: "ft-role", - 2430: "venus", - 2431: "venus-se", - 2432: "codasrv", - 2433: "codasrv-se", - 2434: "pxc-epmap", - 2435: "optilogic", - 2436: "topx", - 2437: "unicontrol", - 2438: "msp", - 2439: "sybasedbsynch", - 2440: "spearway", - 2441: "pvsw-inet", - 2442: "netangel", - 2443: "powerclientcsf", - 2444: "btpp2sectrans", - 2445: "dtn1", - 2446: "bues-service", - 2447: "ovwdb", - 2448: "hpppssvr", - 2449: "ratl", - 2450: "netadmin", - 2451: "netchat", - 2452: "snifferclient", - 2453: "madge-ltd", - 2454: "indx-dds", - 2455: "wago-io-system", - 2456: "altav-remmgt", - 2457: "rapido-ip", - 2458: "griffin", - 2459: "community", - 2460: "ms-theater", - 2461: "qadmifoper", - 2462: "qadmifevent", - 2463: "lsi-raid-mgmt", - 2464: "direcpc-si", - 2465: "lbm", - 2466: "lbf", - 2467: "high-criteria", - 2468: "qip-msgd", - 2469: "mti-tcs-comm", - 2470: "taskman-port", - 2471: "seaodbc", - 2472: "c3", - 2473: "aker-cdp", - 2474: "vitalanalysis", - 2475: "ace-server", - 2476: "ace-svr-prop", - 2477: "ssm-cvs", - 2478: "ssm-cssps", - 2479: "ssm-els", - 2480: "powerexchange", - 2481: "giop", - 2482: "giop-ssl", - 2483: "ttc", - 2484: "ttc-ssl", - 2485: "netobjects1", - 2486: "netobjects2", - 2487: "pns", - 2488: "moy-corp", - 2489: "tsilb", - 2490: "qip-qdhcp", - 2491: "conclave-cpp", - 2492: "groove", - 2493: "talarian-mqs", - 2494: "bmc-ar", - 2495: "fast-rem-serv", - 2496: "dirgis", - 2497: "quaddb", - 2498: "odn-castraq", - 2499: "unicontrol", - 2500: "rtsserv", - 2501: "rtsclient", - 2502: "kentrox-prot", - 2503: "nms-dpnss", - 2504: "wlbs", - 2505: "ppcontrol", - 2506: "jbroker", - 2507: "spock", - 2508: "jdatastore", - 2509: "fjmpss", - 2510: "fjappmgrbulk", - 2511: "metastorm", - 2512: "citrixima", - 2513: "citrixadmin", - 2514: "facsys-ntp", - 2515: "facsys-router", - 2516: "maincontrol", - 2517: "call-sig-trans", - 2518: "willy", - 2519: "globmsgsvc", - 2520: "pvsw", - 2521: "adaptecmgr", - 2522: "windb", - 2523: "qke-llc-v3", - 2524: "optiwave-lm", - 2525: "ms-v-worlds", - 2526: "ema-sent-lm", - 2527: "iqserver", - 2528: "ncr-ccl", - 2529: "utsftp", - 2530: "vrcommerce", - 2531: "ito-e-gui", - 2532: "ovtopmd", - 2533: "snifferserver", - 2534: "combox-web-acc", - 2535: "madcap", - 2536: "btpp2audctr1", - 2537: "upgrade", - 2538: "vnwk-prapi", - 2539: "vsiadmin", - 2540: "lonworks", - 2541: "lonworks2", - 2542: "udrawgraph", - 2543: "reftek", - 2544: "novell-zen", - 2545: "sis-emt", - 2546: "vytalvaultbrtp", - 2547: "vytalvaultvsmp", - 2548: "vytalvaultpipe", - 2549: "ipass", - 2550: "ads", - 2551: "isg-uda-server", - 2552: "call-logging", - 2553: "efidiningport", - 2554: "vcnet-link-v10", - 2555: "compaq-wcp", - 2556: "nicetec-nmsvc", - 2557: "nicetec-mgmt", - 2558: "pclemultimedia", - 2559: "lstp", - 2560: "labrat", - 2561: "mosaixcc", - 2562: "delibo", - 2563: "cti-redwood", - 2564: "hp-3000-telnet", - 2565: "coord-svr", - 2566: "pcs-pcw", - 2567: "clp", - 2568: "spamtrap", - 2569: "sonuscallsig", - 2570: "hs-port", - 2571: "cecsvc", - 2572: "ibp", - 2573: "trustestablish", - 2574: "blockade-bpsp", - 2575: "hl7", - 2576: "tclprodebugger", - 2577: "scipticslsrvr", - 2578: "rvs-isdn-dcp", - 2579: "mpfoncl", - 2580: "tributary", - 2581: "argis-te", - 2582: "argis-ds", - 2583: "mon", - 2584: "cyaserv", - 2585: "netx-server", - 2586: "netx-agent", - 2587: "masc", - 2588: "privilege", - 2589: "quartus-tcl", - 2590: "idotdist", - 2591: "maytagshuffle", - 2592: "netrek", - 2593: "mns-mail", - 2594: "dts", - 2595: "worldfusion1", - 2596: "worldfusion2", - 2597: "homesteadglory", - 2598: "citriximaclient", - 2599: "snapd", - 2600: "hpstgmgr", - 2601: "discp-client", - 2602: "discp-server", - 2603: "servicemeter", - 2604: "nsc-ccs", - 2605: "nsc-posa", - 2606: "netmon", - 2607: "connection", - 2608: "wag-service", - 2609: "system-monitor", - 2610: "versa-tek", - 2611: "lionhead", - 2612: "qpasa-agent", - 2613: "smntubootstrap", - 2614: "neveroffline", - 2615: "firepower", - 2616: "appswitch-emp", - 2617: "cmadmin", - 2618: "priority-e-com", - 2619: "bruce", - 2620: "lpsrecommender", - 2621: "miles-apart", - 2622: "metricadbc", - 2623: "lmdp", - 2624: "aria", - 2625: "blwnkl-port", - 2626: "gbjd816", - 2627: "moshebeeri", - 2628: "dict", - 2629: "sitaraserver", - 2630: "sitaramgmt", - 2631: "sitaradir", - 2632: "irdg-post", - 2633: "interintelli", - 2634: "pk-electronics", - 2635: "backburner", - 2636: "solve", - 2637: "imdocsvc", - 2638: "sybaseanywhere", - 2639: "aminet", - 2640: "sai-sentlm", - 2641: "hdl-srv", - 2642: "tragic", - 2643: "gte-samp", - 2644: "travsoft-ipx-t", - 2645: "novell-ipx-cmd", - 2646: "and-lm", - 2647: "syncserver", - 2648: "upsnotifyprot", - 2649: "vpsipport", - 2650: "eristwoguns", - 2651: "ebinsite", - 2652: "interpathpanel", - 2653: "sonus", - 2654: "corel-vncadmin", - 2655: "unglue", - 2656: "kana", - 2657: "sns-dispatcher", - 2658: "sns-admin", - 2659: "sns-query", - 2660: "gcmonitor", - 2661: "olhost", - 2662: "bintec-capi", - 2663: "bintec-tapi", - 2664: "patrol-mq-gm", - 2665: "patrol-mq-nm", - 2666: "extensis", - 2667: "alarm-clock-s", - 2668: "alarm-clock-c", - 2669: "toad", - 2670: "tve-announce", - 2671: "newlixreg", - 2672: "nhserver", - 2673: "firstcall42", - 2674: "ewnn", - 2675: "ttc-etap", - 2676: "simslink", - 2677: "gadgetgate1way", - 2678: "gadgetgate2way", - 2679: "syncserverssl", - 2680: "pxc-sapxom", - 2681: "mpnjsomb", - 2683: "ncdloadbalance", - 2684: "mpnjsosv", - 2685: "mpnjsocl", - 2686: "mpnjsomg", - 2687: "pq-lic-mgmt", - 2688: "md-cg-http", - 2689: "fastlynx", - 2690: "hp-nnm-data", - 2691: "itinternet", - 2692: "admins-lms", - 2694: "pwrsevent", - 2695: "vspread", - 2696: "unifyadmin", - 2697: "oce-snmp-trap", - 2698: "mck-ivpip", - 2699: "csoft-plusclnt", - 2700: "tqdata", - 2701: "sms-rcinfo", - 2702: "sms-xfer", - 2703: "sms-chat", - 2704: "sms-remctrl", - 2705: "sds-admin", - 2706: "ncdmirroring", - 2707: "emcsymapiport", - 2708: "banyan-net", - 2709: "supermon", - 2710: "sso-service", - 2711: "sso-control", - 2712: "aocp", - 2713: "raventbs", - 2714: "raventdm", - 2715: "hpstgmgr2", - 2716: "inova-ip-disco", - 2717: "pn-requester", - 2718: "pn-requester2", - 2719: "scan-change", - 2720: "wkars", - 2721: "smart-diagnose", - 2722: "proactivesrvr", - 2723: "watchdog-nt", - 2724: "qotps", - 2725: "msolap-ptp2", - 2726: "tams", - 2727: "mgcp-callagent", - 2728: "sqdr", - 2729: "tcim-control", - 2730: "nec-raidplus", - 2731: "fyre-messanger", - 2732: "g5m", - 2733: "signet-ctf", - 2734: "ccs-software", - 2735: "netiq-mc", - 2736: "radwiz-nms-srv", - 2737: "srp-feedback", - 2738: "ndl-tcp-ois-gw", - 2739: "tn-timing", - 2740: "alarm", - 2741: "tsb", - 2742: "tsb2", - 2743: "murx", - 2744: "honyaku", - 2745: "urbisnet", - 2746: "cpudpencap", - 2747: "fjippol-swrly", - 2748: "fjippol-polsvr", - 2749: "fjippol-cnsl", - 2750: "fjippol-port1", - 2751: "fjippol-port2", - 2752: "rsisysaccess", - 2753: "de-spot", - 2754: "apollo-cc", - 2755: "expresspay", - 2756: "simplement-tie", - 2757: "cnrp", - 2758: "apollo-status", - 2759: "apollo-gms", - 2760: "sabams", - 2761: "dicom-iscl", - 2762: "dicom-tls", - 2763: "desktop-dna", - 2764: "data-insurance", - 2765: "qip-audup", - 2766: "compaq-scp", - 2767: "uadtc", - 2768: "uacs", - 2769: "exce", - 2770: "veronica", - 2771: "vergencecm", - 2772: "auris", - 2773: "rbakcup1", - 2774: "rbakcup2", - 2775: "smpp", - 2776: "ridgeway1", - 2777: "ridgeway2", - 2778: "gwen-sonya", - 2779: "lbc-sync", - 2780: "lbc-control", - 2781: "whosells", - 2782: "everydayrc", - 2783: "aises", - 2784: "www-dev", - 2785: "aic-np", - 2786: "aic-oncrpc", - 2787: "piccolo", - 2788: "fryeserv", - 2789: "media-agent", - 2790: "plgproxy", - 2791: "mtport-regist", - 2792: "f5-globalsite", - 2793: "initlsmsad", - 2795: "livestats", - 2796: "ac-tech", - 2797: "esp-encap", - 2798: "tmesis-upshot", - 2799: "icon-discover", - 2800: "acc-raid", - 2801: "igcp", - 2802: "veritas-tcp1", - 2803: "btprjctrl", - 2804: "dvr-esm", - 2805: "wta-wsp-s", - 2806: "cspuni", - 2807: "cspmulti", - 2808: "j-lan-p", - 2809: "corbaloc", - 2810: "netsteward", - 2811: "gsiftp", - 2812: "atmtcp", - 2813: "llm-pass", - 2814: "llm-csv", - 2815: "lbc-measure", - 2816: "lbc-watchdog", - 2817: "nmsigport", - 2818: "rmlnk", - 2819: "fc-faultnotify", - 2820: "univision", - 2821: "vrts-at-port", - 2822: "ka0wuc", - 2823: "cqg-netlan", - 2824: "cqg-netlan-1", - 2826: "slc-systemlog", - 2827: "slc-ctrlrloops", - 2828: "itm-lm", - 2829: "silkp1", - 2830: "silkp2", - 2831: "silkp3", - 2832: "silkp4", - 2833: "glishd", - 2834: "evtp", - 2835: "evtp-data", - 2836: "catalyst", - 2837: "repliweb", - 2838: "starbot", - 2839: "nmsigport", - 2840: "l3-exprt", - 2841: "l3-ranger", - 2842: "l3-hawk", - 2843: "pdnet", - 2844: "bpcp-poll", - 2845: "bpcp-trap", - 2846: "aimpp-hello", - 2847: "aimpp-port-req", - 2848: "amt-blc-port", - 2849: "fxp", - 2850: "metaconsole", - 2851: "webemshttp", - 2852: "bears-01", - 2853: "ispipes", - 2854: "infomover", - 2855: "msrp", - 2856: "cesdinv", - 2857: "simctlp", - 2858: "ecnp", - 2859: "activememory", - 2860: "dialpad-voice1", - 2861: "dialpad-voice2", - 2862: "ttg-protocol", - 2863: "sonardata", - 2864: "astromed-main", - 2865: "pit-vpn", - 2866: "iwlistener", - 2867: "esps-portal", - 2868: "npep-messaging", - 2869: "icslap", - 2870: "daishi", - 2871: "msi-selectplay", - 2872: "radix", - 2874: "dxmessagebase1", - 2875: "dxmessagebase2", - 2876: "sps-tunnel", - 2877: "bluelance", - 2878: "aap", - 2879: "ucentric-ds", - 2880: "synapse", - 2881: "ndsp", - 2882: "ndtp", - 2883: "ndnp", - 2884: "flashmsg", - 2885: "topflow", - 2886: "responselogic", - 2887: "aironetddp", - 2888: "spcsdlobby", - 2889: "rsom", - 2890: "cspclmulti", - 2891: "cinegrfx-elmd", - 2892: "snifferdata", - 2893: "vseconnector", - 2894: "abacus-remote", - 2895: "natuslink", - 2896: "ecovisiong6-1", - 2897: "citrix-rtmp", - 2898: "appliance-cfg", - 2899: "powergemplus", - 2900: "quicksuite", - 2901: "allstorcns", - 2902: "netaspi", - 2903: "suitcase", - 2904: "m2ua", - 2905: "m3ua", - 2906: "caller9", - 2907: "webmethods-b2b", - 2908: "mao", - 2909: "funk-dialout", - 2910: "tdaccess", - 2911: "blockade", - 2912: "epicon", - 2913: "boosterware", - 2914: "gamelobby", - 2915: "tksocket", - 2916: "elvin-server", - 2917: "elvin-client", - 2918: "kastenchasepad", - 2919: "roboer", - 2920: "roboeda", - 2921: "cesdcdman", - 2922: "cesdcdtrn", - 2923: "wta-wsp-wtp-s", - 2924: "precise-vip", - 2926: "mobile-file-dl", - 2927: "unimobilectrl", - 2928: "redstone-cpss", - 2929: "amx-webadmin", - 2930: "amx-weblinx", - 2931: "circle-x", - 2932: "incp", - 2933: "4-tieropmgw", - 2934: "4-tieropmcli", - 2935: "qtp", - 2936: "otpatch", - 2937: "pnaconsult-lm", - 2938: "sm-pas-1", - 2939: "sm-pas-2", - 2940: "sm-pas-3", - 2941: "sm-pas-4", - 2942: "sm-pas-5", - 2943: "ttnrepository", - 2944: "megaco-h248", - 2945: "h248-binary", - 2946: "fjsvmpor", - 2947: "gpsd", - 2948: "wap-push", - 2949: "wap-pushsecure", - 2950: "esip", - 2951: "ottp", - 2952: "mpfwsas", - 2953: "ovalarmsrv", - 2954: "ovalarmsrv-cmd", - 2955: "csnotify", - 2956: "ovrimosdbman", - 2957: "jmact5", - 2958: "jmact6", - 2959: "rmopagt", - 2960: "dfoxserver", - 2961: "boldsoft-lm", - 2962: "iph-policy-cli", - 2963: "iph-policy-adm", - 2964: "bullant-srap", - 2965: "bullant-rap", - 2966: "idp-infotrieve", - 2967: "ssc-agent", - 2968: "enpp", - 2969: "essp", - 2970: "index-net", - 2971: "netclip", - 2972: "pmsm-webrctl", - 2973: "svnetworks", - 2974: "signal", - 2975: "fjmpcm", - 2976: "cns-srv-port", - 2977: "ttc-etap-ns", - 2978: "ttc-etap-ds", - 2979: "h263-video", - 2980: "wimd", - 2981: "mylxamport", - 2982: "iwb-whiteboard", - 2983: "netplan", - 2984: "hpidsadmin", - 2985: "hpidsagent", - 2986: "stonefalls", - 2987: "identify", - 2988: "hippad", - 2989: "zarkov", - 2990: "boscap", - 2991: "wkstn-mon", - 2992: "avenyo", - 2993: "veritas-vis1", - 2994: "veritas-vis2", - 2995: "idrs", - 2996: "vsixml", - 2997: "rebol", - 2998: "realsecure", - 2999: "remoteware-un", - 3000: "hbci", - 3001: "origo-native", - 3002: "exlm-agent", - 3003: "cgms", - 3004: "csoftragent", - 3005: "geniuslm", - 3006: "ii-admin", - 3007: "lotusmtap", - 3008: "midnight-tech", - 3009: "pxc-ntfy", - 3010: "gw", - 3011: "trusted-web", - 3012: "twsdss", - 3013: "gilatskysurfer", - 3014: "broker-service", - 3015: "nati-dstp", - 3016: "notify-srvr", - 3017: "event-listener", - 3018: "srvc-registry", - 3019: "resource-mgr", - 3020: "cifs", - 3021: "agriserver", - 3022: "csregagent", - 3023: "magicnotes", - 3024: "nds-sso", - 3025: "arepa-raft", - 3026: "agri-gateway", - 3027: "LiebDevMgmt-C", - 3028: "LiebDevMgmt-DM", - 3029: "LiebDevMgmt-A", - 3030: "arepa-cas", - 3031: "eppc", - 3032: "redwood-chat", - 3033: "pdb", - 3034: "osmosis-aeea", - 3035: "fjsv-gssagt", - 3036: "hagel-dump", - 3037: "hp-san-mgmt", - 3038: "santak-ups", - 3039: "cogitate", - 3040: "tomato-springs", - 3041: "di-traceware", - 3042: "journee", - 3043: "brp", - 3044: "epp", - 3045: "responsenet", - 3046: "di-ase", - 3047: "hlserver", - 3048: "pctrader", - 3049: "nsws", - 3050: "gds-db", - 3051: "galaxy-server", - 3052: "apc-3052", - 3053: "dsom-server", - 3054: "amt-cnf-prot", - 3055: "policyserver", - 3056: "cdl-server", - 3057: "goahead-fldup", - 3058: "videobeans", - 3059: "qsoft", - 3060: "interserver", - 3061: "cautcpd", - 3062: "ncacn-ip-tcp", - 3063: "ncadg-ip-udp", - 3064: "rprt", - 3065: "slinterbase", - 3066: "netattachsdmp", - 3067: "fjhpjp", - 3068: "ls3bcast", - 3069: "ls3", - 3070: "mgxswitch", - 3071: "csd-mgmt-port", - 3072: "csd-monitor", - 3073: "vcrp", - 3074: "xbox", - 3075: "orbix-locator", - 3076: "orbix-config", - 3077: "orbix-loc-ssl", - 3078: "orbix-cfg-ssl", - 3079: "lv-frontpanel", - 3080: "stm-pproc", - 3081: "tl1-lv", - 3082: "tl1-raw", - 3083: "tl1-telnet", - 3084: "itm-mccs", - 3085: "pcihreq", - 3086: "jdl-dbkitchen", - 3087: "asoki-sma", - 3088: "xdtp", - 3089: "ptk-alink", - 3090: "stss", - 3091: "1ci-smcs", - 3093: "rapidmq-center", - 3094: "rapidmq-reg", - 3095: "panasas", - 3096: "ndl-aps", - 3098: "umm-port", - 3099: "chmd", - 3100: "opcon-xps", - 3101: "hp-pxpib", - 3102: "slslavemon", - 3103: "autocuesmi", - 3104: "autocuelog", - 3105: "cardbox", - 3106: "cardbox-http", - 3107: "business", - 3108: "geolocate", - 3109: "personnel", - 3110: "sim-control", - 3111: "wsynch", - 3112: "ksysguard", - 3113: "cs-auth-svr", - 3114: "ccmad", - 3115: "mctet-master", - 3116: "mctet-gateway", - 3117: "mctet-jserv", - 3118: "pkagent", - 3119: "d2000kernel", - 3120: "d2000webserver", - 3121: "pcmk-remote", - 3122: "vtr-emulator", - 3123: "edix", - 3124: "beacon-port", - 3125: "a13-an", - 3127: "ctx-bridge", - 3128: "ndl-aas", - 3129: "netport-id", - 3130: "icpv2", - 3131: "netbookmark", - 3132: "ms-rule-engine", - 3133: "prism-deploy", - 3134: "ecp", - 3135: "peerbook-port", - 3136: "grubd", - 3137: "rtnt-1", - 3138: "rtnt-2", - 3139: "incognitorv", - 3140: "ariliamulti", - 3141: "vmodem", - 3142: "rdc-wh-eos", - 3143: "seaview", - 3144: "tarantella", - 3145: "csi-lfap", - 3146: "bears-02", - 3147: "rfio", - 3148: "nm-game-admin", - 3149: "nm-game-server", - 3150: "nm-asses-admin", - 3151: "nm-assessor", - 3152: "feitianrockey", - 3153: "s8-client-port", - 3154: "ccmrmi", - 3155: "jpegmpeg", - 3156: "indura", - 3157: "e3consultants", - 3158: "stvp", - 3159: "navegaweb-port", - 3160: "tip-app-server", - 3161: "doc1lm", - 3162: "sflm", - 3163: "res-sap", - 3164: "imprs", - 3165: "newgenpay", - 3166: "sossecollector", - 3167: "nowcontact", - 3168: "poweronnud", - 3169: "serverview-as", - 3170: "serverview-asn", - 3171: "serverview-gf", - 3172: "serverview-rm", - 3173: "serverview-icc", - 3174: "armi-server", - 3175: "t1-e1-over-ip", - 3176: "ars-master", - 3177: "phonex-port", - 3178: "radclientport", - 3179: "h2gf-w-2m", - 3180: "mc-brk-srv", - 3181: "bmcpatrolagent", - 3182: "bmcpatrolrnvu", - 3183: "cops-tls", - 3184: "apogeex-port", - 3185: "smpppd", - 3186: "iiw-port", - 3187: "odi-port", - 3188: "brcm-comm-port", - 3189: "pcle-infex", - 3190: "csvr-proxy", - 3191: "csvr-sslproxy", - 3192: "firemonrcc", - 3193: "spandataport", - 3194: "magbind", - 3195: "ncu-1", - 3196: "ncu-2", - 3197: "embrace-dp-s", - 3198: "embrace-dp-c", - 3199: "dmod-workspace", - 3200: "tick-port", - 3201: "cpq-tasksmart", - 3202: "intraintra", - 3203: "netwatcher-mon", - 3204: "netwatcher-db", - 3205: "isns", - 3206: "ironmail", - 3207: "vx-auth-port", - 3208: "pfu-prcallback", - 3209: "netwkpathengine", - 3210: "flamenco-proxy", - 3211: "avsecuremgmt", - 3212: "surveyinst", - 3213: "neon24x7", - 3214: "jmq-daemon-1", - 3215: "jmq-daemon-2", - 3216: "ferrari-foam", - 3217: "unite", - 3218: "smartpackets", - 3219: "wms-messenger", - 3220: "xnm-ssl", - 3221: "xnm-clear-text", - 3222: "glbp", - 3223: "digivote", - 3224: "aes-discovery", - 3225: "fcip-port", - 3226: "isi-irp", - 3227: "dwnmshttp", - 3228: "dwmsgserver", - 3229: "global-cd-port", - 3230: "sftdst-port", - 3231: "vidigo", - 3232: "mdtp", - 3233: "whisker", - 3234: "alchemy", - 3235: "mdap-port", - 3236: "apparenet-ts", - 3237: "apparenet-tps", - 3238: "apparenet-as", - 3239: "apparenet-ui", - 3240: "triomotion", - 3241: "sysorb", - 3242: "sdp-id-port", - 3243: "timelot", - 3244: "onesaf", - 3245: "vieo-fe", - 3246: "dvt-system", - 3247: "dvt-data", - 3248: "procos-lm", - 3249: "ssp", - 3250: "hicp", - 3251: "sysscanner", - 3252: "dhe", - 3253: "pda-data", - 3254: "pda-sys", - 3255: "semaphore", - 3256: "cpqrpm-agent", - 3257: "cpqrpm-server", - 3258: "ivecon-port", - 3259: "epncdp2", - 3260: "iscsi-target", - 3261: "winshadow", - 3262: "necp", - 3263: "ecolor-imager", - 3264: "ccmail", - 3265: "altav-tunnel", - 3266: "ns-cfg-server", - 3267: "ibm-dial-out", - 3268: "msft-gc", - 3269: "msft-gc-ssl", - 3270: "verismart", - 3271: "csoft-prev", - 3272: "user-manager", - 3273: "sxmp", - 3274: "ordinox-server", - 3275: "samd", - 3276: "maxim-asics", - 3277: "awg-proxy", - 3278: "lkcmserver", - 3279: "admind", - 3280: "vs-server", - 3281: "sysopt", - 3282: "datusorb", - 3283: "Apple Remote Desktop (Net Assistant)", - 3284: "4talk", - 3285: "plato", - 3286: "e-net", - 3287: "directvdata", - 3288: "cops", - 3289: "enpc", - 3290: "caps-lm", - 3291: "sah-lm", - 3292: "cart-o-rama", - 3293: "fg-fps", - 3294: "fg-gip", - 3295: "dyniplookup", - 3296: "rib-slm", - 3297: "cytel-lm", - 3298: "deskview", - 3299: "pdrncs", - 3302: "mcs-fastmail", - 3303: "opsession-clnt", - 3304: "opsession-srvr", - 3305: "odette-ftp", - 3306: "mysql", - 3307: "opsession-prxy", - 3308: "tns-server", - 3309: "tns-adv", - 3310: "dyna-access", - 3311: "mcns-tel-ret", - 3312: "appman-server", - 3313: "uorb", - 3314: "uohost", - 3315: "cdid", - 3316: "aicc-cmi", - 3317: "vsaiport", - 3318: "ssrip", - 3319: "sdt-lmd", - 3320: "officelink2000", - 3321: "vnsstr", - 3326: "sftu", - 3327: "bbars", - 3328: "egptlm", - 3329: "hp-device-disc", - 3330: "mcs-calypsoicf", - 3331: "mcs-messaging", - 3332: "mcs-mailsvr", - 3333: "dec-notes", - 3334: "directv-web", - 3335: "directv-soft", - 3336: "directv-tick", - 3337: "directv-catlg", - 3338: "anet-b", - 3339: "anet-l", - 3340: "anet-m", - 3341: "anet-h", - 3342: "webtie", - 3343: "ms-cluster-net", - 3344: "bnt-manager", - 3345: "influence", - 3346: "trnsprntproxy", - 3347: "phoenix-rpc", - 3348: "pangolin-laser", - 3349: "chevinservices", - 3350: "findviatv", - 3351: "btrieve", - 3352: "ssql", - 3353: "fatpipe", - 3354: "suitjd", - 3355: "ordinox-dbase", - 3356: "upnotifyps", - 3357: "adtech-test", - 3358: "mpsysrmsvr", - 3359: "wg-netforce", - 3360: "kv-server", - 3361: "kv-agent", - 3362: "dj-ilm", - 3363: "nati-vi-server", - 3364: "creativeserver", - 3365: "contentserver", - 3366: "creativepartnr", - 3372: "tip2", - 3373: "lavenir-lm", - 3374: "cluster-disc", - 3375: "vsnm-agent", - 3376: "cdbroker", - 3377: "cogsys-lm", - 3378: "wsicopy", - 3379: "socorfs", - 3380: "sns-channels", - 3381: "geneous", - 3382: "fujitsu-neat", - 3383: "esp-lm", - 3384: "hp-clic", - 3385: "qnxnetman", - 3386: "gprs-data", - 3387: "backroomnet", - 3388: "cbserver", - 3389: "ms-wbt-server", - 3390: "dsc", - 3391: "savant", - 3392: "efi-lm", - 3393: "d2k-tapestry1", - 3394: "d2k-tapestry2", - 3395: "dyna-lm", - 3396: "printer-agent", - 3397: "cloanto-lm", - 3398: "mercantile", - 3399: "csms", - 3400: "csms2", - 3401: "filecast", - 3402: "fxaengine-net", - 3405: "nokia-ann-ch1", - 3406: "nokia-ann-ch2", - 3407: "ldap-admin", - 3408: "BESApi", - 3409: "networklens", - 3410: "networklenss", - 3411: "biolink-auth", - 3412: "xmlblaster", - 3413: "svnet", - 3414: "wip-port", - 3415: "bcinameservice", - 3416: "commandport", - 3417: "csvr", - 3418: "rnmap", - 3419: "softaudit", - 3420: "ifcp-port", - 3421: "bmap", - 3422: "rusb-sys-port", - 3423: "xtrm", - 3424: "xtrms", - 3425: "agps-port", - 3426: "arkivio", - 3427: "websphere-snmp", - 3428: "twcss", - 3429: "gcsp", - 3430: "ssdispatch", - 3431: "ndl-als", - 3432: "osdcp", - 3433: "opnet-smp", - 3434: "opencm", - 3435: "pacom", - 3436: "gc-config", - 3437: "autocueds", - 3438: "spiral-admin", - 3439: "hri-port", - 3440: "ans-console", - 3441: "connect-client", - 3442: "connect-server", - 3443: "ov-nnm-websrv", - 3444: "denali-server", - 3445: "monp", - 3446: "3comfaxrpc", - 3447: "directnet", - 3448: "dnc-port", - 3449: "hotu-chat", - 3450: "castorproxy", - 3451: "asam", - 3452: "sabp-signal", - 3453: "pscupd", - 3454: "mira", - 3455: "prsvp", - 3456: "vat", - 3457: "vat-control", - 3458: "d3winosfi", - 3459: "integral", - 3460: "edm-manager", - 3461: "edm-stager", - 3462: "edm-std-notify", - 3463: "edm-adm-notify", - 3464: "edm-mgr-sync", - 3465: "edm-mgr-cntrl", - 3466: "workflow", - 3467: "rcst", - 3468: "ttcmremotectrl", - 3469: "pluribus", - 3470: "jt400", - 3471: "jt400-ssl", - 3472: "jaugsremotec-1", - 3473: "jaugsremotec-2", - 3474: "ttntspauto", - 3475: "genisar-port", - 3476: "nppmp", - 3477: "ecomm", - 3478: "stun", - 3479: "twrpc", - 3480: "plethora", - 3481: "cleanerliverc", - 3482: "vulture", - 3483: "slim-devices", - 3484: "gbs-stp", - 3485: "celatalk", - 3486: "ifsf-hb-port", - 3487: "ltctcp", - 3488: "fs-rh-srv", - 3489: "dtp-dia", - 3490: "colubris", - 3491: "swr-port", - 3492: "tvdumtray-port", - 3493: "nut", - 3494: "ibm3494", - 3495: "seclayer-tcp", - 3496: "seclayer-tls", - 3497: "ipether232port", - 3498: "dashpas-port", - 3499: "sccip-media", - 3500: "rtmp-port", - 3501: "isoft-p2p", - 3502: "avinstalldisc", - 3503: "lsp-ping", - 3504: "ironstorm", - 3505: "ccmcomm", - 3506: "apc-3506", - 3507: "nesh-broker", - 3508: "interactionweb", - 3509: "vt-ssl", - 3510: "xss-port", - 3511: "webmail-2", - 3512: "aztec", - 3513: "arcpd", - 3514: "must-p2p", - 3515: "must-backplane", - 3516: "smartcard-port", - 3517: "802-11-iapp", - 3518: "artifact-msg", - 3519: "nvmsgd", - 3520: "galileolog", - 3521: "mc3ss", - 3522: "nssocketport", - 3523: "odeumservlink", - 3524: "ecmport", - 3525: "eisport", - 3526: "starquiz-port", - 3527: "beserver-msg-q", - 3528: "jboss-iiop", - 3529: "jboss-iiop-ssl", - 3530: "gf", - 3531: "joltid", - 3532: "raven-rmp", - 3533: "raven-rdp", - 3534: "urld-port", - 3535: "ms-la", - 3536: "snac", - 3537: "ni-visa-remote", - 3538: "ibm-diradm", - 3539: "ibm-diradm-ssl", - 3540: "pnrp-port", - 3541: "voispeed-port", - 3542: "hacl-monitor", - 3543: "qftest-lookup", - 3544: "teredo", - 3545: "camac", - 3547: "symantec-sim", - 3548: "interworld", - 3549: "tellumat-nms", - 3550: "ssmpp", - 3551: "apcupsd", - 3552: "taserver", - 3553: "rbr-discovery", - 3554: "questnotify", - 3555: "razor", - 3556: "sky-transport", - 3557: "personalos-001", - 3558: "mcp-port", - 3559: "cctv-port", - 3560: "iniserve-port", - 3561: "bmc-onekey", - 3562: "sdbproxy", - 3563: "watcomdebug", - 3564: "esimport", - 3565: "m2pa", - 3566: "quest-data-hub", - 3567: "enc-eps", - 3568: "enc-tunnel-sec", - 3569: "mbg-ctrl", - 3570: "mccwebsvr-port", - 3571: "megardsvr-port", - 3572: "megaregsvrport", - 3573: "tag-ups-1", - 3574: "dmaf-server", - 3575: "ccm-port", - 3576: "cmc-port", - 3577: "config-port", - 3578: "data-port", - 3579: "ttat3lb", - 3580: "nati-svrloc", - 3581: "kfxaclicensing", - 3582: "press", - 3583: "canex-watch", - 3584: "u-dbap", - 3585: "emprise-lls", - 3586: "emprise-lsc", - 3587: "p2pgroup", - 3588: "sentinel", - 3589: "isomair", - 3590: "wv-csp-sms", - 3591: "gtrack-server", - 3592: "gtrack-ne", - 3593: "bpmd", - 3594: "mediaspace", - 3595: "shareapp", - 3596: "iw-mmogame", - 3597: "a14", - 3598: "a15", - 3599: "quasar-server", - 3600: "trap-daemon", - 3601: "visinet-gui", - 3602: "infiniswitchcl", - 3603: "int-rcv-cntrl", - 3604: "bmc-jmx-port", - 3605: "comcam-io", - 3606: "splitlock", - 3607: "precise-i3", - 3608: "trendchip-dcp", - 3609: "cpdi-pidas-cm", - 3610: "echonet", - 3611: "six-degrees", - 3612: "hp-dataprotect", - 3613: "alaris-disc", - 3614: "sigma-port", - 3615: "start-network", - 3616: "cd3o-protocol", - 3617: "sharp-server", - 3618: "aairnet-1", - 3619: "aairnet-2", - 3620: "ep-pcp", - 3621: "ep-nsp", - 3622: "ff-lr-port", - 3623: "haipe-discover", - 3624: "dist-upgrade", - 3625: "volley", - 3626: "bvcdaemon-port", - 3627: "jamserverport", - 3628: "ept-machine", - 3629: "escvpnet", - 3630: "cs-remote-db", - 3631: "cs-services", - 3632: "distcc", - 3633: "wacp", - 3634: "hlibmgr", - 3635: "sdo", - 3636: "servistaitsm", - 3637: "scservp", - 3638: "ehp-backup", - 3639: "xap-ha", - 3640: "netplay-port1", - 3641: "netplay-port2", - 3642: "juxml-port", - 3643: "audiojuggler", - 3644: "ssowatch", - 3645: "cyc", - 3646: "xss-srv-port", - 3647: "splitlock-gw", - 3648: "fjcp", - 3649: "nmmp", - 3650: "prismiq-plugin", - 3651: "xrpc-registry", - 3652: "vxcrnbuport", - 3653: "tsp", - 3654: "vaprtm", - 3655: "abatemgr", - 3656: "abatjss", - 3657: "immedianet-bcn", - 3658: "ps-ams", - 3659: "apple-sasl", - 3660: "can-nds-ssl", - 3661: "can-ferret-ssl", - 3662: "pserver", - 3663: "dtp", - 3664: "ups-engine", - 3665: "ent-engine", - 3666: "eserver-pap", - 3667: "infoexch", - 3668: "dell-rm-port", - 3669: "casanswmgmt", - 3670: "smile", - 3671: "efcp", - 3672: "lispworks-orb", - 3673: "mediavault-gui", - 3674: "wininstall-ipc", - 3675: "calltrax", - 3676: "va-pacbase", - 3677: "roverlog", - 3678: "ipr-dglt", - 3679: "Escale (Newton Dock)", - 3680: "npds-tracker", - 3681: "bts-x73", - 3682: "cas-mapi", - 3683: "bmc-ea", - 3684: "faxstfx-port", - 3685: "dsx-agent", - 3686: "tnmpv2", - 3687: "simple-push", - 3688: "simple-push-s", - 3689: "daap", - 3690: "svn", - 3691: "magaya-network", - 3692: "intelsync", - 3695: "bmc-data-coll", - 3696: "telnetcpcd", - 3697: "nw-license", - 3698: "sagectlpanel", - 3699: "kpn-icw", - 3700: "lrs-paging", - 3701: "netcelera", - 3702: "ws-discovery", - 3703: "adobeserver-3", - 3704: "adobeserver-4", - 3705: "adobeserver-5", - 3706: "rt-event", - 3707: "rt-event-s", - 3708: "sun-as-iiops", - 3709: "ca-idms", - 3710: "portgate-auth", - 3711: "edb-server2", - 3712: "sentinel-ent", - 3713: "tftps", - 3714: "delos-dms", - 3715: "anoto-rendezv", - 3716: "wv-csp-sms-cir", - 3717: "wv-csp-udp-cir", - 3718: "opus-services", - 3719: "itelserverport", - 3720: "ufastro-instr", - 3721: "xsync", - 3722: "xserveraid", - 3723: "sychrond", - 3724: "blizwow", - 3725: "na-er-tip", - 3726: "array-manager", - 3727: "e-mdu", - 3728: "e-woa", - 3729: "fksp-audit", - 3730: "client-ctrl", - 3731: "smap", - 3732: "m-wnn", - 3733: "multip-msg", - 3734: "synel-data", - 3735: "pwdis", - 3736: "rs-rmi", - 3737: "xpanel", - 3738: "versatalk", - 3739: "launchbird-lm", - 3740: "heartbeat", - 3741: "wysdma", - 3742: "cst-port", - 3743: "ipcs-command", - 3744: "sasg", - 3745: "gw-call-port", - 3746: "linktest", - 3747: "linktest-s", - 3748: "webdata", - 3749: "cimtrak", - 3750: "cbos-ip-port", - 3751: "gprs-cube", - 3752: "vipremoteagent", - 3753: "nattyserver", - 3754: "timestenbroker", - 3755: "sas-remote-hlp", - 3756: "canon-capt", - 3757: "grf-port", - 3758: "apw-registry", - 3759: "exapt-lmgr", - 3760: "adtempusclient", - 3761: "gsakmp", - 3762: "gbs-smp", - 3763: "xo-wave", - 3764: "mni-prot-rout", - 3765: "rtraceroute", - 3766: "sitewatch-s", - 3767: "listmgr-port", - 3768: "rblcheckd", - 3769: "haipe-otnk", - 3770: "cindycollab", - 3771: "paging-port", - 3772: "ctp", - 3773: "ctdhercules", - 3774: "zicom", - 3775: "ispmmgr", - 3776: "dvcprov-port", - 3777: "jibe-eb", - 3778: "c-h-it-port", - 3779: "cognima", - 3780: "nnp", - 3781: "abcvoice-port", - 3782: "iso-tp0s", - 3783: "bim-pem", - 3784: "bfd-control", - 3785: "bfd-echo", - 3786: "upstriggervsw", - 3787: "fintrx", - 3788: "isrp-port", - 3789: "remotedeploy", - 3790: "quickbooksrds", - 3791: "tvnetworkvideo", - 3792: "sitewatch", - 3793: "dcsoftware", - 3794: "jaus", - 3795: "myblast", - 3796: "spw-dialer", - 3797: "idps", - 3798: "minilock", - 3799: "radius-dynauth", - 3800: "pwgpsi", - 3801: "ibm-mgr", - 3802: "vhd", - 3803: "soniqsync", - 3804: "iqnet-port", - 3805: "tcpdataserver", - 3806: "wsmlb", - 3807: "spugna", - 3808: "sun-as-iiops-ca", - 3809: "apocd", - 3810: "wlanauth", - 3811: "amp", - 3812: "neto-wol-server", - 3813: "rap-ip", - 3814: "neto-dcs", - 3815: "lansurveyorxml", - 3816: "sunlps-http", - 3817: "tapeware", - 3818: "crinis-hb", - 3819: "epl-slp", - 3820: "scp", - 3821: "pmcp", - 3822: "acp-discovery", - 3823: "acp-conduit", - 3824: "acp-policy", - 3825: "ffserver", - 3826: "warmux", - 3827: "netmpi", - 3828: "neteh", - 3829: "neteh-ext", - 3830: "cernsysmgmtagt", - 3831: "dvapps", - 3832: "xxnetserver", - 3833: "aipn-auth", - 3834: "spectardata", - 3835: "spectardb", - 3836: "markem-dcp", - 3837: "mkm-discovery", - 3838: "sos", - 3839: "amx-rms", - 3840: "flirtmitmir", - 3841: "shiprush-db-svr", - 3842: "nhci", - 3843: "quest-agent", - 3844: "rnm", - 3845: "v-one-spp", - 3846: "an-pcp", - 3847: "msfw-control", - 3848: "item", - 3849: "spw-dnspreload", - 3850: "qtms-bootstrap", - 3851: "spectraport", - 3852: "sse-app-config", - 3853: "sscan", - 3854: "stryker-com", - 3855: "opentrac", - 3856: "informer", - 3857: "trap-port", - 3858: "trap-port-mom", - 3859: "nav-port", - 3860: "sasp", - 3861: "winshadow-hd", - 3862: "giga-pocket", - 3863: "asap-tcp", - 3864: "asap-tcp-tls", - 3865: "xpl", - 3866: "dzdaemon", - 3867: "dzoglserver", - 3868: "diameter", - 3869: "ovsam-mgmt", - 3870: "ovsam-d-agent", - 3871: "avocent-adsap", - 3872: "oem-agent", - 3873: "fagordnc", - 3874: "sixxsconfig", - 3875: "pnbscada", - 3876: "dl-agent", - 3877: "xmpcr-interface", - 3878: "fotogcad", - 3879: "appss-lm", - 3880: "igrs", - 3881: "idac", - 3882: "msdts1", - 3883: "vrpn", - 3884: "softrack-meter", - 3885: "topflow-ssl", - 3886: "nei-management", - 3887: "ciphire-data", - 3888: "ciphire-serv", - 3889: "dandv-tester", - 3890: "ndsconnect", - 3891: "rtc-pm-port", - 3892: "pcc-image-port", - 3893: "cgi-starapi", - 3894: "syam-agent", - 3895: "syam-smc", - 3896: "sdo-tls", - 3897: "sdo-ssh", - 3898: "senip", - 3899: "itv-control", - 3900: "udt-os", - 3901: "nimsh", - 3902: "nimaux", - 3903: "charsetmgr", - 3904: "omnilink-port", - 3905: "mupdate", - 3906: "topovista-data", - 3907: "imoguia-port", - 3908: "hppronetman", - 3909: "surfcontrolcpa", - 3910: "prnrequest", - 3911: "prnstatus", - 3912: "gbmt-stars", - 3913: "listcrt-port", - 3914: "listcrt-port-2", - 3915: "agcat", - 3916: "wysdmc", - 3917: "aftmux", - 3918: "pktcablemmcops", - 3919: "hyperip", - 3920: "exasoftport1", - 3921: "herodotus-net", - 3922: "sor-update", - 3923: "symb-sb-port", - 3924: "mpl-gprs-port", - 3925: "zmp", - 3926: "winport", - 3927: "natdataservice", - 3928: "netboot-pxe", - 3929: "smauth-port", - 3930: "syam-webserver", - 3931: "msr-plugin-port", - 3932: "dyn-site", - 3933: "plbserve-port", - 3934: "sunfm-port", - 3935: "sdp-portmapper", - 3936: "mailprox", - 3937: "dvbservdsc", - 3938: "dbcontrol-agent", - 3939: "aamp", - 3940: "xecp-node", - 3941: "homeportal-web", - 3942: "srdp", - 3943: "tig", - 3944: "sops", - 3945: "emcads", - 3946: "backupedge", - 3947: "ccp", - 3948: "apdap", - 3949: "drip", - 3950: "namemunge", - 3951: "pwgippfax", - 3952: "i3-sessionmgr", - 3953: "xmlink-connect", - 3954: "adrep", - 3955: "p2pcommunity", - 3956: "gvcp", - 3957: "mqe-broker", - 3958: "mqe-agent", - 3959: "treehopper", - 3960: "bess", - 3961: "proaxess", - 3962: "sbi-agent", - 3963: "thrp", - 3964: "sasggprs", - 3965: "ati-ip-to-ncpe", - 3966: "bflckmgr", - 3967: "ppsms", - 3968: "ianywhere-dbns", - 3969: "landmarks", - 3970: "lanrevagent", - 3971: "lanrevserver", - 3972: "iconp", - 3973: "progistics", - 3974: "citysearch", - 3975: "airshot", - 3976: "opswagent", - 3977: "opswmanager", - 3978: "secure-cfg-svr", - 3979: "smwan", - 3980: "acms", - 3981: "starfish", - 3982: "eis", - 3983: "eisp", - 3984: "mapper-nodemgr", - 3985: "mapper-mapethd", - 3986: "mapper-ws-ethd", - 3987: "centerline", - 3988: "dcs-config", - 3989: "bv-queryengine", - 3990: "bv-is", - 3991: "bv-smcsrv", - 3992: "bv-ds", - 3993: "bv-agent", - 3995: "iss-mgmt-ssl", - 3996: "abcsoftware", - 3997: "agentsease-db", - 3998: "dnx", - 3999: "nvcnet", - 4000: "terabase", - 4001: "newoak", - 4002: "pxc-spvr-ft", - 4003: "pxc-splr-ft", - 4004: "pxc-roid", - 4005: "pxc-pin", - 4006: "pxc-spvr", - 4007: "pxc-splr", - 4008: "netcheque", - 4009: "chimera-hwm", - 4010: "samsung-unidex", - 4011: "altserviceboot", - 4012: "pda-gate", - 4013: "acl-manager", - 4014: "taiclock", - 4015: "talarian-mcast1", - 4016: "talarian-mcast2", - 4017: "talarian-mcast3", - 4018: "talarian-mcast4", - 4019: "talarian-mcast5", - 4020: "trap", - 4021: "nexus-portal", - 4022: "dnox", - 4023: "esnm-zoning", - 4024: "tnp1-port", - 4025: "partimage", - 4026: "as-debug", - 4027: "bxp", - 4028: "dtserver-port", - 4029: "ip-qsig", - 4030: "jdmn-port", - 4031: "suucp", - 4032: "vrts-auth-port", - 4033: "sanavigator", - 4034: "ubxd", - 4035: "wap-push-http", - 4036: "wap-push-https", - 4037: "ravehd", - 4038: "fazzt-ptp", - 4039: "fazzt-admin", - 4040: "yo-main", - 4041: "houston", - 4042: "ldxp", - 4043: "nirp", - 4044: "ltp", - 4045: "npp", - 4046: "acp-proto", - 4047: "ctp-state", - 4049: "wafs", - 4050: "cisco-wafs", - 4051: "cppdp", - 4052: "interact", - 4053: "ccu-comm-1", - 4054: "ccu-comm-2", - 4055: "ccu-comm-3", - 4056: "lms", - 4057: "wfm", - 4058: "kingfisher", - 4059: "dlms-cosem", - 4060: "dsmeter-iatc", - 4061: "ice-location", - 4062: "ice-slocation", - 4063: "ice-router", - 4064: "ice-srouter", - 4065: "avanti-cdp", - 4066: "pmas", - 4067: "idp", - 4068: "ipfltbcst", - 4069: "minger", - 4070: "tripe", - 4071: "aibkup", - 4072: "zieto-sock", - 4073: "iRAPP", - 4074: "cequint-cityid", - 4075: "perimlan", - 4076: "seraph", - 4078: "cssp", - 4079: "santools", - 4080: "lorica-in", - 4081: "lorica-in-sec", - 4082: "lorica-out", - 4083: "lorica-out-sec", - 4085: "ezmessagesrv", - 4087: "applusservice", - 4088: "npsp", - 4089: "opencore", - 4090: "omasgport", - 4091: "ewinstaller", - 4092: "ewdgs", - 4093: "pvxpluscs", - 4094: "sysrqd", - 4095: "xtgui", - 4096: "bre", - 4097: "patrolview", - 4098: "drmsfsd", - 4099: "dpcp", - 4100: "igo-incognito", - 4101: "brlp-0", - 4102: "brlp-1", - 4103: "brlp-2", - 4104: "brlp-3", - 4105: "shofar", - 4106: "synchronite", - 4107: "j-ac", - 4108: "accel", - 4109: "izm", - 4110: "g2tag", - 4111: "xgrid", - 4112: "apple-vpns-rp", - 4113: "aipn-reg", - 4114: "jomamqmonitor", - 4115: "cds", - 4116: "smartcard-tls", - 4117: "hillrserv", - 4118: "netscript", - 4119: "assuria-slm", - 4121: "e-builder", - 4122: "fprams", - 4123: "z-wave", - 4124: "tigv2", - 4125: "opsview-envoy", - 4126: "ddrepl", - 4127: "unikeypro", - 4128: "nufw", - 4129: "nuauth", - 4130: "fronet", - 4131: "stars", - 4132: "nuts-dem", - 4133: "nuts-bootp", - 4134: "nifty-hmi", - 4135: "cl-db-attach", - 4136: "cl-db-request", - 4137: "cl-db-remote", - 4138: "nettest", - 4139: "thrtx", - 4140: "cedros-fds", - 4141: "oirtgsvc", - 4142: "oidocsvc", - 4143: "oidsr", - 4145: "vvr-control", - 4146: "tgcconnect", - 4147: "vrxpservman", - 4148: "hhb-handheld", - 4149: "agslb", - 4150: "PowerAlert-nsa", - 4151: "menandmice-noh", - 4152: "idig-mux", - 4153: "mbl-battd", - 4154: "atlinks", - 4155: "bzr", - 4156: "stat-results", - 4157: "stat-scanner", - 4158: "stat-cc", - 4159: "nss", - 4160: "jini-discovery", - 4161: "omscontact", - 4162: "omstopology", - 4163: "silverpeakpeer", - 4164: "silverpeakcomm", - 4165: "altcp", - 4166: "joost", - 4167: "ddgn", - 4168: "pslicser", - 4169: "iadt", - 4170: "d-cinema-csp", - 4171: "ml-svnet", - 4172: "pcoip", - 4174: "smcluster", - 4175: "bccp", - 4176: "tl-ipcproxy", - 4177: "wello", - 4178: "storman", - 4179: "MaxumSP", - 4180: "httpx", - 4181: "macbak", - 4182: "pcptcpservice", - 4183: "gmmp", - 4184: "universe-suite", - 4185: "wcpp", - 4186: "boxbackupstore", - 4187: "csc-proxy", - 4188: "vatata", - 4189: "pcep", - 4190: "sieve", - 4192: "azeti", - 4193: "pvxplusio", - 4199: "eims-admin", - 4300: "corelccam", - 4301: "d-data", - 4302: "d-data-control", - 4303: "srcp", - 4304: "owserver", - 4305: "batman", - 4306: "pinghgl", - 4307: "visicron-vs", - 4308: "compx-lockview", - 4309: "dserver", - 4310: "mirrtex", - 4311: "p6ssmc", - 4312: "pscl-mgt", - 4313: "perrla", - 4314: "choiceview-agt", - 4316: "choiceview-clt", - 4320: "fdt-rcatp", - 4321: "rwhois", - 4322: "trim-event", - 4323: "trim-ice", - 4324: "balour", - 4325: "geognosisman", - 4326: "geognosis", - 4327: "jaxer-web", - 4328: "jaxer-manager", - 4329: "publiqare-sync", - 4330: "dey-sapi", - 4331: "ktickets-rest", - 4333: "ahsp", - 4340: "gaia", - 4341: "lisp-data", - 4342: "lisp-cons", - 4343: "unicall", - 4344: "vinainstall", - 4345: "m4-network-as", - 4346: "elanlm", - 4347: "lansurveyor", - 4348: "itose", - 4349: "fsportmap", - 4350: "net-device", - 4351: "plcy-net-svcs", - 4352: "pjlink", - 4353: "f5-iquery", - 4354: "qsnet-trans", - 4355: "qsnet-workst", - 4356: "qsnet-assist", - 4357: "qsnet-cond", - 4358: "qsnet-nucl", - 4359: "omabcastltkm", - 4360: "matrix-vnet", - 4368: "wxbrief", - 4369: "epmd", - 4370: "elpro-tunnel", - 4371: "l2c-control", - 4372: "l2c-data", - 4373: "remctl", - 4374: "psi-ptt", - 4375: "tolteces", - 4376: "bip", - 4377: "cp-spxsvr", - 4378: "cp-spxdpy", - 4379: "ctdb", - 4389: "xandros-cms", - 4390: "wiegand", - 4391: "apwi-imserver", - 4392: "apwi-rxserver", - 4393: "apwi-rxspooler", - 4395: "omnivisionesx", - 4396: "fly", - 4400: "ds-srv", - 4401: "ds-srvr", - 4402: "ds-clnt", - 4403: "ds-user", - 4404: "ds-admin", - 4405: "ds-mail", - 4406: "ds-slp", - 4407: "nacagent", - 4408: "slscc", - 4409: "netcabinet-com", - 4410: "itwo-server", - 4411: "found", - 4425: "netrockey6", - 4426: "beacon-port-2", - 4427: "drizzle", - 4428: "omviserver", - 4429: "omviagent", - 4430: "rsqlserver", - 4431: "wspipe", - 4432: "l-acoustics", - 4433: "vop", - 4442: "saris", - 4443: "pharos", - 4444: "krb524", - 4445: "upnotifyp", - 4446: "n1-fwp", - 4447: "n1-rmgmt", - 4448: "asc-slmd", - 4449: "privatewire", - 4450: "camp", - 4451: "ctisystemmsg", - 4452: "ctiprogramload", - 4453: "nssalertmgr", - 4454: "nssagentmgr", - 4455: "prchat-user", - 4456: "prchat-server", - 4457: "prRegister", - 4458: "mcp", - 4484: "hpssmgmt", - 4485: "assyst-dr", - 4486: "icms", - 4487: "prex-tcp", - 4488: "awacs-ice", - 4500: "ipsec-nat-t", - 4535: "ehs", - 4536: "ehs-ssl", - 4537: "wssauthsvc", - 4538: "swx-gate", - 4545: "worldscores", - 4546: "sf-lm", - 4547: "lanner-lm", - 4548: "synchromesh", - 4549: "aegate", - 4550: "gds-adppiw-db", - 4551: "ieee-mih", - 4552: "menandmice-mon", - 4553: "icshostsvc", - 4554: "msfrs", - 4555: "rsip", - 4556: "dtn-bundle", - 4559: "hylafax", - 4563: "amahi-anywhere", - 4566: "kwtc", - 4567: "tram", - 4568: "bmc-reporting", - 4569: "iax", - 4570: "deploymentmap", - 4590: "rid", - 4591: "l3t-at-an", - 4593: "ipt-anri-anri", - 4594: "ias-session", - 4595: "ias-paging", - 4596: "ias-neighbor", - 4597: "a21-an-1xbs", - 4598: "a16-an-an", - 4599: "a17-an-an", - 4600: "piranha1", - 4601: "piranha2", - 4602: "mtsserver", - 4603: "menandmice-upg", - 4604: "irp", - 4658: "playsta2-app", - 4659: "playsta2-lob", - 4660: "smaclmgr", - 4661: "kar2ouche", - 4662: "oms", - 4663: "noteit", - 4664: "ems", - 4665: "contclientms", - 4666: "eportcomm", - 4667: "mmacomm", - 4668: "mmaeds", - 4669: "eportcommdata", - 4670: "light", - 4671: "acter", - 4672: "rfa", - 4673: "cxws", - 4674: "appiq-mgmt", - 4675: "dhct-status", - 4676: "dhct-alerts", - 4677: "bcs", - 4678: "traversal", - 4679: "mgesupervision", - 4680: "mgemanagement", - 4681: "parliant", - 4682: "finisar", - 4683: "spike", - 4684: "rfid-rp1", - 4685: "autopac", - 4686: "msp-os", - 4687: "nst", - 4688: "mobile-p2p", - 4689: "altovacentral", - 4690: "prelude", - 4691: "mtn", - 4692: "conspiracy", - 4700: "netxms-agent", - 4701: "netxms-mgmt", - 4702: "netxms-sync", - 4703: "npqes-test", - 4704: "assuria-ins", - 4725: "truckstar", - 4727: "fcis", - 4728: "capmux", - 4730: "gearman", - 4731: "remcap", - 4733: "resorcs", - 4737: "ipdr-sp", - 4738: "solera-lpn", - 4739: "ipfix", - 4740: "ipfixs", - 4741: "lumimgrd", - 4742: "sicct", - 4743: "openhpid", - 4744: "ifsp", - 4745: "fmp", - 4749: "profilemac", - 4750: "ssad", - 4751: "spocp", - 4752: "snap", - 4753: "simon", - 4784: "bfd-multi-ctl", - 4786: "smart-install", - 4787: "sia-ctrl-plane", - 4788: "xmcp", - 4800: "iims", - 4801: "iwec", - 4802: "ilss", - 4803: "notateit", - 4827: "htcp", - 4837: "varadero-0", - 4838: "varadero-1", - 4839: "varadero-2", - 4840: "opcua-tcp", - 4841: "quosa", - 4842: "gw-asv", - 4843: "opcua-tls", - 4844: "gw-log", - 4845: "wcr-remlib", - 4846: "contamac-icm", - 4847: "wfc", - 4848: "appserv-http", - 4849: "appserv-https", - 4850: "sun-as-nodeagt", - 4851: "derby-repli", - 4867: "unify-debug", - 4868: "phrelay", - 4869: "phrelaydbg", - 4870: "cc-tracking", - 4871: "wired", - 4876: "tritium-can", - 4877: "lmcs", - 4879: "wsdl-event", - 4880: "hislip", - 4883: "wmlserver", - 4884: "hivestor", - 4885: "abbs", - 4894: "lyskom", - 4899: "radmin-port", - 4900: "hfcs", - 4901: "flr-agent", - 4902: "magiccontrol", - 4912: "lutap", - 4913: "lutcp", - 4914: "bones", - 4915: "frcs", - 4940: "eq-office-4940", - 4941: "eq-office-4941", - 4942: "eq-office-4942", - 4949: "munin", - 4950: "sybasesrvmon", - 4951: "pwgwims", - 4952: "sagxtsds", - 4953: "dbsyncarbiter", - 4969: "ccss-qmm", - 4970: "ccss-qsm", - 4984: "webyast", - 4985: "gerhcs", - 4986: "mrip", - 4987: "smar-se-port1", - 4988: "smar-se-port2", - 4989: "parallel", - 4990: "busycal", - 4991: "vrt", - 4999: "hfcs-manager", - 5000: "commplex-main", - 5001: "commplex-link", - 5002: "rfe", - 5003: "fmpro-internal", - 5004: "avt-profile-1", - 5005: "avt-profile-2", - 5006: "wsm-server", - 5007: "wsm-server-ssl", - 5008: "synapsis-edge", - 5009: "winfs", - 5010: "telelpathstart", - 5011: "telelpathattack", - 5012: "nsp", - 5013: "fmpro-v6", - 5015: "fmwp", - 5020: "zenginkyo-1", - 5021: "zenginkyo-2", - 5022: "mice", - 5023: "htuilsrv", - 5024: "scpi-telnet", - 5025: "scpi-raw", - 5026: "strexec-d", - 5027: "strexec-s", - 5028: "qvr", - 5029: "infobright", - 5030: "surfpass", - 5032: "signacert-agent", - 5042: "asnaacceler8db", - 5043: "swxadmin", - 5044: "lxi-evntsvc", - 5045: "osp", - 5048: "texai", - 5049: "ivocalize", - 5050: "mmcc", - 5051: "ita-agent", - 5052: "ita-manager", - 5053: "rlm", - 5054: "rlm-admin", - 5055: "unot", - 5056: "intecom-ps1", - 5057: "intecom-ps2", - 5059: "sds", - 5060: "sip", - 5061: "sips", - 5062: "na-localise", - 5063: "csrpc", - 5064: "ca-1", - 5065: "ca-2", - 5066: "stanag-5066", - 5067: "authentx", - 5068: "bitforestsrv", - 5069: "i-net-2000-npr", - 5070: "vtsas", - 5071: "powerschool", - 5072: "ayiya", - 5073: "tag-pm", - 5074: "alesquery", - 5075: "pvaccess", - 5080: "onscreen", - 5081: "sdl-ets", - 5082: "qcp", - 5083: "qfp", - 5084: "llrp", - 5085: "encrypted-llrp", - 5086: "aprigo-cs", - 5087: "biotic", - 5093: "sentinel-lm", - 5094: "hart-ip", - 5099: "sentlm-srv2srv", - 5100: "socalia", - 5101: "talarian-tcp", - 5102: "oms-nonsecure", - 5103: "actifio-c2c", - 5106: "actifioudsagent", - 5111: "taep-as-svc", - 5112: "pm-cmdsvr", - 5114: "ev-services", - 5115: "autobuild", - 5117: "gradecam", - 5120: "barracuda-bbs", - 5133: "nbt-pc", - 5134: "ppactivation", - 5135: "erp-scale", - 5137: "ctsd", - 5145: "rmonitor-secure", - 5146: "social-alarm", - 5150: "atmp", - 5151: "esri-sde", - 5152: "sde-discovery", - 5153: "toruxserver", - 5154: "bzflag", - 5155: "asctrl-agent", - 5156: "rugameonline", - 5157: "mediat", - 5161: "snmpssh", - 5162: "snmpssh-trap", - 5163: "sbackup", - 5164: "vpa", - 5165: "ife-icorp", - 5166: "winpcs", - 5167: "scte104", - 5168: "scte30", - 5172: "pcoip-mgmt", - 5190: "aol", - 5191: "aol-1", - 5192: "aol-2", - 5193: "aol-3", - 5194: "cpscomm", - 5195: "ampl-lic", - 5196: "ampl-tableproxy", - 5200: "targus-getdata", - 5201: "targus-getdata1", - 5202: "targus-getdata2", - 5203: "targus-getdata3", - 5209: "nomad", - 5215: "noteza", - 5221: "3exmp", - 5222: "xmpp-client", - 5223: "hpvirtgrp", - 5224: "hpvirtctrl", - 5225: "hp-server", - 5226: "hp-status", - 5227: "perfd", - 5228: "hpvroom", - 5229: "jaxflow", - 5230: "jaxflow-data", - 5231: "crusecontrol", - 5232: "csedaemon", - 5233: "enfs", - 5234: "eenet", - 5235: "galaxy-network", - 5236: "padl2sim", - 5237: "mnet-discovery", - 5245: "downtools", - 5248: "caacws", - 5249: "caaclang2", - 5250: "soagateway", - 5251: "caevms", - 5252: "movaz-ssc", - 5253: "kpdp", - 5264: "3com-njack-1", - 5265: "3com-njack-2", - 5269: "xmpp-server", - 5270: "cartographerxmp", - 5271: "cuelink", - 5272: "pk", - 5280: "xmpp-bosh", - 5281: "undo-lm", - 5282: "transmit-port", - 5298: "presence", - 5299: "nlg-data", - 5300: "hacl-hb", - 5301: "hacl-gs", - 5302: "hacl-cfg", - 5303: "hacl-probe", - 5304: "hacl-local", - 5305: "hacl-test", - 5306: "sun-mc-grp", - 5307: "sco-aip", - 5308: "cfengine", - 5309: "jprinter", - 5310: "outlaws", - 5312: "permabit-cs", - 5313: "rrdp", - 5314: "opalis-rbt-ipc", - 5315: "hacl-poll", - 5316: "hpbladems", - 5317: "hpdevms", - 5318: "pkix-cmc", - 5320: "bsfserver-zn", - 5321: "bsfsvr-zn-ssl", - 5343: "kfserver", - 5344: "xkotodrcp", - 5349: "stuns", - 5352: "dns-llq", - 5353: "mdns", - 5354: "mdnsresponder", - 5355: "llmnr", - 5356: "ms-smlbiz", - 5357: "wsdapi", - 5358: "wsdapi-s", - 5359: "ms-alerter", - 5360: "ms-sideshow", - 5361: "ms-s-sideshow", - 5362: "serverwsd2", - 5363: "net-projection", - 5397: "stresstester", - 5398: "elektron-admin", - 5399: "securitychase", - 5400: "excerpt", - 5401: "excerpts", - 5402: "mftp", - 5403: "hpoms-ci-lstn", - 5404: "hpoms-dps-lstn", - 5405: "netsupport", - 5406: "systemics-sox", - 5407: "foresyte-clear", - 5408: "foresyte-sec", - 5409: "salient-dtasrv", - 5410: "salient-usrmgr", - 5411: "actnet", - 5412: "continuus", - 5413: "wwiotalk", - 5414: "statusd", - 5415: "ns-server", - 5416: "sns-gateway", - 5417: "sns-agent", - 5418: "mcntp", - 5419: "dj-ice", - 5420: "cylink-c", - 5421: "netsupport2", - 5422: "salient-mux", - 5423: "virtualuser", - 5424: "beyond-remote", - 5425: "br-channel", - 5426: "devbasic", - 5427: "sco-peer-tta", - 5428: "telaconsole", - 5429: "base", - 5430: "radec-corp", - 5431: "park-agent", - 5432: "postgresql", - 5433: "pyrrho", - 5434: "sgi-arrayd", - 5435: "sceanics", - 5443: "spss", - 5445: "smbdirect", - 5453: "surebox", - 5454: "apc-5454", - 5455: "apc-5455", - 5456: "apc-5456", - 5461: "silkmeter", - 5462: "ttl-publisher", - 5463: "ttlpriceproxy", - 5464: "quailnet", - 5465: "netops-broker", - 5500: "fcp-addr-srvr1", - 5501: "fcp-addr-srvr2", - 5502: "fcp-srvr-inst1", - 5503: "fcp-srvr-inst2", - 5504: "fcp-cics-gw1", - 5505: "checkoutdb", - 5506: "amc", - 5553: "sgi-eventmond", - 5554: "sgi-esphttp", - 5555: "personal-agent", - 5556: "freeciv", - 5557: "farenet", - 5566: "westec-connect", - 5567: "enc-eps-mc-sec", - 5568: "sdt", - 5569: "rdmnet-ctrl", - 5573: "sdmmp", - 5574: "lsi-bobcat", - 5575: "ora-oap", - 5579: "fdtracks", - 5580: "tmosms0", - 5581: "tmosms1", - 5582: "fac-restore", - 5583: "tmo-icon-sync", - 5584: "bis-web", - 5585: "bis-sync", - 5586: "att-mt-sms", - 5597: "ininmessaging", - 5598: "mctfeed", - 5599: "esinstall", - 5600: "esmmanager", - 5601: "esmagent", - 5602: "a1-msc", - 5603: "a1-bs", - 5604: "a3-sdunode", - 5605: "a4-sdunode", - 5618: "efr", - 5627: "ninaf", - 5628: "htrust", - 5629: "symantec-sfdb", - 5630: "precise-comm", - 5631: "pcanywheredata", - 5632: "pcanywherestat", - 5633: "beorl", - 5634: "xprtld", - 5635: "sfmsso", - 5636: "sfm-db-server", - 5637: "cssc", - 5638: "flcrs", - 5639: "ics", - 5646: "vfmobile", - 5670: "filemq", - 5671: "amqps", - 5672: "amqp", - 5673: "jms", - 5674: "hyperscsi-port", - 5675: "v5ua", - 5676: "raadmin", - 5677: "questdb2-lnchr", - 5678: "rrac", - 5679: "dccm", - 5680: "auriga-router", - 5681: "ncxcp", - 5688: "ggz", - 5689: "qmvideo", - 5693: "rbsystem", - 5696: "kmip", - 5713: "proshareaudio", - 5714: "prosharevideo", - 5715: "prosharedata", - 5716: "prosharerequest", - 5717: "prosharenotify", - 5718: "dpm", - 5719: "dpm-agent", - 5720: "ms-licensing", - 5721: "dtpt", - 5722: "msdfsr", - 5723: "omhs", - 5724: "omsdk", - 5725: "ms-ilm", - 5726: "ms-ilm-sts", - 5727: "asgenf", - 5728: "io-dist-data", - 5729: "openmail", - 5730: "unieng", - 5741: "ida-discover1", - 5742: "ida-discover2", - 5743: "watchdoc-pod", - 5744: "watchdoc", - 5745: "fcopy-server", - 5746: "fcopys-server", - 5747: "tunatic", - 5748: "tunalyzer", - 5750: "rscd", - 5755: "openmailg", - 5757: "x500ms", - 5766: "openmailns", - 5767: "s-openmail", - 5768: "openmailpxy", - 5769: "spramsca", - 5770: "spramsd", - 5771: "netagent", - 5777: "dali-port", - 5780: "vts-rpc", - 5781: "3par-evts", - 5782: "3par-mgmt", - 5783: "3par-mgmt-ssl", - 5785: "3par-rcopy", - 5793: "xtreamx", - 5813: "icmpd", - 5814: "spt-automation", - 5841: "shiprush-d-ch", - 5842: "reversion", - 5859: "wherehoo", - 5863: "ppsuitemsg", - 5868: "diameters", - 5883: "jute", - 5900: "rfb", - 5910: "cm", - 5911: "cpdlc", - 5912: "fis", - 5913: "ads-c", - 5963: "indy", - 5968: "mppolicy-v5", - 5969: "mppolicy-mgr", - 5984: "couchdb", - 5985: "wsman", - 5986: "wsmans", - 5987: "wbem-rmi", - 5988: "wbem-http", - 5989: "wbem-https", - 5990: "wbem-exp-https", - 5991: "nuxsl", - 5992: "consul-insight", - 5999: "cvsup", - 6064: "ndl-ahp-svc", - 6065: "winpharaoh", - 6066: "ewctsp", - 6068: "gsmp-ancp", - 6069: "trip", - 6070: "messageasap", - 6071: "ssdtp", - 6072: "diagnose-proc", - 6073: "directplay8", - 6074: "max", - 6075: "dpm-acm", - 6076: "msft-dpm-cert", - 6077: "iconstructsrv", - 6084: "reload-config", - 6085: "konspire2b", - 6086: "pdtp", - 6087: "ldss", - 6088: "doglms", - 6099: "raxa-mgmt", - 6100: "synchronet-db", - 6101: "synchronet-rtc", - 6102: "synchronet-upd", - 6103: "rets", - 6104: "dbdb", - 6105: "primaserver", - 6106: "mpsserver", - 6107: "etc-control", - 6108: "sercomm-scadmin", - 6109: "globecast-id", - 6110: "softcm", - 6111: "spc", - 6112: "dtspcd", - 6113: "dayliteserver", - 6114: "wrspice", - 6115: "xic", - 6116: "xtlserv", - 6117: "daylitetouch", - 6121: "spdy", - 6122: "bex-webadmin", - 6123: "backup-express", - 6124: "pnbs", - 6130: "damewaremobgtwy", - 6133: "nbt-wol", - 6140: "pulsonixnls", - 6141: "meta-corp", - 6142: "aspentec-lm", - 6143: "watershed-lm", - 6144: "statsci1-lm", - 6145: "statsci2-lm", - 6146: "lonewolf-lm", - 6147: "montage-lm", - 6148: "ricardo-lm", - 6149: "tal-pod", - 6159: "efb-aci", - 6160: "ecmp", - 6161: "patrol-ism", - 6162: "patrol-coll", - 6163: "pscribe", - 6200: "lm-x", - 6222: "radmind", - 6241: "jeol-nsdtp-1", - 6242: "jeol-nsdtp-2", - 6243: "jeol-nsdtp-3", - 6244: "jeol-nsdtp-4", - 6251: "tl1-raw-ssl", - 6252: "tl1-ssh", - 6253: "crip", - 6267: "gld", - 6268: "grid", - 6269: "grid-alt", - 6300: "bmc-grx", - 6301: "bmc-ctd-ldap", - 6306: "ufmp", - 6315: "scup", - 6316: "abb-escp", - 6317: "nav-data-cmd", - 6320: "repsvc", - 6321: "emp-server1", - 6322: "emp-server2", - 6324: "hrd-ncs", - 6325: "dt-mgmtsvc", - 6326: "dt-vra", - 6343: "sflow", - 6344: "streletz", - 6346: "gnutella-svc", - 6347: "gnutella-rtr", - 6350: "adap", - 6355: "pmcs", - 6360: "metaedit-mu", - 6370: "metaedit-se", - 6382: "metatude-mds", - 6389: "clariion-evr01", - 6390: "metaedit-ws", - 6417: "faxcomservice", - 6418: "syserverremote", - 6419: "svdrp", - 6420: "nim-vdrshell", - 6421: "nim-wan", - 6432: "pgbouncer", - 6442: "tarp", - 6443: "sun-sr-https", - 6444: "sge-qmaster", - 6445: "sge-execd", - 6446: "mysql-proxy", - 6455: "skip-cert-recv", - 6456: "skip-cert-send", - 6471: "lvision-lm", - 6480: "sun-sr-http", - 6481: "servicetags", - 6482: "ldoms-mgmt", - 6483: "SunVTS-RMI", - 6484: "sun-sr-jms", - 6485: "sun-sr-iiop", - 6486: "sun-sr-iiops", - 6487: "sun-sr-iiop-aut", - 6488: "sun-sr-jmx", - 6489: "sun-sr-admin", - 6500: "boks", - 6501: "boks-servc", - 6502: "boks-servm", - 6503: "boks-clntd", - 6505: "badm-priv", - 6506: "badm-pub", - 6507: "bdir-priv", - 6508: "bdir-pub", - 6509: "mgcs-mfp-port", - 6510: "mcer-port", - 6513: "netconf-tls", - 6514: "syslog-tls", - 6515: "elipse-rec", - 6543: "lds-distrib", - 6544: "lds-dump", - 6547: "apc-6547", - 6548: "apc-6548", - 6549: "apc-6549", - 6550: "fg-sysupdate", - 6551: "sum", - 6558: "xdsxdm", - 6566: "sane-port", - 6568: "canit-store", - 6579: "affiliate", - 6580: "parsec-master", - 6581: "parsec-peer", - 6582: "parsec-game", - 6583: "joaJewelSuite", - 6600: "mshvlm", - 6601: "mstmg-sstp", - 6602: "wsscomfrmwk", - 6619: "odette-ftps", - 6620: "kftp-data", - 6621: "kftp", - 6622: "mcftp", - 6623: "ktelnet", - 6624: "datascaler-db", - 6625: "datascaler-ctl", - 6626: "wago-service", - 6627: "nexgen", - 6628: "afesc-mc", - 6632: "mxodbc-connect", - 6640: "ovsdb", - 6653: "openflow", - 6655: "pcs-sf-ui-man", - 6656: "emgmsg", - 6670: "vocaltec-gold", - 6671: "p4p-portal", - 6672: "vision-server", - 6673: "vision-elmd", - 6678: "vfbp", - 6679: "osaut", - 6687: "clever-ctrace", - 6688: "clever-tcpip", - 6689: "tsa", - 6697: "ircs-u", - 6701: "kti-icad-srvr", - 6702: "e-design-net", - 6703: "e-design-web", - 6714: "ibprotocol", - 6715: "fibotrader-com", - 6716: "printercare-cc", - 6767: "bmc-perf-agent", - 6768: "bmc-perf-mgrd", - 6769: "adi-gxp-srvprt", - 6770: "plysrv-http", - 6771: "plysrv-https", - 6777: "ntz-tracker", - 6778: "ntz-p2p-storage", - 6785: "dgpf-exchg", - 6786: "smc-jmx", - 6787: "smc-admin", - 6788: "smc-http", - 6789: "smc-https", - 6790: "hnmp", - 6791: "hnm", - 6801: "acnet", - 6817: "pentbox-sim", - 6831: "ambit-lm", - 6841: "netmo-default", - 6842: "netmo-http", - 6850: "iccrushmore", - 6868: "acctopus-cc", - 6888: "muse", - 6901: "jetstream", - 6935: "ethoscan", - 6936: "xsmsvc", - 6946: "bioserver", - 6951: "otlp", - 6961: "jmact3", - 6962: "jmevt2", - 6963: "swismgr1", - 6964: "swismgr2", - 6965: "swistrap", - 6966: "swispol", - 6969: "acmsoda", - 6997: "MobilitySrv", - 6998: "iatp-highpri", - 6999: "iatp-normalpri", - 7000: "afs3-fileserver", - 7001: "afs3-callback", - 7002: "afs3-prserver", - 7003: "afs3-vlserver", - 7004: "afs3-kaserver", - 7005: "afs3-volser", - 7006: "afs3-errors", - 7007: "afs3-bos", - 7008: "afs3-update", - 7009: "afs3-rmtsys", - 7010: "ups-onlinet", - 7011: "talon-disc", - 7012: "talon-engine", - 7013: "microtalon-dis", - 7014: "microtalon-com", - 7015: "talon-webserver", - 7018: "fisa-svc", - 7019: "doceri-ctl", - 7020: "dpserve", - 7021: "dpserveadmin", - 7022: "ctdp", - 7023: "ct2nmcs", - 7024: "vmsvc", - 7025: "vmsvc-2", - 7030: "op-probe", - 7031: "iposplanet", - 7070: "arcp", - 7071: "iwg1", - 7073: "martalk", - 7080: "empowerid", - 7099: "lazy-ptop", - 7100: "font-service", - 7101: "elcn", - 7121: "virprot-lm", - 7128: "scenidm", - 7129: "scenccs", - 7161: "cabsm-comm", - 7162: "caistoragemgr", - 7163: "cacsambroker", - 7164: "fsr", - 7165: "doc-server", - 7166: "aruba-server", - 7167: "casrmagent", - 7168: "cnckadserver", - 7169: "ccag-pib", - 7170: "nsrp", - 7171: "drm-production", - 7172: "metalbend", - 7173: "zsecure", - 7174: "clutild", - 7200: "fodms", - 7201: "dlip", - 7227: "ramp", - 7228: "citrixupp", - 7229: "citrixuppg", - 7236: "display", - 7237: "pads", - 7262: "cnap", - 7272: "watchme-7272", - 7273: "oma-rlp", - 7274: "oma-rlp-s", - 7275: "oma-ulp", - 7276: "oma-ilp", - 7277: "oma-ilp-s", - 7278: "oma-dcdocbs", - 7279: "ctxlic", - 7280: "itactionserver1", - 7281: "itactionserver2", - 7282: "mzca-action", - 7283: "genstat", - 7365: "lcm-server", - 7391: "mindfilesys", - 7392: "mrssrendezvous", - 7393: "nfoldman", - 7394: "fse", - 7395: "winqedit", - 7397: "hexarc", - 7400: "rtps-discovery", - 7401: "rtps-dd-ut", - 7402: "rtps-dd-mt", - 7410: "ionixnetmon", - 7411: "daqstream", - 7421: "mtportmon", - 7426: "pmdmgr", - 7427: "oveadmgr", - 7428: "ovladmgr", - 7429: "opi-sock", - 7430: "xmpv7", - 7431: "pmd", - 7437: "faximum", - 7443: "oracleas-https", - 7471: "sttunnel", - 7473: "rise", - 7474: "neo4j", - 7491: "telops-lmd", - 7500: "silhouette", - 7501: "ovbus", - 7508: "adcp", - 7509: "acplt", - 7510: "ovhpas", - 7511: "pafec-lm", - 7542: "saratoga", - 7543: "atul", - 7544: "nta-ds", - 7545: "nta-us", - 7546: "cfs", - 7547: "cwmp", - 7548: "tidp", - 7549: "nls-tl", - 7560: "sncp", - 7563: "cfw", - 7566: "vsi-omega", - 7569: "dell-eql-asm", - 7570: "aries-kfinder", - 7574: "coherence", - 7588: "sun-lm", - 7624: "indi", - 7626: "simco", - 7627: "soap-http", - 7628: "zen-pawn", - 7629: "xdas", - 7630: "hawk", - 7631: "tesla-sys-msg", - 7633: "pmdfmgt", - 7648: "cuseeme", - 7672: "imqstomp", - 7673: "imqstomps", - 7674: "imqtunnels", - 7675: "imqtunnel", - 7676: "imqbrokerd", - 7677: "sun-user-https", - 7680: "pando-pub", - 7689: "collaber", - 7697: "klio", - 7700: "em7-secom", - 7707: "sync-em7", - 7708: "scinet", - 7720: "medimageportal", - 7724: "nsdeepfreezectl", - 7725: "nitrogen", - 7726: "freezexservice", - 7727: "trident-data", - 7734: "smip", - 7738: "aiagent", - 7741: "scriptview", - 7742: "msss", - 7743: "sstp-1", - 7744: "raqmon-pdu", - 7747: "prgp", - 7777: "cbt", - 7778: "interwise", - 7779: "vstat", - 7781: "accu-lmgr", - 7786: "minivend", - 7787: "popup-reminders", - 7789: "office-tools", - 7794: "q3ade", - 7797: "pnet-conn", - 7798: "pnet-enc", - 7799: "altbsdp", - 7800: "asr", - 7801: "ssp-client", - 7810: "rbt-wanopt", - 7845: "apc-7845", - 7846: "apc-7846", - 7847: "csoauth", - 7869: "mobileanalyzer", - 7870: "rbt-smc", - 7871: "mdm", - 7878: "owms", - 7880: "pss", - 7887: "ubroker", - 7900: "mevent", - 7901: "tnos-sp", - 7902: "tnos-dp", - 7903: "tnos-dps", - 7913: "qo-secure", - 7932: "t2-drm", - 7933: "t2-brm", - 7962: "generalsync", - 7967: "supercell", - 7979: "micromuse-ncps", - 7980: "quest-vista", - 7981: "sossd-collect", - 7982: "sossd-agent", - 7997: "pushns", - 7999: "irdmi2", - 8000: "irdmi", - 8001: "vcom-tunnel", - 8002: "teradataordbms", - 8003: "mcreport", - 8005: "mxi", - 8008: "http-alt", - 8019: "qbdb", - 8020: "intu-ec-svcdisc", - 8021: "intu-ec-client", - 8022: "oa-system", - 8025: "ca-audit-da", - 8026: "ca-audit-ds", - 8032: "pro-ed", - 8033: "mindprint", - 8034: "vantronix-mgmt", - 8040: "ampify", - 8042: "fs-agent", - 8043: "fs-server", - 8044: "fs-mgmt", - 8051: "rocrail", - 8052: "senomix01", - 8053: "senomix02", - 8054: "senomix03", - 8055: "senomix04", - 8056: "senomix05", - 8057: "senomix06", - 8058: "senomix07", - 8059: "senomix08", - 8066: "toad-bi-appsrvr", - 8074: "gadugadu", - 8080: "http-alt", - 8081: "sunproxyadmin", - 8082: "us-cli", - 8083: "us-srv", - 8086: "d-s-n", - 8087: "simplifymedia", - 8088: "radan-http", - 8091: "jamlink", - 8097: "sac", - 8100: "xprint-server", - 8101: "ldoms-migr", - 8102: "kz-migr", - 8115: "mtl8000-matrix", - 8116: "cp-cluster", - 8117: "purityrpc", - 8118: "privoxy", - 8121: "apollo-data", - 8122: "apollo-admin", - 8128: "paycash-online", - 8129: "paycash-wbp", - 8130: "indigo-vrmi", - 8131: "indigo-vbcp", - 8132: "dbabble", - 8148: "isdd", - 8153: "quantastor", - 8160: "patrol", - 8161: "patrol-snmp", - 8162: "lpar2rrd", - 8181: "intermapper", - 8182: "vmware-fdm", - 8183: "proremote", - 8184: "itach", - 8191: "limnerpressure", - 8192: "spytechphone", - 8194: "blp1", - 8195: "blp2", - 8199: "vvr-data", - 8200: "trivnet1", - 8201: "trivnet2", - 8204: "lm-perfworks", - 8205: "lm-instmgr", - 8206: "lm-dta", - 8207: "lm-sserver", - 8208: "lm-webwatcher", - 8230: "rexecj", - 8243: "synapse-nhttps", - 8276: "pando-sec", - 8280: "synapse-nhttp", - 8292: "blp3", - 8293: "hiperscan-id", - 8294: "blp4", - 8300: "tmi", - 8301: "amberon", - 8313: "hub-open-net", - 8320: "tnp-discover", - 8321: "tnp", - 8351: "server-find", - 8376: "cruise-enum", - 8377: "cruise-swroute", - 8378: "cruise-config", - 8379: "cruise-diags", - 8380: "cruise-update", - 8383: "m2mservices", - 8400: "cvd", - 8401: "sabarsd", - 8402: "abarsd", - 8403: "admind", - 8404: "svcloud", - 8405: "svbackup", - 8415: "dlpx-sp", - 8416: "espeech", - 8417: "espeech-rtp", - 8442: "cybro-a-bus", - 8443: "pcsync-https", - 8444: "pcsync-http", - 8445: "copy", - 8450: "npmp", - 8457: "nexentamv", - 8470: "cisco-avp", - 8471: "pim-port", - 8472: "otv", - 8473: "vp2p", - 8474: "noteshare", - 8500: "fmtp", - 8501: "cmtp-mgt", - 8502: "ftnmtp", - 8554: "rtsp-alt", - 8555: "d-fence", - 8567: "enc-tunnel", - 8600: "asterix", - 8610: "canon-mfnp", - 8611: "canon-bjnp1", - 8612: "canon-bjnp2", - 8613: "canon-bjnp3", - 8614: "canon-bjnp4", - 8615: "imink", - 8665: "monetra", - 8666: "monetra-admin", - 8675: "msi-cps-rm", - 8686: "sun-as-jmxrmi", - 8688: "openremote-ctrl", - 8699: "vnyx", - 8711: "nvc", - 8733: "ibus", - 8750: "dey-keyneg", - 8763: "mc-appserver", - 8764: "openqueue", - 8765: "ultraseek-http", - 8766: "amcs", - 8770: "dpap", - 8778: "uec", - 8786: "msgclnt", - 8787: "msgsrvr", - 8793: "acd-pm", - 8800: "sunwebadmin", - 8804: "truecm", - 8873: "dxspider", - 8880: "cddbp-alt", - 8881: "galaxy4d", - 8883: "secure-mqtt", - 8888: "ddi-tcp-1", - 8889: "ddi-tcp-2", - 8890: "ddi-tcp-3", - 8891: "ddi-tcp-4", - 8892: "ddi-tcp-5", - 8893: "ddi-tcp-6", - 8894: "ddi-tcp-7", - 8899: "ospf-lite", - 8900: "jmb-cds1", - 8901: "jmb-cds2", - 8910: "manyone-http", - 8911: "manyone-xml", - 8912: "wcbackup", - 8913: "dragonfly", - 8937: "twds", - 8953: "ub-dns-control", - 8954: "cumulus-admin", - 8989: "sunwebadmins", - 8990: "http-wmap", - 8991: "https-wmap", - 8998: "canto-roboflow", - 8999: "bctp", - 9000: "cslistener", - 9001: "etlservicemgr", - 9002: "dynamid", - 9008: "ogs-server", - 9009: "pichat", - 9010: "sdr", - 9020: "tambora", - 9021: "panagolin-ident", - 9022: "paragent", - 9023: "swa-1", - 9024: "swa-2", - 9025: "swa-3", - 9026: "swa-4", - 9050: "versiera", - 9051: "fio-cmgmt", - 9080: "glrpc", - 9083: "emc-pp-mgmtsvc", - 9084: "aurora", - 9085: "ibm-rsyscon", - 9086: "net2display", - 9087: "classic", - 9088: "sqlexec", - 9089: "sqlexec-ssl", - 9090: "websm", - 9091: "xmltec-xmlmail", - 9092: "XmlIpcRegSvc", - 9093: "copycat", - 9100: "hp-pdl-datastr", - 9101: "bacula-dir", - 9102: "bacula-fd", - 9103: "bacula-sd", - 9104: "peerwire", - 9105: "xadmin", - 9106: "astergate", - 9107: "astergatefax", - 9119: "mxit", - 9122: "grcmp", - 9123: "grcp", - 9131: "dddp", - 9160: "apani1", - 9161: "apani2", - 9162: "apani3", - 9163: "apani4", - 9164: "apani5", - 9191: "sun-as-jpda", - 9200: "wap-wsp", - 9201: "wap-wsp-wtp", - 9202: "wap-wsp-s", - 9203: "wap-wsp-wtp-s", - 9204: "wap-vcard", - 9205: "wap-vcal", - 9206: "wap-vcard-s", - 9207: "wap-vcal-s", - 9208: "rjcdb-vcards", - 9209: "almobile-system", - 9210: "oma-mlp", - 9211: "oma-mlp-s", - 9212: "serverviewdbms", - 9213: "serverstart", - 9214: "ipdcesgbs", - 9215: "insis", - 9216: "acme", - 9217: "fsc-port", - 9222: "teamcoherence", - 9255: "mon", - 9278: "pegasus", - 9279: "pegasus-ctl", - 9280: "pgps", - 9281: "swtp-port1", - 9282: "swtp-port2", - 9283: "callwaveiam", - 9284: "visd", - 9285: "n2h2server", - 9287: "cumulus", - 9292: "armtechdaemon", - 9293: "storview", - 9294: "armcenterhttp", - 9295: "armcenterhttps", - 9300: "vrace", - 9306: "sphinxql", - 9312: "sphinxapi", - 9318: "secure-ts", - 9321: "guibase", - 9343: "mpidcmgr", - 9344: "mphlpdmc", - 9346: "ctechlicensing", - 9374: "fjdmimgr", - 9380: "boxp", - 9387: "d2dconfig", - 9388: "d2ddatatrans", - 9389: "adws", - 9390: "otp", - 9396: "fjinvmgr", - 9397: "mpidcagt", - 9400: "sec-t4net-srv", - 9401: "sec-t4net-clt", - 9402: "sec-pc2fax-srv", - 9418: "git", - 9443: "tungsten-https", - 9444: "wso2esb-console", - 9445: "mindarray-ca", - 9450: "sntlkeyssrvr", - 9500: "ismserver", - 9535: "mngsuite", - 9536: "laes-bf", - 9555: "trispen-sra", - 9592: "ldgateway", - 9593: "cba8", - 9594: "msgsys", - 9595: "pds", - 9596: "mercury-disc", - 9597: "pd-admin", - 9598: "vscp", - 9599: "robix", - 9600: "micromuse-ncpw", - 9612: "streamcomm-ds", - 9614: "iadt-tls", - 9616: "erunbook-agent", - 9617: "erunbook-server", - 9618: "condor", - 9628: "odbcpathway", - 9629: "uniport", - 9630: "peoctlr", - 9631: "peocoll", - 9640: "pqsflows", - 9666: "zoomcp", - 9667: "xmms2", - 9668: "tec5-sdctp", - 9694: "client-wakeup", - 9695: "ccnx", - 9700: "board-roar", - 9747: "l5nas-parchan", - 9750: "board-voip", - 9753: "rasadv", - 9762: "tungsten-http", - 9800: "davsrc", - 9801: "sstp-2", - 9802: "davsrcs", - 9875: "sapv1", - 9876: "sd", - 9888: "cyborg-systems", - 9889: "gt-proxy", - 9898: "monkeycom", - 9900: "iua", - 9909: "domaintime", - 9911: "sype-transport", - 9925: "xybrid-cloud", - 9950: "apc-9950", - 9951: "apc-9951", - 9952: "apc-9952", - 9953: "acis", - 9954: "hinp", - 9955: "alljoyn-stm", - 9966: "odnsp", - 9978: "xybrid-rt", - 9987: "dsm-scm-target", - 9988: "nsesrvr", - 9990: "osm-appsrvr", - 9991: "osm-oev", - 9992: "palace-1", - 9993: "palace-2", - 9994: "palace-3", - 9995: "palace-4", - 9996: "palace-5", - 9997: "palace-6", - 9998: "distinct32", - 9999: "distinct", - 10000: "ndmp", - 10001: "scp-config", - 10002: "documentum", - 10003: "documentum-s", - 10004: "emcrmirccd", - 10005: "emcrmird", - 10006: "netapp-sync", - 10007: "mvs-capacity", - 10008: "octopus", - 10009: "swdtp-sv", - 10010: "rxapi", - 10050: "zabbix-agent", - 10051: "zabbix-trapper", - 10055: "qptlmd", - 10080: "amanda", - 10081: "famdc", - 10100: "itap-ddtp", - 10101: "ezmeeting-2", - 10102: "ezproxy-2", - 10103: "ezrelay", - 10104: "swdtp", - 10107: "bctp-server", - 10110: "nmea-0183", - 10113: "netiq-endpoint", - 10114: "netiq-qcheck", - 10115: "netiq-endpt", - 10116: "netiq-voipa", - 10117: "iqrm", - 10128: "bmc-perf-sd", - 10129: "bmc-gms", - 10160: "qb-db-server", - 10161: "snmptls", - 10162: "snmptls-trap", - 10200: "trisoap", - 10201: "rsms", - 10252: "apollo-relay", - 10260: "axis-wimp-port", - 10288: "blocks", - 10321: "cosir", - 10540: "MOS-lower", - 10541: "MOS-upper", - 10542: "MOS-aux", - 10543: "MOS-soap", - 10544: "MOS-soap-opt", - 10631: "printopia", - 10800: "gap", - 10805: "lpdg", - 10809: "nbd", - 10860: "helix", - 10880: "bveapi", - 10990: "rmiaux", - 11000: "irisa", - 11001: "metasys", - 11095: "weave", - 11103: "origo-sync", - 11104: "netapp-icmgmt", - 11105: "netapp-icdata", - 11106: "sgi-lk", - 11109: "sgi-dmfmgr", - 11110: "sgi-soap", - 11111: "vce", - 11112: "dicom", - 11161: "suncacao-snmp", - 11162: "suncacao-jmxmp", - 11163: "suncacao-rmi", - 11164: "suncacao-csa", - 11165: "suncacao-websvc", - 11172: "oemcacao-jmxmp", - 11173: "t5-straton", - 11174: "oemcacao-rmi", - 11175: "oemcacao-websvc", - 11201: "smsqp", - 11202: "dcsl-backup", - 11208: "wifree", - 11211: "memcache", - 11319: "imip", - 11320: "imip-channels", - 11321: "arena-server", - 11367: "atm-uhas", - 11371: "hkp", - 11489: "asgcypresstcps", - 11600: "tempest-port", - 11623: "emc-xsw-dconfig", - 11720: "h323callsigalt", - 11723: "emc-xsw-dcache", - 11751: "intrepid-ssl", - 11796: "lanschool", - 11876: "xoraya", - 11967: "sysinfo-sp", - 12000: "entextxid", - 12001: "entextnetwk", - 12002: "entexthigh", - 12003: "entextmed", - 12004: "entextlow", - 12005: "dbisamserver1", - 12006: "dbisamserver2", - 12007: "accuracer", - 12008: "accuracer-dbms", - 12010: "edbsrvr", - 12012: "vipera", - 12013: "vipera-ssl", - 12109: "rets-ssl", - 12121: "nupaper-ss", - 12168: "cawas", - 12172: "hivep", - 12300: "linogridengine", - 12302: "rads", - 12321: "warehouse-sss", - 12322: "warehouse", - 12345: "italk", - 12753: "tsaf", - 12865: "netperf", - 13160: "i-zipqd", - 13216: "bcslogc", - 13217: "rs-pias", - 13218: "emc-vcas-tcp", - 13223: "powwow-client", - 13224: "powwow-server", - 13400: "doip-data", - 13720: "bprd", - 13721: "bpdbm", - 13722: "bpjava-msvc", - 13724: "vnetd", - 13782: "bpcd", - 13783: "vopied", - 13785: "nbdb", - 13786: "nomdb", - 13818: "dsmcc-config", - 13819: "dsmcc-session", - 13820: "dsmcc-passthru", - 13821: "dsmcc-download", - 13822: "dsmcc-ccp", - 13823: "bmdss", - 13894: "ucontrol", - 13929: "dta-systems", - 13930: "medevolve", - 14000: "scotty-ft", - 14001: "sua", - 14033: "sage-best-com1", - 14034: "sage-best-com2", - 14141: "vcs-app", - 14142: "icpp", - 14145: "gcm-app", - 14149: "vrts-tdd", - 14150: "vcscmd", - 14154: "vad", - 14250: "cps", - 14414: "ca-web-update", - 14936: "hde-lcesrvr-1", - 14937: "hde-lcesrvr-2", - 15000: "hydap", - 15002: "onep-tls", - 15345: "xpilot", - 15363: "3link", - 15555: "cisco-snat", - 15660: "bex-xr", - 15740: "ptp", - 15999: "programmar", - 16000: "fmsas", - 16001: "fmsascon", - 16002: "gsms", - 16020: "jwpc", - 16021: "jwpc-bin", - 16161: "sun-sea-port", - 16162: "solaris-audit", - 16309: "etb4j", - 16310: "pduncs", - 16311: "pdefmns", - 16360: "netserialext1", - 16361: "netserialext2", - 16367: "netserialext3", - 16368: "netserialext4", - 16384: "connected", - 16619: "xoms", - 16900: "newbay-snc-mc", - 16950: "sgcip", - 16991: "intel-rci-mp", - 16992: "amt-soap-http", - 16993: "amt-soap-https", - 16994: "amt-redir-tcp", - 16995: "amt-redir-tls", - 17007: "isode-dua", - 17184: "vestasdlp", - 17185: "soundsvirtual", - 17219: "chipper", - 17220: "avtp", - 17221: "avdecc", - 17234: "integrius-stp", - 17235: "ssh-mgmt", - 17500: "db-lsp", - 17555: "ailith", - 17729: "ea", - 17754: "zep", - 17755: "zigbee-ip", - 17756: "zigbee-ips", - 17777: "sw-orion", - 18000: "biimenu", - 18104: "radpdf", - 18136: "racf", - 18181: "opsec-cvp", - 18182: "opsec-ufp", - 18183: "opsec-sam", - 18184: "opsec-lea", - 18185: "opsec-omi", - 18186: "ohsc", - 18187: "opsec-ela", - 18241: "checkpoint-rtm", - 18242: "iclid", - 18243: "clusterxl", - 18262: "gv-pf", - 18463: "ac-cluster", - 18634: "rds-ib", - 18635: "rds-ip", - 18769: "ique", - 18881: "infotos", - 18888: "apc-necmp", - 19000: "igrid", - 19007: "scintilla", - 19020: "j-link", - 19191: "opsec-uaa", - 19194: "ua-secureagent", - 19283: "keysrvr", - 19315: "keyshadow", - 19398: "mtrgtrans", - 19410: "hp-sco", - 19411: "hp-sca", - 19412: "hp-sessmon", - 19539: "fxuptp", - 19540: "sxuptp", - 19541: "jcp", - 19998: "iec-104-sec", - 19999: "dnp-sec", - 20000: "dnp", - 20001: "microsan", - 20002: "commtact-http", - 20003: "commtact-https", - 20005: "openwebnet", - 20013: "ss-idi", - 20014: "opendeploy", - 20034: "nburn-id", - 20046: "tmophl7mts", - 20048: "mountd", - 20049: "nfsrdma", - 20167: "tolfab", - 20202: "ipdtp-port", - 20222: "ipulse-ics", - 20480: "emwavemsg", - 20670: "track", - 20999: "athand-mmp", - 21000: "irtrans", - 21010: "notezilla-lan", - 21553: "rdm-tfs", - 21554: "dfserver", - 21590: "vofr-gateway", - 21800: "tvpm", - 21845: "webphone", - 21846: "netspeak-is", - 21847: "netspeak-cs", - 21848: "netspeak-acd", - 21849: "netspeak-cps", - 22000: "snapenetio", - 22001: "optocontrol", - 22002: "optohost002", - 22003: "optohost003", - 22004: "optohost004", - 22005: "optohost004", - 22125: "dcap", - 22128: "gsidcap", - 22222: "easyengine", - 22273: "wnn6", - 22305: "cis", - 22343: "cis-secure", - 22347: "wibukey", - 22350: "codemeter", - 22351: "codemeter-cmwan", - 22537: "caldsoft-backup", - 22555: "vocaltec-wconf", - 22763: "talikaserver", - 22800: "aws-brf", - 22951: "brf-gw", - 23000: "inovaport1", - 23001: "inovaport2", - 23002: "inovaport3", - 23003: "inovaport4", - 23004: "inovaport5", - 23005: "inovaport6", - 23053: "gntp", - 23333: "elxmgmt", - 23400: "novar-dbase", - 23401: "novar-alarm", - 23402: "novar-global", - 23456: "aequus", - 23457: "aequus-alt", - 23546: "areaguard-neo", - 24000: "med-ltp", - 24001: "med-fsp-rx", - 24002: "med-fsp-tx", - 24003: "med-supp", - 24004: "med-ovw", - 24005: "med-ci", - 24006: "med-net-svc", - 24242: "filesphere", - 24249: "vista-4gl", - 24321: "ild", - 24386: "intel-rci", - 24465: "tonidods", - 24554: "binkp", - 24577: "bilobit", - 24676: "canditv", - 24677: "flashfiler", - 24678: "proactivate", - 24680: "tcc-http", - 24754: "cslg", - 24922: "find", - 25000: "icl-twobase1", - 25001: "icl-twobase2", - 25002: "icl-twobase3", - 25003: "icl-twobase4", - 25004: "icl-twobase5", - 25005: "icl-twobase6", - 25006: "icl-twobase7", - 25007: "icl-twobase8", - 25008: "icl-twobase9", - 25009: "icl-twobase10", - 25576: "sauterdongle", - 25604: "idtp", - 25793: "vocaltec-hos", - 25900: "tasp-net", - 25901: "niobserver", - 25902: "nilinkanalyst", - 25903: "niprobe", - 26000: "quake", - 26133: "scscp", - 26208: "wnn6-ds", - 26260: "ezproxy", - 26261: "ezmeeting", - 26262: "k3software-svr", - 26263: "k3software-cli", - 26486: "exoline-tcp", - 26487: "exoconfig", - 26489: "exonet", - 27345: "imagepump", - 27442: "jesmsjc", - 27504: "kopek-httphead", - 27782: "ars-vista", - 27876: "astrolink", - 27999: "tw-auth-key", - 28000: "nxlmd", - 28001: "pqsp", - 28200: "voxelstorm", - 28240: "siemensgsm", - 29167: "otmp", - 29999: "bingbang", - 30000: "ndmps", - 30001: "pago-services1", - 30002: "pago-services2", - 30003: "amicon-fpsu-ra", - 30260: "kingdomsonline", - 30999: "ovobs", - 31020: "autotrac-acp", - 31400: "pace-licensed", - 31416: "xqosd", - 31457: "tetrinet", - 31620: "lm-mon", - 31685: "dsx-monitor", - 31765: "gamesmith-port", - 31948: "iceedcp-tx", - 31949: "iceedcp-rx", - 32034: "iracinghelper", - 32249: "t1distproc60", - 32483: "apm-link", - 32635: "sec-ntb-clnt", - 32636: "DMExpress", - 32767: "filenet-powsrm", - 32768: "filenet-tms", - 32769: "filenet-rpc", - 32770: "filenet-nch", - 32771: "filenet-rmi", - 32772: "filenet-pa", - 32773: "filenet-cm", - 32774: "filenet-re", - 32775: "filenet-pch", - 32776: "filenet-peior", - 32777: "filenet-obrok", - 32801: "mlsn", - 32811: "retp", - 32896: "idmgratm", - 33123: "aurora-balaena", - 33331: "diamondport", - 33333: "dgi-serv", - 33334: "speedtrace", - 33434: "traceroute", - 33656: "snip-slave", - 34249: "turbonote-2", - 34378: "p-net-local", - 34379: "p-net-remote", - 34567: "dhanalakshmi", - 34962: "profinet-rt", - 34963: "profinet-rtm", - 34964: "profinet-cm", - 34980: "ethercat", - 35000: "heathview", - 35001: "rt-viewer", - 35002: "rt-sound", - 35003: "rt-devicemapper", - 35004: "rt-classmanager", - 35005: "rt-labtracker", - 35006: "rt-helper", - 35354: "kitim", - 35355: "altova-lm", - 35356: "guttersnex", - 35357: "openstack-id", - 36001: "allpeers", - 36524: "febooti-aw", - 36602: "observium-agent", - 36865: "kastenxpipe", - 37475: "neckar", - 37483: "gdrive-sync", - 37654: "unisys-eportal", - 38000: "ivs-database", - 38001: "ivs-insertion", - 38201: "galaxy7-data", - 38202: "fairview", - 38203: "agpolicy", - 38800: "sruth", - 38865: "secrmmsafecopya", - 39681: "turbonote-1", - 40000: "safetynetp", - 40404: "sptx", - 40841: "cscp", - 40842: "csccredir", - 40843: "csccfirewall", - 41111: "fs-qos", - 41121: "tentacle", - 41794: "crestron-cip", - 41795: "crestron-ctp", - 41796: "crestron-cips", - 41797: "crestron-ctps", - 42508: "candp", - 42509: "candrp", - 42510: "caerpc", - 43000: "recvr-rc", - 43188: "reachout", - 43189: "ndm-agent-port", - 43190: "ip-provision", - 43191: "noit-transport", - 43210: "shaperai", - 43439: "eq3-update", - 43440: "ew-mgmt", - 43441: "ciscocsdb", - 44123: "z-wave-s", - 44321: "pmcd", - 44322: "pmcdproxy", - 44323: "pmwebapi", - 44444: "cognex-dataman", - 44553: "rbr-debug", - 44818: "EtherNet-IP-2", - 44900: "m3da", - 45000: "asmp", - 45001: "asmps", - 45045: "synctest", - 45054: "invision-ag", - 45678: "eba", - 45824: "dai-shell", - 45825: "qdb2service", - 45966: "ssr-servermgr", - 46998: "spremotetablet", - 46999: "mediabox", - 47000: "mbus", - 47001: "winrm", - 47557: "dbbrowse", - 47624: "directplaysrvr", - 47806: "ap", - 47808: "bacnet", - 48000: "nimcontroller", - 48001: "nimspooler", - 48002: "nimhub", - 48003: "nimgtw", - 48004: "nimbusdb", - 48005: "nimbusdbctrl", - 48049: "3gpp-cbsp", - 48050: "weandsf", - 48128: "isnetserv", - 48129: "blp5", - 48556: "com-bardac-dw", - 48619: "iqobject", - 48653: "robotraconteur", - 49000: "matahari"} - -IANA_PORTS_UDP = { - 1: "tcpmux", - 2: "compressnet", - 3: "compressnet", - 5: "rje", - 7: "echo", - 9: "discard", - 11: "systat", - 13: "daytime", - 17: "qotd", - 18: "msp", - 19: "chargen", - 20: "ftp-data", - 21: "ftp", - 22: "ssh", - 23: "telnet", - 25: "smtp", - 27: "nsw-fe", - 29: "msg-icp", - 31: "msg-auth", - 33: "dsp", - 37: "time", - 38: "rap", - 39: "rlp", - 41: "graphics", - 42: "name", - 43: "nicname", - 44: "mpm-flags", - 45: "mpm", - 46: "mpm-snd", - 47: "ni-ftp", - 48: "auditd", - 49: "tacacs", - 50: "re-mail-ck", - 52: "xns-time", - 53: "domain", - 54: "xns-ch", - 55: "isi-gl", - 56: "xns-auth", - 58: "xns-mail", - 61: "ni-mail", - 62: "acas", - 63: "whoispp", - 64: "covia", - 65: "tacacs-ds", - 66: "sql-net", - 67: "bootps", - 68: "bootpc", - 69: "tftp", - 70: "gopher", - 71: "netrjs-1", - 72: "netrjs-2", - 73: "netrjs-3", - 74: "netrjs-4", - 76: "deos", - 78: "vettcp", - 79: "finger", - 80: "http", - 82: "xfer", - 83: "mit-ml-dev", - 84: "ctf", - 85: "mit-ml-dev", - 86: "mfcobol", - 88: "kerberos", - 89: "su-mit-tg", - 90: "dnsix", - 91: "mit-dov", - 92: "npp", - 93: "dcp", - 94: "objcall", - 95: "supdup", - 96: "dixie", - 97: "swift-rvf", - 98: "tacnews", - 99: "metagram", - 101: "hostname", - 102: "iso-tsap", - 103: "gppitnp", - 104: "acr-nema", - 105: "cso", - 106: "3com-tsmux", - 107: "rtelnet", - 108: "snagas", - 109: "pop2", - 110: "pop3", - 111: "sunrpc", - 112: "mcidas", - 113: "auth", - 115: "sftp", - 116: "ansanotify", - 117: "uucp-path", - 118: "sqlserv", - 119: "nntp", - 120: "cfdptkt", - 121: "erpc", - 122: "smakynet", - 123: "ntp", - 124: "ansatrader", - 125: "locus-map", - 126: "nxedit", - 127: "locus-con", - 128: "gss-xlicen", - 129: "pwdgen", - 130: "cisco-fna", - 131: "cisco-tna", - 132: "cisco-sys", - 133: "statsrv", - 134: "ingres-net", - 135: "epmap", - 136: "profile", - 137: "netbios-ns", - 138: "netbios-dgm", - 139: "netbios-ssn", - 140: "emfis-data", - 141: "emfis-cntl", - 142: "bl-idm", - 143: "imap", - 144: "uma", - 145: "uaac", - 146: "iso-tp0", - 147: "iso-ip", - 148: "jargon", - 149: "aed-512", - 150: "sql-net", - 151: "hems", - 152: "bftp", - 153: "sgmp", - 154: "netsc-prod", - 155: "netsc-dev", - 156: "sqlsrv", - 157: "knet-cmp", - 158: "pcmail-srv", - 159: "nss-routing", - 160: "sgmp-traps", - 161: "snmp", - 162: "snmptrap", - 163: "cmip-man", - 164: "cmip-agent", - 165: "xns-courier", - 166: "s-net", - 167: "namp", - 168: "rsvd", - 169: "send", - 170: "print-srv", - 171: "multiplex", - 172: "cl-1", - 173: "xyplex-mux", - 174: "mailq", - 175: "vmnet", - 176: "genrad-mux", - 177: "xdmcp", - 178: "nextstep", - 179: "bgp", - 180: "ris", - 181: "unify", - 182: "audit", - 183: "ocbinder", - 184: "ocserver", - 185: "remote-kis", - 186: "kis", - 187: "aci", - 188: "mumps", - 189: "qft", - 190: "gacp", - 191: "prospero", - 192: "osu-nms", - 193: "srmp", - 194: "irc", - 195: "dn6-nlm-aud", - 196: "dn6-smm-red", - 197: "dls", - 198: "dls-mon", - 199: "smux", - 200: "src", - 201: "at-rtmp", - 202: "at-nbp", - 203: "at-3", - 204: "at-echo", - 205: "at-5", - 206: "at-zis", - 207: "at-7", - 208: "at-8", - 209: "qmtp", - 210: "z39-50", - 211: "914c-g", - 212: "anet", - 213: "ipx", - 214: "vmpwscs", - 215: "softpc", - 216: "CAIlic", - 217: "dbase", - 218: "mpp", - 219: "uarps", - 220: "imap3", - 221: "fln-spx", - 222: "rsh-spx", - 223: "cdc", - 224: "masqdialer", - 242: "direct", - 243: "sur-meas", - 244: "inbusiness", - 245: "link", - 246: "dsp3270", - 247: "subntbcst-tftp", - 248: "bhfhs", - 256: "rap", - 257: "set", - 259: "esro-gen", - 260: "openport", - 261: "nsiiops", - 262: "arcisdms", - 263: "hdap", - 264: "bgmp", - 265: "x-bone-ctl", - 266: "sst", - 267: "td-service", - 268: "td-replica", - 269: "manet", - 270: "gist", - 280: "http-mgmt", - 281: "personal-link", - 282: "cableport-ax", - 283: "rescap", - 284: "corerjd", - 286: "fxp", - 287: "k-block", - 308: "novastorbakcup", - 309: "entrusttime", - 310: "bhmds", - 311: "asip-webadmin", - 312: "vslmp", - 313: "magenta-logic", - 314: "opalis-robot", - 315: "dpsi", - 316: "decauth", - 317: "zannet", - 318: "pkix-timestamp", - 319: "ptp-event", - 320: "ptp-general", - 321: "pip", - 322: "rtsps", - 333: "texar", - 344: "pdap", - 345: "pawserv", - 346: "zserv", - 347: "fatserv", - 348: "csi-sgwp", - 349: "mftp", - 350: "matip-type-a", - 351: "matip-type-b", - 352: "dtag-ste-sb", - 353: "ndsauth", - 354: "bh611", - 355: "datex-asn", - 356: "cloanto-net-1", - 357: "bhevent", - 358: "shrinkwrap", - 359: "nsrmp", - 360: "scoi2odialog", - 361: "semantix", - 362: "srssend", - 363: "rsvp-tunnel", - 364: "aurora-cmgr", - 365: "dtk", - 366: "odmr", - 367: "mortgageware", - 368: "qbikgdp", - 369: "rpc2portmap", - 370: "codaauth2", - 371: "clearcase", - 372: "ulistproc", - 373: "legent-1", - 374: "legent-2", - 375: "hassle", - 376: "nip", - 377: "tnETOS", - 378: "dsETOS", - 379: "is99c", - 380: "is99s", - 381: "hp-collector", - 382: "hp-managed-node", - 383: "hp-alarm-mgr", - 384: "arns", - 385: "ibm-app", - 386: "asa", - 387: "aurp", - 388: "unidata-ldm", - 389: "ldap", - 390: "uis", - 391: "synotics-relay", - 392: "synotics-broker", - 393: "meta5", - 394: "embl-ndt", - 395: "netcp", - 396: "netware-ip", - 397: "mptn", - 398: "kryptolan", - 399: "iso-tsap-c2", - 400: "osb-sd", - 401: "ups", - 402: "genie", - 403: "decap", - 404: "nced", - 405: "ncld", - 406: "imsp", - 407: "timbuktu", - 408: "prm-sm", - 409: "prm-nm", - 410: "decladebug", - 411: "rmt", - 412: "synoptics-trap", - 413: "smsp", - 414: "infoseek", - 415: "bnet", - 416: "silverplatter", - 417: "onmux", - 418: "hyper-g", - 419: "ariel1", - 420: "smpte", - 421: "ariel2", - 422: "ariel3", - 423: "opc-job-start", - 424: "opc-job-track", - 425: "icad-el", - 426: "smartsdp", - 427: "svrloc", - 428: "ocs-cmu", - 429: "ocs-amu", - 430: "utmpsd", - 431: "utmpcd", - 432: "iasd", - 433: "nnsp", - 434: "mobileip-agent", - 435: "mobilip-mn", - 436: "dna-cml", - 437: "comscm", - 438: "dsfgw", - 439: "dasp", - 440: "sgcp", - 441: "decvms-sysmgt", - 442: "cvc-hostd", - 443: "https", - 444: "snpp", - 445: "microsoft-ds", - 446: "ddm-rdb", - 447: "ddm-dfm", - 448: "ddm-ssl", - 449: "as-servermap", - 450: "tserver", - 451: "sfs-smp-net", - 452: "sfs-config", - 453: "creativeserver", - 454: "contentserver", - 455: "creativepartnr", - 456: "macon-udp", - 457: "scohelp", - 458: "appleqtc", - 459: "ampr-rcmd", - 460: "skronk", - 461: "datasurfsrv", - 462: "datasurfsrvsec", - 463: "alpes", - 464: "kpasswd", - 465: "igmpv3lite", - 466: "digital-vrc", - 467: "mylex-mapd", - 468: "photuris", - 469: "rcp", - 470: "scx-proxy", - 471: "mondex", - 472: "ljk-login", - 473: "hybrid-pop", - 474: "tn-tl-w2", - 475: "tcpnethaspsrv", - 476: "tn-tl-fd1", - 477: "ss7ns", - 478: "spsc", - 479: "iafserver", - 480: "iafdbase", - 481: "ph", - 482: "bgs-nsi", - 483: "ulpnet", - 484: "integra-sme", - 485: "powerburst", - 486: "avian", - 487: "saft", - 488: "gss-http", - 489: "nest-protocol", - 490: "micom-pfs", - 491: "go-login", - 492: "ticf-1", - 493: "ticf-2", - 494: "pov-ray", - 495: "intecourier", - 496: "pim-rp-disc", - 497: "retrospect", - 498: "siam", - 499: "iso-ill", - 500: "isakmp", - 501: "stmf", - 502: "mbap", - 503: "intrinsa", - 504: "citadel", - 505: "mailbox-lm", - 506: "ohimsrv", - 507: "crs", - 508: "xvttp", - 509: "snare", - 510: "fcp", - 511: "passgo", - 512: "comsat", - 513: "who", - 514: "syslog", - 515: "printer", - 516: "videotex", - 517: "talk", - 518: "ntalk", - 519: "utime", - 520: "router", - 521: "ripng", - 522: "ulp", - 523: "ibm-db2", - 524: "ncp", - 525: "timed", - 526: "tempo", - 527: "stx", - 528: "custix", - 529: "irc-serv", - 530: "courier", - 531: "conference", - 532: "netnews", - 533: "netwall", - 534: "windream", - 535: "iiop", - 536: "opalis-rdv", - 537: "nmsp", - 538: "gdomap", - 539: "apertus-ldp", - 540: "uucp", - 541: "uucp-rlogin", - 542: "commerce", - 543: "klogin", - 544: "kshell", - 545: "appleqtcsrvr", - 546: "dhcpv6-client", - 547: "dhcpv6-server", - 548: "afpovertcp", - 549: "idfp", - 550: "new-rwho", - 551: "cybercash", - 552: "devshr-nts", - 553: "pirp", - 554: "rtsp", - 555: "dsf", - 556: "remotefs", - 557: "openvms-sysipc", - 558: "sdnskmp", - 559: "teedtap", - 560: "rmonitor", - 561: "monitor", - 562: "chshell", - 563: "nntps", - 564: "9pfs", - 565: "whoami", - 566: "streettalk", - 567: "banyan-rpc", - 568: "ms-shuttle", - 569: "ms-rome", - 570: "meter", - 571: "meter", - 572: "sonar", - 573: "banyan-vip", - 574: "ftp-agent", - 575: "vemmi", - 576: "ipcd", - 577: "vnas", - 578: "ipdd", - 579: "decbsrv", - 580: "sntp-heartbeat", - 581: "bdp", - 582: "scc-security", - 583: "philips-vc", - 584: "keyserver", - 586: "password-chg", - 587: "submission", - 588: "cal", - 589: "eyelink", - 590: "tns-cml", - 591: "http-alt", - 592: "eudora-set", - 593: "http-rpc-epmap", - 594: "tpip", - 595: "cab-protocol", - 596: "smsd", - 597: "ptcnameservice", - 598: "sco-websrvrmg3", - 599: "acp", - 600: "ipcserver", - 601: "syslog-conn", - 602: "xmlrpc-beep", - 603: "idxp", - 604: "tunnel", - 605: "soap-beep", - 606: "urm", - 607: "nqs", - 608: "sift-uft", - 609: "npmp-trap", - 610: "npmp-local", - 611: "npmp-gui", - 612: "hmmp-ind", - 613: "hmmp-op", - 614: "sshell", - 615: "sco-inetmgr", - 616: "sco-sysmgr", - 617: "sco-dtmgr", - 618: "dei-icda", - 619: "compaq-evm", - 620: "sco-websrvrmgr", - 621: "escp-ip", - 622: "collaborator", - 623: "asf-rmcp", - 624: "cryptoadmin", - 625: "dec-dlm", - 626: "asia", - 627: "passgo-tivoli", - 628: "qmqp", - 629: "3com-amp3", - 630: "rda", - 631: "ipp", - 632: "bmpp", - 633: "servstat", - 634: "ginad", - 635: "rlzdbase", - 636: "ldaps", - 637: "lanserver", - 638: "mcns-sec", - 639: "msdp", - 640: "entrust-sps", - 641: "repcmd", - 642: "esro-emsdp", - 643: "sanity", - 644: "dwr", - 645: "pssc", - 646: "ldp", - 647: "dhcp-failover", - 648: "rrp", - 649: "cadview-3d", - 650: "obex", - 651: "ieee-mms", - 652: "hello-port", - 653: "repscmd", - 654: "aodv", - 655: "tinc", - 656: "spmp", - 657: "rmc", - 658: "tenfold", - 660: "mac-srvr-admin", - 661: "hap", - 662: "pftp", - 663: "purenoise", - 664: "asf-secure-rmcp", - 665: "sun-dr", - 666: "mdqs", - 667: "disclose", - 668: "mecomm", - 669: "meregister", - 670: "vacdsm-sws", - 671: "vacdsm-app", - 672: "vpps-qua", - 673: "cimplex", - 674: "acap", - 675: "dctp", - 676: "vpps-via", - 677: "vpp", - 678: "ggf-ncp", - 679: "mrm", - 680: "entrust-aaas", - 681: "entrust-aams", - 682: "xfr", - 683: "corba-iiop", - 684: "corba-iiop-ssl", - 685: "mdc-portmapper", - 686: "hcp-wismar", - 687: "asipregistry", - 688: "realm-rusd", - 689: "nmap", - 690: "vatp", - 691: "msexch-routing", - 692: "hyperwave-isp", - 693: "connendp", - 694: "ha-cluster", - 695: "ieee-mms-ssl", - 696: "rushd", - 697: "uuidgen", - 698: "olsr", - 699: "accessnetwork", - 700: "epp", - 701: "lmp", - 702: "iris-beep", - 704: "elcsd", - 705: "agentx", - 706: "silc", - 707: "borland-dsj", - 709: "entrust-kmsh", - 710: "entrust-ash", - 711: "cisco-tdp", - 712: "tbrpf", - 713: "iris-xpc", - 714: "iris-xpcs", - 715: "iris-lwz", - 716: "pana", - 729: "netviewdm1", - 730: "netviewdm2", - 731: "netviewdm3", - 741: "netgw", - 742: "netrcs", - 744: "flexlm", - 747: "fujitsu-dev", - 748: "ris-cm", - 749: "kerberos-adm", - 750: "loadav", - 751: "pump", - 752: "qrh", - 753: "rrh", - 754: "tell", - 758: "nlogin", - 759: "con", - 760: "ns", - 761: "rxe", - 762: "quotad", - 763: "cycleserv", - 764: "omserv", - 765: "webster", - 767: "phonebook", - 769: "vid", - 770: "cadlock", - 771: "rtip", - 772: "cycleserv2", - 773: "notify", - 774: "acmaint-dbd", - 775: "acmaint-transd", - 776: "wpages", - 777: "multiling-http", - 780: "wpgs", - 800: "mdbs-daemon", - 801: "device", - 802: "mbap-s", - 810: "fcp-udp", - 828: "itm-mcell-s", - 829: "pkix-3-ca-ra", - 830: "netconf-ssh", - 831: "netconf-beep", - 832: "netconfsoaphttp", - 833: "netconfsoapbeep", - 847: "dhcp-failover2", - 848: "gdoi", - 860: "iscsi", - 861: "owamp-control", - 862: "twamp-control", - 873: "rsync", - 886: "iclcnet-locate", - 887: "iclcnet-svinfo", - 888: "accessbuilder", - 900: "omginitialrefs", - 901: "smpnameres", - 902: "ideafarm-door", - 903: "ideafarm-panic", - 910: "kink", - 911: "xact-backup", - 912: "apex-mesh", - 913: "apex-edge", - 989: "ftps-data", - 990: "ftps", - 991: "nas", - 992: "telnets", - 993: "imaps", - 995: "pop3s", - 996: "vsinet", - 997: "maitrd", - 998: "puparp", - 999: "applix", - 1000: "cadlock2", - 1010: "surf", - 1021: "exp1", - 1022: "exp2", - 1025: "blackjack", - 1026: "cap", - 1027: "6a44", - 1029: "solid-mux", - 1033: "netinfo-local", - 1034: "activesync", - 1035: "mxxrlogin", - 1036: "nsstp", - 1037: "ams", - 1038: "mtqp", - 1039: "sbl", - 1040: "netarx", - 1041: "danf-ak2", - 1042: "afrog", - 1043: "boinc-client", - 1044: "dcutility", - 1045: "fpitp", - 1046: "wfremotertm", - 1047: "neod1", - 1048: "neod2", - 1049: "td-postman", - 1050: "cma", - 1051: "optima-vnet", - 1052: "ddt", - 1053: "remote-as", - 1054: "brvread", - 1055: "ansyslmd", - 1056: "vfo", - 1057: "startron", - 1058: "nim", - 1059: "nimreg", - 1060: "polestar", - 1061: "kiosk", - 1062: "veracity", - 1063: "kyoceranetdev", - 1064: "jstel", - 1065: "syscomlan", - 1066: "fpo-fns", - 1067: "instl-boots", - 1068: "instl-bootc", - 1069: "cognex-insight", - 1070: "gmrupdateserv", - 1071: "bsquare-voip", - 1072: "cardax", - 1073: "bridgecontrol", - 1074: "warmspotMgmt", - 1075: "rdrmshc", - 1076: "dab-sti-c", - 1077: "imgames", - 1078: "avocent-proxy", - 1079: "asprovatalk", - 1080: "socks", - 1081: "pvuniwien", - 1082: "amt-esd-prot", - 1083: "ansoft-lm-1", - 1084: "ansoft-lm-2", - 1085: "webobjects", - 1086: "cplscrambler-lg", - 1087: "cplscrambler-in", - 1088: "cplscrambler-al", - 1089: "ff-annunc", - 1090: "ff-fms", - 1091: "ff-sm", - 1092: "obrpd", - 1093: "proofd", - 1094: "rootd", - 1095: "nicelink", - 1096: "cnrprotocol", - 1097: "sunclustermgr", - 1098: "rmiactivation", - 1099: "rmiregistry", - 1100: "mctp", - 1101: "pt2-discover", - 1102: "adobeserver-1", - 1103: "adobeserver-2", - 1104: "xrl", - 1105: "ftranhc", - 1106: "isoipsigport-1", - 1107: "isoipsigport-2", - 1108: "ratio-adp", - 1110: "nfsd-keepalive", - 1111: "lmsocialserver", - 1112: "icp", - 1113: "ltp-deepspace", - 1114: "mini-sql", - 1115: "ardus-trns", - 1116: "ardus-cntl", - 1117: "ardus-mtrns", - 1118: "sacred", - 1119: "bnetgame", - 1120: "bnetfile", - 1121: "rmpp", - 1122: "availant-mgr", - 1123: "murray", - 1124: "hpvmmcontrol", - 1125: "hpvmmagent", - 1126: "hpvmmdata", - 1127: "kwdb-commn", - 1128: "saphostctrl", - 1129: "saphostctrls", - 1130: "casp", - 1131: "caspssl", - 1132: "kvm-via-ip", - 1133: "dfn", - 1134: "aplx", - 1135: "omnivision", - 1136: "hhb-gateway", - 1137: "trim", - 1138: "encrypted-admin", - 1139: "evm", - 1140: "autonoc", - 1141: "mxomss", - 1142: "edtools", - 1143: "imyx", - 1144: "fuscript", - 1145: "x9-icue", - 1146: "audit-transfer", - 1147: "capioverlan", - 1148: "elfiq-repl", - 1149: "bvtsonar", - 1150: "blaze", - 1151: "unizensus", - 1152: "winpoplanmess", - 1153: "c1222-acse", - 1154: "resacommunity", - 1155: "nfa", - 1156: "iascontrol-oms", - 1157: "iascontrol", - 1158: "dbcontrol-oms", - 1159: "oracle-oms", - 1160: "olsv", - 1161: "health-polling", - 1162: "health-trap", - 1163: "sddp", - 1164: "qsm-proxy", - 1165: "qsm-gui", - 1166: "qsm-remote", - 1167: "cisco-ipsla", - 1168: "vchat", - 1169: "tripwire", - 1170: "atc-lm", - 1171: "atc-appserver", - 1172: "dnap", - 1173: "d-cinema-rrp", - 1174: "fnet-remote-ui", - 1175: "dossier", - 1176: "indigo-server", - 1177: "dkmessenger", - 1178: "sgi-storman", - 1179: "b2n", - 1180: "mc-client", - 1181: "3comnetman", - 1182: "accelenet-data", - 1183: "llsurfup-http", - 1184: "llsurfup-https", - 1185: "catchpole", - 1186: "mysql-cluster", - 1187: "alias", - 1188: "hp-webadmin", - 1189: "unet", - 1190: "commlinx-avl", - 1191: "gpfs", - 1192: "caids-sensor", - 1193: "fiveacross", - 1194: "openvpn", - 1195: "rsf-1", - 1196: "netmagic", - 1197: "carrius-rshell", - 1198: "cajo-discovery", - 1199: "dmidi", - 1200: "scol", - 1201: "nucleus-sand", - 1202: "caiccipc", - 1203: "ssslic-mgr", - 1204: "ssslog-mgr", - 1205: "accord-mgc", - 1206: "anthony-data", - 1207: "metasage", - 1208: "seagull-ais", - 1209: "ipcd3", - 1210: "eoss", - 1211: "groove-dpp", - 1212: "lupa", - 1213: "mpc-lifenet", - 1214: "kazaa", - 1215: "scanstat-1", - 1216: "etebac5", - 1217: "hpss-ndapi", - 1218: "aeroflight-ads", - 1219: "aeroflight-ret", - 1220: "qt-serveradmin", - 1221: "sweetware-apps", - 1222: "nerv", - 1223: "tgp", - 1224: "vpnz", - 1225: "slinkysearch", - 1226: "stgxfws", - 1227: "dns2go", - 1228: "florence", - 1229: "zented", - 1230: "periscope", - 1231: "menandmice-lpm", - 1232: "first-defense", - 1233: "univ-appserver", - 1234: "search-agent", - 1235: "mosaicsyssvc1", - 1236: "bvcontrol", - 1237: "tsdos390", - 1238: "hacl-qs", - 1239: "nmsd", - 1240: "instantia", - 1241: "nessus", - 1242: "nmasoverip", - 1243: "serialgateway", - 1244: "isbconference1", - 1245: "isbconference2", - 1246: "payrouter", - 1247: "visionpyramid", - 1248: "hermes", - 1249: "mesavistaco", - 1250: "swldy-sias", - 1251: "servergraph", - 1252: "bspne-pcc", - 1253: "q55-pcc", - 1254: "de-noc", - 1255: "de-cache-query", - 1256: "de-server", - 1257: "shockwave2", - 1258: "opennl", - 1259: "opennl-voice", - 1260: "ibm-ssd", - 1261: "mpshrsv", - 1262: "qnts-orb", - 1263: "dka", - 1264: "prat", - 1265: "dssiapi", - 1266: "dellpwrappks", - 1267: "epc", - 1268: "propel-msgsys", - 1269: "watilapp", - 1270: "opsmgr", - 1271: "excw", - 1272: "cspmlockmgr", - 1273: "emc-gateway", - 1274: "t1distproc", - 1275: "ivcollector", - 1277: "miva-mqs", - 1278: "dellwebadmin-1", - 1279: "dellwebadmin-2", - 1280: "pictrography", - 1281: "healthd", - 1282: "emperion", - 1283: "productinfo", - 1284: "iee-qfx", - 1285: "neoiface", - 1286: "netuitive", - 1287: "routematch", - 1288: "navbuddy", - 1289: "jwalkserver", - 1290: "winjaserver", - 1291: "seagulllms", - 1292: "dsdn", - 1293: "pkt-krb-ipsec", - 1294: "cmmdriver", - 1295: "ehtp", - 1296: "dproxy", - 1297: "sdproxy", - 1298: "lpcp", - 1299: "hp-sci", - 1300: "h323hostcallsc", - 1301: "ci3-software-1", - 1302: "ci3-software-2", - 1303: "sftsrv", - 1304: "boomerang", - 1305: "pe-mike", - 1306: "re-conn-proto", - 1307: "pacmand", - 1308: "odsi", - 1309: "jtag-server", - 1310: "husky", - 1311: "rxmon", - 1312: "sti-envision", - 1313: "bmc-patroldb", - 1314: "pdps", - 1315: "els", - 1316: "exbit-escp", - 1317: "vrts-ipcserver", - 1318: "krb5gatekeeper", - 1319: "amx-icsp", - 1320: "amx-axbnet", - 1321: "pip", - 1322: "novation", - 1323: "brcd", - 1324: "delta-mcp", - 1325: "dx-instrument", - 1326: "wimsic", - 1327: "ultrex", - 1328: "ewall", - 1329: "netdb-export", - 1330: "streetperfect", - 1331: "intersan", - 1332: "pcia-rxp-b", - 1333: "passwrd-policy", - 1334: "writesrv", - 1335: "digital-notary", - 1336: "ischat", - 1337: "menandmice-dns", - 1338: "wmc-log-svc", - 1339: "kjtsiteserver", - 1340: "naap", - 1341: "qubes", - 1342: "esbroker", - 1343: "re101", - 1344: "icap", - 1345: "vpjp", - 1346: "alta-ana-lm", - 1347: "bbn-mmc", - 1348: "bbn-mmx", - 1349: "sbook", - 1350: "editbench", - 1351: "equationbuilder", - 1352: "lotusnote", - 1353: "relief", - 1354: "XSIP-network", - 1355: "intuitive-edge", - 1356: "cuillamartin", - 1357: "pegboard", - 1358: "connlcli", - 1359: "ftsrv", - 1360: "mimer", - 1361: "linx", - 1362: "timeflies", - 1363: "ndm-requester", - 1364: "ndm-server", - 1365: "adapt-sna", - 1366: "netware-csp", - 1367: "dcs", - 1368: "screencast", - 1369: "gv-us", - 1370: "us-gv", - 1371: "fc-cli", - 1372: "fc-ser", - 1373: "chromagrafx", - 1374: "molly", - 1375: "bytex", - 1376: "ibm-pps", - 1377: "cichlid", - 1378: "elan", - 1379: "dbreporter", - 1380: "telesis-licman", - 1381: "apple-licman", - 1382: "udt-os", - 1383: "gwha", - 1384: "os-licman", - 1385: "atex-elmd", - 1386: "checksum", - 1387: "cadsi-lm", - 1388: "objective-dbc", - 1389: "iclpv-dm", - 1390: "iclpv-sc", - 1391: "iclpv-sas", - 1392: "iclpv-pm", - 1393: "iclpv-nls", - 1394: "iclpv-nlc", - 1395: "iclpv-wsm", - 1396: "dvl-activemail", - 1397: "audio-activmail", - 1398: "video-activmail", - 1399: "cadkey-licman", - 1400: "cadkey-tablet", - 1401: "goldleaf-licman", - 1402: "prm-sm-np", - 1403: "prm-nm-np", - 1404: "igi-lm", - 1405: "ibm-res", - 1406: "netlabs-lm", - 1407: "dbsa-lm", - 1408: "sophia-lm", - 1409: "here-lm", - 1410: "hiq", - 1411: "af", - 1412: "innosys", - 1413: "innosys-acl", - 1414: "ibm-mqseries", - 1415: "dbstar", - 1416: "novell-lu6-2", - 1417: "timbuktu-srv1", - 1418: "timbuktu-srv2", - 1419: "timbuktu-srv3", - 1420: "timbuktu-srv4", - 1421: "gandalf-lm", - 1422: "autodesk-lm", - 1423: "essbase", - 1424: "hybrid", - 1425: "zion-lm", - 1426: "sais", - 1427: "mloadd", - 1428: "informatik-lm", - 1429: "nms", - 1430: "tpdu", - 1431: "rgtp", - 1432: "blueberry-lm", - 1433: "ms-sql-s", - 1434: "ms-sql-m", - 1435: "ibm-cics", - 1436: "saism", - 1437: "tabula", - 1438: "eicon-server", - 1439: "eicon-x25", - 1440: "eicon-slp", - 1441: "cadis-1", - 1442: "cadis-2", - 1443: "ies-lm", - 1444: "marcam-lm", - 1445: "proxima-lm", - 1446: "ora-lm", - 1447: "apri-lm", - 1448: "oc-lm", - 1449: "peport", - 1450: "dwf", - 1451: "infoman", - 1452: "gtegsc-lm", - 1453: "genie-lm", - 1454: "interhdl-elmd", - 1455: "esl-lm", - 1456: "dca", - 1457: "valisys-lm", - 1458: "nrcabq-lm", - 1459: "proshare1", - 1460: "proshare2", - 1461: "ibm-wrless-lan", - 1462: "world-lm", - 1463: "nucleus", - 1464: "msl-lmd", - 1465: "pipes", - 1466: "oceansoft-lm", - 1467: "csdmbase", - 1468: "csdm", - 1469: "aal-lm", - 1470: "uaiact", - 1471: "csdmbase", - 1472: "csdm", - 1473: "openmath", - 1474: "telefinder", - 1475: "taligent-lm", - 1476: "clvm-cfg", - 1477: "ms-sna-server", - 1478: "ms-sna-base", - 1479: "dberegister", - 1480: "pacerforum", - 1481: "airs", - 1482: "miteksys-lm", - 1483: "afs", - 1484: "confluent", - 1485: "lansource", - 1486: "nms-topo-serv", - 1487: "localinfosrvr", - 1488: "docstor", - 1489: "dmdocbroker", - 1490: "insitu-conf", - 1492: "stone-design-1", - 1493: "netmap-lm", - 1494: "ica", - 1495: "cvc", - 1496: "liberty-lm", - 1497: "rfx-lm", - 1498: "sybase-sqlany", - 1499: "fhc", - 1500: "vlsi-lm", - 1501: "saiscm", - 1502: "shivadiscovery", - 1503: "imtc-mcs", - 1504: "evb-elm", - 1505: "funkproxy", - 1506: "utcd", - 1507: "symplex", - 1508: "diagmond", - 1509: "robcad-lm", - 1510: "mvx-lm", - 1511: "3l-l1", - 1512: "wins", - 1513: "fujitsu-dtc", - 1514: "fujitsu-dtcns", - 1515: "ifor-protocol", - 1516: "vpad", - 1517: "vpac", - 1518: "vpvd", - 1519: "vpvc", - 1520: "atm-zip-office", - 1521: "ncube-lm", - 1522: "ricardo-lm", - 1523: "cichild-lm", - 1524: "ingreslock", - 1525: "orasrv", - 1526: "pdap-np", - 1527: "tlisrv", - 1529: "coauthor", - 1530: "rap-service", - 1531: "rap-listen", - 1532: "miroconnect", - 1533: "virtual-places", - 1534: "micromuse-lm", - 1535: "ampr-info", - 1536: "ampr-inter", - 1537: "sdsc-lm", - 1538: "3ds-lm", - 1539: "intellistor-lm", - 1540: "rds", - 1541: "rds2", - 1542: "gridgen-elmd", - 1543: "simba-cs", - 1544: "aspeclmd", - 1545: "vistium-share", - 1546: "abbaccuray", - 1547: "laplink", - 1548: "axon-lm", - 1549: "shivasound", - 1550: "3m-image-lm", - 1551: "hecmtl-db", - 1552: "pciarray", - 1553: "sna-cs", - 1554: "caci-lm", - 1555: "livelan", - 1556: "veritas-pbx", - 1557: "arbortext-lm", - 1558: "xingmpeg", - 1559: "web2host", - 1560: "asci-val", - 1561: "facilityview", - 1562: "pconnectmgr", - 1563: "cadabra-lm", - 1564: "pay-per-view", - 1565: "winddlb", - 1566: "corelvideo", - 1567: "jlicelmd", - 1568: "tsspmap", - 1569: "ets", - 1570: "orbixd", - 1571: "rdb-dbs-disp", - 1572: "chip-lm", - 1573: "itscomm-ns", - 1574: "mvel-lm", - 1575: "oraclenames", - 1576: "moldflow-lm", - 1577: "hypercube-lm", - 1578: "jacobus-lm", - 1579: "ioc-sea-lm", - 1580: "tn-tl-r2", - 1581: "mil-2045-47001", - 1582: "msims", - 1583: "simbaexpress", - 1584: "tn-tl-fd2", - 1585: "intv", - 1586: "ibm-abtact", - 1587: "pra-elmd", - 1588: "triquest-lm", - 1589: "vqp", - 1590: "gemini-lm", - 1591: "ncpm-pm", - 1592: "commonspace", - 1593: "mainsoft-lm", - 1594: "sixtrak", - 1595: "radio", - 1596: "radio-bc", - 1597: "orbplus-iiop", - 1598: "picknfs", - 1599: "simbaservices", - 1600: "issd", - 1601: "aas", - 1602: "inspect", - 1603: "picodbc", - 1604: "icabrowser", - 1605: "slp", - 1606: "slm-api", - 1607: "stt", - 1608: "smart-lm", - 1609: "isysg-lm", - 1610: "taurus-wh", - 1611: "ill", - 1612: "netbill-trans", - 1613: "netbill-keyrep", - 1614: "netbill-cred", - 1615: "netbill-auth", - 1616: "netbill-prod", - 1617: "nimrod-agent", - 1618: "skytelnet", - 1619: "xs-openstorage", - 1620: "faxportwinport", - 1621: "softdataphone", - 1622: "ontime", - 1623: "jaleosnd", - 1624: "udp-sr-port", - 1625: "svs-omagent", - 1626: "shockwave", - 1627: "t128-gateway", - 1628: "lontalk-norm", - 1629: "lontalk-urgnt", - 1630: "oraclenet8cman", - 1631: "visitview", - 1632: "pammratc", - 1633: "pammrpc", - 1634: "loaprobe", - 1635: "edb-server1", - 1636: "isdc", - 1637: "islc", - 1638: "ismc", - 1639: "cert-initiator", - 1640: "cert-responder", - 1641: "invision", - 1642: "isis-am", - 1643: "isis-ambc", - 1644: "saiseh", - 1645: "sightline", - 1646: "sa-msg-port", - 1647: "rsap", - 1648: "concurrent-lm", - 1649: "kermit", - 1650: "nkd", - 1651: "shiva-confsrvr", - 1652: "xnmp", - 1653: "alphatech-lm", - 1654: "stargatealerts", - 1655: "dec-mbadmin", - 1656: "dec-mbadmin-h", - 1657: "fujitsu-mmpdc", - 1658: "sixnetudr", - 1659: "sg-lm", - 1660: "skip-mc-gikreq", - 1661: "netview-aix-1", - 1662: "netview-aix-2", - 1663: "netview-aix-3", - 1664: "netview-aix-4", - 1665: "netview-aix-5", - 1666: "netview-aix-6", - 1667: "netview-aix-7", - 1668: "netview-aix-8", - 1669: "netview-aix-9", - 1670: "netview-aix-10", - 1671: "netview-aix-11", - 1672: "netview-aix-12", - 1673: "proshare-mc-1", - 1674: "proshare-mc-2", - 1675: "pdp", - 1676: "netcomm2", - 1677: "groupwise", - 1678: "prolink", - 1679: "darcorp-lm", - 1680: "microcom-sbp", - 1681: "sd-elmd", - 1682: "lanyon-lantern", - 1683: "ncpm-hip", - 1684: "snaresecure", - 1685: "n2nremote", - 1686: "cvmon", - 1687: "nsjtp-ctrl", - 1688: "nsjtp-data", - 1689: "firefox", - 1690: "ng-umds", - 1691: "empire-empuma", - 1692: "sstsys-lm", - 1693: "rrirtr", - 1694: "rrimwm", - 1695: "rrilwm", - 1696: "rrifmm", - 1697: "rrisat", - 1698: "rsvp-encap-1", - 1699: "rsvp-encap-2", - 1700: "mps-raft", - 1701: "l2f", - 1702: "deskshare", - 1703: "hb-engine", - 1704: "bcs-broker", - 1705: "slingshot", - 1706: "jetform", - 1707: "vdmplay", - 1708: "gat-lmd", - 1709: "centra", - 1710: "impera", - 1711: "pptconference", - 1712: "registrar", - 1713: "conferencetalk", - 1714: "sesi-lm", - 1715: "houdini-lm", - 1716: "xmsg", - 1717: "fj-hdnet", - 1718: "h323gatedisc", - 1719: "h323gatestat", - 1720: "h323hostcall", - 1721: "caicci", - 1722: "hks-lm", - 1723: "pptp", - 1724: "csbphonemaster", - 1725: "iden-ralp", - 1726: "iberiagames", - 1727: "winddx", - 1728: "telindus", - 1729: "citynl", - 1730: "roketz", - 1731: "msiccp", - 1732: "proxim", - 1733: "siipat", - 1734: "cambertx-lm", - 1735: "privatechat", - 1736: "street-stream", - 1737: "ultimad", - 1738: "gamegen1", - 1739: "webaccess", - 1740: "encore", - 1741: "cisco-net-mgmt", - 1742: "3Com-nsd", - 1743: "cinegrfx-lm", - 1744: "ncpm-ft", - 1745: "remote-winsock", - 1746: "ftrapid-1", - 1747: "ftrapid-2", - 1748: "oracle-em1", - 1749: "aspen-services", - 1750: "sslp", - 1751: "swiftnet", - 1752: "lofr-lm", - 1754: "oracle-em2", - 1755: "ms-streaming", - 1756: "capfast-lmd", - 1757: "cnhrp", - 1758: "tftp-mcast", - 1759: "spss-lm", - 1760: "www-ldap-gw", - 1761: "cft-0", - 1762: "cft-1", - 1763: "cft-2", - 1764: "cft-3", - 1765: "cft-4", - 1766: "cft-5", - 1767: "cft-6", - 1768: "cft-7", - 1769: "bmc-net-adm", - 1770: "bmc-net-svc", - 1771: "vaultbase", - 1772: "essweb-gw", - 1773: "kmscontrol", - 1774: "global-dtserv", - 1776: "femis", - 1777: "powerguardian", - 1778: "prodigy-intrnet", - 1779: "pharmasoft", - 1780: "dpkeyserv", - 1781: "answersoft-lm", - 1782: "hp-hcip", - 1784: "finle-lm", - 1785: "windlm", - 1786: "funk-logger", - 1787: "funk-license", - 1788: "psmond", - 1789: "hello", - 1790: "nmsp", - 1791: "ea1", - 1792: "ibm-dt-2", - 1793: "rsc-robot", - 1794: "cera-bcm", - 1795: "dpi-proxy", - 1796: "vocaltec-admin", - 1797: "uma", - 1798: "etp", - 1799: "netrisk", - 1800: "ansys-lm", - 1801: "msmq", - 1802: "concomp1", - 1803: "hp-hcip-gwy", - 1804: "enl", - 1805: "enl-name", - 1806: "musiconline", - 1807: "fhsp", - 1808: "oracle-vp2", - 1809: "oracle-vp1", - 1810: "jerand-lm", - 1811: "scientia-sdb", - 1812: "radius", - 1813: "radius-acct", - 1814: "tdp-suite", - 1815: "mmpft", - 1816: "harp", - 1817: "rkb-oscs", - 1818: "etftp", - 1819: "plato-lm", - 1820: "mcagent", - 1821: "donnyworld", - 1822: "es-elmd", - 1823: "unisys-lm", - 1824: "metrics-pas", - 1825: "direcpc-video", - 1826: "ardt", - 1827: "asi", - 1828: "itm-mcell-u", - 1829: "optika-emedia", - 1830: "net8-cman", - 1831: "myrtle", - 1832: "tht-treasure", - 1833: "udpradio", - 1834: "ardusuni", - 1835: "ardusmul", - 1836: "ste-smsc", - 1837: "csoft1", - 1838: "talnet", - 1839: "netopia-vo1", - 1840: "netopia-vo2", - 1841: "netopia-vo3", - 1842: "netopia-vo4", - 1843: "netopia-vo5", - 1844: "direcpc-dll", - 1845: "altalink", - 1846: "tunstall-pnc", - 1847: "slp-notify", - 1848: "fjdocdist", - 1849: "alpha-sms", - 1850: "gsi", - 1851: "ctcd", - 1852: "virtual-time", - 1853: "vids-avtp", - 1854: "buddy-draw", - 1855: "fiorano-rtrsvc", - 1856: "fiorano-msgsvc", - 1857: "datacaptor", - 1858: "privateark", - 1859: "gammafetchsvr", - 1860: "sunscalar-svc", - 1861: "lecroy-vicp", - 1862: "mysql-cm-agent", - 1863: "msnp", - 1864: "paradym-31port", - 1865: "entp", - 1866: "swrmi", - 1867: "udrive", - 1868: "viziblebrowser", - 1869: "transact", - 1870: "sunscalar-dns", - 1871: "canocentral0", - 1872: "canocentral1", - 1873: "fjmpjps", - 1874: "fjswapsnp", - 1875: "westell-stats", - 1876: "ewcappsrv", - 1877: "hp-webqosdb", - 1878: "drmsmc", - 1879: "nettgain-nms", - 1880: "vsat-control", - 1881: "ibm-mqseries2", - 1882: "ecsqdmn", - 1883: "ibm-mqisdp", - 1884: "idmaps", - 1885: "vrtstrapserver", - 1886: "leoip", - 1887: "filex-lport", - 1888: "ncconfig", - 1889: "unify-adapter", - 1890: "wilkenlistener", - 1891: "childkey-notif", - 1892: "childkey-ctrl", - 1893: "elad", - 1894: "o2server-port", - 1896: "b-novative-ls", - 1897: "metaagent", - 1898: "cymtec-port", - 1899: "mc2studios", - 1900: "ssdp", - 1901: "fjicl-tep-a", - 1902: "fjicl-tep-b", - 1903: "linkname", - 1904: "fjicl-tep-c", - 1905: "sugp", - 1906: "tpmd", - 1907: "intrastar", - 1908: "dawn", - 1909: "global-wlink", - 1910: "ultrabac", - 1911: "mtp", - 1912: "rhp-iibp", - 1913: "armadp", - 1914: "elm-momentum", - 1915: "facelink", - 1916: "persona", - 1917: "noagent", - 1918: "can-nds", - 1919: "can-dch", - 1920: "can-ferret", - 1921: "noadmin", - 1922: "tapestry", - 1923: "spice", - 1924: "xiip", - 1925: "discovery-port", - 1926: "egs", - 1927: "videte-cipc", - 1928: "emsd-port", - 1929: "bandwiz-system", - 1930: "driveappserver", - 1931: "amdsched", - 1932: "ctt-broker", - 1933: "xmapi", - 1934: "xaapi", - 1935: "macromedia-fcs", - 1936: "jetcmeserver", - 1937: "jwserver", - 1938: "jwclient", - 1939: "jvserver", - 1940: "jvclient", - 1941: "dic-aida", - 1942: "res", - 1943: "beeyond-media", - 1944: "close-combat", - 1945: "dialogic-elmd", - 1946: "tekpls", - 1947: "sentinelsrm", - 1948: "eye2eye", - 1949: "ismaeasdaqlive", - 1950: "ismaeasdaqtest", - 1951: "bcs-lmserver", - 1952: "mpnjsc", - 1953: "rapidbase", - 1954: "abr-api", - 1955: "abr-secure", - 1956: "vrtl-vmf-ds", - 1957: "unix-status", - 1958: "dxadmind", - 1959: "simp-all", - 1960: "nasmanager", - 1961: "bts-appserver", - 1962: "biap-mp", - 1963: "webmachine", - 1964: "solid-e-engine", - 1965: "tivoli-npm", - 1966: "slush", - 1967: "sns-quote", - 1968: "lipsinc", - 1969: "lipsinc1", - 1970: "netop-rc", - 1971: "netop-school", - 1972: "intersys-cache", - 1973: "dlsrap", - 1974: "drp", - 1975: "tcoflashagent", - 1976: "tcoregagent", - 1977: "tcoaddressbook", - 1978: "unisql", - 1979: "unisql-java", - 1980: "pearldoc-xact", - 1981: "p2pq", - 1982: "estamp", - 1983: "lhtp", - 1984: "bb", - 1985: "hsrp", - 1986: "licensedaemon", - 1987: "tr-rsrb-p1", - 1988: "tr-rsrb-p2", - 1989: "tr-rsrb-p3", - 1990: "stun-p1", - 1991: "stun-p2", - 1992: "stun-p3", - 1993: "snmp-tcp-port", - 1994: "stun-port", - 1995: "perf-port", - 1996: "tr-rsrb-port", - 1997: "gdp-port", - 1998: "x25-svc-port", - 1999: "tcp-id-port", - 2000: "cisco-sccp", - 2001: "wizard", - 2002: "globe", - 2003: "brutus", - 2004: "emce", - 2005: "oracle", - 2006: "raid-cd", - 2007: "raid-am", - 2008: "terminaldb", - 2009: "whosockami", - 2010: "pipe-server", - 2011: "servserv", - 2012: "raid-ac", - 2013: "raid-cd", - 2014: "raid-sf", - 2015: "raid-cs", - 2016: "bootserver", - 2017: "bootclient", - 2018: "rellpack", - 2019: "about", - 2020: "xinupageserver", - 2021: "xinuexpansion1", - 2022: "xinuexpansion2", - 2023: "xinuexpansion3", - 2024: "xinuexpansion4", - 2025: "xribs", - 2026: "scrabble", - 2027: "shadowserver", - 2028: "submitserver", - 2029: "hsrpv6", - 2030: "device2", - 2031: "mobrien-chat", - 2032: "blackboard", - 2033: "glogger", - 2034: "scoremgr", - 2035: "imsldoc", - 2036: "e-dpnet", - 2037: "applus", - 2038: "objectmanager", - 2039: "prizma", - 2040: "lam", - 2041: "interbase", - 2042: "isis", - 2043: "isis-bcast", - 2044: "rimsl", - 2045: "cdfunc", - 2046: "sdfunc", - 2047: "dls", - 2048: "dls-monitor", - 2049: "shilp", - 2050: "av-emb-config", - 2051: "epnsdp", - 2052: "clearvisn", - 2053: "lot105-ds-upd", - 2054: "weblogin", - 2055: "iop", - 2056: "omnisky", - 2057: "rich-cp", - 2058: "newwavesearch", - 2059: "bmc-messaging", - 2060: "teleniumdaemon", - 2061: "netmount", - 2062: "icg-swp", - 2063: "icg-bridge", - 2064: "icg-iprelay", - 2065: "dlsrpn", - 2066: "aura", - 2067: "dlswpn", - 2068: "avauthsrvprtcl", - 2069: "event-port", - 2070: "ah-esp-encap", - 2071: "acp-port", - 2072: "msync", - 2073: "gxs-data-port", - 2074: "vrtl-vmf-sa", - 2075: "newlixengine", - 2076: "newlixconfig", - 2077: "tsrmagt", - 2078: "tpcsrvr", - 2079: "idware-router", - 2080: "autodesk-nlm", - 2081: "kme-trap-port", - 2082: "infowave", - 2083: "radsec", - 2084: "sunclustergeo", - 2085: "ada-cip", - 2086: "gnunet", - 2087: "eli", - 2088: "ip-blf", - 2089: "sep", - 2090: "lrp", - 2091: "prp", - 2092: "descent3", - 2093: "nbx-cc", - 2094: "nbx-au", - 2095: "nbx-ser", - 2096: "nbx-dir", - 2097: "jetformpreview", - 2098: "dialog-port", - 2099: "h2250-annex-g", - 2100: "amiganetfs", - 2101: "rtcm-sc104", - 2102: "zephyr-srv", - 2103: "zephyr-clt", - 2104: "zephyr-hm", - 2105: "minipay", - 2106: "mzap", - 2107: "bintec-admin", - 2108: "comcam", - 2109: "ergolight", - 2110: "umsp", - 2111: "dsatp", - 2112: "idonix-metanet", - 2113: "hsl-storm", - 2114: "newheights", - 2115: "kdm", - 2116: "ccowcmr", - 2117: "mentaclient", - 2118: "mentaserver", - 2119: "gsigatekeeper", - 2120: "qencp", - 2121: "scientia-ssdb", - 2122: "caupc-remote", - 2123: "gtp-control", - 2124: "elatelink", - 2125: "lockstep", - 2126: "pktcable-cops", - 2127: "index-pc-wb", - 2128: "net-steward", - 2129: "cs-live", - 2130: "xds", - 2131: "avantageb2b", - 2132: "solera-epmap", - 2133: "zymed-zpp", - 2134: "avenue", - 2135: "gris", - 2136: "appworxsrv", - 2137: "connect", - 2138: "unbind-cluster", - 2139: "ias-auth", - 2140: "ias-reg", - 2141: "ias-admind", - 2142: "tdmoip", - 2143: "lv-jc", - 2144: "lv-ffx", - 2145: "lv-pici", - 2146: "lv-not", - 2147: "lv-auth", - 2148: "veritas-ucl", - 2149: "acptsys", - 2150: "dynamic3d", - 2151: "docent", - 2152: "gtp-user", - 2153: "ctlptc", - 2154: "stdptc", - 2155: "brdptc", - 2156: "trp", - 2157: "xnds", - 2158: "touchnetplus", - 2159: "gdbremote", - 2160: "apc-2160", - 2161: "apc-2161", - 2162: "navisphere", - 2163: "navisphere-sec", - 2164: "ddns-v3", - 2165: "x-bone-api", - 2166: "iwserver", - 2167: "raw-serial", - 2168: "easy-soft-mux", - 2169: "brain", - 2170: "eyetv", - 2171: "msfw-storage", - 2172: "msfw-s-storage", - 2173: "msfw-replica", - 2174: "msfw-array", - 2175: "airsync", - 2176: "rapi", - 2177: "qwave", - 2178: "bitspeer", - 2179: "vmrdp", - 2180: "mc-gt-srv", - 2181: "eforward", - 2182: "cgn-stat", - 2183: "cgn-config", - 2184: "nvd", - 2185: "onbase-dds", - 2186: "gtaua", - 2187: "ssmd", - 2190: "tivoconnect", - 2191: "tvbus", - 2192: "asdis", - 2193: "drwcs", - 2197: "mnp-exchange", - 2198: "onehome-remote", - 2199: "onehome-help", - 2200: "ici", - 2201: "ats", - 2202: "imtc-map", - 2203: "b2-runtime", - 2204: "b2-license", - 2205: "jps", - 2206: "hpocbus", - 2207: "hpssd", - 2208: "hpiod", - 2209: "rimf-ps", - 2210: "noaaport", - 2211: "emwin", - 2212: "leecoposserver", - 2213: "kali", - 2214: "rpi", - 2215: "ipcore", - 2216: "vtu-comms", - 2217: "gotodevice", - 2218: "bounzza", - 2219: "netiq-ncap", - 2220: "netiq", - 2221: "rockwell-csp1", - 2222: "EtherNet-IP-1", - 2223: "rockwell-csp2", - 2224: "efi-mg", - 2226: "di-drm", - 2227: "di-msg", - 2228: "ehome-ms", - 2229: "datalens", - 2230: "queueadm", - 2231: "wimaxasncp", - 2232: "ivs-video", - 2233: "infocrypt", - 2234: "directplay", - 2235: "sercomm-wlink", - 2236: "nani", - 2237: "optech-port1-lm", - 2238: "aviva-sna", - 2239: "imagequery", - 2240: "recipe", - 2241: "ivsd", - 2242: "foliocorp", - 2243: "magicom", - 2244: "nmsserver", - 2245: "hao", - 2246: "pc-mta-addrmap", - 2247: "antidotemgrsvr", - 2248: "ums", - 2249: "rfmp", - 2250: "remote-collab", - 2251: "dif-port", - 2252: "njenet-ssl", - 2253: "dtv-chan-req", - 2254: "seispoc", - 2255: "vrtp", - 2256: "pcc-mfp", - 2257: "simple-tx-rx", - 2258: "rcts", - 2260: "apc-2260", - 2261: "comotionmaster", - 2262: "comotionback", - 2263: "ecwcfg", - 2264: "apx500api-1", - 2265: "apx500api-2", - 2266: "mfserver", - 2267: "ontobroker", - 2268: "amt", - 2269: "mikey", - 2270: "starschool", - 2271: "mmcals", - 2272: "mmcal", - 2273: "mysql-im", - 2274: "pcttunnell", - 2275: "ibridge-data", - 2276: "ibridge-mgmt", - 2277: "bluectrlproxy", - 2278: "s3db", - 2279: "xmquery", - 2280: "lnvpoller", - 2281: "lnvconsole", - 2282: "lnvalarm", - 2283: "lnvstatus", - 2284: "lnvmaps", - 2285: "lnvmailmon", - 2286: "nas-metering", - 2287: "dna", - 2288: "netml", - 2289: "dict-lookup", - 2290: "sonus-logging", - 2291: "eapsp", - 2292: "mib-streaming", - 2293: "npdbgmngr", - 2294: "konshus-lm", - 2295: "advant-lm", - 2296: "theta-lm", - 2297: "d2k-datamover1", - 2298: "d2k-datamover2", - 2299: "pc-telecommute", - 2300: "cvmmon", - 2301: "cpq-wbem", - 2302: "binderysupport", - 2303: "proxy-gateway", - 2304: "attachmate-uts", - 2305: "mt-scaleserver", - 2306: "tappi-boxnet", - 2307: "pehelp", - 2308: "sdhelp", - 2309: "sdserver", - 2310: "sdclient", - 2311: "messageservice", - 2312: "wanscaler", - 2313: "iapp", - 2314: "cr-websystems", - 2315: "precise-sft", - 2316: "sent-lm", - 2317: "attachmate-g32", - 2318: "cadencecontrol", - 2319: "infolibria", - 2320: "siebel-ns", - 2321: "rdlap", - 2322: "ofsd", - 2323: "3d-nfsd", - 2324: "cosmocall", - 2325: "ansysli", - 2326: "idcp", - 2327: "xingcsm", - 2328: "netrix-sftm", - 2329: "nvd", - 2330: "tscchat", - 2331: "agentview", - 2332: "rcc-host", - 2333: "snapp", - 2334: "ace-client", - 2335: "ace-proxy", - 2336: "appleugcontrol", - 2337: "ideesrv", - 2338: "norton-lambert", - 2339: "3com-webview", - 2340: "wrs-registry", - 2341: "xiostatus", - 2342: "manage-exec", - 2343: "nati-logos", - 2344: "fcmsys", - 2345: "dbm", - 2346: "redstorm-join", - 2347: "redstorm-find", - 2348: "redstorm-info", - 2349: "redstorm-diag", - 2350: "psbserver", - 2351: "psrserver", - 2352: "pslserver", - 2353: "pspserver", - 2354: "psprserver", - 2355: "psdbserver", - 2356: "gxtelmd", - 2357: "unihub-server", - 2358: "futrix", - 2359: "flukeserver", - 2360: "nexstorindltd", - 2361: "tl1", - 2362: "digiman", - 2363: "mediacntrlnfsd", - 2364: "oi-2000", - 2365: "dbref", - 2366: "qip-login", - 2367: "service-ctrl", - 2368: "opentable", - 2370: "l3-hbmon", - 2372: "lanmessenger", - 2381: "compaq-https", - 2382: "ms-olap3", - 2383: "ms-olap4", - 2384: "sd-capacity", - 2385: "sd-data", - 2386: "virtualtape", - 2387: "vsamredirector", - 2388: "mynahautostart", - 2389: "ovsessionmgr", - 2390: "rsmtp", - 2391: "3com-net-mgmt", - 2392: "tacticalauth", - 2393: "ms-olap1", - 2394: "ms-olap2", - 2395: "lan900-remote", - 2396: "wusage", - 2397: "ncl", - 2398: "orbiter", - 2399: "fmpro-fdal", - 2400: "opequus-server", - 2401: "cvspserver", - 2402: "taskmaster2000", - 2403: "taskmaster2000", - 2404: "iec-104", - 2405: "trc-netpoll", - 2406: "jediserver", - 2407: "orion", - 2409: "sns-protocol", - 2410: "vrts-registry", - 2411: "netwave-ap-mgmt", - 2412: "cdn", - 2413: "orion-rmi-reg", - 2414: "beeyond", - 2415: "codima-rtp", - 2416: "rmtserver", - 2417: "composit-server", - 2418: "cas", - 2419: "attachmate-s2s", - 2420: "dslremote-mgmt", - 2421: "g-talk", - 2422: "crmsbits", - 2423: "rnrp", - 2424: "kofax-svr", - 2425: "fjitsuappmgr", - 2427: "mgcp-gateway", - 2428: "ott", - 2429: "ft-role", - 2430: "venus", - 2431: "venus-se", - 2432: "codasrv", - 2433: "codasrv-se", - 2434: "pxc-epmap", - 2435: "optilogic", - 2436: "topx", - 2437: "unicontrol", - 2438: "msp", - 2439: "sybasedbsynch", - 2440: "spearway", - 2441: "pvsw-inet", - 2442: "netangel", - 2443: "powerclientcsf", - 2444: "btpp2sectrans", - 2445: "dtn1", - 2446: "bues-service", - 2447: "ovwdb", - 2448: "hpppssvr", - 2449: "ratl", - 2450: "netadmin", - 2451: "netchat", - 2452: "snifferclient", - 2453: "madge-ltd", - 2454: "indx-dds", - 2455: "wago-io-system", - 2456: "altav-remmgt", - 2457: "rapido-ip", - 2458: "griffin", - 2459: "community", - 2460: "ms-theater", - 2461: "qadmifoper", - 2462: "qadmifevent", - 2463: "lsi-raid-mgmt", - 2464: "direcpc-si", - 2465: "lbm", - 2466: "lbf", - 2467: "high-criteria", - 2468: "qip-msgd", - 2469: "mti-tcs-comm", - 2470: "taskman-port", - 2471: "seaodbc", - 2472: "c3", - 2473: "aker-cdp", - 2474: "vitalanalysis", - 2475: "ace-server", - 2476: "ace-svr-prop", - 2477: "ssm-cvs", - 2478: "ssm-cssps", - 2479: "ssm-els", - 2480: "powerexchange", - 2481: "giop", - 2482: "giop-ssl", - 2483: "ttc", - 2484: "ttc-ssl", - 2485: "netobjects1", - 2486: "netobjects2", - 2487: "pns", - 2488: "moy-corp", - 2489: "tsilb", - 2490: "qip-qdhcp", - 2491: "conclave-cpp", - 2492: "groove", - 2493: "talarian-mqs", - 2494: "bmc-ar", - 2495: "fast-rem-serv", - 2496: "dirgis", - 2497: "quaddb", - 2498: "odn-castraq", - 2499: "unicontrol", - 2500: "rtsserv", - 2501: "rtsclient", - 2502: "kentrox-prot", - 2503: "nms-dpnss", - 2504: "wlbs", - 2505: "ppcontrol", - 2506: "jbroker", - 2507: "spock", - 2508: "jdatastore", - 2509: "fjmpss", - 2510: "fjappmgrbulk", - 2511: "metastorm", - 2512: "citrixima", - 2513: "citrixadmin", - 2514: "facsys-ntp", - 2515: "facsys-router", - 2516: "maincontrol", - 2517: "call-sig-trans", - 2518: "willy", - 2519: "globmsgsvc", - 2520: "pvsw", - 2521: "adaptecmgr", - 2522: "windb", - 2523: "qke-llc-v3", - 2524: "optiwave-lm", - 2525: "ms-v-worlds", - 2526: "ema-sent-lm", - 2527: "iqserver", - 2528: "ncr-ccl", - 2529: "utsftp", - 2530: "vrcommerce", - 2531: "ito-e-gui", - 2532: "ovtopmd", - 2533: "snifferserver", - 2534: "combox-web-acc", - 2535: "madcap", - 2536: "btpp2audctr1", - 2537: "upgrade", - 2538: "vnwk-prapi", - 2539: "vsiadmin", - 2540: "lonworks", - 2541: "lonworks2", - 2542: "udrawgraph", - 2543: "reftek", - 2544: "novell-zen", - 2545: "sis-emt", - 2546: "vytalvaultbrtp", - 2547: "vytalvaultvsmp", - 2548: "vytalvaultpipe", - 2549: "ipass", - 2550: "ads", - 2551: "isg-uda-server", - 2552: "call-logging", - 2553: "efidiningport", - 2554: "vcnet-link-v10", - 2555: "compaq-wcp", - 2556: "nicetec-nmsvc", - 2557: "nicetec-mgmt", - 2558: "pclemultimedia", - 2559: "lstp", - 2560: "labrat", - 2561: "mosaixcc", - 2562: "delibo", - 2563: "cti-redwood", - 2564: "hp-3000-telnet", - 2565: "coord-svr", - 2566: "pcs-pcw", - 2567: "clp", - 2568: "spamtrap", - 2569: "sonuscallsig", - 2570: "hs-port", - 2571: "cecsvc", - 2572: "ibp", - 2573: "trustestablish", - 2574: "blockade-bpsp", - 2575: "hl7", - 2576: "tclprodebugger", - 2577: "scipticslsrvr", - 2578: "rvs-isdn-dcp", - 2579: "mpfoncl", - 2580: "tributary", - 2581: "argis-te", - 2582: "argis-ds", - 2583: "mon", - 2584: "cyaserv", - 2585: "netx-server", - 2586: "netx-agent", - 2587: "masc", - 2588: "privilege", - 2589: "quartus-tcl", - 2590: "idotdist", - 2591: "maytagshuffle", - 2592: "netrek", - 2593: "mns-mail", - 2594: "dts", - 2595: "worldfusion1", - 2596: "worldfusion2", - 2597: "homesteadglory", - 2598: "citriximaclient", - 2599: "snapd", - 2600: "hpstgmgr", - 2601: "discp-client", - 2602: "discp-server", - 2603: "servicemeter", - 2604: "nsc-ccs", - 2605: "nsc-posa", - 2606: "netmon", - 2607: "connection", - 2608: "wag-service", - 2609: "system-monitor", - 2610: "versa-tek", - 2611: "lionhead", - 2612: "qpasa-agent", - 2613: "smntubootstrap", - 2614: "neveroffline", - 2615: "firepower", - 2616: "appswitch-emp", - 2617: "cmadmin", - 2618: "priority-e-com", - 2619: "bruce", - 2620: "lpsrecommender", - 2621: "miles-apart", - 2622: "metricadbc", - 2623: "lmdp", - 2624: "aria", - 2625: "blwnkl-port", - 2626: "gbjd816", - 2627: "moshebeeri", - 2628: "dict", - 2629: "sitaraserver", - 2630: "sitaramgmt", - 2631: "sitaradir", - 2632: "irdg-post", - 2633: "interintelli", - 2634: "pk-electronics", - 2635: "backburner", - 2636: "solve", - 2637: "imdocsvc", - 2638: "sybaseanywhere", - 2639: "aminet", - 2640: "sai-sentlm", - 2641: "hdl-srv", - 2642: "tragic", - 2643: "gte-samp", - 2644: "travsoft-ipx-t", - 2645: "novell-ipx-cmd", - 2646: "and-lm", - 2647: "syncserver", - 2648: "upsnotifyprot", - 2649: "vpsipport", - 2650: "eristwoguns", - 2651: "ebinsite", - 2652: "interpathpanel", - 2653: "sonus", - 2654: "corel-vncadmin", - 2655: "unglue", - 2656: "kana", - 2657: "sns-dispatcher", - 2658: "sns-admin", - 2659: "sns-query", - 2660: "gcmonitor", - 2661: "olhost", - 2662: "bintec-capi", - 2663: "bintec-tapi", - 2664: "patrol-mq-gm", - 2665: "patrol-mq-nm", - 2666: "extensis", - 2667: "alarm-clock-s", - 2668: "alarm-clock-c", - 2669: "toad", - 2670: "tve-announce", - 2671: "newlixreg", - 2672: "nhserver", - 2673: "firstcall42", - 2674: "ewnn", - 2675: "ttc-etap", - 2676: "simslink", - 2677: "gadgetgate1way", - 2678: "gadgetgate2way", - 2679: "syncserverssl", - 2680: "pxc-sapxom", - 2681: "mpnjsomb", - 2683: "ncdloadbalance", - 2684: "mpnjsosv", - 2685: "mpnjsocl", - 2686: "mpnjsomg", - 2687: "pq-lic-mgmt", - 2688: "md-cg-http", - 2689: "fastlynx", - 2690: "hp-nnm-data", - 2691: "itinternet", - 2692: "admins-lms", - 2694: "pwrsevent", - 2695: "vspread", - 2696: "unifyadmin", - 2697: "oce-snmp-trap", - 2698: "mck-ivpip", - 2699: "csoft-plusclnt", - 2700: "tqdata", - 2701: "sms-rcinfo", - 2702: "sms-xfer", - 2703: "sms-chat", - 2704: "sms-remctrl", - 2705: "sds-admin", - 2706: "ncdmirroring", - 2707: "emcsymapiport", - 2708: "banyan-net", - 2709: "supermon", - 2710: "sso-service", - 2711: "sso-control", - 2712: "aocp", - 2713: "raventbs", - 2714: "raventdm", - 2715: "hpstgmgr2", - 2716: "inova-ip-disco", - 2717: "pn-requester", - 2718: "pn-requester2", - 2719: "scan-change", - 2720: "wkars", - 2721: "smart-diagnose", - 2722: "proactivesrvr", - 2723: "watchdog-nt", - 2724: "qotps", - 2725: "msolap-ptp2", - 2726: "tams", - 2727: "mgcp-callagent", - 2728: "sqdr", - 2729: "tcim-control", - 2730: "nec-raidplus", - 2731: "fyre-messanger", - 2732: "g5m", - 2733: "signet-ctf", - 2734: "ccs-software", - 2735: "netiq-mc", - 2736: "radwiz-nms-srv", - 2737: "srp-feedback", - 2738: "ndl-tcp-ois-gw", - 2739: "tn-timing", - 2740: "alarm", - 2741: "tsb", - 2742: "tsb2", - 2743: "murx", - 2744: "honyaku", - 2745: "urbisnet", - 2746: "cpudpencap", - 2747: "fjippol-swrly", - 2748: "fjippol-polsvr", - 2749: "fjippol-cnsl", - 2750: "fjippol-port1", - 2751: "fjippol-port2", - 2752: "rsisysaccess", - 2753: "de-spot", - 2754: "apollo-cc", - 2755: "expresspay", - 2756: "simplement-tie", - 2757: "cnrp", - 2758: "apollo-status", - 2759: "apollo-gms", - 2760: "sabams", - 2761: "dicom-iscl", - 2762: "dicom-tls", - 2763: "desktop-dna", - 2764: "data-insurance", - 2765: "qip-audup", - 2766: "compaq-scp", - 2767: "uadtc", - 2768: "uacs", - 2769: "exce", - 2770: "veronica", - 2771: "vergencecm", - 2772: "auris", - 2773: "rbakcup1", - 2774: "rbakcup2", - 2775: "smpp", - 2776: "ridgeway1", - 2777: "ridgeway2", - 2778: "gwen-sonya", - 2779: "lbc-sync", - 2780: "lbc-control", - 2781: "whosells", - 2782: "everydayrc", - 2783: "aises", - 2784: "www-dev", - 2785: "aic-np", - 2786: "aic-oncrpc", - 2787: "piccolo", - 2788: "fryeserv", - 2789: "media-agent", - 2790: "plgproxy", - 2791: "mtport-regist", - 2792: "f5-globalsite", - 2793: "initlsmsad", - 2795: "livestats", - 2796: "ac-tech", - 2797: "esp-encap", - 2798: "tmesis-upshot", - 2799: "icon-discover", - 2800: "acc-raid", - 2801: "igcp", - 2802: "veritas-udp1", - 2803: "btprjctrl", - 2804: "dvr-esm", - 2805: "wta-wsp-s", - 2806: "cspuni", - 2807: "cspmulti", - 2808: "j-lan-p", - 2809: "corbaloc", - 2810: "netsteward", - 2811: "gsiftp", - 2812: "atmtcp", - 2813: "llm-pass", - 2814: "llm-csv", - 2815: "lbc-measure", - 2816: "lbc-watchdog", - 2817: "nmsigport", - 2818: "rmlnk", - 2819: "fc-faultnotify", - 2820: "univision", - 2821: "vrts-at-port", - 2822: "ka0wuc", - 2823: "cqg-netlan", - 2824: "cqg-netlan-1", - 2826: "slc-systemlog", - 2827: "slc-ctrlrloops", - 2828: "itm-lm", - 2829: "silkp1", - 2830: "silkp2", - 2831: "silkp3", - 2832: "silkp4", - 2833: "glishd", - 2834: "evtp", - 2835: "evtp-data", - 2836: "catalyst", - 2837: "repliweb", - 2838: "starbot", - 2839: "nmsigport", - 2840: "l3-exprt", - 2841: "l3-ranger", - 2842: "l3-hawk", - 2843: "pdnet", - 2844: "bpcp-poll", - 2845: "bpcp-trap", - 2846: "aimpp-hello", - 2847: "aimpp-port-req", - 2848: "amt-blc-port", - 2849: "fxp", - 2850: "metaconsole", - 2851: "webemshttp", - 2852: "bears-01", - 2853: "ispipes", - 2854: "infomover", - 2856: "cesdinv", - 2857: "simctlp", - 2858: "ecnp", - 2859: "activememory", - 2860: "dialpad-voice1", - 2861: "dialpad-voice2", - 2862: "ttg-protocol", - 2863: "sonardata", - 2864: "astromed-main", - 2865: "pit-vpn", - 2866: "iwlistener", - 2867: "esps-portal", - 2868: "npep-messaging", - 2869: "icslap", - 2870: "daishi", - 2871: "msi-selectplay", - 2872: "radix", - 2874: "dxmessagebase1", - 2875: "dxmessagebase2", - 2876: "sps-tunnel", - 2877: "bluelance", - 2878: "aap", - 2879: "ucentric-ds", - 2880: "synapse", - 2881: "ndsp", - 2882: "ndtp", - 2883: "ndnp", - 2884: "flashmsg", - 2885: "topflow", - 2886: "responselogic", - 2887: "aironetddp", - 2888: "spcsdlobby", - 2889: "rsom", - 2890: "cspclmulti", - 2891: "cinegrfx-elmd", - 2892: "snifferdata", - 2893: "vseconnector", - 2894: "abacus-remote", - 2895: "natuslink", - 2896: "ecovisiong6-1", - 2897: "citrix-rtmp", - 2898: "appliance-cfg", - 2899: "powergemplus", - 2900: "quicksuite", - 2901: "allstorcns", - 2902: "netaspi", - 2903: "suitcase", - 2904: "m2ua", - 2906: "caller9", - 2907: "webmethods-b2b", - 2908: "mao", - 2909: "funk-dialout", - 2910: "tdaccess", - 2911: "blockade", - 2912: "epicon", - 2913: "boosterware", - 2914: "gamelobby", - 2915: "tksocket", - 2916: "elvin-server", - 2917: "elvin-client", - 2918: "kastenchasepad", - 2919: "roboer", - 2920: "roboeda", - 2921: "cesdcdman", - 2922: "cesdcdtrn", - 2923: "wta-wsp-wtp-s", - 2924: "precise-vip", - 2926: "mobile-file-dl", - 2927: "unimobilectrl", - 2928: "redstone-cpss", - 2929: "amx-webadmin", - 2930: "amx-weblinx", - 2931: "circle-x", - 2932: "incp", - 2933: "4-tieropmgw", - 2934: "4-tieropmcli", - 2935: "qtp", - 2936: "otpatch", - 2937: "pnaconsult-lm", - 2938: "sm-pas-1", - 2939: "sm-pas-2", - 2940: "sm-pas-3", - 2941: "sm-pas-4", - 2942: "sm-pas-5", - 2943: "ttnrepository", - 2944: "megaco-h248", - 2945: "h248-binary", - 2946: "fjsvmpor", - 2947: "gpsd", - 2948: "wap-push", - 2949: "wap-pushsecure", - 2950: "esip", - 2951: "ottp", - 2952: "mpfwsas", - 2953: "ovalarmsrv", - 2954: "ovalarmsrv-cmd", - 2955: "csnotify", - 2956: "ovrimosdbman", - 2957: "jmact5", - 2958: "jmact6", - 2959: "rmopagt", - 2960: "dfoxserver", - 2961: "boldsoft-lm", - 2962: "iph-policy-cli", - 2963: "iph-policy-adm", - 2964: "bullant-srap", - 2965: "bullant-rap", - 2966: "idp-infotrieve", - 2967: "ssc-agent", - 2968: "enpp", - 2969: "essp", - 2970: "index-net", - 2971: "netclip", - 2972: "pmsm-webrctl", - 2973: "svnetworks", - 2974: "signal", - 2975: "fjmpcm", - 2976: "cns-srv-port", - 2977: "ttc-etap-ns", - 2978: "ttc-etap-ds", - 2979: "h263-video", - 2980: "wimd", - 2981: "mylxamport", - 2982: "iwb-whiteboard", - 2983: "netplan", - 2984: "hpidsadmin", - 2985: "hpidsagent", - 2986: "stonefalls", - 2987: "identify", - 2988: "hippad", - 2989: "zarkov", - 2990: "boscap", - 2991: "wkstn-mon", - 2992: "avenyo", - 2993: "veritas-vis1", - 2994: "veritas-vis2", - 2995: "idrs", - 2996: "vsixml", - 2997: "rebol", - 2998: "realsecure", - 2999: "remoteware-un", - 3000: "hbci", - 3002: "exlm-agent", - 3003: "cgms", - 3004: "csoftragent", - 3005: "geniuslm", - 3006: "ii-admin", - 3007: "lotusmtap", - 3008: "midnight-tech", - 3009: "pxc-ntfy", - 3010: "ping-pong", - 3011: "trusted-web", - 3012: "twsdss", - 3013: "gilatskysurfer", - 3014: "broker-service", - 3015: "nati-dstp", - 3016: "notify-srvr", - 3017: "event-listener", - 3018: "srvc-registry", - 3019: "resource-mgr", - 3020: "cifs", - 3021: "agriserver", - 3022: "csregagent", - 3023: "magicnotes", - 3024: "nds-sso", - 3025: "arepa-raft", - 3026: "agri-gateway", - 3027: "LiebDevMgmt-C", - 3028: "LiebDevMgmt-DM", - 3029: "LiebDevMgmt-A", - 3030: "arepa-cas", - 3031: "eppc", - 3032: "redwood-chat", - 3033: "pdb", - 3034: "osmosis-aeea", - 3035: "fjsv-gssagt", - 3036: "hagel-dump", - 3037: "hp-san-mgmt", - 3038: "santak-ups", - 3039: "cogitate", - 3040: "tomato-springs", - 3041: "di-traceware", - 3042: "journee", - 3043: "brp", - 3044: "epp", - 3045: "responsenet", - 3046: "di-ase", - 3047: "hlserver", - 3048: "pctrader", - 3049: "nsws", - 3050: "gds-db", - 3051: "galaxy-server", - 3052: "apc-3052", - 3053: "dsom-server", - 3054: "amt-cnf-prot", - 3055: "policyserver", - 3056: "cdl-server", - 3057: "goahead-fldup", - 3058: "videobeans", - 3059: "qsoft", - 3060: "interserver", - 3061: "cautcpd", - 3062: "ncacn-ip-tcp", - 3063: "ncadg-ip-udp", - 3064: "rprt", - 3065: "slinterbase", - 3066: "netattachsdmp", - 3067: "fjhpjp", - 3068: "ls3bcast", - 3069: "ls3", - 3070: "mgxswitch", - 3071: "csd-mgmt-port", - 3072: "csd-monitor", - 3073: "vcrp", - 3074: "xbox", - 3075: "orbix-locator", - 3076: "orbix-config", - 3077: "orbix-loc-ssl", - 3078: "orbix-cfg-ssl", - 3079: "lv-frontpanel", - 3080: "stm-pproc", - 3081: "tl1-lv", - 3082: "tl1-raw", - 3083: "tl1-telnet", - 3084: "itm-mccs", - 3085: "pcihreq", - 3086: "jdl-dbkitchen", - 3087: "asoki-sma", - 3088: "xdtp", - 3089: "ptk-alink", - 3090: "stss", - 3091: "1ci-smcs", - 3093: "rapidmq-center", - 3094: "rapidmq-reg", - 3095: "panasas", - 3096: "ndl-aps", - 3098: "umm-port", - 3099: "chmd", - 3100: "opcon-xps", - 3101: "hp-pxpib", - 3102: "slslavemon", - 3103: "autocuesmi", - 3104: "autocuetime", - 3105: "cardbox", - 3106: "cardbox-http", - 3107: "business", - 3108: "geolocate", - 3109: "personnel", - 3110: "sim-control", - 3111: "wsynch", - 3112: "ksysguard", - 3113: "cs-auth-svr", - 3114: "ccmad", - 3115: "mctet-master", - 3116: "mctet-gateway", - 3117: "mctet-jserv", - 3118: "pkagent", - 3119: "d2000kernel", - 3120: "d2000webserver", - 3122: "vtr-emulator", - 3123: "edix", - 3124: "beacon-port", - 3125: "a13-an", - 3127: "ctx-bridge", - 3128: "ndl-aas", - 3129: "netport-id", - 3130: "icpv2", - 3131: "netbookmark", - 3132: "ms-rule-engine", - 3133: "prism-deploy", - 3134: "ecp", - 3135: "peerbook-port", - 3136: "grubd", - 3137: "rtnt-1", - 3138: "rtnt-2", - 3139: "incognitorv", - 3140: "ariliamulti", - 3141: "vmodem", - 3142: "rdc-wh-eos", - 3143: "seaview", - 3144: "tarantella", - 3145: "csi-lfap", - 3146: "bears-02", - 3147: "rfio", - 3148: "nm-game-admin", - 3149: "nm-game-server", - 3150: "nm-asses-admin", - 3151: "nm-assessor", - 3152: "feitianrockey", - 3153: "s8-client-port", - 3154: "ccmrmi", - 3155: "jpegmpeg", - 3156: "indura", - 3157: "e3consultants", - 3158: "stvp", - 3159: "navegaweb-port", - 3160: "tip-app-server", - 3161: "doc1lm", - 3162: "sflm", - 3163: "res-sap", - 3164: "imprs", - 3165: "newgenpay", - 3166: "sossecollector", - 3167: "nowcontact", - 3168: "poweronnud", - 3169: "serverview-as", - 3170: "serverview-asn", - 3171: "serverview-gf", - 3172: "serverview-rm", - 3173: "serverview-icc", - 3174: "armi-server", - 3175: "t1-e1-over-ip", - 3176: "ars-master", - 3177: "phonex-port", - 3178: "radclientport", - 3179: "h2gf-w-2m", - 3180: "mc-brk-srv", - 3181: "bmcpatrolagent", - 3182: "bmcpatrolrnvu", - 3183: "cops-tls", - 3184: "apogeex-port", - 3185: "smpppd", - 3186: "iiw-port", - 3187: "odi-port", - 3188: "brcm-comm-port", - 3189: "pcle-infex", - 3190: "csvr-proxy", - 3191: "csvr-sslproxy", - 3192: "firemonrcc", - 3193: "spandataport", - 3194: "magbind", - 3195: "ncu-1", - 3196: "ncu-2", - 3197: "embrace-dp-s", - 3198: "embrace-dp-c", - 3199: "dmod-workspace", - 3200: "tick-port", - 3201: "cpq-tasksmart", - 3202: "intraintra", - 3203: "netwatcher-mon", - 3204: "netwatcher-db", - 3205: "isns", - 3206: "ironmail", - 3207: "vx-auth-port", - 3208: "pfu-prcallback", - 3209: "netwkpathengine", - 3210: "flamenco-proxy", - 3211: "avsecuremgmt", - 3212: "surveyinst", - 3213: "neon24x7", - 3214: "jmq-daemon-1", - 3215: "jmq-daemon-2", - 3216: "ferrari-foam", - 3217: "unite", - 3218: "smartpackets", - 3219: "wms-messenger", - 3220: "xnm-ssl", - 3221: "xnm-clear-text", - 3222: "glbp", - 3223: "digivote", - 3224: "aes-discovery", - 3225: "fcip-port", - 3226: "isi-irp", - 3227: "dwnmshttp", - 3228: "dwmsgserver", - 3229: "global-cd-port", - 3230: "sftdst-port", - 3231: "vidigo", - 3232: "mdtp", - 3233: "whisker", - 3234: "alchemy", - 3235: "mdap-port", - 3236: "apparenet-ts", - 3237: "apparenet-tps", - 3238: "apparenet-as", - 3239: "apparenet-ui", - 3240: "triomotion", - 3241: "sysorb", - 3242: "sdp-id-port", - 3243: "timelot", - 3244: "onesaf", - 3245: "vieo-fe", - 3246: "dvt-system", - 3247: "dvt-data", - 3248: "procos-lm", - 3249: "ssp", - 3250: "hicp", - 3251: "sysscanner", - 3252: "dhe", - 3253: "pda-data", - 3254: "pda-sys", - 3255: "semaphore", - 3256: "cpqrpm-agent", - 3257: "cpqrpm-server", - 3258: "ivecon-port", - 3259: "epncdp2", - 3260: "iscsi-target", - 3261: "winshadow", - 3262: "necp", - 3263: "ecolor-imager", - 3264: "ccmail", - 3265: "altav-tunnel", - 3266: "ns-cfg-server", - 3267: "ibm-dial-out", - 3268: "msft-gc", - 3269: "msft-gc-ssl", - 3270: "verismart", - 3271: "csoft-prev", - 3272: "user-manager", - 3273: "sxmp", - 3274: "ordinox-server", - 3275: "samd", - 3276: "maxim-asics", - 3277: "awg-proxy", - 3278: "lkcmserver", - 3279: "admind", - 3280: "vs-server", - 3281: "sysopt", - 3282: "datusorb", - 3283: "Apple Remote Desktop (Net Assistant)", - 3284: "4talk", - 3285: "plato", - 3286: "e-net", - 3287: "directvdata", - 3288: "cops", - 3289: "enpc", - 3290: "caps-lm", - 3291: "sah-lm", - 3292: "cart-o-rama", - 3293: "fg-fps", - 3294: "fg-gip", - 3295: "dyniplookup", - 3296: "rib-slm", - 3297: "cytel-lm", - 3298: "deskview", - 3299: "pdrncs", - 3302: "mcs-fastmail", - 3303: "opsession-clnt", - 3304: "opsession-srvr", - 3305: "odette-ftp", - 3306: "mysql", - 3307: "opsession-prxy", - 3308: "tns-server", - 3309: "tns-adv", - 3310: "dyna-access", - 3311: "mcns-tel-ret", - 3312: "appman-server", - 3313: "uorb", - 3314: "uohost", - 3315: "cdid", - 3316: "aicc-cmi", - 3317: "vsaiport", - 3318: "ssrip", - 3319: "sdt-lmd", - 3320: "officelink2000", - 3321: "vnsstr", - 3326: "sftu", - 3327: "bbars", - 3328: "egptlm", - 3329: "hp-device-disc", - 3330: "mcs-calypsoicf", - 3331: "mcs-messaging", - 3332: "mcs-mailsvr", - 3333: "dec-notes", - 3334: "directv-web", - 3335: "directv-soft", - 3336: "directv-tick", - 3337: "directv-catlg", - 3338: "anet-b", - 3339: "anet-l", - 3340: "anet-m", - 3341: "anet-h", - 3342: "webtie", - 3343: "ms-cluster-net", - 3344: "bnt-manager", - 3345: "influence", - 3346: "trnsprntproxy", - 3347: "phoenix-rpc", - 3348: "pangolin-laser", - 3349: "chevinservices", - 3350: "findviatv", - 3351: "btrieve", - 3352: "ssql", - 3353: "fatpipe", - 3354: "suitjd", - 3355: "ordinox-dbase", - 3356: "upnotifyps", - 3357: "adtech-test", - 3358: "mpsysrmsvr", - 3359: "wg-netforce", - 3360: "kv-server", - 3361: "kv-agent", - 3362: "dj-ilm", - 3363: "nati-vi-server", - 3364: "creativeserver", - 3365: "contentserver", - 3366: "creativepartnr", - 3372: "tip2", - 3373: "lavenir-lm", - 3374: "cluster-disc", - 3375: "vsnm-agent", - 3376: "cdbroker", - 3377: "cogsys-lm", - 3378: "wsicopy", - 3379: "socorfs", - 3380: "sns-channels", - 3381: "geneous", - 3382: "fujitsu-neat", - 3383: "esp-lm", - 3384: "hp-clic", - 3385: "qnxnetman", - 3386: "gprs-sig", - 3387: "backroomnet", - 3388: "cbserver", - 3389: "ms-wbt-server", - 3390: "dsc", - 3391: "savant", - 3392: "efi-lm", - 3393: "d2k-tapestry1", - 3394: "d2k-tapestry2", - 3395: "dyna-lm", - 3396: "printer-agent", - 3397: "cloanto-lm", - 3398: "mercantile", - 3399: "csms", - 3400: "csms2", - 3401: "filecast", - 3402: "fxaengine-net", - 3405: "nokia-ann-ch1", - 3406: "nokia-ann-ch2", - 3407: "ldap-admin", - 3408: "BESApi", - 3409: "networklens", - 3410: "networklenss", - 3411: "biolink-auth", - 3412: "xmlblaster", - 3413: "svnet", - 3414: "wip-port", - 3415: "bcinameservice", - 3416: "commandport", - 3417: "csvr", - 3418: "rnmap", - 3419: "softaudit", - 3420: "ifcp-port", - 3421: "bmap", - 3422: "rusb-sys-port", - 3423: "xtrm", - 3424: "xtrms", - 3425: "agps-port", - 3426: "arkivio", - 3427: "websphere-snmp", - 3428: "twcss", - 3429: "gcsp", - 3430: "ssdispatch", - 3431: "ndl-als", - 3432: "osdcp", - 3433: "opnet-smp", - 3434: "opencm", - 3435: "pacom", - 3436: "gc-config", - 3437: "autocueds", - 3438: "spiral-admin", - 3439: "hri-port", - 3440: "ans-console", - 3441: "connect-client", - 3442: "connect-server", - 3443: "ov-nnm-websrv", - 3444: "denali-server", - 3445: "monp", - 3446: "3comfaxrpc", - 3447: "directnet", - 3448: "dnc-port", - 3449: "hotu-chat", - 3450: "castorproxy", - 3451: "asam", - 3452: "sabp-signal", - 3453: "pscupd", - 3454: "mira", - 3455: "prsvp", - 3456: "vat", - 3457: "vat-control", - 3458: "d3winosfi", - 3459: "integral", - 3460: "edm-manager", - 3461: "edm-stager", - 3462: "edm-std-notify", - 3463: "edm-adm-notify", - 3464: "edm-mgr-sync", - 3465: "edm-mgr-cntrl", - 3466: "workflow", - 3467: "rcst", - 3468: "ttcmremotectrl", - 3469: "pluribus", - 3470: "jt400", - 3471: "jt400-ssl", - 3472: "jaugsremotec-1", - 3473: "jaugsremotec-2", - 3474: "ttntspauto", - 3475: "genisar-port", - 3476: "nppmp", - 3477: "ecomm", - 3478: "stun", - 3479: "twrpc", - 3480: "plethora", - 3481: "cleanerliverc", - 3482: "vulture", - 3483: "slim-devices", - 3484: "gbs-stp", - 3485: "celatalk", - 3486: "ifsf-hb-port", - 3487: "ltcudp", - 3488: "fs-rh-srv", - 3489: "dtp-dia", - 3490: "colubris", - 3491: "swr-port", - 3492: "tvdumtray-port", - 3493: "nut", - 3494: "ibm3494", - 3495: "seclayer-tcp", - 3496: "seclayer-tls", - 3497: "ipether232port", - 3498: "dashpas-port", - 3499: "sccip-media", - 3500: "rtmp-port", - 3501: "isoft-p2p", - 3502: "avinstalldisc", - 3503: "lsp-ping", - 3504: "ironstorm", - 3505: "ccmcomm", - 3506: "apc-3506", - 3507: "nesh-broker", - 3508: "interactionweb", - 3509: "vt-ssl", - 3510: "xss-port", - 3511: "webmail-2", - 3512: "aztec", - 3513: "arcpd", - 3514: "must-p2p", - 3515: "must-backplane", - 3516: "smartcard-port", - 3517: "802-11-iapp", - 3518: "artifact-msg", - 3519: "galileo", - 3520: "galileolog", - 3521: "mc3ss", - 3522: "nssocketport", - 3523: "odeumservlink", - 3524: "ecmport", - 3525: "eisport", - 3526: "starquiz-port", - 3527: "beserver-msg-q", - 3528: "jboss-iiop", - 3529: "jboss-iiop-ssl", - 3530: "gf", - 3531: "joltid", - 3532: "raven-rmp", - 3533: "raven-rdp", - 3534: "urld-port", - 3535: "ms-la", - 3536: "snac", - 3537: "ni-visa-remote", - 3538: "ibm-diradm", - 3539: "ibm-diradm-ssl", - 3540: "pnrp-port", - 3541: "voispeed-port", - 3542: "hacl-monitor", - 3543: "qftest-lookup", - 3544: "teredo", - 3545: "camac", - 3547: "symantec-sim", - 3548: "interworld", - 3549: "tellumat-nms", - 3550: "ssmpp", - 3551: "apcupsd", - 3552: "taserver", - 3553: "rbr-discovery", - 3554: "questnotify", - 3555: "razor", - 3556: "sky-transport", - 3557: "personalos-001", - 3558: "mcp-port", - 3559: "cctv-port", - 3560: "iniserve-port", - 3561: "bmc-onekey", - 3562: "sdbproxy", - 3563: "watcomdebug", - 3564: "esimport", - 3567: "enc-eps", - 3568: "enc-tunnel-sec", - 3569: "mbg-ctrl", - 3570: "mccwebsvr-port", - 3571: "megardsvr-port", - 3572: "megaregsvrport", - 3573: "tag-ups-1", - 3574: "dmaf-caster", - 3575: "ccm-port", - 3576: "cmc-port", - 3577: "config-port", - 3578: "data-port", - 3579: "ttat3lb", - 3580: "nati-svrloc", - 3581: "kfxaclicensing", - 3582: "press", - 3583: "canex-watch", - 3584: "u-dbap", - 3585: "emprise-lls", - 3586: "emprise-lsc", - 3587: "p2pgroup", - 3588: "sentinel", - 3589: "isomair", - 3590: "wv-csp-sms", - 3591: "gtrack-server", - 3592: "gtrack-ne", - 3593: "bpmd", - 3594: "mediaspace", - 3595: "shareapp", - 3596: "iw-mmogame", - 3597: "a14", - 3598: "a15", - 3599: "quasar-server", - 3600: "trap-daemon", - 3601: "visinet-gui", - 3602: "infiniswitchcl", - 3603: "int-rcv-cntrl", - 3604: "bmc-jmx-port", - 3605: "comcam-io", - 3606: "splitlock", - 3607: "precise-i3", - 3608: "trendchip-dcp", - 3609: "cpdi-pidas-cm", - 3610: "echonet", - 3611: "six-degrees", - 3612: "hp-dataprotect", - 3613: "alaris-disc", - 3614: "sigma-port", - 3615: "start-network", - 3616: "cd3o-protocol", - 3617: "sharp-server", - 3618: "aairnet-1", - 3619: "aairnet-2", - 3620: "ep-pcp", - 3621: "ep-nsp", - 3622: "ff-lr-port", - 3623: "haipe-discover", - 3624: "dist-upgrade", - 3625: "volley", - 3626: "bvcdaemon-port", - 3627: "jamserverport", - 3628: "ept-machine", - 3629: "escvpnet", - 3630: "cs-remote-db", - 3631: "cs-services", - 3632: "distcc", - 3633: "wacp", - 3634: "hlibmgr", - 3635: "sdo", - 3636: "servistaitsm", - 3637: "scservp", - 3638: "ehp-backup", - 3639: "xap-ha", - 3640: "netplay-port1", - 3641: "netplay-port2", - 3642: "juxml-port", - 3643: "audiojuggler", - 3644: "ssowatch", - 3645: "cyc", - 3646: "xss-srv-port", - 3647: "splitlock-gw", - 3648: "fjcp", - 3649: "nmmp", - 3650: "prismiq-plugin", - 3651: "xrpc-registry", - 3652: "vxcrnbuport", - 3653: "tsp", - 3654: "vaprtm", - 3655: "abatemgr", - 3656: "abatjss", - 3657: "immedianet-bcn", - 3658: "ps-ams", - 3659: "apple-sasl", - 3660: "can-nds-ssl", - 3661: "can-ferret-ssl", - 3662: "pserver", - 3663: "dtp", - 3664: "ups-engine", - 3665: "ent-engine", - 3666: "eserver-pap", - 3667: "infoexch", - 3668: "dell-rm-port", - 3669: "casanswmgmt", - 3670: "smile", - 3671: "efcp", - 3672: "lispworks-orb", - 3673: "mediavault-gui", - 3674: "wininstall-ipc", - 3675: "calltrax", - 3676: "va-pacbase", - 3677: "roverlog", - 3678: "ipr-dglt", - 3679: "Escale (Newton Dock)", - 3680: "npds-tracker", - 3681: "bts-x73", - 3682: "cas-mapi", - 3683: "bmc-ea", - 3684: "faxstfx-port", - 3685: "dsx-agent", - 3686: "tnmpv2", - 3687: "simple-push", - 3688: "simple-push-s", - 3689: "daap", - 3690: "svn", - 3691: "magaya-network", - 3692: "intelsync", - 3695: "bmc-data-coll", - 3696: "telnetcpcd", - 3697: "nw-license", - 3698: "sagectlpanel", - 3699: "kpn-icw", - 3700: "lrs-paging", - 3701: "netcelera", - 3702: "ws-discovery", - 3703: "adobeserver-3", - 3704: "adobeserver-4", - 3705: "adobeserver-5", - 3706: "rt-event", - 3707: "rt-event-s", - 3708: "sun-as-iiops", - 3709: "ca-idms", - 3710: "portgate-auth", - 3711: "edb-server2", - 3712: "sentinel-ent", - 3713: "tftps", - 3714: "delos-dms", - 3715: "anoto-rendezv", - 3716: "wv-csp-sms-cir", - 3717: "wv-csp-udp-cir", - 3718: "opus-services", - 3719: "itelserverport", - 3720: "ufastro-instr", - 3721: "xsync", - 3722: "xserveraid", - 3723: "sychrond", - 3724: "blizwow", - 3725: "na-er-tip", - 3726: "array-manager", - 3727: "e-mdu", - 3728: "e-woa", - 3729: "fksp-audit", - 3730: "client-ctrl", - 3731: "smap", - 3732: "m-wnn", - 3733: "multip-msg", - 3734: "synel-data", - 3735: "pwdis", - 3736: "rs-rmi", - 3738: "versatalk", - 3739: "launchbird-lm", - 3740: "heartbeat", - 3741: "wysdma", - 3742: "cst-port", - 3743: "ipcs-command", - 3744: "sasg", - 3745: "gw-call-port", - 3746: "linktest", - 3747: "linktest-s", - 3748: "webdata", - 3749: "cimtrak", - 3750: "cbos-ip-port", - 3751: "gprs-cube", - 3752: "vipremoteagent", - 3753: "nattyserver", - 3754: "timestenbroker", - 3755: "sas-remote-hlp", - 3756: "canon-capt", - 3757: "grf-port", - 3758: "apw-registry", - 3759: "exapt-lmgr", - 3760: "adtempusclient", - 3761: "gsakmp", - 3762: "gbs-smp", - 3763: "xo-wave", - 3764: "mni-prot-rout", - 3765: "rtraceroute", - 3767: "listmgr-port", - 3768: "rblcheckd", - 3769: "haipe-otnk", - 3770: "cindycollab", - 3771: "paging-port", - 3772: "ctp", - 3773: "ctdhercules", - 3774: "zicom", - 3775: "ispmmgr", - 3776: "dvcprov-port", - 3777: "jibe-eb", - 3778: "c-h-it-port", - 3779: "cognima", - 3780: "nnp", - 3781: "abcvoice-port", - 3782: "iso-tp0s", - 3783: "bim-pem", - 3784: "bfd-control", - 3785: "bfd-echo", - 3786: "upstriggervsw", - 3787: "fintrx", - 3788: "isrp-port", - 3789: "remotedeploy", - 3790: "quickbooksrds", - 3791: "tvnetworkvideo", - 3792: "sitewatch", - 3793: "dcsoftware", - 3794: "jaus", - 3795: "myblast", - 3796: "spw-dialer", - 3797: "idps", - 3798: "minilock", - 3799: "radius-dynauth", - 3800: "pwgpsi", - 3801: "ibm-mgr", - 3802: "vhd", - 3803: "soniqsync", - 3804: "iqnet-port", - 3805: "tcpdataserver", - 3806: "wsmlb", - 3807: "spugna", - 3808: "sun-as-iiops-ca", - 3809: "apocd", - 3810: "wlanauth", - 3811: "amp", - 3812: "neto-wol-server", - 3813: "rap-ip", - 3814: "neto-dcs", - 3815: "lansurveyorxml", - 3816: "sunlps-http", - 3817: "tapeware", - 3818: "crinis-hb", - 3819: "epl-slp", - 3820: "scp", - 3821: "pmcp", - 3822: "acp-discovery", - 3823: "acp-conduit", - 3824: "acp-policy", - 3825: "ffserver", - 3826: "warmux", - 3827: "netmpi", - 3828: "neteh", - 3829: "neteh-ext", - 3830: "cernsysmgmtagt", - 3831: "dvapps", - 3832: "xxnetserver", - 3833: "aipn-auth", - 3834: "spectardata", - 3835: "spectardb", - 3836: "markem-dcp", - 3837: "mkm-discovery", - 3838: "sos", - 3839: "amx-rms", - 3840: "flirtmitmir", - 3842: "nhci", - 3843: "quest-agent", - 3844: "rnm", - 3845: "v-one-spp", - 3846: "an-pcp", - 3847: "msfw-control", - 3848: "item", - 3849: "spw-dnspreload", - 3850: "qtms-bootstrap", - 3851: "spectraport", - 3852: "sse-app-config", - 3853: "sscan", - 3854: "stryker-com", - 3855: "opentrac", - 3856: "informer", - 3857: "trap-port", - 3858: "trap-port-mom", - 3859: "nav-port", - 3860: "sasp", - 3861: "winshadow-hd", - 3862: "giga-pocket", - 3863: "asap-udp", - 3865: "xpl", - 3866: "dzdaemon", - 3867: "dzoglserver", - 3869: "ovsam-mgmt", - 3870: "ovsam-d-agent", - 3871: "avocent-adsap", - 3872: "oem-agent", - 3873: "fagordnc", - 3874: "sixxsconfig", - 3875: "pnbscada", - 3876: "dl-agent", - 3877: "xmpcr-interface", - 3878: "fotogcad", - 3879: "appss-lm", - 3880: "igrs", - 3881: "idac", - 3882: "msdts1", - 3883: "vrpn", - 3884: "softrack-meter", - 3885: "topflow-ssl", - 3886: "nei-management", - 3887: "ciphire-data", - 3888: "ciphire-serv", - 3889: "dandv-tester", - 3890: "ndsconnect", - 3891: "rtc-pm-port", - 3892: "pcc-image-port", - 3893: "cgi-starapi", - 3894: "syam-agent", - 3895: "syam-smc", - 3896: "sdo-tls", - 3897: "sdo-ssh", - 3898: "senip", - 3899: "itv-control", - 3900: "udt-os", - 3901: "nimsh", - 3902: "nimaux", - 3903: "charsetmgr", - 3904: "omnilink-port", - 3905: "mupdate", - 3906: "topovista-data", - 3907: "imoguia-port", - 3908: "hppronetman", - 3909: "surfcontrolcpa", - 3910: "prnrequest", - 3911: "prnstatus", - 3912: "gbmt-stars", - 3913: "listcrt-port", - 3914: "listcrt-port-2", - 3915: "agcat", - 3916: "wysdmc", - 3917: "aftmux", - 3918: "pktcablemmcops", - 3919: "hyperip", - 3920: "exasoftport1", - 3921: "herodotus-net", - 3922: "sor-update", - 3923: "symb-sb-port", - 3924: "mpl-gprs-port", - 3925: "zmp", - 3926: "winport", - 3927: "natdataservice", - 3928: "netboot-pxe", - 3929: "smauth-port", - 3930: "syam-webserver", - 3931: "msr-plugin-port", - 3932: "dyn-site", - 3933: "plbserve-port", - 3934: "sunfm-port", - 3935: "sdp-portmapper", - 3936: "mailprox", - 3937: "dvbservdsc", - 3938: "dbcontrol-agent", - 3939: "aamp", - 3940: "xecp-node", - 3941: "homeportal-web", - 3942: "srdp", - 3943: "tig", - 3944: "sops", - 3945: "emcads", - 3946: "backupedge", - 3947: "ccp", - 3948: "apdap", - 3949: "drip", - 3950: "namemunge", - 3951: "pwgippfax", - 3952: "i3-sessionmgr", - 3953: "xmlink-connect", - 3954: "adrep", - 3955: "p2pcommunity", - 3956: "gvcp", - 3957: "mqe-broker", - 3958: "mqe-agent", - 3959: "treehopper", - 3960: "bess", - 3961: "proaxess", - 3962: "sbi-agent", - 3963: "thrp", - 3964: "sasggprs", - 3965: "ati-ip-to-ncpe", - 3966: "bflckmgr", - 3967: "ppsms", - 3968: "ianywhere-dbns", - 3969: "landmarks", - 3970: "lanrevagent", - 3971: "lanrevserver", - 3972: "iconp", - 3973: "progistics", - 3974: "citysearch", - 3975: "airshot", - 3976: "opswagent", - 3977: "opswmanager", - 3978: "secure-cfg-svr", - 3979: "smwan", - 3980: "acms", - 3981: "starfish", - 3982: "eis", - 3983: "eisp", - 3984: "mapper-nodemgr", - 3985: "mapper-mapethd", - 3986: "mapper-ws-ethd", - 3987: "centerline", - 3988: "dcs-config", - 3989: "bv-queryengine", - 3990: "bv-is", - 3991: "bv-smcsrv", - 3992: "bv-ds", - 3993: "bv-agent", - 3995: "iss-mgmt-ssl", - 3996: "abcsoftware", - 3997: "agentsease-db", - 3998: "dnx", - 3999: "nvcnet", - 4000: "terabase", - 4001: "newoak", - 4002: "pxc-spvr-ft", - 4003: "pxc-splr-ft", - 4004: "pxc-roid", - 4005: "pxc-pin", - 4006: "pxc-spvr", - 4007: "pxc-splr", - 4008: "netcheque", - 4009: "chimera-hwm", - 4010: "samsung-unidex", - 4011: "altserviceboot", - 4012: "pda-gate", - 4013: "acl-manager", - 4014: "taiclock", - 4015: "talarian-mcast1", - 4016: "talarian-mcast2", - 4017: "talarian-mcast3", - 4018: "talarian-mcast4", - 4019: "talarian-mcast5", - 4020: "trap", - 4021: "nexus-portal", - 4022: "dnox", - 4023: "esnm-zoning", - 4024: "tnp1-port", - 4025: "partimage", - 4026: "as-debug", - 4027: "bxp", - 4028: "dtserver-port", - 4029: "ip-qsig", - 4030: "jdmn-port", - 4031: "suucp", - 4032: "vrts-auth-port", - 4033: "sanavigator", - 4034: "ubxd", - 4035: "wap-push-http", - 4036: "wap-push-https", - 4037: "ravehd", - 4038: "fazzt-ptp", - 4039: "fazzt-admin", - 4040: "yo-main", - 4041: "houston", - 4042: "ldxp", - 4043: "nirp", - 4044: "ltp", - 4045: "npp", - 4046: "acp-proto", - 4047: "ctp-state", - 4049: "wafs", - 4050: "cisco-wafs", - 4051: "cppdp", - 4052: "interact", - 4053: "ccu-comm-1", - 4054: "ccu-comm-2", - 4055: "ccu-comm-3", - 4056: "lms", - 4057: "wfm", - 4058: "kingfisher", - 4059: "dlms-cosem", - 4060: "dsmeter-iatc", - 4061: "ice-location", - 4062: "ice-slocation", - 4063: "ice-router", - 4064: "ice-srouter", - 4065: "avanti-cdp", - 4066: "pmas", - 4067: "idp", - 4068: "ipfltbcst", - 4069: "minger", - 4070: "tripe", - 4071: "aibkup", - 4072: "zieto-sock", - 4073: "iRAPP", - 4074: "cequint-cityid", - 4075: "perimlan", - 4076: "seraph", - 4077: "ascomalarm", - 4079: "santools", - 4080: "lorica-in", - 4081: "lorica-in-sec", - 4082: "lorica-out", - 4083: "lorica-out-sec", - 4084: "fortisphere-vm", - 4086: "ftsync", - 4089: "opencore", - 4090: "omasgport", - 4091: "ewinstaller", - 4092: "ewdgs", - 4093: "pvxpluscs", - 4094: "sysrqd", - 4095: "xtgui", - 4096: "bre", - 4097: "patrolview", - 4098: "drmsfsd", - 4099: "dpcp", - 4100: "igo-incognito", - 4101: "brlp-0", - 4102: "brlp-1", - 4103: "brlp-2", - 4104: "brlp-3", - 4105: "shofar", - 4106: "synchronite", - 4107: "j-ac", - 4108: "accel", - 4109: "izm", - 4110: "g2tag", - 4111: "xgrid", - 4112: "apple-vpns-rp", - 4113: "aipn-reg", - 4114: "jomamqmonitor", - 4115: "cds", - 4116: "smartcard-tls", - 4117: "hillrserv", - 4118: "netscript", - 4119: "assuria-slm", - 4121: "e-builder", - 4122: "fprams", - 4123: "z-wave", - 4124: "tigv2", - 4125: "opsview-envoy", - 4126: "ddrepl", - 4127: "unikeypro", - 4128: "nufw", - 4129: "nuauth", - 4130: "fronet", - 4131: "stars", - 4132: "nuts-dem", - 4133: "nuts-bootp", - 4134: "nifty-hmi", - 4135: "cl-db-attach", - 4136: "cl-db-request", - 4137: "cl-db-remote", - 4138: "nettest", - 4139: "thrtx", - 4140: "cedros-fds", - 4141: "oirtgsvc", - 4142: "oidocsvc", - 4143: "oidsr", - 4145: "vvr-control", - 4146: "tgcconnect", - 4147: "vrxpservman", - 4148: "hhb-handheld", - 4149: "agslb", - 4150: "PowerAlert-nsa", - 4151: "menandmice-noh", - 4152: "idig-mux", - 4153: "mbl-battd", - 4154: "atlinks", - 4155: "bzr", - 4156: "stat-results", - 4157: "stat-scanner", - 4158: "stat-cc", - 4159: "nss", - 4160: "jini-discovery", - 4161: "omscontact", - 4162: "omstopology", - 4163: "silverpeakpeer", - 4164: "silverpeakcomm", - 4165: "altcp", - 4166: "joost", - 4167: "ddgn", - 4168: "pslicser", - 4169: "iadt-disc", - 4172: "pcoip", - 4173: "mma-discovery", - 4174: "sm-disc", - 4177: "wello", - 4178: "storman", - 4179: "MaxumSP", - 4180: "httpx", - 4181: "macbak", - 4182: "pcptcpservice", - 4183: "gmmp", - 4184: "universe-suite", - 4185: "wcpp", - 4188: "vatata", - 4191: "dsmipv6", - 4192: "azeti-bd", - 4199: "eims-admin", - 4300: "corelccam", - 4301: "d-data", - 4302: "d-data-control", - 4303: "srcp", - 4304: "owserver", - 4305: "batman", - 4306: "pinghgl", - 4307: "visicron-vs", - 4308: "compx-lockview", - 4309: "dserver", - 4310: "mirrtex", - 4320: "fdt-rcatp", - 4321: "rwhois", - 4322: "trim-event", - 4323: "trim-ice", - 4324: "balour", - 4325: "geognosisman", - 4326: "geognosis", - 4327: "jaxer-web", - 4328: "jaxer-manager", - 4333: "ahsp", - 4340: "gaia", - 4341: "lisp-data", - 4342: "lisp-control", - 4343: "unicall", - 4344: "vinainstall", - 4345: "m4-network-as", - 4346: "elanlm", - 4347: "lansurveyor", - 4348: "itose", - 4349: "fsportmap", - 4350: "net-device", - 4351: "plcy-net-svcs", - 4352: "pjlink", - 4353: "f5-iquery", - 4354: "qsnet-trans", - 4355: "qsnet-workst", - 4356: "qsnet-assist", - 4357: "qsnet-cond", - 4358: "qsnet-nucl", - 4359: "omabcastltkm", - 4361: "nacnl", - 4362: "afore-vdp-disc", - 4368: "wxbrief", - 4369: "epmd", - 4370: "elpro-tunnel", - 4371: "l2c-disc", - 4372: "l2c-data", - 4373: "remctl", - 4375: "tolteces", - 4376: "bip", - 4377: "cp-spxsvr", - 4378: "cp-spxdpy", - 4379: "ctdb", - 4389: "xandros-cms", - 4390: "wiegand", - 4394: "apwi-disc", - 4395: "omnivisionesx", - 4400: "ds-srv", - 4401: "ds-srvr", - 4402: "ds-clnt", - 4403: "ds-user", - 4404: "ds-admin", - 4405: "ds-mail", - 4406: "ds-slp", - 4425: "netrockey6", - 4426: "beacon-port-2", - 4430: "rsqlserver", - 4432: "l-acoustics", - 4441: "netblox", - 4442: "saris", - 4443: "pharos", - 4444: "krb524", - 4445: "upnotifyp", - 4446: "n1-fwp", - 4447: "n1-rmgmt", - 4448: "asc-slmd", - 4449: "privatewire", - 4450: "camp", - 4451: "ctisystemmsg", - 4452: "ctiprogramload", - 4453: "nssalertmgr", - 4454: "nssagentmgr", - 4455: "prchat-user", - 4456: "prchat-server", - 4457: "prRegister", - 4458: "mcp", - 4484: "hpssmgmt", - 4486: "icms", - 4488: "awacs-ice", - 4500: "ipsec-nat-t", - 4534: "armagetronad", - 4535: "ehs", - 4536: "ehs-ssl", - 4537: "wssauthsvc", - 4538: "swx-gate", - 4545: "worldscores", - 4546: "sf-lm", - 4547: "lanner-lm", - 4548: "synchromesh", - 4549: "aegate", - 4550: "gds-adppiw-db", - 4551: "ieee-mih", - 4552: "menandmice-mon", - 4554: "msfrs", - 4555: "rsip", - 4556: "dtn-bundle", - 4557: "mtcevrunqss", - 4558: "mtcevrunqman", - 4559: "hylafax", - 4566: "kwtc", - 4567: "tram", - 4568: "bmc-reporting", - 4569: "iax", - 4591: "l3t-at-an", - 4592: "hrpd-ith-at-an", - 4593: "ipt-anri-anri", - 4594: "ias-session", - 4595: "ias-paging", - 4596: "ias-neighbor", - 4597: "a21-an-1xbs", - 4598: "a16-an-an", - 4599: "a17-an-an", - 4600: "piranha1", - 4601: "piranha2", - 4658: "playsta2-app", - 4659: "playsta2-lob", - 4660: "smaclmgr", - 4661: "kar2ouche", - 4662: "oms", - 4663: "noteit", - 4664: "ems", - 4665: "contclientms", - 4666: "eportcomm", - 4667: "mmacomm", - 4668: "mmaeds", - 4669: "eportcommdata", - 4670: "light", - 4671: "acter", - 4672: "rfa", - 4673: "cxws", - 4674: "appiq-mgmt", - 4675: "dhct-status", - 4676: "dhct-alerts", - 4677: "bcs", - 4678: "traversal", - 4679: "mgesupervision", - 4680: "mgemanagement", - 4681: "parliant", - 4682: "finisar", - 4683: "spike", - 4684: "rfid-rp1", - 4685: "autopac", - 4686: "msp-os", - 4687: "nst", - 4688: "mobile-p2p", - 4689: "altovacentral", - 4690: "prelude", - 4691: "mtn", - 4692: "conspiracy", - 4700: "netxms-agent", - 4701: "netxms-mgmt", - 4702: "netxms-sync", - 4725: "truckstar", - 4726: "a26-fap-fgw", - 4727: "fcis-disc", - 4728: "capmux", - 4729: "gsmtap", - 4730: "gearman", - 4732: "ohmtrigger", - 4737: "ipdr-sp", - 4738: "solera-lpn", - 4739: "ipfix", - 4740: "ipfixs", - 4741: "lumimgrd", - 4742: "sicct-sdp", - 4743: "openhpid", - 4744: "ifsp", - 4745: "fmp", - 4747: "buschtrommel", - 4749: "profilemac", - 4750: "ssad", - 4751: "spocp", - 4752: "snap", - 4753: "simon-disc", - 4784: "bfd-multi-ctl", - 4785: "cncp", - 4789: "vxlan", - 4790: "vxlan-gpe", - 4800: "iims", - 4801: "iwec", - 4802: "ilss", - 4803: "notateit-disc", - 4804: "aja-ntv4-disc", - 4827: "htcp", - 4837: "varadero-0", - 4838: "varadero-1", - 4839: "varadero-2", - 4840: "opcua-udp", - 4841: "quosa", - 4842: "gw-asv", - 4843: "opcua-tls", - 4844: "gw-log", - 4845: "wcr-remlib", - 4846: "contamac-icm", - 4847: "wfc", - 4848: "appserv-http", - 4849: "appserv-https", - 4850: "sun-as-nodeagt", - 4851: "derby-repli", - 4867: "unify-debug", - 4868: "phrelay", - 4869: "phrelaydbg", - 4870: "cc-tracking", - 4871: "wired", - 4876: "tritium-can", - 4877: "lmcs", - 4878: "inst-discovery", - 4881: "socp-t", - 4882: "socp-c", - 4884: "hivestor", - 4885: "abbs", - 4894: "lyskom", - 4899: "radmin-port", - 4900: "hfcs", - 4914: "bones", - 4936: "an-signaling", - 4937: "atsc-mh-ssc", - 4940: "eq-office-4940", - 4941: "eq-office-4941", - 4942: "eq-office-4942", - 4949: "munin", - 4950: "sybasesrvmon", - 4951: "pwgwims", - 4952: "sagxtsds", - 4969: "ccss-qmm", - 4970: "ccss-qsm", - 4986: "mrip", - 4987: "smar-se-port1", - 4988: "smar-se-port2", - 4989: "parallel", - 4990: "busycal", - 4991: "vrt", - 4999: "hfcs-manager", - 5000: "commplex-main", - 5001: "commplex-link", - 5002: "rfe", - 5003: "fmpro-internal", - 5004: "avt-profile-1", - 5005: "avt-profile-2", - 5006: "wsm-server", - 5007: "wsm-server-ssl", - 5008: "synapsis-edge", - 5009: "winfs", - 5010: "telelpathstart", - 5011: "telelpathattack", - 5012: "nsp", - 5013: "fmpro-v6", - 5014: "onpsocket", - 5020: "zenginkyo-1", - 5021: "zenginkyo-2", - 5022: "mice", - 5023: "htuilsrv", - 5024: "scpi-telnet", - 5025: "scpi-raw", - 5026: "strexec-d", - 5027: "strexec-s", - 5029: "infobright", - 5030: "surfpass", - 5031: "dmp", - 5042: "asnaacceler8db", - 5043: "swxadmin", - 5044: "lxi-evntsvc", - 5046: "vpm-udp", - 5047: "iscape", - 5049: "ivocalize", - 5050: "mmcc", - 5051: "ita-agent", - 5052: "ita-manager", - 5053: "rlm-disc", - 5055: "unot", - 5056: "intecom-ps1", - 5057: "intecom-ps2", - 5058: "locus-disc", - 5059: "sds", - 5060: "sip", - 5061: "sips", - 5062: "na-localise", - 5064: "ca-1", - 5065: "ca-2", - 5066: "stanag-5066", - 5067: "authentx", - 5069: "i-net-2000-npr", - 5070: "vtsas", - 5071: "powerschool", - 5072: "ayiya", - 5073: "tag-pm", - 5074: "alesquery", - 5078: "pixelpusher", - 5079: "cp-spxrpts", - 5080: "onscreen", - 5081: "sdl-ets", - 5082: "qcp", - 5083: "qfp", - 5084: "llrp", - 5085: "encrypted-llrp", - 5092: "magpie", - 5093: "sentinel-lm", - 5094: "hart-ip", - 5099: "sentlm-srv2srv", - 5100: "socalia", - 5101: "talarian-udp", - 5102: "oms-nonsecure", - 5104: "tinymessage", - 5105: "hughes-ap", - 5111: "taep-as-svc", - 5112: "pm-cmdsvr", - 5116: "emb-proj-cmd", - 5120: "barracuda-bbs", - 5133: "nbt-pc", - 5136: "minotaur-sa", - 5137: "ctsd", - 5145: "rmonitor-secure", - 5150: "atmp", - 5151: "esri-sde", - 5152: "sde-discovery", - 5154: "bzflag", - 5155: "asctrl-agent", - 5164: "vpa-disc", - 5165: "ife-icorp", - 5166: "winpcs", - 5167: "scte104", - 5168: "scte30", - 5190: "aol", - 5191: "aol-1", - 5192: "aol-2", - 5193: "aol-3", - 5200: "targus-getdata", - 5201: "targus-getdata1", - 5202: "targus-getdata2", - 5203: "targus-getdata3", - 5223: "hpvirtgrp", - 5224: "hpvirtctrl", - 5225: "hp-server", - 5226: "hp-status", - 5227: "perfd", - 5234: "eenet", - 5235: "galaxy-network", - 5236: "padl2sim", - 5237: "mnet-discovery", - 5245: "downtools-disc", - 5246: "capwap-control", - 5247: "capwap-data", - 5248: "caacws", - 5249: "caaclang2", - 5250: "soagateway", - 5251: "caevms", - 5252: "movaz-ssc", - 5264: "3com-njack-1", - 5265: "3com-njack-2", - 5270: "cartographerxmp", - 5271: "cuelink-disc", - 5272: "pk", - 5282: "transmit-port", - 5298: "presence", - 5299: "nlg-data", - 5300: "hacl-hb", - 5301: "hacl-gs", - 5302: "hacl-cfg", - 5303: "hacl-probe", - 5304: "hacl-local", - 5305: "hacl-test", - 5306: "sun-mc-grp", - 5307: "sco-aip", - 5308: "cfengine", - 5309: "jprinter", - 5310: "outlaws", - 5312: "permabit-cs", - 5313: "rrdp", - 5314: "opalis-rbt-ipc", - 5315: "hacl-poll", - 5343: "kfserver", - 5344: "xkotodrcp", - 5349: "stuns", - 5350: "pcp-multicast", - 5351: "pcp", - 5352: "dns-llq", - 5353: "mdns", - 5354: "mdnsresponder", - 5355: "llmnr", - 5356: "ms-smlbiz", - 5357: "wsdapi", - 5358: "wsdapi-s", - 5359: "ms-alerter", - 5360: "ms-sideshow", - 5361: "ms-s-sideshow", - 5362: "serverwsd2", - 5363: "net-projection", - 5364: "kdnet", - 5397: "stresstester", - 5398: "elektron-admin", - 5399: "securitychase", - 5400: "excerpt", - 5401: "excerpts", - 5402: "mftp", - 5403: "hpoms-ci-lstn", - 5404: "hpoms-dps-lstn", - 5405: "netsupport", - 5406: "systemics-sox", - 5407: "foresyte-clear", - 5408: "foresyte-sec", - 5409: "salient-dtasrv", - 5410: "salient-usrmgr", - 5411: "actnet", - 5412: "continuus", - 5413: "wwiotalk", - 5414: "statusd", - 5415: "ns-server", - 5416: "sns-gateway", - 5417: "sns-agent", - 5418: "mcntp", - 5419: "dj-ice", - 5420: "cylink-c", - 5421: "netsupport2", - 5422: "salient-mux", - 5423: "virtualuser", - 5424: "beyond-remote", - 5425: "br-channel", - 5426: "devbasic", - 5427: "sco-peer-tta", - 5428: "telaconsole", - 5429: "base", - 5430: "radec-corp", - 5431: "park-agent", - 5432: "postgresql", - 5433: "pyrrho", - 5434: "sgi-arrayd", - 5435: "sceanics", - 5436: "pmip6-cntl", - 5437: "pmip6-data", - 5443: "spss", - 5453: "surebox", - 5454: "apc-5454", - 5455: "apc-5455", - 5456: "apc-5456", - 5461: "silkmeter", - 5462: "ttl-publisher", - 5463: "ttlpriceproxy", - 5464: "quailnet", - 5465: "netops-broker", - 5500: "fcp-addr-srvr1", - 5501: "fcp-addr-srvr2", - 5502: "fcp-srvr-inst1", - 5503: "fcp-srvr-inst2", - 5504: "fcp-cics-gw1", - 5505: "checkoutdb", - 5506: "amc", - 5553: "sgi-eventmond", - 5554: "sgi-esphttp", - 5555: "personal-agent", - 5556: "freeciv", - 5567: "enc-eps-mc-sec", - 5568: "sdt", - 5569: "rdmnet-device", - 5573: "sdmmp", - 5580: "tmosms0", - 5581: "tmosms1", - 5582: "fac-restore", - 5583: "tmo-icon-sync", - 5584: "bis-web", - 5585: "bis-sync", - 5597: "ininmessaging", - 5598: "mctfeed", - 5599: "esinstall", - 5600: "esmmanager", - 5601: "esmagent", - 5602: "a1-msc", - 5603: "a1-bs", - 5604: "a3-sdunode", - 5605: "a4-sdunode", - 5627: "ninaf", - 5628: "htrust", - 5629: "symantec-sfdb", - 5630: "precise-comm", - 5631: "pcanywheredata", - 5632: "pcanywherestat", - 5633: "beorl", - 5634: "xprtld", - 5670: "zre-disc", - 5671: "amqps", - 5672: "amqp", - 5673: "jms", - 5674: "hyperscsi-port", - 5675: "v5ua", - 5676: "raadmin", - 5677: "questdb2-lnchr", - 5678: "rrac", - 5679: "dccm", - 5680: "auriga-router", - 5681: "ncxcp", - 5682: "brightcore", - 5683: "coap", - 5684: "coaps", - 5687: "gog-multiplayer", - 5688: "ggz", - 5689: "qmvideo", - 5713: "proshareaudio", - 5714: "prosharevideo", - 5715: "prosharedata", - 5716: "prosharerequest", - 5717: "prosharenotify", - 5718: "dpm", - 5719: "dpm-agent", - 5720: "ms-licensing", - 5721: "dtpt", - 5722: "msdfsr", - 5723: "omhs", - 5724: "omsdk", - 5728: "io-dist-group", - 5729: "openmail", - 5730: "unieng", - 5741: "ida-discover1", - 5742: "ida-discover2", - 5743: "watchdoc-pod", - 5744: "watchdoc", - 5745: "fcopy-server", - 5746: "fcopys-server", - 5747: "tunatic", - 5748: "tunalyzer", - 5750: "rscd", - 5755: "openmailg", - 5757: "x500ms", - 5766: "openmailns", - 5767: "s-openmail", - 5768: "openmailpxy", - 5769: "spramsca", - 5770: "spramsd", - 5771: "netagent", - 5777: "dali-port", - 5781: "3par-evts", - 5782: "3par-mgmt", - 5783: "3par-mgmt-ssl", - 5784: "ibar", - 5785: "3par-rcopy", - 5786: "cisco-redu", - 5787: "waascluster", - 5793: "xtreamx", - 5794: "spdp", - 5813: "icmpd", - 5814: "spt-automation", - 5859: "wherehoo", - 5863: "ppsuitemsg", - 5900: "rfb", - 5910: "cm", - 5911: "cpdlc", - 5912: "fis", - 5913: "ads-c", - 5963: "indy", - 5968: "mppolicy-v5", - 5969: "mppolicy-mgr", - 5984: "couchdb", - 5985: "wsman", - 5986: "wsmans", - 5987: "wbem-rmi", - 5988: "wbem-http", - 5989: "wbem-https", - 5990: "wbem-exp-https", - 5991: "nuxsl", - 5992: "consul-insight", - 5999: "cvsup", - 6064: "ndl-ahp-svc", - 6065: "winpharaoh", - 6066: "ewctsp", - 6069: "trip", - 6070: "messageasap", - 6071: "ssdtp", - 6072: "diagnose-proc", - 6073: "directplay8", - 6074: "max", - 6081: "geneve", - 6082: "p25cai", - 6083: "miami-bcast", - 6085: "konspire2b", - 6086: "pdtp", - 6087: "ldss", - 6088: "doglms-notify", - 6100: "synchronet-db", - 6101: "synchronet-rtc", - 6102: "synchronet-upd", - 6103: "rets", - 6104: "dbdb", - 6105: "primaserver", - 6106: "mpsserver", - 6107: "etc-control", - 6108: "sercomm-scadmin", - 6109: "globecast-id", - 6110: "softcm", - 6111: "spc", - 6112: "dtspcd", - 6118: "tipc", - 6122: "bex-webadmin", - 6123: "backup-express", - 6124: "pnbs", - 6133: "nbt-wol", - 6140: "pulsonixnls", - 6141: "meta-corp", - 6142: "aspentec-lm", - 6143: "watershed-lm", - 6144: "statsci1-lm", - 6145: "statsci2-lm", - 6146: "lonewolf-lm", - 6147: "montage-lm", - 6148: "ricardo-lm", - 6149: "tal-pod", - 6160: "ecmp-data", - 6161: "patrol-ism", - 6162: "patrol-coll", - 6163: "pscribe", - 6200: "lm-x", - 6201: "thermo-calc", - 6222: "radmind", - 6241: "jeol-nsddp-1", - 6242: "jeol-nsddp-2", - 6243: "jeol-nsddp-3", - 6244: "jeol-nsddp-4", - 6251: "tl1-raw-ssl", - 6252: "tl1-ssh", - 6253: "crip", - 6268: "grid", - 6269: "grid-alt", - 6300: "bmc-grx", - 6301: "bmc-ctd-ldap", - 6306: "ufmp", - 6315: "scup-disc", - 6316: "abb-escp", - 6317: "nav-data", - 6320: "repsvc", - 6321: "emp-server1", - 6322: "emp-server2", - 6324: "hrd-ns-disc", - 6343: "sflow", - 6346: "gnutella-svc", - 6347: "gnutella-rtr", - 6350: "adap", - 6355: "pmcs", - 6360: "metaedit-mu", - 6363: "ndn", - 6370: "metaedit-se", - 6382: "metatude-mds", - 6389: "clariion-evr01", - 6390: "metaedit-ws", - 6417: "faxcomservice", - 6420: "nim-vdrshell", - 6421: "nim-wan", - 6443: "sun-sr-https", - 6444: "sge-qmaster", - 6445: "sge-execd", - 6446: "mysql-proxy", - 6455: "skip-cert-recv", - 6456: "skip-cert-send", - 6471: "lvision-lm", - 6480: "sun-sr-http", - 6481: "servicetags", - 6482: "ldoms-mgmt", - 6483: "SunVTS-RMI", - 6484: "sun-sr-jms", - 6485: "sun-sr-iiop", - 6486: "sun-sr-iiops", - 6487: "sun-sr-iiop-aut", - 6488: "sun-sr-jmx", - 6489: "sun-sr-admin", - 6500: "boks", - 6501: "boks-servc", - 6502: "boks-servm", - 6503: "boks-clntd", - 6505: "badm-priv", - 6506: "badm-pub", - 6507: "bdir-priv", - 6508: "bdir-pub", - 6509: "mgcs-mfp-port", - 6510: "mcer-port", - 6511: "dccp-udp", - 6514: "syslog-tls", - 6515: "elipse-rec", - 6543: "lds-distrib", - 6544: "lds-dump", - 6547: "apc-6547", - 6548: "apc-6548", - 6549: "apc-6549", - 6550: "fg-sysupdate", - 6551: "sum", - 6558: "xdsxdm", - 6566: "sane-port", - 6568: "rp-reputation", - 6579: "affiliate", - 6580: "parsec-master", - 6581: "parsec-peer", - 6582: "parsec-game", - 6583: "joaJewelSuite", - 6619: "odette-ftps", - 6620: "kftp-data", - 6621: "kftp", - 6622: "mcftp", - 6623: "ktelnet", - 6626: "wago-service", - 6627: "nexgen", - 6628: "afesc-mc", - 6633: "cisco-vpath-tun", - 6634: "mpls-pm", - 6653: "openflow", - 6657: "palcom-disc", - 6670: "vocaltec-gold", - 6671: "p4p-portal", - 6672: "vision-server", - 6673: "vision-elmd", - 6678: "vfbp-disc", - 6679: "osaut", - 6689: "tsa", - 6696: "babel", - 6701: "kti-icad-srvr", - 6702: "e-design-net", - 6703: "e-design-web", - 6714: "ibprotocol", - 6715: "fibotrader-com", - 6767: "bmc-perf-agent", - 6768: "bmc-perf-mgrd", - 6769: "adi-gxp-srvprt", - 6770: "plysrv-http", - 6771: "plysrv-https", - 6784: "bfd-lag", - 6785: "dgpf-exchg", - 6786: "smc-jmx", - 6787: "smc-admin", - 6788: "smc-http", - 6789: "smc-https", - 6790: "hnmp", - 6791: "hnm", - 6801: "acnet", - 6831: "ambit-lm", - 6841: "netmo-default", - 6842: "netmo-http", - 6850: "iccrushmore", - 6868: "acctopus-st", - 6888: "muse", - 6935: "ethoscan", - 6936: "xsmsvc", - 6946: "bioserver", - 6951: "otlp", - 6961: "jmact3", - 6962: "jmevt2", - 6963: "swismgr1", - 6964: "swismgr2", - 6965: "swistrap", - 6966: "swispol", - 6969: "acmsoda", - 6997: "MobilitySrv", - 6998: "iatp-highpri", - 6999: "iatp-normalpri", - 7000: "afs3-fileserver", - 7001: "afs3-callback", - 7002: "afs3-prserver", - 7003: "afs3-vlserver", - 7004: "afs3-kaserver", - 7005: "afs3-volser", - 7006: "afs3-errors", - 7007: "afs3-bos", - 7008: "afs3-update", - 7009: "afs3-rmtsys", - 7010: "ups-onlinet", - 7011: "talon-disc", - 7012: "talon-engine", - 7013: "microtalon-dis", - 7014: "microtalon-com", - 7015: "talon-webserver", - 7019: "doceri-view", - 7020: "dpserve", - 7021: "dpserveadmin", - 7022: "ctdp", - 7023: "ct2nmcs", - 7024: "vmsvc", - 7025: "vmsvc-2", - 7030: "op-probe", - 7040: "quest-disc", - 7070: "arcp", - 7071: "iwg1", - 7080: "empowerid", - 7095: "jdp-disc", - 7099: "lazy-ptop", - 7100: "font-service", - 7101: "elcn", - 7107: "aes-x170", - 7121: "virprot-lm", - 7128: "scenidm", - 7129: "scenccs", - 7161: "cabsm-comm", - 7162: "caistoragemgr", - 7163: "cacsambroker", - 7164: "fsr", - 7165: "doc-server", - 7166: "aruba-server", - 7169: "ccag-pib", - 7170: "nsrp", - 7171: "drm-production", - 7174: "clutild", - 7181: "janus-disc", - 7200: "fodms", - 7201: "dlip", - 7227: "ramp", - 7235: "aspcoordination", - 7262: "cnap", - 7272: "watchme-7272", - 7273: "oma-rlp", - 7274: "oma-rlp-s", - 7275: "oma-ulp", - 7276: "oma-ilp", - 7277: "oma-ilp-s", - 7278: "oma-dcdocbs", - 7279: "ctxlic", - 7280: "itactionserver1", - 7281: "itactionserver2", - 7282: "mzca-alert", - 7365: "lcm-server", - 7391: "mindfilesys", - 7392: "mrssrendezvous", - 7393: "nfoldman", - 7394: "fse", - 7395: "winqedit", - 7397: "hexarc", - 7400: "rtps-discovery", - 7401: "rtps-dd-ut", - 7402: "rtps-dd-mt", - 7410: "ionixnetmon", - 7411: "daqstream", - 7421: "mtportmon", - 7426: "pmdmgr", - 7427: "oveadmgr", - 7428: "ovladmgr", - 7429: "opi-sock", - 7430: "xmpv7", - 7431: "pmd", - 7437: "faximum", - 7443: "oracleas-https", - 7473: "rise", - 7491: "telops-lmd", - 7500: "silhouette", - 7501: "ovbus", - 7510: "ovhpas", - 7511: "pafec-lm", - 7542: "saratoga", - 7543: "atul", - 7544: "nta-ds", - 7545: "nta-us", - 7546: "cfs", - 7547: "cwmp", - 7548: "tidp", - 7549: "nls-tl", - 7550: "cloudsignaling", - 7560: "sncp", - 7566: "vsi-omega", - 7570: "aries-kfinder", - 7574: "coherence-disc", - 7588: "sun-lm", - 7624: "indi", - 7627: "soap-http", - 7628: "zen-pawn", - 7629: "xdas", - 7633: "pmdfmgt", - 7648: "cuseeme", - 7674: "imqtunnels", - 7675: "imqtunnel", - 7676: "imqbrokerd", - 7677: "sun-user-https", - 7680: "pando-pub", - 7689: "collaber", - 7697: "klio", - 7707: "sync-em7", - 7708: "scinet", - 7720: "medimageportal", - 7724: "nsdeepfreezectl", - 7725: "nitrogen", - 7726: "freezexservice", - 7727: "trident-data", - 7734: "smip", - 7738: "aiagent", - 7741: "scriptview", - 7743: "sstp-1", - 7744: "raqmon-pdu", - 7747: "prgp", - 7777: "cbt", - 7778: "interwise", - 7779: "vstat", - 7781: "accu-lmgr", - 7786: "minivend", - 7787: "popup-reminders", - 7789: "office-tools", - 7794: "q3ade", - 7797: "pnet-conn", - 7798: "pnet-enc", - 7799: "altbsdp", - 7800: "asr", - 7801: "ssp-client", - 7802: "vns-tp", - 7810: "rbt-wanopt", - 7845: "apc-7845", - 7846: "apc-7846", - 7872: "mipv6tls", - 7880: "pss", - 7887: "ubroker", - 7900: "mevent", - 7901: "tnos-sp", - 7902: "tnos-dp", - 7903: "tnos-dps", - 7913: "qo-secure", - 7932: "t2-drm", - 7933: "t2-brm", - 7962: "generalsync", - 7967: "supercell", - 7979: "micromuse-ncps", - 7980: "quest-vista", - 7982: "sossd-disc", - 7998: "usicontentpush", - 7999: "irdmi2", - 8000: "irdmi", - 8001: "vcom-tunnel", - 8002: "teradataordbms", - 8003: "mcreport", - 8005: "mxi", - 8008: "http-alt", - 8019: "qbdb", - 8020: "intu-ec-svcdisc", - 8021: "intu-ec-client", - 8022: "oa-system", - 8025: "ca-audit-da", - 8026: "ca-audit-ds", - 8032: "pro-ed", - 8033: "mindprint", - 8034: "vantronix-mgmt", - 8040: "ampify", - 8052: "senomix01", - 8053: "senomix02", - 8054: "senomix03", - 8055: "senomix04", - 8056: "senomix05", - 8057: "senomix06", - 8058: "senomix07", - 8059: "senomix08", - 8060: "aero", - 8074: "gadugadu", - 8080: "http-alt", - 8081: "sunproxyadmin", - 8082: "us-cli", - 8083: "us-srv", - 8086: "d-s-n", - 8087: "simplifymedia", - 8088: "radan-http", - 8097: "sac", - 8100: "xprint-server", - 8115: "mtl8000-matrix", - 8116: "cp-cluster", - 8118: "privoxy", - 8121: "apollo-data", - 8122: "apollo-admin", - 8128: "paycash-online", - 8129: "paycash-wbp", - 8130: "indigo-vrmi", - 8131: "indigo-vbcp", - 8132: "dbabble", - 8148: "isdd", - 8149: "eor-game", - 8160: "patrol", - 8161: "patrol-snmp", - 8182: "vmware-fdm", - 8184: "itach", - 8192: "spytechphone", - 8194: "blp1", - 8195: "blp2", - 8199: "vvr-data", - 8200: "trivnet1", - 8201: "trivnet2", - 8202: "aesop", - 8204: "lm-perfworks", - 8205: "lm-instmgr", - 8206: "lm-dta", - 8207: "lm-sserver", - 8208: "lm-webwatcher", - 8230: "rexecj", - 8243: "synapse-nhttps", - 8276: "pando-sec", - 8280: "synapse-nhttp", - 8292: "blp3", - 8294: "blp4", - 8300: "tmi", - 8301: "amberon", - 8320: "tnp-discover", - 8321: "tnp", - 8351: "server-find", - 8376: "cruise-enum", - 8377: "cruise-swroute", - 8378: "cruise-config", - 8379: "cruise-diags", - 8380: "cruise-update", - 8383: "m2mservices", - 8400: "cvd", - 8401: "sabarsd", - 8402: "abarsd", - 8403: "admind", - 8416: "espeech", - 8417: "espeech-rtp", - 8442: "cybro-a-bus", - 8443: "pcsync-https", - 8444: "pcsync-http", - 8445: "copy-disc", - 8450: "npmp", - 8472: "otv", - 8473: "vp2p", - 8474: "noteshare", - 8500: "fmtp", - 8501: "cmtp-av", - 8554: "rtsp-alt", - 8555: "d-fence", - 8567: "enc-tunnel", - 8600: "asterix", - 8609: "canon-cpp-disc", - 8610: "canon-mfnp", - 8611: "canon-bjnp1", - 8612: "canon-bjnp2", - 8613: "canon-bjnp3", - 8614: "canon-bjnp4", - 8675: "msi-cps-rm-disc", - 8686: "sun-as-jmxrmi", - 8732: "dtp-net", - 8733: "ibus", - 8763: "mc-appserver", - 8764: "openqueue", - 8765: "ultraseek-http", - 8766: "amcs", - 8770: "dpap", - 8786: "msgclnt", - 8787: "msgsrvr", - 8793: "acd-pm", - 8800: "sunwebadmin", - 8804: "truecm", - 8873: "dxspider", - 8880: "cddbp-alt", - 8883: "secure-mqtt", - 8888: "ddi-udp-1", - 8889: "ddi-udp-2", - 8890: "ddi-udp-3", - 8891: "ddi-udp-4", - 8892: "ddi-udp-5", - 8893: "ddi-udp-6", - 8894: "ddi-udp-7", - 8899: "ospf-lite", - 8900: "jmb-cds1", - 8901: "jmb-cds2", - 8910: "manyone-http", - 8911: "manyone-xml", - 8912: "wcbackup", - 8913: "dragonfly", - 8954: "cumulus-admin", - 8989: "sunwebadmins", - 8990: "http-wmap", - 8991: "https-wmap", - 8999: "bctp", - 9000: "cslistener", - 9001: "etlservicemgr", - 9002: "dynamid", - 9007: "ogs-client", - 9009: "pichat", - 9020: "tambora", - 9021: "panagolin-ident", - 9022: "paragent", - 9023: "swa-1", - 9024: "swa-2", - 9025: "swa-3", - 9026: "swa-4", - 9080: "glrpc", - 9084: "aurora", - 9085: "ibm-rsyscon", - 9086: "net2display", - 9087: "classic", - 9088: "sqlexec", - 9089: "sqlexec-ssl", - 9090: "websm", - 9091: "xmltec-xmlmail", - 9092: "XmlIpcRegSvc", - 9100: "hp-pdl-datastr", - 9101: "bacula-dir", - 9102: "bacula-fd", - 9103: "bacula-sd", - 9104: "peerwire", - 9105: "xadmin", - 9106: "astergate-disc", - 9119: "mxit", - 9131: "dddp", - 9160: "apani1", - 9161: "apani2", - 9162: "apani3", - 9163: "apani4", - 9164: "apani5", - 9191: "sun-as-jpda", - 9200: "wap-wsp", - 9201: "wap-wsp-wtp", - 9202: "wap-wsp-s", - 9203: "wap-wsp-wtp-s", - 9204: "wap-vcard", - 9205: "wap-vcal", - 9206: "wap-vcard-s", - 9207: "wap-vcal-s", - 9208: "rjcdb-vcards", - 9209: "almobile-system", - 9210: "oma-mlp", - 9211: "oma-mlp-s", - 9212: "serverviewdbms", - 9213: "serverstart", - 9214: "ipdcesgbs", - 9215: "insis", - 9216: "acme", - 9217: "fsc-port", - 9222: "teamcoherence", - 9255: "mon", - 9277: "traingpsdata", - 9278: "pegasus", - 9279: "pegasus-ctl", - 9280: "pgps", - 9281: "swtp-port1", - 9282: "swtp-port2", - 9283: "callwaveiam", - 9284: "visd", - 9285: "n2h2server", - 9286: "n2receive", - 9287: "cumulus", - 9292: "armtechdaemon", - 9293: "storview", - 9294: "armcenterhttp", - 9295: "armcenterhttps", - 9300: "vrace", - 9318: "secure-ts", - 9321: "guibase", - 9343: "mpidcmgr", - 9344: "mphlpdmc", - 9346: "ctechlicensing", - 9374: "fjdmimgr", - 9380: "boxp", - 9396: "fjinvmgr", - 9397: "mpidcagt", - 9400: "sec-t4net-srv", - 9401: "sec-t4net-clt", - 9402: "sec-pc2fax-srv", - 9418: "git", - 9443: "tungsten-https", - 9444: "wso2esb-console", - 9450: "sntlkeyssrvr", - 9500: "ismserver", - 9522: "sma-spw", - 9535: "mngsuite", - 9536: "laes-bf", - 9555: "trispen-sra", - 9592: "ldgateway", - 9593: "cba8", - 9594: "msgsys", - 9595: "pds", - 9596: "mercury-disc", - 9597: "pd-admin", - 9598: "vscp", - 9599: "robix", - 9600: "micromuse-ncpw", - 9612: "streamcomm-ds", - 9618: "condor", - 9628: "odbcpathway", - 9629: "uniport", - 9632: "mc-comm", - 9667: "xmms2", - 9668: "tec5-sdctp", - 9694: "client-wakeup", - 9695: "ccnx", - 9700: "board-roar", - 9747: "l5nas-parchan", - 9750: "board-voip", - 9753: "rasadv", - 9762: "tungsten-http", - 9800: "davsrc", - 9801: "sstp-2", - 9802: "davsrcs", - 9875: "sapv1", - 9878: "kca-service", - 9888: "cyborg-systems", - 9889: "gt-proxy", - 9898: "monkeycom", - 9899: "sctp-tunneling", - 9900: "iua", - 9901: "enrp", - 9903: "multicast-ping", - 9909: "domaintime", - 9911: "sype-transport", - 9950: "apc-9950", - 9951: "apc-9951", - 9952: "apc-9952", - 9953: "acis", - 9955: "alljoyn-mcm", - 9956: "alljoyn", - 9966: "odnsp", - 9987: "dsm-scm-target", - 9990: "osm-appsrvr", - 9991: "osm-oev", - 9992: "palace-1", - 9993: "palace-2", - 9994: "palace-3", - 9995: "palace-4", - 9996: "palace-5", - 9997: "palace-6", - 9998: "distinct32", - 9999: "distinct", - 10000: "ndmp", - 10001: "scp-config", - 10002: "documentum", - 10003: "documentum-s", - 10007: "mvs-capacity", - 10008: "octopus", - 10009: "swdtp-sv", - 10050: "zabbix-agent", - 10051: "zabbix-trapper", - 10080: "amanda", - 10081: "famdc", - 10100: "itap-ddtp", - 10101: "ezmeeting-2", - 10102: "ezproxy-2", - 10103: "ezrelay", - 10104: "swdtp", - 10107: "bctp-server", - 10110: "nmea-0183", - 10111: "nmea-onenet", - 10113: "netiq-endpoint", - 10114: "netiq-qcheck", - 10115: "netiq-endpt", - 10116: "netiq-voipa", - 10117: "iqrm", - 10128: "bmc-perf-sd", - 10160: "qb-db-server", - 10161: "snmpdtls", - 10162: "snmpdtls-trap", - 10200: "trisoap", - 10201: "rscs", - 10252: "apollo-relay", - 10260: "axis-wimp-port", - 10288: "blocks", - 10439: "bngsync", - 10500: "hip-nat-t", - 10540: "MOS-lower", - 10541: "MOS-upper", - 10542: "MOS-aux", - 10543: "MOS-soap", - 10544: "MOS-soap-opt", - 10800: "gap", - 10805: "lpdg", - 10810: "nmc-disc", - 10860: "helix", - 10880: "bveapi", - 10990: "rmiaux", - 11000: "irisa", - 11001: "metasys", - 10023: "cefd-vmp", - 11095: "weave", - 11106: "sgi-lk", - 11108: "myq-termlink", - 11111: "vce", - 11112: "dicom", - 11161: "suncacao-snmp", - 11162: "suncacao-jmxmp", - 11163: "suncacao-rmi", - 11164: "suncacao-csa", - 11165: "suncacao-websvc", - 11171: "snss", - 11201: "smsqp", - 11208: "wifree", - 11211: "memcache", - 11319: "imip", - 11320: "imip-channels", - 11321: "arena-server", - 11367: "atm-uhas", - 11371: "hkp", - 11430: "lsdp", - 11600: "tempest-port", - 11720: "h323callsigalt", - 11723: "emc-xsw-dcache", - 11751: "intrepid-ssl", - 11796: "lanschool-mpt", - 11876: "xoraya", - 11877: "x2e-disc", - 11967: "sysinfo-sp", - 12000: "entextxid", - 12001: "entextnetwk", - 12002: "entexthigh", - 12003: "entextmed", - 12004: "entextlow", - 12005: "dbisamserver1", - 12006: "dbisamserver2", - 12007: "accuracer", - 12008: "accuracer-dbms", - 12009: "ghvpn", - 12012: "vipera", - 12013: "vipera-ssl", - 12109: "rets-ssl", - 12121: "nupaper-ss", - 12168: "cawas", - 12172: "hivep", - 12300: "linogridengine", - 12321: "warehouse-sss", - 12322: "warehouse", - 12345: "italk", - 12753: "tsaf", - 13160: "i-zipqd", - 13216: "bcslogc", - 13217: "rs-pias", - 13218: "emc-vcas-udp", - 13223: "powwow-client", - 13224: "powwow-server", - 13400: "doip-disc", - 13720: "bprd", - 13721: "bpdbm", - 13722: "bpjava-msvc", - 13724: "vnetd", - 13782: "bpcd", - 13783: "vopied", - 13785: "nbdb", - 13786: "nomdb", - 13818: "dsmcc-config", - 13819: "dsmcc-session", - 13820: "dsmcc-passthru", - 13821: "dsmcc-download", - 13822: "dsmcc-ccp", - 13894: "ucontrol", - 13929: "dta-systems", - 14000: "scotty-ft", - 14001: "sua", - 14002: "scotty-disc", - 14033: "sage-best-com1", - 14034: "sage-best-com2", - 14141: "vcs-app", - 14142: "icpp", - 14145: "gcm-app", - 14149: "vrts-tdd", - 14154: "vad", - 14250: "cps", - 14414: "ca-web-update", - 14936: "hde-lcesrvr-1", - 14937: "hde-lcesrvr-2", - 15000: "hydap", - 15118: "v2g-secc", - 15345: "xpilot", - 15363: "3link", - 15555: "cisco-snat", - 15660: "bex-xr", - 15740: "ptp", - 15998: "2ping", - 16003: "alfin", - 16161: "sun-sea-port", - 16309: "etb4j", - 16310: "pduncs", - 16311: "pdefmns", - 16360: "netserialext1", - 16361: "netserialext2", - 16367: "netserialext3", - 16368: "netserialext4", - 16384: "connected", - 16666: "vtp", - 16900: "newbay-snc-mc", - 16950: "sgcip", - 16991: "intel-rci-mp", - 16992: "amt-soap-http", - 16993: "amt-soap-https", - 16994: "amt-redir-tcp", - 16995: "amt-redir-tls", - 17007: "isode-dua", - 17185: "soundsvirtual", - 17219: "chipper", - 17220: "avtp", - 17221: "avdecc", - 17222: "cpsp", - 17234: "integrius-stp", - 17235: "ssh-mgmt", - 17500: "db-lsp-disc", - 17729: "ea", - 17754: "zep", - 17755: "zigbee-ip", - 17756: "zigbee-ips", - 18000: "biimenu", - 18181: "opsec-cvp", - 18182: "opsec-ufp", - 18183: "opsec-sam", - 18184: "opsec-lea", - 18185: "opsec-omi", - 18186: "ohsc", - 18187: "opsec-ela", - 18241: "checkpoint-rtm", - 18262: "gv-pf", - 18463: "ac-cluster", - 18634: "rds-ib", - 18635: "rds-ip", - 18769: "ique", - 18881: "infotos", - 18888: "apc-necmp", - 19000: "igrid", - 19007: "scintilla", - 19191: "opsec-uaa", - 19194: "ua-secureagent", - 19283: "keysrvr", - 19315: "keyshadow", - 19398: "mtrgtrans", - 19410: "hp-sco", - 19411: "hp-sca", - 19412: "hp-sessmon", - 19539: "fxuptp", - 19540: "sxuptp", - 19541: "jcp", - 19788: "mle", - 19999: "dnp-sec", - 20000: "dnp", - 20001: "microsan", - 20002: "commtact-http", - 20003: "commtact-https", - 20005: "openwebnet", - 20012: "ss-idi-disc", - 20014: "opendeploy", - 20034: "nburn-id", - 20046: "tmophl7mts", - 20048: "mountd", - 20049: "nfsrdma", - 20167: "tolfab", - 20202: "ipdtp-port", - 20222: "ipulse-ics", - 20480: "emwavemsg", - 20670: "track", - 20999: "athand-mmp", - 21000: "irtrans", - 21554: "dfserver", - 21590: "vofr-gateway", - 21800: "tvpm", - 21845: "webphone", - 21846: "netspeak-is", - 21847: "netspeak-cs", - 21848: "netspeak-acd", - 21849: "netspeak-cps", - 22000: "snapenetio", - 22001: "optocontrol", - 22002: "optohost002", - 22003: "optohost003", - 22004: "optohost004", - 22005: "optohost004", - 22273: "wnn6", - 22305: "cis", - 22343: "cis-secure", - 22347: "wibukey", - 22350: "codemeter", - 22555: "vocaltec-phone", - 22763: "talikaserver", - 22800: "aws-brf", - 22951: "brf-gw", - 23000: "inovaport1", - 23001: "inovaport2", - 23002: "inovaport3", - 23003: "inovaport4", - 23004: "inovaport5", - 23005: "inovaport6", - 23272: "s102", - 23333: "elxmgmt", - 23400: "novar-dbase", - 23401: "novar-alarm", - 23402: "novar-global", - 24000: "med-ltp", - 24001: "med-fsp-rx", - 24002: "med-fsp-tx", - 24003: "med-supp", - 24004: "med-ovw", - 24005: "med-ci", - 24006: "med-net-svc", - 24242: "filesphere", - 24249: "vista-4gl", - 24321: "ild", - 24322: "hid", - 24386: "intel-rci", - 24465: "tonidods", - 24554: "binkp", - 24577: "bilobit-update", - 24676: "canditv", - 24677: "flashfiler", - 24678: "proactivate", - 24680: "tcc-http", - 24850: "assoc-disc", - 24922: "find", - 25000: "icl-twobase1", - 25001: "icl-twobase2", - 25002: "icl-twobase3", - 25003: "icl-twobase4", - 25004: "icl-twobase5", - 25005: "icl-twobase6", - 25006: "icl-twobase7", - 25007: "icl-twobase8", - 25008: "icl-twobase9", - 25009: "icl-twobase10", - 25793: "vocaltec-hos", - 25900: "tasp-net", - 25901: "niobserver", - 25902: "nilinkanalyst", - 25903: "niprobe", - 25954: "bf-game", - 25955: "bf-master", - 26000: "quake", - 26133: "scscp", - 26208: "wnn6-ds", - 26260: "ezproxy", - 26261: "ezmeeting", - 26262: "k3software-svr", - 26263: "k3software-cli", - 26486: "exoline-udp", - 26487: "exoconfig", - 26489: "exonet", - 27345: "imagepump", - 27442: "jesmsjc", - 27504: "kopek-httphead", - 27782: "ars-vista", - 27999: "tw-auth-key", - 28000: "nxlmd", - 28119: "a27-ran-ran", - 28200: "voxelstorm", - 28240: "siemensgsm", - 29167: "otmp", - 30001: "pago-services1", - 30002: "pago-services2", - 30003: "amicon-fpsu-ra", - 30004: "amicon-fpsu-s", - 30260: "kingdomsonline", - 30832: "samsung-disc", - 30999: "ovobs", - 31029: "yawn", - 31416: "xqosd", - 31457: "tetrinet", - 31620: "lm-mon", - 31765: "gamesmith-port", - 31948: "iceedcp-tx", - 31949: "iceedcp-rx", - 32034: "iracinghelper", - 32249: "t1distproc60", - 32483: "apm-link", - 32635: "sec-ntb-clnt", - 32636: "DMExpress", - 32767: "filenet-powsrm", - 32768: "filenet-tms", - 32769: "filenet-rpc", - 32770: "filenet-nch", - 32771: "filenet-rmi", - 32772: "filenet-pa", - 32773: "filenet-cm", - 32774: "filenet-re", - 32775: "filenet-pch", - 32776: "filenet-peior", - 32777: "filenet-obrok", - 32801: "mlsn", - 32896: "idmgratm", - 33123: "aurora-balaena", - 33331: "diamondport", - 33334: "speedtrace-disc", - 33434: "traceroute", - 33656: "snip-slave", - 34249: "turbonote-2", - 34378: "p-net-local", - 34379: "p-net-remote", - 34962: "profinet-rt", - 34963: "profinet-rtm", - 34964: "profinet-cm", - 34980: "ethercat", - 35001: "rt-viewer", - 35004: "rt-classmanager", - 35355: "altova-lm-disc", - 36001: "allpeers", - 36865: "kastenxpipe", - 37475: "neckar", - 37654: "unisys-eportal", - 38201: "galaxy7-data", - 38202: "fairview", - 38203: "agpolicy", - 39681: "turbonote-1", - 40000: "safetynetp", - 40841: "cscp", - 40842: "csccredir", - 40843: "csccfirewall", - 40853: "ortec-disc", - 41111: "fs-qos", - 41794: "crestron-cip", - 41795: "crestron-ctp", - 42508: "candp", - 42509: "candrp", - 42510: "caerpc", - 43000: "recvr-rc-disc", - 43188: "reachout", - 43189: "ndm-agent-port", - 43190: "ip-provision", - 43210: "shaperai-disc", - 43439: "eq3-config", - 43440: "ew-disc-cmd", - 43441: "ciscocsdb", - 44321: "pmcd", - 44322: "pmcdproxy", - 44544: "domiq", - 44553: "rbr-debug", - 44600: "asihpi", - 44818: "EtherNet-IP-2", - 44900: "m3da-disc", - 45000: "asmp-mon", - 45054: "invision-ag", - 45678: "eba", - 45825: "qdb2service", - 45966: "ssr-servermgr", - 46999: "mediabox", - 47000: "mbus", - 47100: "jvl-mactalk", - 47557: "dbbrowse", - 47624: "directplaysrvr", - 47806: "ap", - 47808: "bacnet", - 47809: "presonus-ucnet", - 48000: "nimcontroller", - 48001: "nimspooler", - 48002: "nimhub", - 48003: "nimgtw", - 48128: "isnetserv", - 48129: "blp5", - 48556: "com-bardac-dw", - 48619: "iqobject", - 48653: "robotraconteur"} diff --git a/fibratus/tcpip/tcpip.py b/fibratus/tcpip/tcpip.py deleted file mode 100644 index d175d43e7..000000000 --- a/fibratus/tcpip/tcpip.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from enum import Enum - -from fibratus.common import NA -from fibratus.kevent_types import SEND_SOCKET_TCPV4, SEND_SOCKET_UDPV4, RECV_SOCKET_TCPV4, RECV_SOCKET_UDPV4, \ - ACCEPT_SOCKET_TCPV4, CONNECT_SOCKET_TCPV4, DISCONNECT_SOCKET_TCPV4, RECONNECT_SOCKET_TCPV4 - -import fibratus.tcpip.ports as ports - - -def port_to_proto(port, l4_proto): - if 'TCP' in l4_proto: - return ports.IANA_PORTS_TCP[port] if port in ports.IANA_PORTS_TCP else NA - else: - return ports.IANA_PORTS_UDP[port] if port in ports.IANA_PORTS_UDP else NA - - -class IpVer(Enum): - - IPV4 = 0 - IPV6 = 1 - - -class TcpIpParser(object): - - def __init__(self, kevent): - """TCP/IP kernel event parser. - - Packages the TCP and UDP requests into a single - kernel event. - - Parameters: - ---------- - - kevent: dict - kernel event representing the UDP / TCP request - """ - self._kevent = kevent - - def parse_tcpip(self, ketype, ktcpip): - """Parses the TCP/IP kernel events. - - Parameters - ---------- - - ketype: tuple - network kernel event - ktcpip: - kevent payload as forwarded from the collector - - """ - pid = ktcpip.pid - ip_src = ktcpip.saddr - ip_dst = ktcpip.daddr - sport = ktcpip.sport - dport = ktcpip.dport - - self._kevent.pid = pid - - if ketype in [SEND_SOCKET_TCPV4, - SEND_SOCKET_UDPV4, - RECV_SOCKET_TCPV4, - RECV_SOCKET_UDPV4]: - # get the application layer protocol - # associated with the tcp segment - # or the udp datagram - if ketype in [SEND_SOCKET_TCPV4, - RECV_SOCKET_TCPV4]: - l4_proto = 'TCP' - else: - l4_proto = 'UDP' - protocol = port_to_proto(dport, l4_proto) - if protocol == NA: - protocol = port_to_proto(sport, l4_proto) - self._kevent.params = {'pid': pid, - 'ip_src': ip_src, - 'ip_dst': ip_dst, - 'sport': sport, - 'dport': dport, - 'packet_size': ktcpip.size, - 'l4_proto': l4_proto, - 'protocol': protocol} - elif ketype == ACCEPT_SOCKET_TCPV4: - self._kevent.params = dict(pid=pid, ip_src=ip_src, ip_dst=ip_dst, - sport=sport, - dport=dport, - rwin=ktcpip.rcvwin, - protocol=port_to_proto(sport, 'TCP')) - - elif ketype == CONNECT_SOCKET_TCPV4: - self._kevent.params = dict(pid=pid, - ip_src=ip_src, - ip_dst=ip_dst, - sport=sport, - dport=dport, - rwin=ktcpip.rcvwin, - protocol=port_to_proto(dport, 'TCP')) - elif ketype == DISCONNECT_SOCKET_TCPV4: - self._kevent.params = dict(pid=pid, - ip_src=ip_src, - ip_dst=ip_dst, - sport=sport, - dport=dport) - - elif ketype == RECONNECT_SOCKET_TCPV4: - self._kevent.params = dict(pid=pid, - ip_src=ip_src, - ip_dst=ip_dst, - sport=sport, - dport=dport) \ No newline at end of file diff --git a/fibratus/term.py b/fibratus/term.py deleted file mode 100644 index 892cd9b69..000000000 --- a/fibratus/term.py +++ /dev/null @@ -1,191 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from _ctypes import byref -from ctypes import c_ulong -import os - -from fibratus.apidefs.sys import CONSOLE_SCREEN_BUFFER_INFO, INVALID_HANDLE_VALUE, get_std_handle, STD_OUTPUT_HANDLE, \ - create_console_screen_buffer, GENERIC_READ, GENERIC_WRITE, FILE_SHARE_READ, FILE_SHARE_WRITE, \ - CONSOLE_TEXTMODE_BUFFER, get_console_screen_buffer_info, COORD, SMALL_RECT, CHAR_INFO, \ - set_console_active_screen_buffer, write_console_output, CURSOR_INFO, get_console_cursor_info, \ - set_console_cursor_info, write_console_unicode -from fibratus.errors import TermInitializationError - - -HIGH_INTENSITY = 0x0008 - -# terminal colors -BLACK = 0x0000 -DARK_BLUE = 0x0001 -DARK_GREEN = 0x0002 -DARK_RED = 0x0004 -GRAY = DARK_BLUE | DARK_GREEN | DARK_RED -DARK_YELLOW = DARK_RED | DARK_GREEN -DARK_PURPLE = DARK_RED | DARK_BLUE -DARK_CYAN = DARK_GREEN | DARK_BLUE -LIGHT_WHITE = GRAY | HIGH_INTENSITY - - -class AnsiTerm(object): - """Terminal's low level interface. - - Provides a set of methods to interact - with the Windows terminals. By writing the chars - directly to the screen buffer can prevent the - annoying screen flickering. - """ - - def __init__(self): - """Creates a new instance of the terminal. - """ - self._cursor_info = CURSOR_INFO() - self._console = INVALID_HANDLE_VALUE - self._framebuffer = INVALID_HANDLE_VALUE - self._char_buffer = None - self._cols = 0 - self._rows = 0 - self._rect = None - self._coord = COORD(0, 0) - self._size = COORD(0, 0) - - def setup_console(self): - """Initializes the screen frame buffer. - - Swaps the current screen buffer with a - brand new created back buffer where the - characters can be written to the flicker-free - rectangular region. - - """ - self._console = get_std_handle(STD_OUTPUT_HANDLE) - # could not get the standard - # console handle, raise an exception - if self._console == INVALID_HANDLE_VALUE: - raise TermInitializationError() - - buffer_info = CONSOLE_SCREEN_BUFFER_INFO() - get_console_screen_buffer_info(self._console, byref(buffer_info)) - get_console_cursor_info(self._console, byref(self._cursor_info)) - self._cursor_info.visible = False - - self._cols = buffer_info.size.x - self._rows = buffer_info.size.y - self._size = COORD(self._cols, self._rows) - self._rect = SMALL_RECT(0, 0, self._cols - 1, self._rows - 1) - self._char_buffer = (CHAR_INFO * (self._size.x * self._size.y))() - self._framebuffer = create_console_screen_buffer(GENERIC_READ | GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_WRITE, - None, - CONSOLE_TEXTMODE_BUFFER, - None) - if self._framebuffer == INVALID_HANDLE_VALUE: - raise TermInitializationError() - # hide the cursor and swap - # the console active screen buffer - set_console_cursor_info(self._framebuffer, byref(self._cursor_info)) - set_console_active_screen_buffer(self._framebuffer) - - def restore_console(self): - if self._console: - set_console_active_screen_buffer(self._console) - self._cursor_info.visible = True - set_console_cursor_info(self._console, byref(self._cursor_info)) - - def write_output(self, charseq, color=LIGHT_WHITE): - """Writes character and color attribute data to the frame buffer. - - The data to be written is taken from a correspondingly sized rectangular - block at a specified location in the source buffer. - - Parameters - ---------- - - charseq: str - the sequence of characters to be written on the frame buffer - - color: int - the terminal output color - """ - - col = 0 - x = 0 - crlf = False - - if not charseq or len(charseq) <= 0: - return - - try: - for char in charseq: - if char == '\n': - crlf = True - col += 1 - # the last column has been reached. - # If there was a carriage return - # then stop the iteration - if col == self._cols: - col = 0 - if crlf: - crlf = False - continue - - if crlf: - crlf = False - space = col - # keep filling the rectangle with spaces - # until we reach the last column - while space <= self._cols: - self._char_buffer[space - 1].char.unicode_char = ' ' - space += 1 - x += 1 - # reset the column and - # stop the current iteration - col = 0 - continue - self._char_buffer[x].char.unicode_char = char - self._char_buffer[x].attributes = color - x += 1 - except IndexError: - pass - # write the character attribute data - # to the screen buffer - write_console_output(self._framebuffer, - self._char_buffer, - self._size, - self._coord, - byref(self._rect)) - - def write_console(self, charseq): - """Writes a string to a console frame buffer - beginning at the current cursor location. - - charseq: str - the string to be written on the frame buffer - """ - write_console_unicode(self._framebuffer, charseq, len(charseq), byref(c_ulong()), None) - - def cls(self): - """Clears the current screen buffer. - """ - for y in range(self._rows): - for x in range(self._cols): - i = (y * self._cols) + x - self._char_buffer[i].char.unicode_char = ' ' - write_console_output(self._framebuffer, - self._char_buffer, - self._coord, - self._size, - byref(self._rect)) - diff --git a/fibratus/thread.py b/fibratus/thread.py deleted file mode 100644 index 980f05ae1..000000000 --- a/fibratus/thread.py +++ /dev/null @@ -1,504 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from _ctypes import sizeof -from ctypes import byref, cast -from ctypes.wintypes import MAX_PATH -import os - -from fibratus.kevent_types import CREATE_PROCESS, ENUM_PROCESS, TERMINATE_THREAD, TERMINATE_PROCESS, \ - CREATE_THREAD, ENUM_THREAD -from fibratus.apidefs.process import * -from fibratus.apidefs.cdefs import STATUS_SUCCESS -from fibratus.apidefs.sys import close_handle, malloc, free -from fibratus.common import DotD as ddict, NA - - -class ThreadRegistry(object): - - def __init__(self, handle_repository, handles, image_meta_registry): - self._threads = {} - self.on_thread_added_callback = None - self.handle_repository = handle_repository - self.image_meta_registry = image_meta_registry - self._handles = handles - - def add_thread(self, ketype, kti): - """Adds a new process or thread to thread registry. - - Parameters - ---------- - - ketype: tuple - kernel event type - kti: dict - event payload as coming from the - kernel event stream collector - """ - if ketype == CREATE_PROCESS or ketype == ENUM_PROCESS: - parent_pid = int(kti.parent_id, 16) - process_id = int(kti.process_id, 16) - # we assume the process id is - # equal to thread id (in a single - # threaded process) - thread_id = process_id - name = kti.image_file_name - comm = kti.command_line - - thread = ThreadInfo(process_id, thread_id, - parent_pid, - name, - comm) - if ketype == ENUM_PROCESS: - thread.handles = [handle for handle in self._handles if handle.pid == process_id] - if ketype == CREATE_PROCESS: - image_meta = self.image_meta_registry.get_image_meta(thread.exe) - if not image_meta: - image_meta = self.image_meta_registry.add_image_meta(thread.exe) - thread.image_meta = image_meta - self._threads[process_id] = thread - - elif ketype == CREATE_THREAD or ketype == ENUM_THREAD: - # new thread created in the - # context of the existing process - # `procces_id` is the parent - # of this thread - process_id = int(kti.process_id, 16) - parent_pid = process_id - thread_id = int(kti.t_thread_id, 16) - - if parent_pid in self._threads: - # copy info from the process - # which created this thread - pthread = self._threads[parent_pid] - # increment the number of threads - # for this process - pthread.increment_child_count() - - name = pthread.name - comm = pthread.comm - - thread = ThreadInfo(process_id, thread_id, - parent_pid, - name, - comm) - thread.ustack_base = hex(kti.user_stack_base) - thread.kstack_base = hex(kti.stack_base) - thread.base_priority = kti.base_priority - thread.io_priority = kti.io_priority - self._threads[thread_id] = thread - else: - # the parent process has not been found - # query the os for process information - handle = open_process(PROCESS_QUERY_INFORMATION | PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ, - False, - parent_pid) - info = {} - if handle: - info = self._query_process_info(handle) - close_handle(handle) - else: - if get_last_error() == ERROR_ACCESS_DENIED: - if parent_pid == 0: - info = ddict(name='idle', - comm='idle', - parent_id=0) - else: - # the access to protected / system process - # can't be done with PROCESS_VM_READ or PROCESS_QUERY_INFORMATION - # flags. Open the process again but with - # restricted access rights, so we can get the process image file name - handle = open_process(PROCESS_QUERY_LIMITED_INFORMATION, - False, - parent_pid) - if handle: - info = self._query_process_info(handle, False) - close_handle(handle) - - # add a new thread and the parent process - # we just found to avoid continuous lookup - name = info.name if len(info) > 0 and info.name else NA - comm = info.comm if len(info) > 0 and info.comm else NA - ppid = info.parent_pid if len(info) > 0 and info.parent_pid else NA - - thread = ThreadInfo(process_id, thread_id, - process_id, - name, - comm) - thread.ustack_base = hex(kti.user_stack_base) - thread.kstack_base = hex(kti.stack_base) - thread.base_priority = kti.base_priority - thread.io_priority = kti.io_priority - - parent = ThreadInfo(process_id, process_id, - ppid, - name, - comm) - # enumerate parent handles - parent.handles = self.handle_repository.query_handles(process_id) - - self._threads[thread_id] = thread - self._threads[parent_pid] = parent - - if self.on_thread_added_callback and callable(self.on_thread_added_callback): - self.on_thread_added_callback(thread) - - def remove_thread(self, ketype, kti): - """Removes the thread or process from the registry. - - Parameters - ---------- - - ketype: tuple - kernel event type - kti: dict - event payload as coming from the - kernel event stream collector - """ - if ketype == TERMINATE_THREAD: - thread_id = int(kti.t_thread_id, 16) - if thread_id in self._threads: - # remove the thread and - # decrement the child count of - # the parent process - if self._threads[thread_id].child_count == 0: - thread = self._threads.pop(thread_id) - if thread and thread.pid in self._threads: - parent = self._threads[thread.pid] - parent.decrement_child_count() - elif ketype == TERMINATE_PROCESS: - # the process has exited - # remove all of its threads - process_id = int(kti.process_id, 16) - if process_id in self._threads: - proc = self._threads.pop(process_id) - if proc.child_count > 0: - self._threads = dict((k, v) for k, v in self._threads.items() - if v.child_count == 0 and k != process_id) - - def init_thread_kevent(self, kevent, ketype, kti): - """Initialize kernel event. - - Parameters - ---------- - - kevent: KEvent - instance of `KEvent` class - ketype: tuple - kernel event type - kti: dict - kernel event payload - """ - if ketype == CREATE_THREAD or ketype == TERMINATE_THREAD: - tid = int(kti.t_thread_id, 16) - thread = self.get_thread(tid) - if thread: - kevent.params = dict(pid=thread.pid, - tid=tid, - kstack_base=hex(kti.stack_base), - ustack_base=hex(kti.user_stack_base), - io_priority=kti.io_priority, - base_priority=kti.base_priority) - kevent.pid = thread.pid - kevent.tid = tid - else: - pid = int(kti.process_id, 16) - thread = self.get_thread(pid) - if thread: - kparams = dict(pid=pid, - name=thread.name, - comm=thread.comm, - exe=thread.exe, - ppid=thread.ppid) - if thread.image_meta: - # include image meta info - image_meta = dict(arch=thread.image_meta.arch, - timestamp=thread.image_meta.timestamp, - num_sections=thread.image_meta.num_sections, - sections=thread.image_meta.sections, - imports=thread.image_meta.imports, - org=thread.image_meta.org, - description=thread.image_meta.description, - version=thread.image_meta.version, - internal_name=thread.image_meta.internal_name, - copyright=thread.image_meta.copyright) - kparams['image_meta'] = image_meta - kevent.params = kparams - kevent.pid = thread.ppid - - def set_thread_added_callback(self, callback): - self.on_thread_added_callback = callback - - def get_thread(self, tid): - return self._threads[tid] if tid in self._threads else None - - @property - def threads(self): - return self._threads - - def _query_process_info(self, handle, read_peb=True): - """Gets an extended proc info. - - Parameters - ----------- - - handle: HANDLE - handle to process for which the info - should be acquired - read_peb: boolean - true in case the process PEB should be read - - """ - pbi_buff = malloc(sizeof(PROCESS_BASIC_INFORMATION)) - status = zw_query_information_process(handle, - PROCESS_BASIC_INFO, - pbi_buff, - sizeof(PROCESS_BASIC_INFORMATION), - byref(ULONG())) - - info = {} - - if status == STATUS_SUCCESS: - pbi = cast(pbi_buff, POINTER(PROCESS_BASIC_INFORMATION)) - ppid = pbi.contents.inherited_from_unique_process_id - if read_peb: - # read the PEB to get the process parameters. - # Because the PEB structure resides - # in the address space of another process - # we must read the memory block in order - # to access the structure's fields - peb_addr = pbi.contents.peb_base_address - peb_buff = read_process_memory(handle, peb_addr, sizeof(PEB)) - if peb_buff: - peb = cast(peb_buff, POINTER(PEB)) - # read the RTL_USER_PROCESS_PARAMETERS struct - # which contains the command line and the image - # name of the process - pp = peb.contents.process_parameters - pp_buff = read_process_memory(handle, - pp, - sizeof(RTL_USER_PROCESS_PARAMETERS)) - if pp_buff: - pp = cast(pp_buff, POINTER(RTL_USER_PROCESS_PARAMETERS)) - - comm = pp.contents.command_line.buffer - comm_len = pp.contents.command_line.length - exe = pp.contents.image_path_name.buffer - exe_len = pp.contents.image_path_name.length - - # these memory reads are required - # to copy the command line and image name buffers - cb = read_process_memory(handle, comm, comm_len) - eb = read_process_memory(handle, exe, exe_len) - - if cb and eb: - # cast the buffers to - # UNICODE strings - comm = cast(cb, c_wchar_p).value - exe = cast(eb, c_wchar_p).value - - # the image name contains the full path - # split the string to get the exec name - name = exe[exe.rfind('\\') + 1:] - info = ddict(name=name, - comm=comm, - parent_pid=ppid) - free(cb) - free(eb) - free(pp_buff) - - free(peb_buff) - else: - # query only the process image file name - exe = ctypes.create_unicode_buffer(MAX_PATH) - size = DWORD(MAX_PATH) - name = None - status = query_full_process_image_name(handle, - 0, - exe, - byref(size)) - if status: - exe = exe.value - name = exe[exe.rfind('\\') + 1:] - info = ddict(name=name if name else NA, - comm=exe if type(exe) is str else None, - parent_pid=ppid) - if pbi_buff: - free(pbi_buff) - - return info - - -class ThreadInfo(object): - """Represents the state of thread or process. - """ - def __init__(self, pid, tid, ppid, name, comm): - """Creates an instance of `ThreadInfo` class. - - Parameters - ---------- - - pid: int - process identifier - tid: int - thread identifier in the scope of - an existing process - ppid: int - parent process identifier - name: str - process name (cmd.exe) - comm: str - the full command line of a process - (C:\Windows\system32\cmd.exe /cdir /-C /W) - - Attributes - ---------- - - exe: str - the full name of the executable - (C:\Windows\system32\cmd.exe) - args: list - command line arguments for the process - (/cdir, /-C, /W) - child_count: int - the number of threads for this process - handles: list - a list of handles which owns the process - ustack_base: int - the base address of the thread user-space stack - kstack_base: int - the base address of the thread kernel-space stack - io_priority: int - thread I/O priority - base_priority: int - thread CPU priority - """ - self._pid = pid - self._tid = tid - self._ppid = ppid - - # get the executable from the - # full file system path - head, _ = os.path.split(comm[0:comm.find('exe')]) - self._exe = '%s\%s' % (head, name) - self._exe = self._exe.replace("\"", '') - - if 'SystemRoot' in self._exe: - sys_root = os.path.expandvars("%SystemRoot%") - self._exe = self._exe.replace('%SystemRoot%', sys_root)\ - .replace('\\SystemRoot', sys_root) - - self._name = name.lower() if NA not in name else NA - self._comm = comm - # the command line arguments - # are separated by blank space - self._args = comm.split()[1:] - self._child_count = 0 - self._handles = [] - - self._ustack_base = 0x0 - self._kstack_base = 0x0 - self._io_priority = 0 - self._base_priority = 0 - - self._image_meta = None - - @property - def pid(self): - return self._pid - - @property - def ppid(self): - return self._ppid - - @property - def tid(self): - return self._tid - - @property - def exe(self): - return self._exe - - @property - def name(self): - return self._name - - @property - def comm(self): - return self._comm - - @property - def args(self): - return self._args - - @property - def child_count(self): - return self._child_count - - @property - def handles(self): - return self._handles - - @handles.setter - def handles(self, handles): - self._handles = handles - - @property - def ustack_base(self): - return self._ustack_base - - @ustack_base.setter - def ustack_base(self, ustack_base): - self._ustack_base = ustack_base - - @property - def kstack_base(self): - return self._kstack_base - - @kstack_base.setter - def kstack_base(self, kstack_base): - self._kstack_base = kstack_base - - @property - def io_priority(self): - return self._io_priority - - @io_priority.setter - def io_priority(self, io_priority): - self._io_priority = io_priority - - @property - def base_priority(self): - return self._base_priority - - @base_priority.setter - def base_priority(self, base_priority): - self._base_priority = base_priority - - @property - def image_meta(self): - return self._image_meta - - @image_meta.setter - def image_meta(self, image_meta): - self._image_meta = image_meta - - def increment_child_count(self): - self._child_count += 1 - - def decrement_child_count(self): - if self._child_count != 0: - self._child_count -= 1 diff --git a/fibratus/version.py b/fibratus/version.py deleted file mode 100644 index 5981fb911..000000000 --- a/fibratus/version.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -_MAJOR_ = 0 -_MINOR_ = 7 -_REV_ = 2 - -VERSION = '%s.%s.%s' % (_MAJOR_, _MINOR_, _REV_) diff --git a/filaments/anomalous_process_netio.py b/filaments/anomalous_process_netio.py deleted file mode 100644 index 6c345580b..000000000 --- a/filaments/anomalous_process_netio.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2015/2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -An unusual process attempts to make a network request or it accepts -the incoming connection. -""" - -activations = [] -processes = ['notepad.exe', 'calc.exe', 'mspaint.exe'] - - -def on_init(): - set_filter('Send', 'Accept', 'Recv', 'Connect') - - -def on_next_kevent(kevent): - if kevent.thread: - process_name = kevent.thread.name - if process_name in processes: - triggered = True if process_name in activations else False - if not triggered: - message = 'Unusual network activity of kind %s ' \ - 'detected from %s process. ' \ - 'The source ip address is %s and ' \ - 'the destination ip address is %s' \ - % (kevent.name, process_name, - kevent.params.ip_src, - kevent.params.ip_dst) - smtp.emit(message, subject='Anomalous network activity detected') - activations.append(process_name) diff --git a/filaments/elasticsearch_indexing.py b/filaments/elasticsearch_indexing.py deleted file mode 100644 index 478b991aa..000000000 --- a/filaments/elasticsearch_indexing.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -""" -Performs the indexing of the kernel's event stream to -Elasticsearch on interval basis. When the scheduled -interval elapses, the list of documents aggregated -are indexed to Elasticsearch. -""" - -from datetime import datetime -documents = [] - - -def on_init(): - set_filter('CreateThread', 'CreateProcess', 'TerminateThread', 'TerminateProcess', - 'CreateFile', 'DeleteFile', 'WriteFile', 'RenameFile', 'Recv', 'Send', - 'Accept', 'Connect', 'Disconnect', 'LoadImage', 'UnloadImage', - 'RegCreateKey', 'RegDeleteKey', 'RegSetValue') - set_interval(1) - - -def on_next_kevent(kevent): - doco = {'image': kevent.thread.name, - 'thread': { - 'exe': kevent.thread.exe, - 'comm': kevent.thread.comm, - 'pid': kevent.thread.pid, - 'tid': kevent.tid, - 'ppid': kevent.thread.ppid}, - 'category': kevent.category, - 'name': kevent.name, - 'ts': '%s %s' % (datetime.now().strftime('%m/%d/%Y'), - kevent.timestamp.strftime('%H:%M:%S.%f')), - 'cpuid': kevent.cpuid, - 'params': kevent.params} - documents.append(doco) - - -def on_interval(): - if len(documents) > 0: - elasticsearch.emit(documents) - documents.clear() diff --git a/filaments/fishy_netio.py b/filaments/fishy_netio.py new file mode 100644 index 000000000..87451de3f --- /dev/null +++ b/filaments/fishy_netio.py @@ -0,0 +1,73 @@ +# Copyright 2019-2020 by Nedim Sabic (RabbitStack) +# All Rights Reserved. +# http://rabbitstack.github.io + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Anomalous process attempts to make a network request or it accepts an inbound connection +""" + +from utils.dotdict import dotdictify + +__pids__ = [] +__procs__ = [ + 'calc.exe', + 'notepad.exe', + 'mspaint.exe', +] + + +def on_init(): + kfilter("kevt.category = 'net' and ps.name in (%s)" % (', '.join([f'\'{ps}\'' for ps in __procs__]))) + + +@dotdictify +def on_next_kevent(kevent): + print(kevent) + notify = True if kevent.pid in __pids__ else False + if not notify: + emit_alert( + f'Anomalous network I/O detected to {kevent.kparams.dip}:{kevent.kparams.dport}', + text(kevent), + severity='critical', + tags=['anomalous netio'] + ) + __pids__.append(kevent.pid) + + +def text(kevent): + return """ + + Source IP: %s + Source port: %s + Destination IP: %s + Destination port: %s + Protocol: %s + + Process ================================================================================== + + Name: %s + Comm: %s + Cwd: %s + User: %s + + """ % ( + kevent.kparams.sip, + kevent.kparams.sport, + kevent.kparams.dip, + kevent.kparams.dport, + kevent.kparams.dport_name, + kevent.exe, + kevent.comm, + kevent.cwd, kevent.sid) diff --git a/filaments/registry_persistence.py b/filaments/registry_persistence.py new file mode 100644 index 000000000..74f813163 --- /dev/null +++ b/filaments/registry_persistence.py @@ -0,0 +1,91 @@ +# Copyright 2019-2020 by Nedim Sabic (RabbitStack) +# All Rights Reserved. +# http://rabbitstack.github.io + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Surfaces registry operations that would allow a process to execute on system startup +""" + +import os +from utils.dotdict import dotdictify + +__keys__ = [ + r'HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run', + r'HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run', + r'HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce', + r'HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnce', + r'HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunServices', + r'HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunServices', + + r'HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon', + r'HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunServicesOnce', + + r'HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Debug', + r'HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Debug', + + r'HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunServicesOnce', + + r'HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnceEx\0001', + r'HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnceEx\0001\Depend' +] + +WINLOGON_KEY = r'HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon' + + +def on_init(): + kfilter("kevt.name = 'RegSetValue'") + + +@dotdictify +def on_next_kevent(kevent): + key = os.path.dirname(kevent.kparams.key_name) + + # We check if the value being modified under the Winlogon key is Userinit. + # The Userinit registry value defines which programs are run by Winlogon + # when a user logs in to the system. Typically, Winlogon runs Userinit.exe, + # which in turn runs logon scripts, reestablishes network connections, + # and then starts explorer. Attackers can prepend the userinit.exe executable + # with their own malicious binary/script. + if key.lower() == WINLOGON_KEY.lower() and os.path.basename(kevent.kparams.key_name) != 'Userinit': + return + + if any(k.lower() == key.lower() for k in __keys__): + emit_alert( + f'Registry persistence gained via {kevent.kparams.key_name}', + text(kevent), + severity='medium', + tags=['registry persistence'] + ) + + +def text(kevent): + return """ + + Key content: %s + Key type: %s + + Process ================================================================================== + + Name: %s + Comm: %s + Cwd: %s + User: %s + + """ % ( + kevent.kparams.value, + kevent.kparams.type, + kevent.exe, + kevent.comm, + kevent.cwd, kevent.sid) diff --git a/filaments/registry_persistence_detection.py b/filaments/registry_persistence_detection.py deleted file mode 100644 index 27557e103..000000000 --- a/filaments/registry_persistence_detection.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2015/2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Triggers when a process creates the registry value which -would enable it to execute on system startup. -""" - - -keys = ['Run', 'RunOnce', 'RunServices', 'RunServicesOnce', 'Userinit'] - - -def on_init(): - set_filter('RegSetValue') - - -def on_next_kevent(kevent): - if kevent.thread: - process_name = kevent.thread.name - key = kevent.params.key - if key in keys: - message = 'The process %s has created a ' \ - 'persistent registry value , ' \ - 'under %s with content %s' \ - % (process_name, - '%s/%s' % (kevent.params.hive, key), - kevent.params.value) - smtp.emit(message, subject='Registry persistence detected') diff --git a/filaments/top_in_packets.py b/filaments/top_in_packets.py index a6f80dcd8..fa51db168 100644 --- a/filaments/top_in_packets.py +++ b/filaments/top_in_packets.py @@ -1,4 +1,4 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) +# Copyright 2019-2020 by Nedim Sabic (RabbitStack) # All Rights Reserved. # http://rabbitstack.github.io @@ -15,29 +15,29 @@ # under the License. """ -Shows the top TCP / UDP incoming packets. +Shows the top TCP / UDP inbound packets by IP/port tuple """ import collections +from utils.dotdict import dotdictify -connections = collections.Counter() +__connections__ = collections.Counter() def on_init(): - set_filter('Recv') + kfilter("kevt.name = 'Recv'") columns(["Source", "Count"]) sort_by('Count') - set_interval(1) - title('Top incoming TCP/UDP packets') + interval(1) +@dotdictify def on_next_kevent(kevent): - src = ['%s:%d' % (kevent.params.ip_dst, kevent.params.dport)] - connections.update(src) + src = ['%s:%d' % (kevent.kparams.sip, kevent.kparams.sport)] + __connections__.update(src) def on_interval(): - for ip, count in connections.items(): + for ip, count in __connections__.copy().items(): add_row([ip, count]) - render_tabular() - + render_table() \ No newline at end of file diff --git a/filaments/top_registry_io_process.py b/filaments/top_keys.py similarity index 61% rename from filaments/top_registry_io_process.py rename to filaments/top_keys.py index afc519e5a..d8580bae8 100644 --- a/filaments/top_registry_io_process.py +++ b/filaments/top_keys.py @@ -14,29 +14,32 @@ # License for the specific language governing permissions and limitations # under the License. + """ -Shows top processes by registry I/O activity. +Shows top keys by number of registry operations """ import collections +from utils.dotdict import dotdictify -processes_registry_io = collections.Counter() +__keys__ = collections.Counter() def on_init(): - set_filter('RegOpenKey', 'RegQueryKey', 'RegCreateKey', 'RegQueryValue', 'RegSetValue', 'RegDeleteValue') - columns(["Process", "#Ops"]) + kfilter("kevt.category = 'registry'") + columns(["Key", "#Ops"]) sort_by('#Ops') - set_interval(1) - limit(20) + interval(1) +@dotdictify def on_next_kevent(kevent): - process = ['%s (%d)' % (kevent.thread.name, kevent.thread.pid)] - processes_registry_io.update(process) + key = kevent.kparams.key_name + if key: + __keys__.update((key, )) def on_interval(): - for process, io in processes_registry_io.items(): - add_row([process, io]) - render_tabular() + for key, count in __keys__.copy().items(): + add_row([key, count]) + render_table() \ No newline at end of file diff --git a/filaments/top_out_packets.py b/filaments/top_out_packets.py index 436957825..ac00e10b2 100644 --- a/filaments/top_out_packets.py +++ b/filaments/top_out_packets.py @@ -1,4 +1,4 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) +# Copyright 2019-2020 by Nedim Sabic (RabbitStack) # All Rights Reserved. # http://rabbitstack.github.io @@ -15,29 +15,29 @@ # under the License. """ -Shows the top TCP / UDP outbound packets. +Shows the top TCP / UDP outbound packets by IP/port tuple """ import collections +from utils.dotdict import dotdictify -connections = collections.Counter() +__connections__ = collections.Counter() def on_init(): - set_filter('Send') + kfilter("kevt.name = 'Send'") columns(["Destination", "Count"]) sort_by('Count') - set_interval(1) - title('Top outbound TCP/UDP packets') + interval(1) +@dotdictify def on_next_kevent(kevent): - dst = ['%s:%d' % (kevent.params.ip_dst, kevent.params.dport)] - connections.update(dst) + dst = ['%s:%d' % (kevent.kparams.dip, kevent.kparams.dport)] + __connections__.update(dst) def on_interval(): - for ip, count in connections.items(): + for ip, count in __connections__.copy().items(): add_row([ip, count]) - render_tabular() - + render_table() \ No newline at end of file diff --git a/filaments/utils/dotdict.py b/filaments/utils/dotdict.py new file mode 100644 index 000000000..bda93f304 --- /dev/null +++ b/filaments/utils/dotdict.py @@ -0,0 +1,17 @@ +# https://stackoverflow.com/questions/2352181/how-to-use-a-dot-to-access-members-of-dictionary +class dotdict(dict): + """dot.notation access to dictionary attributes""" + __getattr__ = dict.get + __setattr__ = dict.__setitem__ + __delattr__ = dict.__delitem__ + + +def dotdictify(fn): + """ + The decorator for converting the dict parameter to dot notation access dictionary. + """ + def __wrap(kevent): + kevent = dotdict(kevent) + kevent.kparams = dotdict(kevent.kparams) + return fn(kevent) + return __wrap diff --git a/filaments/created_files.py b/filaments/watch_files.py similarity index 64% rename from filaments/created_files.py rename to filaments/watch_files.py index 0b52bf1ce..e0cc8c3eb 100644 --- a/filaments/created_files.py +++ b/filaments/watch_files.py @@ -1,4 +1,4 @@ -# Copyright 2015/2016 by Nedim Sabic (RabbitStack) +# Copyright 2019-2020 by Nedim Sabic (RabbitStack) # All Rights Reserved. # http://rabbitstack.github.io @@ -15,23 +15,24 @@ # under the License. """ -Monitors the files created by processes +Watches files and directories created in the file system """ -files = [] +from utils.dotdict import dotdictify + +__files__ = [] def on_init(): - set_filter('CreateFile') + kfilter("kevt.name = 'CreateFile' and file.operation = 'create'") columns(["Process", "File"]) +@dotdictify def on_next_kevent(kevent): - if kevent.params.operation == 'CREATE' \ - and kevent.params.file_type == 'FILE': - files.append((kevent.thread.name, kevent.params.file, )) - for f in files: + file_name = kevent.kparams.file_name + if file_name: + __files__.append((kevent.exe, file_name, )) + for f in __files__: add_row([f[0], f[1]]) - render_tabular() - - + render_table() \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..04c1f767e --- /dev/null +++ b/go.mod @@ -0,0 +1,36 @@ +module github.com/rabbitstack/fibratus + +require ( + github.com/Microsoft/go-winio v0.4.14 + github.com/akavel/rsrc v0.9.0 // indirect + github.com/briandowns/spinner v1.11.1 + github.com/dustin/go-humanize v1.0.0 + github.com/go-openapi/strfmt v0.19.4 // indirect + github.com/hashicorp/go-version v1.2.1 + github.com/hillu/go-yara v1.2.1 + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/jedib0t/go-pretty/v6 v6.0.1 + github.com/magiconair/properties v1.8.1 + github.com/mitchellh/mapstructure v1.1.2 + github.com/olivere/elastic/v7 v7.0.20 + github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 + github.com/pkg/errors v0.9.1 + github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 + github.com/sirupsen/logrus v1.4.1 + github.com/spf13/cobra v0.0.3 + github.com/spf13/pflag v1.0.3 + github.com/spf13/viper v1.6.2 + github.com/streadway/amqp v1.0.0 + github.com/stretchr/testify v1.5.1 + github.com/valyala/bytebufferpool v1.0.0 + github.com/valyala/gozstd v1.6.4 + github.com/xeipuuv/gojsonschema v1.2.0 + golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b + golang.org/x/text v0.3.2 + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df + gopkg.in/natefinch/lumberjack.v2 v2.0.0 + gopkg.in/yaml.v2 v2.2.4 +) + +go 1.15 diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..c9a772480 --- /dev/null +++ b/go.sum @@ -0,0 +1,283 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/akavel/rsrc v0.9.0 h1:HwUDC0+tMFWqN4D5G+o5siGD4oVsC3jn6zM8ocjc3nY= +github.com/akavel/rsrc v0.9.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go v1.34.13/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/briandowns/spinner v1.11.1 h1:OixPqDEcX3juo5AjQZAnFPbeUA0jvkp2qzB5gOZJ/L0= +github.com/briandowns/spinner v1.11.1/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2 h1:a2kIyV3w+OS3S97zxUndRVD46+FhGOUBDFY7nmu4CsY= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/strfmt v0.19.4 h1:eRvaqAhpL0IL6Trh5fDsGnGhiXndzHFuA05w6sXH6/g= +github.com/go-openapi/strfmt v0.19.4/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hillu/go-yara v1.2.1 h1:th+MRa37XEuugX/eeSgfNRwf+lYNuClt28AJGrGhzdc= +github.com/hillu/go-yara v1.2.1/go.mod h1:KLxCsvD3F8cgVK866UDHi961qbzP+twKjhNdDsuz/2M= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jedib0t/go-pretty/v6 v6.0.1 h1:uUMwi75B5+yaLy6sldJusQYXXkTttIxnsDBzNYL+XdI= +github.com/jedib0t/go-pretty/v6 v6.0.1/go.mod h1:Qu/2Or3TWvmQjNOb13IwTwj8msdvAmiPANdOUTt7Z+Q= +github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olivere/elastic/v7 v7.0.20 h1:5FFpGPVJlBSlWBOdict406Y3yNTIpVpAiUvdFZeSbAo= +github.com/olivere/elastic/v7 v7.0.20/go.mod h1:Kh7iIsXIBl5qRQOBFoylCsXVTtye3keQU2Y/YbR7HD8= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo= +github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.1.1 h1:T/YLemO5Yp7KPzS+lVtu+WsHn8yoSwTfItdAd1r3cck= +github.com/smartystreets/assertions v1.1.1/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/gunit v1.4.2/go.mod h1:ZjM1ozSIMJlAz/ay4SG8PeKF00ckUp+zMHZXV9/bvak= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= +github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= +github.com/streadway/amqp v1.0.0 h1:kuuDrUJFZL1QYL9hUNuCxNObNzB0bV/ZG5jV3RWAQgo= +github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/gozstd v1.6.4 h1:nFLddjEf90SFl5cVWyElSHozQDsbvLljPK703/skBS0= +github.com/valyala/gozstd v1.6.4/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.mongodb.org/mongo-driver v1.0.3 h1:GKoji1ld3tw2aC+GX1wbr/J2fX13yNacEYoJ8Nhr0yU= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/kstream/__init__.py b/kstream/__init__.py deleted file mode 100644 index b4b9e2a2a..000000000 --- a/kstream/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. \ No newline at end of file diff --git a/kstream/includes/__init__.py b/kstream/includes/__init__.py deleted file mode 100644 index b4b9e2a2a..000000000 --- a/kstream/includes/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. \ No newline at end of file diff --git a/kstream/includes/etw.pxd b/kstream/includes/etw.pxd deleted file mode 100644 index c7f948886..000000000 --- a/kstream/includes/etw.pxd +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# http://rabbitstack.github.io -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from .windows cimport * - -cdef extern from "evntcons.h": - - enum: EVENT_HEADER_FLAG_32_BIT_HEADER - enum: PROCESS_TRACE_MODE_EVENT_RECORD - - ctypedef struct EVENT_TRACE_PROPERTIES: - pass - ctypedef struct ETW_BUFFER_CONTEXT: - UCHAR cpuid "ProcessorNumber" - UCHAR Alignment - USHORT LoggerId - - ctypedef struct EVENT_DESCRIPTOR: - USHORT Id - UCHAR Version - UCHAR Channel - UCHAR Level - UCHAR opcode "Opcode" - USHORT Task - ULONGLONG Keyword - - ctypedef struct EVENT_HEADER: - USHORT Size - USHORT HeaderType - USHORT flags "Flags" - USHORT EventProperty - ULONG thread_id "ThreadId" - ULONG process_id "ProcessId" - LARGE_INTEGER timestamp "TimeStamp" - GUID ProviderId - EVENT_DESCRIPTOR descriptor "EventDescriptor" - ULONG KernelTime - ULONG UserTime - ULONGLONG ProcessorTime - GUID ActivityId - - ctypedef struct LINKAGE: - USHORT Linkage - USHORT Reserverd2 - ctypedef struct EVENT_HEADER_EXTENDED_DATA_ITEM: - USHORT Reserved1 - USHORT ExtType - LINKAGE Linkage - USHORT DataSize - ULONGLONG DataPtr - - ctypedef struct EVENT_RECORD: - EVENT_HEADER header "EventHeader" - ETW_BUFFER_CONTEXT buffer_ctx "BufferContext" - USHORT ExtendedDataCount - USHORT UserDataLength - EVENT_HEADER_EXTENDED_DATA_ITEM* ExtendedData - PVOID UserData - PVOID user_ctx "UserContext" - -cdef extern from "evntrace.h": - - ctypedef VOID (__stdcall *PEVENT_RECORD_CALLBACK) (EVENT_RECORD* e) - - ctypedef ULONG64 TRACEHANDLE - - enum: INVALID_PROCESSTRACE_HANDLE - enum: EVENT_TRACE_REAL_TIME_MODE - - ctypedef struct EVENT_TRACE_LOGFILE: - LPTSTR LogFileName - LPSTR logger_name "LoggerName" - ULONG LogFileMode - ULONG trace_mode "ProcessTraceMode" - PEVENT_RECORD_CALLBACK callback "EventRecordCallback" - PVOID context "Context" - - TRACEHANDLE open_trace "OpenTrace"(EVENT_TRACE_LOGFILE* logfile) - - ULONG close_trace "CloseTrace"(TRACEHANDLE handle) - - ULONG process_trace "ProcessTrace"(TRACEHANDLE* handle, ULONG count, - FILETIME* start, - FILETIME* end) \ No newline at end of file diff --git a/kstream/includes/python.pxd b/kstream/includes/python.pxd deleted file mode 100644 index 0a869d2dc..000000000 --- a/kstream/includes/python.pxd +++ /dev/null @@ -1,141 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# http://rabbitstack.github.io -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from cpython.ref cimport PyObject -from libc.stddef cimport wchar_t -from .windows cimport WCHAR, CHAR, BYTE, ULONGLONG, LONGLONG, ULONG, LONG, SHORT, USHORT, ntohs, htonl, inet_ntoa, \ - in_addr, FLOAT, DOUBLE, DWORD, INT32 -from .string cimport wstring, sprintf -from cython.operator cimport dereference as deref - -cdef extern from "python.h": - PyObject* PyUnicode_FromString(const char* u) nogil - PyObject* PyUnicode_FromWideChar (wchar_t* w, Py_ssize_t size) nogil - wchar_t* PyUnicode_AsWideCharString(PyObject* unicode, Py_ssize_t* size) nogil - long PyLong_AsLong(PyObject *obj) nogil - PyObject* Py_BuildValue(char* format, ...) nogil - - PyObject* PyTuple_New(Py_ssize_t len) nogil - PyObject* PyTuple_GetItem(PyObject* p, Py_ssize_t pos) nogil - int PyTuple_SetItem(PyObject* p, Py_ssize_t pos, PyObject* o) nogil - - void Py_XDECREF(PyObject* o) nogil - void Py_XINCREF(PyObject* o) - - void PyMem_Free(void *p) nogil - - -cdef inline PyObject* _unicode(wchar_t* wchars) nogil: - return PyUnicode_FromWideChar(wchars, -1) - - -cdef inline PyObject* _ansi(char* chars) nogil: - return PyUnicode_FromString(chars) - - -cdef inline PyObject* _unicodec(void* buf) nogil: - return Py_BuildValue('u', (buf)[0]) - - -cdef inline PyObject* _ansic(void* buf) nogil: - return Py_BuildValue('s', (buf)[0]) - - -cdef inline PyObject* _i8(void* buf) nogil: - return Py_BuildValue('h', (buf)[0]) - - -cdef inline PyObject* _u8(void* buf) nogil: - return Py_BuildValue('b', (buf)[0]) - - -cdef inline PyObject* _u8_hex(void* buf) nogil: - cdef char hx[200] - sprintf(hx, "%02x", (buf)[0]) - return _ansi(hx) - - -cdef inline PyObject* _i16_hex(void* buf) nogil: - cdef char hx[200] - sprintf(hx, "%02x", (buf)[0]) - return _ansi(hx) - - -cdef inline PyObject* _i64_hex(void* buf) nogil: - cdef char hx[200] - sprintf(hx, "%02x", (buf)[0]) - return _ansi(hx) - - -cdef inline PyObject* _i64(void* buf) nogil: - return Py_BuildValue('L', (buf)[0]) - - -cdef inline PyObject* _u64(void* buf) nogil: - return Py_BuildValue('K', (buf)[0]) - - -cdef inline PyObject* _i32(void* buf) nogil: - return Py_BuildValue('i', (buf)[0]) - - -cdef inline PyObject* _i32_hex(void* buf) nogil: - cdef char hx[200] - sprintf(hx, "0x%x", (buf)[0]) - return _ansi(hx) - - -cdef inline PyObject* _u32(void* buf) nogil: - return Py_BuildValue('k', (buf)[0]) - - -cdef inline PyObject* _i16(void* buf) nogil: - return Py_BuildValue('h', (buf)[0]) - - -cdef inline PyObject* _u16(void* buf) nogil: - return Py_BuildValue('h', (buf)[0]) - - -cdef inline PyObject* _float(void* buf) nogil: - return Py_BuildValue('f', (buf)[0]) - - -cdef inline PyObject* _double(void* buf) nogil: - return Py_BuildValue('d', (buf)[0]) - - -cdef inline PyObject* _ntohs(void* buf) nogil: - return Py_BuildValue('h', ntohs((buf)[0])) - - -cdef inline PyObject* _wstring(wstring ws): - return PyUnicode_FromWideChar(ws.data(), ws.size()) - - -cdef inline wchar_t* _wchar_t(PyObject* o) nogil: - cdef Py_ssize_t size - return PyUnicode_AsWideCharString(o, &size) - - -cdef inline PyObject* ip_addr(void* buf) nogil: - cdef in_addr addr - addr.S_un.S_addr = (buf)[0] - return Py_BuildValue('s', inet_ntoa(addr)) - - -cdef inline wstring deref_prop(prop_name): - return deref(new wstring(_wchar_t(prop_name))) \ No newline at end of file diff --git a/kstream/includes/stdlib.pxd b/kstream/includes/stdlib.pxd deleted file mode 100644 index e68e0ebfb..000000000 --- a/kstream/includes/stdlib.pxd +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# http://rabbitstack.github.io -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -cdef extern from "stdlib.h": - void free(void* ptr) nogil - void* malloc(size_t size) nogil \ No newline at end of file diff --git a/kstream/includes/string.pxd b/kstream/includes/string.pxd deleted file mode 100644 index cd09ae751..000000000 --- a/kstream/includes/string.pxd +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# http://rabbitstack.github.io -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from libc.stddef cimport wchar_t - -cdef extern from "wchar.h": - int wprintf(const wchar_t *, ...) nogil - int printf( const char* format, ... ) nogil - int sprintf (char* str, const char* format, ... ) nogil - long strtol(const char* nptr, char** endptr, int base) nogil - long wcstol(const wchar_t* nptr, wchar_t** endptr, int base) nogil - wchar_t* _wcslwr(wchar_t * s) nogil - int wcscmp(const wchar_t* string1, const wchar_t* string2) nogil - size_t wcslen (const wchar_t* wcs) - -cdef extern from "" namespace "std" nogil: - - cdef cppclass wstring "std::wstring": - wstring() except + - wstring(wchar_t *) except + - wstring(wchar_t *, size_t) except + - wstring(wstring&) except + - - const wchar_t* data() - size_t size() - - int compare(wstring&) - diff --git a/kstream/includes/tdh.pxd b/kstream/includes/tdh.pxd deleted file mode 100644 index d5bfc6bbc..000000000 --- a/kstream/includes/tdh.pxd +++ /dev/null @@ -1,148 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# http://rabbitstack.github.io -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from .windows cimport ULONG -from .etw cimport * - -cdef extern from "tdh.h": - - ctypedef ULONG TDHAPI - - ctypedef enum TDH_IN_TYPE: - TDH_INTYPE_NULL = 0 - TDH_INTYPE_UNICODESTRING = 1 - TDH_INTYPE_ANSISTRING = 2 - TDH_INTYPE_INT8 = 3 - TDH_INTYPE_UINT8 = 4 - TDH_INTYPE_INT16 = 5 - TDH_INTYPE_UINT16 = 6 - TDH_INTYPE_INT32 = 7 - TDH_INTYPE_UINT32 = 8 - TDH_INTYPE_INT64 = 9 - TDH_INTYPE_UINT64 = 10 - TDH_INTYPE_FLOAT = 11 - TDH_INTYPE_DOUBLE = 12 - TDH_INTYPE_BOOLEAN = 13 - TDH_INTYPE_BINARY = 14 - TDH_INTYPE_GUID = 15 - TDH_INTYPE_POINTER = 16 - TDH_INTYPE_FILETIME = 17 - TDH_INTYPE_SYSTEMTIME = 18 - TDH_INTYPE_SID = 19 - TDH_INTYPE_HEXINT32 = 20 - TDH_INTYPE_HEXINT64 = 21 - TDH_INTYPE_COUNTEDSTRING = 300 - TDH_INTYPE_COUNTEDANSISTRING = 301 - TDH_INTYPE_REVERSEDCOUNTEDSTRING = 302 - TDH_INTYPE_REVERSEDCOUNTEDANSISTRING = 303 - TDH_INTYPE_NONNULLTERMINATEDSTRING = 304 - TDH_INTYPE_NONNULLTERMINATEDANSISTRING = 305 - TDH_INTYPE_UNICODECHAR = 306 - TDH_INTYPE_ANSICHAR = 307 - TDH_INTYPE_SIZET = 308 - TDH_INTYPE_HEXDUMP = 309 - TDH_INTYPE_WBEMSID = 310 - - ctypedef enum TDH_OUT_TYPE: - TDH_OUTTYPE_NULL = 0 - TDH_OUTTYPE_STRING = 1 - TDH_OUTTYPE_DATETIME = 2 - TDH_OUTTYPE_BYTE = 3 - TDH_OUTTYPE_UNSIGNEDBYTE = 4 - TDH_OUTTYPE_SHORT = 5 - TDH_OUTTYPE_UNSIGNEDSHORT = 6 - TDH_OUTTYPE_INT = 6 - TDH_OUTTYPE_UNSIGNEDINT = 7 - TDH_OUTTYPE_LONG = 8 - TDH_OUTTYPE_UNSIGNEDLONG = 9 - TDH_OUTTYPE_FLOAT = 10 - TDH_OUTTYPE_DOUBLE = 11 - TDH_OUTTYPE_BOOLEAN = 12 - TDH_OUTTYPE_GUID = 13 - TDH_OUTTYPE_HEXBINARY = 14 - TDH_OUTTYPE_HEXINT8 = 15 - TDH_OUTTYPE_HEXINT16 = 16 - TDH_OUTTYPE_HEXINT32 = 17 - TDH_OUTTYPE_HEXINT64 = 18 - TDH_OUTTYPE_PID = 19 - TDH_OUTTYPE_TID = 20 - TDH_OUTTYPE_PORT = 21 - TDH_OUTTYPE_IPV4 = 22 - TDH_OUTTYPE_IPV6 = 23 - TDH_OUTTYPE_SOCKETADDRESS = 24 - TDH_OUTTYPE_CIMDATETIME = 25 - TDH_OUTTYPE_ETWTIME = 26 - TDH_OUTTYPE_XML = 27 - TDH_OUTYTPE_ERRORCODE = 28, - TDH_OUTTYPE_REDUCEDSTRING = 300 - - ctypedef enum PROPERTY_FLAGS: - PropertyStruct = 0x1 - PropertyParamLength = 0x2 - PropertyParamCount = 0x4 - PropertyWBEMXmlFragment = 0x8 - PropertyParamFixedLength = 0x10 - - - ctypedef struct NON_STRUCT_TYPE: - USHORT in_type "InType" - USHORT out_type "OutType" - - ctypedef struct EVENT_PROPERTY_INFO: - ULONG name_offset "NameOffset" - NON_STRUCT_TYPE non_struct_type "nonStructType" - - ctypedef struct TRACE_PROVIDER_INFO: - GUID ProviderGuid - USHORT PropertyCount - - - ctypedef struct TDH_CONTEXT: - pass - - ctypedef struct PROPERTY_DATA_DESCRIPTOR: - ULONGLONG property_name "PropertyName" - ULONG array_index "ArrayIndex" - ULONG reserved "Reserved" - - - ctypedef struct TRACE_EVENT_INFO: - GUID ProviderGuid - GUID event_guid "EventGuid" - ULONG ProviderNameOffset - ULONG OpcodeNameOffset - ULONG PropertyCount - ULONG property_count "TopLevelPropertyCount" - EVENT_PROPERTY_INFO properties "EventPropertyInfoArray"[1] - - - TDHAPI tdh_get_event_information "TdhGetEventInformation"(EVENT_RECORD* e, ULONG cc, - TDH_CONTEXT* ctx, - TRACE_EVENT_INFO* buf, - ULONG* buf_size) nogil - - ULONG tdh_get_property_size "TdhGetPropertySize"(EVENT_RECORD* e, ULONG cc, - TDH_CONTEXT* ctx, - ULONG count, - PROPERTY_DATA_DESCRIPTOR* descriptor, - ULONG *size) nogil - - ULONG tdh_get_property "TdhGetProperty"(EVENT_RECORD* e, ULONG cc, - TDH_CONTEXT* ctx, - ULONG count, - PROPERTY_DATA_DESCRIPTOR* descriptor, - ULONG buf_size, - BYTE* buf) nogil diff --git a/kstream/includes/windows.pxd b/kstream/includes/windows.pxd deleted file mode 100644 index 2dfa9354a..000000000 --- a/kstream/includes/windows.pxd +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# http://rabbitstack.github.io -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from libc.stddef cimport wchar_t - -cdef extern from "windows.h": - ctypedef unsigned long ULONG - ctypedef unsigned char BYTE - ctypedef unsigned long DWORD - ctypedef signed int INT32 - ctypedef unsigned short WORD - ctypedef float FLOAT - ctypedef double DOUBLE - ctypedef char CHAR - ctypedef unsigned char UCHAR - ctypedef void VOID - ctypedef void* PVOID - ctypedef PVOID HANDLE - ctypedef short SHORT - ctypedef unsigned short USHORT - ctypedef long long LONGLONG - ctypedef char* LPSTR - ctypedef unsigned long long ULONGLONG - ctypedef long LONG - ctypedef void* PVOID - ctypedef DWORD* LPDWORD - ctypedef int BOOL - ctypedef const char* LPCTSTR - ctypedef const wchar_t* LPWSTR - ctypedef wchar_t* LPCWSTR - ctypedef unsigned long long ULONG64 - ctypedef wchar_t* WCHAR - ctypedef WCHAR* LPTSTR - ctypedef wchar_t* LPSIDSTR - ctypedef LPWSTR LPOLESTR - ctypedef long HRESULT - - enum: ERROR_SUCCESS - enum: ERROR_CANCELLED - - enum: THREAD_QUERY_INFORMATION - enum: THREAD_QUERY_LIMITED_INFORMATION - - ctypedef struct GUID: - DWORD Data1 - WORD Data2 - WORD Data3 - BYTE Data4[8] - - ctypedef const GUID & REFGUID - - ctypedef struct U: - DWORD LowPart - LONG HighPart - ctypedef union LARGE_INTEGER: - DWORD low"LowPart" - LONG high "HighPart" - U u - LONGLONG QuadPart - - ctypedef struct FILETIME: - DWORD low_date "dwLowDateTime" - DWORD high_date "dwHighDateTime" - - ctypedef struct SYSTEMTIME: - WORD year "wYear" - WORD month "wMonth" - WORD day_of_week "wDayOfWeek" - WORD day "wDay" - WORD hour "wHour" - WORD minute "wMinute" - WORD second "wSecond" - WORD millis "wMilliseconds" - - ctypedef struct TIME_ZONE_INFORMATION: - LONG Bias - WCHAR StandardName[32] - SYSTEMTIME StandardDate - LONG StandardBias - WCHAR DaylightName[32] - SYSTEMTIME DaylightDate - LONG DaylightBias - - int string_from_guid "StringFromGUID2"(REFGUID guid, LPOLESTR lpsz, int cch) nogil - - BOOL filetime_to_systemtime "FileTimeToSystemTime"(FILETIME *ft, SYSTEMTIME *st) nogil - - BOOL systemtime_to_tz_specific_localtime "SystemTimeToTzSpecificLocalTime"(TIME_ZONE_INFORMATION *zone, - SYSTEMTIME *uni_time, - SYSTEMTIME *local_time) nogil - - HANDLE open_thread "OpenThread"(DWORD desired_access, BOOL inherit_handle, DWORD thread_id) nogil - - DWORD get_process_id_of_thread "GetProcessIdOfThread"(HANDLE thread) nogil - - BOOL close_handle "CloseHandle"(HANDLE handle) nogil - -cdef extern from "winsock.h": - USHORT ntohs(USHORT netshort) nogil - ULONG htonl(ULONG hostlong) nogil - - ctypedef union S_un: - ULONG S_addr - ctypedef struct in_addr: - S_un S_un - - char* inet_ntoa(in_addr addr) nogil - diff --git a/kstream/kstreamc.pxd b/kstream/kstreamc.pxd deleted file mode 100644 index db1b78898..000000000 --- a/kstream/kstreamc.pxd +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# http://rabbitstack.github.io -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from kstream.ktuple cimport build_ktuple -from cpython.ref cimport PyObject - - - -cdef class KEventStreamCollector: - cdef: - - EVENT_TRACE_LOGFILE ktrace - TRACEHANDLE handle - int pointer_size - - -from kstream.includes.etw cimport * -from kstream.includes.tdh cimport * - diff --git a/kstream/kstreamc.pyx b/kstream/kstreamc.pyx deleted file mode 100644 index 270c9c310..000000000 --- a/kstream/kstreamc.pyx +++ /dev/null @@ -1,648 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# http://rabbitstack.github.io -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import re -import os -import traceback - -from libcpp.unordered_map cimport unordered_map -from cython.operator cimport dereference as deref, preincrement as inc -from libcpp.vector cimport vector -from libcpp.utility cimport pair - -from cpython cimport PyBytes_AsString -from cpython.exc cimport PyErr_CheckSignals - -from kstream.includes.etw cimport * -from kstream.includes.tdh cimport * -from kstream.includes.windows cimport * -from kstream.includes.python cimport * -from kstream.includes.stdlib cimport * -from kstream.includes.string cimport * -from kstream.time cimport sys_time -from kstream.ktuple cimport build_ktuple -from kstream.process cimport PROCESS_INFO, THREAD_INFO, pid_from_tid - - -cdef enum: - GUID_LENGTH = 36 - INVALID_PID = 4294967295 - -cdef PyObject* ENUM_PROCESS = build_ktuple('{3d6fa8d0-fe05-11d0-9dda-00c04fd7ba7c}', 3) -cdef PyObject* ENUM_THREAD = build_ktuple('{3d6fa8d1-fe05-11d0-9dda-00c04fd7ba7c}', 3) -cdef PyObject* ENUM_IMAGE = build_ktuple('{2cb15d1d-5fc1-11d2-abe1-00a0c911f518}', 3) -cdef PyObject* REG_CREATE_KCB = build_ktuple('{ae53722e-c863-11d2-8659-00c04fa321a1}', 22) -cdef PyObject* REG_DELETE_KCB = build_ktuple('{ae53722e-c863-11d2-8659-00c04fa321a1}', 23) - -cdef PyObject* CREATE_PROCESS = build_ktuple('{3d6fa8d0-fe05-11d0-9dda-00c04fd7ba7c}', 1) -cdef PyObject* CREATE_THREAD = build_ktuple('{3d6fa8d1-fe05-11d0-9dda-00c04fd7ba7c}', 1) -cdef PyObject* TERMINATE_THREAD = build_ktuple('{3d6fa8d1-fe05-11d0-9dda-00c04fd7ba7c}', 2) -cdef PyObject* TERMINATE_PROCESS = build_ktuple('{3d6fa8d0-fe05-11d0-9dda-00c04fd7ba7c}', 2) - -cdef PyObject* CREATE_FILE = build_ktuple('{90cbdc39-4a3e-11d1-84f4-0000f80464e3}', 64) -cdef PyObject* WRITE_FILE = build_ktuple('{90cbdc39-4a3e-11d1-84f4-0000f80464e3}', 68) -cdef PyObject* READ_FILE = build_ktuple('{90cbdc39-4a3e-11d1-84f4-0000f80464e3}', 67) -cdef PyObject* DELETE_FILE = build_ktuple('{90cbdc39-4a3e-11d1-84f4-0000f80464e3}', 70) -cdef PyObject* CLOSE_FILE = build_ktuple('{90cbdc39-4a3e-11d1-84f4-0000f80464e3}', 66) -cdef PyObject* RENAME_FILE = build_ktuple('{90cbdc39-4a3e-11d1-84f4-0000f80464e3}', 71) -cdef PyObject* SET_FILE_INFORMATION = build_ktuple('{90cbdc39-4a3e-11d1-84f4-0000f80464e3}', 69) - -cdef PyObject* UNLOAD_IMAGE = build_ktuple('{2cb15d1d-5fc1-11d2-abe1-00a0c911f518}', 2) - -cdef wstring PID_PROP = deref_prop("PID") -cdef wstring PPID_PROP = deref_prop("ParentId") -cdef wstring PROCESS_ID_PROP = deref_prop("ProcessId") -cdef wstring FS_THREAD_ID_PROP = deref_prop("TTID") -cdef wstring THREAD_ID_PROP = deref_prop("TThreadId") -cdef wstring IMAGE_FILE_NAME_PROP = deref_prop("ImageFileName") - -REGISTRY_KGUID = '{ae53722e-c863-11d2-8659-00c04fa321a1}' -FS_KGUID = '{90cbdc39-4a3e-11d1-84f4-0000f80464e3}' - - -cdef class KEventStreamCollector: - """Kernel event stream collector. - - - Collects events from the kernel event stream and invokes - a python callback method for each event delivered - to the collector. - - The main motivation behind this Cython extension are the perfomance reasons, where - ETW can generate a huge volume of events, and the parsing - process is very CPU intensive. - - Use - --- - - kevt_stream_collector = KEventStreamCollector(logger_name) - - Register a python callback: - - def next_kevt(ktype, cpuid, ts, kparams): - # your logic here - kevt_stream_collector.open_kstream(next_kevt) - - - """ - cdef EVENT_TRACE_LOGFILE ktrace - cdef TRACEHANDLE handle - cdef int pointer_size - - cdef vector[PyObject*]* ktuple_filters - cdef vector[wchar_t*]* skips - cdef unordered_map[ULONG, PROCESS_INFO]* proc_map - cdef unordered_map[ULONG, THREAD_INFO]* thread_map - - cdef ULONG pid_filter - cdef wchar_t* image_filter - cdef ULONG own_pid - - cdef next_kevt_callback - cdef on_kstream_open_callback - cdef klogger - cdef regex - - def __init__(self, klogger): - self.klogger = klogger - self.handle = 0 - self.next_kevt_callback = None - self.on_kstream_open_callback = None - self.ktrace.logger_name = PyBytes_AsString(self.klogger) - self.regex = re.compile('((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))') - self.pointer_size = 8 - self.ktuple_filters = new vector[PyObject*]() - self.proc_map = new unordered_map[ULONG, PROCESS_INFO]() - self.thread_map = new unordered_map[ULONG, THREAD_INFO]() - self.skips = new vector[wchar_t*]() - self.pid_filter = 0 - self.image_filter = NULL - self.own_pid = os.getpid() - - def open_kstream(self, callback): - """Initializes the kernel event stream. - - Sets the event record callback and open - the trace to consume from kernel event - stream. - - Parameters - ---------- - - callback: callable - A python method which is called - when kernel event is consumed successfully - - """ - self.next_kevt_callback = callback - - self.ktrace.trace_mode = EVENT_TRACE_REAL_TIME_MODE | PROCESS_TRACE_MODE_EVENT_RECORD - self.ktrace.callback = self.process_kevent_callback - # because `process_kevent` callback is the instance - # method and the ETW API expects a callback function with - # single parameter, the `self` argument refers to - # an invalid context. We need to inject the reference - # to this instance into `Context` member. - self.ktrace.context = self - - self.handle = open_trace(&self.ktrace) - - if self.on_kstream_open_callback: - self.on_kstream_open_callback() - - # foward the kernel event stream - # to the consumer and start the processing - status = process_trace(&self.handle, 1, - NULL, - NULL) - if status != ERROR_SUCCESS or status != ERROR_CANCELLED: - if status != INVALID_PROCESSTRACE_HANDLE: - close_trace(self.handle) - else: - raise RuntimeError('ERROR - Unable to open kernel event stream. Error %s' % status) - - def close_kstream(self): - close_trace(self.handle) - - def set_kstream_open_callback(self, callback): - self.on_kstream_open_callback = callback - - def add_skip(self, skip): - self.skips.push_back(_wchar_t(skip)) - - def add_ktuple_filter(self, ktuple): - kguid, opcode = ktuple - self.ktuple_filters.push_back(build_ktuple(kguid, opcode)) - - def add_pid_filter(self, pid): - self.pid_filter = int(pid) if pid else 0 - - def add_image_filter(self, image): - self.image_filter = _wchar_t(image) if image else NULL - - cdef process_kevent_callback(self, EVENT_RECORD* kevent_trace): - with nogil: - (kevent_trace.user_ctx)._process_kevent(kevent_trace) - - cdef void _process_kevent(self, EVENT_RECORD* kevent_trace) nogil except *: - """Kernel event stream callback. - - Parameters - ---------- - - kevent_trace: EVENT_RECORD - The pointer to kernel event metadata - - """ - cdef TRACE_EVENT_INFO* info = malloc(4096) - - # the allocation has failed probably - # because there is no enough memory - if info == NULL: - return - - cdef EVENT_HEADER kevt_hdr = kevent_trace.header - cdef ULONG buffer_size = 4096 - cdef unordered_map[wstring, PyObject*] params - cdef ULONG property_size - cdef PROPERTY_DATA_DESCRIPTOR descriptor - cdef BOOL dropped = False - cdef PROCESS_INFO pi - cdef THREAD_INFO ti - - status = tdh_get_event_information(kevent_trace, 0, - NULL, - info, - &buffer_size) - - cpuid = kevent_trace.buffer_ctx.cpuid - opcode = kevt_hdr.descriptor.opcode - pid = kevt_hdr.process_id - tid = kevt_hdr.thread_id - - ktuple = self.__wrap_ktuple(info.event_guid, opcode) - # this shouldn't happen, but just in - # case simply discard the kernel event - if ktuple == NULL: - free(info) - return - dropped = self.__apply_filters(pid, tid, ktuple, params, True) - - if dropped: - with gil: - free(info) - Py_XDECREF(ktuple) - return - - if (kevt_hdr.flags & EVENT_HEADER_FLAG_32_BIT_HEADER) == \ - EVENT_HEADER_FLAG_32_BIT_HEADER: - self.pointer_size = 4 - else: - self.pointer_size = 8 - - if status == ERROR_SUCCESS: - props = info.properties - for i from 0 <= i < info.property_count: - prop = props[i] - - property_name = info + prop.name_offset - - descriptor.property_name = info + \ - prop.name_offset - descriptor.array_index = 0xFFFFFFFF - - # get the property size which - # is used to allocate the buffer - tdh_get_property_size(kevent_trace, 0, - NULL, - 1, - &descriptor, - &property_size) - property_buffer = malloc(property_size) - if property_buffer == NULL: - return - - # fill the property buffer - status = tdh_get_property(kevent_trace, 0, - NULL, - 1, - &descriptor, - property_size, - property_buffer) - # get the property value and store it in the map - if status == ERROR_SUCCESS: - if property_name != NULL: - mapk = new wstring(property_name) - params[deref(mapk)] = \ - self.__parse_property(prop.non_struct_type.in_type, - prop.non_struct_type.out_type, - property_buffer) - del mapk - free(property_buffer) - else: - if property_buffer != NULL: - free(property_buffer) - - free(info) - ts = sys_time(kevt_hdr.timestamp) - - # build a tiny state machine around the - # currently running processes/threads on the system - if self.__ktuple_equals(ktuple, ENUM_PROCESS) or \ - self.__ktuple_equals(ktuple, CREATE_PROCESS): - pi.pid = wcstol(_wchar_t(params.at(PROCESS_ID_PROP)), NULL, 16) - pi.ppid = wcstol(_wchar_t(params.at(PPID_PROP)), NULL, 16) - pi.name = _wchar_t(params.at(IMAGE_FILE_NAME_PROP)) - k = new pair[ULONG, PROCESS_INFO](wcstol(_wchar_t(params.at(PROCESS_ID_PROP)), NULL, 16), - pi) - self.proc_map.insert(deref(k)) - del k - elif self.__ktuple_equals(ktuple, ENUM_THREAD) or \ - self.__ktuple_equals(ktuple, CREATE_THREAD): - ti.tid = wcstol(_wchar_t(params.at(THREAD_ID_PROP)), NULL, 16) - ti.pid = wcstol(_wchar_t(params.at(PROCESS_ID_PROP)), NULL, 16) - tk = new pair[ULONG, THREAD_INFO](wcstol(_wchar_t(params.at(THREAD_ID_PROP)), NULL, 16), - ti) - self.thread_map.insert(deref(tk)) - del tk - elif self.__ktuple_equals(ktuple, TERMINATE_THREAD): - prop_tid = wcstol(_wchar_t(params.at(THREAD_ID_PROP)), NULL, 16) - self.thread_map.erase(prop_tid) - elif self.__ktuple_equals(ktuple, TERMINATE_PROCESS): - # defer the removal of the pid to be able to capture - # `TerminateProcess` if the image filter is set - if self.image_filter == NULL: - prop_pid = wcstol(_wchar_t(params.at(PROCESS_ID_PROP)), NULL, 16) - self.proc_map.erase(prop_pid) - elif self.__ktuple_equals(ktuple, CREATE_FILE) or \ - self.__ktuple_equals(ktuple, WRITE_FILE) or \ - self.__ktuple_equals(ktuple, READ_FILE) or \ - self.__ktuple_equals(ktuple, DELETE_FILE) or \ - self.__ktuple_equals(ktuple, CLOSE_FILE) or \ - self.__ktuple_equals(ktuple, RENAME_FILE) or \ - self.__ktuple_equals(ktuple, SET_FILE_INFORMATION): - # on some Windows versions the value of - # the PID attribute is invalid for the - # file system kernel events - if pid == INVALID_PID: - prop_fs_tid = params.at(FS_THREAD_ID_PROP) - if prop_fs_tid != NULL: - # try to resolve the pid from the thread id - pid = pid_from_tid(PyLong_AsLong(prop_fs_tid), - self.thread_map) - elif self.__ktuple_equals(ktuple, UNLOAD_IMAGE): - # on Windows 7 the pid field of the event header - # is invalid, so use the pid found in the event params - if pid == INVALID_PID: - p = params.at(PROCESS_ID_PROP) - if p != NULL: - pid = PyLong_AsLong(p) - - dropped = self.__apply_filters(pid, tid, ktuple, params, False) - # now we can erase the pid - if self.image_filter != NULL and \ - self.__ktuple_equals(ktuple, TERMINATE_PROCESS): - prop_pid = wcstol(_wchar_t(params.at(PROCESS_ID_PROP)), NULL, 16) - self.proc_map.erase(prop_pid) - if dropped: - with gil: - # decrement references to avoid memory leaks - if self.image_filter != NULL: - self._decref_params(params) - Py_XDECREF(ktuple) - return - with gil: - # check for pending signals. - # The default behaviour is to - # raise `KeyboardInterrupt` exception - # which will be propagated to the caller - if PyErr_CheckSignals() > 0: - self.close_kstream() - return - try: - timestamp = '%d-%d-%d %d:%02d:%02d.%d' % (ts.year, ts.month, - ts.day, ts.hour, - ts.minute, ts.second, - ts.millis) - # convert the property name from - # camel case to underscore so we have - # PEP 8 compliant coding style - kparams = {self._underscore(self._decref(_wstring(kparam.first))): self._decref(kparam.second) - for kparam in params - if kparam.second != NULL} - kguid, opc = ktuple - kguid = kguid[:-1] - # registry events have a valid process/thread id - # in the event header so we aggregate them - if kguid in REGISTRY_KGUID: - kparams['thread_id'] = tid - kparams['process_id'] = pid - elif kguid in FS_KGUID: - kparams['process_id'] = pid - if self.next_kevt_callback: - self.next_kevt_callback((kguid, opc,), cpuid, - timestamp, - kparams) - except Exception as e: - print(traceback.print_exc()) - except KeyboardInterrupt: - pass - finally: - Py_XDECREF(ktuple) - else: - free(info) - - cdef _decref(self, PyObject* o): - pyo = None - if o != NULL: - pyo = o - Py_XDECREF(o) - return pyo - - cdef _decref_params(self, unordered_map[wstring, PyObject*] params): - for kparam in params: - Py_XDECREF(_wstring(kparam.first)) - if kparam.second != NULL: - Py_XDECREF(kparam.second) - - cdef _underscore(self, o): - return self.regex.sub(r'_\1', o).lower() - - cdef PyObject* __parse_property(self, USHORT in_type, USHORT out_type, - BYTE* buf) nogil: - """Parses the property value. - - Given the property input / output types, - transforms the buffer with property payload. - - Parameters - ---------- - - {in|out}_type : USHORT - The property input/output types. An input type can - have multiple output types. For example, UINT16 input - type can have HEXINT16 or PORT output types. - - buffer: BYTE - The pointer to the property buffer. - """ - if buf == NULL: - return NULL - - if in_type == TDH_INTYPE_UNICODESTRING: - return _unicode(buf) - elif in_type == TDH_INTYPE_ANSISTRING: - return _ansi(buf) - elif in_type == TDH_INTYPE_UNICODECHAR: - return _unicodec(buf) - elif in_type == TDH_INTYPE_ANSICHAR: - return _ansic(buf) - - elif in_type == TDH_INTYPE_INT8: - return _i8(buf) - elif in_type == TDH_INTYPE_UINT8: - if out_type == TDH_OUTTYPE_HEXINT8: - return _u8_hex(buf) - else: - return _u8(buf) - - elif in_type == TDH_INTYPE_INT16: - return _i16(buf) - elif in_type == TDH_INTYPE_UINT16: - if out_type == TDH_OUTTYPE_HEXINT16: - return _i16_hex(buf) - elif out_type == TDH_OUTTYPE_PORT: - return _ntohs(buf) - else: - return _u16(buf) - - elif in_type == TDH_INTYPE_INT32: - return _i32(buf) - elif in_type == TDH_INTYPE_UINT32: - if out_type == TDH_OUTTYPE_HEXINT32: - return _i32_hex(buf) - elif out_type == TDH_OUTTYPE_IPV4: - return ip_addr(buf) - else: - return _u32(buf) - - elif in_type == TDH_INTYPE_INT64: - return _i64(buf) - elif in_type == TDH_INTYPE_UINT64: - if out_type == TDH_OUTTYPE_HEXINT64: - return _i64_hex(buf) - else: - return _u64(buf) - - elif in_type == TDH_INTYPE_HEXINT32: - return _i32_hex(buf) - elif in_type == TDH_INTYPE_HEXINT64: - return _i64_hex(buf) - - elif in_type == TDH_INTYPE_FLOAT: - return _float(buf) - elif in_type == TDH_INTYPE_DOUBLE: - return _double(buf) - - elif in_type == TDH_INTYPE_POINTER or \ - in_type == TDH_INTYPE_SIZET: - if self.pointer_size == 8: - return _u64(buf) - else: - return _u32(buf) - else: - return NULL - - cdef BOOL __apply_filters(self, ULONG pid, ULONG tid, - PyObject* ktuple, - unordered_map[wstring, PyObject*] params, - BOOL defer) nogil: - cdef BOOL drop = True - - # we don't want to capture any events - # coming from the fibratus process - # nor from the process we've declared - # in the excluded process list - if self.own_pid == pid: - return True - - if self.__ktuple_equals(ktuple, ENUM_PROCESS) or \ - self.__ktuple_equals(ktuple, ENUM_THREAD) or \ - self.__ktuple_equals(ktuple, ENUM_IMAGE) or \ - self.__ktuple_equals(ktuple, REG_CREATE_KCB) or \ - self.__ktuple_equals(ktuple, REG_DELETE_KCB): - return False - - # apply skip list as defined - # in the configuration descriptor - drop = self.__apply_skips(pid) - if drop: - return True - elif self.image_filter != NULL: - drop = True - - for i from 0 <= i < self.ktuple_filters.size(): - ktuple_filter = self.ktuple_filters.at(i) - if self.__ktuple_equals(ktuple, ktuple_filter): - drop = False - break - - # apply pid filter - if self.pid_filter != 0 and self.pid_filter != pid: - drop = True - # we got an invalid pid from the header - # and we can't still drop the event - if pid == INVALID_PID: - drop = False - elif self.image_filter != NULL: - # apply image filter - if defer: - return False - if pid == INVALID_PID: - drop = False - else: - drop = self.__apply_image_filter(pid) - # this only apply to `CreateProcess` events where - # parent pid is mapped to an image which doesn't match - # the child pid's image - if drop and \ - self.__ktuple_equals(ktuple, CREATE_PROCESS): - if params.size() > 0: - image_name = _wchar_t(params.at(IMAGE_FILE_NAME_PROP)) - drop = wcscmp(_wcslwr(image_name), - _wcslwr(self.image_filter)) != 0 - if drop: - return True - # now scan for the kernel event - # properties to find the pid value - drop = self.__apply_prop_filters(params) - - return drop - - cdef inline BOOL __apply_skips(self, ULONG pid) nogil: - cdef BOOL ignored = False - cdef unordered_map[ULONG, PROCESS_INFO].iterator proc_iterator = self.proc_map.find(pid) - if proc_iterator != self.proc_map.end(): - for i from 0 <= i < self.skips.size(): - skip = self.skips.at(i) - # compare the image name found - # on the proc map with the value - # as defined on the skip list - pi = deref(proc_iterator).second - if wcscmp(_wcslwr(pi.name), _wcslwr(skip)) == 0: - ignored = True - break - return ignored - - cdef inline BOOL __apply_prop_filters(self, unordered_map[wstring, PyObject*] params) nogil: - cdef unordered_map[wstring, PyObject*].iterator piter = params.begin() - cdef BOOL drop = False - cdef ULONG pid = 0 - - while piter != params.end(): - prop = deref(piter) - prop_name = prop.first - - # get the value of the pid property - if prop_name.compare(PID_PROP) == 0: - pid = PyLong_AsLong(prop.second) - - # apply the filters. At this point - # we also check the kernel event is - # not coming from the fibratus process - if pid != 0: - if pid == self.own_pid: - drop = True - break - elif self.pid_filter != 0 and self.pid_filter != pid: - drop = True - break - elif self.image_filter != NULL: - drop = self.__apply_image_filter(pid) - if drop: - break - inc(piter) - return drop - - cdef inline BOOL __apply_image_filter(self, ULONG pid) nogil: - cdef BOOL drop = True - proc_iterator = self.proc_map.find(pid) - if proc_iterator != self.proc_map.end(): - pi = deref(proc_iterator).second - drop = wcscmp(_wcslwr(pi.name), - _wcslwr(self.image_filter)) != 0 - return drop - - cdef PyObject* __wrap_ktuple(self, GUID guid, UCHAR opcode) nogil: - cdef wchar_t buf[39] - - if string_from_guid(guid, buf, 39) > 0: - kguid = PyUnicode_FromWideChar(_wcslwr(buf), 39) - return build_ktuple(kguid, opcode) - else: - return NULL - - cdef inline BOOL __ktuple_equals(self, PyObject* k1, PyObject* k2) nogil: - cdef BOOL ktuple_equals = False - if PyLong_AsLong(PyTuple_GetItem(k1, 1)) == PyLong_AsLong(PyTuple_GetItem(k2, 1)): - kguid1 = _wchar_t(PyTuple_GetItem(k1, 0)) - kguid2 = _wchar_t(PyTuple_GetItem(k2, 0)) - ktuple_equals = wcscmp(kguid1, kguid2) == 0 - if kguid1 != NULL: - PyMem_Free(kguid1) - if kguid2 != NULL: - PyMem_Free(kguid2) - return ktuple_equals diff --git a/kstream/ktuple.pxd b/kstream/ktuple.pxd deleted file mode 100644 index bbc4352a2..000000000 --- a/kstream/ktuple.pxd +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# http://rabbitstack.github.io -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -from kstream.includes.python cimport PyTuple_SetItem, Py_BuildValue, PyTuple_New -from cpython.ref cimport PyObject -from kstream.includes.windows cimport UCHAR - -cdef inline PyObject* build_ktuple(PyObject* kguid, UCHAR opcode) nogil: - cdef PyObject* ktuple = PyTuple_New(2) - cdef PyObject* oco = Py_BuildValue('b', opcode) - PyTuple_SetItem(ktuple, 0, kguid) - PyTuple_SetItem(ktuple, 1, oco) - return ktuple diff --git a/kstream/process.pxd b/kstream/process.pxd deleted file mode 100644 index 0aade3a33..000000000 --- a/kstream/process.pxd +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2017 by Nedim Sabic (RabbitStack) -# http://rabbitstack.github.io -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from kstream.includes.windows cimport UCHAR, ULONG, wchar_t, get_process_id_of_thread, open_thread, close_handle, \ - THREAD_QUERY_LIMITED_INFORMATION, HANDLE -from libcpp.unordered_map cimport unordered_map -from cython.operator cimport dereference as deref - -cdef enum: - INVALID_PID = 4294967295 - -cdef struct PROCESS_INFO: - # process identifier - ULONG pid - # process parent identifier - ULONG ppid - # name of the image file - wchar_t* name - -cdef struct THREAD_INFO: - # thread identifier - ULONG tid - # process identifier - ULONG pid - -cdef inline ULONG pid_from_tid(ULONG tid, unordered_map[ULONG, THREAD_INFO]* thread_map) nogil: - cdef unordered_map[ULONG, THREAD_INFO].iterator thread_iter = thread_map.find(tid) - # try to resolve pid from tid by - # querying the thread map - if thread_iter != thread_map.end(): - ti = deref(thread_iter).second - return ti.pid - else: - # if not found, try to resolve via - # `GetProcessIdOfThread` Windows API function - thread = open_thread(THREAD_QUERY_LIMITED_INFORMATION, - False, - tid) - if thread != NULL: - pid = get_process_id_of_thread(thread) - close_handle(thread) - return pid - else: - return INVALID_PID diff --git a/kstream/time.pxd b/kstream/time.pxd deleted file mode 100644 index 335d60734..000000000 --- a/kstream/time.pxd +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# http://rabbitstack.github.io -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from kstream.includes.windows cimport * - -cdef inline SYSTEMTIME sys_time(LARGE_INTEGER timestamp) nogil: - cdef FILETIME f - cdef SYSTEMTIME s - cdef SYSTEMTIME tzt - - f.high_date = timestamp.high - f.low_date = timestamp.low - filetime_to_systemtime(&f, &s) - systemtime_to_tz_specific_localtime(NULL, &s, &tzt) - - return tzt \ No newline at end of file diff --git a/make.bat b/make.bat new file mode 100644 index 000000000..985c50ed6 --- /dev/null +++ b/make.bat @@ -0,0 +1,110 @@ +:: Copyright 2019-2020 by Nedim Sabic Sabic +:: https://www.fibratus.io +:: All Rights Reserved. +:: +:: Licensed under the Apache License, Version 2.0 (the "License"); you may +:: not use this file except in compliance with the License. You may obtain +:: a copy of the License at +:: +:: http://www.apache.org/licenses/LICENSE-2.0 + +@echo off +SetLocal EnableDelayedExpansion + +set PYTHON_VER=3.7.9 +set PYTHON_URL=https://www.python.org/ftp/python/%PYTHON_VER%/python-%PYTHON_VER%-embed-amd64.zip + +set GOBIN=%USERPROFILE%\go\bin + +set GOTEST=go test -v -race +set GOVET=go vet +set GOFMT=gofmt -e -s -l -w +set GOLINT=%GOBIN%\golint -set_exit_status + +set LDFLAGS="-s -w -X github.com/rabbitstack/fibratus/cmd/fibratus/app.version=%VERSION% -X github.com/rabbitstack/fibratus/cmd/fibratus/app.commit=%COMMIT%" + +:: In case you want to avoid CGO overhead or don't need a specific feature, +:: try tweaking these conditional compilation tags. By default, Fibratus is +:: built with filament, yara and kcap support. +if NOT DEFINED TAGS ( + set TAGS=kcap,filament,yara +) + +set PKGS= +:: Get the list of packages that we'll use to run tests/linter +for /f %%p in ('go list .\...') do call set "PKGS=%%PKGS%% %%p" + +if "%~1"=="build" goto build +if "%~1"=="test" goto test +if "%~1"=="lint" goto lint +if "%~1"=="fmt" goto fmt +if "%~1"=="clean" goto clean +if "%~1"=="pkg" goto pkg +if "%~1"=="deps" goto deps +if "%~1"=="rsrc" goto rsrc + +:build +:: set PKG_CONFIG_PATH=pkg-config +go build -ldflags %LDFLAGS% -tags %TAGS% -o .\cmd\fibratus\fibratus.exe .\cmd\fibratus +goto :EOF + +:test +%GOTEST% %PKGS% +goto :EOF + +:lint +%GOVET% +%GOLINT% %PKGS% +goto :EOF + +:fmt +%GOFMT% pkg cmd +goto :EOF + +:deps +go get -v -u golang.org/x/lint/golint +goto :EOF + +:rsrc +set RC_VER=%VERSION:.=,% +windres --define RC_VER=%RC_VER% --define VER=%VERSION% -i cmd\fibratus\fibratus.rc -O coff -o cmd\fibratus\fibratus.syso +goto :EOF + +:pkg +set RELEASE_DIR=.\build\package\release + +mkdir "%~dp0\%RELEASE_DIR%" +mkdir "%~dp0\%RELEASE_DIR%\Bin" +mkdir "%~dp0\%RELEASE_DIR%\Config" +mkdir "%~dp0\%RELEASE_DIR%\Python" + +copy /y ".\cmd\fibratus\fibratus.exe" "%RELEASE_DIR%\bin" +copy /y ".\configs\fibratus.yml" "%RELEASE_DIR%\config\fibratus.yml" +xcopy /s /f /y ".\filaments" "%RELEASE_DIR%\Filaments\*" + +echo Downloading Python %PYTHON_VER%... +powershell -Command "Invoke-WebRequest %PYTHON_URL% -OutFile %RELEASE_DIR%\python.zip" + +echo Extracting Python distribution... +powershell -Command "Expand-Archive %RELEASE_DIR%\python.zip -DestinationPath %RELEASE_DIR%\python" + +:: Bring in the pip +:: https://stackoverflow.com/questions/42666121/pip-with-embedded-python +powershell -Command "(Get-Content -path %RELEASE_DIR%\python\python*._pth -Raw) -replace '#import','import' | Set-Content -Path %RELEASE_DIR%\python\python*._pth" +echo Downloading get-pip.py... +powershell -Command "Invoke-WebRequest https://bootstrap.pypa.io/get-pip.py -OutFile %RELEASE_DIR%\get-pip.py" +%RELEASE_DIR%\python\python.exe %RELEASE_DIR%\get-pip.py + +:: Move Python DLLs and other dependencies to the same directory where the fibratus binary +:: is located to advise Windows on the DLL search path strategy. +move %RELEASE_DIR%\python\*.dll %RELEASE_DIR%\bin + +:: Download env var plugin: https://nsis.sourceforge.io/mediawiki/images/7/7f/EnVar_plugin.zip +FOR /F "usebackq" %%A IN ('%RELEASE_DIR%\bin\fibratus.exe') DO set /a SIZE=%%~zA / 1024 + +makensis /DVERSION=1.0.0 /DINSTALLSIZE=%SIZE% build/package/fibratus.nsi +goto :EOF + +:clean +rm cmd\fibratus\fibratus.exe +goto :EOF \ No newline at end of file diff --git a/pkg-config/python-37.pc b/pkg-config/python-37.pc new file mode 100644 index 000000000..a66bed835 --- /dev/null +++ b/pkg-config/python-37.pc @@ -0,0 +1,11 @@ +prefix=C:/Python37 +exec_prefix=${prefix} +libdir=${exec_prefix}/libs +includedir=${prefix}/include + +Name: Python +Description: Python library +Requires: +Version: 3.7 +Libs: -L${libdir} -lpython37 +Cflags: -I${includedir} -DMS_WIN64 \ No newline at end of file diff --git a/pkg/aggregator/aggregator.go b/pkg/aggregator/aggregator.go new file mode 100644 index 000000000..265de8707 --- /dev/null +++ b/pkg/aggregator/aggregator.go @@ -0,0 +1,187 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package aggregator + +import ( + "errors" + "expvar" + "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" + "github.com/rabbitstack/fibratus/pkg/alertsender" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/outputs" + log "github.com/sirupsen/logrus" + "time" + // initialize outputs + _ "github.com/rabbitstack/fibratus/pkg/outputs/amqp" + _ "github.com/rabbitstack/fibratus/pkg/outputs/console" + _ "github.com/rabbitstack/fibratus/pkg/outputs/elasticsearch" + _ "github.com/rabbitstack/fibratus/pkg/outputs/null" + // initialize alert senders + _ "github.com/rabbitstack/fibratus/pkg/alertsender/mail" + _ "github.com/rabbitstack/fibratus/pkg/alertsender/slack" + // initialize transformers + _ "github.com/rabbitstack/fibratus/pkg/aggregator/transformers/remove" + _ "github.com/rabbitstack/fibratus/pkg/aggregator/transformers/rename" + _ "github.com/rabbitstack/fibratus/pkg/aggregator/transformers/replace" + _ "github.com/rabbitstack/fibratus/pkg/aggregator/transformers/tags" + _ "github.com/rabbitstack/fibratus/pkg/aggregator/transformers/trim" +) + +var ( + // keventsDequeued counts the number of dequeued events + keventsDequeued = expvar.NewInt("kstream.kevents.dequeued") + // flushesCount computes the total count of aggregator flushes + flushesCount = expvar.NewInt("aggregator.flushes.count") + // batchEvents represents the overall number of processed batches + batchEvents = expvar.NewInt("aggregator.batch.events") + // transformerErrors is the count of transformers errors occurred during event processing + transformerErrors = expvar.NewMap("aggregator.transformer.errors") + /// keventErrors is the number of kernel event errors + keventErrors = expvar.NewInt("aggregator.kevent.errors") +) + +// BufferedAggregator collects events from the inbound channel and produces batches on regular intervals. The batches +// are pushed to the work queue from which load-balanced configured workers consume the batches and publish to the outputs. +type BufferedAggregator struct { + kevtsc chan *kevent.Kevent + errsc chan error + stop chan struct{} + flusher *time.Ticker + // queue of inbound kernel events + kevts []*kevent.Kevent + // work queue that forwarder passes to outputs + wq queue + submitter *submitter + transforms []transformers.Transformer + c Config +} + +// NewBuffered creates a new instance of the event aggregator. +func NewBuffered( + kevents chan *kevent.Kevent, + errs chan error, + config Config, + outputConfig outputs.Config, + transformerConfigs []transformers.Config, + alertsenderConfigs []alertsender.Config, +) (*BufferedAggregator, error) { + + flushInterval := config.FlushPeriod + if flushInterval < time.Millisecond*250 { + flushInterval = time.Millisecond * 250 + } + agg := &BufferedAggregator{ + kevtsc: kevents, + kevts: make([]*kevent.Kevent, 0), + errsc: errs, + stop: make(chan struct{}, 1), + flusher: time.NewTicker(flushInterval), + wq: make(chan *kevent.Batch, 0), + c: config, + } + + var err error + agg.submitter, err = newSubmitter(agg.wq, outputConfig) + if err != nil { + return nil, err + } + agg.transforms, err = transformers.LoadAll(transformerConfigs) + if err != nil { + return nil, err + } + + err = alertsender.LoadAll(alertsenderConfigs) + if err != nil { + return nil, err + } + + go agg.run() + + return agg, nil +} + +func (agg *BufferedAggregator) Stop() error { + agg.flusher.Stop() + // flush enqueued events + b := kevent.NewBatch(agg.kevts...) + if b.Len() > 0 { + done := make(chan struct{}, 1) + go func() { + agg.wq <- b + done <- struct{}{} + }() + + select { + case <-done: + close(agg.wq) + case <-time.After(agg.c.FlushTimeout): + return errors.New("fail to flush events after stop timed out") + } + } + + agg.stop <- struct{}{} + + return nil +} + +// run starts the aggregator loop. The aggregator receives kernel event stream from the upstream channel, buffers +// them to intermediate queue and dispatches batches to downstream worker queue. +func (agg *BufferedAggregator) run() { + for { + select { + case <-agg.stop: + err := agg.submitter.shutdown() + if err != nil { + log.Warnf("fail to gracefully close the submitter: %v", err) + } + return + case <-agg.flusher.C: + if len(agg.kevts) == 0 { + continue + } + b := kevent.NewBatch(agg.kevts...) + l := b.Len() + batchEvents.Add(l) + // push the batch to the work queue + if l > 0 { + agg.wq <- b + } + flushesCount.Add(1) + // clear the queue + agg.kevts = nil + case kevt := <-agg.kevtsc: + for _, transformer := range agg.transforms { + if transformer == nil { + continue + } + err := transformer.Transform(kevt) + if err != nil { + log.Warnf("transformer error occurred: %v", err) + transformerErrors.Add(err.Error(), 1) + } + } + // push the event to the queue + agg.kevts = append(agg.kevts, kevt) + keventsDequeued.Add(1) + case err := <-agg.errsc: + keventErrors.Add(1) + log.Errorf("aggregator dispatch failure: %v", err) + } + } +} diff --git a/pkg/aggregator/aggregator_test.go b/pkg/aggregator/aggregator_test.go new file mode 100644 index 000000000..60cc7d38e --- /dev/null +++ b/pkg/aggregator/aggregator_test.go @@ -0,0 +1,76 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package aggregator + +import ( + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/outputs" + "github.com/rabbitstack/fibratus/pkg/outputs/console" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "net" + "testing" + "time" +) + +func TestNewBufferedAggregator(t *testing.T) { + keventsc := make(chan *kevent.Kevent, 4) + errsc := make(chan error, 1) + agg, err := NewBuffered(keventsc, errsc, Config{FlushPeriod: time.Millisecond * 200}, outputs.Config{Type: outputs.Console, Output: console.Config{Format: "pretty"}}, nil, nil) + require.NoError(t, err) + defer agg.Stop() + + for i := 0; i < 4; i++ { + kevt := &kevent.Kevent{ + Type: ktypes.SendTCPv4, + Tid: 2484, + PID: 859, + Kparams: kevent.Kparams{ + kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, + kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, + kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv4, Value: net.ParseIP("127.0.0.1")}, + kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("216.58.201.174")}, + }, + } + keventsc <- kevt + } + <-time.After(time.Millisecond * 255) + assert.Equal(t, int64(4), batchEvents.Value()) + + for i := 0; i < 2; i++ { + kevt := &kevent.Kevent{ + Type: ktypes.SendTCPv4, + Tid: 2484, + PID: 859, + Seq: uint64(i), + Kparams: kevent.Kparams{ + kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, + kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, + kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv4, Value: net.ParseIP("127.0.0.1")}, + kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("216.58.201.174")}, + }, + } + keventsc <- kevt + } + <-time.After(time.Millisecond * 260) + assert.Equal(t, int64(6), batchEvents.Value()) + assert.Equal(t, int64(2), flushesCount.Value()) +} diff --git a/pkg/aggregator/config.go b/pkg/aggregator/config.go new file mode 100644 index 000000000..ac2c7b9ff --- /dev/null +++ b/pkg/aggregator/config.go @@ -0,0 +1,50 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package aggregator + +import ( + "github.com/spf13/pflag" + "github.com/spf13/viper" + "time" +) + +const ( + flushPeriod = "aggregator.flush-period" + flushTimeout = "aggregator.flush-timeout" +) + +// Config contains aggregator-specific configuration tweaks. +type Config struct { + // FlushPeriod determines the period for flushing batches to outputs. + FlushPeriod time.Duration `json:"aggregator.flush-period" yaml:"aggregator.flush-period"` + // FlushTimeout represents the max time to wait before announcing failed flushing of enqueued events + FlushTimeout time.Duration `json:"aggregator.flush-timeout" yaml:"aggregator.flush-timeout"` +} + +// AddFlags registers persistent aggregator flags. +func AddFlags(flags *pflag.FlagSet) { + flags.Duration(flushPeriod, time.Millisecond*200, "Determines the period for flushing batches to outputs") + flags.Duration(flushTimeout, time.Second*4, "Represents the max time to wait before announcing failed flushing of enqueued events on aggregator shutdown") +} + +// InitFromViper initializes aggregator flags from viper. +func (c *Config) InitFromViper(v *viper.Viper) { + c.FlushPeriod = v.GetDuration(flushPeriod) + c.FlushTimeout = v.GetDuration(flushTimeout) +} diff --git a/pkg/aggregator/submitter.go b/pkg/aggregator/submitter.go new file mode 100644 index 000000000..528f7c20d --- /dev/null +++ b/pkg/aggregator/submitter.go @@ -0,0 +1,57 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package aggregator + +import ( + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/outputs" +) + +// queue defines the type alias for the batch worker queue +type queue chan *kevent.Batch + +// submitter initializes a group of load balanced output producers. +type submitter struct { + wq queue + workers []*worker +} + +func newSubmitter(wq queue, outputConfig outputs.Config) (*submitter, error) { + output, err := outputs.Load(outputConfig.Type, outputConfig) + if err != nil { + return nil, err + } + clients := output.Clients + workers := make([]*worker, len(clients)) + + for i, client := range clients { + workers[i] = initWorker(wq, client) + } + + return &submitter{wq: wq, workers: workers}, nil +} + +func (s *submitter) shutdown() error { + for _, w := range s.workers { + if err := w.close(); err != nil { + return err + } + } + return nil +} diff --git a/pkg/aggregator/transformers/config.go b/pkg/aggregator/transformers/config.go new file mode 100644 index 000000000..41f9d60a3 --- /dev/null +++ b/pkg/aggregator/transformers/config.go @@ -0,0 +1,30 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package transformers + +import "fmt" + +// ErrInvalidConfig signals an invalid configuration input +var ErrInvalidConfig = func(name Type) error { return fmt.Errorf("invalid config for %q transformer", name) } + +// Config acts as a container for the transformer configuration structures. +type Config struct { + Type Type + Transformer interface{} +} diff --git a/pkg/aggregator/transformers/remove/config.go b/pkg/aggregator/transformers/remove/config.go new file mode 100644 index 000000000..1362a983f --- /dev/null +++ b/pkg/aggregator/transformers/remove/config.go @@ -0,0 +1,40 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package remove + +import "github.com/spf13/pflag" + +const ( + kpars = "transformers.remove.kparams" + enabled = "transformers.remove.enabled" +) + +// Config stores the configuration for the remove transformer. +type Config struct { + // Kparams is the list of parameters that are dropped from the event. + Kparams []string `mapstructure:"kparams"` + // Enabled indicates whether this transformer is enabled + Enabled bool `mapstructure:"enabled"` +} + +// AddFlags registers persistent flags. +func AddFlags(flags *pflag.FlagSet) { + flags.StringSlice(kpars, []string{}, "A list of comma-separated parameters that will be removed from the event") + flags.Bool(enabled, false, "Indicates if remove transformer is enabled") +} diff --git a/pkg/aggregator/transformers/remove/remove.go b/pkg/aggregator/transformers/remove/remove.go new file mode 100644 index 000000000..61e4da16d --- /dev/null +++ b/pkg/aggregator/transformers/remove/remove.go @@ -0,0 +1,52 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package remove + +import ( + "expvar" + "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" + "github.com/rabbitstack/fibratus/pkg/kevent" +) + +var removedCount = expvar.NewInt("transformers.removed.params") + +//remove transformer deletes kparams that are given in the list. +type remove struct { + c Config +} + +func init() { + transformers.Register(transformers.Remove, initRemoveTransformer) +} + +func initRemoveTransformer(config transformers.Config) (transformers.Transformer, error) { + cfg, ok := config.Transformer.(Config) + if !ok { + return nil, transformers.ErrInvalidConfig(transformers.Remove) + } + return &remove{c: cfg}, nil +} + +func (r remove) Transform(kevt *kevent.Kevent) error { + for _, kpar := range r.c.Kparams { + delete(kevt.Kparams, kpar) + removedCount.Add(1) + } + return nil +} diff --git a/pkg/aggregator/transformers/remove/remove_test.go b/pkg/aggregator/transformers/remove/remove_test.go new file mode 100644 index 000000000..75dfbb4ec --- /dev/null +++ b/pkg/aggregator/transformers/remove/remove_test.go @@ -0,0 +1,53 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package remove + +import ( + "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "net" + "testing" +) + +func TestTransform(t *testing.T) { + kevt := &kevent.Kevent{ + Type: ktypes.SendTCPv4, + Tid: 2484, + PID: 859, + Kparams: kevent.Kparams{ + kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, + kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, + kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv4, Value: net.ParseIP("127.0.0.1")}, + kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("216.58.201.174")}, + }, + } + assert.Len(t, kevt.Kparams, 4) + + transf, err := transformers.Load(transformers.Config{Type: transformers.Remove, Transformer: Config{Kparams: []string{"dip", "sport", "foo"}}}) + require.NoError(t, err) + err = transf.Transform(kevt) + + require.NoError(t, err) + + assert.Len(t, kevt.Kparams, 2) +} diff --git a/pkg/aggregator/transformers/rename/config.go b/pkg/aggregator/transformers/rename/config.go new file mode 100644 index 000000000..b0b742dee --- /dev/null +++ b/pkg/aggregator/transformers/rename/config.go @@ -0,0 +1,44 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package rename + +import "github.com/spf13/pflag" + +const ( + enabled = "transformers.rename.enabled" +) + +// Rename describes the configuration for the old/new parameter name. +type Rename struct { + Old string `mapstructure:"old"` + New string `mapstructure:"new"` +} + +// Config stores the configuration of the rename transformer. +type Config struct { + // Kparams is the list of parameters that will be renamed. + Kparams []Rename + // Enabled indicates whether this transformer is enabled. + Enabled bool +} + +// AddFlags registers persistent flags. +func AddFlags(flags *pflag.FlagSet) { + flags.Bool(enabled, false, "Indicates if the rename transformer is enabled") +} diff --git a/pkg/aggregator/transformers/rename/rename.go b/pkg/aggregator/transformers/rename/rename.go new file mode 100644 index 000000000..7f89cb977 --- /dev/null +++ b/pkg/aggregator/transformers/rename/rename.go @@ -0,0 +1,54 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package rename + +import ( + "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" + "github.com/rabbitstack/fibratus/pkg/kevent" +) + +// rename as it name implies, it renames a sequence of kparams to their new names. +type rename struct { + c Config +} + +func init() { + transformers.Register(transformers.Rename, initRenameTransformer) +} + +func initRenameTransformer(config transformers.Config) (transformers.Transformer, error) { + cfg, ok := config.Transformer.(Config) + if !ok { + return nil, transformers.ErrInvalidConfig(transformers.Rename) + } + return &rename{c: cfg}, nil +} + +func (r rename) Transform(kevt *kevent.Kevent) error { + for _, par := range r.c.Kparams { + kpar, ok := kevt.Kparams[par.Old] + if !ok { + continue + } + kevt.Kparams.Remove(par.Old) + kpar.Name = par.New + kevt.Kparams[par.New] = kpar + } + return nil +} diff --git a/pkg/aggregator/transformers/rename/rename_test.go b/pkg/aggregator/transformers/rename/rename_test.go new file mode 100644 index 000000000..ff74f4374 --- /dev/null +++ b/pkg/aggregator/transformers/rename/rename_test.go @@ -0,0 +1,56 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package rename + +import ( + "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "net" + "testing" +) + +func TestTransform(t *testing.T) { + kevt := &kevent.Kevent{ + Type: ktypes.SendTCPv4, + Tid: 2484, + PID: 859, + Kparams: kevent.Kparams{ + kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, + kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, + kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv4, Value: net.ParseIP("127.0.0.1")}, + kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("216.58.201.174")}, + }, + Metadata: make(map[string]string), + } + + transf, err := transformers.Load(transformers.Config{Type: transformers.Rename, Transformer: Config{Kparams: []Rename{{Old: "dport", New: "dstport"}, {Old: "sip", New: "srcip"}}}}) + require.NoError(t, err) + + require.NoError(t, transf.Transform(kevt)) + + assert.True(t, kevt.Kparams.Contains("dstport")) + assert.False(t, kevt.Kparams.Contains("dport")) + assert.True(t, kevt.Kparams.Contains("srcip")) + assert.False(t, kevt.Kparams.Contains("sip")) + +} diff --git a/pkg/aggregator/transformers/replace/config.go b/pkg/aggregator/transformers/replace/config.go new file mode 100644 index 000000000..ea8c65cfd --- /dev/null +++ b/pkg/aggregator/transformers/replace/config.go @@ -0,0 +1,45 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package replace + +import "github.com/spf13/pflag" + +const ( + enabled = "transformers.replace.enabled" +) + +// Config stores the configuration for the replace transformer +type Config struct { + // Replacements describes a list of replacements that are applied on the kparam. + Replacements []Replacement `mapstructure:"replacements"` + // Enabled indicates whether this transformer is enabled + Enabled bool `mapstructure:"enabled"` +} + +// Replacement defines the string replacement config for a specific kparam. +type Replacement struct { + Kpar string `mapstructure:"kparam"` + Old string `mapstructure:"old"` + New string `mapstructure:"new"` +} + +// AddFlags registers persistent flags. +func AddFlags(flags *pflag.FlagSet) { + flags.Bool(enabled, false, "Indicates if the replace transformer is enabled") +} diff --git a/pkg/aggregator/transformers/replace/replace.go b/pkg/aggregator/transformers/replace/replace.go new file mode 100644 index 000000000..b899374d9 --- /dev/null +++ b/pkg/aggregator/transformers/replace/replace.go @@ -0,0 +1,68 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package replace + +import ( + "expvar" + "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "strings" +) + +var replaceCount = expvar.NewInt("transformers.replaced.params") + +// replace applies string substitutions in kpar values. +type replace struct { + c Config +} + +func init() { + transformers.Register(transformers.Replace, initReplaceTransformer) +} + +func initReplaceTransformer(config transformers.Config) (transformers.Transformer, error) { + cfg, ok := config.Transformer.(Config) + if !ok { + return nil, transformers.ErrInvalidConfig(transformers.Replace) + } + return &replace{c: cfg}, nil +} + +func (r replace) Transform(kevt *kevent.Kevent) error { + for _, repl := range r.c.Replacements { + kpar := kevt.Kparams.Find(repl.Kpar) + + if kpar == nil { + continue + } + if kpar.Type != kparams.AnsiString && kpar.Type != kparams.UnicodeString { + continue + } + + val, ok := kpar.Value.(string) + if !ok { + continue + } + kpar.Value = strings.ReplaceAll(val, repl.Old, repl.New) + + replaceCount.Add(1) + } + return nil +} diff --git a/pkg/aggregator/transformers/replace/replace_test.go b/pkg/aggregator/transformers/replace/replace_test.go new file mode 100644 index 000000000..785eb21db --- /dev/null +++ b/pkg/aggregator/transformers/replace/replace_test.go @@ -0,0 +1,50 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package replace + +import ( + "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestTransform(t *testing.T) { + kevt := &kevent.Kevent{ + Type: ktypes.RegCreateKey, + Tid: 2484, + PID: 859, + Kparams: kevent.Kparams{ + kparams.RegKeyName: {Name: kparams.RegKeyName, Type: kparams.UnicodeString, Value: `HKEY_LOCAL_MACHINE\SYSTEM\Setup\Pid`}, + kparams.RegKeyHandle: {Name: kparams.RegKeyHandle, Type: kparams.HexInt64, Value: kparams.NewHex(uint64(18446666033449935464))}, + }, + } + + transf, err := transformers.Load(transformers.Config{Type: transformers.Replace, Transformer: Config{Replacements: []Replacement{{Kpar: "key_name", Old: "HKEY_LOCAL_MACHINE", New: "HKLM"}}}}) + require.NoError(t, err) + + require.NoError(t, transf.Transform(kevt)) + + keyName, _ := kevt.Kparams.GetString(kparams.RegKeyName) + + assert.Equal(t, `HKLM\SYSTEM\Setup\Pid`, keyName) +} diff --git a/pkg/aggregator/transformers/tags/config.go b/pkg/aggregator/transformers/tags/config.go new file mode 100644 index 000000000..b520ae531 --- /dev/null +++ b/pkg/aggregator/transformers/tags/config.go @@ -0,0 +1,46 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package tags + +import ( + "github.com/spf13/pflag" +) + +const ( + enabled = "transformers.tags.enabled" +) + +// Tag represents a distinct tag with its key and value attached. +type Tag struct { + Key string `mapstructure:"key"` + Value string `mapstructure:"value"` +} + +// Config stores the configuration for the tags transformer +type Config struct { + // Tags is the sequence of key/value pairs that are added to the event + Tags []Tag `mapstructure:"tags"` + // Enabled indicates whether this transformer is enabled + Enabled bool `mapstructure:"enabled"` +} + +// AddFlags registers persistent flags. +func AddFlags(flags *pflag.FlagSet) { + flags.Bool(enabled, false, "Indicates if the tags transformer is enabled") +} diff --git a/pkg/aggregator/transformers/tags/tags.go b/pkg/aggregator/transformers/tags/tags.go new file mode 100644 index 000000000..bc66391e3 --- /dev/null +++ b/pkg/aggregator/transformers/tags/tags.go @@ -0,0 +1,68 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package tags + +import ( + "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" + "github.com/rabbitstack/fibratus/pkg/kevent" + "os" + "strings" +) + +// tags transformer appends tags to the event's metadata. It is capable of adding literal values as well as +// tags that are stored in environment variables. +type tags struct { + tags map[string]string +} + +func init() { + transformers.Register(transformers.Tags, initTagsTransformer) +} + +func initTagsTransformer(config transformers.Config) (transformers.Transformer, error) { + cfg, ok := config.Transformer.(Config) + if !ok { + return nil, transformers.ErrInvalidConfig(transformers.Tags) + } + + ktags := make(map[string]string) + + for _, tag := range cfg.Tags { + // if the value is enclosed within % symbols this means + // we have to expand it from the environ variable + key, val := tag.Key, tag.Value + if len(val) == 0 { + continue + } + if val[0] == '%' && val[len(val)-1] == '%' { + ktags[key] = os.Getenv(strings.ReplaceAll(val, "%", "")) + continue + } + ktags[key] = val + } + + return &tags{tags: ktags}, nil +} + +func (t tags) Transform(kevt *kevent.Kevent) error { + for k, v := range t.tags { + kevt.AddMeta(k, v) + } + return nil +} diff --git a/pkg/aggregator/transformers/tags/tags_test.go b/pkg/aggregator/transformers/tags/tags_test.go new file mode 100644 index 000000000..9a1077a9e --- /dev/null +++ b/pkg/aggregator/transformers/tags/tags_test.go @@ -0,0 +1,58 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package tags + +import ( + "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "net" + "os" + "testing" +) + +func TestTransform(t *testing.T) { + kevt := &kevent.Kevent{ + Type: ktypes.SendTCPv4, + Tid: 2484, + PID: 859, + Kparams: kevent.Kparams{ + kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, + kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, + kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv4, Value: net.ParseIP("127.0.0.1")}, + kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("216.58.201.174")}, + }, + Metadata: make(map[string]string), + } + require.NoError(t, os.Setenv("NODENAME", "archbunny")) + transf, err := transformers.Load(transformers.Config{Type: transformers.Tags, Transformer: Config{Tags: []Tag{{Key: "env", Value: "staging"}, {Key: "zone", Value: "dmz"}, {Key: "node", Value: "%NODENAME%"}}}}) + require.NoError(t, err) + + require.NoError(t, transf.Transform(kevt)) + + require.Len(t, kevt.Metadata, 3) + require.Contains(t, kevt.Metadata, "env") + + assert.Equal(t, "dmz", kevt.Metadata["zone"]) + + assert.Equal(t, "archbunny", kevt.Metadata["node"]) +} diff --git a/pkg/aggregator/transformers/transformer.go b/pkg/aggregator/transformers/transformer.go new file mode 100644 index 000000000..8235112d9 --- /dev/null +++ b/pkg/aggregator/transformers/transformer.go @@ -0,0 +1,99 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package transformers + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/kevent" +) + +var transformers = map[Type]Factory{} + +// Factory defines the function for transformer factories +type Factory func(config Config) (Transformer, error) + +// Type defines the alias for the transformer types +type Type uint8 + +const ( + // Remove represents the remove transformer type. This transformer deletes the given list of parameters from the event. + Remove Type = iota + // Rename represents the rename transformer type. It renames a sequence of kparam from old to new names. + Rename + // Replace represents the replace tranformer type. It applies string replacements on specific kparams. + Replace + // Trim represents the trim transformer type that that removes suffix/prefix from string kparams. + Trim + // Tags represents the tags transformer type. This transformer appends tags to the event's metadata. + Tags +) + +// String returns the type human-readable name. +func (typ Type) String() string { + switch typ { + case Remove: + return "remove" + case Rename: + return "rename" + case Replace: + return "replace" + case Trim: + return "trim" + case Tags: + return "tags" + default: + return "unknown" + } +} + +// Register registers a singleton instance of the provided transformer. +func Register(typ Type, factory Factory) { + if _, ok := transformers[typ]; ok { + panic(fmt.Sprintf("output %q is already registered", typ)) + } + transformers[typ] = factory +} + +// LoadAll loads all transformers from the configuration inputs. +func LoadAll(configs []Config) ([]Transformer, error) { + transformers := make([]Transformer, len(configs)) + for i, config := range configs { + transformer, err := Load(config) + if err != nil { + return nil, err + } + transformers[i] = transformer + } + return transformers, nil +} + +// Load loads a single transformer from the configuration. +func Load(config Config) (Transformer, error) { + typ := config.Type + factory := transformers[typ] + if factory == nil { + return nil, fmt.Errorf("%q transformer not availaible in the factory", typ) + } + return factory(config) +} + +// Transformer is the minimal interface all transformers have to satisfy. +type Transformer interface { + Transform(*kevent.Kevent) error +} diff --git a/pkg/aggregator/transformers/trim/config.go b/pkg/aggregator/transformers/trim/config.go new file mode 100644 index 000000000..60f56c6a4 --- /dev/null +++ b/pkg/aggregator/transformers/trim/config.go @@ -0,0 +1,46 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package trim + +import "github.com/spf13/pflag" + +const ( + enabled = "transformers.trim.enabled" +) + +// Trim defines the trim configuration for a single event parameter. +type Trim struct { + Name string `mapstructure:"kparam"` + Trim string `mapstructure:"trim"` +} + +// Config stores the configuration for the trim transformer. +type Config struct { + // Prefixes contains the mapping between distinct kparam names and the prefixes that will get trimmed from their values. + Prefixes []Trim `mapstructure:"prefixes"` + // Suffixes contains the mapping between distinct kparam names and the suffixes that will get trimmed from their values. + Suffixes []Trim `mapstructure:"suffixes"` + // Enabled determines whether trim transformer is enabled or disabled. + Enabled bool `mapstructure:"enabled"` +} + +// AddFlags registers persistent flags. +func AddFlags(flags *pflag.FlagSet) { + flags.Bool(enabled, false, "Indicates if the trim transformer is enabled") +} diff --git a/pkg/aggregator/transformers/trim/trim.go b/pkg/aggregator/transformers/trim/trim.go new file mode 100644 index 000000000..01a37de1b --- /dev/null +++ b/pkg/aggregator/transformers/trim/trim.go @@ -0,0 +1,74 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package trim + +import ( + "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "strings" +) + +// trim transformer trims suffixes/prefixes from kpar values. +type trim struct { + c Config +} + +func init() { + transformers.Register(transformers.Trim, initTrimTransformer) +} + +func initTrimTransformer(config transformers.Config) (transformers.Transformer, error) { + cfg, ok := config.Transformer.(Config) + if !ok { + return nil, transformers.ErrInvalidConfig(transformers.Trim) + } + return &trim{c: cfg}, nil +} + +func (r trim) Transform(kevt *kevent.Kevent) error { + for _, kpar := range kevt.Kparams { + if kpar.Type != kparams.AnsiString && kpar.Type != kparams.UnicodeString { + continue + } + // trim prefixes + for _, par := range r.c.Prefixes { + if kpar.Name != par.Name { + continue + } + s, ok := kpar.Value.(string) + if !ok { + continue + } + kpar.Value = strings.TrimPrefix(s, par.Trim) + } + // trim suffixes + for _, par := range r.c.Suffixes { + if kpar.Name != par.Name { + continue + } + s, ok := kpar.Value.(string) + if !ok { + continue + } + kpar.Value = strings.TrimSuffix(s, par.Trim) + } + } + return nil +} diff --git a/pkg/aggregator/transformers/trim/trim_test.go b/pkg/aggregator/transformers/trim/trim_test.go new file mode 100644 index 000000000..0216a0965 --- /dev/null +++ b/pkg/aggregator/transformers/trim/trim_test.go @@ -0,0 +1,67 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package trim + +import ( + "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestTransform(t *testing.T) { + kevt := &kevent.Kevent{ + Type: ktypes.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: kevent.Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "overwriteif"}, + kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, + kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + kparams.KstackLimit: {Name: kparams.KstackLimit, Type: kparams.HexInt8, Value: kparams.Hex("ff")}, + kparams.StartTime: {Name: kparams.StartTime, Type: kparams.Time, Value: time.Now()}, + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(1204)}, + }, + Metadata: map[string]string{"foo": "bar", "fooz": "barz"}, + } + + transf, err := transformers.Load(transformers.Config{Type: transformers.Trim, Transformer: Config{Prefixes: []Trim{{Name: "file_name", Trim: "\\Device"}}, Suffixes: []Trim{{Name: "operation", Trim: "if"}}}}) + require.NoError(t, err) + + require.NoError(t, transf.Transform(kevt)) + filename, _ := kevt.Kparams.GetString(kparams.FileName) + dispo, _ := kevt.Kparams.GetString(kparams.FileOperation) + + assert.Equal(t, "\\HarddiskVolume2\\Windows\\system32\\user32.dll", filename) + assert.Equal(t, "overwrite", dispo) + +} diff --git a/pkg/aggregator/worker.go b/pkg/aggregator/worker.go new file mode 100644 index 000000000..ca2f979cf --- /dev/null +++ b/pkg/aggregator/worker.go @@ -0,0 +1,70 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package aggregator + +import ( + "expvar" + "github.com/rabbitstack/fibratus/pkg/outputs" + log "github.com/sirupsen/logrus" + "time" +) + +// maxBackoff determines the maximum exponential backoff wait time before reconnecting the client +const maxBackoff = time.Minute + +var clientPublishErrors = expvar.NewInt("aggregator.worker.client.publish.errors") + +type worker struct { + qu queue + client outputs.Client + backoff time.Duration +} + +func initWorker(q queue, client outputs.Client) *worker { + w := &worker{qu: q, client: client, backoff: time.Second * 2} + go w.run() + return w +} + +func (w *worker) run() { + for { + err := w.client.Connect() + if err != nil { + // schedule an exponential backoff reconnect strategy for the client + w.backoff *= 2 + log.Warnf("fail to connect the client: %v. Reconnecting in %v...", err, w.backoff) + if w.backoff > maxBackoff { + w.backoff = maxBackoff + } + <-time.After(w.backoff) + continue + } + break + } + for batch := range w.qu { + if err := w.client.Publish(batch); err != nil { + clientPublishErrors.Add(1) + log.Warnf("couldn't publish batch to client: %v", err) + } + } +} + +func (w *worker) close() error { + return w.client.Close() +} diff --git a/pkg/aggregator/worker_test.go b/pkg/aggregator/worker_test.go new file mode 100644 index 000000000..26fd9b4c9 --- /dev/null +++ b/pkg/aggregator/worker_test.go @@ -0,0 +1,108 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package aggregator + +import ( + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +type httpClient struct { + url string + published int + expectedPublished int + wait chan struct{} +} + +func (c *httpClient) Connect() error { + _, err := http.Get(c.url + "/connect") + return err +} + +func (c *httpClient) Close() error { return nil } + +func (c *httpClient) Publish(b *kevent.Batch) error { + _, err := http.Post(c.url+"/publish", "application/json", nil) + if err != nil { + return err + } + c.published++ + if c.published == c.expectedPublished { + c.wait <- struct{}{} + } + return nil +} + +func TestRunWorker(t *testing.T) { + q := make(chan *kevent.Batch, 2) + q <- &kevent.Batch{} + q <- &kevent.Batch{} + + mux := http.NewServeMux() + mux.HandleFunc("/publish", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + srv := httptest.NewServer(mux) + defer srv.Close() + + client := &httpClient{url: srv.URL, wait: make(chan struct{}, 1), expectedPublished: 2} + + w := initWorker(q, client) + defer w.close() + + <-client.wait + + assert.Equal(t, 2, client.published) +} + +func TestConnectClientBackoff(t *testing.T) { + q := make(chan *kevent.Batch, 2) + q <- &kevent.Batch{} + q <- &kevent.Batch{} + + mux := http.NewServeMux() + mux.HandleFunc("/publish", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + mux.HandleFunc("/connect", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + srv := httptest.NewUnstartedServer(mux) + defer srv.Close() + + client := &httpClient{url: srv.URL, wait: make(chan struct{}, 1), expectedPublished: 2} + + go time.AfterFunc(time.Second*3, func() { + srv.Start() + client.url = srv.URL + }) + + w := initWorker(q, client) + defer w.close() + + <-client.wait + + assert.Equal(t, 2, client.published) +} diff --git a/pkg/alertsender/alert.go b/pkg/alertsender/alert.go new file mode 100644 index 000000000..95a623c62 --- /dev/null +++ b/pkg/alertsender/alert.go @@ -0,0 +1,83 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package alertsender + +import "fmt" + +// Severity is the type alias for alert's severity level. +type Severity uint8 + +const ( + // Normal designates alert's normal level + Normal Severity = iota + // Medium designates alert's medium level + Medium + // Critical designates alert's critical level + Critical +) + +// String returns severity human-friendly name. +func (s Severity) String() string { + switch s { + case Normal: + return "normal" + case Medium: + return "medium" + case Critical: + return "critical" + default: + return "unknown" + } +} + +// ParseSeverityFromString parses the severity from the string representation. +func ParseSeverityFromString(sever string) Severity { + switch sever { + case "normal", "Normal": + return Normal + case "medium", "Medium": + return Medium + case "critical", "Critical": + return Critical + default: + return Normal + } +} + +// Alert encapsulates the state of an alert. +type Alert struct { + // Title is the short title that summarizes the purpose of the alert. + Title string + // Text is the longer textual content that further explains what this alert is about. + Text string + // Tags contains a sequence of tags for categorizing the alerts. + Tags []string + // Severity determines the severity of this alert. + Severity Severity +} + +// String returns the alert string representation. +func (a Alert) String() string { + return fmt.Sprintf("Title: %s, Text: %s, Severity: %s, Tags: %v", a.Title, a.Text, a.Severity, a.Tags) +} + +// NewAlert builds a new alert. +func NewAlert(title, text string, tags []string, severity Severity) Alert { + return Alert{Title: title, Text: text, Tags: tags, Severity: severity} +} diff --git a/pkg/alertsender/config.go b/pkg/alertsender/config.go new file mode 100644 index 000000000..5052c4f6e --- /dev/null +++ b/pkg/alertsender/config.go @@ -0,0 +1,25 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package alertsender + +// Config is the container for the alert sender configuration structure. +type Config struct { + Type Type + Sender interface{} +} diff --git a/pkg/alertsender/mail/config.go b/pkg/alertsender/mail/config.go new file mode 100644 index 000000000..368c5778d --- /dev/null +++ b/pkg/alertsender/mail/config.go @@ -0,0 +1,60 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mail + +import "github.com/spf13/pflag" + +const ( + host = "alertsenders.mail.host" + port = "alertsenders.mail.port" + user = "alertsenders.mail.user" + pass = "alertsenders.mail.password" + from = "alertsenders.mail.from" + to = "alertsenders.mail.to" + enabled = "alertsenders.mail.enabled" +) + +// Config contains the configuration for the mail alert sender. +type Config struct { + // Host is the host of the SMTP server. + Host string `mapstructure:"host"` + // Port is the port of the SMTP server. + Port int `mapstructure:"port"` + // User specifies the user name when authenticating to the SMTP server. + User string `mapstructure:"user"` + // Pass specifies the password when authenticating to the SMTP server. + Pass string `mapstructure:"password"` + // From specifies the sender address. + From string `mapstructure:"from"` + // To specifies recipients that receive the alert. + To []string `mapstructure:"to"` + // Enabled indicates whether mail alert sender is enabled + Enabled bool `mapstructure:"enabled"` +} + +// AddFlags registers persistent flags. +func AddFlags(flags *pflag.FlagSet) { + flags.String(host, "", "Represents the host of the SMTP server") + flags.Int(port, 25, "Represents the port of the SMTP server") + flags.String(user, "", "Specifies the user name when authenticating to the SMTP server") + flags.String(pass, "", "Specifies the password when authenticating to the SMTP server") + flags.String(from, "", "Specifies the sender's address") + flags.StringSlice(to, []string{}, "Specifies all the recipients that'll receive the alert") + flags.Bool(enabled, false, "Indicates whether mail alert sender is enabled") +} diff --git a/pkg/alertsender/mail/mail.go b/pkg/alertsender/mail/mail.go new file mode 100644 index 000000000..acd46f120 --- /dev/null +++ b/pkg/alertsender/mail/mail.go @@ -0,0 +1,61 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package mail + +import ( + "github.com/rabbitstack/fibratus/pkg/alertsender" + "gopkg.in/gomail.v2" +) + +type mail struct { + dialer *gomail.Dialer + c Config +} + +func init() { + alertsender.Register(alertsender.Mail, makeSender) +} + +// makeSender constructs a new instance of the email alert sender. +func makeSender(config alertsender.Config) (alertsender.Sender, error) { + c, ok := config.Sender.(Config) + if !ok { + return nil, alertsender.ErrInvalidConfig(alertsender.Mail) + } + dialer := gomail.NewDialer(c.Host, c.Port, c.User, c.Pass) + return &mail{dialer: dialer, c: c}, nil +} + +func (s mail) Send(alert alertsender.Alert) error { + sender, err := s.dialer.Dial() + if err != nil { + return err + } + defer sender.Close() + return gomail.Send(sender, composeMessage(s.c.From, s.c.To, alert)) +} + +func composeMessage(from string, to []string, alert alertsender.Alert) *gomail.Message { + msg := gomail.NewMessage() + msg.SetHeader("From", from) + msg.SetHeader("To", to...) + msg.SetHeader("Subject", alert.Title) + msg.SetBody("text/plain", alert.Text) + return msg +} diff --git a/pkg/alertsender/sender.go b/pkg/alertsender/sender.go new file mode 100644 index 000000000..261f35bf3 --- /dev/null +++ b/pkg/alertsender/sender.go @@ -0,0 +1,122 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package alertsender + +import "fmt" + +// ErrInvalidConfig signals an invalid sender config +var ErrInvalidConfig = func(name Type) error { return fmt.Errorf("invalid config for %q sender", name) } + +var factories = map[Type]Factory{} +var alertsenders = map[Type]Sender{} + +// Factory defines the alias for the alert sender factory +type Factory func(config Config) (Sender, error) + +// Type defines the alias for the alert sender type +type Type uint8 + +const ( + // Mail designates mail alert sender + Mail Type = iota + // Slack designates Slack alert sender + Slack + // Noop is a noop alert sender. Useful for testing. + Noop + // None is the type for unknown alert sender + None +) + +// String returns the string representation of the alert sender type. +func (s Type) String() string { + switch s { + case Mail: + return "mail" + case Slack: + return "slack" + case Noop: + return "noop" + default: + return "none" + } +} + +// Sender is the minimal interface all alert senders have to implement. +type Sender interface { + // Send emits an alert. + Send(Alert) error +} + +// ToType converts the string representation of the alert sender to its corresponding type. +func ToType(s string) Type { + switch s { + case "mail": + return Mail + case "slack": + return Slack + case "noop": + return Noop + default: + return None + } +} + +// Register registers a new alert sender. +func Register(typ Type, factory Factory) { + if _, ok := factories[typ]; ok { + panic(fmt.Sprintf("%q alert sender is already registered", typ)) + } + factories[typ] = factory +} + +// Find locates the sender. +func Find(typ Type) Sender { + return alertsenders[typ] +} + +// FindAll returns all registered senders. +func FindAll() []Sender { + senders := make([]Sender, 0, len(alertsenders)) + for _, s := range alertsenders { + senders = append(senders, s) + } + return senders +} + +// Load loads an alert sender from the registry. +func Load(config Config) (Sender, error) { + typ := config.Type + factory := factories[typ] + if factory == nil { + return nil, fmt.Errorf("%q alert sender not availaible in the factory", typ) + } + return factory(config) +} + +// LoadAll loads all alert senders from the configuration inputs. +func LoadAll(configs []Config) error { + for _, config := range configs { + alertsender, err := Load(config) + if err != nil { + return fmt.Errorf("fail to load %q alertsender: %v", config.Type, err) + } + alertsenders[config.Type] = alertsender + } + return nil +} diff --git a/pkg/alertsender/slack/config.go b/pkg/alertsender/slack/config.go new file mode 100644 index 000000000..76cd4471d --- /dev/null +++ b/pkg/alertsender/slack/config.go @@ -0,0 +1,52 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package slack + +import "github.com/spf13/pflag" + +const ( + enabled = "alertsenders.slack.enabled" + url = "alertsenders.slack.url" + workspace = "alertsenders.slack.workspace" + channel = "alertsenders.slack.channel" + botemoji = "alertsenders.slack.emoji" +) + +// Config stores the settings that dictate the behaviour of the Slack alert sender. +type Config struct { + // URL represents the Webhook URL of the workspace where alerts will be dispatched. + URL string `mapstructure:"url"` + // Workspace designates the Slack workspace where alerts will be routed. + Workspace string `mapstructure:"workspace"` + // Channel is the slack channel in which to post alerts. + Channel string `mapstructure:"channel"` + // BotEmoji is the emoji icon for the Slack bot. + BotEmoji string `mapstructure:"emoji"` + // Enabled determines if Slack alert sender is enabled. + Enabled bool `mapstructure:"enabled"` +} + +// AddFlags registers persistent flags. +func AddFlags(flags *pflag.FlagSet) { + flags.Bool(enabled, false, "Determines whether Slack alert sender is enabled") + flags.String(url, "", "Represents the Webhook URL of the workspace where alerts will be dispatched") + flags.String(workspace, "", "Designates the Slack workspace where alerts will be routed") + flags.String(channel, "", "Represents the slack channel in which to post alerts") + flags.String(botemoji, "", "Represents the emoji icon for the Slack bot") +} diff --git a/pkg/alertsender/slack/slack.go b/pkg/alertsender/slack/slack.go new file mode 100644 index 000000000..a1a7849f1 --- /dev/null +++ b/pkg/alertsender/slack/slack.go @@ -0,0 +1,129 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package slack + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "github.com/rabbitstack/fibratus/pkg/alertsender" + "io/ioutil" + "net" + "net/http" + "time" +) + +const botName = "fibratus" + +type slack struct { + client *http.Client + config Config +} + +// attachment represents Slack attachment info +type attachment struct { + Fallback string `json:"fallback"` + Color string `json:"color"` + Text string `json:"text"` + Mdin []string `json:"mrkdwn_in"` +} + +func init() { + alertsender.Register(alertsender.Slack, makeSender) +} + +// makeSender constructs a new instance of the Slack alert sender. +func makeSender(config alertsender.Config) (alertsender.Sender, error) { + c, ok := config.Sender.(Config) + if !ok { + return nil, alertsender.ErrInvalidConfig(alertsender.Slack) + + } + client := &http.Client{ + Transport: &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: 10 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + }, + } + return &slack{config: c, client: client}, nil +} + +func (s slack) Send(alert alertsender.Alert) error { + var color string + switch alert.Severity { + case alertsender.Medium: + color = "warning" + case alertsender.Critical: + color = "danger" + default: + color = "good" + } + + attach := attachment{ + Fallback: alert.Text, + Text: alert.Text, + Color: color, + Mdin: []string{"text"}, + } + + params := make(map[string]interface{}) + params["as_user"] = false + params["channel"] = s.config.Channel + params["text"] = "" + params["attachments"] = []attachment{attach} + params["username"] = botName + if s.config.BotEmoji != "" { + params["icon_emoji"] = s.config.BotEmoji + } + + var body bytes.Buffer + enc := json.NewEncoder(&body) + err := enc.Encode(params) + if err != nil { + return nil + } + + resp, err := s.client.Post(s.config.URL, "application/json", &body) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + type response struct { + Error string `json:"error"` + } + r := &response{Error: fmt.Sprintf("failed to send alert to Slack. code: %d content: %s", resp.StatusCode, string(body))} + b := bytes.NewReader(body) + dec := json.NewDecoder(b) + if err := dec.Decode(r); err != nil { + return err + } + return errors.New(r.Error) + } + return nil +} diff --git a/pkg/api/handler/config.go b/pkg/api/handler/config.go new file mode 100644 index 000000000..da305f58e --- /dev/null +++ b/pkg/api/handler/config.go @@ -0,0 +1,33 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package handler + +import ( + "github.com/rabbitstack/fibratus/pkg/config" + "net/http" +) + +// Config is the handler the serves the current configuration state as pretty-formatted text. +func Config(c *config.Config) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if _, err := w.Write([]byte(c.Print())); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + }) +} diff --git a/pkg/api/listener.go b/pkg/api/listener.go new file mode 100644 index 000000000..dab6d74f6 --- /dev/null +++ b/pkg/api/listener.go @@ -0,0 +1,65 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package api + +import ( + "context" + "fmt" + "github.com/Microsoft/go-winio" + "net" + "strings" +) + +// MakePipeListener produces a new listener for receiving requests over a named pipe. +func MakePipeListener(pipePath, descriptor string) (net.Listener, error) { + npipe := transformPipePath(pipePath) + l, err := winio.ListenPipe(npipe, &winio.PipeConfig{SecurityDescriptor: descriptor}) + if err != nil { + return nil, fmt.Errorf("fail to listen on the %q pipe: %v", pipePath, err) + } + return l, nil +} + +// makeTCPListener produces a new listener for receiving requests over TCP. +func makeTCPListener(addr string) (net.Listener, error) { + return net.Listen("tcp", addr) +} + +// DialPipe creates a dialer to be used with the http.Client to connect to a named pipe. +func DialPipe(pipePath string) func(context.Context, string, string) (net.Conn, error) { + npipe := transformPipePath(pipePath) + return func(ctx context.Context, _, _ string) (net.Conn, error) { + return winio.DialPipeContext(ctx, npipe) + } +} + +// transformPipePath takes an input type name defined as a URI like `npipe:///hello` and transform it into +// `\\.\pipe\hello`. Borrowed from https://github.com/elastic/beats/blob/master/libbeat/api/npipe/listener_windows.go +func transformPipePath(name string) string { + if strings.HasPrefix(name, "npipe:///") { + path := strings.TrimPrefix(name, "npipe:///") + return `\\.\pipe\` + path + } + + if strings.HasPrefix(name, `\\.\pipe\`) { + return name + } + + return name +} diff --git a/pkg/api/server.go b/pkg/api/server.go new file mode 100644 index 000000000..ecd581e0e --- /dev/null +++ b/pkg/api/server.go @@ -0,0 +1,89 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package api + +import ( + "expvar" + "fmt" + "github.com/rabbitstack/fibratus/pkg/api/handler" + "github.com/rabbitstack/fibratus/pkg/config" + "net" + "net/http" + "net/http/pprof" + "os/user" + "runtime/debug" + "strings" + // register expvar stats + _ "expvar" + // register pprof handlers + _ "net/http/pprof" +) + +var listener net.Listener + +// StartServer starts the HTTP server with the specified configuration. +func StartServer(c *config.Config) error { + var err error + apiConfig := c.API + if strings.HasPrefix(apiConfig.Transport, `npipe:///`) { + usr, err := user.Current() + if err != nil { + return fmt.Errorf("failed to retrieve the current user: %v", err) + } + // Named pipe security and access rights. + // We create the pipe and the specific users should only be able to write to it. + // See docs: https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipe-security-and-access-rights + // String definition: https://docs.microsoft.com/en-us/windows/win32/secauthz/ace-strings + // Give generic read/write access to the specified user. + descriptor := "D:P(A;;GA;;;" + usr.Uid + ")" + listener, err = MakePipeListener(apiConfig.Transport, descriptor) + } else { + listener, err = makeTCPListener(apiConfig.Transport) + } + if err != nil { + return err + } + + mux := http.NewServeMux() + mux.Handle("/config", handler.Config(c)) + mux.Handle("/debug/vars", expvar.Handler()) + + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/freemem", func(writer http.ResponseWriter, request *http.Request) { + debug.FreeOSMemory() + }) + + srv := &http.Server{ + //WriteTimeout: apiConfig.Timeout, + Handler: mux, + } + + go srv.Serve(listener) + + return nil +} + +// CloseServer shutdowns the server by stopping the listener. +func CloseServer() error { + if listener != nil { + return listener.Close() + } + return nil +} diff --git a/pkg/config/_fixtures/fibratus.json b/pkg/config/_fixtures/fibratus.json new file mode 100644 index 000000000..56bd02b44 --- /dev/null +++ b/pkg/config/_fixtures/fibratus.json @@ -0,0 +1,53 @@ +{ + "aggregator": { + "flush-period": "230ms", + "flush-timeout": "8s" + }, + + "alertsenders": { + "mail": { + "enabled": false + }, + + "slack": { + "enabled": false + } + }, + + "api": { + "transport": "localhost:8090", + "timeout": "5s" + }, + + "debug-privilege": true, + + "filament": { + "name": "", + "flush-period": "200ms" + }, + + "handle": { + "init-snapshot": true + }, + + "kevent": { + + }, + + "kcap": { + + }, + + "kstream": {}, + "logging": {}, + "output": { + "console": { + "enabled": true, + "format": "pretty", + "kv-delimiter": "->" + } + }, + "pe": {}, + "transformers": {}, + "yara": {} +} \ No newline at end of file diff --git a/pkg/config/_fixtures/fibratus.yml b/pkg/config/_fixtures/fibratus.yml new file mode 100644 index 000000000..32447be67 --- /dev/null +++ b/pkg/config/_fixtures/fibratus.yml @@ -0,0 +1,230 @@ +###################### Fibratus Configuration File ##################################### + +# =============================== Aggregator ========================================== + +# Aggregator is responsible for creating kernel event batches, applying transformers to each event +# present in the batch, and forwarding those batches to the output sinks. +aggregator: + # Determines the flush period that triggers the flushing of the kernel event batches to output sinks. + flush-period: 230ms + + # Represents the max time to wait before announcing failed flushing of enqueued events when fibratus + # is stopped. + flush-timeout: 8s + +# =============================== Alert senders ======================================== + +# Alert senders deal with emitting alerts via different channels. +alertsenders: + # Mail sender transports the alerts via SMTP protocol. + mail: + # Enables/disables mail alert sender. + enabled: true + + # Represents the host of the SMTP server. + host: smtp.gmail.com + + # Represents the port of the SMTP server. + port: 587 + + # Specifies the user name when authenticating to the SMTP server. + user: bunny + + # Specifies the password when authenticating to the SMTP server. + password: changeit + + # Specifies the sender's address. + from: bunny@gmail.com + + # Specifies all the recipients that'll receive the alert. + to: + - bunny@gmail.com + - rabbit@gmail.com + - cuniculus@gmail.com + + # Slack sender transports the alerts to the Slack workspace. + slack: + # Enables/disables Slack alert sender. + enabled: true + + # Represents the Webhook URL of the workspace where alerts will be dispatched. + url: https://fibratus/232sfghagjhfasr + + # Designates the Slack workspace where alerts will be routed. + workspace: fibratus + + # Is the slack channel in which to post alerts. + channel: fibratus + + # Represents the emoji icon surrounded in ':' characters for the Slack bot. + #emoji: "" + +# =============================== API ================================================== + +# Settings that influence the behaviour of the HTTP server that exposes a number of endpoints such as +# expvar metrics, internal state, and so on. +api: + # Specifies the underlying transport protocol for the API HTTP server. The transport can either be the + # named pipe or TCP socket. Default is named pipe but you can override it to expose theAPI server on + # TCP address, e.g. 192.168.1.32:8084. + transport: npipe:///fibratus + + # Represents the timeout interval for the HTTP server responses. + timeout: 5s + +# =============================== General ============================================== + +# Indicates whether debug privilege is set in Fibratus process' token. Enabling this security policy allows +# Fibratus to obtain handles of protected processes for the purpose of querying the Process Environment Block +# regions. +debug-privilege: true + +# =============================== Filament ============================================= + +# Filaments are lightweight Python scriplets that are executed on top of the kernel event stream. You can easily +# extend Fibratus with custom features that is encapsulated in filaments. This section controls the behaviour of +# the filament engine. +filament: + # Specifies the name of the filament that is executed with the run command. + name: top_netio + + # The directory where all filaments are located. By default, they are stored within the fibratus program + # files directory. + path: $(PROGRAMFILES)/fibratus/filaments + + flush-period: 300ms + +# =============================== Handle =============================================== + +# Indicates whether initial handle snapshot is taken. +handle: + init-snapshot: true + +# =============================== Kcap ================================================= + +kcap: + file: "" + +# =============================== Kevent =============================================== + +kevent: + serialize-threads: false + serialize-images: false + serialize-handles: false + serialize-pe: false + +# =============================== Kstream ============================================== + +kstream: + max-buffers: 2 + min-buffers: 1 + flush-interval: 1s + blacklist: + events: + - CreateThread + - CreateHandle + - CloseHandle + images: + - System + + +# =============================== Logging ================================================ + +logging: + level: info + # max-age: + # max-backups: + # max-size: + # formatter: + # path: + # log-stdout: false + + +# =============================== Output ================================================ + +output: + console: + enabled: false + format: json + template: "" + + + elasticsearch: + enabled: false + servers: + - http://localhost:9200 + timeout: 5s + + amqp: + enabled: true + url: amqp://localhost:5672 + timeout: 5s + exchange-type: topic + routing-key: fibratus + vhost: / + +# =============================== Portable Executable (PE) ============================= + +pe: + enabled: false + read-resources: true + read-symbols: true + read-sections: false + +# =============================== Transformers ========================================= + +transformers: + remove: + enabled: true + kparams: + - disposition + rename: + enabled: true + kparams: + - old: "a" + new: "b" + - old: "c" + new: "d" + replace: + enabled: false + replacements: + - kparam: key_name + old: HKEY_CURRENT_USER + new: HCU + tags: + enabled: false + tags: + - key: foo + value: bar + trim: + enabled: false + prefixes: + - kparam: key_name + trim: CurrentControlSet + suffixes: + - kparam: file_name + trim: .exe + +# =============================== YARA ================================================= + +yara: + enabled: true + rule: + paths: + - path: "C:\\yara-rules" + namespace: default + strings: + - string: "rule test : tag1 { meta: author = \"Hilko Bengen\" strings: $a = \"abc\" fullword condition: $a }" + namespace: default + alert-via: slack + alert-template: + title: "" + text: "" + fastscan: true + scan-timeout: 20s + skip-files: true + excluded-files: + - kernel32.dll + excluded-procs: + - system + - spotify.exe diff --git a/pkg/config/_fixtures/output.yml b/pkg/config/_fixtures/output.yml new file mode 100644 index 000000000..d2db8139e --- /dev/null +++ b/pkg/config/_fixtures/output.yml @@ -0,0 +1,23 @@ +kstream: + max-buffers: 10 + min-buffers: 8 + flush-interval: 1s + blacklist: + events: + - CreateThread + +filament: top_hives_io + +output: + console: + enabled: false + format: pretty + elasticsearch: + amqp: + enabled: true + url: amqp://localhost:5672 + timeout: 5s + exchange: fibratus + exchange-type: topic + routing-key: fibratus + diff --git a/pkg/config/_fixtures/transformers.yml b/pkg/config/_fixtures/transformers.yml new file mode 100644 index 000000000..9785d071e --- /dev/null +++ b/pkg/config/_fixtures/transformers.yml @@ -0,0 +1,29 @@ +kstream: + max-buffers: 10 + min-buffers: 8 + flush-interval: 1s + blacklist: + events: + - CreateThread + +filament: top_hives_io + +output.console: + format: pretty + +transformers.tags: + enabled: true + tags: + - key: 1 + value: k + +transformers.remove: + enabled: true + kparams: + - key_handle + +transformers.rename: + enabled: true + kparams: + - old: key_handle + new: KeyHandle \ No newline at end of file diff --git a/pkg/config/alertsender.go b/pkg/config/alertsender.go new file mode 100644 index 000000000..3c7558f58 --- /dev/null +++ b/pkg/config/alertsender.go @@ -0,0 +1,83 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "errors" + "fmt" + "github.com/rabbitstack/fibratus/pkg/alertsender" + "github.com/rabbitstack/fibratus/pkg/alertsender/mail" + "github.com/rabbitstack/fibratus/pkg/alertsender/slack" + "reflect" +) + +var errNoAlertsendersSection = errors.New("no alertsenders section in config") + +var errAlertsenderConfig = func(sender string, err error) error { + return fmt.Errorf("%s alert sender invalid config: %v", sender, err) +} + +func (c *Config) tryLoadAlertSenders() error { + configs := make([]alertsender.Config, 0) + alertsenders := c.viper.AllSettings()["alertsenders"] + if alertsenders == nil { + return errNoAlertsendersSection + } + + mapping, ok := alertsenders.(map[string]interface{}) + if !ok { + return fmt.Errorf("expected map[string]interface{} type for alertsenders but found %s", reflect.TypeOf(alertsenders)) + } + + for typ, config := range mapping { + switch typ { + case "mail": + var mailConfig mail.Config + if err := decode(config, &mailConfig); err != nil { + return errAlertsenderConfig(typ, err) + } + if !mailConfig.Enabled { + continue + } + config := alertsender.Config{ + Type: alertsender.Mail, + Sender: mailConfig, + } + configs = append(configs, config) + + case "slack": + var slackConfig slack.Config + if err := decode(config, &slackConfig); err != nil { + return errAlertsenderConfig(typ, err) + } + if !slackConfig.Enabled { + continue + } + config := alertsender.Config{ + Type: alertsender.Slack, + Sender: slackConfig, + } + configs = append(configs, config) + } + } + + c.Alertsenders = configs + + return nil +} diff --git a/pkg/config/api.go b/pkg/config/api.go new file mode 100644 index 000000000..22589bb76 --- /dev/null +++ b/pkg/config/api.go @@ -0,0 +1,43 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "github.com/spf13/viper" + "time" +) + +const ( + transport = "api.transport" + timeout = "api.timeout" +) + +// Config contains API specific config options. +type APIConfig struct { + // Transport specifies the underlying transport protocol for the API HTTP server. + Transport string `json:"api.transport" yaml:"api.transport"` + // Timeout determines the timeout for the API server responses + Timeout time.Duration `json:"api.timeout" yaml:"api.timeout"` +} + +// initFromViper initializes API configuration from Viper. +func (c *APIConfig) initFromViper(v *viper.Viper) { + c.Transport = v.GetString(transport) + c.Timeout = v.GetDuration(timeout) +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 000000000..bcde16d80 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,338 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "encoding/json" + "fmt" + "github.com/rabbitstack/fibratus/pkg/aggregator" + "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" + removet "github.com/rabbitstack/fibratus/pkg/aggregator/transformers/remove" + replacet "github.com/rabbitstack/fibratus/pkg/aggregator/transformers/replace" + tagst "github.com/rabbitstack/fibratus/pkg/aggregator/transformers/tags" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp" + "github.com/rabbitstack/fibratus/pkg/outputs/elasticsearch" + "github.com/rabbitstack/fibratus/pkg/util/log" + "github.com/rabbitstack/fibratus/pkg/util/multierror" + yara "github.com/rabbitstack/fibratus/pkg/yara/config" + "gopkg.in/yaml.v2" + "io/ioutil" + "time" + + renamet "github.com/rabbitstack/fibratus/pkg/aggregator/transformers/rename" + trimt "github.com/rabbitstack/fibratus/pkg/aggregator/transformers/trim" + + "github.com/rabbitstack/fibratus/pkg/alertsender" + mailsender "github.com/rabbitstack/fibratus/pkg/alertsender/mail" + slacksender "github.com/rabbitstack/fibratus/pkg/alertsender/slack" + "github.com/rabbitstack/fibratus/pkg/outputs" + "github.com/rabbitstack/fibratus/pkg/outputs/console" + "github.com/rabbitstack/fibratus/pkg/pe" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "os" + "path/filepath" + "strings" +) + +const ( + kcapFile = "kcap.file" + configFile = "config-file" + debugPrivilege = "debug-privilege" + initHandleSnapshot = "handle.init-snapshot" + + serializeThreads = "kevent.serialize-threads" + serializeImages = "kevent.serialize-images" + serializeHandles = "kevent.serialize-handles" + serializePE = "kevent.serialize-pe" + serializeEnvs = "kevent.serialize-envs" +) + +// Config stores configuration options for fine tuning the behaviour of Fibratus. +type Config struct { + // Kstream stores different configuration options for fine tuning kstream consumer/controller settings. + Kstream KstreamConfig `json:"kstream" yaml:"kstream"` + // Filament contains filament settings + Filament FilamentConfig `json:"filament" yaml:"filament"` + // PE contains the settings that influences the behaviour of the PE (Portable Executable) reader. + PE pe.Config `json:"pe" yaml:"pe"` + // Output stores the currently active output config + Output outputs.Config + // InitHandleSnapshot indicates whether initial handle snapshot is built + InitHandleSnapshot bool `json:"init-handle-snapshot" yaml:"init-handle-snapshot"` + DebugPrivilege bool `json:"debug-privilege" yaml:"debug-privilege"` + KcapFile string + + // API stores global HTTP API preferences + API APIConfig `json:"api" yaml:"api"` + // Yara contains configuration that influences the behaviour of the Yara engine + Yara yara.Config `json:"yara" yaml:"yara"` + // Aggregator stores event aggregator configuration + Aggregator aggregator.Config `json:"aggregator" yaml:"aggregator"` + // Log contains log-specific configuration options + Log log.Config `json:"logging" yaml:"logging"` + + // Transformers stores transformer configurations + Transformers []transformers.Config + // Alertsenders stores alert sender configurations + Alertsenders []alertsender.Config + + flags *pflag.FlagSet + viper *viper.Viper + opts *Options +} + +// Options determines which config flags are toggled depending on the command type. +type Options struct { + capture bool + replay bool + run bool + list bool + stats bool +} + +// Option is the type alias for the config option. +type Option func(*Options) + +// WithCapture determines the capture command is executed. +func WithCapture() Option { + return func(o *Options) { + o.capture = true + } +} + +// WithReplay determines the replay command is executed. +func WithReplay() Option { + return func(o *Options) { + o.replay = true + } +} + +// WithRun determines the main command is executed. +func WithRun() Option { + return func(o *Options) { + o.run = true + } +} + +// WithList determines the list command is executed. +func WithList() Option { + return func(o *Options) { + o.list = true + } +} + +// WithStats determines the stats command is executed. +func WithStats() Option { + return func(o *Options) { + o.stats = true + } +} + +// NewWithOpts builds a new configuration store from a variety of sources such as configuration files, +// environment variables or command line flags. +func NewWithOpts(options ...Option) *Config { + opts := &Options{} + + for _, opt := range options { + opt(opts) + } + + v := viper.New() + v.AutomaticEnv() + v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) + + flagSet := new(pflag.FlagSet) + + c := &Config{ + Kstream: KstreamConfig{}, + Filament: FilamentConfig{}, + API: APIConfig{}, + PE: pe.Config{}, + Log: log.Config{}, + Aggregator: aggregator.Config{}, + viper: v, + flags: flagSet, + opts: opts, + } + + if opts.run || opts.replay { + aggregator.AddFlags(flagSet) + console.AddFlags(flagSet) + amqp.AddFlags(flagSet) + elasticsearch.AddFlags(flagSet) + removet.AddFlags(flagSet) + replacet.AddFlags(flagSet) + renamet.AddFlags(flagSet) + trimt.AddFlags(flagSet) + tagst.AddFlags(flagSet) + mailsender.AddFlags(flagSet) + slacksender.AddFlags(flagSet) + yara.AddFlags(flagSet) + } + + if opts.run || opts.capture { + pe.AddFlags(flagSet) + } + + c.addFlags() + + return c +} + +// GetConfigFile gets the path of the configuration file from Viper value. +func (c Config) GetConfigFile() string { + return c.viper.GetString(configFile) +} + +// MustViperize adds the flag set to the Cobra command and binds them within Viper flags. +func (c *Config) MustViperize(cmd *cobra.Command) { + cmd.PersistentFlags().AddFlagSet(c.flags) + if err := c.viper.BindPFlags(cmd.PersistentFlags()); err != nil { + panic(err) + } + if c.opts.capture || c.opts.replay { + if err := cmd.MarkPersistentFlagRequired(kcapFile); err != nil { + panic(err) + } + } +} + +// Init setups the configuration state from Viper. +func (c *Config) Init() error { + c.Kstream.initFromViper(c.viper) + c.Filament.initFromViper(c.viper) + c.API.initFromViper(c.viper) + c.PE.InitFromViper(c.viper) + c.Aggregator.InitFromViper(c.viper) + c.Log.InitFromViper(c.viper) + c.Yara.InitFromViper(c.viper) + + c.InitHandleSnapshot = c.viper.GetBool(initHandleSnapshot) + c.DebugPrivilege = c.viper.GetBool(debugPrivilege) + c.KcapFile = c.viper.GetString(kcapFile) + + kevent.SerializeThreads = c.viper.GetBool(serializeThreads) + kevent.SerializeImages = c.viper.GetBool(serializeImages) + kevent.SerializeHandles = c.viper.GetBool(serializeHandles) + kevent.SerializePE = c.viper.GetBool(serializePE) + kevent.SerializeEnvs = c.viper.GetBool(serializeEnvs) + + if c.opts.run || c.opts.replay { + if err := c.tryLoadOutput(); err != nil { + return err + } + if err := c.tryLoadTransformers(); err != nil { + return err + } + if err := c.tryLoadAlertSenders(); err != nil { + return err + } + } + return nil +} + +// TryLoadFile attempts to load the configuration file from specified path on the file system. +func (c *Config) TryLoadFile(file string) error { + c.viper.SetConfigFile(file) + return c.viper.ReadInConfig() +} + +// Validate ensures that all configuration options provided by user have the expected values. It returns +// a list of validation errors prefixed with the offending configuration property/flag. +func (c *Config) Validate() error { + // we'll first validate the structure and values of the config file + file := c.viper.GetString(configFile) + var out interface{} + b, err := ioutil.ReadFile(file) + if err != nil { + return err + } + switch filepath.Ext(file) { + case ".yaml", ".yml": + err = yaml.Unmarshal(b, &out) + case ".json": + err = json.Unmarshal(b, &out) + default: + return fmt.Errorf("%s is not a supported config file extension", filepath.Ext(file)) + } + if err != nil { + return fmt.Errorf("couldn't read the config file: %v", err) + } + // validate config file content + valid, errs := validate(out) + if !valid || len(errs) > 0 { + return fmt.Errorf("invalid config: %v", multierror.Wrap(errs)) + } + // now validate the Viper config flags + valid, errs = validate(c.viper.AllSettings()) + if !valid || len(errs) > 0 { + return fmt.Errorf("invalid config: %v", multierror.Wrap(errs)) + } + return nil +} + +// File returns the config file path. +func (c *Config) File() string { return c.viper.GetString(configFile) } + +func (c *Config) addFlags() { + c.flags.String(configFile, filepath.Join(os.Getenv("PROGRAMFILES"), "fibratus", "config", "fibratus.yml"), "Indicates the location of the configuration file") + if c.opts.run || c.opts.replay { + c.flags.StringP(filamentName, "f", "", "Specifies the filament to execute") + } + if c.opts.capture { + c.flags.StringP(kcapFile, "o", "", "The path of the output kcap file") + } + if c.opts.replay { + c.flags.StringP(kcapFile, "k", "", "The path of the input kcap file") + + } + if c.opts.run || c.opts.replay || c.opts.list { + c.flags.String(filamentPath, filepath.Join(os.Getenv("PROGRAMFILES"), "fibratus", "filaments"), "Denotes the directory where filaments are located") + } + if c.opts.run || c.opts.replay || c.opts.capture || c.opts.stats { + c.flags.String(transport, `localhost:8080`, "Specifies the underlying transport protocol for the API HTTP server") + c.flags.Duration(timeout, time.Second*15, "Determines the timeout for the API server responses") + } + if c.opts.run || c.opts.capture { + c.flags.Bool(initHandleSnapshot, true, "Indicates whether initial handle snapshot is built. This implies scanning the system handles table and producing an entry for each handle object") + + c.flags.Bool(enableThreadKevents, true, "Determines whether thread kernel events are collected by Kernel Logger provider") + c.flags.Bool(enableRegistryKevents, true, "Determines whether registry kernel events are collected by Kernel Logger provider") + c.flags.Bool(enableNetKevents, true, "Determines whether network (TCP/UDP) kernel events are collected by Kernel Logger provider") + c.flags.Bool(enableFileIOKevents, true, "Determines whether disk I/O kernel events are collected by Kernel Logger provider") + c.flags.Bool(enableImageKevents, true, "Determines whether file I/O kernel events are collected by Kernel Logger provider") + c.flags.Bool(enableHandleKevents, false, "Determines whether object manager kernel events (handle creation/destruction) are collected by Kernel Logger provider") + c.flags.Int(bufferSize, int(maxBufferSize), "Represents the amount of memory allocated for each event tracing session buffer, in kilobytes. The buffer size affects the rate at which buffers fill and must be flushed (small buffer size requires less memory but it increases the rate at which buffers must be flushed)") + c.flags.Int(minBuffers, int(defaultMinBuffers), "Determines the minimum number of buffers allocated for the event tracing session's buffer pool") + c.flags.Int(maxBuffers, int(defaultMaxBuffers), "Determines the maximum number of buffers allocated for the event tracing session's buffer pool") + c.flags.Duration(flushInterval, defaultFlushInterval, "Specifies how often the trace buffers are forcibly flushed") + c.flags.StringSlice(blacklistEvents, []string{}, "A list of symbolical kernel event names that will be dropped from the kernel event stream. By default all events are accepted") + c.flags.StringSlice(blacklistImages, []string{"System"}, "A list of image names that will be dropped from the kernel event stream. Image names are case insensitive") + + c.flags.Bool(serializeThreads, false, "Indicates if threads are serialized as part of the process state") + c.flags.Bool(serializeImages, false, "Indicates if images are serialized as part of the process state") + c.flags.Bool(serializeHandles, false, "Indicates if handles are serialized as part of the process state") + c.flags.Bool(serializePE, false, "Indicates if the PE metadata are serialized as part of the process state") + c.flags.Bool(serializeEnvs, true, "Indicates if environment variables are serialized as part of the process state") + } + c.Log.AddFlags(c.flags) +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 000000000..ff0bc333f --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,113 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" + "github.com/rabbitstack/fibratus/pkg/aggregator/transformers/rename" + "github.com/rabbitstack/fibratus/pkg/alertsender" + "github.com/rabbitstack/fibratus/pkg/alertsender/mail" + "github.com/rabbitstack/fibratus/pkg/alertsender/slack" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestNewFromYamlFile(t *testing.T) { + c := NewWithOpts(WithRun()) + + err := c.flags.Parse([]string{"--config-file=_fixtures/fibratus.yml"}) + require.NoError(t, c.viper.BindPFlags(c.flags)) + require.NoError(t, err) + require.NoError(t, c.TryLoadFile(c.GetConfigFile())) + + require.NoError(t, c.Init()) + + errs := c.Validate() + + require.Empty(t, errs) + + assert.Equal(t, time.Millisecond*230, c.Aggregator.FlushPeriod) + assert.Equal(t, time.Second*8, c.Aggregator.FlushTimeout) + + assert.Len(t, c.Alertsenders, 2) + + for _, c := range c.Alertsenders { + switch c.Type { + case alertsender.Slack: + assert.IsType(t, slack.Config{}, c.Sender) + case alertsender.Mail: + assert.IsType(t, mail.Config{}, c.Sender) + mailConfig := c.Sender.(mail.Config) + assert.Equal(t, "smtp.gmail.com", mailConfig.Host) + assert.Equal(t, 587, mailConfig.Port) + assert.Equal(t, "bunny", mailConfig.User) + assert.Equal(t, "changeit", mailConfig.Pass) + assert.Equal(t, "bunny@gmail.com", mailConfig.From) + assert.Equal(t, []string{"bunny@gmail.com", "rabbit@gmail.com", "cuniculus@gmail.com"}, mailConfig.To) + } + } + + assert.Equal(t, "npipe:///fibratus", c.API.Transport) + assert.Equal(t, time.Second*5, c.API.Timeout) + assert.True(t, c.DebugPrivilege) + + assert.Equal(t, "top_netio", c.Filament.Name) + + require.Len(t, c.Transformers, 2) + + for _, tr := range c.Transformers { + switch tr.Type { + case transformers.Rename: + rconfig := tr.Transformer.(rename.Config) + assert.Len(t, rconfig.Kparams, 2) + r1 := rconfig.Kparams[0] + assert.Equal(t, "b", r1.New) + } + } + + assert.True(t, c.Yara.Enabled) + + require.Len(t, c.Yara.Rule.Paths, 1) + assert.Len(t, c.Yara.Rule.Strings, 1) + + assert.Equal(t, "C:\\yara-rules", c.Yara.Rule.Paths[0].Path) + assert.Equal(t, "default", c.Yara.Rule.Paths[0].Namespace) + +} + +func TestNewFromJsonFile(t *testing.T) { + c := NewWithOpts(WithRun()) + + err := c.flags.Parse([]string{"--config-file=_fixtures/fibratus.json"}) + require.NoError(t, c.viper.BindPFlags(c.flags)) + require.NoError(t, err) + require.NoError(t, c.TryLoadFile(c.GetConfigFile())) + + require.NoError(t, c.Init()) + + errs := c.Validate() + + require.Empty(t, errs) + + assert.Equal(t, time.Millisecond*230, c.Aggregator.FlushPeriod) + assert.Equal(t, time.Second*8, c.Aggregator.FlushTimeout) + +} diff --git a/pkg/config/decoder.go b/pkg/config/decoder.go new file mode 100644 index 000000000..ec3c7d99f --- /dev/null +++ b/pkg/config/decoder.go @@ -0,0 +1,38 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import "github.com/mitchellh/mapstructure" + +func decode(input, output interface{}) error { + var decoderConfig = &mapstructure.DecoderConfig{ + Metadata: nil, + Result: output, + WeaklyTypedInput: true, + DecodeHook: mapstructure.ComposeDecodeHookFunc( + mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToSliceHookFunc(","), + ), + } + decoder, err := mapstructure.NewDecoder(decoderConfig) + if err != nil { + return err + } + return decoder.Decode(input) +} diff --git a/pkg/config/filament.go b/pkg/config/filament.go new file mode 100644 index 000000000..038567eda --- /dev/null +++ b/pkg/config/filament.go @@ -0,0 +1,41 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "github.com/spf13/viper" + "time" +) + +const ( + filamentName = "filament.name" + filamentPath = "filament.path" +) + +// FilamentConfig stores config parameters for tweaking the behaviour of the filament engine. +type FilamentConfig struct { + Name string + Path string + FlushPeriod time.Duration +} + +func (f *FilamentConfig) initFromViper(v *viper.Viper) { + f.Name = v.GetString(filamentName) + f.Path = v.GetString(filamentPath) +} diff --git a/pkg/config/kstream.go b/pkg/config/kstream.go new file mode 100644 index 000000000..26c06452a --- /dev/null +++ b/pkg/config/kstream.go @@ -0,0 +1,94 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "github.com/spf13/viper" + "runtime" + "time" +) + +const ( + enableThreadKevents = "kstream.enable-thread" + enableRegistryKevents = "kstream.enable-registry" + enableNetKevents = "kstream.enable-net" + enableFileIOKevents = "kstream.enable-fileio" + enableImageKevents = "kstream.enable-image" + enableHandleKevents = "kstream.enable-handle" + bufferSize = "kstream.buffer-size" + minBuffers = "kstream.min-buffers" + maxBuffers = "kstream.max-buffers" + flushInterval = "kstream.flush-interval" + + blacklistEvents = "kstream.blacklist.events" + blacklistImages = "kstream.blacklist.images" + + maxBufferSize = uint32(1024) +) + +var ( + defaultMinBuffers = uint32(runtime.NumCPU() * 2) + defaultMaxBuffers = defaultMinBuffers + 20 + defaultFlushInterval = time.Second +) + +// KstreamConfig stores different configuration options for fine tuning kstream consumer/controller settings. +type KstreamConfig struct { + // EnableThreadKevents indicates if thread kernel events are collected by the ETW provider. + EnableThreadKevents bool `json:"enable-thread" yaml:"enable-thread"` + // EnableRegistryKevents indicates if registry kernel events are collected by the ETW provider. + EnableRegistryKevents bool `json:"enable-registry" yaml:"enable-registry"` + // EnableNetKevents determines whether network (TCP/UDP) events are collected by the ETW provider. + EnableNetKevents bool `json:"enable-net" yaml:"enable-net"` + // EnableFileIOKevents indicates if file I/O kernel events are collected by the ETW provider. + EnableFileIOKevents bool `json:"enable-fileio" yaml:"enable-fileio"` + // EnableImageKevents indicates if image kernel events are collected by the ETW provider. + EnableImageKevents bool `json:"enable-image" yaml:"enable-image"` + // EnableHandleKevents indicates whether handle creation/disposal events are enabled. + EnableHandleKevents bool `json:"enable-handle" yaml:"enable-handle"` + // BufferSize represents the amount of memory allocated for each event tracing session buffer, in kilobytes. + // The buffer size affects the rate at which buffers fill and must be flushed (small buffer size requires + // less memory but it increases the rate at which buffers must be flushed). + BufferSize uint32 `json:"buffer-size" yaml:"buffer-size"` + // MinBuffers determines the minimum number of buffers allocated for the event tracing session's buffer pool. + MinBuffers uint32 `json:"min-buffers" yaml:"min-buffers"` + // MaxBuffers is the maximum number of buffers allocated for the event tracing session's buffer pool. + MaxBuffers uint32 `json:"max-buffers" yaml:"max-buffers"` + // FlushTimer specifies how often the trace buffers are forcibly flushed. + FlushTimer time.Duration `json:"flush-interval" yaml:"flush-interval"` + // BlacklistKevents are kernel event names that will be dropped from the kernel event stream. + BlacklistKevents []string `json:"blacklist.events" yaml:"blacklist.events"` + // BlacklistImages are process image names that will be rejected if they generate a kernel event. + BlacklistImages []string `json:"blacklist.images" yaml:"blacklist.images"` +} + +func (c *KstreamConfig) initFromViper(v *viper.Viper) { + c.EnableThreadKevents = v.GetBool(enableThreadKevents) + c.EnableRegistryKevents = v.GetBool(enableRegistryKevents) + c.EnableNetKevents = v.GetBool(enableNetKevents) + c.EnableFileIOKevents = v.GetBool(enableFileIOKevents) + c.EnableImageKevents = v.GetBool(enableImageKevents) + c.EnableHandleKevents = v.GetBool(enableHandleKevents) + c.BufferSize = uint32(v.GetInt(bufferSize)) + c.MinBuffers = uint32(v.GetInt(minBuffers)) + c.MaxBuffers = uint32(v.GetInt(maxBuffers)) + c.FlushTimer = v.GetDuration(flushInterval) + c.BlacklistKevents = v.GetStringSlice(blacklistEvents) + c.BlacklistImages = v.GetStringSlice(blacklistImages) +} diff --git a/pkg/config/kstream_test.go b/pkg/config/kstream_test.go new file mode 100644 index 000000000..8e2d012a2 --- /dev/null +++ b/pkg/config/kstream_test.go @@ -0,0 +1,48 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestKstreamConfig(t *testing.T) { + c := NewWithOpts(WithRun()) + + err := c.flags.Parse([]string{ + "--kstream.enable-thread=false", + "--kstream.enable-registry=false", + "--kstream.enable-fileio=false", + "--kstream.enable-net=false", + "--kstream.enable-image=false", + }) + require.NoError(t, err) + require.NoError(t, c.viper.BindPFlags(c.flags)) + require.NoError(t, err) + + require.NoError(t, c.Init()) + + assert.False(t, c.Kstream.EnableThreadKevents) + assert.False(t, c.Kstream.EnableNetKevents) + assert.False(t, c.Kstream.EnableRegistryKevents) + assert.False(t, c.Kstream.EnableImageKevents) + assert.False(t, c.Kstream.EnableFileIOKevents) +} diff --git a/pkg/config/output.go b/pkg/config/output.go new file mode 100644 index 000000000..45ea21d79 --- /dev/null +++ b/pkg/config/output.go @@ -0,0 +1,137 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "errors" + "fmt" + "github.com/rabbitstack/fibratus/pkg/outputs" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp" + "github.com/rabbitstack/fibratus/pkg/outputs/console" + "github.com/rabbitstack/fibratus/pkg/outputs/elasticsearch" + "github.com/rabbitstack/fibratus/pkg/outputs/null" + log "github.com/sirupsen/logrus" + "golang.org/x/sys/windows/svc" + "reflect" + "strconv" +) + +var errNoOutputSection = errors.New("no output section in config") + +var errOutputConfig = func(output string, err error) error { return fmt.Errorf("%s output invalid config: %v", output, err) } + +func (c *Config) tryLoadOutput() error { + output := c.viper.AllSettings()["output"] + if output == nil { + return errNoOutputSection + } + mapping, ok := output.(map[string]interface{}) + if !ok { + return fmt.Errorf("expected map[string]interface{} type for output but found %s", reflect.TypeOf(output)) + } + + humNum := func(n int) string { + switch n { + case 2: + return "two" + case 3: + return "three" + case 4: + return "four" + case 5: + return "five" + case 6: + return "six" + case 7: + return "seven" + default: + return strconv.Itoa(n) + } + } + // don't permit if there are various outputs enabled at a time + activeOutputs := findActiveOutputs(mapping) + if len(activeOutputs) > 1 { + return fmt.Errorf("expected one but found %s active outputs: %s", humNum(len(activeOutputs)), activeOutputs) + } + + for typ, config := range mapping { + switch typ { + case "console": + var consoleConfig console.Config + if err := decode(config, &consoleConfig); err != nil { + return errOutputConfig(typ, err) + } + if !consoleConfig.Enabled { + continue + } + c.Output.Type, c.Output.Output = outputs.Console, consoleConfig + + case "amqp": + var amqpConfig amqp.Config + if err := decode(config, &amqpConfig); err != nil { + return errOutputConfig(typ, err) + } + if !amqpConfig.Enabled { + continue + } + c.Output.Type, c.Output.Output = outputs.AMQP, amqpConfig + + case "elasticsearch": + var esConfig elasticsearch.Config + if err := decode(config, &esConfig); err != nil { + return errOutputConfig(typ, err) + } + if !esConfig.Enabled { + continue + } + c.Output.Type, c.Output.Output = outputs.Elasticsearch, esConfig + } + } + + // if it is not an interactive session but the console output is enabled + // we default to null output and warn about that + in, err := svc.IsAnInteractiveSession() + if err == nil && !in && c.Output.Output != nil { + if c.Output.Type == outputs.Console { + log.Warn("running in non-interactive session with console output. " + + "Please configure a different output type. Defaulting to null output") + c.Output.Type, c.Output.Output = outputs.Null, &null.Config{} + return nil + } + } + + // default to null output + if c.Output.Output == nil { + log.Warn("all outputs disabled. Defaulting to null output") + c.Output.Type, c.Output.Output = outputs.Null, &null.Config{} + } + + return nil +} + +func findActiveOutputs(outputs map[string]interface{}) []string { + outputTypes := make([]string, 0) + for typ, rawConfig := range outputs { + enabled, ok := rawConfig.(map[string]interface{})["enabled"].(bool) + if ok && enabled { + outputTypes = append(outputTypes, typ) + } + } + return outputTypes +} diff --git a/pkg/config/output_test.go b/pkg/config/output_test.go new file mode 100644 index 000000000..5e2b9a0a5 --- /dev/null +++ b/pkg/config/output_test.go @@ -0,0 +1,49 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "github.com/rabbitstack/fibratus/pkg/outputs/amqp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestOutput(t *testing.T) { + c := NewWithOpts(WithRun()) + + err := c.flags.Parse([]string{"--config-file=_fixtures/output.yml"}) + require.NoError(t, c.viper.BindPFlags(c.flags)) + require.NoError(t, err) + require.NoError(t, c.TryLoadFile(c.GetConfigFile())) + + require.NoError(t, c.Init()) + + require.NotNil(t, c.Output) + require.IsType(t, amqp.Config{}, c.Output.Output) + + amqpConfig := c.Output.Output.(amqp.Config) + assert.Equal(t, "amqp://localhost:5672", amqpConfig.URL) + assert.Equal(t, time.Second*5, amqpConfig.Timeout) + assert.Equal(t, "fibratus", amqpConfig.Exchange) + assert.Equal(t, "topic", amqpConfig.ExchangeType) + assert.Equal(t, "fibratus", amqpConfig.RoutingKey) + assert.Equal(t, "/", amqpConfig.Vhost) +} diff --git a/pkg/config/print.go b/pkg/config/print.go new file mode 100644 index 000000000..df68ca868 --- /dev/null +++ b/pkg/config/print.go @@ -0,0 +1,108 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "bytes" + "fmt" + "reflect" + "sort" + "strings" +) + +func (c *Config) printArray(arr []interface{}) string { + var buffer bytes.Buffer + for v := range arr { + buffer.WriteString(fmt.Sprintf("%v;", v)) + } + return buffer.String() +} + +func (c *Config) printMap(m map[string]interface{}) string { + var buffer bytes.Buffer + buffer.WriteString("[") + for k, v := range m { + val := c.print(v) + if len(val) > 0 { + buffer.WriteString(" ") + buffer.WriteString(k) + buffer.WriteString("=>") + if strings.Contains(k, "password") { + buffer.WriteString("********") + } else { + buffer.WriteString(val) + } + } + } + buffer.WriteString("]") + return buffer.String() +} + +func (c *Config) print(value interface{}) string { + t := reflect.TypeOf(value) + switch t.Kind() { + case reflect.Array: + return c.printArray(value.([]interface{})) + case reflect.Map: + return c.printMap(value.(map[string]interface{})) + default: + return fmt.Sprintf("%v", value) + } +} + +func (c *Config) printLine(buffer *bytes.Buffer, maxLength int, key string, value string) { + if value != "" { + buffer.WriteString("\n\t") + buffer.WriteString(key) + buffer.WriteString(" ") + buffer.WriteString(strings.Repeat(".", maxLength-len(key)+5)) + buffer.WriteString(" ") + buffer.WriteString(value) + } +} + +// Print returns the string with all the config options pretty-printed. +func (c *Config) Print() string { + opts := c.viper.AllSettings() + + var buffer bytes.Buffer + var maxKeyLen = 20 + + type kv struct { + k string + v interface{} + } + + sorted := make([]kv, 0, len(opts)) + // for printing we need to find the max key length + for key, v := range opts { + if len(key) > maxKeyLen { + maxKeyLen = len(key) + } + sorted = append(sorted, kv{k: key, v: v}) + } + sort.Slice(sorted, func(i, j int) bool { return sorted[i].k < sorted[j].k }) + + // print the options + for _, kv := range sorted { + c.printLine(&buffer, maxKeyLen, kv.k, c.print(kv.v)) + } + + return buffer.String() +} diff --git a/pkg/config/print_test.go b/pkg/config/print_test.go new file mode 100644 index 000000000..4e5f9b4ec --- /dev/null +++ b/pkg/config/print_test.go @@ -0,0 +1,34 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestConfigPrint(t *testing.T) { + c := NewWithOpts(WithRun()) + err := c.flags.Parse([]string{"--kstream.enable-thread=false", "--config-file=_fixtures/fibratus.yml"}) + require.NoError(t, c.viper.BindPFlags(c.flags)) + require.NoError(t, err) + require.NoError(t, c.TryLoadFile(c.GetConfigFile())) + opts := c.Print() + require.NotEmpty(t, opts) +} diff --git a/pkg/config/schema.go b/pkg/config/schema.go new file mode 100644 index 000000000..d91fabdc5 --- /dev/null +++ b/pkg/config/schema.go @@ -0,0 +1,446 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "bytes" + "text/template" +) + +var schema = ` +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": {"yara": {"$id": "#yara", "type": "object", "properties": {"enabled": {"type": "boolean"}}}}, + + "type": "object", + "properties": { + "aggregator": { + "type": "object", + "properties": { + "flush-period": {"type": "string", "minLength": 2, "pattern": "[0-9]+ms|s"}, + "flush-timeout": {"type": "string", "minLength": 2, "pattern": "[0-9]+s"} + }, + "additionalProperties": false + }, + "alertsenders": { + "type": "object", + "anyOf": [{ + "properties": { + "mail": { + "type": "object", + "properties": { + "enabled": {"type": "boolean"}, + "host": {"type": "string"}, + "port": {"type": "number"}, + "user": {"type": "string"}, + "password": {"type": "string"}, + "from": {"type": "string"}, + "to": {"type": "array", "items": {"type": "string", "format": "email"}} + }, + "if": { + "properties": {"enabled": { "const": true }} + }, + "then": { + "properties": { + "from": {"type": "string", "format": "email"}, + "to": {"type": "array", "minItems": 1, "items": {"type": "string", "format": "email"}} + } + }, + "additionalProperties": false + }, + "slack": { + "type": "object", + "properties": { + "enabled": {"type": "boolean"}, + "url": {"type": "string"}, + "workspace": {"type": "string"}, + "channel": {"type": "string"}, + "emoji": {"type": "string"} + }, + "if": { + "properties": {"enabled": { "const": true }} + }, + "then": { + "properties": {"url": {"type": "string", "format": "uri", "minLength": 1, "pattern": "^(https?|http?)://"}} + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "api": { + "type": "object", + "properties": { + "transport": {"type": "string", "minLength": 3}, + "timeout": {"type": "string", "minLength": 2, "pattern": "[0-9]+s"} + }, + "additionalProperties": false + }, + "config-file": {"type": "string"}, + "debug-privilege": {"type": "boolean"}, + "handle": { + "type": "object", + "properties": { + "init-snapshot": {"type": "boolean"} + }, + "additionalProperties": false + }, + "kcap": { + "type": "object", + "properties": { + "file": {"type": "string"} + }, + "additionalProperties": false + }, + "filament": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "path": {"type": "string"}, + "flush-period": {"type": "string", "minLength": 2, "pattern": "[0-9]+ms|s"} + }, + "additionalProperties": false + }, + "kevent": { + "type": "object", + "properties": { + "serialize-threads": {"type": "boolean"}, + "serialize-images": {"type": "boolean"}, + "serialize-handles": {"type": "boolean"}, + "serialize-pe": {"type": "boolean"}, + "serialize-envs": {"type": "boolean"} + }, + "additionalProperties": false + }, + "kstream": { + "type": "object", + "properties": { + "enable-thread": {"type": "boolean"}, + "enable-image": {"type": "boolean"}, + "enable-registry": {"type": "boolean"}, + "enable-fileio": {"type": "boolean"}, + "enable-handle": {"type": "boolean"}, + "enable-net": {"type": "boolean"}, + "min-buffers": {"type": "integer", "minimum": 1, "maximum": {{ .MinBuffers }}}, + "max-buffers": {"type": "integer", "minimum": 2, "maximum": {{ .MaxBuffers }}}, + "buffer-size": {"type": "integer", "maximum": {{ .MaxBufferSize }}}, + "flush-interval": {"type": "string", "minLength": 2, "pattern": "[0-9]+s"}, + "blacklist": { + "type": "object", + "properties": { + "events": {"type": "array", "items": [{"type": "string", "enum": ["CreateProcess", "CreateThread", "TerminateProcess", "TerminateThread", "LoadImage", "UnloadImage", "CreateFile", "CloseFile", "ReadFile", "WriteFile", "DeleteFile", "RenameFile", "SetFileInformation", "EnumDirectory", "RegCreateKey", "RegOpenKey", "RegSetValue", "RegQueryValue", "RegQueryKey", "RegDeleteKey", "RegDeleteValue", "Accept", "Send", "Recv", "Connect", "Disconnect", "Reconnect", "Retransmit", "CreateHandle", "CloseHandle"]}]}, + "images": {"type": "array", "items": [{"type": "string", "minLength": 1}]} + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "logging": { + "type": "object", + "properties": { + "level": {"type": "string"}, + "max-age": {"type": "integer"}, + "max-backups": {"type": "integer", "minimum": 1}, + "max-size": {"type": "integer", "minimum": 1}, + "formatter": {"type": "string", "enum": ["json", "text"]}, + "path": {"type": "string"}, + "log-stdout": {"type": "boolean"} + }, + "additionalProperties": false + }, + "output": { + "type": "object", + "anyOf": [{ + "properties": { + "console": { + "type": "object", + "properties": { + "enabled": {"type": "boolean"}, + "format": {"type": "string", "enum": ["json", "pretty"]}, + "template": {"type": "string"}, + "kv-delimiter": {"type": "string"} + }, + "additionalProperties": false + }, + "elasticsearch": { + "type": "object", + "properties": { + "enabled": {"type": "boolean"}, + "servers": {"type": "array", "items": [{"type": "string", "minItems": 1, "format": "uri", "minLength": 1, "maxLength": 255, "pattern": "^(https?|http?)://"}]}, + "timeout": {"type": "string"}, + "index-name": {"type": "string", "minLength": 1}, + "template-config": {"type": "string"}, + "template-name": {"type": "string", "minLength": 1}, + "healthcheck": {"type": "boolean"}, + "bulk-workers": {"type": "integer", "minimum": 1}, + "sniff": {"type": "boolean"}, + "trace-log": {"type": "boolean"}, + "gzip-compression": {"type": "boolean"}, + "healthcheck-interval": {"type": "string", "minLength": 2, "pattern": "[0-9]+s|m}"}, + "healthcheck-timeout": {"type": "string", "minLength": 2, "pattern": "[0-9]+s|m}"}, + "flush-period": {"type": "string", "minLength": 2, "pattern": "[0-9]+s|m}"}, + "username": {"type": "string"}, + "password": {"type": "string"}, + "tls-key": {"type": "string"}, + "tls-cert": {"type": "string"}, + "tls-ca": {"type": "string"}, + "tls-insecure-skip-verify": {"type": "boolean"} + }, + "additionalProperties": false + }, + "amqp": { + "type": "object", + "properties": { + "enabled": {"type": "boolean"}, + "url": {"type": "string", "format": "uri", "minLength": 1, "maxLength": 255, "pattern": "^(amqps?|amqp?)://"}, + "timeout": {"type": "string"}, + "exchange": {"type": "string", "minLength": 1}, + "exchange-type": {"type": "string", "enum": ["direct", "topic", "fanout", "header", "x-consistent-hash"]}, + "routing-key": {"type": "string", "minLength": 1}, + "delivery-mode": {"type": "string", "enum": ["transient", "persistent"]}, + "vhost": {"type": "string", "minLength": 1}, + "passive": {"type": "boolean"}, + "durable": {"type": "boolean"}, + "username": {"type": "string"}, + "password": {"type": "string"}, + "tls-key": {"type": "string"}, + "tls-cert": {"type": "string"}, + "tls-ca": {"type": "string"}, + "tls-insecure-skip-verify": {"type": "boolean"}, + "headers": {"type": "object", "additionalProperties": true} + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "pe": { + "type": "object", + "properties": { + "enabled": {"type": "boolean"}, + "read-resources": {"type": "boolean"}, + "read-symbols": {"type": "boolean"}, + "read-sections": {"type": "boolean"}, + "excluded-images": {"type": "array", "items": [{"type": "string"}]} + }, + "additionalProperties": false + }, + "transformers": { + "type": "object", + "anyOf": [{ + "properties": { + "remove": { + "type": "object", + "properties": { + "enabled": {"type": "boolean"}, + "kparams": {"type": "array", "items": [{"type": "string"}]} + }, + "if": { + "properties": {"enabled": { "const": true }} + }, + "then": { + "properties": {"kparams": {"type": "array", "minItems": 1, "items": [{"type": "string"}]}} + }, + "additionalProperties": false + }, + "rename": { + "type": "object", + "properties": { + "enabled": {"type": "boolean"}, + "kparams": {"type": "array", "items": [ + { + "type": "object", + "properties": { + "old": {"type": "string", "minLength": 1}, + "new": {"type": "string", "minLength": 1} + }, + "additionalProperties": false + } + ]} + }, + "if": { + "properties": {"enabled": { "const": true }} + }, + "then": { + "properties": {"kparams": {"minItems": 1}} + }, + "additionalProperties": false + }, + "replace": { + "type": "object", + "properties": { + "enabled": {"type": "boolean"}, + "replacements": {"type": "array", "items": [ + { + "type": "object", + "properties": { + "kparam": {"type": "string", "minLength": 1}, + "old": {"type": "string", "minLength": 1}, + "new": {"type": "string"} + }, + "additionalProperties": false + } + ]} + }, + "if": { + "properties": {"enabled": { "const": true }} + }, + "then": { + "properties": {"replacements": {"minItems": 1}} + }, + "additionalProperties": false + }, + "tags": { + "type": "object", + "properties": { + "enabled": {"type": "boolean"}, + "tags": {"type": "array", "items": [ + { + "type": "object", + "properties": { + "key": {"type": "string", "minLength": 1}, + "value": {"type": "string", "minLength": 1} + }, + "additionalProperties": false + } + ]} + }, + "if": { + "properties": {"enabled": { "const": true }} + }, + "then": { + "properties": {"tags": {"minItems": 1}} + }, + "additionalProperties": false + }, + "trim": { + "type": "object", + "properties": { + "enabled": {"type": "boolean"}, + "prefixes": {"type": "array", "items": [ + { + "type": "object", + "properties": { + "kparam": {"type": "string", "minLength": 1}, + "trim": {"type": "string", "minLength": 1} + }, + "additionalProperties": false + } + ]}, + "suffixes": {"type": "array", "items": [ + { + "type": "object", + "properties": { + "kparam": {"type": "string", "minLength": 1}, + "trim": {"type": "string", "minLength": 1} + }, + "additionalProperties": false + } + ]} + }, + "if": { + "properties": {"enabled": { "const": true }} + }, + "then": { + "properties": {"suffixes": {"minItems": 1}, "prefixes": {"minItems": 1}} + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "yara": { + "type": "object", + "properties": { + "enabled": {"type": "boolean"}, + "rule": { + "type": "object", + "anyOf": [{ + "properties": { + "paths": {"type": "array", "items": [ + { + "type": "object", + "properties": { + "path": {"type": "string"}, + "namespace": {"type": "string"} + }, + "if": { + "properties": {"enabled": {"$ref": "#yara", "const": true }} + }, + "then": { + "properties": {"path": {"minLength": 0}} + }, + "additionalProperties": false + }] + }, + "strings": {"type": "array"} + }, + "additionalProperties": false + }] + }, + "alert-via": {"type": "string", "enum": ["slack", "mail"]}, + "alert-template": { + "type": "object", + "properties": { + "text": {"type": "string"}, + "title": {"type": "string"} + }, + "additionalProperties": false + }, + "fastscan": {"type": "boolean"}, + "skip-files": {"type": "boolean"}, + "scan-timeout": {"type": "string", "minLength": 2, "pattern": "[0-9]+s"}, + "excluded-files": {"type": "array", "items": [{"type": "string", "minLength": 1}]}, + "excluded-procs": {"type": "array", "items": [{"type": "string", "minLength": 1}]} + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} +` + +type schemaConfig struct { + MaxBuffers uint32 + MinBuffers uint32 + MaxBufferSize uint32 +} + +func interpolateSchema() string { + tmpl := template.Must(template.New("schema").Parse(schema)) + + var b bytes.Buffer + err := tmpl.Execute(&b, &schemaConfig{ + MaxBuffers: defaultMaxBuffers, + MinBuffers: defaultMinBuffers, + MaxBufferSize: maxBufferSize, + }) + if err != nil { + return "" + } + + return b.String() +} diff --git a/pkg/config/transformer.go b/pkg/config/transformer.go new file mode 100644 index 000000000..24b5df7d6 --- /dev/null +++ b/pkg/config/transformer.go @@ -0,0 +1,123 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/aggregator/transformers" + "github.com/rabbitstack/fibratus/pkg/aggregator/transformers/remove" + "github.com/rabbitstack/fibratus/pkg/aggregator/transformers/rename" + "github.com/rabbitstack/fibratus/pkg/aggregator/transformers/replace" + "github.com/rabbitstack/fibratus/pkg/aggregator/transformers/tags" + "github.com/rabbitstack/fibratus/pkg/aggregator/transformers/trim" + "reflect" +) + +var errTransformerConfig = func(t string, err error) error { return fmt.Errorf("%s transformer invalid config: %v", t, err) } + +func (c *Config) tryLoadTransformers() error { + transforms := c.viper.AllSettings()["transformers"] + if transforms == nil { + return nil + } + mapping, ok := transforms.(map[string]interface{}) + if !ok { + return fmt.Errorf("expected map[string]interface{} type for transformers but found %s", reflect.TypeOf(transforms)) + } + + configs := make([]transformers.Config, 0) + + for typ, config := range mapping { + switch typ { + case "remove": + var removeConfig remove.Config + if err := decode(config, &removeConfig); err != nil { + return errTransformerConfig(typ, err) + } + if !removeConfig.Enabled { + continue + } + config := transformers.Config{ + Type: transformers.Remove, + Transformer: removeConfig, + } + configs = append(configs, config) + + case "rename": + var renameConfig rename.Config + if err := decode(config, &renameConfig); err != nil { + return errTransformerConfig(typ, err) + } + if !renameConfig.Enabled { + continue + } + config := transformers.Config{ + Type: transformers.Rename, + Transformer: renameConfig, + } + configs = append(configs, config) + + case "replace": + var replaceConfig replace.Config + if err := decode(config, &replaceConfig); err != nil { + return errTransformerConfig(typ, err) + } + if !replaceConfig.Enabled { + continue + } + config := transformers.Config{ + Type: transformers.Replace, + Transformer: replaceConfig, + } + configs = append(configs, config) + + case "trim": + var trimConfig trim.Config + if err := decode(config, &trimConfig); err != nil { + return errTransformerConfig(typ, err) + } + if !trimConfig.Enabled { + continue + } + config := transformers.Config{ + Type: transformers.Trim, + Transformer: trimConfig, + } + configs = append(configs, config) + + case "tags": + var tagsConfig tags.Config + if err := decode(config, &tagsConfig); err != nil { + return errTransformerConfig(typ, err) + } + if !tagsConfig.Enabled { + continue + } + config := transformers.Config{ + Type: transformers.Tags, + Transformer: tagsConfig, + } + configs = append(configs, config) + } + } + + c.Transformers = configs + + return nil +} diff --git a/pkg/config/transformer_test.go b/pkg/config/transformer_test.go new file mode 100644 index 000000000..9000fd3d0 --- /dev/null +++ b/pkg/config/transformer_test.go @@ -0,0 +1,37 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestTransformers(t *testing.T) { + c := NewWithOpts(WithRun()) + + err := c.flags.Parse([]string{"--config-file=_fixtures/transformers.yml"}) + require.NoError(t, c.viper.BindPFlags(c.flags)) + require.NoError(t, err) + require.NoError(t, c.TryLoadFile(c.GetConfigFile())) + + require.NoError(t, c.Init()) + + require.Len(t, c.Transformers, 3) +} diff --git a/pkg/config/validation.go b/pkg/config/validation.go new file mode 100644 index 000000000..0214328f0 --- /dev/null +++ b/pkg/config/validation.go @@ -0,0 +1,108 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "fmt" + "github.com/pkg/errors" + "github.com/xeipuuv/gojsonschema" +) + +func validate(m interface{}) (bool, []error) { + converted, err := convertToStringKeysRecursive(m, "") + if err != nil { + return false, []error{fmt.Errorf("fail to convert keys to string: %v", err)} + } + loader := gojsonschema.NewGoLoader(converted) + sc := gojsonschema.NewStringLoader(interpolateSchema()) + r, err := gojsonschema.Validate(sc, loader) + if err != nil { + return false, []error{fmt.Errorf("fail to validate config file through schema: %v", err)} + } + errs := make([]error, len(r.Errors())) + for i, err := range r.Errors() { + errs[i] = errors.New(err.String()) + } + return r.Valid(), errs +} + +// convertToStringKeysRecursive ensures keys are converted to strings for jsonschema. +func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) { + if mapping, ok := value.(map[string]interface{}); ok { + dict := make(map[string]interface{}) + for str, entry := range mapping { + var newKeyPrefix string + if keyPrefix == "" { + newKeyPrefix = str + } else { + newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str) + } + convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) + if err != nil { + return nil, err + } + dict[str] = convertedEntry + } + return dict, nil + } + if mapping, ok := value.(map[interface{}]interface{}); ok { + dict := make(map[string]interface{}) + for key, entry := range mapping { + str, ok := key.(string) + if !ok { + return nil, formatInvalidKeyError(keyPrefix, key) + } + var newKeyPrefix string + if keyPrefix == "" { + newKeyPrefix = str + } else { + newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str) + } + convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) + if err != nil { + return nil, err + } + dict[str] = convertedEntry + } + return dict, nil + } + if list, ok := value.([]interface{}); ok { + var convertedList []interface{} + for index, entry := range list { + newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index) + convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) + if err != nil { + return nil, err + } + convertedList = append(convertedList, convertedEntry) + } + return convertedList, nil + } + return value, nil +} + +func formatInvalidKeyError(keyPrefix string, key interface{}) error { + var location string + if keyPrefix == "" { + location = "at top level" + } else { + location = fmt.Sprintf("in %s", keyPrefix) + } + return errors.Errorf("non-string key %s: %#v", location, key) +} diff --git a/pkg/config/validation_test.go b/pkg/config/validation_test.go new file mode 100644 index 000000000..bcaabb25a --- /dev/null +++ b/pkg/config/validation_test.go @@ -0,0 +1,92 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "gopkg.in/yaml.v2" + "testing" +) + +func TestValidate(t *testing.T) { + var tests = []struct { + text string + valid bool + errs int + }{ + {text: `aggregator: + flush-period: 20ms + flush-timeout: 1s`, valid: true}, + {text: `aggregator: + flush-period: 20 + flush-timeout: 1s`, valid: false, errs: 1}, + {text: `aggregator: + flush-perio: 20ms + flush-timeout: 1`, valid: false, errs: 2}, + + {text: `alertsenders: + mail: + enabled: true + host: smtp.gmail.com + port: 465 + user: user + pass: pas$ + from: from@mail.com + to: + - to@mail.com + slack: + enabled: true + url: https://slack.url + workspace: fibratus + channel: fibratus + emoji: ""`, valid: true}, + {text: `alertsenders: + mail: + enabled: true + host: smtp.gmail.com + port: + user: user + pass: pas$ + from: from@mail.com + to: + - invalidmail@ + slack: + enabled: true + url: https://slack.url + workspace: fibratus + channel: fibratus + emoji: ""`, valid: false, errs: 3}, + {text: `api: + transport: "" + timeout: 1s`, valid: false, errs: 1}, + } + + for i, tt := range tests { + var m interface{} + err := yaml.Unmarshal([]byte(tt.text), &m) + if err != nil { + t.Fatal(err) + } + valid, errs := validate(m) + if valid != tt.valid { + t.Errorf("%d. valid mismatch: text=%q exp=%#v got=%#v", i, tt.text, tt.valid, valid) + } else if len(errs) != tt.errs { + t.Errorf("%d. error count mismatch: text=%q exp=%#v got=%#v", i, tt.text, tt.errs, len(errs)) + } + } +} diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go new file mode 100644 index 000000000..003382ab6 --- /dev/null +++ b/pkg/errors/errors.go @@ -0,0 +1,89 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package errors + +import ( + "errors" + "fmt" +) + +var ( + // ErrTraceAccessDenied is returned when user doesn't have enough privileges to start kernel trace + ErrTraceAccessDenied = errors.New("not enough privileges to start the kernel trace. Only users with administrative privileges or users in the Performance Log Users group can start kernel traces") + // ErrTraceInvalidParameter signals invalid values for trace session + ErrTraceInvalidParameter = errors.New("trace has invalid values") + // ErrTraceBadLength signals an incorrect size for internal structure buffer + ErrTraceBadLength = errors.New("incorrect size of internal structure buffer") + // ErrCannotUpdateTrace signals that the session with the same GUID was running and couldn't be updated + ErrCannotUpdateTrace = errors.New("couldn't update the running trace") + // ErrTraceNoSysResources signals that the maximum number of logging sessions has been reached + ErrTraceNoSysResources = errors.New("maximum number of logging sessions has been reached") + // ErrTraceDiskFull signals that there is not enough space on disk for the log file. Should never happen for real-time loggers + ErrTraceDiskFull = errors.New("not enough disk space for writing to log file") + // ErrInvalidTrace signals invalid trace handle + ErrInvalidTrace = errors.New("invalid kernel trace handle") + // ErrStopTrace is bubbled when controller is not able to stop kernel trace session + ErrStopTrace = errors.New("an error occurred while stopping kernel trace") + // ErrRestartTrace signals an error that is thrown when currently running kernel trace cannot be restarted + ErrRestartTrace = errors.New("couldn't restart an already running kernel trace") + // ErrTraceAlreadyRunning identifies kernel trace already running errors + ErrTraceAlreadyRunning = errors.New("kernel trace is already running") + // ErrEventCallbackException signals that an exception has occurred in the event processing function + ErrEventCallbackException = errors.New("an exception occurred in the event callback function") + // ErrKsessionNotRunning is thrown when kernel session from which consumer is trying to collect events is not running + ErrKsessionNotRunning = errors.New("kernel session from which you are trying to consume events in real time is not running") + // ErrTraceCancelled is thrown when in-progress kernel event trace is cancelled + ErrTraceCancelled = errors.New("kernel event trace has been cancelled") + // ErrInsufficentBuffer raises when the buffer size for allocating event metadata is higher then regular buffer size + ErrInsufficentBuffer = errors.New("insufficient buffer size to allocate event metadata") + // ErrEventSchemaNotFound signals missing event schema + ErrEventSchemaNotFound = errors.New("event schema not found") + // ErrNeedsReallocateBuffer is signaled when an API function requires bigger buffer size + ErrNeedsReallocateBuffer = errors.New("buffer size is too small") + // ErrCancelUpstreamKevent represents the error that is returned to denote that event is not going to be passed to upstream components such as aggregator or outputs + ErrCancelUpstreamKevent = errors.New("cancel bubbling up the kernel event to upstream components") + + // ErrFeatureUnsupported is thrown when a certain feature was not triggered via the build flag + ErrFeatureUnsupported = func(s string) error { + return fmt.Errorf("fibratus was compiled without %s support. Please compile with the '%s' build flag", s, s) + } +) + +// ErrKparamNotFound is the error is thrown when a parameter is not present in the list of parameters +type ErrKparamNotFound struct { + Name string +} + +// Error returns the error message. +func (e ErrKparamNotFound) Error() string { + return "couldn't find " + e.Name + " in event parameters" +} + +// IsCancelUpstreamKevent determines if the error being passed if of `ErrCancelUpstreamKevent` type. +func IsCancelUpstreamKevent(err error) bool { return err == ErrCancelUpstreamKevent } + +// IsKparamNotFound returns true if the error is KparamNotFound. +func IsKparamNotFound(err error) bool { + switch err.(type) { + case *ErrKparamNotFound: + return true + default: + return false + } +} diff --git a/tests/fixtures/filaments/test_filament_interval.py b/pkg/filament/_fixtures/test_filter.py similarity index 80% rename from tests/fixtures/filaments/test_filament_interval.py rename to pkg/filament/_fixtures/test_filter.py index 5c1efcd3d..33c1afbbb 100644 --- a/tests/fixtures/filaments/test_filament_interval.py +++ b/pkg/filament/_fixtures/test_filter.py @@ -1,6 +1,7 @@ # Copyright 2016 by Nedim Sabic (RabbitStack) # All Rights Reserved. -# +# http://rabbitstack.github.io + # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -14,17 +15,13 @@ # under the License. """ -Test filament description +Tests the filter expression. """ +kevents = [] def on_init(): - pass - + kfilter('ps.name in (%s)' % ','.join(["'svchost.exe'", "'cmd.exe'", "'mimikatz.exe'"])) def on_next_kevent(kevent): - pass - - -def on_interval(): pass \ No newline at end of file diff --git a/tests/fixtures/filaments/test_filament_invalid_interval.py b/pkg/filament/_fixtures/test_on_next_kevent.py similarity index 62% rename from tests/fixtures/filaments/test_filament_invalid_interval.py rename to pkg/filament/_fixtures/test_on_next_kevent.py index 95abafae4..07b490f96 100644 --- a/tests/fixtures/filaments/test_filament_invalid_interval.py +++ b/pkg/filament/_fixtures/test_on_next_kevent.py @@ -1,4 +1,4 @@ -# Copyright 2015/2016 by Nedim Sabic (RabbitStack) +# Copyright 2016 by Nedim Sabic (RabbitStack) # All Rights Reserved. # http://rabbitstack.github.io @@ -15,13 +15,20 @@ # under the License. """ -Test filament description +Tests the on_next_kevent function. """ +kevents = [] def on_init(): - set_interval('10') - + interval(1) + columns(['Key', '#Seq']) + sort_by('#Seq') def on_next_kevent(kevent): - pass \ No newline at end of file + kevents.append({'key_name': kevent['kparams']['key_name'], 'seq': kevent['seq'], 'dip': kevent['kparams']['dip']}) + +def on_interval(): + for key in kevents: + add_row([key['key_name'], key['seq']]) + render_table() \ No newline at end of file diff --git a/tests/fixtures/filaments/test_filament.py b/pkg/filament/_fixtures/top_hives_io.py similarity index 76% rename from tests/fixtures/filaments/test_filament.py rename to pkg/filament/_fixtures/top_hives_io.py index 8b80e92db..b58fcc640 100644 --- a/tests/fixtures/filaments/test_filament.py +++ b/pkg/filament/_fixtures/top_hives_io.py @@ -1,4 +1,4 @@ -# Copyright 2015/2016 by Nedim Sabic (RabbitStack) +# Copyright 2016 by Nedim Sabic (RabbitStack) # All Rights Reserved. # http://rabbitstack.github.io @@ -15,18 +15,20 @@ # under the License. """ -Test filament description +Shows top registry hives by I/O activity. """ +import collections -def on_init(): - pass +hives = collections.Counter() -def on_next_kevent(kevent): - pass +def on_init(): + interval(4) + columns(['Hive', "1", ["d"]]) + sort_by("1") -def on_stop(): +def on_next_kevent(kevent): pass diff --git a/pkg/filament/_fixtures/top_keys_io_table.py b/pkg/filament/_fixtures/top_keys_io_table.py new file mode 100644 index 000000000..104e3cdb8 --- /dev/null +++ b/pkg/filament/_fixtures/top_keys_io_table.py @@ -0,0 +1,43 @@ +# Copyright 2016 by Nedim Sabic (RabbitStack) +# All Rights Reserved. +# http://rabbitstack.github.io + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Shows top registry keys by I/O activity. +""" + +import collections + +keys = collections.Counter() + + +def on_init(): + interval(1) + columns(['Key', '#Ops']) + sort_by('#Ops') + + +def on_next_kevent(kevent): + pass + + +def on_interval(): + keys.update(("HKLM\\SYSTEM\\ControlSet001\\Services\\WinSock2\\Parameters\\Protocol_Catalog9",)) + keys.update(("HKLM\\SYSTEM\\ControlSet001\\Services\\WinSock2\\Parameters\\Protocol_Catalog9",)) + keys.update(("HKLM\\SYSTEM\\ControlSet001\\Services\\WinSock2\\Parameters\\Protocol_Catalog9",)) + keys.update(("HKLM\\SYSTEM\\ControlSet001\\Control\\Nls\\Sorting\\Ids",)) + for key, count in keys.items(): + add_row([key, count]) + render_table() \ No newline at end of file diff --git a/filaments/top_hives_io.py b/pkg/filament/cpython/_fixtures/top_hives_io.py similarity index 77% rename from filaments/top_hives_io.py rename to pkg/filament/cpython/_fixtures/top_hives_io.py index d6ab4401e..9695ab4fe 100644 --- a/filaments/top_hives_io.py +++ b/pkg/filament/cpython/_fixtures/top_hives_io.py @@ -24,19 +24,22 @@ def on_init(): - set_filter('RegOpenKey', 'RegQueryKey', 'RegCreateKey') - columns(["Hive", "#Ops"]) - sort_by('#Ops') - set_interval(1) + set_interval(2) + print("on_init") + #set_filter('RegOpenKey or proc.name=java') + #columns(["Hive", "#Ops"]) + #sort_by('#Ops') + #set_interval(1) def on_next_kevent(kevent): - if '' not in kevent.params.hive: - hive = (kevent.params.hive, ) - hives.update(hive) - + print("KEVENT\n", kevent["kparams"]) + #raise Exception('eggs', 'eggs') def on_interval(): for hive, count in hives.items(): add_row([hive, count]) render_tabular() + +def sum(v): + return 1 + 1 \ No newline at end of file diff --git a/pkg/filament/cpython/api.c b/pkg/filament/cpython/api.c new file mode 100644 index 000000000..111a97441 --- /dev/null +++ b/pkg/filament/cpython/api.c @@ -0,0 +1,163 @@ +/* + * Copyright 2019-2020 by Nedim Sabic + * http://rabbitstack.github.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ + +#include "api.h" + +int PyArg_ParseInt(PyObject *args, int n) { + int i; + int res; + switch (n) { + case 1: + res = PyArg_ParseTuple(args, "i", &i); + break; + case 2: + res = PyArg_ParseTuple(args, "Oi", &i, &i); + break; + case 3: + res = PyArg_ParseTuple(args, "OOi", &i, &i, &i); + break; + default: + return i; + } + if (!res) { + PyErr_SetString(PyExc_ValueError, "argument must be an integer"); + return 0; + } + return i; +} + +PyObject* PyArg_ParseList(PyObject *args, int n) { + PyObject *ob; + int res; + switch (n) { + case 1: + res = PyArg_ParseTuple(args, "O!", &PyList_Type, &ob); + break; + case 2: + res = PyArg_ParseTuple(args, "OO!", &ob, &PyList_Type, &ob); + break; + case 3: + res = PyArg_ParseTuple(args, "OOO!", &ob, &ob, &PyList_Type, &ob); + break; + default: + return NULL; + } + if (!res) { + PyErr_SetString(PyExc_ValueError, "argument must be a list"); + return NULL; + } + return ob; +} + +PyObject* PyArg_ParseString(PyObject *args, int n) { + PyObject *ob; + int res; + switch (n) { + case 1: + res = PyArg_ParseTuple(args, "U", &ob); + break; + case 2: + res = PyArg_ParseTuple(args, "OU", &ob, &ob); + break; + case 3: + res = PyArg_ParseTuple(args, "OOU", &ob, &ob, &ob); + break; + default: + return NULL; + } + if (!res) { + PyErr_SetString(PyExc_ValueError, "argument must be a string"); + return NULL; + } + return ob; +} + +void PyArg_ParseKeywords(PyObject *args, PyObject *kwargs, char *kwlist[], PyObject **ob1, PyObject **ob2, PyObject **ob3, PyObject **ob4) { + int res; + + res = PyArg_ParseTupleAndKeywords(args, + kwargs, + "OO|$OO", kwlist, + ob1, ob2, ob3, ob4); + if (!res) { + PyErr_SetString(PyExc_ValueError, "parse keywords failed"); + } +} + +PyObject* PyTime_FromDateTime(int year, int month, int day, int hour, int minute, int second, int usecond) { + return PyDateTime_FromDateAndTime(year, month, day, hour, minute, second, usecond); +} + +PyObject* PyChar_FromChar(char v) { + return Py_BuildValue("b", v); +} + +PyObject* PyChar_FromUnsignedChar(unsigned char v) { + return Py_BuildValue("B", v); +} + +PyObject* PyShort_FromShort(short v) { + return Py_BuildValue("h", v); +} + +PyObject* PyShort_FromUnsignedShort(unsigned short v) { + return Py_BuildValue("H", v); +} + +bool Py_IsUnicode(PyObject *ob) { + if (ob == NULL) + return false; + return PyUnicode_CheckExact(ob); +} + +bool Py_IsInteger(PyObject *ob) { + if (ob == NULL) + return false; + return PyLong_CheckExact(ob); +} + +void Py_DateTimeImport() { + PyDateTime_IMPORT; +} + + +bool Py_IsDateTime(PyObject *ob) { + if (ob == NULL) + return false; + return PyDateTime_CheckExact(ob); +} + +const char* Py_Type(PyObject *ob) { + return Py_TYPE(ob)->tp_name; +} + +int PyDate_GetYear(PyObject *ob) { + return PyDateTime_GET_YEAR(ob); +} +int PyDate_GetMonth(PyObject *ob) { + return PyDateTime_GET_MONTH(ob); +} +int PyDate_GetDay(PyObject *ob) { + return PyDateTime_GET_DAY(ob); +} +int PyDate_GetHour(PyObject *ob) { + return PyDateTime_DATE_GET_HOUR(ob); +} +int PyDate_GetMinute(PyObject *ob) { + return PyDateTime_DATE_GET_MINUTE(ob); +} +int PyDate_GetSecond(PyObject *ob) { + return PyDateTime_DATE_GET_SECOND(ob); +} +int PyDate_GetMicroSecond(PyObject *ob) { + return PyDateTime_DATE_GET_MICROSECOND(ob); +} \ No newline at end of file diff --git a/pkg/filament/cpython/api.h b/pkg/filament/cpython/api.h new file mode 100644 index 000000000..62d322eba --- /dev/null +++ b/pkg/filament/cpython/api.h @@ -0,0 +1,58 @@ +/* + * Copyright 2019-2020 by Nedim Sabic + * http://rabbitstack.github.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ + +#include +#include +#include +#include "datetime.h" + +typedef char i8; +typedef unsigned char u8; +typedef short i16; +typedef unsigned short u16; +typedef long i32; +typedef long long i64; +typedef unsigned long long u64; +typedef unsigned long u32; + +/* +Cgo doesn't know how to deal with C macros/variadic functions. This is the main reason we need the wrapper for +some CPython API functions in pure C. +*/ + +int PyArg_ParseInt(PyObject *args, int n); +PyObject* PyArg_ParseString(PyObject *args, int n); +PyObject* PyArg_ParseList(PyObject *args, int n); + +void PyArg_ParseKeywords(PyObject *args, PyObject *kwargs, char *kwlist[], PyObject **ob1, PyObject **ob2, PyObject **ob3, PyObject **ob4); + +PyObject* PyTime_FromDateTime(int year, int month, int day, int hour, int minute, int second, int usecond); +PyObject* PyChar_FromChar(char v); +PyObject* PyChar_FromUnsignedChar(unsigned char v); +PyObject* PyShort_FromShort(short v); +PyObject* PyShort_FromUnsignedShort(unsigned short v); + +bool Py_IsUnicode(PyObject *ob); +bool Py_IsInteger(PyObject *ob); +bool Py_IsDateTime(PyObject *ob); + +void Py_DateTimeImport(); + +int PyDate_GetYear(PyObject *ob); +int PyDate_GetMonth(PyObject *ob); +int PyDate_GetDay(PyObject *ob); +int PyDate_GetHour(PyObject *ob); +int PyDate_GetMinute(PyObject *ob); +int PyDate_GetSecond(PyObject *ob); +int PyDate_GetMicroSecond(PyObject *ob); + +const char* Py_Type(PyObject *ob); \ No newline at end of file diff --git a/pkg/filament/cpython/dict.go b/pkg/filament/cpython/dict.go new file mode 100644 index 000000000..ddf0a2220 --- /dev/null +++ b/pkg/filament/cpython/dict.go @@ -0,0 +1,55 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cpython + +/* +#include "api.h" +*/ +import "C" + +// Dict represents the abstraction over the Python dictionary object. +type Dict struct { + *PyObject +} + +// NewDict constructs a new empty dictionary object. +func NewDict() *Dict { + return &Dict{PyObject: &PyObject{rawptr: C.PyDict_New()}} +} + +// NewDictFromObject constructs a new dictionary object from the existing dictionary. +func NewDictFromObject(o *PyObject) *Dict { + return &Dict{PyObject: o} +} + +// Insert inserts a value into the dictionary indexed with a key. Key must be hashable, otherwise TypeError is raised. +func (d *Dict) Insert(k, v *PyObject) { + C.PyDict_SetItem(d.rawptr, k.rawptr, v.rawptr) +} + +// Get returns the object from dictionary with the provided key. Returns a null object if the key key is not present, +// but without setting an exception. +func (d *Dict) Get(k *PyObject) *PyObject { + return &PyObject{rawptr: C.PyDict_GetItem(d.rawptr, k.rawptr)} +} + +// Object returns the underlying Python object reference. +func (d *Dict) Object() *PyObject { + return d.PyObject +} diff --git a/pkg/filament/cpython/dict_test.go b/pkg/filament/cpython/dict_test.go new file mode 100644 index 000000000..cd9139c63 --- /dev/null +++ b/pkg/filament/cpython/dict_test.go @@ -0,0 +1,36 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cpython + +import ( + "github.com/magiconair/properties/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestDict(t *testing.T) { + dict := NewDict() + require.False(t, dict.IsNull()) + + dict.Insert(PyUnicodeFromString("filename"), PyUnicodeFromString("C:\\Windows\\System32\\kernel32.dll")) + v := dict.Get(PyUnicodeFromString("filename")) + require.NotNil(t, v) + + assert.Equal(t, `'C:\\Windows\\System32\\kernel32.dll'`, v.String()) +} diff --git a/pkg/filament/cpython/errors.go b/pkg/filament/cpython/errors.go new file mode 100644 index 000000000..0247e1016 --- /dev/null +++ b/pkg/filament/cpython/errors.go @@ -0,0 +1,86 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cpython + +/* +#include "api.h" +*/ +import "C" +import ( + "errors" + "sync" +) + +var once sync.Once +var formatException *PyObject + +// FetchErr retrieves the error indicator into three variables whose addresses are passed. If the error indicator is not +// set, set all three variables to NULL. If it is set, it will be cleared and you own a reference to each object retrieved. +// The value and traceback object may be NULL even when the type object is not. +func FetchErr() error { + if C.PyErr_Occurred() == nil { + // error indicator not set, nothing to do + return nil + } + + exc := &PyObject{} + val := &PyObject{} + traceback := &PyObject{} + defer exc.DecRef() + defer val.DecRef() + defer traceback.DecRef() + + C.PyErr_Fetch(&exc.rawptr, &val.rawptr, &traceback.rawptr) + // normalize exception values as per python C API + C.PyErr_NormalizeException(&exc.rawptr, &val.rawptr, &traceback.rawptr) + + if !traceback.IsNull() { + once.Do(func() { + tb, _ := NewModule("traceback") + if tb != nil { + formatException, _ = tb.GetAttrString("format_exception") + } + }) + if !formatException.IsNull() { + ob := formatException.Call(exc, val, traceback) + if !ob.IsNull() { + defer ob.DecRef() + return errors.New(ob.String()) + } + } + return errors.New("can't format traceback exception") + } + if !val.IsNull() { + return errors.New(val.String()) + } + if !exc.IsNull() { + return errors.New(exc.String()) + } + return nil +} + +// ClearError clears the error indicator. +func ClearError() { + C.PyErr_Clear() +} + +// CheckSignal checks the signal queue. +func CheckSignals() bool { + return C.PyErr_CheckSignals() == -1 +} diff --git a/pkg/filament/cpython/gil.go b/pkg/filament/cpython/gil.go new file mode 100644 index 000000000..539325848 --- /dev/null +++ b/pkg/filament/cpython/gil.go @@ -0,0 +1,85 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cpython + +/* +#include "api.h" +*/ +import "C" +import ( + "fmt" + "runtime" + "sync/atomic" +) + +// PyGILState is the type alias for the native Python GIL state +type PyGILState C.PyGILState_STATE + +// GIL is responsible for interacting with the Python GIL. Goroutines are executed on +// multiple threads and the scheduler might decide to pause the goroutine on one thread +// and resume it later in a different thread. This would cause catastrophic effects if +// the Python interpreter finds out the GIL was acquired in one thread but released in a +// different one. We have to provide extra safety to avoid runtime crashes at the cost of +// sacrificing some performance since we'll always stick the goroutine to be scheduled on +// the same thread. +type GIL struct { + locked uint32 + state PyGILState + tstate *C.PyThreadState +} + +// NewGIL creates a new instance of the GIL manager. +func NewGIL() *GIL { + return &GIL{} +} + +// SaveThread releases the global interpreter lock (if it has been created and thread +// support is enabled) and reset the thread state to NULL, returning the +// previous thread state (which is not NULL). +func (g *GIL) SaveThread() { + g.tstate = C.PyEval_SaveThread() +} + +// RestoreThread acquire the global interpreter lock (if it has been created and thread +// support is enabled) and set the thread state to tstate, which must not be +// NULL. If the lock has been created, the current thread must not have +// acquired it, otherwise deadlock ensues. +func (g *GIL) RestoreThread() { + C.PyEval_RestoreThread(g.tstate) +} + +// Lock acquires the GIL lock by ensuring the current thread is ready to call Python C API. +func (g *GIL) Lock() { + runtime.LockOSThread() + atomic.StoreUint32(&g.locked, 1) + g.state = PyGILState(C.PyGILState_Ensure()) +} + +// Unlock releases the lock on the GIL. +func (g *GIL) Unlock() { + atomic.StoreUint32(&g.locked, 0) + C.PyGILState_Release(C.PyGILState_STATE(g.state)) + runtime.UnlockOSThread() +} + +// Locked indicates if the lock on the GIL was acquired. +func (g *GIL) Locked() bool { + fmt.Println(atomic.LoadUint32(&g.locked) > 0) + return atomic.LoadUint32(&g.locked) > 0 +} diff --git a/pkg/filament/cpython/gil_test.go b/pkg/filament/cpython/gil_test.go new file mode 100644 index 000000000..38e49b24f --- /dev/null +++ b/pkg/filament/cpython/gil_test.go @@ -0,0 +1,42 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cpython + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestGILLock(t *testing.T) { + require.NoError(t, Initialize()) + defer Finalize() + gil := NewGIL() + gil.Lock() + require.True(t, gil.Locked()) +} + +func TestGILUnlock(t *testing.T) { + require.NoError(t, Initialize()) + defer Finalize() + gil := NewGIL() + gil.Lock() + require.True(t, gil.Locked()) + gil.Unlock() + require.False(t, gil.Locked()) +} diff --git a/pkg/filament/cpython/interpreter.go b/pkg/filament/cpython/interpreter.go new file mode 100644 index 000000000..86cc6e32c --- /dev/null +++ b/pkg/filament/cpython/interpreter.go @@ -0,0 +1,91 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cpython + +/* +#cgo pkg-config: python-37 + +#include "api.h" + +*/ +import "C" +import ( + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "syscall" + "unsafe" +) + +// ErrPyInit signals that an error ocurred while initializing the Python interpreter +var ErrPyInit = errors.New("couldn't initialize the Python interpreter") + +// ErrGILInit indicates that the global interpreter lock failed to initialize +var ErrGILInit = errors.New("unable to initialize the GIL") + +// Initialize initializes the Python interpreter and its global interpreter lock (GIL). +func Initialize() error { + if C.Py_IsInitialized() == 0 { + // initialize the interpreter without signal handlers + C.Py_InitializeEx(0) + } + if C.Py_IsInitialized() == 0 { + return ErrPyInit + } + if C.PyEval_ThreadsInitialized() == 0 { + C.PyEval_InitThreads() + } + if C.PyEval_ThreadsInitialized() == 0 { + return ErrGILInit + } + // this calls into PyDateTime_IMPORT macro to initialize the PyDateTimeAPI + C.Py_DateTimeImport() + + if err := initializeIpFnAndClasses(); err != nil { + log.Warn(err) + } + + return nil +} + +// Finalize undos all initializations made by Initialize() and subsequent use of Python/C API functions, +// and destroys all sub-interpreters that were created and not yet destroyed since the last call to Initialize(). +// Ideally, this frees all memory allocated by the Python interpreter. +func Finalize() { + C.Py_Finalize() +} + +// AddPythonPath adds a new path to the PYTHONPATH environment variable. +func AddPythonPath(path string) { + syspath := C.CString("path") + defer C.free(unsafe.Pointer(syspath)) + newPath := PyUnicodeFromString(path) + defer newPath.DecRef() + C.PyList_Append(C.PySys_GetObject(syspath), newPath.rawptr) +} + +// SetPath sets the default module search path. If this function is called before Py_Initialize(), then Py_GetPath() +// won’t attempt to compute a default search path but uses the one provided instead. This is useful if Python is +// embedded by an application that has full knowledge of the location of all modules. +func SetPath(path string) { + w, err := syscall.UTF16FromString(path) + if err != nil { + return + } + C.Py_SetPath((*C.wchar_t)(&w[0])) +} diff --git a/pkg/filament/cpython/interpreter_test.go b/pkg/filament/cpython/interpreter_test.go new file mode 100644 index 000000000..5823b1524 --- /dev/null +++ b/pkg/filament/cpython/interpreter_test.go @@ -0,0 +1,29 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cpython + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestInitialize(t *testing.T) { + require.NoError(t, Initialize()) + Finalize() +} diff --git a/pkg/filament/cpython/ip.go b/pkg/filament/cpython/ip.go new file mode 100644 index 000000000..d493d3ebb --- /dev/null +++ b/pkg/filament/cpython/ip.go @@ -0,0 +1,50 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cpython + +import "errors" + +var ipv4Class *PyObject +var ipv6Class *PyObject +var ipaddressFn *PyObject + +func initializeIpFnAndClasses() error { + mod, err := NewModule("ipaddress") + if err != nil { + return err + } + if mod.IsNull() { + return errors.New("ipaddress module was not initialized") + } + + ipaddressFn, err = mod.GetAttrString("ip_address") + if err != nil { + return err + } + ipv4Class, err = mod.GetAttrString("IPv4Address") + if err != nil { + return err + } + ipv6Class, err = mod.GetAttrString("IPv6Address") + if err != nil { + return err + } + + return nil +} diff --git a/pkg/filament/cpython/module.go b/pkg/filament/cpython/module.go new file mode 100644 index 000000000..b87889816 --- /dev/null +++ b/pkg/filament/cpython/module.go @@ -0,0 +1,88 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cpython + +/* +#include +#include "api.h" +*/ +import "C" +import ( + "fmt" + "syscall" + "unsafe" +) + +type Module struct { + *PyObject + name string +} + +// NewModule imports a module leaving the globals and locals arguments set to NULL and level set to 0. When the name argument contains +// a dot (when it specifies a submodule of a package), the fromlist argument is set to the list ['*'] so that the return +// value is the named module rather than the top-level package containing it as would otherwise be the case. +// (Unfortunately, this has an additional side effect when name in fact specifies a subpackage instead of a submodule: +// the submodules specified in the package’s __all__ variable are loaded.) Return a new reference to the imported module, +// or NULL with an exception set on failure. A failing import of a module doesn't leave the module in sys.modules. +func NewModule(name string) (*Module, error) { + n := C.CString(name) + defer C.free(unsafe.Pointer(n)) + mod := C.PyImport_ImportModule(n) + if mod == nil { + return nil, fmt.Errorf("couldn't import %q module", name) + } + return &Module{ + PyObject: &PyObject{rawptr: mod}, + name: name, + }, nil +} + +// MethFlags is the type alias for the method flags +type MethFlags int + +const ( + MethVarArgs MethFlags = C.METH_VARARGS + MethKeyWords MethFlags = C.METH_KEYWORDS + MethNoArgs MethFlags = C.METH_NOARGS +) + +// DefaultMethFlags represents the default method flags +var DefaultMethFlags = MethVarArgs | MethKeyWords + +// declared globally to guard PyMethodDef structures from the garbage collector +var defs = map[string]*C.PyMethodDef{} + +// RegisterFn anchors the function to this module. The callable Python object is built from the method definition +// that specifies the function name, the args expected by the function and the pointer to the Go callback. +func (m *Module) RegisterFn(name string, fn interface{}, flags MethFlags) error { + n := C.CString(name) + defer C.free(unsafe.Pointer(n)) + defs[name] = &C.PyMethodDef{ + ml_name: n, + ml_meth: (C.PyCFunction)(unsafe.Pointer(syscall.NewCallback(fn))), + ml_flags: C.int(flags), + } + mod := C.CString(m.name) + defer C.free(unsafe.Pointer(mod)) + f := C.PyCFunction_NewEx((*C.PyMethodDef)(unsafe.Pointer(defs[name])), m.rawptr, C.PyUnicode_FromString(mod)) + if f == nil { + return fmt.Errorf("unable to attach the %s function to the %s module", name, m.name) + } + return m.SetAttrString(name, f) +} diff --git a/pkg/filament/cpython/module_test.go b/pkg/filament/cpython/module_test.go new file mode 100644 index 000000000..cb16d64cc --- /dev/null +++ b/pkg/filament/cpython/module_test.go @@ -0,0 +1,60 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cpython + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestNewModule(t *testing.T) { + require.NoError(t, Initialize()) + defer Finalize() + AddPythonPath("_fixtures/") + mod, err := NewModule("top_hives_io") + require.NoError(t, err) + require.NotNil(t, mod) +} + +func TestModuleRegisterFn(t *testing.T) { + require.NoError(t, Initialize()) + defer Finalize() + AddPythonPath("_fixtures/") + mod, err := NewModule("top_hives_io") + require.NoError(t, err) + require.NotNil(t, mod) + + f := func() uintptr { return 0 } + + err = mod.RegisterFn("set_interval", f, MethVarArgs) + require.NoError(t, err) + + fn, err := mod.GetAttrString("set_interval") + require.NoError(t, err) + require.False(t, fn.IsNull()) + require.True(t, fn.IsCallable()) + fn.Call() + + fn, err = mod.GetAttrString("sum") + require.NoError(t, err) + require.False(t, fn.IsNull()) + require.True(t, fn.IsCallable()) + r := fn.Call(PyUnicodeFromString("test")) + require.False(t, r.IsNull()) +} diff --git a/pkg/filament/cpython/object.go b/pkg/filament/cpython/object.go new file mode 100644 index 000000000..928320b9d --- /dev/null +++ b/pkg/filament/cpython/object.go @@ -0,0 +1,379 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cpython + +/* +#include "api.h" +*/ +import "C" +import ( + "errors" + "fmt" + "github.com/rabbitstack/fibratus/pkg/fs" + "net" + "syscall" + "time" + "unsafe" +) + +var errTypeNotList = errors.New("couldn't parse the argument. It is probably not a list") + +// PyRawObject is the type alias for the raw Python object pointer. +type PyRawObject *C.PyObject + +// PyArgs represents the alias for the Python positional arguments. +type PyArgs uintptr + +// PyKwargs represents the alias for the Python keyword arguments. +type PyKwargs uintptr + +// GetInt returns the nth positional argument as an integer. +func (args PyArgs) GetInt(n uint8) int { + return int(C.PyArg_ParseInt((*C.PyObject)(unsafe.Pointer(args)), C.int(n))) +} + +// GetString returns the nth positional argument as a string. +func (args PyArgs) GetString(n uint8) string { + return fromRawOb(C.PyArg_ParseString((*C.PyObject)(unsafe.Pointer(args)), C.int(n))).String() +} + +// GetStringSlice returns the nth argument as a slice of string values. +func (args PyArgs) GetStringSlice(n uint8) ([]string, error) { + ob := C.PyArg_ParseList((*C.PyObject)(unsafe.Pointer(args)), C.int(n)) + if ob == nil { + return nil, errTypeNotList + } + l := int(C.PyList_Size(ob)) + s := make([]string, l) + for i := 0; i < l; i++ { + item := C.PyList_GetItem(ob, C.longlong(i)) + if item == nil { + continue + } + isUnicode := bool(C.Py_IsUnicode(item)) + if !isUnicode { + continue + } + s[i] = fromRawOb(item).String() + } + return s, nil +} + +// GetSlice returns the nth argument as a slice of generic values. +func (args PyArgs) GetSlice(n uint8) ([]interface{}, error) { + ob := C.PyArg_ParseList((*C.PyObject)(unsafe.Pointer(args)), C.int(n)) + if ob == nil { + return nil, errTypeNotList + } + l := int(C.PyList_Size(ob)) + if l < 0 { + return nil, nil + } + s := make([]interface{}, l) + for i := 0; i < l; i++ { + item := C.PyList_GetItem(ob, C.longlong(i)) + if item == nil { + continue + } + switch { + case bool(C.Py_IsUnicode(item)): + s[i] = fromRawOb(item).String() + case bool(C.Py_IsInteger(item)): + s[i] = fromRawOb(item).Int() + case bool(C.Py_IsDateTime(item)): + s[i] = fromRawOb(item).Time() + default: + if ipv4Class != nil { + if !ipv4Class.IsNull() && C.PyObject_IsInstance(item, ipv4Class.rawptr) > 0 { + s[i] = net.ParseIP(fromRawOb(item).String()) + } + } + if ipv6Class != nil { + if !ipv6Class.IsNull() && C.PyObject_IsInstance(item, ipv6Class.rawptr) > 0 { + s[i] = net.ParseIP(fromRawOb(item).String()) + } + } + } + } + return s, nil +} + +// PyArgsParseKeywords parses tuple and keywords arguments. +func PyArgsParseKeywords(args PyArgs, kwargs PyKwargs, kwlist []string) (string, string, string, []string) { + var ( + ob1 *C.PyObject + ob2 *C.PyObject + ob3 *C.PyObject + ob4 *C.PyObject + ) + + klist := make([]*C.char, len(kwlist)+1) + + for i, k := range kwlist { + klist[i] = C.CString(k) + defer C.free(unsafe.Pointer(klist[i])) + } + + C.PyArg_ParseKeywords( + (*C.PyObject)(unsafe.Pointer(args)), + (*C.PyObject)(unsafe.Pointer(kwargs)), + &klist[0], + (**C.PyObject)(unsafe.Pointer(&ob1)), + (**C.PyObject)(unsafe.Pointer(&ob2)), + (**C.PyObject)(unsafe.Pointer(&ob3)), + (**C.PyObject)(unsafe.Pointer(&ob4)), + ) + + return fromRawOb(ob1).String(), fromRawOb(ob2).String(), fromRawOb(ob3).String(), fromRawOb(ob4).StringSlice() +} + +// PyObject is the main abstraction for manipulating the native CPython objects. +type PyObject struct { + rawptr *C.PyObject +} + +// NewPyNone creates a new none Python object. +func NewPyNone() *C.PyObject { + return C.Py_None +} + +func NewPyLong(v int64) *C.PyObject { + return C.PyLong_FromLongLong(C.i64(v)) +} + +// NewPyObjectFromFmt builds a new Python object based on the underlying interface type. +func NewPyObjectFromValue(value interface{}) *PyObject { + var ob *C.PyObject + switch v := value.(type) { + case int8: + ob = C.PyChar_FromChar(C.i8(v)) + case uint8: + ob = C.PyChar_FromUnsignedChar(C.u8(v)) + case int16: + ob = C.PyShort_FromShort(C.i16(v)) + case uint16: + ob = C.PyShort_FromUnsignedShort(C.u16(v)) + case int32: + ob = C.PyLong_FromLong(C.i32(v)) + case uint32: + ob = C.PyLong_FromUnsignedLong(C.u32(v)) + case int64: + ob = C.PyLong_FromLongLong(C.i64(v)) + case uint64: + ob = C.PyLong_FromUnsignedLongLong(C.u64(v)) + case string: + ob = PyUnicodeFromString(v).asRaw() + case fs.FileDisposition: + ob = PyUnicodeFromString(v.String()).asRaw() + case time.Time: + ob = C.PyTime_FromDateTime(C.int(v.Year()), C.int(v.Month()), C.int(v.Day()), C.int(v.Hour()), C.int(v.Minute()), C.int(v.Second()), C.int(v.Nanosecond()/1000)) + case net.IP: + if !ipaddressFn.IsNull() { + ob = ipaddressFn.Call(PyUnicodeFromString(v.String())).asRaw() + } else { + ob = PyUnicodeFromString(v.String()).asRaw() + } + case func(arg1, arg2 PyArgs) PyRawObject: + n := C.CString("func") + defer C.free(unsafe.Pointer(n)) + mdef := &C.PyMethodDef{ + ml_name: n, + ml_meth: (C.PyCFunction)(unsafe.Pointer(syscall.NewCallback(v))), + ml_flags: C.int(DefaultMethFlags), + } + ob = C.PyCFunction_NewEx((*C.PyMethodDef)(unsafe.Pointer(mdef)), nil, C.PyUnicode_FromString(n)) + } + return &PyObject{rawptr: ob} +} + +// fromRawOb builds a new Python object from the raw pointer. +func fromRawOb(ob *C.PyObject) *PyObject { return &PyObject{rawptr: ob} } + +// DecRef decrements the reference count for object o. If the object is NULL, nothing happens. If the reference count +// reaches zero, the object’s type’s deallocation function (which must not be NULL) is invoked. +func (ob *PyObject) DecRef() { + if ob != nil || ob.rawptr == nil { + return + } + C.Py_DecRef(ob.rawptr) +} + +// IncRef increment the reference count for object o. The object may be NULL, in which case this method has no effect. +func (ob *PyObject) IncRef() { + if ob != nil && ob.rawptr == nil { + return + } + C.Py_IncRef(ob.rawptr) +} + +// IsNull determines whether this object's instance is null. +func (ob *PyObject) IsNull() bool { + if ob == nil { + return true + } + return ob.rawptr == nil +} + +func (ob *PyObject) asRaw() *C.PyObject { + return ob.rawptr +} + +// SetAttrString set the value of the attribute provided for this object to the specified value. +func (ob *PyObject) SetAttrString(name string, value *C.PyObject) error { + attr := C.CString(name) + defer C.free(unsafe.Pointer(attr)) + err := int(C.PyObject_SetAttrString(ob.rawptr, attr, value)) + if err == -1 { + return fmt.Errorf("couldn't set the value of the %q attribute", name) + } + return nil +} + +// GetAttrString retrieves an attribute named from object the object. Returns an error if the attribute can't be fetched. +func (ob *PyObject) GetAttrString(name string) (*PyObject, error) { + attr := C.CString(name) + defer C.free(unsafe.Pointer(attr)) + v := C.PyObject_GetAttrString(ob.rawptr, attr) + if v == nil { + return nil, fmt.Errorf("couldn't get the %q attribute", name) + } + return &PyObject{rawptr: v}, nil +} + +// HasAttr determines if the Python object has the specified attribute. +func (ob *PyObject) HasAttr(name string) bool { + attr := C.CString(name) + defer C.free(unsafe.Pointer(attr)) + return C.PyObject_HasAttrString(ob.rawptr, attr) > 0 +} + +var encoding = C.CString("utf-8") +var codecErrors = C.CString("strict") + +// String encodes an Unicode object and returns the result as a Python bytes object converted to the Go string. +func (ob *PyObject) String() string { + if ob.rawptr == nil { + return "" + } + repr := C.PyObject_Str(ob.rawptr) + if repr == nil { + return "" + } + defer C.Py_DecRef(repr) + s := C.PyUnicode_AsEncodedString(repr, encoding, codecErrors) + if s == nil { + return "invalid Unicode string" + } + defer C.Py_DecRef(s) + return C.GoString(C.PyBytes_AsString(s)) +} + +// StringSlice returns this object as a string slice. +func (ob *PyObject) StringSlice() []string { + if ob.rawptr == nil { + return []string{} + } + l := int(C.PyList_Size(ob.asRaw())) + if l < 0 { + return nil + } + s := make([]string, l) + for i := 0; i < l; i++ { + item := C.PyList_GetItem(ob.asRaw(), C.longlong(i)) + if item == nil { + continue + } + isUnicode := bool(C.Py_IsUnicode(item)) + if !isUnicode { + continue + } + s[i] = fromRawOb(item).String() + } + return s +} + +// Uint32 returns an uint32 integer from the raw Python object. +func (ob *PyObject) Uint32() uint32 { + return uint32(C.PyLong_AsUnsignedLong(ob.rawptr)) +} + +// Uint64 returns an uint64 integer from the raw Python object. +func (ob *PyObject) Uint64() uint64 { + return uint64(C.PyLong_AsUnsignedLongLong(ob.rawptr)) +} + +// Uint64 returns an integer from the raw Python object. +func (ob *PyObject) Int() int { + return int(C.PyLong_AsUnsignedLongLong(ob.rawptr)) +} + +// Time returns the time from the raw Python object. +func (ob *PyObject) Time() time.Time { + year := int(C.PyDate_GetYear((*C.PyObject)(unsafe.Pointer(ob)))) + month := int(C.PyDate_GetMonth((*C.PyObject)(unsafe.Pointer(ob)))) + day := int(C.PyDate_GetDay((*C.PyObject)(unsafe.Pointer(ob)))) + hour := int(C.PyDate_GetHour((*C.PyObject)(unsafe.Pointer(ob)))) + minute := int(C.PyDate_GetMinute((*C.PyObject)(unsafe.Pointer(ob)))) + second := int(C.PyDate_GetSecond((*C.PyObject)(unsafe.Pointer(ob)))) + microsecond := int(C.PyDate_GetMicroSecond((*C.PyObject)(unsafe.Pointer(ob)))) + return time.Date(year, time.Month(month), day, hour, minute, second, microsecond*1000, time.Local) +} + +// Type returns the Python type representation. +func (ob *PyObject) Type() string { + return C.GoString(C.Py_Type(ob.rawptr)) +} + +// IsCallable determines if the object is callable. +func (ob *PyObject) IsCallable() bool { + return C.PyCallable_Check(ob.rawptr) > 0 +} + +// CallableArgCount returns the number of arguments declared in the callable Python object. +func (ob *PyObject) CallableArgCount() uint32 { + fnCode, err := ob.GetAttrString("__code__") + if err != nil || fnCode.IsNull() { + return 0 + } + defer fnCode.DecRef() + count, err := fnCode.GetAttrString("co_argcount") + if err != nil { + return 0 + } + defer count.DecRef() + return count.Uint32() +} + +// Call calls a callable Python object with arguments given by the tuple args. If no arguments are needed, then args +// may be NULL. Returns the result of the call on success, or a null reference on failure. +func (ob *PyObject) Call(args ...*PyObject) *PyObject { + if ob.rawptr == nil { + return nil + } + if len(args) == 0 { + return &PyObject{rawptr: C.PyObject_CallObject(ob.rawptr, nil)} + } + tuple := NewTuple(len(args)) + for pos, arg := range args { + tuple.Set(pos, arg) + } + defer tuple.DecRef() + r := C.PyObject_CallObject(ob.rawptr, tuple.rawptr) + return &PyObject{rawptr: r} +} diff --git a/pkg/filament/cpython/sequence.go b/pkg/filament/cpython/sequence.go new file mode 100644 index 000000000..25047f0d0 --- /dev/null +++ b/pkg/filament/cpython/sequence.go @@ -0,0 +1,39 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cpython + +/* +#include "api.h" +*/ +import "C" + +// Tuple represents the Python tuple sequence object. +type Tuple struct { + *PyObject +} + +// NewTuple constructs a new tuple object of the provided size. +func NewTuple(size int) *Tuple { + return &Tuple{PyObject: &PyObject{rawptr: C.PyTuple_New(C.Py_ssize_t(size))}} +} + +// Set inserts a reference to object at specified position of the tuple. +func (t *Tuple) Set(pos int, ob *PyObject) { + C.PyTuple_SetItem(t.rawptr, C.Py_ssize_t(pos), ob.rawptr) +} diff --git a/pkg/filament/cpython/string.go b/pkg/filament/cpython/string.go new file mode 100644 index 000000000..18a9042c2 --- /dev/null +++ b/pkg/filament/cpython/string.go @@ -0,0 +1,32 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cpython + +/* + #include "api.h" +*/ +import "C" +import "unsafe" + +// PyUnicodeFromString creates the Python Unicode object from the Go string. +func PyUnicodeFromString(s string) *PyObject { + u := C.CString(s) + defer C.free(unsafe.Pointer(u)) + return &PyObject{rawptr: C.PyUnicode_FromString(u)} +} diff --git a/pkg/filament/filament.go b/pkg/filament/filament.go new file mode 100644 index 000000000..2c5085b0b --- /dev/null +++ b/pkg/filament/filament.go @@ -0,0 +1,554 @@ +// +build filament + +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filament + +import ( + "errors" + "expvar" + "fmt" + "github.com/rabbitstack/fibratus/pkg/alertsender" + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/filament/cpython" + "github.com/rabbitstack/fibratus/pkg/filter" + "github.com/rabbitstack/fibratus/pkg/handle" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/util/multierror" + "github.com/rabbitstack/fibratus/pkg/util/term" + log "github.com/sirupsen/logrus" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" + // initialize alert senders + _ "github.com/rabbitstack/fibratus/pkg/alertsender/mail" + _ "github.com/rabbitstack/fibratus/pkg/alertsender/slack" +) + +// pyver designates the current Python version +const pyver = "37" + +// useEmbeddedPython instructs the filament engine to use the embedded Python distribution. +var useEmbeddedPython = true + +const ( + intervalFn = "interval" + columnsFn = "columns" + sortbyFn = "sort_by" + kfilterFn = "kfilter" + addRowFn = "add_row" + maxRowsFn = "max_rows" + titleFn = "title" + renderTableFn = "render_table" + findHandleFn = "find_handle" + findHandlesFn = "find_handles" + findProcessFn = "find_process" + findProcessesFn = "find_processes" + emitAlertFn = "emit_alert" + + onInitFn = "on_init" + onStopFn = "on_stop" + onNextKeventFn = "on_next_kevent" + onIntervalFn = "on_interval" + doc = "__doc__" +) + +var ( + keventErrors = expvar.NewMap("filament.kevent.errors") + keventProcessErrors = expvar.NewInt("filament.kevent.process.errors") + kdictErrors = expvar.NewInt("filament.kdict.errors") + batchFlushes = expvar.NewInt("filament.kevent.batch.flushes") + + errFilamentsDir = func(path string) error { return fmt.Errorf("%s does not exist or is not a directory", path) } + + errNoDoc = errors.New("filament description is required") + errNoOnNextKevent = errors.New("required on_next_kevent function is not defined") + errOnNextKeventNotCallable = errors.New("on_next_kevent is not callable") + errOnNextKeventMismatchArgs = func(c uint32) error { return fmt.Errorf("expected 1 argument for on_next_kevent but found %d args", c) } + + tableOutput io.Writer +) + +type kbatch []*kevent.Kevent + +func (k *kbatch) append(kevt *kevent.Kevent) { + if *k == nil { + *k = make([]*kevent.Kevent, 0) + } + *k = append(*k, kevt) +} + +func (k *kbatch) reset() { *k = nil } +func (k kbatch) len() int { return len(k) } + +type filament struct { + name string + sortBy string + interval time.Duration + columns []string + fexpr string + fnerrs chan error + close chan struct{} + gil *cpython.GIL + + tick *time.Ticker + mod *cpython.Module + + config config.FilamentConfig + + psnap ps.Snapshotter + hsnap handle.Snapshotter + filter filter.Filter + + initErrors []error + + onNextKevent *cpython.PyObject + onStop *cpython.PyObject + + table tab +} + +// New creates a new instance of the filament by starting an embedded Python interpreter. It imports the filament +// module and anchors required functions for controlling the filament options as well as providing the access to +// the kernel event flow. +func New( + name string, + psnap ps.Snapshotter, + hsnap handle.Snapshotter, + config config.FilamentConfig, +) (Filament, error) { + if useEmbeddedPython { + exe, err := os.Executable() + if err != nil { + return nil, err + } + pylib := filepath.Join(filepath.Dir(exe), "..", "Python", fmt.Sprintf("python%s.zip", pyver)) + if _, err := os.Stat(pylib); err != nil { + return nil, fmt.Errorf("python lib not found: %v", err) + } + // set the default module search path so it points to our embedded Python distribution + cpython.SetPath(pylib) + } + + // initialize the Python interpreter + if err := cpython.Initialize(); err != nil { + return nil, err + } + + // set the PYTHON_PATH to the filaments directory so the interpreter + // is aware of our filament module prior to its loading + path := config.Path + fstat, err := os.Stat(path) + if err != nil || !fstat.IsDir() { + return nil, errFilamentsDir(path) + } + filaments, err := ioutil.ReadDir(path) + if err != nil { + return nil, err + } + // check if the filament is present in the directory + var exists bool + for _, f := range filaments { + if strings.TrimSuffix(f.Name(), filepath.Ext(f.Name())) == name { + exists = true + } + } + + if !exists { + return nil, fmt.Errorf("%q filament does not exist. Run 'fibratus list filaments' to view available filaments", name) + } + + cpython.AddPythonPath(path) + mod, err := cpython.NewModule(name) + if err != nil { + if err = cpython.FetchErr(); err != nil { + return nil, err + } + return nil, err + } + + // ensure required attributes are present before proceeding with + // further initialization. For instance, if the documentation + // string is not provided, on_next_kevent function is missing + // or has a wrong signature we won't run the filament + doc, err := mod.GetAttrString(doc) + if err != nil || doc.IsNull() { + return nil, errNoDoc + } + defer doc.DecRef() + if !mod.HasAttr(onNextKeventFn) { + return nil, errNoOnNextKevent + } + onNextKevent, err := mod.GetAttrString(onNextKeventFn) + if err != nil || onNextKevent.IsNull() { + return nil, errNoOnNextKevent + } + if !onNextKevent.IsCallable() { + return nil, errOnNextKeventNotCallable + } + argCount := onNextKevent.CallableArgCount() + if argCount != 1 { + return nil, errOnNextKeventMismatchArgs(argCount) + } + + f := &filament{ + name: name, + mod: mod, + config: config, + psnap: psnap, + hsnap: hsnap, + close: make(chan struct{}, 1), + fnerrs: make(chan error, 100), + gil: cpython.NewGIL(), + columns: make([]string, 0), + onNextKevent: onNextKevent, + interval: time.Second, + initErrors: make([]error, 0), + table: newTable(), + } + + if mod.HasAttr(onStopFn) { + f.onStop, _ = mod.GetAttrString(onStopFn) + } + // register all the functions for interacting with filament + // within the Python module + err = f.mod.RegisterFn(addRowFn, f.addRowFn, cpython.DefaultMethFlags) + if err != nil { + return nil, err + } + err = f.mod.RegisterFn(renderTableFn, f.renderTableFn, cpython.MethNoArgs) + if err != nil { + return nil, err + } + err = f.mod.RegisterFn(titleFn, f.titleFn, cpython.DefaultMethFlags) + if err != nil { + return nil, err + } + err = f.mod.RegisterFn(sortbyFn, f.sortByFn, cpython.DefaultMethFlags) + if err != nil { + return nil, err + } + err = f.mod.RegisterFn(maxRowsFn, f.maxRowsFn, cpython.DefaultMethFlags) + if err != nil { + return nil, err + } + err = f.mod.RegisterFn(columnsFn, f.columnsFn, cpython.DefaultMethFlags) + if err != nil { + return nil, err + } + err = f.mod.RegisterFn(kfilterFn, f.kfilterFn, cpython.DefaultMethFlags) + if err != nil { + return nil, err + } + err = f.mod.RegisterFn(intervalFn, f.intervalFn, cpython.DefaultMethFlags) + if err != nil { + return nil, err + } + err = f.mod.RegisterFn(emitAlertFn, f.emitAlertFn, cpython.DefaultMethFlags) + if err != nil { + return nil, err + } + err = f.mod.RegisterFn(findHandleFn, f.findHandleFn, cpython.DefaultMethFlags) + if err != nil { + return nil, err + } + err = f.mod.RegisterFn(findHandlesFn, f.findHandlesFn, cpython.DefaultMethFlags) + if err != nil { + return nil, err + } + err = f.mod.RegisterFn(findProcessFn, f.findProcessFn, cpython.DefaultMethFlags) + if err != nil { + return nil, err + } + err = f.mod.RegisterFn(findProcessesFn, f.findProcessesFn, cpython.DefaultMethFlags) + if err != nil { + return nil, err + } + // invoke the on_init function if it has been declared in the filament + if mod.HasAttr(onInitFn) { + onInit, _ := mod.GetAttrString(onInitFn) + if !onInit.IsNull() { + onInit.Call() + if err := cpython.FetchErr(); err != nil { + return nil, fmt.Errorf("filament init error: %v", err) + } + if len(f.initErrors) > 0 { + return nil, multierror.Wrap(f.initErrors) + } + } + } + + // initialize the console frame buffer + var fb io.Writer + if len(f.columns) > 0 { + fb, err = term.NewFrameBuffer() + if err != nil { + return nil, fmt.Errorf("couldn't create console frame buffer: %v", err) + } + } + if fb != nil { + f.table.setWriter(fb) + f.table.setColumnConfigs(f.columns, term.GetColumns()/2+15) + } else if tableOutput != nil { + f.table.setWriter(tableOutput) + } else { + f.table.setWriter(os.Stdout) + } + if len(f.columns) > 0 && f.sortBy != "" { + var sortBy bool + for _, col := range f.columns { + if col == f.sortBy { + sortBy = true + break + } + } + if !sortBy { + return nil, fmt.Errorf("%s column can't be sorted since it is not defined", f.sortBy) + } + } + + // compile filter from the expression + if f.fexpr != "" { + f.filter = filter.New(f.fexpr) + if err := f.filter.Compile(); err != nil { + return nil, err + } + } + // if on_interval function has been declared in the module, we'll + // schedule the ticker to the interval value set during filament + // bootstrap in on_init function or otherwise we'll use the default interval + if mod.HasAttr(onIntervalFn) { + onInterval, err := mod.GetAttrString(onIntervalFn) + if err == nil && !onInterval.IsNull() { + f.tick = time.NewTicker(f.interval) + go f.onInterval(onInterval) + } + } + // we acquired the GIL as a side effect of threading initialization (the call to cpython.Initialize()) + // but now we have to reset the current thread state and release the GIL. It is the responsibility of + // the caller to acquire the GIL before executing any Python code from now on + f.gil.SaveThread() + + return f, nil +} + +func (f *filament) Run(kevents chan *kevent.Kevent, errs chan error) error { + var batch kbatch + var flusher = time.NewTicker(time.Second) + for { + select { + case <-f.close: + flusher.Stop() + return nil + default: + } + + select { + case kevt := <-kevents: + batch.append(kevt) + case err := <-errs: + keventErrors.Add(err.Error(), 1) + case <-flusher.C: + batchFlushes.Add(1) + if batch.len() > 0 { + err := f.pushKevents(batch) + if err != nil { + log.Warnf("on_next_kevent failed: %v", err) + keventProcessErrors.Add(1) + } + batch.reset() + } + case err := <-f.fnerrs: + return err + case <-f.close: + flusher.Stop() + return nil + } + } +} + +func (f *filament) pushKevents(b kbatch) error { + f.gil.Lock() + defer f.gil.Unlock() + for _, kevt := range b { + kdict, err := newKDict(kevt) + kevt.Release() + if err != nil { + kdict.DecRef() + kdictErrors.Add(1) + continue + } + r := f.onNextKevent.Call(kdict.Object()) + if r != nil { + r.DecRef() + } + kdict.DecRef() + if err := cpython.FetchErr(); err != nil { + return err + } + } + return nil +} + +func (f *filament) Close() error { + if f.onStop != nil && !f.onStop.IsNull() { + f.gil.Lock() + f.onStop.Call() + f.gil.Unlock() + } + f.close <- struct{}{} + if f.tick != nil { + f.tick.Stop() + } + return nil +} + +func (f *filament) Filter() filter.Filter { return f.filter } + +func (f *filament) intervalFn(_, args cpython.PyArgs) cpython.PyRawObject { + f.interval = time.Second * time.Duration(args.GetInt(1)) + if f.interval == 0 { + f.initErrors = append(f.initErrors, errors.New("invalid interval value specified")) + } + return cpython.NewPyNone() +} + +func (f *filament) sortByFn(_, args cpython.PyArgs) cpython.PyRawObject { + f.sortBy = args.GetString(1) + f.table.sortBy(f.sortBy) + return cpython.NewPyNone() +} + +func (f *filament) maxRowsFn(_, args cpython.PyArgs) cpython.PyRawObject { + f.table.maxRows(args.GetInt(1)) + return cpython.NewPyNone() +} + +func (f *filament) columnsFn(_, args cpython.PyArgs) cpython.PyRawObject { + var err error + f.columns, err = args.GetStringSlice(1) + if err != nil { + f.initErrors = append(f.initErrors, err) + } + f.table.appendHeader(f.columns) + return cpython.NewPyNone() +} + +func (f *filament) kfilterFn(_, args cpython.PyArgs) cpython.PyRawObject { + f.fexpr = args.GetString(1) + return cpython.NewPyNone() +} + +func (f *filament) addRowFn(_, args cpython.PyArgs) cpython.PyRawObject { + s, err := args.GetSlice(1) + if err != nil { + f.fnerrs <- err + return cpython.NewPyNone() + } + if len(s) != len(f.columns) { + f.fnerrs <- fmt.Errorf("add_row has %d row(s) but expected %d rows(s)", len(s), len(f.columns)) + return cpython.NewPyNone() + } + f.table.appendRow(s) + return cpython.NewPyLong(int64(len(s))) +} + +func (f *filament) renderTableFn(_ cpython.PyArgs, args cpython.PyArgs) cpython.PyRawObject { + f.table.render() + f.table.reset() + return cpython.NewPyNone() +} + +func (f *filament) titleFn(_ cpython.PyArgs, args cpython.PyArgs) cpython.PyRawObject { + f.table.title(args.GetString(1)) + return cpython.NewPyNone() +} + +var keywords = []string{"", "", "severity", "tags"} + +func (f *filament) emitAlertFn(_, args cpython.PyArgs, kwargs cpython.PyKwargs) cpython.PyRawObject { + f.gil.Lock() + defer f.gil.Unlock() + senders := alertsender.FindAll() + if len(senders) == 0 { + log.Warn("no alertsenders registered. Alert won't be sent") + return cpython.NewPyNone() + } + + title, text, sever, tags := cpython.PyArgsParseKeywords(args, kwargs, keywords) + + for _, s := range senders { + alert := alertsender.NewAlert( + title, + text, + tags, + alertsender.ParseSeverityFromString(sever), + ) + if err := s.Send(alert); err != nil { + log.Warnf("unable to emit alert from filament: %v", err) + } + } + + return cpython.NewPyNone() +} + +func (f *filament) findProcessFn(_, args cpython.PyArgs) cpython.PyRawObject { + f.gil.Lock() + defer f.gil.Unlock() + return cpython.NewPyNone() +} + +func (f *filament) findHandleFn(_, args cpython.PyArgs) cpython.PyRawObject { + f.gil.Lock() + defer f.gil.Unlock() + return cpython.NewPyNone() +} + +func (f *filament) findProcessesFn(_, args cpython.PyArgs) cpython.PyRawObject { + f.gil.Lock() + defer f.gil.Unlock() + return cpython.NewPyNone() +} + +func (f *filament) findHandlesFn(_, args cpython.PyArgs) cpython.PyRawObject { + f.gil.Lock() + defer f.gil.Unlock() + return cpython.NewPyNone() +} + +func (f *filament) onInterval(fn *cpython.PyObject) { + for { + select { + case <-f.tick.C: + f.gil.Lock() + r := fn.Call() + if r != nil { + r.DecRef() + } + if err := cpython.FetchErr(); err != nil { + f.fnerrs <- err + } + f.gil.Unlock() + } + } +} diff --git a/pkg/filament/filament_test.go b/pkg/filament/filament_test.go new file mode 100644 index 000000000..eace8b87e --- /dev/null +++ b/pkg/filament/filament_test.go @@ -0,0 +1,127 @@ +// +build filament + +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filament + +import ( + "bufio" + "bytes" + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "net" + "strings" + "testing" + "time" +) + +func init() { + useEmbeddedPython = false +} + +func TestNewFilament(t *testing.T) { + filament, err := New("top_hives_io", nil, nil, config.FilamentConfig{Path: "_fixtures"}) + require.NoError(t, err) + require.NotNil(t, filament) + defer filament.Close() +} + +var buf bytes.Buffer + +func init() { + tableOutput = &buf +} + +func TestFilamentTable(t *testing.T) { + filament, err := New("top_keys_io_table", nil, nil, config.FilamentConfig{Path: "_fixtures"}) + require.NoError(t, err) + require.NotNil(t, filament) + defer filament.Close() + + time.Sleep(time.Millisecond * 1020) + output := "╭──────────────────────────────────────────────────────────────────────────┬──────╮\n│ KEY │ #OPS │\n├──────────────────────────────────────────────────────────────────────────┼──────┤\n│ HKLM\\SYSTEM\\ControlSet001\\Services\\WinSock2\\Parameters\\Protocol_Catalog9 │ 3 │\n│ HKLM\\SYSTEM\\ControlSet001\\Control\\Nls\\Sorting\\Ids │ 1 │\n╰──────────────────────────────────────────────────────────────────────────┴──────╯\n" + + assert.Equal(t, output, buf.String()) +} + +func TestOnNextKevent(t *testing.T) { + filament, err := New("test_on_next_kevent", nil, nil, config.FilamentConfig{FlushPeriod: time.Millisecond * 250, Path: "_fixtures"}) + require.NoError(t, err) + require.NotNil(t, filament) + time.AfterFunc(time.Millisecond*1050, func() { + filament.Close() + }) + + kevents := make(chan *kevent.Kevent, 100) + errs := make(chan error, 10) + for i := 1; i <= 100; i++ { + kevt := &kevent.Kevent{ + Type: ktypes.RegCreateKey, + Tid: 2484, + Pid: 859, + Name: "RegCreateKey", + Host: "archrabbit", + CPU: uint8(i / 2), + Category: ktypes.Registry, + Seq: uint64(i), + Timestamp: time.Now(), + Kparams: kevent.Kparams{ + kparams.RegKeyName: {Name: kparams.RegKeyName, Type: kparams.UnicodeString, Value: `HKEY_LOCAL_MACHINE\SYSTEM\Setup`}, + kparams.RegKeyHandle: {Name: kparams.RegKeyHandle, Type: kparams.HexInt64, Value: kparams.NewHex(uint64(18446666033449935464))}, + kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("216.58.201.174")}, + }, + } + kevents <- kevt + } + err = filament.Run(kevents, errs) + require.Nil(t, err) + sn := bufio.NewScanner(strings.NewReader(buf.String())) + const headerOffset = 4 + rows := 0 + for sn.Scan() { + rows++ + } + assert.Equal(t, 100, rows-headerOffset) +} + +func TestFilamentFilter(t *testing.T) { + filament, err := New("test_filter", nil, nil, config.FilamentConfig{Path: "_fixtures"}) + require.NoError(t, err) + require.NotNil(t, filament) + defer filament.Close() + require.NotNil(t, filament.Filter()) + kpars := kevent.Kparams{ + kparams.Comm: {Name: kparams.Comm, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe -k RPCSS"}, + kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.AnsiString, Value: "svchost.exe"}, + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.Uint32, Value: uint32(1234)}, + kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.Uint32, Value: uint32(345)}, + } + + kevt := &kevent.Kevent{ + Type: ktypes.CreateProcess, + Kparams: kpars, + Name: "CreateProcess", + } + + require.True(t, filament.Filter().Run(kevt)) +} diff --git a/pkg/filament/filament_unsupported.go b/pkg/filament/filament_unsupported.go new file mode 100644 index 000000000..546e65af2 --- /dev/null +++ b/pkg/filament/filament_unsupported.go @@ -0,0 +1,38 @@ +// +build !filament + +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filament + +import ( + "github.com/rabbitstack/fibratus/pkg/config" + kerrors "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/handle" + "github.com/rabbitstack/fibratus/pkg/ps" +) + +// New returns unsupported filament error. +func New( + name string, + psnap ps.Snapshotter, + hsnap handle.Snapshotter, + config config.FilamentConfig, +) (Filament, error) { + return nil, kerrors.ErrFeatureUnsupported("filament") +} diff --git a/pkg/filament/kdict.go b/pkg/filament/kdict.go new file mode 100644 index 000000000..6957f4d0e --- /dev/null +++ b/pkg/filament/kdict.go @@ -0,0 +1,89 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filament + +import ( + "errors" + "github.com/rabbitstack/fibratus/pkg/filament/cpython" + "github.com/rabbitstack/fibratus/pkg/kevent" +) + +var ( + seq = cpython.PyUnicodeFromString("seq") + pid = cpython.PyUnicodeFromString("pid") + ppid = cpython.PyUnicodeFromString("ppid") + cwd = cpython.PyUnicodeFromString("cwd") + exec = cpython.PyUnicodeFromString("exe") + comm = cpython.PyUnicodeFromString("comm") + sid = cpython.PyUnicodeFromString("sid") + tid = cpython.PyUnicodeFromString("tid") + cpu = cpython.PyUnicodeFromString("cpu") + name = cpython.PyUnicodeFromString("name") + cat = cpython.PyUnicodeFromString("category") + desc = cpython.PyUnicodeFromString("description") + host = cpython.PyUnicodeFromString("host") + ts = cpython.PyUnicodeFromString("timestamp") + kparamsk = cpython.PyUnicodeFromString("kparams") + + errDictAllocate = errors.New("couldn't allocate a new dict") +) + +// newKDict constructs a Python dictionary object from the kernel event structure. This dictionary object is +// passed to the event dispatching function in the filament. +func newKDict(kevt *kevent.Kevent) (*cpython.Dict, error) { + kdict := cpython.NewDict() + if kdict.IsNull() { + return nil, errDictAllocate + } + + // insert canonical kevent fields + kdict.Insert(seq, cpython.NewPyObjectFromValue(kevt.Seq)) + kdict.Insert(pid, cpython.NewPyObjectFromValue(kevt.PID)) + kdict.Insert(tid, cpython.NewPyObjectFromValue(kevt.Tid)) + kdict.Insert(cpu, cpython.NewPyObjectFromValue(kevt.CPU)) + kdict.Insert(name, cpython.NewPyObjectFromValue(kevt.Name)) + kdict.Insert(cat, cpython.NewPyObjectFromValue(string(kevt.Category))) + kdict.Insert(desc, cpython.NewPyObjectFromValue(kevt.Description)) + kdict.Insert(host, cpython.NewPyObjectFromValue(kevt.Host)) + kdict.Insert(ts, cpython.NewPyObjectFromValue(kevt.Timestamp)) + + // insert process state fields + ps := kevt.PS + if ps != nil { + kdict.Insert(ppid, cpython.NewPyObjectFromValue(ps.Ppid)) + kdict.Insert(cwd, cpython.NewPyObjectFromValue(ps.Cwd)) + kdict.Insert(exec, cpython.NewPyObjectFromValue(ps.Name)) + kdict.Insert(comm, cpython.NewPyObjectFromValue(ps.Comm)) + kdict.Insert(sid, cpython.NewPyObjectFromValue(ps.SID)) + } + + // insert kevent parameters + kpars := cpython.NewDict() + for _, kpar := range kevt.Kparams { + kparam := cpython.NewPyObjectFromValue(kpar.Value) + if kparam.IsNull() { + continue + } + kpars.Insert(cpython.PyUnicodeFromString(kpar.Name), kparam) + } + + kdict.Insert(kparamsk, kpars.Object()) + + return kdict, nil +} diff --git a/pkg/filament/kdict_test.go b/pkg/filament/kdict_test.go new file mode 100644 index 000000000..5da5fe854 --- /dev/null +++ b/pkg/filament/kdict_test.go @@ -0,0 +1,117 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filament + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/filament/cpython" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "net" + "testing" + "time" +) + +func TestProduceKdict(t *testing.T) { + err := cpython.Initialize() + require.NoError(t, err) + defer cpython.Finalize() + now := time.Now() + kevt := &kevent.Kevent{ + Seq: uint64(12456738026482168384), + Tid: 2484, + PID: 859, + CPU: 1, + Name: "CreateFile", + Timestamp: now, + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + } + dict, err := newKDict(kevt) + require.NoError(t, err) + require.NotNil(t, dict) + + assert.Equal(t, uint64(12456738026482168384), dict.Get(seq).Uint64()) + assert.Equal(t, uint32(859), dict.Get(pid).Uint32()) + assert.Equal(t, uint32(2484), dict.Get(tid).Uint32()) + assert.Equal(t, uint8(1), uint8(dict.Get(cpu).Uint32())) + assert.Equal(t, "CreateFile", dict.Get(name).String()) + assert.Equal(t, "file", dict.Get(cat).String()) + assert.Equal(t, "archrabbit", dict.Get(host).String()) + assert.Equal(t, "Creates or opens a new file, directory, I/O device, pipe, console", dict.Get(desc).String()) + assert.Equal(t, fmt.Sprintf("%d-%0d-%02d %d:%d:%d.%d", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second(), now.Nanosecond()/1000), dict.Get(ts).String()) +} + +func TestProduceKdictWithIPAddresses(t *testing.T) { + err := cpython.Initialize() + require.NoError(t, err) + defer cpython.Finalize() + + kevt := &kevent.Kevent{ + Name: "Send", + Tid: 2484, + PID: 859, + Kparams: kevent.Kparams{ + kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, + kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, + kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv6, Value: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")}, + kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("216.58.201.174")}, + }, + } + + dict, err := newKDict(kevt) + require.NoError(t, err) + require.NotNil(t, dict) + + kpars := dict.Get(cpython.PyUnicodeFromString("kparams")) + kparamsDict := cpython.NewDictFromObject(kpars) + + assert.Equal(t, "216.58.201.174", kparamsDict.Get(cpython.PyUnicodeFromString("dip")).String()) + assert.Equal(t, "2001:db8:85a3::8a2e:370:7334", kparamsDict.Get(cpython.PyUnicodeFromString("sip")).String()) +} + +func BenchmarkTestProduceKdict(b *testing.B) { + b.ReportAllocs() + err := cpython.Initialize() + require.NoError(b, err) + defer cpython.Finalize() + + kevt := &kevent.Kevent{ + Seq: uint64(12456738026482168384), + Tid: 2484, + PID: 859, + CPU: 1, + Name: "CreateFile", + Timestamp: time.Now(), + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + } + + for i := 0; i < b.N; i++ { + dict, err := newKDict(kevt) + if err != nil || dict.IsNull() { + b.Fatal("invalid dict produced") + } + } +} diff --git a/pkg/filament/table.go b/pkg/filament/table.go new file mode 100644 index 000000000..b39bef382 --- /dev/null +++ b/pkg/filament/table.go @@ -0,0 +1,78 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filament + +import ( + "github.com/jedib0t/go-pretty/v6/table" + "io" +) + +type tab struct { + writer table.Writer +} + +func newTable() tab { + writer := table.NewWriter() + writer.SetStyle(table.StyleLight) + return tab{writer: writer} +} + +func (t tab) setWriter(output io.Writer) { + t.writer.SetOutputMirror(output) +} + +func (t tab) setColumnConfigs(cols []string, maxWidth int) { + configs := make([]table.ColumnConfig, len(cols)) + for i, col := range cols { + configs[i] = table.ColumnConfig{Name: col, WidthMax: maxWidth} + } + t.writer.SetColumnConfigs(configs) +} + +func (t tab) appendHeader(cols []string) { + r := make(table.Row, len(cols)) + for i, col := range cols { + r[i] = col + } + t.writer.AppendHeader(r) +} + +func (t tab) appendRow(row []interface{}) { + t.writer.AppendRow(row) +} + +func (t tab) sortBy(column string) { + t.writer.SortBy([]table.SortBy{{Name: column, Mode: table.DscNumeric}}) +} + +func (t tab) maxRows(size int) { + t.writer.SetPageSize(size) +} + +func (t tab) render() { + t.writer.Render() +} + +func (t tab) reset() { + t.writer.ResetRows() +} + +func (t tab) title(title string) { + t.writer.SetTitle(title) +} diff --git a/pkg/filament/table_test.go b/pkg/filament/table_test.go new file mode 100644 index 000000000..ed40ad25b --- /dev/null +++ b/pkg/filament/table_test.go @@ -0,0 +1,36 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filament + +import ( + "os" + "testing" +) + +func TestTable(t *testing.T) { + table := newTable() + table.setWriter(os.Stdout) + table.appendHeader([]string{"Hive", "#Ops"}) + table.appendRow([]interface{}{"HKLM/CurrentUser", 2000}) + table.render() + + table.reset() + table.appendRow([]interface{}{"HKLM/CurrentUser", 2000}) + table.render() +} diff --git a/pkg/filament/types.go b/pkg/filament/types.go new file mode 100644 index 000000000..e29b679c2 --- /dev/null +++ b/pkg/filament/types.go @@ -0,0 +1,41 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filament + +import ( + "github.com/rabbitstack/fibratus/pkg/filter" + "github.com/rabbitstack/fibratus/pkg/kevent" +) + +// Filament defines the set of operations all filaments have to satisfy. Filament represents a full-fledged +// Python interpreter that runs the modules given by users. +type Filament interface { + // Run consumes all events from the kernel event stream and dispatches them to the filament. + Run(chan *kevent.Kevent, chan error) error + // Close shutdowns the filament by releasing all allocated resources. + Close() error + // Filter returns the filter compiled from filament. + Filter() filter.Filter +} + +// Info stores metadata about the filament. +type Info struct { + Name string + Description string +} diff --git a/pkg/filter/accessor.go b/pkg/filter/accessor.go new file mode 100644 index 000000000..994a1a0b7 --- /dev/null +++ b/pkg/filter/accessor.go @@ -0,0 +1,543 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filter + +import ( + "errors" + "github.com/rabbitstack/fibratus/pkg/filter/fields" + "github.com/rabbitstack/fibratus/pkg/fs" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/net" + "github.com/rabbitstack/fibratus/pkg/pe" + "path/filepath" + "strings" +) + +// accessor dictates the behaviour of the field accessors. One of the main responsibilities of the accessor is +// to extract the underlying parameter for the field given in the filter expression. It can also produce a value +// from the non-params constructs such as process' state or PE metadata. +type accessor interface { + // get fetches the parameter value for the specified filter field. + get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) +} + +// kevtAccessor extracts kernel event specific values. +type kevtAccessor struct{} + +func newKevtAccessor() accessor { + return &kevtAccessor{} +} + +const timeFmt = "15:04:05" +const dateFmt = "2006-01-02" + +func (k *kevtAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) { + switch f { + case fields.KevtSeq: + return kevt.Seq, nil + case fields.KevtPID: + return kevt.PID, nil + case fields.KevtTID: + return kevt.Tid, nil + case fields.KevtCPU: + return kevt.CPU, nil + case fields.KevtName: + return kevt.Name, nil + case fields.KevtCategory: + return string(kevt.Category), nil + case fields.KevtDesc: + return kevt.Description, nil + case fields.KevtHost: + return kevt.Host, nil + case fields.KevtTime: + return kevt.Timestamp.Format(timeFmt), nil + case fields.KevtTimeHour: + return uint8(kevt.Timestamp.Hour()), nil + case fields.KevtTimeMin: + return uint8(kevt.Timestamp.Minute()), nil + case fields.KevtTimeSec: + return uint8(kevt.Timestamp.Second()), nil + case fields.KevtTimeNs: + return kevt.Timestamp.UnixNano(), nil + case fields.KevtDate: + return kevt.Timestamp.Format(dateFmt), nil + case fields.KevtDateDay: + return uint8(kevt.Timestamp.Day()), nil + case fields.KevtDateMonth: + return uint8(kevt.Timestamp.Month()), nil + case fields.KevtDateTz: + tz, _ := kevt.Timestamp.Zone() + return tz, nil + case fields.KevtDateYear: + return uint32(kevt.Timestamp.Year()), nil + case fields.KevtDateWeek: + _, week := kevt.Timestamp.ISOWeek() + return uint8(week), nil + case fields.KevtDateWeekday: + return kevt.Timestamp.Weekday().String(), nil + case fields.KevtNparams: + return uint64(kevt.Kparams.Len()), nil + default: + return nil, nil + } +} + +// psAccessor extracts process's state or kevent specific values. +type psAccessor struct{} + +func newPSAccessor() accessor { return &psAccessor{} } + +func (ps *psAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) { + switch f { + case fields.PsPid: + return kevt.PID, nil + case fields.PsPpid: + ps := kevt.PS + if ps == nil { + return kevt.Kparams.GetPpid() + } + return ps.Ppid, nil + case fields.PsName: + ps := kevt.PS + if ps == nil || ps.Name == "" { + return kevt.Kparams.GetString(kparams.ProcessName) + } + return ps.Name, nil + case fields.PsComm: + ps := kevt.PS + if ps == nil { + return kevt.Kparams.GetString(kparams.Comm) + } + return ps.Comm, nil + case fields.PsExe: + ps := kevt.PS + if ps == nil { + return nil, nil + } + return ps.Exe, nil + case fields.PsArgs: + ps := kevt.PS + if ps == nil { + return nil, nil + } + return ps.Args, nil + case fields.PsCwd: + ps := kevt.PS + if ps == nil { + return nil, nil + } + return ps.Cwd, nil + case fields.PsSID: + ps := kevt.PS + if ps == nil { + return nil, nil + } + return ps.SID, nil + case fields.PsSessionID: + ps := kevt.PS + if ps == nil { + return nil, nil + } + return ps.SessionID, nil + case fields.PsEnvs: + ps := kevt.PS + if ps == nil { + return nil, nil + } + envs := make([]string, 0, len(ps.Envs)) + for env := range ps.Envs { + envs = append(envs, env) + } + return envs, nil + case fields.PsModules: + ps := kevt.PS + if ps == nil { + return nil, nil + } + mods := make([]string, 0, len(ps.Modules)) + for _, m := range ps.Modules { + mods = append(mods, filepath.Base(m.Name)) + } + return mods, nil + case fields.PsHandles: + ps := kevt.PS + if ps == nil { + return nil, nil + } + handles := make([]string, len(ps.Handles)) + for i, handle := range ps.Handles { + handles[i] = handle.Name + } + return handles, nil + case fields.PsHandleTypes: + ps := kevt.PS + if ps == nil { + return nil, nil + } + types := make([]string, len(ps.Handles)) + for i, handle := range ps.Handles { + if types[i] == handle.Type { + continue + } + types[i] = handle.Type + } + return types, nil + default: + field := f.String() + switch { + case strings.HasPrefix(field, fields.PsEnvsSubfield): + // access the specific environment variable + env, _ := captureInBrackets(field) + ps := kevt.PS + if ps == nil { + return nil, nil + } + v, ok := ps.Envs[env] + if ok { + return v, nil + } + // match on prefix + for k, v := range ps.Envs { + if strings.HasPrefix(k, env) { + return v, nil + } + } + + case strings.HasPrefix(field, fields.PsModsSubfield): + name, subfield := captureInBrackets(field) + ps := kevt.PS + if ps == nil { + return nil, nil + } + mod := ps.FindModule(name) + if mod == nil { + return nil, nil + } + + switch subfield { + case fields.ModuleSize: + return mod.Size, nil + case fields.ModuleChecksum: + return mod.Checksum, nil + case fields.ModuleBaseAddress: + return mod.BaseAddress.String(), nil + case fields.ModuleDefaultAddress: + return mod.DefaultBaseAddress.String(), nil + case fields.ModuleLocation: + return filepath.Dir(mod.Name), nil + } + } + + return nil, nil + } +} + +// threadAccessor fetches thread parameters from thread kernel events. +type threadAccessor struct{} + +func newThreadAccessor() accessor { + return &threadAccessor{} +} + +func (t *threadAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) { + switch f { + case fields.ThreadBasePrio: + return kevt.Kparams.GetUint8(kparams.BasePrio) + case fields.ThreadIOPrio: + return kevt.Kparams.GetUint8(kparams.IOPrio) + case fields.ThreadPagePrio: + return kevt.Kparams.GetUint8(kparams.PagePrio) + case fields.ThreadKstackBase: + v, err := kevt.Kparams.GetHex(kparams.KstackBase) + if err != nil { + return nil, err + } + return v.String(), nil + case fields.ThreadKstackLimit: + v, err := kevt.Kparams.GetHex(kparams.KstackLimit) + if err != nil { + return nil, err + } + return v.String(), nil + case fields.ThreadUstackBase: + v, err := kevt.Kparams.GetHex(kparams.UstackBase) + if err != nil { + return nil, err + } + return v.String(), nil + case fields.ThreadUstackLimit: + v, err := kevt.Kparams.GetHex(kparams.UstackLimit) + if err != nil { + return nil, err + } + return v.String(), nil + case fields.ThreadEntrypoint: + v, err := kevt.Kparams.GetHex(kparams.ThreadEntrypoint) + if err != nil { + return nil, err + } + return v.String(), nil + } + return nil, nil +} + +// fileAccessor extracts file specific values. +type fileAccessor struct{} + +func newFileAccessor() accessor { + return &fileAccessor{} +} + +func (l *fileAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) { + switch f { + case fields.FileName: + return kevt.Kparams.GetString(kparams.FileName) + case fields.FileOffset: + return kevt.Kparams.GetUint64(kparams.FileOffset) + case fields.FileIOSize: + return kevt.Kparams.GetUint32(kparams.FileIoSize) + case fields.FileShareMask: + m, err := kevt.Kparams.Get(kparams.FileShareMask) + if err != nil { + return nil, err + } + mode, ok := m.(fs.FileShareMode) + if !ok { + return nil, errors.New("couldn't type assert to file share mode enum") + } + return mode.String(), nil + case fields.FileOperation: + op, err := kevt.Kparams.Get(kparams.FileOperation) + if err != nil { + return nil, err + } + fop, ok := op.(fs.FileDisposition) + if !ok { + return nil, errors.New("couldn't type assert to file operation enum") + } + return fop.String(), nil + case fields.FileObject: + return kevt.Kparams.GetUint64(kparams.FileObject) + case fields.FileType: + return kevt.Kparams.GetString(kparams.FileType) + } + return nil, nil +} + +// imageAccessor extracts image (DLL) kevent values. +type imageAccessor struct{} + +func newImageAccessor() accessor { + return &imageAccessor{} +} + +func (i *imageAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) { + switch f { + case fields.ImageName: + return kevt.Kparams.GetString(kparams.ImageFilename) + case fields.ImageDefaultAddress: + address, err := kevt.Kparams.GetHex(kparams.ImageDefaultBase) + if err != nil { + return nil, err + } + return address.String(), nil + case fields.ImageBase: + address, err := kevt.Kparams.GetHex(kparams.ImageBase) + if err != nil { + return nil, err + } + return address.String(), nil + case fields.ImageSize: + return kevt.Kparams.GetUint32(kparams.ImageSize) + case fields.ImageChecksum: + return kevt.Kparams.GetUint32(kparams.ImageCheckSum) + } + return nil, nil +} + +// registryAccessor extracts registry specific parameters. +type registryAccessor struct{} + +func newRegistryAccessor() accessor { + return ®istryAccessor{} +} + +func (r *registryAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) { + switch f { + case fields.RegistryKeyName: + return kevt.Kparams.GetString(kparams.RegKeyName) + case fields.RegistryKeyHandle: + keyHandle, err := kevt.Kparams.GetHex(kparams.RegKeyHandle) + if err != nil { + return nil, err + } + return keyHandle.String(), nil + case fields.RegistryValue: + return kevt.Kparams.Get(kparams.RegValue) + case fields.RegistryValueType: + return kevt.Kparams.GetString(kparams.RegValueType) + case fields.RegistryStatus: + return kevt.Kparams.GetString(kparams.NTStatus) + } + return nil, nil +} + +// netAccessor deals with extracting the network specific kernel event parameters. +type netAccessor struct{} + +func newNetAccessor() accessor { return &netAccessor{} } + +func (n *netAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) { + switch f { + case fields.NetDIP: + return kevt.Kparams.GetIP(kparams.NetDIP) + case fields.NetSIP: + return kevt.Kparams.GetIP(kparams.NetSIP) + case fields.NetDport: + return kevt.Kparams.GetUint16(kparams.NetDport) + case fields.NetSport: + return kevt.Kparams.GetUint16(kparams.NetSport) + case fields.NetDportName: + return kevt.Kparams.GetString(kparams.NetDportName) + case fields.NetSportName: + return kevt.Kparams.GetString(kparams.NetSportName) + case fields.NetL4Proto: + v, err := kevt.Kparams.Get(kparams.NetL4Proto) + if err != nil { + return nil, err + } + l4proto, ok := v.(net.L4Proto) + if !ok { + return nil, errors.New("couldn't type assert to L4 proto enum") + } + return l4proto.String(), nil + case fields.NetPacketSize: + return kevt.Kparams.GetUint32(kparams.NetSize) + } + return nil, nil +} + +// handleAccessor extracts handle event values. +type handleAccessor struct{} + +func newHandleAccessor() accessor { return &handleAccessor{} } + +func (h *handleAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) { + switch f { + case fields.HandleID: + return kevt.Kparams.GetHexAsUint32(kparams.HandleID) + case fields.HandleType: + return kevt.Kparams.GetString(kparams.HandleObjectTypeName) + case fields.HandleName: + return kevt.Kparams.GetString(kparams.HandleObjectName) + case fields.HandleObject: + handleObject, err := kevt.Kparams.GetHex(kparams.HandleObject) + if err != nil { + return nil, err + } + return handleObject.String(), nil + } + return nil, nil +} + +// peAccessor extracts PE specific values. +type peAccessor struct{} + +func newPEAccessor() accessor { + return &peAccessor{} +} + +func (p *peAccessor) get(f fields.Field, kevt *kevent.Kevent) (kparams.Value, error) { + var pex *pe.PE + if kevt.PS != nil && kevt.PS.PE != nil { + pex = kevt.PS.PE + } + if pex == nil { + return nil, nil + } + + switch f { + case fields.PeEntrypoint: + return pex.EntryPoint, nil + case fields.PeBaseAddress: + return pex.ImageBase, nil + case fields.PeNumSections: + return pex.NumberOfSections, nil + case fields.PeNumSymbols: + return pex.NumberOfSymbols, nil + case fields.PeSymbols: + return pex.Symbols, nil + case fields.PeImports: + return pex.Imports, nil + default: + field := f.String() + if strings.HasPrefix(field, fields.PeSectionsSubfield) { + // get the section name + sname, subfield := captureInBrackets(field) + sec := pex.Section(sname) + if sec == nil { + return nil, nil + } + switch subfield { + case fields.SectionEntropy: + return sec.Entropy, nil + case fields.SectionMD5Hash: + return sec.Md5, nil + case fields.SectionSize: + return sec.Size, nil + } + } + + if strings.HasPrefix(field, fields.PeResourcesSubfield) { + // consult the resource name + key, _ := captureInBrackets(field) + v, ok := pex.VersionResources[key] + if ok { + return v, nil + } + // match on prefix (e.g. pe.resources[Org] = Blackwater) + for k, v := range pex.VersionResources { + if strings.HasPrefix(k, key) { + return v, nil + } + } + } + } + + return nil, nil +} + +func captureInBrackets(s string) (string, fields.Subfield) { + lbracket := strings.Index(s, "[") + if lbracket == -1 { + return "", "" + } + rbracket := strings.Index(s, "]") + if rbracket == -1 { + return "", "" + } + if lbracket+1 > len(s) { + return "", "" + } + if rbracket+2 < len(s) { + return s[lbracket+1 : rbracket], fields.Subfield(s[rbracket+2:]) + } + return s[lbracket+1 : rbracket], "" +} diff --git a/pkg/filter/accessor_test.go b/pkg/filter/accessor_test.go new file mode 100644 index 000000000..e254c32c1 --- /dev/null +++ b/pkg/filter/accessor_test.go @@ -0,0 +1,98 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filter + +import ( + "github.com/rabbitstack/fibratus/pkg/filter/fields" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/pe" + ptypes "github.com/rabbitstack/fibratus/pkg/ps/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestPSAccessor(t *testing.T) { + ps := newPSAccessor() + kevt := &kevent.Kevent{ + PS: &ptypes.PS{ + Envs: map[string]string{"ALLUSERSPROFILE": "C:\\ProgramData", "OS": "Windows_NT", "ProgramFiles(x86)": "C:\\Program Files (x86)"}, + }, + } + + env, err := ps.get(fields.Field("ps.envs[ALLUSERSPROFILE]"), kevt) + require.NoError(t, err) + assert.Equal(t, "C:\\ProgramData", env) + + env, err = ps.get(fields.Field("ps.envs[ALLUSER]"), kevt) + require.NoError(t, err) + assert.Equal(t, "C:\\ProgramData", env) + + env, err = ps.get(fields.Field("ps.envs[ProgramFiles]"), kevt) + require.NoError(t, err) + assert.Equal(t, "C:\\Program Files (x86)", env) +} + +func TestPEAccessor(t *testing.T) { + pea := newPEAccessor() + kevt := &kevent.Kevent{ + PS: &ptypes.PS{ + PE: &pe.PE{ + NumberOfSections: 2, + NumberOfSymbols: 10, + EntryPoint: "0x20110", + ImageBase: "0x140000000", + LinkTime: time.Now(), + Sections: []pe.Sec{ + {Name: ".text", Size: 132608, Entropy: 6.368381, Md5: "db23dce3911a42e987041d98abd4f7cd"}, + {Name: ".rdata", Size: 35840, Entropy: 5.996976, Md5: "ffa5c960b421ca9887e54966588e97e8"}, + }, + Symbols: []string{"SelectObject", "GetTextFaceW", "EnumFontsW", "TextOutW", "GetProcessHeap"}, + Imports: []string{"GDI32.dll", "USER32.dll", "msvcrt.dll", "api-ms-win-core-libraryloader-l1-2-0.dl"}, + VersionResources: map[string]string{"CompanyName": "Microsoft Corporation", "FileDescription": "Notepad", "FileVersion": "10.0.18362.693"}, + }, + }, + } + + entropy, err := pea.get(fields.Field("pe.sections[.text].entropy"), kevt) + require.NoError(t, err) + assert.Equal(t, 6.368381, entropy) + + v, err := pea.get(fields.Field("pe.sections[.text].md6"), kevt) + require.Nil(t, v) + + md5, err := pea.get(fields.Field("pe.sections[.rdata].md5"), kevt) + require.Nil(t, v) + assert.Equal(t, "ffa5c960b421ca9887e54966588e97e8", md5) + + company, err := pea.get(fields.Field("pe.resources[CompanyName]"), kevt) + require.NoError(t, err) + assert.Equal(t, "Microsoft Corporation", company) +} + +func TestCaptureInBrackets(t *testing.T) { + v, subfield := captureInBrackets("ps.envs[ALLUSERSPROFILE]") + assert.Equal(t, "ALLUSERSPROFILE", v) + assert.Empty(t, subfield) + + v, subfield = captureInBrackets("ps.pe.sections[.debug$S].entropy") + assert.Equal(t, ".debug$S", v) + assert.Equal(t, fields.SectionEntropy, subfield) +} diff --git a/pkg/filter/fields/fields.go b/pkg/filter/fields/fields.go new file mode 100644 index 000000000..769cb2988 --- /dev/null +++ b/pkg/filter/fields/fields.go @@ -0,0 +1,309 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fields + +import ( + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "regexp" + "sort" +) + +var subfieldRegexp = regexp.MustCompile(`(pe.sections|pe.resources|ps.envs|ps.modules)\[.+\s*].?(.*)`) + +// Field represents the type alias for the field +type Field string + +const ( + PsPid Field = "ps.pid" + PsPpid Field = "ps.ppid" + PsName Field = "ps.name" + PsComm Field = "ps.comm" + PsExe Field = "ps.exe" + PsArgs Field = "ps.args" + PsCwd Field = "ps.cwd" + PsSID Field = "ps.sid" + PsSessionID Field = "ps.sessionid" + PsEnvs Field = "ps.envs" + PsHandles Field = "ps.handles" + PsHandleTypes Field = "ps.handle.types" + PsDTB Field = "ps.dtb" + PsModules Field = "ps.modules" + + ThreadBasePrio Field = "thread.prio" + ThreadIOPrio Field = "thread.io.prio" + ThreadPagePrio Field = "thread.page.prio" + ThreadKstackBase Field = "thread.kstack.base" + ThreadKstackLimit Field = "thread.kstack.limit" + ThreadUstackBase Field = "thread.ustack.base" + ThreadUstackLimit Field = "thread.ustack.limit" + ThreadEntrypoint Field = "thread.entrypoint" + + PeNumSections Field = "pe.nsections" + PeSections Field = "pe.sections" + PeNumSymbols Field = "pe.nsymbols" + PeSymbols Field = "pe.symbols" + PeImports Field = "pe.imports" + PeTimestamp Field = "pe.timestamp" + PeBaseAddress Field = "pe.address.base" + PeEntrypoint Field = "pe.address.entrypoint" + PeResources Field = "pe.resources" + + KevtSeq Field = "kevt.seq" + KevtPID Field = "kevt.pid" + KevtTID Field = "kevt.tid" + KevtCPU Field = "kevt.cpu" + KevtDesc Field = "kevt.desc" + KevtHost Field = "kevt.host" + KevtTime Field = "kevt.time" + KevtTimeHour Field = "kevt.time.h" + KevtTimeMin Field = "kevt.time.m" + KevtTimeSec Field = "kevt.time.s" + KevtTimeNs Field = "kevt.time.ns" + KevtDate Field = "kevt.date" + KevtDateDay Field = "kevt.date.d" + KevtDateMonth Field = "kevt.date.m" + KevtDateYear Field = "kevt.date.y" + KevtDateTz Field = "kevt.date.tz" + KevtDateWeek Field = "kevt.date.week" + KevtDateWeekday Field = "kevt.date.weekday" + KevtName Field = "kevt.name" + KevtCategory Field = "kevt.category" + KevtMeta Field = "kevt.meta" + KevtNparams Field = "kevt.nparams" + + HandleID Field = "handle.id" + HandleObject Field = "handle.object" + HandleName Field = "handle.name" + HandleType Field = "handle.type" + + NetDIP Field = "net.dip" + NetSIP Field = "net.sip" + NetDport Field = "net.dport" + NetSport Field = "net.sport" + NetDportName Field = "net.dport.name" + NetSportName Field = "net.sport.name" + NetL4Proto Field = "net.l4.proto" + NetPacketSize Field = "net.size" + + FileObject Field = "file.object" + FileName Field = "file.name" + FileOperation Field = "file.operation" + FileShareMask Field = "file.share.mask" + FileIOSize Field = "file.io.size" + FileOffset Field = "file.offset" + FileType Field = "file.type" + + RegistryKeyName Field = "registry.key.name" + RegistryKeyHandle Field = "registry.key.handle" + RegistryValue Field = "registry.value" + RegistryValueType Field = "registry.value.type" + RegistryStatus Field = "registry.status" + + ImageBase Field = "image.base.address" + ImageSize Field = "image.size" + ImageChecksum Field = "image.checksum" + ImageDefaultAddress Field = "image.default.address" + ImageName Field = "image.name" + ImagePID Field = "image.pid" + + None Field = "" +) + +// String casts the field type to string. +func (f Field) String() string { return string(f) } + +// Subfield represents the type alias for the subfield. +type Subfield string + +const ( + // SectionEntropy is the entropy value of the specific PE section + SectionEntropy Subfield = "entropy" + // SectionMD5Hash refers to the section md5 sum + SectionMD5Hash Subfield = "md5" + SectionSize Subfield = "size" + + ModuleSize Subfield = "size" + ModuleChecksum Subfield = "checksum" + ModuleLocation Subfield = "location" + ModuleBaseAddress Subfield = "address.base" + ModuleDefaultAddress Subfield = "address.default" +) + +const ( + PsEnvsSubfield = "ps.envs[" + PsModsSubfield = "ps.modules[" + PeSectionsSubfield = "pe.sections[" + PeResourcesSubfield = "pe.resources[" +) + +// FieldInfo is the field metadata descriptor. +type FieldInfo struct { + Field Field + Desc string + Type kparams.Type + Examples []string +} + +var fields = map[Field]FieldInfo{ + KevtSeq: {KevtSeq, "event sequence number", kparams.Uint64, []string{"kevt.seq > 666"}}, + KevtPID: {KevtPID, "process identifier generating the kernel event", kparams.Uint32, []string{"kevt.pid = 6"}}, + KevtTID: {KevtTID, "thread identifier generating the kernel event", kparams.Uint32, []string{"kevt.tid = 1024"}}, + KevtCPU: {KevtCPU, "logical processor core where the event was generated", kparams.Uint8, []string{"kevt.cpu = 2"}}, + KevtName: {KevtName, "symbolical kernel event name", kparams.AnsiString, []string{"kevt.name = 'CreateThread'"}}, + KevtCategory: {KevtCategory, "event category", kparams.AnsiString, []string{"kevt.category = 'registry'"}}, + KevtDesc: {KevtDesc, "event description", kparams.AnsiString, []string{"kevt.desc contains 'Creates a new process'"}}, + KevtHost: {KevtHost, "host name on which the event was produced", kparams.UnicodeString, []string{"kevt.host contains 'kitty'"}}, + KevtTime: {KevtTime, "event timestamp as a time string", kparams.Time, []string{"kevt.time = '17:05:32'"}}, + KevtTimeHour: {KevtTimeHour, "hour within the day on which the event occurred", kparams.Time, []string{"kevt.time.h = 23"}}, + KevtTimeMin: {KevtTimeMin, "minute offset within the hour on which the event occurred", kparams.Time, []string{"kevt.time.m = 54"}}, + KevtTimeSec: {KevtTimeSec, "second offset within the minute on which the event occurred", kparams.Time, []string{"kevt.time.s = 0"}}, + KevtTimeNs: {KevtTimeNs, "nanoseconds specified by event timestamp", kparams.Int64, []string{"kevt.time.ns > 1591191629102337000"}}, + KevtDate: {KevtDate, "event timestamp as a date string", kparams.Time, []string{"kevt.date = '2018-03-03'"}}, + KevtDateDay: {KevtDateDay, "day of the month on which the event occurred", kparams.Time, []string{"kevt.date.d = 12"}}, + KevtDateMonth: {KevtDateMonth, "month of the year on which the event occurred", kparams.Time, []string{"kevt.date.m = 11"}}, + KevtDateYear: {KevtDateYear, "year on which the event occurred", kparams.Uint32, []string{"kevt.date.y = 2020"}}, + KevtDateTz: {KevtDateTz, "time zone associated with the event timestamp", kparams.AnsiString, []string{"kevt.date.tz = 'UTC'"}}, + KevtDateWeek: {KevtDateWeek, "week number within the year on which the event occurred", kparams.Uint8, []string{"kevt.date.week = 2"}}, + KevtDateWeekday: {KevtDateWeekday, "week day on which the event occurred", kparams.AnsiString, []string{"kevt.date.weekday = 'Monday'"}}, + KevtNparams: {KevtNparams, "number of parameters", kparams.Int8, []string{"kevt.nparams > 2"}}, + + PsPid: {PsPid, "process identifier", kparams.PID, []string{"ps.pid = 1024"}}, + PsPpid: {PsPpid, "parent process identifier", kparams.PID, []string{"ps.ppid = 45"}}, + PsName: {PsName, "process image name including the file extension", kparams.UnicodeString, []string{"ps.name contains 'firefox'"}}, + PsComm: {PsComm, "process command line", kparams.UnicodeString, []string{"ps.comm contains 'java'"}}, + PsExe: {PsExe, "full name of the process' executable", kparams.UnicodeString, []string{"ps.exe = 'C:\\Windows\\system32\\cmd.exe'"}}, + PsArgs: {PsArgs, "process command line arguments", kparams.Slice, []string{"ps.args in ('/cdir', '/-C')"}}, + PsCwd: {PsCwd, "process current working directory", kparams.UnicodeString, []string{"ps.cwd = 'C:\\Users\\Default'"}}, + PsSID: {PsSID, "security identifier under which this process is run", kparams.UnicodeString, []string{"ps.sid contains 'SYSTEM'"}}, + PsSessionID: {PsSessionID, "unique identifier for the current session", kparams.Int16, []string{"ps.sessionid = 1"}}, + PsEnvs: {PsEnvs, "process environment variables", kparams.Slice, []string{"ps.envs in ('MOZ_CRASHREPORTER_DATA_DIRECTORY')"}}, + PsHandles: {PsHandles, "allocated process handle names", kparams.Slice, []string{"ps.handles in ('\\BaseNamedObjects\\__ComCatalogCache__')"}}, + PsHandleTypes: {PsHandleTypes, "allocated process handle types", kparams.Slice, []string{"ps.handle.types in ('Key', 'Mutant', 'Section')"}}, + PsDTB: {PsDTB, "process directory table base address", kparams.HexInt64, []string{"ps.dtb = '7ffe0000'"}}, + PsModules: {PsModules, "modules loaded by the process", kparams.Slice, []string{"ps.modules in ('crypt32.dll', 'xul.dll')"}}, + + ThreadBasePrio: {ThreadBasePrio, "scheduler priority of the thread", kparams.Int8, []string{"thread.prio = 5"}}, + ThreadIOPrio: {ThreadIOPrio, "I/O priority hint for scheduling I/O operations", kparams.Int8, []string{"thread.io.prio = 4"}}, + ThreadPagePrio: {ThreadPagePrio, "memory page priority hint for memory pages accessed by the thread", kparams.Int8, []string{"thread.page.prio = 12"}}, + ThreadKstackBase: {ThreadKstackBase, "base address of the thread's kernel space stack", kparams.HexInt64, []string{"thread.kstack.base = 'a65d800000'"}}, + ThreadKstackLimit: {ThreadKstackLimit, "limit of the thread's kernel space stack", kparams.HexInt64, []string{"thread.kstack.limit = 'a85d800000'"}}, + ThreadUstackBase: {ThreadUstackBase, "base address of the thread's user space stack", kparams.HexInt64, []string{"thread.ustack.base = '7ffe0000'"}}, + ThreadUstackLimit: {ThreadUstackLimit, "limit of the thread's user space stack", kparams.HexInt64, []string{"thread.ustack.limit = '8ffe0000'"}}, + ThreadEntrypoint: {ThreadEntrypoint, "starting address of the function to be executed by the thread", kparams.HexInt64, []string{"thread.entrypoint = '7efe0000'"}}, + + ImageName: {ImageName, "full image name", kparams.UnicodeString, []string{"image.name contains 'advapi32.dll'"}}, + ImageBase: {ImageBase, "the base address of process in which the image is loaded", kparams.HexInt64, []string{"image.base.address = 'a65d800000'"}}, + ImageChecksum: {ImageChecksum, "image checksum", kparams.Uint32, []string{"image.checksum = 746424"}}, + ImageSize: {ImageSize, "image size", kparams.Uint32, []string{"image.size > 1024"}}, + ImageDefaultAddress: {ImageDefaultAddress, "default image address", kparams.HexInt64, []string{"image.default.address = '7efe0000'"}}, + + FileObject: {FileObject, "file object address", kparams.Uint64, []string{"file.object = 18446738026482168384"}}, + FileName: {FileName, "full file name", kparams.UnicodeString, []string{"file.name contains 'mimikatz'"}}, + FileOperation: {FileOperation, "file operation", kparams.AnsiString, []string{"file.operation = 'open'"}}, + FileShareMask: {FileShareMask, "file share mask", kparams.AnsiString, []string{"file.share.mask = 'rw-'"}}, + FileIOSize: {FileIOSize, "file I/O size", kparams.Uint32, []string{"file.io.size > 512"}}, + FileOffset: {FileOffset, "file offset", kparams.Uint64, []string{"file.offset = 1024"}}, + FileType: {FileType, "file type", kparams.AnsiString, []string{"file.type = 'directory'"}}, + + RegistryKeyName: {RegistryKeyName, "fully qualified key name", kparams.UnicodeString, []string{"registry.key.name contains 'HKEY_LOCAL_MACHINE'"}}, + RegistryKeyHandle: {RegistryKeyHandle, "registry key object address", kparams.HexInt64, []string{"registry.key.handle = 'FFFFB905D60C2268'"}}, + RegistryValue: {RegistryValue, "registry value content", kparams.UnicodeString, []string{"registry.value = '%SystemRoot%\\system32'"}}, + RegistryValueType: {RegistryValueType, "type of registry value", kparams.UnicodeString, []string{"registry.value.type = 'REG_SZ'"}}, + RegistryStatus: {RegistryStatus, "status of registry operation", kparams.UnicodeString, []string{"registry.status != 'success'"}}, + + NetDIP: {NetDIP, "destination IP address", kparams.IP, []string{"net.dip = 172.17.0.3"}}, + NetSIP: {NetSIP, "source IP address", kparams.IP, []string{"net.sip = 127.0.0.1"}}, + NetDport: {NetDport, "destination port", kparams.Uint16, []string{"net.dport in (80, 443, 8080)"}}, + NetSport: {NetSport, "source port", kparams.Uint16, []string{"net.sport != 3306"}}, + NetDportName: {NetDportName, "destination port name", kparams.AnsiString, []string{"net.dport.name = 'dns'"}}, + NetSportName: {NetSportName, "source port name", kparams.AnsiString, []string{"net.sport.name = 'http'"}}, + NetL4Proto: {NetL4Proto, "layer 4 protocol name", kparams.AnsiString, []string{"net.l4.proto = 'TCP"}}, + NetPacketSize: {NetPacketSize, "packet size", kparams.Uint32, []string{"net.size > 512"}}, + + HandleID: {HandleID, "handle identifier", kparams.Uint16, []string{"handle.id = 24"}}, + HandleObject: {HandleObject, "handle object address", kparams.HexInt64, []string{"handle.object = 'FFFFB905DBF61988'"}}, + HandleName: {HandleName, "handle name", kparams.UnicodeString, []string{"handle.name = '\\Device\\NamedPipe\\chrome.12644.28.105826381'"}}, + HandleType: {HandleType, "handle type", kparams.AnsiString, []string{"handle.type = 'Mutant'"}}, + + PeNumSections: {PeNumSections, "number of sections", kparams.Uint16, []string{"pe.nsections < 5"}}, + PeNumSymbols: {PeNumSymbols, "number of entries in the symbol table", kparams.Uint32, []string{"pe.nsymbols > 230"}}, + PeBaseAddress: {PeBaseAddress, "image base address", kparams.HexInt64, []string{"pe.address.base = '140000000'"}}, + PeEntrypoint: {PeEntrypoint, "address of the entrypoint function", kparams.HexInt64, []string{"pe.address.entrypoint = '20110'"}}, + PeSections: {PeSections, "PE sections", kparams.Object, []string{"pe.sections[.text].entropy > 6.2"}}, + PeSymbols: {PeSymbols, "imported symbols", kparams.Slice, []string{"pe.symbols in ('GetTextFaceW', 'GetProcessHeap')"}}, + PeImports: {PeImports, "imported dynamic linked libraries", kparams.Slice, []string{"pe.imports in ('msvcrt.dll', 'GDI32.dll'"}}, + PeResources: {PeResources, "version and other resources", kparams.Map, []string{"pe.resources[FileDescription] = 'Notepad'"}}, +} + +// Get returns a slice of field information. +func Get() []FieldInfo { + fi := make([]FieldInfo, 0, len(fields)) + for _, field := range fields { + fi = append(fi, field) + } + sort.Slice(fi, func(i, j int) bool { return fi[i].Field < fi[j].Field }) + return fi +} + +// Lookup finds the field literal in the map. For the nested fields, it checks the pattern matches +// the expected one and compares the subfields. If all checks pass, the full nested field literal +// is returned. +func Lookup(name string) Field { + if _, ok := fields[Field(name)]; ok { + return Field(name) + } + groups := subfieldRegexp.FindStringSubmatch(name) + if len(groups) != 3 { + return None + } + + field := groups[1] + subfield := groups[2] + + switch Field(field) { + case PeSections: + switch Subfield(subfield) { + case SectionEntropy: + return Field(name) + case SectionMD5Hash: + return Field(name) + case SectionSize: + return Field(name) + } + case PeResources: + return Field(name) + case PsEnvs: + return Field(name) + case PsModules: + switch Subfield(subfield) { + case ModuleSize: + return Field(ModuleSize) + case ModuleChecksum: + return Field(ModuleChecksum) + case ModuleDefaultAddress: + return Field(ModuleDefaultAddress) + case ModuleBaseAddress: + return Field(ModuleBaseAddress) + case ModuleLocation: + return Field(ModuleLocation) + } + } + + return None +} diff --git a/pkg/filter/fields/fields_test.go b/pkg/filter/fields/fields_test.go new file mode 100644 index 000000000..edabef219 --- /dev/null +++ b/pkg/filter/fields/fields_test.go @@ -0,0 +1,38 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fields + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestLookup(t *testing.T) { + assert.Equal(t, PsPid, Lookup("ps.pid")) + assert.Equal(t, Field("ps.envs[ALLUSERSPROFILE]"), Lookup("ps.envs[ALLUSERSPROFILE]")) + assert.Empty(t, Lookup("ps.envs[ALLUSERSPROFILE")) + assert.Empty(t, Lookup("ps.envs[")) + assert.Empty(t, Lookup("ps.envs[]")) + assert.Equal(t, PsEnvs, Lookup("ps.envs")) + assert.Equal(t, Field("ps.pe.sections[.debug$S].entropy"), Lookup("ps.pe.sections[.debug$S].entropy")) + assert.Empty(t, Lookup("ps.pe.sections[.debug$S")) + assert.Empty(t, Lookup("ps.pe.sections[.debug$S]")) + assert.Empty(t, Lookup("ps.pe.sections[.debug$S].")) + assert.Empty(t, Lookup("ps.pe.sections[.debug$S].e")) +} diff --git a/pkg/filter/filter.go b/pkg/filter/filter.go new file mode 100644 index 000000000..64e4156b3 --- /dev/null +++ b/pkg/filter/filter.go @@ -0,0 +1,133 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filter + +import ( + "errors" + "expvar" + "fmt" + kerrors "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/filter/fields" + "github.com/rabbitstack/fibratus/pkg/filter/ql" + "github.com/rabbitstack/fibratus/pkg/kevent" + "strings" +) + +var ( + accessorErrors = expvar.NewMap("filter.accessor.errors") + errNoFields = errors.New("expected at least one field or operator but zero found") +) + +// Filter is the main interface for the filter engine implementors. +type Filter interface { + // Compile compiles the filter by parsing the filtering expression. + Compile() error + // Run runs a filter on the inbound kernel event and decides whether the event should be dropped or propagated to the downstream channel. + Run(kevt *kevent.Kevent) bool +} + +type filter struct { + expr ql.Expr + parser *ql.Parser + accessors []accessor + fields []fields.Field +} + +// New creates a new filter with the specified filter expression. The consumers must ensure the expression is lexically +// well-parsed before executing the filter. This is achieved by calling the`Compile` method after constructing the filter. +func New(expr string) Filter { + return &filter{ + parser: ql.NewParser(expr), + accessors: []accessor{ + // general event parameters + newKevtAccessor(), + // process state and parameters + newPSAccessor(), + // thread parameters + newThreadAccessor(), + // image parameters + newImageAccessor(), + // file parameters + newFileAccessor(), + // registry parameters + newRegistryAccessor(), + // network parameters + newNetAccessor(), + // handle parameters + newHandleAccessor(), + // PE attributes + newPEAccessor(), + }, + fields: make([]fields.Field, 0), + } +} + +// NewFromCLI builds and compiles a filter by joining all the command line arguments into the filter expression. +func NewFromCLI(args []string) (Filter, error) { + expr := strings.Join(args, " ") + if expr == "" { + return nil, nil + } + filter := New(expr) + if err := filter.Compile(); err != nil { + return nil, fmt.Errorf("bad filter: \n %v", err) + } + return filter, nil +} + +func (f *filter) Compile() error { + expr, err := f.parser.ParseExpr() + if err != nil { + return err + } + f.expr = expr + ql.WalkFunc(expr, func(n ql.Node) { + if ex, ok := n.(*ql.BinaryExpr); ok { + if lhs, ok := ex.LHS.(*ql.FieldLiteral); ok { + f.fields = append(f.fields, fields.Field(lhs.Value)) + } + } + }) + if len(f.fields) == 0 { + return errNoFields + } + return nil +} + +func (f *filter) Run(kevt *kevent.Kevent) bool { + valuer := make(map[string]interface{}) + // for each field present in the AST, we run the + // accessors and extract the field vales that are + // supplied to the valuer. The valuer feeds the + // expression with correct values. + for _, field := range f.fields { + for _, accessor := range f.accessors { + v, err := accessor.get(field, kevt) + if err != nil && !kerrors.IsKparamNotFound(err) { + accessorErrors.Add(err.Error(), 1) + continue + } + if v == nil { + continue + } + valuer[field.String()] = v + } + } + return ql.Eval(f.expr, valuer) +} diff --git a/pkg/filter/filter_test.go b/pkg/filter/filter_test.go new file mode 100644 index 000000000..bb1f03157 --- /dev/null +++ b/pkg/filter/filter_test.go @@ -0,0 +1,297 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filter + +import ( + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/pe" + pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" + "github.com/stretchr/testify/require" + "net" + "testing" + "time" +) + +func TestFilterCompile(t *testing.T) { + f := New(`ps.name = 'cmd.exe'`) + require.NoError(t, f.Compile()) + f = New(`'cmd.exe'`) + require.EqualError(t, f.Compile(), "expected at least one field or operator but zero found") + f = New(`ps.name`) + require.EqualError(t, f.Compile(), "expected at least one field or operator but zero found") + f = New(`ps.name =`) + require.EqualError(t, f.Compile(), "ps.name =\n ^ expected field, string, number, bool, ip") +} + +func TestFilterRunProcessKevent(t *testing.T) { + kpars := kevent.Kparams{ + kparams.Comm: {Name: kparams.Comm, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe -k RPCSS"}, + kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.AnsiString, Value: "svchost.exe"}, + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.Uint32, Value: uint32(1234)}, + kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.Uint32, Value: uint32(345)}, + } + kevt := &kevent.Kevent{ + Type: ktypes.CreateProcess, + Kparams: kpars, + Name: "CreateProcess", + PS: &pstypes.PS{ + Envs: map[string]string{"ALLUSERSPROFILE": "C:\\ProgramData", "OS": "Windows_NT", "ProgramFiles(x86)": "C:\\Program Files (x86)"}, + Modules: []pstypes.Module{ + {Name: "C:\\Windows\\System32\\kernel32.dll", Size: 12354, Checksum: 23123343, BaseAddress: kparams.Hex("fff23fff"), DefaultBaseAddress: kparams.Hex("fff124fd")}, + {Name: "C:\\Windows\\System32\\user32.dll", Size: 212354, Checksum: 33123343, BaseAddress: kparams.Hex("fef23fff"), DefaultBaseAddress: kparams.Hex("fff124fd")}, + }, + }, + } + kevt.Timestamp, _ = time.Parse(time.RFC3339, "2011-05-03T15:04:05.323Z") + + var tests = []struct { + filter string + matches bool + }{ + + {`ps.name = 'svchost.exe'`, true}, + {`ps.name = 'svchot.exe'`, false}, + {`ps.name = 'mimikatz.exe' or ps.name contains 'svc'`, true}, + {`ps.envs in ('ALLUSERSPROFILE')`, true}, + {`kevt.name='CreateProcess' and ps.name contains 'svchost'`, true}, + + {`ps.modules IN ('kernel32.dll')`, true}, + {`ps.modules[kernel32.dll].size = 12354`, true}, + {`ps.modules[kernel32.dll].checksum = 23123343`, true}, + {`ps.modules[kernel32.dll].address.default = 'fff124fd'`, true}, + {`ps.modules[kernel32.dll].address.base = 'fff23fff'`, true}, + {`ps.modules[kernel32.dll].location = 'C:\\Windows\\System32'`, true}, + {`ps.modules[xul.dll].size = 12354`, false}, + } + + for i, tt := range tests { + f := New(tt.filter) + err := f.Compile() + if err != nil { + t.Fatal(err) + } + matches := f.Run(kevt) + if matches != tt.matches { + t.Errorf("%d. %q ps filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) + } + } +} + +func TestFilterRunThreadKevent(t *testing.T) { + kpars := kevent.Kparams{ + kparams.Comm: {Name: kparams.Comm, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe -k RPCSS"}, + kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.AnsiString, Value: "svchost.exe"}, + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.Uint32, Value: uint32(1234)}, + kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.Uint32, Value: uint32(345)}, + } + + kevt := &kevent.Kevent{ + Type: ktypes.CreateThread, + Kparams: kpars, + Name: "CreateThread", + PS: &pstypes.PS{ + Envs: map[string]string{"ALLUSERSPROFILE": "C:\\ProgramData", "OS": "Windows_NT", "ProgramFiles(x86)": "C:\\Program Files (x86)"}, + }, + } + + var tests = []struct { + filter string + matches bool + }{ + + {`ps.name = 'svchost.exe'`, true}, + } + + for i, tt := range tests { + f := New(tt.filter) + err := f.Compile() + if err != nil { + t.Fatal(err) + } + matches := f.Run(kevt) + if matches != tt.matches { + t.Errorf("%d. %q thread filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) + } + } +} + +func TestFilterRunKevent(t *testing.T) { + kevt := &kevent.Kevent{ + Type: ktypes.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: kevent.Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + }, + Metadata: map[string]string{"foo": "bar", "fooz": "barz"}, + } + + kevt.Timestamp, _ = time.Parse(time.RFC3339, "2011-05-03T15:04:05.323Z") + + var tests = []struct { + filter string + matches bool + }{ + + {`kevt.seq = 2`, true}, + {`kevt.pid = 859`, true}, + {`kevt.tid = 2484`, true}, + {`kevt.cpu = 1`, true}, + {`kevt.name = 'CreateFile'`, true}, + {`kevt.category = 'file'`, true}, + {`kevt.host = 'archrabbit'`, true}, + {`kevt.nparams = 4`, true}, + + {`kevt.desc contains 'Creates or opens a new file'`, true}, + + {`kevt.date.d = 3 AND kevt.date.m = 5 AND kevt.time.s = 5 AND kevt.time.m = 4 and kevt.time.h = 15`, true}, + {`kevt.time = '15:04:05'`, true}, + } + + for i, tt := range tests { + f := New(tt.filter) + err := f.Compile() + if err != nil { + t.Fatal(err) + } + matches := f.Run(kevt) + if matches != tt.matches { + t.Errorf("%d. %q kevt filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) + } + } +} + +func TestFilterRunNetKevent(t *testing.T) { + kevt := &kevent.Kevent{ + Type: ktypes.SendTCPv4, + Tid: 2484, + PID: 859, + Kparams: kevent.Kparams{ + kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, + kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, + kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv4, Value: net.ParseIP("127.0.0.1")}, + kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("216.58.201.174")}, + }, + } + + var tests = []struct { + filter string + matches bool + }{ + + {`net.dip = 216.58.201.174`, true}, + {`net.dip != 216.58.201.174`, false}, + {`net.dip != 116.58.201.174`, true}, + } + + for i, tt := range tests { + f := New(tt.filter) + err := f.Compile() + if err != nil { + t.Fatal(err) + } + matches := f.Run(kevt) + if matches != tt.matches { + t.Errorf("%d. %q net filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) + } + } +} + +func TestFilterRunPE(t *testing.T) { + kevt := &kevent.Kevent{ + PS: &pstypes.PS{ + PE: &pe.PE{ + NumberOfSections: 2, + NumberOfSymbols: 10, + EntryPoint: "20110", + ImageBase: "140000000", + LinkTime: time.Now(), + Sections: []pe.Sec{ + {Name: ".text", Size: 132608, Entropy: 6.368381, Md5: "db23dce3911a42e987041d98abd4f7cd"}, + {Name: ".rdata", Size: 35840, Entropy: 5.996976, Md5: "ffa5c960b421ca9887e54966588e97e8"}, + }, + Symbols: []string{"SelectObject", "GetTextFaceW", "EnumFontsW", "TextOutW", "GetProcessHeap"}, + Imports: []string{"GDI32.dll", "USER32.dll", "msvcrt.dll", "api-ms-win-core-libraryloader-l1-2-0.dl"}, + VersionResources: map[string]string{"CompanyName": "Microsoft Corporation", "FileDescription": "Notepad", "FileVersion": "10.0.18362.693"}, + }, + }, + } + + var tests = []struct { + filter string + matches bool + }{ + + {`pe.sections[.text].entropy = 6.368381`, true}, + {`pe.sections[.text].entropy > 4.45`, true}, + {`pe.sections[.text].size = 132608`, true}, + {`pe.symbols IN ('GetTextFaceW', 'GetProcessHeap')`, true}, + {`pe.resources[FileDesc] = 'Notepad'`, true}, + {`pe.nsymbols = 10 AND pe.nsections = 2`, true}, + {`pe.nsections > 1`, true}, + {`pe.address.base = '140000000' AND pe.address.entrypoint = '20110'`, true}, + } + + for i, tt := range tests { + f := New(tt.filter) + err := f.Compile() + if err != nil { + t.Fatal(err) + } + matches := f.Run(kevt) + if matches != tt.matches { + t.Errorf("%d. %q ps filter mismatch: exp=%t got=%t", i, tt.filter, tt.matches, matches) + } + } + +} + +func BenchmarkFilterRun(b *testing.B) { + b.ReportAllocs() + f := New(`ps.name = 'mimikatz.exe' or ps.name contains 'svc'`) + require.NoError(b, f.Compile()) + + kpars := kevent.Kparams{ + kparams.Comm: {Name: kparams.Comm, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe -k RPCSS"}, + kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.AnsiString, Value: "svchost.exe"}, + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.Uint32, Value: uint32(1234)}, + kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.Uint32, Value: uint32(345)}, + } + + kevt := &kevent.Kevent{ + Type: ktypes.CreateProcess, + Kparams: kpars, + Name: "CreateProcess", + } + + for i := 0; i < b.N; i++ { + f.Run(kevt) + } +} diff --git a/pkg/filter/ql/ast.go b/pkg/filter/ql/ast.go new file mode 100644 index 000000000..b28a1196e --- /dev/null +++ b/pkg/filter/ql/ast.go @@ -0,0 +1,611 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2013-2016 Errplane Inc. + */ + +package ql + +import ( + "net" + "strings" +) + +// Eval evaluates expr against a map. +func Eval(expr Expr, m map[string]interface{}) bool { + eval := ValuerEval{Valuer: MapValuer(m)} + v, ok := eval.Eval(expr).(bool) + if !ok { + return false + } + return v +} + +// MapValuer is a valuer that substitutes values for the mapped interface. +type MapValuer map[string]interface{} + +// Value returns the value for a key in the MapValuer. +func (m MapValuer) Value(key string) (interface{}, bool) { + v, ok := m[key] + return v, ok +} + +// Valuer is the interface that wraps the Value() method. +type Valuer interface { + // Value returns the value and existence flag for a given key. + Value(key string) (interface{}, bool) +} + +// ValuerEval will evaluate an expression using the Valuer. +type ValuerEval struct { + Valuer Valuer + + // IntegerFloatDivision will set the eval system to treat + // a division between two integers as a floating point division. + IntegerFloatDivision bool +} + +// Eval evaluates an expression and returns a value. +func (v *ValuerEval) Eval(expr Expr) interface{} { + if expr == nil { + return nil + } + + switch expr := expr.(type) { + case *BinaryExpr: + return v.evalBinaryExpr(expr) + case *IntegerLiteral: + return expr.Value + case *UnsignedLiteral: + return expr.Value + case *DecimalLiteral: + return expr.Value + case *ParenExpr: + return v.Eval(expr.Expr) + case *StringLiteral: + return expr.Value + case *ListLiteral: + return expr.Values + case *FieldLiteral: + val, ok := v.Valuer.Value(expr.Value) + if !ok { + return nil + } + return val + case *IPLiteral: + return expr.Value + default: + return nil + } +} + +func (v *ValuerEval) evalBinaryExpr(expr *BinaryExpr) interface{} { + lhs := v.Eval(expr.LHS) + rhs := v.Eval(expr.RHS) + if lhs == nil && rhs != nil { + // when the LHS is nil and the RHS is a boolean, implicitly cast the + // nil to false. + if _, ok := rhs.(bool); ok { + lhs = false + } + } else if lhs != nil && rhs == nil { + // implicit cast of the RHS nil to false when the LHS is a boolean. + if _, ok := lhs.(bool); ok { + rhs = false + } + } + // evaluate if both sides are simple types. + switch lhs := lhs.(type) { + case bool: + rhs, ok := rhs.(bool) + switch expr.Op { + case and: + return ok && (lhs && rhs) + case or: + return ok && (lhs || rhs) + case eq: + return ok && (lhs == rhs) + case neq: + return ok && (lhs != rhs) + } + case uint8: + switch rhs := rhs.(type) { + case float64: + lhs := float64(lhs) + switch expr.Op { + case eq: + return lhs == rhs + case neq: + return lhs != rhs + case lt: + return lhs < rhs + case lte: + return lhs <= rhs + case gt: + return lhs > rhs + case gte: + return lhs >= rhs + } + case int64: + switch expr.Op { + case eq: + return int64(lhs) == rhs + case neq: + return int64(lhs) != rhs + case lt: + return int64(lhs) < rhs + case lte: + return int64(lhs) <= rhs + case gt: + return int64(lhs) > rhs + case gte: + return int64(lhs) >= rhs + } + case uint64: + switch expr.Op { + case eq: + return uint64(lhs) == rhs + case neq: + return uint64(lhs) != rhs + case lt: + if lhs < 0 { + return true + } + return uint64(lhs) < rhs + case lte: + if lhs < 0 { + return true + } + return uint64(lhs) <= rhs + case gt: + if lhs < 0 { + return false + } + return uint64(lhs) > rhs + case gte: + if lhs < 0 { + return false + } + return uint64(lhs) >= rhs + } + } + case float64: + // try the rhs as a float64, int64, or uint64 + rhsf, ok := rhs.(float64) + if !ok { + switch val := rhs.(type) { + case int64: + rhsf, ok = float64(val), true + case uint64: + rhsf, ok = float64(val), true + } + } + + rhs := rhsf + switch expr.Op { + case eq: + return ok && (lhs == rhs) + case neq: + return ok && (lhs != rhs) + case lt: + return ok && (lhs < rhs) + case lte: + return ok && (lhs <= rhs) + case gt: + return ok && (lhs > rhs) + case gte: + return ok && (lhs >= rhs) + } + case int64: + // try as a float64 to see if a float cast is required. + switch rhs := rhs.(type) { + case float64: + lhs := float64(lhs) + switch expr.Op { + case eq: + return lhs == rhs + case neq: + return lhs != rhs + case lt: + return lhs < rhs + case lte: + return lhs <= rhs + case gt: + return lhs > rhs + case gte: + return lhs >= rhs + } + case int64: + switch expr.Op { + case eq: + return lhs == rhs + case neq: + return lhs != rhs + case lt: + return lhs < rhs + case lte: + return lhs <= rhs + case gt: + return lhs > rhs + case gte: + return lhs >= rhs + } + case uint64: + switch expr.Op { + case eq: + return uint64(lhs) == rhs + case neq: + return uint64(lhs) != rhs + case lt: + if lhs < 0 { + return true + } + return uint64(lhs) < rhs + case lte: + if lhs < 0 { + return true + } + return uint64(lhs) <= rhs + case gt: + if lhs < 0 { + return false + } + return uint64(lhs) > rhs + case gte: + if lhs < 0 { + return false + } + return uint64(lhs) >= rhs + } + } + case uint64: + // try as a float64 to see if a float cast is required. + switch rhs := rhs.(type) { + case float64: + lhs := float64(lhs) + switch expr.Op { + case eq: + return lhs == rhs + case neq: + return lhs != rhs + case lt: + return lhs < rhs + case lte: + return lhs <= rhs + case gt: + return lhs > rhs + case gte: + return lhs >= rhs + } + case int64: + switch expr.Op { + case eq: + return lhs == uint64(rhs) + case neq: + return lhs != uint64(rhs) + case lt: + if rhs < 0 { + return false + } + return lhs < uint64(rhs) + case lte: + if rhs < 0 { + return false + } + return lhs <= uint64(rhs) + case gt: + if rhs < 0 { + return true + } + return lhs > uint64(rhs) + case gte: + if rhs < 0 { + return true + } + return lhs >= uint64(rhs) + } + case uint64: + switch expr.Op { + case eq: + return lhs == rhs + case neq: + return lhs != rhs + case lt: + return lhs < rhs + case lte: + return lhs <= rhs + case gt: + return lhs > rhs + case gte: + return lhs >= rhs + } + } + case uint32: + switch rhs := rhs.(type) { + case float64: + lhs := float64(lhs) + switch expr.Op { + case eq: + return lhs == rhs + case neq: + return lhs != rhs + case lt: + return lhs < rhs + case lte: + return lhs <= rhs + case gt: + return lhs > rhs + case gte: + return lhs >= rhs + } + case int32: + switch expr.Op { + case eq: + return lhs == uint32(rhs) + case neq: + return lhs != uint32(rhs) + case lt: + if rhs < 0 { + return false + } + return lhs < uint32(rhs) + case lte: + if rhs < 0 { + return false + } + return lhs <= uint32(rhs) + case gt: + if rhs < 0 { + return true + } + return lhs > uint32(rhs) + case gte: + if rhs < 0 { + return true + } + return lhs >= uint32(rhs) + } + case int64: + switch expr.Op { + case eq: + return lhs == uint32(rhs) + case neq: + return lhs != uint32(rhs) + case lt: + if rhs < 0 { + return false + } + return lhs < uint32(rhs) + case lte: + if rhs < 0 { + return false + } + return lhs <= uint32(rhs) + case gt: + if rhs < 0 { + return true + } + return lhs > uint32(rhs) + case gte: + if rhs < 0 { + return true + } + return lhs >= uint32(rhs) + } + case uint32: + switch expr.Op { + case eq: + return lhs == rhs + case neq: + return lhs != rhs + case lt: + return lhs < rhs + case lte: + return lhs <= rhs + case gt: + return lhs > rhs + case gte: + return lhs >= rhs + } + } + case uint16: + switch rhs := rhs.(type) { + case float64: + lhs := float64(lhs) + switch expr.Op { + case eq: + return lhs == rhs + case neq: + return lhs != rhs + case lt: + return lhs < rhs + case lte: + return lhs <= rhs + case gt: + return lhs > rhs + case gte: + return lhs >= rhs + } + case int32: + switch expr.Op { + case eq: + return lhs == uint16(rhs) + case neq: + return lhs != uint16(rhs) + case lt: + if rhs < 0 { + return false + } + return lhs < uint16(rhs) + case lte: + if rhs < 0 { + return false + } + return lhs <= uint16(rhs) + case gt: + if rhs < 0 { + return true + } + return lhs > uint16(rhs) + case gte: + if rhs < 0 { + return true + } + return lhs >= uint16(rhs) + } + case int64: + switch expr.Op { + case eq: + return lhs == uint16(rhs) + case neq: + return lhs != uint16(rhs) + case lt: + if rhs < 0 { + return false + } + return lhs < uint16(rhs) + case lte: + if rhs < 0 { + return false + } + return lhs <= uint16(rhs) + case gt: + if rhs < 0 { + return true + } + return lhs > uint16(rhs) + case gte: + if rhs < 0 { + return true + } + return lhs >= uint16(rhs) + } + case uint16: + switch expr.Op { + case eq: + return lhs == rhs + case neq: + return lhs != rhs + case lt: + return lhs < rhs + case lte: + return lhs <= rhs + case gt: + return lhs > rhs + case gte: + return lhs >= rhs + } + } + case string: + switch expr.Op { + case eq: + rhs, ok := rhs.(string) + if !ok { + return false + } + return lhs == rhs + case neq: + rhs, ok := rhs.(string) + if !ok { + return false + } + return lhs != rhs + case contains: + rhs, ok := rhs.(string) + if !ok { + return false + } + return strings.Contains(lhs, rhs) + case icontains: + rhs, ok := rhs.(string) + if !ok { + return false + } + return strings.Contains(strings.ToLower(lhs), strings.ToLower(rhs)) + case in: + rhs, ok := rhs.([]string) + if !ok { + return false + } + for _, i := range rhs { + if i == lhs { + return true + } + } + case startswith: + rhs, ok := rhs.(string) + if !ok { + return false + } + return strings.HasPrefix(lhs, rhs) + case endswith: + rhs, ok := rhs.(string) + if !ok { + return false + } + return strings.HasSuffix(lhs, rhs) + } + case net.IP: + switch expr.Op { + case eq: + rhs, ok := rhs.(net.IP) + if !ok { + return false + } + return lhs.Equal(rhs) + case neq: + rhs, ok := rhs.(net.IP) + if !ok { + return false + } + return !lhs.Equal(rhs) + } + case []string: + switch expr.Op { + case contains: + rhs, ok := rhs.(string) + if !ok { + return false + } + for _, s := range lhs { + if s == rhs { + return true + } + } + case in: + rhs, ok := rhs.([]string) + if !ok { + return false + } + for _, i := range lhs { + for _, j := range rhs { + if i == j { + return true + } + } + } + } + } + + // the types were not comparable. If our operation was an equality operation, + // return false instead of true. + switch expr.Op { + case eq, neq, lt, lte, gt, gte: + return false + } + return nil +} diff --git a/pkg/filter/ql/error.go b/pkg/filter/ql/error.go new file mode 100644 index 000000000..376624e24 --- /dev/null +++ b/pkg/filter/ql/error.go @@ -0,0 +1,57 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ql + +import ( + "fmt" + "strings" +) + +// ParseError represents an error that occurred during parsing. +type ParseError struct { + Expr string + Message string + Found string + Expected []string + Pos int +} + +// newParseError returns a new instance of ParseError. +func newParseError(found string, expected []string, pos int, expr string) *ParseError { + return &ParseError{Found: found, Expected: expected, Pos: pos, Expr: expr} +} + +// Error returns the string representation of the error. +func (e *ParseError) Error() string { + if e.Message != "" { + return fmt.Sprintf("%s at line %d, char %d", e.Message, e.Pos+1, e.Pos+1) + } + l := e.Pos + 1 + var sb strings.Builder + sb.WriteString(e.Expr) + sb.WriteRune('\n') + for l > 0 { + l-- + sb.WriteRune(' ') + if l == 0 { + sb.WriteString(fmt.Sprintf("^ expected %s", strings.Join(e.Expected, ", "))) + } + } + return sb.String() +} diff --git a/pkg/filter/ql/error_test.go b/pkg/filter/ql/error_test.go new file mode 100644 index 000000000..835e4272e --- /dev/null +++ b/pkg/filter/ql/error_test.go @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ql + +import ( + "github.com/magiconair/properties/assert" + "testing" +) + +func TestParseError(t *testing.T) { + err := newParseError("[", []string{"("}, 10, "ps.name in ['svchost.exe', 'cmd.exe')") + expected := "ps.name in ['svchost.exe', 'cmd.exe')\n" + + " ^ expected (" + assert.Equal(t, expected, err.Error()) +} diff --git a/pkg/filter/ql/expr.go b/pkg/filter/ql/expr.go new file mode 100644 index 000000000..7111d399e --- /dev/null +++ b/pkg/filter/ql/expr.go @@ -0,0 +1,51 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ql + +import "fmt" + +// Node represents a node in the abstract syntax tree. +type Node interface { + String() string +} + +// Expr represents an expression that can be evaluated to a value. +type Expr interface { + Node +} + +// ParenExpr represents a parenthesized expression. +type ParenExpr struct { + Expr Expr +} + +// String returns a string representation of the parenthesized expression. +func (e *ParenExpr) String() string { return fmt.Sprintf("(%s)", e.Expr.String()) } + +// BinaryExpr represents an operation between two expressions. +type BinaryExpr struct { + Op token + LHS Expr + RHS Expr +} + +// String returns a string representation of the binary expression. +func (e *BinaryExpr) String() string { + return fmt.Sprintf("%s %s %s", e.LHS.String(), e.Op.String(), e.RHS.String()) +} diff --git a/pkg/filter/ql/lexer.go b/pkg/filter/ql/lexer.go new file mode 100644 index 000000000..f87898d0d --- /dev/null +++ b/pkg/filter/ql/lexer.go @@ -0,0 +1,507 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2013-2016 Errplane Inc. + */ + +package ql + +import ( + "bufio" + "bytes" + "errors" + "io" + "strconv" + "strings" +) + +// scanner is responsible for splitting up the filter expression into individual tokens. This code is mostly borrowed +// from the influxql repository (https://github.com/influxdata/influxql) with some changes to support the lexing +// of additional tokens such as IP addresses. +type scanner struct { + r *reader +} + +func newScanner(r io.Reader) *scanner { + return &scanner{r: &reader{r: bufio.NewReader(r)}} +} + +// scan returns the next token and position from the underlying reader. +// Also returns the literal text read for strings, numbers, and duration tokens +// since these token types can have different literal representations. +func (s *scanner) scan() (tok token, pos int, lit string) { + // Read next code point. + ch0, pos := s.r.read() + + // if we see whitespace then consume all contiguous whitespace. + // if we see a letter, or certain acceptable special characters, then consume + // as an ident or reserved word. + if isWhitespace(ch0) { + return s.scanWhitespace() + } else if isLetter(ch0) || ch0 == '_' { + s.r.unread() + return s.scanIdent() + } else if isDigit(ch0) { + return s.scanNumber() + } + + // Otherwise parse individual characters. + switch ch0 { + case reof: + return eof, pos, "" + case '"': + s.r.unread() + return s.scanIdent() + case '\'': + return s.scanString() + case '.': + ch1, _ := s.r.read() + s.r.unread() + if isDigit(ch1) { + return s.scanNumber() + } + return dot, pos, "" + case '=': + return eq, pos, "" + case '!': + if ch1, _ := s.r.read(); ch1 == '=' { + return neq, pos, "" + } + s.r.unread() + case '>': + if ch1, _ := s.r.read(); ch1 == '=' { + return gte, pos, "" + } + s.r.unread() + return gt, pos, "" + case '<': + if ch1, _ := s.r.read(); ch1 == '=' { + return lte, pos, "" + } else if ch1 == '>' { + return neq, pos, "" + } + s.r.unread() + return lt, pos, "" + case '(': + return lparen, pos, "" + case ')': + return rparen, pos, "" + case ',': + return comma, pos, "" + } + return illegal, pos, string(ch0) +} + +// scanWhitespace consumes the current rune and all contiguous whitespace. +func (s *scanner) scanWhitespace() (tok token, pos int, lit string) { + // Create a buffer and read the current character into it. + var buf bytes.Buffer + ch, pos := s.r.curr() + _, _ = buf.WriteRune(ch) + + // Read every subsequent whitespace character into the buffer. + // Non-whitespace characters and EOF will cause the loop to exit. + for { + ch, _ = s.r.read() + if ch == reof { + break + } else if !isWhitespace(ch) { + s.r.unread() + break + } else { + _, _ = buf.WriteRune(ch) + } + } + + return ws, pos, buf.String() +} + +func (s *scanner) scanIdent() (tok token, pos int, lit string) { + // Save the starting position of the identifier. + _, pos = s.r.read() + s.r.unread() + + var buf bytes.Buffer + for { + if ch, _ := s.r.read(); ch == reof { + break + } else if ch == '"' { + tok0, pos0, lit0 := s.scanString() + if tok0 == badstr || tok0 == badesc { + return tok0, pos0, lit0 + } + return ident, pos, lit0 + } else if isIdentChar(ch) { + s.r.unread() + buf.WriteString(scanBareIdent(s.r)) + } else { + s.r.unread() + break + } + } + lit = buf.String() + + if tok, lit = lookup(lit); tok != ident { + return tok, pos, lit + } + + return ident, pos, lit +} + +// scanNumber consumes anything that looks like the start of a number. +func (s *scanner) scanNumber() (tok token, pos int, lit string) { + var buf bytes.Buffer + + // Check if the initial rune is a ".". + ch, pos := s.r.curr() + if ch == '.' { + // Peek and see if the next rune is a digit. + ch1, _ := s.r.read() + s.r.unread() + if !isDigit(ch1) { + return illegal, pos, "." + } + // Unread the full stop so we can read it later. + s.r.unread() + } else { + s.r.unread() + } + + // Read as many digits as possible. + _, _ = buf.WriteString(s.scanDigits()) + + // If next code points are a full stop and digit then consume them. + isDecimal := false + if ch0, _ := s.r.read(); ch0 == '.' { + isDecimal = true + if ch1, _ := s.r.read(); isDigit(ch1) { + _, _ = buf.WriteRune(ch0) + _, _ = buf.WriteRune(ch1) + _, _ = buf.WriteString(s.scanDigits()) + } else { + s.r.unread() + } + } else { + s.r.unread() + } + + // Check if next token is a "." and has at least 2 more subsequent "." runes + // to confirm we have an IP address string + ch, _ = s.r.read() + if ch == '.' { + buf.WriteRune(ch) + nbDots := 2 + for { + buf.WriteString(s.scanDigits()) + ch, _ := s.r.read() + if ch != '.' { + s.r.unread() + break + } + nbDots++ + _, _ = buf.WriteRune(ch) + } + if nbDots != 3 { + s.r.unread() + return badip, pos, buf.String() + } + octets := strings.Split(buf.String(), ".") + if len(octets) != 4 { + return badip, pos, buf.String() + } + // check the range of each octet + for _, oct := range octets { + n, err := strconv.Atoi(oct) + if err != nil { + return badip, pos, buf.String() + } + if n < 0 || n > 255 { + return badip, pos, buf.String() + } + } + return ip, pos, buf.String() + } else { + s.r.unread() + } + + // Read as a duration or integer if it doesn't have a fractional part. + if !isDecimal { + // If the next rune is a letter then this is a duration token. + if ch0, _ := s.r.read(); isLetter(ch0) || ch0 == 'µ' { + _, _ = buf.WriteRune(ch0) + for { + ch1, _ := s.r.read() + if !isLetter(ch1) && ch1 != 'µ' { + s.r.unread() + break + } + _, _ = buf.WriteRune(ch1) + } + + // Continue reading digits and letters as part of this token. + for { + if ch0, _ := s.r.read(); isLetter(ch0) || ch0 == 'µ' || isDigit(ch0) { + _, _ = buf.WriteRune(ch0) + } else { + s.r.unread() + break + } + } + return duration, pos, buf.String() + } else { + s.r.unread() + return integer, pos, buf.String() + } + } + + return dec, pos, buf.String() +} + +// scanDigits consumes a contiguous series of digits. +func (s *scanner) scanDigits() string { + var buf bytes.Buffer + for { + ch, _ := s.r.read() + if !isDigit(ch) { + s.r.unread() + break + } + _, _ = buf.WriteRune(ch) + } + return buf.String() +} + +// scanBareIdent reads bare identifier from a rune reader. +func scanBareIdent(r io.RuneScanner) string { + // Read every ident character into the buffer. + // Non-ident characters and EOF will cause the loop to exit. + var buf bytes.Buffer + for { + ch, _, err := r.ReadRune() + if err != nil { + break + } else if !isIdentChar(ch) { + _ = r.UnreadRune() + break + } else { + _, _ = buf.WriteRune(ch) + } + } + return buf.String() +} + +// scanString consumes a contiguous string of non-quote characters. +// Quote characters can be consumed if they're first escaped with a backslash. +func (s *scanner) scanString() (tok token, pos int, lit string) { + s.r.unread() + _, pos = s.r.curr() + + var err error + lit, err = ScanString(s.r) + if err == errBadString { + return badstr, pos, lit + } else if err == errBadEscape { + _, pos = s.r.curr() + return badstr, pos, lit + } + return str, pos, lit +} + +var errBadString = errors.New("bad string") +var errBadEscape = errors.New("bad escape") + +// ScanString reads a quoted string from a rune reader. +func ScanString(r io.RuneScanner) (string, error) { + ending, _, err := r.ReadRune() + if err != nil { + return "", errBadString + } + + var buf bytes.Buffer + for { + ch0, _, err := r.ReadRune() + if ch0 == ending { + return buf.String(), nil + } else if err != nil || ch0 == '\n' { + return buf.String(), errBadString + } else if ch0 == '\\' { + // If the next character is an escape then write the escaped char. + // If it's not a valid escape then return an error. + ch1, _, _ := r.ReadRune() + if ch1 == 'n' { + _, _ = buf.WriteRune('\n') + } else if ch1 == '\\' { + _, _ = buf.WriteRune('\\') + } else if ch1 == '"' { + _, _ = buf.WriteRune('"') + } else if ch1 == '\'' { + _, _ = buf.WriteRune('\'') + } else { + return string(ch0) + string(ch1), errBadEscape + } + } else { + _, _ = buf.WriteRune(ch0) + } + } +} + +// bufScanner represents a wrapper for scanner to add a buffer. +// It provides a fixed-length circular buffer that can be unread. +type bufScanner struct { + s *scanner + i int // buffer index + n int // buffer size + buf [3]struct { + tok token + pos int + lit string + } +} + +// newBufScanner returns a new buffered scanner for a reader. +func newBufScanner(r io.Reader) *bufScanner { + return &bufScanner{s: newScanner(r)} +} + +// scan reads the next token from the scanner. +func (s *bufScanner) scan() (tok token, pos int, lit string) { + return s.scanFunc(s.s.scan) +} + +// scanFunc uses the provided function to scan the next token. +func (s *bufScanner) scanFunc(scan func() (token, int, string)) (tok token, pos int, lit string) { + // If we have unread tokens then read them off the buffer first. + if s.n > 0 { + s.n-- + return s.curr() + } + + // Move buffer position forward and save the token. + s.i = (s.i + 1) % len(s.buf) + buf := &s.buf[s.i] + buf.tok, buf.pos, buf.lit = scan() + + return s.curr() +} + +// unscan pushes the previously token back onto the buffer. +func (s *bufScanner) unscan() { s.n++ } + +// curr returns the last read token. +func (s *bufScanner) curr() (tok token, pos int, lit string) { + buf := &s.buf[(s.i-s.n+len(s.buf))%len(s.buf)] + return buf.tok, buf.pos, buf.lit +} + +type reader struct { + r io.RuneScanner + i int + n int // buffer char count + pos int // last read rune position + buf [3]struct { + ch rune + pos int + } + eof bool +} + +// readRune reads the next rune from the reader. +// This is a wrapper function to implement the io.RuneReader interface. +// Note that this function does not return size. +func (r *reader) ReadRune() (ch rune, size int, err error) { + ch, _ = r.read() + if ch == reof { + err = io.EOF + } + return +} + +// unreadRune pushes the previously read rune back onto the buffer. +// This is a wrapper function to implement the io.RuneScanner interface. +func (r *reader) UnreadRune() error { + r.unread() + return nil +} + +var reof = rune(0) + +// read reads the next rune from the reader. +func (r *reader) read() (ch rune, pos int) { + // if we have unread characters then read them off the buffer first. + if r.n > 0 { + r.n-- + return r.curr() + } + + // Read next rune from underlying reader. + // Any error (including io.EOF) should return as EOF. + ch, _, err := r.r.ReadRune() + if err != nil { + ch = reof + } else if ch == '\r' { + if ch, _, err := r.r.ReadRune(); err != nil { + // nop + } else if ch != '\n' { + _ = r.r.UnreadRune() + } + ch = '\n' + } + + // Save character and position to the buffer. + r.i = (r.i + 1) % len(r.buf) + buf := &r.buf[r.i] + buf.ch, buf.pos = ch, r.pos + + // Update position. + if !r.eof { + r.pos++ + } + + // Mark the reader as EOF. + // This is used so we don't double count EOF characters. + if ch == reof { + r.eof = true + } + + return r.curr() +} + +// unread pushes the previously read rune back onto the buffer. +func (r *reader) unread() { r.n++ } + +// curr returns the last read character and position. +func (r *reader) curr() (ch rune, pos int) { + i := (r.i - r.n + len(r.buf)) % len(r.buf) + buf := &r.buf[i] + return buf.ch, buf.pos +} + +// isWhitespace returns true if the rune is a space, tab, or newline. +func isWhitespace(ch rune) bool { return ch == ' ' || ch == '\t' || ch == '\n' } + +// isLetter returns true if the rune is a letter. +func isLetter(ch rune) bool { + return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') +} + +// isDigit returns true if the rune is a digit. +func isDigit(ch rune) bool { return ch >= '0' && ch <= '9' } + +// isIdentChar returns true if the rune can be used in an unquoted identifier. $ rune is for special PE section names (e.g. .debug$ | .tls$) +func isIdentChar(ch rune) bool { + return isLetter(ch) || isDigit(ch) || ch == '_' || ch == '.' || ch == '[' || ch == ']' || ch == '$' +} diff --git a/pkg/filter/ql/lexer_test.go b/pkg/filter/ql/lexer_test.go new file mode 100644 index 000000000..a8f1cc04d --- /dev/null +++ b/pkg/filter/ql/lexer_test.go @@ -0,0 +1,97 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ql + +import ( + "strings" + "testing" +) + +func TestScanner(t *testing.T) { + var tests = []struct { + s string + tok token + lit string + pos int + }{ + // special tokens + {s: ``, tok: eof}, + {s: `#`, tok: illegal, lit: `#`}, + {s: ` `, tok: ws, lit: " "}, + {s: "\t", tok: ws, lit: "\t"}, + + // logical operators + {s: `AND`, tok: and}, + {s: `and`, tok: and}, + {s: `OR`, tok: or}, + {s: `or`, tok: or}, + + {s: `=`, tok: eq}, + {s: `<>`, tok: neq}, + {s: `! `, tok: illegal, lit: "!"}, + {s: `<`, tok: lt}, + {s: `<=`, tok: lte}, + {s: `>`, tok: gt}, + {s: `>=`, tok: gte}, + {s: `IN`, tok: in}, + {s: `in`, tok: in}, + + // misc tokens + {s: `(`, tok: lparen}, + {s: `)`, tok: rparen}, + {s: `,`, tok: comma}, + + // fields + {s: `ps.name`, tok: field, lit: "ps.name"}, + {s: `ps.pe.sections[.debug$S].entropy`, tok: field, lit: "ps.pe.sections[.debug$S].entropy"}, + {s: `ps.envs[CommonProgramFiles86]`, tok: field, lit: "ps.envs[CommonProgramFiles86]"}, + + // identifiers + {s: `foo`, tok: ident, lit: `foo`}, + {s: `_foo`, tok: ident, lit: `_foo`}, + {s: `Zx12_3U_-`, tok: ident, lit: `Zx12_3U_`}, + {s: `"foo\"bar\""`, tok: ident, lit: `foo"bar"`}, + + // IP address + {s: "172.17.0.1", tok: ip, lit: "172.17.0.1"}, + {s: "172.17.1", tok: badip, lit: "172.17.1"}, + {s: "172.317.1.2", tok: badip, lit: "172.317.1.2"}, + {s: "172.2.266.2", tok: badip, lit: "172.2.266.2"}, + + // strings + {s: `'testing 123!'`, tok: str, lit: `testing 123!`}, + {s: `'foo\nbar'`, tok: str, lit: "foo\nbar"}, + {s: `'foo\\bar'`, tok: str, lit: "foo\\bar"}, + + // numbers + {s: "6.2323", tok: dec, lit: "6.2323"}, + } + + for i, tt := range tests { + s := newScanner(strings.NewReader(tt.s)) + tok, pos, lit := s.scan() + if tt.tok != tok { + t.Errorf("%d. %q token mismatch: exp=%q got=%q <%q>", i, tt.s, tt.tok, tok, lit) + } else if tt.pos != pos { + t.Errorf("%d. %q pos mismatch: exp=%#v got=%#v", i, tt.s, tt.pos, pos) + } else if tt.lit != lit { + t.Errorf("%d. %q literal mismatch: exp=%q got=%q", i, tt.s, tt.lit, lit) + } + } +} diff --git a/pkg/filter/ql/literal.go b/pkg/filter/ql/literal.go new file mode 100644 index 000000000..a3939ee50 --- /dev/null +++ b/pkg/filter/ql/literal.go @@ -0,0 +1,98 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ql + +import ( + "bytes" + "net" + "strconv" +) + +// StringLiteral represents a string literal. +type StringLiteral struct { + Value string +} + +// FieldLiteral represents a field literal. +type FieldLiteral struct { + Value string +} + +// IntegerLiteral represents a signed number literal. +type IntegerLiteral struct { + Value int64 +} + +// UnsignedLiteral represents an unsigned number literal. +type UnsignedLiteral struct { + Value uint64 +} + +// DecimalLiteral represents an floating point number literal. +type DecimalLiteral struct { + Value float64 +} + +// IPLiteral represents an IP literal. +type IPLiteral struct { + Value net.IP +} + +func (i IPLiteral) String() string { + return i.Value.String() +} + +func (i IntegerLiteral) String() string { + return strconv.Itoa(int(i.Value)) +} + +func (s StringLiteral) String() string { + return s.Value +} + +func (f FieldLiteral) String() string { + return f.Value +} + +func (u UnsignedLiteral) String() string { + return strconv.Itoa(int(u.Value)) +} + +func (d DecimalLiteral) String() string { + return strconv.FormatFloat(d.Value, 'e', -1, 64) +} + +// ListLiteral represents a list of tag key literals. +type ListLiteral struct { + Values []string +} + +// String returns a string representation of the literal. +func (s *ListLiteral) String() string { + var buf bytes.Buffer + _, _ = buf.WriteString("(") + for idx, tagKey := range s.Values { + if idx != 0 { + _, _ = buf.WriteString(", ") + } + _, _ = buf.WriteString(tagKey) + } + _, _ = buf.WriteString(")") + return buf.String() +} diff --git a/pkg/filter/ql/parser.go b/pkg/filter/ql/parser.go new file mode 100644 index 000000000..c8e7339cf --- /dev/null +++ b/pkg/filter/ql/parser.go @@ -0,0 +1,203 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright (c) 2013-2016 Errplane Inc. + */ + +package ql + +import ( + "net" + "strconv" + "strings" +) + +// Parser builds the binary expression tree from the filter string. +type Parser struct { + s *bufScanner + lparen bool + expr string +} + +// NewParsers builds a new parser instance from the expression string. +func NewParser(expr string) *Parser { + return &Parser{s: newBufScanner(strings.NewReader(expr)), expr: expr} +} + +// ParseExpr parses an expression by building the binary expression tree. +func (p *Parser) ParseExpr() (Expr, error) { + var err error + root := &BinaryExpr{} + + // parse a non-binary expression type to start. This variable will always be + // the root of the expression tree. + root.RHS, err = p.parseUnaryExpr() + if err != nil { + return nil, err + } + + // loop over operations and unary exprs and build a tree based on precendence. + for { + // if the next token is NOT an operator then return the expression. + op, pos, lit := p.scanIgnoreWhitespace() + if !op.isOperator() { + p.unscan() + if op == rparen && !p.lparen { + return nil, newParseError(tokstr(op, lit), []string{"("}, pos, p.expr) + } + if op != eof && op != rparen { + return nil, newParseError(tokstr(op, lit), []string{"operator"}, pos, p.expr) + } + return root.RHS, nil + } + + var rhs Expr + if op == in { + // parse required ( token + if tok, pos, lit := p.scanIgnoreWhitespace(); tok != lparen { + return nil, newParseError(tokstr(tok, lit), []string{"("}, pos, p.expr) + } + + // parse a comma-separated list + tagKeys, err := p.parseList() + if err != nil { + return nil, err + } + + // parse required ) token + if tok, pos, lit := p.scanIgnoreWhitespace(); tok != rparen { + return nil, newParseError(tokstr(tok, lit), []string{")"}, pos, p.expr) + } + + rhs = &ListLiteral{Values: tagKeys} + } else { + // Otherwise parse the next expression. + rhs, err = p.parseUnaryExpr() + if err != nil { + return nil, err + } + } + + // find the right spot in the tree to add the new expression by + // descending the RHS of the expression tree until we reach the last + // BinaryExpr or a BinaryExpr whose RHS has an operator with + // precedence >= the operator being added. + for node := root; ; { + r, ok := node.RHS.(*BinaryExpr) + if !ok || r.Op.precedence() >= op.precedence() { + // Add the new expression here and break. + node.RHS = &BinaryExpr{LHS: node.RHS, RHS: rhs, Op: op} + break + } + node = r + } + } +} + +// parseUnaryExpr parses an non-binary expression. +func (p *Parser) parseUnaryExpr() (Expr, error) { + // If the first token is a LPAREN then parse it as its own grouped expression. + if tok, _, _ := p.scanIgnoreWhitespace(); tok == lparen { + p.lparen = true + expr, err := p.ParseExpr() + if err != nil { + return nil, err + } + // Expect an RPAREN at the end. + if tok, pos, lit := p.scanIgnoreWhitespace(); tok != rparen { + return nil, newParseError(tokstr(tok, lit), []string{")"}, pos, p.expr) + } + return &ParenExpr{Expr: expr}, nil + } + + p.unscan() + + tok, pos, lit := p.scanIgnoreWhitespace() + switch tok { + case ip: + return &IPLiteral{Value: net.ParseIP(lit)}, nil + case str: + return &StringLiteral{Value: lit}, nil + case field: + return &FieldLiteral{Value: lit}, nil + case integer: + v, err := strconv.ParseInt(lit, 10, 64) + if err != nil { + // The literal may be too large to fit into an int64. If it is, use an unsigned integer. + // The check for negative numbers is handled somewhere else so this should always be a positive number. + if v, err := strconv.ParseUint(lit, 10, 64); err == nil { + return &UnsignedLiteral{Value: v}, nil + } + return nil, &ParseError{Message: "unable to parse integer", Pos: pos} + } + return &IntegerLiteral{Value: v}, nil + case dec: + v, err := strconv.ParseFloat(lit, 64) + if err != nil { + return nil, &ParseError{Message: "unable to parse decimal", Pos: pos} + } + return &DecimalLiteral{Value: v}, nil + } + + expectations := []string{"field", "string", "number", "bool", "ip"} + if tok == badip { + expectations = []string{"a valid IP address"} + } + p.lparen = false + + return nil, newParseError(tokstr(tok, lit), expectations, pos, p.expr) +} + +func (p *Parser) parseList() ([]string, error) { + tok, pos, lit := p.scanIgnoreWhitespace() + if tok != str { + return []string{}, newParseError(tokstr(tok, lit), []string{"identifier"}, pos, p.expr) + } + idents := []string{lit} + + // parse remaining identifiers + for { + if tok, _, _ := p.scanIgnoreWhitespace(); tok != comma { + p.unscan() + return idents, nil + } + + tok, pos, lit := p.scanIgnoreWhitespace() + if tok != str { + return []string{}, newParseError(tokstr(tok, lit), []string{"identifier"}, pos, p.expr) + } + + idents = append(idents, lit) + } +} + +// scan returns the next token from the underlying scanner. +func (p *Parser) scan() (tok token, pos int, lit string) { return p.s.scan() } + +// scanIgnoreWhitespace scans the next non-whitespace. +func (p *Parser) scanIgnoreWhitespace() (tok token, pos int, lit string) { + for { + tok, pos, lit = p.scan() + if tok == ws { + continue + } + return + } +} + +// unscan pushes the previously read token back onto the buffer. +func (p *Parser) unscan() { p.s.unscan() } diff --git a/pkg/filter/ql/parser_test.go b/pkg/filter/ql/parser_test.go new file mode 100644 index 000000000..a8330bcf2 --- /dev/null +++ b/pkg/filter/ql/parser_test.go @@ -0,0 +1,69 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ql + +import ( + "errors" + "testing" +) + +func TestParser(t *testing.T) { + var tests = []struct { + expr string + err error + }{ + {expr: "ps.name = 'cmd.exe'"}, + {expr: "ps.name != 'cmd.exe'"}, + {expr: "ps.name <> 'cmd.exe'"}, + {expr: "ps.name <> 'cmd.exe", err: errors.New("ps.name <> 'cmd.exe\n" + + " ^ expected field, string, number, bool, ip")}, + {expr: "ps.name = 123"}, + {expr: "net.dip = 172.17.0.9"}, + {expr: "net.dip = 172.17.0", err: errors.New("net.dip = 172.17.0\n" + + " ^ expected a valid IP address")}, + + {expr: "ps.name = 'cmd.exe' OR ps.name contains 'svc'"}, + {expr: "ps.name = 'cmd.exe' AND (ps.name contains 'svc' OR ps.name != 'lsass')"}, + {expr: "ps.name = 'cmd.exe' AND ps.name contains 'svc' OR ps.name != 'lsass')", err: errors.New("ps.name = 'cmd.exe' AND ps.name contains 'svc' OR ps.name != 'lsass')" + + "^ expected)")}, + {expr: "ps.name = 'cmd.exe' AND (ps.name contains 'svc' OR ps.name != 'lsass'", err: errors.New("ps.name = 'cmd.exe' AND (ps.name contains 'svc' OR ps.name != 'lsass'" + + "^ expected")}, + + {expr: "ps.name = 'cmd.exe' OR ((ps.name contains 'svc' AND ps.name != 'lsass') AND ps.ppid != 1)"}, + + {expr: "ps.name = 'cmd.exe' OR ((ps.name contains 'svc' AND ps.name != 'lsass' AND ps.ppid != 1)", err: errors.New("ps.name = 'cmd.exe' OR ((ps.name contains 'svc' AND ps.name != 'lsass' AND ps.ppid != 1)" + + " ^ expected )")}, + + {expr: "ps.name = 'cmd.exe' OR ((ps.name contains 'svc' AND ps.name != 'lsass') AND ps.ppid != 1", err: errors.New("ps.name = 'cmd.exe' OR ((ps.name contains 'svc' AND ps.name != 'lsass') AND ps.ppid != 1" + + " ^ expected )")}, + + {expr: "ps.none = 'cmd.exe'", err: errors.New("ps.none = 'cmd.exe'" + + " ^ expected field, string, number, bool, ip")}, + } + + for i, tt := range tests { + p := NewParser(tt.expr) + _, err := p.ParseExpr() + if err == nil && tt.err != nil { + t.Errorf("%d. exp=%s expected error=%v", i, tt.expr, tt.err) + } else if err != nil && tt.err == nil { + t.Errorf("%d. exp=%s got error=%v", i, tt.expr, err) + } + } +} diff --git a/pkg/filter/ql/token.go b/pkg/filter/ql/token.go new file mode 100644 index 000000000..24924fc57 --- /dev/null +++ b/pkg/filter/ql/token.go @@ -0,0 +1,157 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ql + +import ( + "github.com/rabbitstack/fibratus/pkg/filter/fields" + "strings" +) + +// token represents the lexical token of the filter expression +type token int + +const ( + illegal token = iota + ws + eof + + field // ps.name + str // 'cmd.exe' + badstr + badesc + ident + dec // 123.3 + integer // 123 + duration // 13h + ip // 192.168.1.23 + badip // 192.156.300.12 + + opBeg + and // and + or // or + not // not + in // in + contains // contains + icontains // icontains + startswith // startswith + endswith // endswith + eq // = + neq // != + lt // < + lte // <= + gt // > + gte // >= + opEnd + + lparen // ( + rparen // ) + comma // , + dot // . +) + +var keywords map[string]token + +func init() { + keywords = make(map[string]token) + for _, tok := range []token{and, or, contains, icontains, not, in, startswith, endswith} { + keywords[strings.ToLower(tokens[tok])] = tok + } +} + +var tokens = [...]string{ + illegal: "ILLEGAL", + eof: "EOF", + ws: "WS", + + ident: "IDENT", + field: "FIELD", + integer: "INTEGER", + dec: "DECIMAL", + duration: "DURATION", + str: "STRING", + badstr: "BADSTRING", + badesc: "BADESCAPE", + ip: "IPADDRESS", + badip: "BADIPADDRESS", + + and: "AND", + or: "OR", + contains: "CONTAINS", + icontains: "ICONTAINS", + not: "NOT", + in: "IN", + startswith: "STARTSWITH", + endswith: "ENDSWITH", + + eq: "=", + neq: "!=", + lt: "<", + lte: "<=", + gt: ">", + gte: ">=", + + lparen: "(", + rparen: ")", + comma: ",", + dot: ".", +} + +// isOperator determines whether the current token is an operator. +func (tok token) isOperator() bool { return tok > opBeg && tok < opEnd } + +// String returns the string representation of the token. +func (tok token) String() string { + if tok >= 0 && tok < token(len(tokens)) { + return tokens[tok] + } + return "" +} + +// precedence returns the operator precedence of the binary operator token. +func (tok token) precedence() int { + switch tok { + case or: + return 1 + case and: + return 2 + case eq, neq, lt, lte, gt, gte: + return 3 + case in, contains, icontains, startswith, endswith: + return 4 + } + return 0 +} + +func tokstr(tok token, lit string) string { + if lit != "" { + return lit + } + return tok.String() +} + +// lookup returns the token associated with a given string. +func lookup(id string) (token, string) { + if tok, ok := keywords[strings.ToLower(id)]; ok { + return tok, "" + } + if tok := fields.Lookup(id); tok != "" { + return field, id + } + return ident, id +} diff --git a/pkg/filter/ql/visitor.go b/pkg/filter/ql/visitor.go new file mode 100644 index 000000000..1010a567a --- /dev/null +++ b/pkg/filter/ql/visitor.go @@ -0,0 +1,49 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ql + +// Visitor can be called by Walk to traverse an AST hierarchy. +// The Visit() function is called once per node. +type Visitor interface { + Visit(Node) Visitor +} + +// Walk traverses a node hierarchy in depth-first order. +func Walk(v Visitor, node Node) { + if node == nil { + return + } + if v = v.Visit(node); v == nil { + return + } + switch n := node.(type) { + case *BinaryExpr: + Walk(v, n.LHS) + Walk(v, n.RHS) + } +} + +// WalkFunc traverses a node hierarchy in depth-first order. +func WalkFunc(node Node, fn func(Node)) { + Walk(walkFuncVisitor(fn), node) +} + +type walkFuncVisitor func(Node) + +func (fn walkFuncVisitor) Visit(n Node) Visitor { fn(n); return fn } diff --git a/fibratus/output/__init__.py b/pkg/fs/_fixtures/.gitkeep similarity index 100% rename from fibratus/output/__init__.py rename to pkg/fs/_fixtures/.gitkeep diff --git a/pkg/fs/attrs.go b/pkg/fs/attrs.go new file mode 100644 index 000000000..607dcff52 --- /dev/null +++ b/pkg/fs/attrs.go @@ -0,0 +1,64 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fs + +// FileAttr represents a type alias for the file attribute enumeration. +type FileAttr uint32 + +const ( + FileDirectory FileAttr = 0x10 + // FileArchive denotes a file or directory that is an archive file or directory. Applications typically use this attribute to mark files for backup or removal. + FileArchive FileAttr = 0x20 + // FileCompressed represents a file or a directory that is compressed. + FileCompressed FileAttr = 0x800 + // FileEncrypted represents a file or a directory that is encrypted + FileEncrypted FileAttr = 0x4000 + // FileHidden designates a file or directory that is hidden, i.e. it is not included in an ordinary directory listing. + FileHidden FileAttr = 0x2 + // FileReparsePoint represents a file or directory that has an associated reparse point, or a file that is a symbolic link. + FileReparsePoint FileAttr = 0x400 + // FileSparse denotes a sparse file. Spares files can optimize disk usage as the system does not allocate disk space for the file regions with sparse data. + FileSparse = 0x200 + // FileTemporary denotes files that are used for temporary storage. + FileTemporary = 0x100 +) + +// FileAttr returns human-readable file attribute name. +func (fa FileAttr) String() string { + switch fa { + case FileDirectory: + return "directory" + case FileArchive: + return "archive" + case FileCompressed: + return "compressed" + case FileEncrypted: + return "encrypted" + case FileHidden: + return "hidden" + case FileReparsePoint: + return "junction" + case FileSparse: + return "sparse" + case FileTemporary: + return "temporary" + default: + return "unknown" + } +} diff --git a/pkg/fs/dev.go b/pkg/fs/dev.go new file mode 100644 index 000000000..0154dc511 --- /dev/null +++ b/pkg/fs/dev.go @@ -0,0 +1,67 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fs + +import ( + "github.com/rabbitstack/fibratus/pkg/syscall/file" + "strings" +) + +const deviceOffset = 8 + +// DevMapper is the minimal interface for the device converters. +type DevMapper interface { + // Convert receives the fully qualified file path and replaces the DOS device name with a drive letter. + Convert(filename string) string +} + +type mapper struct { + cache map[string]string +} + +// NewDevMapper creates a new instance of the DOS device replacer. +func NewDevMapper() DevMapper { + m := &mapper{ + cache: make(map[string]string, 0), + } + // loop through logical drives and query the DOS device name + for _, drive := range file.GetLogicalDrives() { + device, err := file.QueryDosDevice(drive) + if err != nil { + continue + } + m.cache[device] = drive + } + return m +} + +func (m *mapper) Convert(filename string) string { + if filename == "" || len(filename) < deviceOffset { + return filename + } + i := strings.Index(filename[deviceOffset:], "\\") + if i < 0 { + return m.cache[filename] + } + dev := filename[:i+deviceOffset] + if drive, ok := m.cache[dev]; ok { + return strings.Replace(filename, dev, drive, 1) + } + return filename +} diff --git a/pkg/fs/dev_test.go b/pkg/fs/dev_test.go new file mode 100644 index 000000000..f5617fa83 --- /dev/null +++ b/pkg/fs/dev_test.go @@ -0,0 +1,70 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fs + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "strings" + "testing" +) + +var drives = []string{ + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z"} + +func TestConvertDosDevice(t *testing.T) { + mapper := NewDevMapper() + files := make([]string, 0, len(drives)) + for _, drive := range drives { + files = append(files, fmt.Sprintf("%s:\\Windows\\system32\\kernel32.dll", drive)) + } + var filename string + for i := 0; i < len(drives); i++ { + filename = mapper.Convert(fmt.Sprintf("\\Device\\HarddiskVolume%d\\Windows\\system32\\kernel32.dll", i)) + if !strings.HasPrefix(filename, "\\Device") { + break + } + } + assert.Contains(t, files, filename) +} diff --git a/pkg/fs/file.go b/pkg/fs/file.go new file mode 100644 index 000000000..cf363974d --- /dev/null +++ b/pkg/fs/file.go @@ -0,0 +1,114 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fs + +import ( + "expvar" + "github.com/rabbitstack/fibratus/pkg/syscall/file" + "os" + "path/filepath" + "strings" +) + +const ( + directoryFile = 0x00000001 // file being created or opened is a directory file + + deviceCDROM = 0x00000002 + deviceCDROMFs = 0x00000003 + deviceController = 0x00000004 + deviceDatalink = 0x00000005 + deviceDFS = 0x00000006 + deviceDisk = 0x00000007 + deviceDiskFs = 0x00000008 + + devMailslot = 0x0000000c + devNamedPipe = 0x00000011 + + deviceVirtualDisk = 0x00000024 + devConsole = 0x00000050 +) + +// queryVolumeCalls represents the number of times the query volume function was called +var queryVolumeCalls = expvar.NewInt("file.query.volume.info.calls") + +// GetFileType returns the underlying file type. The opts parameter corresponds to the NtCreateFile CreateOptions argument +// that specifies the options to be applied when creating or opening the file. +func GetFileType(filename string, opts uint32) FileType { + if filename == "" { + return Other + } + // if the CreateOptions argument of the NtCreateFile syscall has been invoked + // with the FILE_DIRECTORY_FILE flag, it is likely that the target file object + // is a directory. We ensure that by calling the API function for checking whether + // the path name is truly a directory + if (opts&directoryFile) != 0 && file.IsPathDirectory(filename) { + return Directory + } + // FILE_DIRECTORY_FILE flag only gives us a hint on the CreateFile op outcome. If this flag is + // not present in the argument but the file is a directory, we can apply some simple heuristics + // like checking the extension/suffix, even though they are not bullet proof + if filename[:len(filename)-1] == "\\" || filepath.Ext(filename) == "" { + return Directory + } + // non directory file can be a regular file, logical, virtual or physical device or a volume. + // If the filename doesn't start with a drive letter it's probably not a regular + // file since we already have mapped the DOS name to drive letter + if !strings.HasPrefix(filename, "\\Device") { + return Regular + } + // if the filename contains the HardiskVolume string then we assume it is a file. This + // could happen if we fail to resolve the DOS name + if strings.HasPrefix(filename, "\\Device\\HarddiskVolume") { + return Regular + } + // logical, virtual, physical device or a volume + // obtain the device type that is linked to this file object + return getFileTypeFromVolumeInfo(filename) +} + +func getFileTypeFromVolumeInfo(filename string) FileType { + f, err := os.Open(filename) + if err != nil { + return Other + } + defer f.Close() + + queryVolumeCalls.Add(1) + + dev, err := file.QueryVolumeInfo(f.Fd()) + if err != nil { + return Other + } + switch dev.Type { + case deviceCDROM, deviceCDROMFs, deviceController, + deviceDatalink, deviceDFS, deviceDisk, deviceDiskFs: + if file.IsPathDirectory(filename) { + return Directory + } + return Regular + case devConsole: + return Console + case devMailslot: + return Mailslot + case devNamedPipe: + return Pipe + default: + return Other + } +} diff --git a/pkg/fs/file_test.go b/pkg/fs/file_test.go new file mode 100644 index 000000000..acea22efe --- /dev/null +++ b/pkg/fs/file_test.go @@ -0,0 +1,35 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fs + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestGetFileType(t *testing.T) { + typ := GetFileType(`_fixtures`, 16777249) + assert.Equal(t, Directory, typ) + + typ = GetFileType(`_fixtures`, 25165857) + assert.Equal(t, Directory, typ) + + typ = GetFileType(`C:\Users\bunny\AppData\Local\Mozilla\Firefox\Profiles\profile1.tmp`, 18874368) + assert.Equal(t, Regular, typ) +} diff --git a/pkg/fs/types.go b/pkg/fs/types.go new file mode 100644 index 000000000..96087a9ff --- /dev/null +++ b/pkg/fs/types.go @@ -0,0 +1,123 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fs + +// FileDisposition is the alias for the file disposition modes +type FileDisposition uint8 + +const ( + // Supersede dictates that if the file already exists, it is replaced with the given file. Otherwise the file with given name is created. + Supersede FileDisposition = iota + // Open opens the file if it already exists instead of creating a new file. + Open + // Create fails if the file already exists. + Create + // OpenIf opens the file if it already exists or creates a new file otherwise. + OpenIf + // Overwrite opens and overwrites the file if it already exists. Otherwise it fails. + Overwrite + // OverwriteIf opens and overwrites the file is it already exists. Otherwise it creates a new file. + OverwriteIf +) + +// String returns the textual representation of the file disposition. +func (fd FileDisposition) String() string { + switch fd { + case Supersede: + return "supersede" + case Open: + return "open" + case Create: + return "create" + case OpenIf: + return "openif" + case Overwrite: + return "overwrite" + case OverwriteIf: + return "overwriteif" + default: + return "" + } +} + +// FileType is the the type alias for the file type +type FileType uint8 + +const ( + // File represents the file, volume or hard disk device. + Regular FileType = iota + Directory + Pipe + Console + Mailslot + Other + Unknown +) + +// String returns the textual representation of the file type. +func (typ FileType) String() string { + switch typ { + case Regular: + return "file" + case Directory: + return "directory" + case Pipe: + return "pipe" + case Console: + return "console" + case Mailslot: + return "mailslot" + case Other: + return "other" + default: + return "unknown" + } +} + +// FileShareMode designates a type alias for file share mode values +type FileShareMode uint8 + +const ( + // FileShareRead allows threads to gain read access to the file + FileShareRead FileShareMode = 1 + // FileShareWrite allows threads to gain write access to the file + FileShareWrite FileShareMode = 1 << 1 + // FileShareDelete grants threads the possibility to delete files + FileShareDelete FileShareMode = 1 << 2 +) + +// String returns user-friendly representation of the file share mask. +func (shareMode FileShareMode) String() string { + if shareMode == FileShareRead { + return "r--" + } else if shareMode == FileShareWrite { + return "-w-" + } else if shareMode == FileShareDelete { + return "--d" + } else if shareMode&FileShareRead == FileShareRead && shareMode&FileShareWrite == FileShareWrite { + return "rw-" + } else if shareMode&FileShareRead == FileShareRead && shareMode&FileShareDelete == FileShareDelete { + return "r-d" + } else if shareMode&FileShareWrite == FileShareWrite && shareMode&FileShareDelete == FileShareDelete { + return "-wd" + } else if shareMode&FileShareRead == FileShareRead && shareMode&FileShareWrite == FileShareWrite && shareMode&FileShareDelete == FileShareDelete { + return "rwd" + } + return "---" +} diff --git a/pkg/handle/_fixtures/.fibratus b/pkg/handle/_fixtures/.fibratus new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/handle/alpc.go b/pkg/handle/alpc.go new file mode 100644 index 000000000..76da4b24b --- /dev/null +++ b/pkg/handle/alpc.go @@ -0,0 +1,35 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package handle + +import ( + htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/rabbitstack/fibratus/pkg/syscall/object" + "unsafe" +) + +// GetAlpcPort get ALPC port information for the specified ALPC handle and process id. +func GetAlpcPort(h handle.Handle) (*htypes.AlpcPortInfo, error) { + buf := make([]byte, 16) + if err := object.GetAlpcInformation(h, object.AlpcBasicPortInfo, buf); err != nil { + return nil, err + } + return (*htypes.AlpcPortInfo)(unsafe.Pointer(&buf[0])), nil +} diff --git a/pkg/handle/key.go b/pkg/handle/key.go new file mode 100644 index 000000000..661e28e58 --- /dev/null +++ b/pkg/handle/key.go @@ -0,0 +1,111 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package handle + +import ( + "expvar" + "github.com/rabbitstack/fibratus/pkg/syscall/registry" + "github.com/rabbitstack/fibratus/pkg/syscall/security" + "strings" + "sync" +) + +const ( + hklmPrefixUppercase = "\\REGISTRY\\MACHINE" + hklmPrefixCapitalized = "\\Registry\\Machine" + hkcrPrefixUppercase = "\\REGISTRY\\MACHINE\\SOFTWARE\\CLASSES" + hkcrPrefixCapitalized = "\\Registry\\Machine\\Software\\Classes" + hkuPrefixUppercase = "\\REGISTRY\\USER" + hkuPrefixCapitalized = "\\Registry\\User" + // hive represents an application hive. Application hives are loaded by user-mode processes via RegLoadAppKey to store application-specific state data. + hive = "\\REGISTRY\\A" +) + +var ( + keys = make([]string, 0) + mux sync.Mutex + once sync.Once + // sidsCount reflects the total count of the resolved SIDs + sidsCount = expvar.NewInt("sids.count") + lookupSids = security.LookupAllSids +) + +// FormatKey produces a root,key tuple from registry native key name. +func FormatKey(key string) (registry.Key, string) { + if strings.HasPrefix(key, hklmPrefixUppercase) || strings.HasPrefix(key, hklmPrefixCapitalized) { + return registry.LocalMachine, subkey(key, hklmPrefixUppercase) + } + + if strings.HasPrefix(key, hkcrPrefixUppercase) || strings.HasPrefix(key, hkcrPrefixCapitalized) { + return registry.ClassesRoot, subkey(key, hkcrPrefixUppercase) + } + + once.Do(func() { initKeys() }) + + if root, k := findSIDKey(key); root != registry.InvalidKey { + return root, k + } + + if strings.HasPrefix(key, hkuPrefixUppercase) || strings.HasPrefix(key, hkuPrefixCapitalized) { + return registry.Users, subkey(key, hkuPrefixUppercase) + } + + if strings.HasPrefix(key, hive) { + return registry.Hive, key + } + + return registry.InvalidKey, key +} + +// initKeys retrieves all security identifiers on the local machine and builds a slice of +// prefixes targeting \\Registry\\User\\ and \\Registry\\User\\\\_Classes keys. +func initKeys() { + sids, err := lookupSids() + if err != nil { + return + } + sidsCount.Add(int64(len(sids))) + mux.Lock() + defer mux.Unlock() + for _, sid := range sids { + user := hkuPrefixUppercase + "\\" + sid + keys = append(keys, user, user+"\\_Classes") + } +} + +func findSIDKey(key string) (registry.Key, string) { + mux.Lock() + defer mux.Unlock() + for _, k := range keys { + if strings.HasPrefix(key, k) { + if strings.Contains(key, "_Classes") { + return registry.CurrentUser, strings.Replace(subkey(key, k), "_Classes", "Software\\Classes", -1) + } + return registry.CurrentUser, subkey(key, k) + } + } + return registry.InvalidKey, key +} + +func subkey(key string, prefix string) string { + if len(key) > len(prefix) { + return key[len(prefix)+1:] + } + return "" +} diff --git a/pkg/handle/key_test.go b/pkg/handle/key_test.go new file mode 100644 index 000000000..130f7c7de --- /dev/null +++ b/pkg/handle/key_test.go @@ -0,0 +1,55 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package handle + +import ( + "github.com/rabbitstack/fibratus/pkg/syscall/registry" + "github.com/stretchr/testify/assert" + "testing" +) + +func init() { + lookupSids = func() ([]string, error) { + return []string{"S-1-5-21-2271034452-2606270099-984871569-500", "S-1-5-21-2271034452-2606270099-984871569-501"}, nil + } +} + +func TestFormatKey(t *testing.T) { + root, key := FormatKey(`\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Windows Workflow Foundation 4.0.0.0\Linkage`) + assert.Equal(t, registry.LocalMachine, root) + assert.Equal(t, `SYSTEM\ControlSet001\Services\Windows Workflow Foundation 4.0.0.0\Linkage`, key) + + root, key = FormatKey(`\Registry\Machine\SYSTEM\ControlSet001\Services\Windows Workflow Foundation 4.0.0.0\Linkage`) + assert.Equal(t, `SYSTEM\ControlSet001\Services\Windows Workflow Foundation 4.0.0.0\Linkage`, key) + + root, key = FormatKey(`\REGISTRY\MACHINE`) + assert.Equal(t, registry.LocalMachine, root) + + root, key = FormatKey(`\REGISTRY\USER\S-1-5-21-2271034452-2606270099-984871569-500\Console`) + assert.Equal(t, registry.CurrentUser, root) + assert.Equal(t, `Console`, key) + + root, key = FormatKey(`\REGISTRY\USER\S-1-5-21-2271034452-2606270099-984871569-500\_Classes`) + assert.Equal(t, registry.CurrentUser, root) + assert.Equal(t, `Software\Classes`, key) + + root, key = FormatKey(`\REGISTRY\USER\S-1-5-21-2271034452-2606270099-984871569-500\_Classes\.all`) + assert.Equal(t, registry.CurrentUser, root) + assert.Equal(t, `Software\Classes\.all`, key) +} diff --git a/pkg/handle/mutant.go b/pkg/handle/mutant.go new file mode 100644 index 000000000..7a0e0c354 --- /dev/null +++ b/pkg/handle/mutant.go @@ -0,0 +1,42 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package handle + +import ( + htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/rabbitstack/fibratus/pkg/syscall/object" + "unsafe" +) + +type mutant struct { + count int32 + owned bool + abandoned bool +} + +// GetMutant gets the information about specified mutant handle. +func GetMutant(h handle.Handle) (*htypes.MutantInfo, error) { + buf := make([]byte, 8) + if err := object.QueryMutant(h, object.MutantBasicInfo, buf); err != nil { + return nil, err + } + basicInfo := (*mutant)(unsafe.Pointer(&buf[0])) + return &htypes.MutantInfo{Count: basicInfo.count, IsAbandoned: basicInfo.abandoned}, nil +} diff --git a/pkg/handle/object.go b/pkg/handle/object.go new file mode 100644 index 000000000..c3f39a19b --- /dev/null +++ b/pkg/handle/object.go @@ -0,0 +1,249 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package handle + +import ( + "errors" + "expvar" + "fmt" + errs "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/fs" + htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/syscall/file" + "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/rabbitstack/fibratus/pkg/syscall/object" + "github.com/rabbitstack/fibratus/pkg/syscall/process" + "github.com/rabbitstack/fibratus/pkg/syscall/registry" + "github.com/rabbitstack/fibratus/pkg/util/typesize" + "os" + "sort" + "unsafe" +) + +var ( + // typeBufSize specifies the size of the object type name buffer + typeBufSize = 512 + // nameBufSize specifies the size of the object name buffer + nameBufSize = 1024 + // typesCount counts the number of resolved object type names + typesCount = expvar.NewInt("handle.types.count") + typeMisses = expvar.NewInt("handle.types.name.misses") +) + +var devMapper = fs.NewDevMapper() + +// ObjectTypeStore holds all object type names as exposed by the Object Manager. The store represents a efficient +// way of resolving object type indices to human-friendly names. +type ObjectTypeStore interface { + FindByID(id uint8) string + RegisterType(id uint8, typ string) + TypeNames() []string +} + +type otstore struct { + types map[uint8]string +} + +// NewObjectTypeStore creates a new object store instance. +func NewObjectTypeStore() ObjectTypeStore { + s := &otstore{ + types: make(map[uint8]string), + } + s.queryTypes() + return s +} + +func (s *otstore) FindByID(id uint8) string { + if typ, ok := s.types[id]; ok { + return typ + } + typeMisses.Add(1) + return "" +} + +func (s *otstore) RegisterType(id uint8, typ string) { + s.types[id] = typ +} + +func (s *otstore) TypeNames() []string { + types := make([]string, 0, len(s.types)) + for _, v := range s.types { + types = append(types, v) + } + sort.Slice(types, func(i, j int) bool { return types[i] < types[j] }) + return types +} + +func (s *otstore) queryTypes() { + bufSize := 8824 + buf := make([]byte, bufSize) + size, err := object.Query(0, object.TypesInformationClass, buf) + if err == errs.ErrNeedsReallocateBuffer { + buf = make([]byte, size) + if _, err = object.Query(0, object.TypesInformationClass, buf); err != nil { + return + } + } + + if err != nil { + return + } + + types := (*object.TypesInformation)(unsafe.Pointer(&buf[0])) + typesCount.Add(int64(types.NumberOfTypes)) + + // heavily influenced by ProcessHacker pointer arithmetic hackery to + // dereference the first and all subsequent file object type instances + // starting from the address of the TypesInformation structure + objectTypeInfo := (*object.TypeInformation)(unsafe.Pointer(s.first(buf))) + for i := 0; i < int(types.NumberOfTypes); i++ { + objectTypeInfo = (*object.TypeInformation)(unsafe.Pointer(s.next(objectTypeInfo))) + s.types[objectTypeInfo.TypeIndex] = objectTypeInfo.TypeName.String() + } +} + +func (s *otstore) first(b []byte) unsafe.Pointer { + return unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + (unsafe.Sizeof(object.TypesInformation{})+typesize.Pointer()-1)&^(typesize.Pointer()-1)) +} + +func (s *otstore) next(typ *object.TypeInformation) unsafe.Pointer { + align := (uintptr(typ.TypeName.MaxLength) + typesize.Pointer() - 1) &^ (typesize.Pointer() - 1) + offset := uintptr(unsafe.Pointer(typ)) + unsafe.Sizeof(object.TypeInformation{}) + return unsafe.Pointer(offset + align) +} + +// Duplicate duplicates the handle in the caller process's address space. +func Duplicate(h handle.Handle, pid uint32, access handle.DuplicateAccess) (handle.Handle, error) { + targetPs, err := process.Open(process.DupHandle, false, pid) + if err != nil { + return ^handle.Handle(0), err + } + defer targetPs.Close() + currentPs, err := process.Open(process.DupHandle, false, uint32(os.Getpid())) + if err != nil { + return ^handle.Handle(0), err + } + defer currentPs.Close() + // duplicate the remote handle in the current process's address space. + // Note that for certain handle types this operation might fail + // as they don't permit duplicate operations + dup, err := h.Duplicate(targetPs, currentPs, access) + if err != nil { + return ^handle.Handle(0), fmt.Errorf("couldn't duplicate handle: %v", err) + } + return dup, nil +} + +// QueryType returns the type of the specified handle. +func QueryType(handle handle.Handle) (string, error) { + buffer := make([]byte, typeBufSize) + size, err := object.Query(handle, object.TypeInformationClass, buffer) + if err == errs.ErrNeedsReallocateBuffer { + buffer = make([]byte, size) + if _, err = object.Query(handle, object.TypeInformationClass, buffer); err != nil { + return "", fmt.Errorf("couldn't query handle type after buffer reallocation: %v", err) + } + } + if err != nil { + return "", fmt.Errorf("couldn't query handle type: %v", err) + } + // transform buffer into type information structure and get + // the underlying UNICODE string that identifies handle's type name + typeInfo := (*object.TypeInformation)(unsafe.Pointer(&buffer[0])) + length := typeInfo.TypeName.Length + if length > 0 { + return typeInfo.TypeName.String(), nil + } + return "", errors.New("zero length handle type name encountered") +} + +// QueryName gets the name of the underlying handle reference and extra metadata if it is available. +func QueryName(handle handle.Handle, typ string, withTimeout bool) (string, htypes.Meta, error) { + switch typ { + case File: + if !withTimeout { + return "", nil, nil + } + // delegate the name resolution to the deadlock aware handle timeout + name, err := GetHandleWithTimeout(handle, 500) + if err != nil { + return "", nil, err + } + name = devMapper.Convert(name) + fileInfo := &htypes.FileInfo{IsDirectory: file.IsPathDirectory(name)} + return name, fileInfo, nil + case ALPCPort: + port, err := GetAlpcPort(handle) + if err != nil { + return "", nil, nil + } + return "", port, nil + case Process: + name, err := process.QueryFullImageName(handle) + if err != nil { + return "", nil, nil + } + return name, nil, nil + case Mutant: + mutant, err := GetMutant(handle) + if err != nil { + return "", nil, nil + } + return "", mutant, nil + default: + name, err := queryObjectName(handle) + if err != nil { + return "", nil, err + } + switch typ { + case Key: + key, subkey := FormatKey(name) + rootKey := key.String() + if key == registry.InvalidKey { + return name, nil, nil + } + if subkey != "" { + return rootKey + "\\" + subkey, nil, nil + } + return key.String(), nil, nil + default: + return name, nil, nil + } + } +} + +func queryObjectName(handle handle.Handle) (string, error) { + buffer := make([]byte, nameBufSize) + size, err := object.Query(handle, object.NameInformationClass, buffer) + if err == errs.ErrNeedsReallocateBuffer { + buffer = make([]byte, size) + if _, err = object.Query(handle, object.NameInformationClass, buffer); err != nil { + return "", fmt.Errorf("couldn't query handle name after buffer reallocation: %v", err) + } + } + if err != nil { + return "", fmt.Errorf("couldn't query handle name: %v", err) + } + nameInfo := (*object.NameInformation)(unsafe.Pointer(&buffer[0])) + length := nameInfo.ObjectName.Length + if length > 0 { + return nameInfo.ObjectName.String(), nil + } + return "", nil +} diff --git a/pkg/handle/object_test.go b/pkg/handle/object_test.go new file mode 100644 index 000000000..19e022bfe --- /dev/null +++ b/pkg/handle/object_test.go @@ -0,0 +1,108 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package handle + +import ( + "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/rabbitstack/fibratus/pkg/syscall/process" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "path/filepath" + "syscall" + "testing" + "unsafe" +) + +var ( + modkernel32 = syscall.NewLazyDLL("kernel32.dll") + + procCreateNamedPipeW = modkernel32.NewProc("CreateNamedPipeW") +) + +func createNamedPipe(name *uint16, openMode uint32, pipeMode uint32, maxInstances uint32, outBufSize uint32, inBufSize uint32, defaultTimeout uint32, sa *syscall.SecurityAttributes) (handle syscall.Handle, err error) { + r0, _, e1 := syscall.Syscall9(procCreateNamedPipeW.Addr(), 8, uintptr(unsafe.Pointer(name)), uintptr(openMode), uintptr(pipeMode), uintptr(maxInstances), uintptr(outBufSize), uintptr(inBufSize), uintptr(defaultTimeout), uintptr(unsafe.Pointer(sa)), 0) + handle = syscall.Handle(r0) + if handle == syscall.InvalidHandle { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +// createPipe is mainly borrowed from: https://github.com/natefinch/npipe for testing purposes. +func createPipe(address string, first bool) (syscall.Handle, error) { + n, err := syscall.UTF16PtrFromString(address) + if err != nil { + return 0, err + } + mode := uint32(0x3 | syscall.FILE_FLAG_OVERLAPPED) + if first { + mode |= 0x00080000 + } + return createNamedPipe(n, + mode, + 0x0, + 255, + 512, 512, 0, nil) +} + +func TestQueryAllObjectTypes(t *testing.T) { + otstore := NewObjectTypeStore() + require.Contains(t, otstore.TypeNames(), "Directory") + require.Contains(t, otstore.TypeNames(), "Key") +} + +func TestQueryType(t *testing.T) { + h, err := process.Open(process.QueryInformation, false, uint32(os.Getpid())) + require.NoError(t, err) + defer h.Close() + typeName, err := QueryType(h) + require.NoError(t, err) + assert.Equal(t, Process, typeName) +} + +func TestQueryTypeSmallBuffer(t *testing.T) { + typeBufSize = 25 + h, err := process.Open(process.QueryInformation, false, uint32(os.Getpid())) + require.NoError(t, err) + typeName, err := QueryType(h) + assert.Equal(t, Process, typeName) +} + +func TestQueryNameFileHandle(t *testing.T) { + f, err := syscall.Open("_fixtures/.fibratus", syscall.O_RDONLY, syscall.S_ISUID) + require.NoError(t, err) + defer syscall.Close(f) + handleName, _, err := QueryName(handle.Handle(f), File, false) + require.NoError(t, err) + assert.Equal(t, ".fibratus", filepath.Base(handleName)) +} + +func TestQueryNamedPipe(t *testing.T) { + h, err := createPipe(`\\.\pipe\fibratus`, true) + require.NoError(t, err) + defer syscall.Close(h) + handleName, _, err := QueryName(handle.Handle(h), File, true) + require.NoError(t, err) + assert.Equal(t, `\Device\NamedPipe\fibratus`, handleName) +} diff --git a/pkg/handle/snapshotter.go b/pkg/handle/snapshotter.go new file mode 100644 index 000000000..2842ec07f --- /dev/null +++ b/pkg/handle/snapshotter.go @@ -0,0 +1,414 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package handle + +import ( + "expvar" + "fmt" + "github.com/rabbitstack/fibratus/pkg/config" + errs "github.com/rabbitstack/fibratus/pkg/errors" + htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/rabbitstack/fibratus/pkg/syscall/object" + "github.com/rabbitstack/fibratus/pkg/syscall/process" + "github.com/rabbitstack/fibratus/pkg/syscall/sys" + log "github.com/sirupsen/logrus" + "os" + "strconv" + "sync" + "time" + "unsafe" +) + +var ( + globalBufferSize = 4096 + bufferSize = 1024 + + handleNameQueryFailures = expvar.NewMap("handle.name.query.failures") + handleSnapshotCount = expvar.NewInt("handle.snapshot.count") + handleSnapshotBytes = expvar.NewInt("handle.snapshot.bytes") + + currentPid = uint32(os.Getpid()) +) + +// CreateCallback defines the function that is triggered when new handle is conceived +type CreateCallback func(pid uint32, handle htypes.Handle) + +// DestroyCallback defines the function signature that is fired upon handle's destruction +type DestroyCallback func(pid uint32, num handle.Handle) + +// SnapshotBuildCompleted is the function type for snapshot completed signal +type SnapshotBuildCompleted func(total uint64, withName uint64) + +// Snapshotter keeps the system-wide snapshot of allocated handles always when handle kernel events are enabled or +// supported on the target system. It also provides facilities for obtaining a list of handles pertaining to the specific +// process. +type Snapshotter interface { + // Write updates the snapshotter state by storing a new entry for the inbound create handle event. It also notifies + // the registered callback that a new handle has been created. + Write(kevt *kevent.Kevent) error + // Remove destroys the handle state for the specified handle object. The removal callback is triggered when an item + // is deleted from the store. + Remove(kevt *kevent.Kevent) error + // FindHandles returns a list of all known handles for the specified process identifier. + FindHandles(pid uint32) ([]htypes.Handle, error) + // FindByObject returns the handle for the given handle object reference. + FindByObject(object uint64) (htypes.Handle, bool) + // RegisterCreateCallback registers a function that's triggered when new handle is created. + RegisterCreateCallback(fn CreateCallback) + // RegisterDestroyCallback registers a function that's called when existing handle is disposed. + RegisterDestroyCallback(fn DestroyCallback) + // GetSnapshot returns all the handles present in the snapshotter state. + GetSnapshot() []htypes.Handle +} + +type snapshotter struct { + sync.Mutex + handlesByObject map[uint64]htypes.Handle + hc chan htypes.Handle + hdone chan struct{} + config *config.Config + snapshotBuildCompleted SnapshotBuildCompleted + createCallback CreateCallback + destroyCallback DestroyCallback + store ObjectTypeStore + housekeepTick *time.Ticker + initSnap bool + capture bool +} + +// NewSnapshotter constructs a new instance of the handle snapshotter. If `SnapshotBuildCompleted` function is provided +// it will receive the total number of discovered handles as well as the count of the non-nameless handles. +func NewSnapshotter(config *config.Config, fn SnapshotBuildCompleted) Snapshotter { + s := &snapshotter{ + hc: make(chan htypes.Handle), + hdone: make(chan struct{}, 1), + handlesByObject: make(map[uint64]htypes.Handle), + snapshotBuildCompleted: fn, + config: config, + store: NewObjectTypeStore(), + housekeepTick: time.NewTicker(time.Minute), + initSnap: config.InitHandleSnapshot, + } + + if s.initSnap { + go s.consumeHandles() + go s.initSnapshot() + go s.housekeeping() + } else { + if fn != nil { + fn(0, 0) + } + } + + return s +} + +func NewFromKcap(handles []htypes.Handle) Snapshotter { + handlesByObject := make(map[uint64]htypes.Handle) + for _, h := range handles { + handlesByObject[h.Object] = h + } + return &snapshotter{ + handlesByObject: handlesByObject, + capture: true, + } +} + +func (s *snapshotter) FindByObject(object uint64) (htypes.Handle, bool) { + s.Lock() + defer s.Unlock() + if h, ok := s.handlesByObject[object]; ok { + return h, ok + } + return htypes.Handle{}, false +} + +func (s *snapshotter) FindHandles(pid uint32) ([]htypes.Handle, error) { + if pid == currentPid || pid == 0 { // ignore current and idle processes + return []htypes.Handle{}, nil + } + if s.capture { + handles := make([]htypes.Handle, 0) + s.Lock() + defer s.Unlock() + for _, h := range s.handlesByObject { + if h.Pid == pid { + handles = append(handles, h) + } + } + return handles, nil + } + ps, err := process.Open(process.QueryInformation, false, pid) + if err != nil { + // trying to obtain the handle with `QueryInformation` access on a protected + // process will always fail, so our best effort is to collect handles for those + // processes in the snapshot's state + handles := make([]htypes.Handle, 0) + s.Lock() + defer s.Unlock() + for _, h := range s.handlesByObject { + if h.Pid == pid && h.Type != "" { + handles = append(handles, h) + } + } + return handles, nil + } + defer ps.Close() + buf := make([]byte, bufferSize) + n, err := process.QueryInfo(ps, process.HandleInformationClass, buf) + if err == errs.ErrNeedsReallocateBuffer { + buf = make([]byte, n) + _, err = process.QueryInfo(ps, process.HandleInformationClass, buf) + } + if err != nil { + return nil, fmt.Errorf("unable to query handles for process id %d: %v", pid, err) + } + + snapshot := (*object.ProcessHandleSnapshotInformation)(unsafe.Pointer(&buf[0])) + + // enumerate process's handles and try to resolve + // the type and the name of each allocated handle + handles := make([]htypes.Handle, 0) + count := snapshot.NumberOfHandles + sysHandles := (*[1 << 30]object.ProcessHandleTableEntryInfo)(unsafe.Pointer(&snapshot.Handles[0]))[:count:count] + + for _, sh := range sysHandles { + h, err := s.getHandle(sh.Handle, 0, uint8(sh.ObjectTypeIndex), pid, false) + if err != nil { + continue + } + // ignore file handles since we can't get the file name... + if h.Type == File && h.Name == "" { + continue + } + handles = append(handles, h) + } + + return handles, nil +} + +// initSnapshot builds the initial snapshot state by enumerating system-wide handles. +func (s *snapshotter) initSnapshot() { + size := globalBufferSize + buf := make([]byte, size) + for { + _, err := sys.QuerySystemInformation(object.SystemExtendedHandleInformation, buf) + if err == errs.ErrNeedsReallocateBuffer { + size *= 2 + buf = make([]byte, size) + } else if err == nil { + sysHandleInfo := (*object.SystemHandleInformationEx)(unsafe.Pointer(&buf[0])) + count := int(sysHandleInfo.NumberOfHandles) + sysHandles := (*[1 << 30]object.SystemHandleTableEntryInfoEx)(unsafe.Pointer(&sysHandleInfo.Handles[0]))[:count:count] + + // iterate through available handles to get extended info + // and send handle structure instances to the channel + for _, sysHandle := range sysHandles { + pid := sysHandle.ProcessID + if pid == uintptr(currentPid) { + continue + } + h, err := s.getHandle(sysHandle.Handle, sysHandle.Object, sysHandle.ObjectTypeIndex, uint32(pid), true) + if err != nil || h.Type == "" { + continue + } + s.hc <- h + } + s.hdone <- struct{}{} + break + } else { + log.Warnf("couldn't enumerate system-wide handles: %v", err) + break + } + } +} + +func (s *snapshotter) getHandle(rawHandle handle.Handle, obj uint64, typeIndex uint8, pid uint32, withTimeout bool) (htypes.Handle, error) { + typ := s.store.FindByID(typeIndex) + if typ == "" { + dup, err := Duplicate(rawHandle, pid, handle.AllAccess) + if err != nil { + return htypes.Handle{Num: rawHandle, Object: obj}, nil + } + defer dup.Close() + typ, err = QueryType(dup) + if err != nil { + return htypes.Handle{Num: rawHandle, Object: obj}, nil + } + } + h := htypes.Handle{ + Num: rawHandle, + Object: obj, + Type: typ, + Pid: pid, + } + // use the required duplicate access to query handle name + var dupAccess handle.DuplicateAccess + switch typ { + case ALPCPort: + dupAccess = handle.ReadControlAccess + case Process: + dupAccess = handle.ProcessQueryAccess + case Mutant: + dupAccess = handle.SemaQueryAccess + default: + dupAccess = handle.AllAccess + } + dup, err := Duplicate(rawHandle, pid, dupAccess) + if err != nil { + return h, err + } + defer dup.Close() + h.Name, h.MD, err = QueryName(dup, typ, withTimeout) + if err != nil { + // even though we weren't able to query handle name we still + // return handle info with handle type and other metadata + handleNameQueryFailures.Add(strconv.Itoa(int(pid)), 1) + return h, nil + } + return h, nil +} + +func (s *snapshotter) consumeHandles() { + for { + select { + case h := <-s.hc: + s.Lock() + handleSnapshotCount.Add(1) + handleSnapshotBytes.Add(int64(h.Len())) + s.handlesByObject[h.Object] = h + s.Unlock() + case <-s.hdone: + log.Debug("initial handle enumeration has finalized") + s.Lock() + var withName uint64 + for _, h := range s.handlesByObject { + if h.Name != "" { + withName++ + } + if s.createCallback != nil && h.Type == File { + // for safety reasons related to deadlocks we are skipping file handles + // for Enum/Create process events, we'll send these handles after initial + // system-wide scan has completed + if h.Name != "" { + s.createCallback(h.Pid, h) + } + } + } + s.Unlock() + if s.snapshotBuildCompleted != nil { + s.snapshotBuildCompleted(uint64(len(s.handlesByObject)), withName) + } + return + } + } +} + +func (s *snapshotter) housekeeping() { + for { + <-s.housekeepTick.C + + size := globalBufferSize + buf := make([]byte, size) + loop: + for { + _, err := sys.QuerySystemInformation(object.SystemExtendedHandleInformation, buf) + if err == errs.ErrNeedsReallocateBuffer { + size *= 2 + buf = make([]byte, size) + } else if err == nil { + sysHandleInfo := (*object.SystemHandleInformationEx)(unsafe.Pointer(&buf[0])) + count := int(sysHandleInfo.NumberOfHandles) + sysHandles := (*[1 << 30]object.SystemHandleTableEntryInfoEx)(unsafe.Pointer(&sysHandleInfo.Handles[0]))[:count:count] + + s.Lock() + for _, sysHandle := range sysHandles { + if h, ok := s.handlesByObject[sysHandle.Object]; !ok { + handleSnapshotCount.Add(-1) + handleSnapshotBytes.Add(-int64(h.Len())) + delete(s.handlesByObject, sysHandle.Object) + } + } + s.Unlock() + + break loop + } else { + log.Warnf("couldn't get system-wide handles in housekeeping timer: %v", err) + break loop + } + } + } +} + +func (s *snapshotter) RegisterCreateCallback(fn CreateCallback) { + s.createCallback = fn +} + +func (s *snapshotter) RegisterDestroyCallback(fn DestroyCallback) { + s.destroyCallback = fn +} + +func (s *snapshotter) GetSnapshot() []htypes.Handle { + handles := make([]htypes.Handle, 0, len(s.handlesByObject)) + for _, h := range s.handlesByObject { + handles = append(handles, h) + } + return handles +} + +func (s *snapshotter) Write(kevt *kevent.Kevent) error { + if kevt.Type != ktypes.CreateHandle { + return fmt.Errorf("expected CreateHandle kernel event but got %s", kevt.Type) + } + h := unwrapHandle(kevt) + obj, err := kevt.Kparams.GetHexAsUint64(kparams.HandleObject) + if err != nil { + return err + } + s.Lock() + s.handlesByObject[obj] = h + s.Unlock() + return nil +} + +func (s *snapshotter) Remove(kevt *kevent.Kevent) error { + if kevt.Type != ktypes.CloseHandle { + return fmt.Errorf("expected CloseHandle kernel event but got %s", kevt.Type) + } + obj, err := kevt.Kparams.GetHexAsUint64(kparams.HandleObject) + if err != nil { + return err + } + s.Lock() + delete(s.handlesByObject, obj) + s.Unlock() + return nil +} + +func unwrapHandle(kevt *kevent.Kevent) htypes.Handle { + h := htypes.Handle{} + h.Type, _ = kevt.Kparams.GetString(kparams.HandleObjectTypeName) + h.Object, _ = kevt.Kparams.GetHexAsUint64(kparams.HandleObject) + h.Name, _ = kevt.Kparams.GetString(kparams.HandleObjectName) + return h +} diff --git a/pkg/handle/snapshotter_mock.go b/pkg/handle/snapshotter_mock.go new file mode 100644 index 000000000..3b864f4ab --- /dev/null +++ b/pkg/handle/snapshotter_mock.go @@ -0,0 +1,42 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package handle + +import ( + htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/stretchr/testify/mock" +) + +type SnapshotterMock struct { + mock.Mock +} + +func (s *SnapshotterMock) Write(kevt *kevent.Kevent) error { return nil } +func (s *SnapshotterMock) Remove(kevt *kevent.Kevent) error { return nil } +func (s *SnapshotterMock) FindHandles(pid uint32) ([]htypes.Handle, error) { return nil, nil } +func (s *SnapshotterMock) FindByObject(object uint64) (htypes.Handle, bool) { + return htypes.Handle{}, false +} +func (s *SnapshotterMock) RegisterCreateCallback(fn CreateCallback) {} +func (s *SnapshotterMock) RegisterDestroyCallback(fn DestroyCallback) {} +func (s *SnapshotterMock) GetSnapshot() []htypes.Handle { + handles := s.Called() + return handles.Get(0).([]htypes.Handle) +} diff --git a/pkg/handle/snapshotter_test.go b/pkg/handle/snapshotter_test.go new file mode 100644 index 000000000..0ef809650 --- /dev/null +++ b/pkg/handle/snapshotter_test.go @@ -0,0 +1,45 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package handle + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/stretchr/testify/require" + "testing" +) + +func TestInitSnapshot(t *testing.T) { + ch := make(chan bool) + snap := NewSnapshotter(&config.Config{}, func(total, known uint64) { + ch <- true + }) + require.NotNil(t, snap) + <-ch +} + +func TestFindHandles(t *testing.T) { + snap := NewSnapshotter(&config.Config{}, nil) + handles, err := snap.FindHandles(uint32(6716)) + require.NoError(t, err) + require.NotEmpty(t, handles) + for _, h := range handles { + fmt.Println(h) + } +} diff --git a/pkg/handle/timeout.go b/pkg/handle/timeout.go new file mode 100644 index 000000000..f5a889641 --- /dev/null +++ b/pkg/handle/timeout.go @@ -0,0 +1,122 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package handle + +import ( + "errors" + "expvar" + "fmt" + "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/rabbitstack/fibratus/pkg/syscall/object" + "github.com/rabbitstack/fibratus/pkg/syscall/thread" + "sync/atomic" + "syscall" +) + +var ( + threadHandle handle.Handle + rawHandle atomic.Value + ini object.Event + done object.Event + name string + + waitTimeoutCounts = expvar.NewInt("handle.wait.timeouts") +) + +func init() { + ini, _ = object.NewEvent(false, false) + done, _ = object.NewEvent(false, false) +} + +// GetHandleWithTimeout is in charge of resolving handle names on handle instances that are under the risk +// of producing a deadlock, and thus hanging the caller thread. To prevent this kind of unwanted scenarios, +// deadlock aware timeout calls into `NtQueryObject` in a separate native thread. The thread is reused across +// invocations as it is blocked waiting to be signaled by an event, but the query thread also signals back the main +// thread after completion of the `NtQueryObject` call. If the query thread doesn't notify the main thread after a prudent +// timeout, then the query thread is killed. Subsequent calls for handle name resolution will recreate the thread in case +// of it not being alive. +func GetHandleWithTimeout(handle handle.Handle, timeout uint32) (string, error) { + if threadHandle == 0 { + if err := ini.Reset(); err != nil { + return "", fmt.Errorf("couldn't reset init event: %v", err) + } + if err := done.Reset(); err != nil { + return "", fmt.Errorf("couldn't reset done event: %v", err) + } + h, _, err := thread.Create(nil, syscall.NewCallback(cb)) + if err != nil { + return "", fmt.Errorf("cannot create handle query thread: %v", err) + } + threadHandle = h + } + + rawHandle.Store(handle) + + if err := ini.Set(); err != nil { + return "", err + } + + switch s, _ := syscall.WaitForSingleObject(syscall.Handle(done), timeout); s { + case syscall.WAIT_OBJECT_0: + return name, nil + case syscall.WAIT_TIMEOUT: + waitTimeoutCounts.Add(1) + // kill the thread and wait for its termination to orderly cleanup resources + if err := thread.Terminate(threadHandle, 0); err != nil { + return "", fmt.Errorf("unable to terminate timeout thread: %v", err) + } + if _, err := syscall.WaitForSingleObject(syscall.Handle(threadHandle), timeout); err != nil { + return "", fmt.Errorf("failed awaiting timeout thread termination: %v", err) + } + threadHandle = 0 + threadHandle.Close() + + return "", errors.New("couldn't resolve handle name due to timeout") + } + return "", nil +} + +// CloseTimeout releases handle timeut resources. +func CloseTimeout() error { + if err := ini.Close(); err != nil { + return done.Close() + } + threadHandle.Close() + return done.Close() +} + +func cb(ctx uintptr) uintptr { + for { + s, err := syscall.WaitForSingleObject(syscall.Handle(ini), syscall.INFINITE) + if err != nil || s != syscall.WAIT_OBJECT_0 { + break + } + name, err = queryObjectName(rawHandle.Load().(handle.Handle)) + if err != nil { + if err := done.Set(); err != nil { + break + } + continue + } + if err := done.Set(); err != nil { + break + } + } + return 0 +} diff --git a/pkg/handle/timeout_test.go b/pkg/handle/timeout_test.go new file mode 100644 index 000000000..c6f8c419d --- /dev/null +++ b/pkg/handle/timeout_test.go @@ -0,0 +1,30 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package handle + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestTimeout(t *testing.T) { + deadlockTimeout, err := GetHandleWithTimeout(1, 500) + require.NoError(t, err) + require.NotNil(t, deadlockTimeout) +} diff --git a/pkg/handle/types.go b/pkg/handle/types.go new file mode 100644 index 000000000..fe2f64299 --- /dev/null +++ b/pkg/handle/types.go @@ -0,0 +1,72 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package handle + +const ( + // ALPCPort represents the ALPC (Advanced Local Procedure Call) object ports + ALPCPort = "ALPC Port" + // Directory designates directory objects. They exist only within the object manager scope and do not correspond to any directory on the disk. + Directory = "Directory" + EtwRegistration = "EtwRegistration" + EtwConsumer = "EtwConsumer" + Event = "Event" + // File designates file handles (e.g. pipe, device, mailslot) + File = "File" + Key = "Key" + Job = "Job" + WaitCompletionPacket = "WaitCompletionPacket" + IRTimer = "IRTimer" + TpWorkerFactory = "TpWorkerFactory" + IoCompletion = "IoCompletion" + Thread = "Thread" + Semaphore = "Semaphore" + Section = "Section" + Mutant = "Mutant" + Desktop = "Desktop" + WindowStation = "WindowStation" + Token = "Token" + UserApcReserve = "UserApcReserve" + + Process = "Process" + Unknown = "Unknown" +) + +// GetShortName returns the short name for the handle type. +func GetShortName(typ string) string { + switch typ { + case ALPCPort: + return "alpc" + case Directory: + return "d" + case EtwRegistration: + return "etwr" + case Event: + return "e" + case File: + return "f" + case Process: + return "ps" + case Section: + return "sec" + case Semaphore: + return "sem" + default: + return Unknown + } +} diff --git a/pkg/handle/types/marshaller.go b/pkg/handle/types/marshaller.go new file mode 100644 index 000000000..50f34b92f --- /dev/null +++ b/pkg/handle/types/marshaller.go @@ -0,0 +1,166 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package types + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/rabbitstack/fibratus/pkg/util/bytes" + "unsafe" +) + +// md is the type alias for the metadata type +type md uint8 + +const ( + alpcport md = iota + 1 + mutant + file + unknown + none +) + +// Offset returns the next offset from which to read the binary data. +func (h Handle) Offset() uint16 { + offset := 8 + 8 + 4 + 2 + uint16(len(h.Type)) + 2 + uint16(len(h.Name)) + 1 + if h.MD != nil { + switch h.MD.(type) { + case *AlpcPortInfo: + offset += 16 + case *MutantInfo: + offset += 5 + case *FileInfo: + offset += 1 + } + } + return offset +} + +// Marshal dumps the state of the handle to byte slice that is suitable for serializing to kcap file. +func (h *Handle) Marshal() []byte { + b := make([]byte, 0) + + // write handle id, object address and the pid that owns this handle + b = append(b, bytes.WriteUint64(uint64(h.Num))...) + b = append(b, bytes.WriteUint64(h.Object)...) + b = append(b, bytes.WriteUint32(h.Pid)...) + + // write handle type and name + b = append(b, bytes.WriteUint16(uint16(len(h.Type)))...) + b = append(b, h.Type...) + + b = append(b, bytes.WriteUint16(uint16(len(h.Name)))...) + b = append(b, h.Name...) + + // write handle metadata + if h.MD != nil { + switch meta := h.MD.(type) { + case *AlpcPortInfo: + b = append(b, byte(alpcport)) + b = append(b, bytes.WriteUint32(meta.Flags)...) + b = append(b, bytes.WriteUint32(meta.Seqno)...) + b = append(b, bytes.WriteUint64(uint64(meta.Context))...) + case *MutantInfo: + b = append(b, byte(mutant)) + b = append(b, bytes.WriteUint32(uint32(meta.Count))...) + if meta.IsAbandoned { + b = append(b, 1) + } else { + b = append(b, 0) + } + case *FileInfo: + b = append(b, byte(file)) + if meta.IsDirectory { + b = append(b, 1) + } else { + b = append(b, 0) + } + default: + b = append(b, byte(unknown)) + } + } else { + b = append(b, byte(none)) + } + + return b +} + +// Unmarshal transforms the byte slice back to handle structure. +func (h *Handle) Unmarshal(b []byte) error { + if len(b) < 20 { + return fmt.Errorf("expected at least 20 bytes but got %d bytes", len(b)) + } + + // read handle identifier + h.Num = handle.Handle(bytes.ReadUint64(b[0:])) + // read object address + h.Object = bytes.ReadUint64(b[8:]) + // read pid + h.Pid = bytes.ReadUint32(b[16:]) + + // read handle type and name + l := bytes.ReadUint16(b[20:]) + buf := b[22:] + offset := l + if len(buf) > 0 { + h.Type = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + } + + l = bytes.ReadUint16(b[22+offset:]) + buf = b[24+offset:] + offset += l + if len(buf) > 0 { + h.Name = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + } + + typ := md(b[24+offset]) + if typ == none { + return nil + } + + switch typ { + case alpcport: + alpcPort := &AlpcPortInfo{ + Flags: bytes.ReadUint32(b[25+offset:]), + Seqno: bytes.ReadUint32(b[29+offset:]), + Context: uintptr(bytes.ReadUint64(b[33+offset:])), + } + h.MD = alpcPort + case mutant: + mut := &MutantInfo{ + Count: int32(bytes.ReadUint32(b[25+offset:])), + IsAbandoned: utob(b[29+offset]), + } + h.MD = mut + case file: + f := &FileInfo{ + IsDirectory: utob(b[25+offset]), + } + h.MD = f + } + + return nil +} + +func utob(u uint8) bool { + if u > 0 { + return true + } + return false +} diff --git a/pkg/handle/types/marshaller_test.go b/pkg/handle/types/marshaller_test.go new file mode 100644 index 000000000..fa5c23803 --- /dev/null +++ b/pkg/handle/types/marshaller_test.go @@ -0,0 +1,72 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package types + +import ( + "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestMarshaller(t *testing.T) { + h := Handle{ + Num: handle.Handle(0xffffd105e9baaf70), + Name: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, + Type: "Key", + Object: 777488883434455544, + Pid: uint32(1023), + } + buf := h.Marshal() + + clone := Handle{} + err := clone.Unmarshal(buf) + require.NoError(t, err) + + assert.Equal(t, handle.Handle(18446692422059208560), clone.Num) + assert.Equal(t, "Key", clone.Type) + assert.Equal(t, `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, clone.Name) + assert.Equal(t, uint32(1023), clone.Pid) + assert.Equal(t, uint64(777488883434455544), clone.Object) + + h = Handle{ + Num: handle.Handle(0xefffd105e9adaf70), + Name: `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, + Type: "ALPC Port", + Pid: uint32(1023), + MD: &AlpcPortInfo{ + Seqno: 1, + Context: 0x0, + Flags: 0x0, + }, + } + buf = h.Marshal() + + err = clone.Unmarshal(buf) + require.NoError(t, err) + + assert.Equal(t, handle.Handle(0xefffd105e9adaf70), clone.Num) + assert.Equal(t, "ALPC Port", clone.Type) + assert.Equal(t, `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, clone.Name) + assert.Equal(t, uint32(1023), clone.Pid) + assert.NotNil(t, clone.MD) + assert.IsType(t, &AlpcPortInfo{}, clone.MD) + alpcPortInfo := clone.MD.(*AlpcPortInfo) + assert.Equal(t, uint32(1), alpcPortInfo.Seqno) +} diff --git a/pkg/handle/types/types.go b/pkg/handle/types/types.go new file mode 100644 index 000000000..aefb68e19 --- /dev/null +++ b/pkg/handle/types/types.go @@ -0,0 +1,105 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package types + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "strings" +) + +// Meta represents the type alias for handle meta information +type Meta interface{} + +// Handles represents a collection of handles. +type Handles []Handle + +// Handle stores various metadata specific to the handle allocated by a process. +type Handle struct { + // Num represents the internal handle identifier. + Num handle.Handle `json:"id"` + // Object is the kernel address that this handle references. + Object uint64 `json:"object"` + // Pid represents the process's identifier that owns the handle. + Pid uint32 `json:"-"` + // Type is the type of this handle (e.g. File, Key, Mutant, Section) + Type string `json:"type"` + // Name is the actual value of the handle (e.g. \Device\HarddiskVolume4\Windows\Temp\DPTF) + Name string `json:"name"` + // MD is the handle meta information (e.g. ALPC port info) + MD Meta `json:"meta,omitempty"` +} + +// String returns a string representation of the handle. +func (h Handle) String() string { + return fmt.Sprintf("Num: %d Type: %s, Name: %s, Object: 0x%x, PID: %d", h.Num, h.Type, h.Name, h.Object, h.Pid) +} + +// Len returns the length in bytes of the Handle structure. +func (h Handle) Len() int { + l := 8 + 8 + 4 + len(h.Type) + len(h.Name) + if h.MD != nil { + switch h.MD.(type) { + case *AlpcPortInfo: + l += 16 + case *MutantInfo: + l += 5 + case *FileInfo: + l += 1 + } + } + return l +} + +// NewFromKcap restores handle state from the kcap buffer. +func NewFromKcap(buf []byte) (Handle, error) { + h := Handle{} + err := h.Unmarshal(buf) + if err != nil { + return Handle{}, err + } + return h, nil +} + +// AlpcPortInfo stores ALPC port basic information. +type AlpcPortInfo struct { + Flags uint32 + Seqno uint32 + Context uintptr +} + +// MutantInfo stores metadata about particular mutant object. +type MutantInfo struct { + Count int32 + IsAbandoned bool +} + +// FileInfo contains file handle metadata. +type FileInfo struct { + IsDirectory bool +} + +// String returns the string representation of all handles. +func (handles Handles) String() string { + var sb strings.Builder + for _, h := range handles { + sb.WriteString(h.String() + " | ") + } + return strings.TrimSuffix(sb.String(), " | ") +} diff --git a/pkg/kcap/_fixtures/cap.kcap b/pkg/kcap/_fixtures/cap.kcap new file mode 100644 index 0000000000000000000000000000000000000000..369b2152bcb4e38dc49060942b9b23004156bad0 GIT binary patch literal 977 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 W`?5|MO#S}8{I>XURMYqm0RRAG-H0Rr literal 0 HcmV?d00001 diff --git a/pkg/kcap/config.go b/pkg/kcap/config.go new file mode 100644 index 000000000..bf63f3655 --- /dev/null +++ b/pkg/kcap/config.go @@ -0,0 +1,26 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kcap + +import "time" + +// Config stores options that influences the behaviour of the kernel capture reader/writer. +type Config struct { + FlushPeriod time.Duration +} diff --git a/pkg/kcap/header.go b/pkg/kcap/header.go new file mode 100644 index 000000000..69332424f --- /dev/null +++ b/pkg/kcap/header.go @@ -0,0 +1,51 @@ +// +build kcap + +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kcap + +import ( + "github.com/rabbitstack/fibratus/pkg/kcap/section" + kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version" +) + +// magic has two purposes. It is used to identify kcap files. The magic is stored within the first 8 bytes of the file. +// The reader ensures the magic number matches this constant. Besides identifying the capture file, it serves as an +// input for initializing the byte order on the machine where kcap file is read. This implies capture can be taken on a +// machine with different endianness from the one capture is replayed. +const magic = 0x6669627261747573 + +// major represents the major digit of the kcap file format. Incrementing the major digit makes older kcap readers not +// capable to replay the capture file. +const major = uint8(1) + +// minor represents the minor digit of the kcap file format +const minor = uint8(0) + +// flags denotes extra flags for the purpose of the header description +const flags = uint64(0) + +// ws writes the section block with the specified parameters. +func (w *writer) ws(typ section.Type, ver kcapver.Version, l, size uint32) error { + sec := section.New(typ, ver, l, size) + if _, err := w.zw.Write(sec[:]); err != nil { + return errWriteSection(typ, err) + } + return nil +} diff --git a/pkg/kcap/reader.go b/pkg/kcap/reader.go new file mode 100644 index 000000000..c335f1c3e --- /dev/null +++ b/pkg/kcap/reader.go @@ -0,0 +1,254 @@ +// +build kcap + +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kcap + +import ( + "context" + "errors" + "expvar" + "fmt" + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/filter" + "github.com/rabbitstack/fibratus/pkg/handle" + htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/kcap/section" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/util/bytes" + log "github.com/sirupsen/logrus" + zstd "github.com/valyala/gozstd" + "io" + "os" + "path/filepath" +) + +var ( + errKcapMagicMismatch = errors.New("invalid kcap file magic number") + errMajorVer = errors.New("incompatible kcap version format. Please upgrade Fibratus to newer version") + errReadVersion = func(s string, err error) error { return fmt.Errorf("couldn't read %s version digit: %v", s, err) } + errReadSection = func(s section.Type, err error) error { return fmt.Errorf("couldn't read %s section: %v", s, err) } + + kcapReadKevents = expvar.NewInt("kcap.read.kevents") + kcapDroppedKevents = expvar.NewInt("kcap.dropped.kevents") + kcapReadBytes = expvar.NewInt("kcap.read.bytes") + kcapKeventUnmarshalErrors = expvar.NewInt("kcap.kevent.unmarshal.errors") + kcapHandleUnmarshalErrors = expvar.NewInt("kcap.reader.handle.unmarshal.errors") + kcapDroppedByFilter = expvar.NewInt("kcap.reader.dropped.by.filter") +) + +type reader struct { + zr *zstd.Reader + f *os.File + psnapshotter ps.Snapshotter + hsnapshotter handle.Snapshotter + filter filter.Filter + config *config.Config +} + +// NewReader builds a new instance of the kcap reader. +func NewReader(filename string, config *config.Config) (Reader, error) { + if filepath.Ext(filename) == "" { + filename += ".kcap" + } + f, err := os.Open(filename) + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("%q capture file does not exist", filename) + } + return nil, err + } + zr := zstd.NewReader(f) + + mag := make([]byte, 8) + if n, err := zr.Read(mag); err != nil || n != 8 { + return nil, errKcapMagicMismatch + } + bytes.InitNativeEndian(mag) + // from now on all byte reads will use the endianness of the magic number. + // This guarantees we'll be able to replay kcaptures that were taken + // on a machine with a different endianness from the machine where + // actual kcapture is being read. + if bytes.ReadUint64(mag) != magic { + return nil, errKcapMagicMismatch + } + + maj := make([]byte, 1) + min := make([]byte, 1) + + if n, err := zr.Read(maj); err != nil || n != 1 { + return nil, errReadVersion("major", err) + } + if n, err := zr.Read(min); err != nil || n != 1 { + return nil, errReadVersion("minor", err) + } + if maj[0] < major { + return nil, errMajorVer + } + + // read the flags bit vector but do nothing with it at the moment + flags := make([]byte, 8) + if n, err := zr.Read(flags); err != nil || n != 8 { + return nil, fmt.Errorf("fail to read kcap flags: %v", err) + } + + return &reader{f: f, zr: zr, config: config}, nil +} + +func (r *reader) SetFilter(f filter.Filter) { r.filter = f } + +func (r *reader) Read(ctx context.Context) (chan *kevent.Kevent, chan error) { + errsc := make(chan error, 100) + keventsc := make(chan *kevent.Kevent, 55000) + go func() { + for { + select { + case <-ctx.Done(): + return + default: + } + + var sec section.Section + if _, err := io.ReadFull(r.zr, sec[:]); err != nil { + if err != io.EOF { + errsc <- err + continue + } + break + } + + l := sec.Size() + buf := make([]byte, l) + if _, err := io.ReadFull(r.zr, buf); err != nil { + if err != io.EOF { + errsc <- err + continue + } + break + } + kevt, err := kevent.NewFromKcap(buf) + if err != nil { + errsc <- fmt.Errorf("fail to unmarshal kevent: %v", err) + kcapKeventUnmarshalErrors.Add(1) + continue + } + // update the state of the ps/handle snapshotters + if err := r.updateSnapshotters(kevt); err != nil { + log.Warn(err) + } + + if kevt.Type.Dropped(false) { + continue + } + if r.filter != nil && !r.filter.Run(kevt) { + kcapDroppedByFilter.Add(1) + continue + } + + select { + case keventsc <- kevt: + kcapReadKevents.Add(1) + kcapReadBytes.Add(int64(len(buf))) + default: + kcapDroppedKevents.Add(1) + } + } + }() + + return keventsc, errsc +} + +func (r *reader) Close() error { + if r.zr != nil { + r.zr.Release() + } + if r.f != nil { + return r.f.Close() + } + return nil +} + +func (r *reader) updateSnapshotters(kevt *kevent.Kevent) error { + switch kevt.Type { + case ktypes.TerminateThread, ktypes.TerminateProcess, ktypes.UnloadImage: + if err := r.psnapshotter.Remove(kevt); err != nil { + return err + } + case ktypes.CreateProcess, + ktypes.CreateThread, + ktypes.LoadImage, + ktypes.EnumImage, + ktypes.EnumProcess, ktypes.EnumThread: + if err := r.psnapshotter.WriteFromKcap(kevt); err != nil { + return err + } + case ktypes.CreateHandle: + if err := r.hsnapshotter.Write(kevt); err != nil { + return err + } + case ktypes.CloseHandle: + if err := r.hsnapshotter.Remove(kevt); err != nil { + return err + } + } + if kevt.PS == nil { + kevt.PS = r.psnapshotter.Find(kevt.PID) + } + return nil +} + +func (r *reader) RecoverSnapshotters() (handle.Snapshotter, ps.Snapshotter, error) { + hsnap, err := r.recoverHandleSnapshotter() + if err != nil { + return nil, nil, err + } + r.psnapshotter = ps.NewSnapshotterFromKcap(hsnap, r.config) + return hsnap, r.psnapshotter, nil +} + +func (r *reader) recoverHandleSnapshotter() (handle.Snapshotter, error) { + var sec section.Section + if _, err := io.ReadFull(r.zr, sec[:]); err != nil { + return nil, errReadSection(section.Handle, err) + } + nbHandles := sec.Len() + handles := make([]htypes.Handle, nbHandles) + for i := 0; i < int(nbHandles); i++ { + b := make([]byte, 2) + if _, err := io.ReadFull(r.zr, b); err != nil { + continue + } + + l := bytes.ReadUint16(b) + b = make([]byte, l) + if _, err := io.ReadFull(r.zr, b); err != nil { + continue + } + + var err error + handles[i], err = htypes.NewFromKcap(b) + if err != nil { + kcapHandleUnmarshalErrors.Add(1) + } + } + r.hsnapshotter = handle.NewFromKcap(handles) + return r.hsnapshotter, nil +} diff --git a/pkg/kcap/reader_test.go b/pkg/kcap/reader_test.go new file mode 100644 index 000000000..c042ac9eb --- /dev/null +++ b/pkg/kcap/reader_test.go @@ -0,0 +1,56 @@ +// +build kcap + +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kcap + +import ( + "context" + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/stretchr/testify/require" + "testing" +) + +func TestRead(t *testing.T) { + r, err := NewReader("_fixtures/cap1.kcap", &config.Config{}) + if err != nil { + t.Fatal(err) + } + defer r.Close() + _, _, err = r.RecoverSnapshotters() + require.NoError(t, err) + + ctx := context.Background() + + kevtsc, errs := r.Read(ctx) + i := 0 + for { + select { + case kevt := <-kevtsc: + require.NotNil(t, kevt) + require.True(t, kevt.Seq > 0) + i++ + if i == 100 { + return + } + case err := <-errs: + t.Fatal(t, err) + } + } +} diff --git a/pkg/kcap/reader_unsupported.go b/pkg/kcap/reader_unsupported.go new file mode 100644 index 000000000..d72078763 --- /dev/null +++ b/pkg/kcap/reader_unsupported.go @@ -0,0 +1,31 @@ +// +build !kcap + +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kcap + +import ( + "github.com/rabbitstack/fibratus/pkg/config" + kerrors "github.com/rabbitstack/fibratus/pkg/errors" +) + +// NewReader returns unsupported reader. +func NewReader(filename string, config *config.Config) (Reader, error) { + return nil, kerrors.ErrFeatureUnsupported("kcap") +} diff --git a/pkg/kcap/section/section.go b/pkg/kcap/section/section.go new file mode 100644 index 000000000..578543699 --- /dev/null +++ b/pkg/kcap/section/section.go @@ -0,0 +1,86 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package section + +import ( + "fmt" + kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version" + "github.com/rabbitstack/fibratus/pkg/util/bytes" +) + +// Section represents the header describing the type, length and the version of each section. +type Section [10]byte + +func (s Section) String() string { + return fmt.Sprintf("type: %s, version: %d, len: %d, size: %d", s.Type(), s.Version(), s.Len(), s.Size()) +} + +// Type describes the type of a section +type Type uint8 + +const ( + Process Type = iota + 1 + Handle + Kevt + PE +) + +func (s Type) String() string { + switch s { + case Process: + return "process" + case Handle: + return "handle" + case Kevt: + return "kevent" + case PE: + return "pe" + default: + return "" + } +} + +// New buils a new section block with the specified type, version, optional length and size. +func New(typ Type, ver kcapver.Version, l, size uint32) Section { + var s Section + s[0] = uint8(typ) + s[1] = uint8(ver) + copy(s[2:6], bytes.WriteUint32(l)) + copy(s[6:], bytes.WriteUint32(size)) + return s +} + +// Read reads the section from the byte slice. +func Read(b []byte) Section { + var s Section + copy(s[:], b) + return s +} + +// Type returns the type of this section. +func (s Section) Type() Type { return Type(s[0]) } + +// Version returns the version of the captured section block. +func (s Section) Version() kcapver.Version { return kcapver.Version(s[1]) } + +// Len returns the length of the section. +func (s Section) Len() uint32 { return bytes.ReadUint32(s[2:6]) } + +// Size returns the size of the section. +func (s Section) Size() uint32 { return bytes.ReadUint32(s[6:]) } diff --git a/pkg/kcap/section/section_test.go b/pkg/kcap/section/section_test.go new file mode 100644 index 000000000..e464708c1 --- /dev/null +++ b/pkg/kcap/section/section_test.go @@ -0,0 +1,33 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package section + +import ( + kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSection(t *testing.T) { + s := New(Process, kcapver.ProcessSecV1, uint32(2456), uint32(30000)) + assert.Equal(t, Process, s.Type()) + assert.Equal(t, kcapver.ProcessSecV1, s.Version()) + assert.Equal(t, uint32(2456), s.Len()) + assert.Equal(t, uint32(30000), s.Size()) +} diff --git a/pkg/kcap/types.go b/pkg/kcap/types.go new file mode 100644 index 000000000..33631acef --- /dev/null +++ b/pkg/kcap/types.go @@ -0,0 +1,55 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kcap + +import ( + "context" + "github.com/rabbitstack/fibratus/pkg/filter" + "github.com/rabbitstack/fibratus/pkg/handle" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/ps" +) + +// Writer is the minimal interface that all kcap writers need to satisfy. The kcap file format has the layout as +// depicted in the following diagram: +// +// +-+-+-+-+-+-+-+-++-+-+-+-+-+-+-+-++-+-+-+ +// | Magic Number | Major | Minor | Flags | +// |---------------------------------------- +// | Handle Section | Handles | +// ----------------------------------------- +// | Kevt Section | Kevt ..................| +// | ......................................| +// | ......................................| +// | ......................................| +// | ........ Kevt Section n Kevt n EOF | +// +-+-+-+-+-+-+-+-++-+-+-+-+-+-+-+-++-+-+-+ +// +type Writer interface { + Write(chan *kevent.Kevent, chan error) chan error + Close() error +} + +// Reader offers the mechanism for recovering the state of the kcapture and replaying all captured events. +type Reader interface { + Read(ctx context.Context) (chan *kevent.Kevent, chan error) + Close() error + RecoverSnapshotters() (handle.Snapshotter, ps.Snapshotter, error) + SetFilter(f filter.Filter) +} diff --git a/pkg/kcap/version/version.go b/pkg/kcap/version/version.go new file mode 100644 index 000000000..2851d2f48 --- /dev/null +++ b/pkg/kcap/version/version.go @@ -0,0 +1,38 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package version + +// Version designates the type for specifying the current section version. +type Version uint16 + +const ( + KevtSecV1 Version = iota + 1 +) + +const ( + ProcessSecV1 Version = iota + 1 +) + +const ( + HandleSecV1 Version = iota + 1 +) + +const ( + PESecV1 Version = iota + 1 +) diff --git a/pkg/kcap/writer.go b/pkg/kcap/writer.go new file mode 100644 index 000000000..a2b981574 --- /dev/null +++ b/pkg/kcap/writer.go @@ -0,0 +1,277 @@ +// +build kcap + +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kcap + +import ( + "expvar" + "fmt" + "github.com/dustin/go-humanize" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/rabbitstack/fibratus/pkg/handle" + "github.com/rabbitstack/fibratus/pkg/kcap/section" + kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/util/bytes" + zstd "github.com/valyala/gozstd" + "math" + "os" + "path/filepath" + "time" +) + +var ( + errWriteMagic = func(err error) error { return fmt.Errorf("couldn't write magic number: %v", err) } + errWriteVersion = func(v string, err error) error { return fmt.Errorf("couldn't write %s kcap digit: %v", v, err) } + errWriteSection = func(s section.Type, err error) error { return fmt.Errorf("couldn't write %s kcap section: %v", s, err) } + + handleWriteErrors = expvar.NewInt("kcap.handle.write.errors") + kevtWriteErrors = expvar.NewInt("kcap.kevt.write.errors") + flusherErrors = expvar.NewMap("kcap.flusher.errors") + overflowKevents = expvar.NewInt("kcap.overflow.kevents") + kstreamConsumerErrors = expvar.NewInt("kcap.kstream.consumer.errors") +) + +const maxKevtSize = math.MaxUint32 + +type stats struct { + kcapFile string + + kevtsWritten uint64 + bytesWritten uint64 + handlesWritten uint64 + procsWritten uint64 + + pids map[uint32]bool +} + +func (s *stats) incKevts(kevt *kevent.Kevent) { + switch kevt.Type { + case ktypes.FileRundown, ktypes.RegCreateKCB, ktypes.FileOpEnd, + ktypes.EnumProcess, ktypes.EnumThread, ktypes.EnumImage, ktypes.RegKCBRundown: + default: + s.kevtsWritten++ + } +} +func (s *stats) incBytes(bytes uint64) { s.bytesWritten += bytes } +func (s *stats) incHandles() { s.handlesWritten++ } +func (s *stats) incProcs(kevt *kevent.Kevent) { + // EnumProcess events can arrive twice for the same kernel session, so we + // ignore incrementing the number of processes if we've already seen the process + if kevt.Type == ktypes.EnumProcess { + pid, _ := kevt.Kparams.GetPid() + if _, ok := s.pids[pid]; ok { + return + } + s.pids[pid] = true + } + if kevt.Type == ktypes.CreateProcess || kevt.Type == ktypes.EnumProcess { + s.procsWritten++ + } +} + +func (s *stats) printStats() { + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.SetTitle("Capture Statistics") + t.SetStyle(table.StyleLight) + + t.AppendRow(table.Row{"File", filepath.Base(s.kcapFile)}) + t.AppendSeparator() + + t.AppendRow(table.Row{"Events written", s.kevtsWritten}) + t.AppendRow(table.Row{"Bytes written", s.bytesWritten}) + t.AppendRow(table.Row{"Processes written", s.procsWritten}) + t.AppendRow(table.Row{"Handles written", s.handlesWritten}) + + f, err := os.Stat(s.kcapFile) + if err != nil { + t.Render() + return + } + t.AppendSeparator() + t.AppendRow(table.Row{"Capture size", humanize.Bytes(uint64(f.Size()))}) + + t.Render() +} + +type writer struct { + zw *zstd.Writer + f *os.File + flusher *time.Ticker + psnap ps.Snapshotter + hsnap handle.Snapshotter + stop chan struct{} + // stats contains the capture statistics + stats *stats +} + +// NewWriter constructs a new instance of the kcap writer. +func NewWriter(filename string, psnap ps.Snapshotter, hsnap handle.Snapshotter) (Writer, error) { + if filepath.Ext(filename) == "" { + filename += ".kcap" + } + f, err := os.Create(filename) + if err != nil { + return nil, err + } + zw := zstd.NewWriter(f) + // start by writing the kcap header that is comprised + // of magic number, major/minor digits and the optional + // flags bit vector. The flags bit vector is reserved + // for the future uses. + // The header is followed by the handle snapshot. + // It contains the current state of the system handles + // at the time the capture was started. + // Handle snapshots are prepended with a section + // that describes the version and the number of handles + // in the snapshot. This information is used by the reader to + // restore the state of the snapshotters. + if _, err := zw.Write(bytes.WriteUint64(magic)); err != nil { + return nil, errWriteMagic(err) + } + if _, err := zw.Write([]byte{major}); err != nil { + return nil, errWriteVersion("major", err) + } + if _, err := zw.Write([]byte{minor}); err != nil { + return nil, errWriteVersion("minor", err) + } + if _, err := zw.Write(bytes.WriteUint64(flags)); err != nil { + return nil, err + } + + w := &writer{ + zw: zw, + f: f, + flusher: time.NewTicker(time.Second), + psnap: psnap, + hsnap: hsnap, + stop: make(chan struct{}, 1), + stats: &stats{kcapFile: filename, pids: make(map[uint32]bool)}, + } + + if err := w.writeSnapshots(); err != nil { + return nil, err + } + + go w.flush() + + return w, nil +} + +func (w *writer) writeSnapshots() error { + handles := w.hsnap.GetSnapshot() + // write handle section and the data blocks + err := w.ws(section.Handle, kcapver.HandleSecV1, uint32(len(handles)), 0) + if err != nil { + return err + } + for _, khandle := range handles { + if err := w.writeHandle(khandle.Marshal()); err != nil { + handleWriteErrors.Add(1) + continue + } + w.stats.incHandles() + } + return w.zw.Flush() +} + +func (w *writer) Write(kevtsc chan *kevent.Kevent, errs chan error) chan error { + errsc := make(chan error, 100) + go func() { + for { + select { + case kevt := <-kevtsc: + b := kevt.MarshalRaw() + l := len(b) + if l == 0 { + continue + } + if l > maxKevtSize { + overflowKevents.Add(1) + errsc <- fmt.Errorf("kevent size overflow by %d bytes", l-maxKevtSize) + continue + } + if err := w.ws(section.Kevt, kcapver.KevtSecV1, 0, uint32(l)); err != nil { + kevtWriteErrors.Add(1) + errsc <- err + continue + } + if _, err := w.zw.Write(b); err != nil { + errsc <- err + kevtWriteErrors.Add(1) + } + // update stats + w.stats.incKevts(kevt) + w.stats.incBytes(uint64(l)) + w.stats.incProcs(kevt) + // return to pool + kevt.Release() + case err := <-errs: + errsc <- err + kstreamConsumerErrors.Add(1) + case <-w.stop: + return + } + } + }() + return errsc +} + +func (w *writer) Close() error { + w.stats.printStats() + + w.flusher.Stop() + w.stop <- struct{}{} + if w.zw != nil { + defer w.zw.Release() + if err := w.zw.Close(); err != nil { + return err + } + } + if w.f != nil { + return w.f.Close() + } + + return nil +} + +func (w *writer) flush() { + for { + <-w.flusher.C + err := w.zw.Flush() + if err != nil { + flusherErrors.Add(err.Error(), 1) + } + } +} + +func (w *writer) writeHandle(buf []byte) error { + l := bytes.WriteUint16(uint16(len(buf))) + if _, err := w.zw.Write(l); err != nil { + return err + } + if _, err := w.zw.Write(buf); err != nil { + return err + } + return nil +} diff --git a/pkg/kcap/writer_test.go b/pkg/kcap/writer_test.go new file mode 100644 index 000000000..dbf1c7e29 --- /dev/null +++ b/pkg/kcap/writer_test.go @@ -0,0 +1,163 @@ +// +build kcap + +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kcap + +import ( + "github.com/rabbitstack/fibratus/pkg/handle" + htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/ps" + pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" + shandle "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestWrite(t *testing.T) { + psnap := new(ps.SnapshotterMock) + hsnap := new(handle.SnapshotterMock) + + procs := []*pstypes.PS{ + {PID: 8390, Ppid: 1096, Name: "spotify.exe", Exe: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe`, Comm: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`, Cwd: `C:\Users\admin\AppData\Roaming\Spotify`, SID: "admin\\SYSTEM"}, + {PID: 2436, Ppid: 6304, Name: "firefox.exe", Exe: `C:\Program Files\Mozilla Firefox\firefox.exe`, Comm: `C:\Program Files\Mozilla Firefox\firefox.exe" -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, Cwd: `C:\Program Files\Mozilla Firefox\`, SID: "archrabbit\\SYSTEM"}, + } + + handles := []htypes.Handle{ + {Pid: 8390, Name: "C:\\Windows", Type: "File"}, + {Pid: 8390, Name: "C:\\Windows\\System32", Type: "File"}, + } + + psnap.On("GetSnapshot").Return(procs) + psnap.On("Size").Return(len(procs)) + + hsnap.On("GetSnapshot").Return(handles) + + w, err := NewWriter("_fixtures/cap1.kcap", psnap, hsnap) + require.NoError(t, err) + require.NotNil(t, w) + + kevtsc := make(chan *kevent.Kevent, 100) + errs := make(chan error, 10) + + for i := 0; i < 100; i++ { + typ := ktypes.CreateFile + if i%2 == 0 { + typ = ktypes.CreateProcess + } + kevt := &kevent.Kevent{ + Type: typ, + Tid: 2484, + PID: 859, + CPU: uint8(i / 2), + Seq: uint64(i + 1), + Name: "CreateFile", + Timestamp: time.Now(), + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: kevent.Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + }, + Metadata: map[string]string{"foo": "bar", "fooz": "barz"}, + PS: &pstypes.PS{ + PID: 2436, + Ppid: 6304, + Name: "firefox.exe", + Exe: `C:\Program Files\Mozilla Firefox\firefox.exe`, + Comm: `C:\Program Files\Mozilla Firefox\firefox.exe -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, + Cwd: `C:\Program Files\Mozilla Firefox\`, + SID: "archrabbit\\SYSTEM", + Args: []string{"-contentproc", `--channel="6304.3.1055809391\1014207667`, "-childID", "1", "-isForBrowser", "-prefsHandle", "2584", "-prefMapHandle", "2580", "-prefsLen", "70", "-prefMapSize", "216993", "-parentBuildID"}, + SessionID: 4, + Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, + Handles: []htypes.Handle{ + { + Num: shandle.Handle(0xffffd105e9baaf70), + Name: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, + Type: "Key", + Object: 777488883434455544, + Pid: uint32(1023), + }, + { + Num: shandle.Handle(0xe1ffd105e9baaf70), + Type: "Event", + Object: 777488883434455544, + Pid: uint32(1023), + }, + { + Type: "Event", + }, + { + Num: shandle.Handle(0xe1ecd105e9baaf70), + Type: "Event", + Pid: uint32(1023), + }, + { + Num: shandle.Handle(0xffffd105e9adaf70), + Name: `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, + Type: "ALPC Port", + Pid: uint32(1023), + MD: &htypes.AlpcPortInfo{ + Seqno: 1, + Context: 0x0, + Flags: 0x0, + }, + Object: 457488883434455544, + }, + { + Num: shandle.Handle(0xeaffd105e9adaf30), + Name: `C:\Users\bunny`, + Type: "File", + Pid: uint32(1023), + MD: &htypes.FileInfo{ + IsDirectory: true, + }, + Object: 357488883434455544, + }, + }, + }, + } + if i%2 == 0 { + kevt.PS.Handles = append(kevt.PS.Handles, htypes.Handle{}) + } + kevtsc <- kevt + } + + werrs := w.Write(kevtsc, errs) + quit := make(chan struct{}, 1) + time.AfterFunc(time.Second*5, func() { + quit <- struct{}{} + }) + select { + case err := <-werrs: + t.Fatal(err) + case <-quit: + err := w.Close() + require.NoError(t, err) + return + } +} diff --git a/pkg/kcap/writer_unsupported.go b/pkg/kcap/writer_unsupported.go new file mode 100644 index 000000000..60d7ccba6 --- /dev/null +++ b/pkg/kcap/writer_unsupported.go @@ -0,0 +1,32 @@ +// +build !kcap + +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kcap + +import ( + kerrors "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/handle" + "github.com/rabbitstack/fibratus/pkg/ps" +) + +// NewWriter returns unsupported writer. +func NewWriter(filename string, psnap ps.Snapshotter, hsnap handle.Snapshotter) (Writer, error) { + return nil, kerrors.ErrFeatureUnsupported("kcap") +} diff --git a/pkg/kevent/README.md b/pkg/kevent/README.md new file mode 100644 index 000000000..76d143a0b --- /dev/null +++ b/pkg/kevent/README.md @@ -0,0 +1 @@ +`Kevent` is the fundamental data structure for transporting kernel events. diff --git a/pkg/kevent/batch.go b/pkg/kevent/batch.go new file mode 100644 index 000000000..54202c38f --- /dev/null +++ b/pkg/kevent/batch.go @@ -0,0 +1,58 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kevent + +// Batch contains a sequence of kernel events. +type Batch struct { + Events []*Kevent +} + +// NewBatch produces a new batch from the group of events. +func NewBatch(evts ...*Kevent) *Batch { + return &Batch{Events: evts} +} + +// Len returns the length of the batch. +func (b *Batch) Len() int64 { return int64(len(b.Events)) } + +// Release releases all events from the batch and returns them to the pool. +func (b *Batch) Release() { + for _, e := range b.Events { + e.Release() + } +} + +// MarshalJSON serializes the batch of events to JSON format. +func (b *Batch) MarshalJSON() []byte { + buf := make([]byte, 0) + buf = append(buf, '[') + for i, kevt := range b.Events { + writeMore := true + if i == len(b.Events)-1 { + writeMore = false + } + buf = append(buf, kevt.MarshalJSON()...) + buf = append(buf, '\n') + if writeMore { + buf = append(buf, ',') + } + } + buf = append(buf, ']') + return buf +} diff --git a/pkg/kevent/batch_test.go b/pkg/kevent/batch_test.go new file mode 100644 index 000000000..3fdd8b34e --- /dev/null +++ b/pkg/kevent/batch_test.go @@ -0,0 +1,252 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kevent + +import ( + "encoding/json" + "github.com/magiconair/properties/assert" + htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" + shandle "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestBatchMarshalJSON(t *testing.T) { + kevt := &Kevent{ + Type: ktypes.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, + kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + }, + Metadata: map[string]string{"foo": "bar", "fooz": "baarz"}, + PS: &pstypes.PS{ + PID: 2436, + Ppid: 6304, + Name: "firefox.exe", + Exe: `C:\Program Files\Mozilla Firefox\firefox.exe`, + Comm: `C:\Program Files\Mozilla Firefox\firefox.exe -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, + Cwd: `C:\Program Files\Mozilla Firefox\`, + SID: "archrabbit\\SYSTEM", + Args: []string{"-contentproc", `--channel=6304.3.1055809391\1014207667`, "-childID", "1", "-isForBrowser", "-prefsHandle", "2584", "-prefMapHandle", "2580", "-prefsLen", "70", "-prefMapSize", "216993", "-parentBuildID"}, + SessionID: 4, + Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, + Threads: map[uint32]pstypes.Thread{ + 3453: {Tid: 3453, Entrypoint: kparams.Hex("0x7ffe2557ff80"), IOPrio: 2, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + 3455: {Tid: 3455, Entrypoint: kparams.Hex("0x5efe2557ff80"), IOPrio: 3, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + }, + Handles: []htypes.Handle{ + {Num: shandle.Handle(0xffffd105e9baaf70), + Name: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, + Type: "Key", + Object: 777488883434455544, + Pid: uint32(1023), + }, + { + Num: shandle.Handle(0xffffd105e9adaf70), + Name: `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, + Type: "ALPC Port", + Pid: uint32(1023), + MD: &htypes.AlpcPortInfo{ + Seqno: 1, + Context: 0x0, + Flags: 0x0, + }, + Object: 457488883434455544, + }, + { + Num: shandle.Handle(0xeaffd105e9adaf30), + Name: `C:\Users\bunny`, + Type: "File", + Pid: uint32(1023), + MD: &htypes.FileInfo{ + IsDirectory: true, + }, + Object: 357488883434455544, + }, + }, + }, + } + + kevt1 := &Kevent{ + Type: ktypes.CreateFile, + Tid: 2484, + PID: 459, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, + kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + }, + Metadata: map[string]string{"foo": "bar", "fooz": "baarz"}, + PS: &pstypes.PS{ + PID: 2436, + Ppid: 6304, + Name: "firefox.exe", + Exe: `C:\Program Files\Mozilla Firefox\firefox.exe`, + Comm: `C:\Program Files\Mozilla Firefox\firefox.exe -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, + Cwd: `C:\Program Files\Mozilla Firefox\`, + SID: "archrabbit\\SYSTEM", + Args: []string{"-contentproc", `--channel=6304.3.1055809391\1014207667`, "-childID", "1", "-isForBrowser", "-prefsHandle", "2584", "-prefMapHandle", "2580", "-prefsLen", "70", "-prefMapSize", "216993", "-parentBuildID"}, + SessionID: 4, + Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, + Threads: map[uint32]pstypes.Thread{ + 3453: {Tid: 3453, Entrypoint: kparams.Hex("0x7ffe2557ff80"), IOPrio: 2, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + 3455: {Tid: 3455, Entrypoint: kparams.Hex("0x5efe2557ff80"), IOPrio: 3, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + }, + Handles: []htypes.Handle{ + {Num: shandle.Handle(0xffffd105e9baaf70), + Name: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, + Type: "Key", + Object: 777488883434455544, + Pid: uint32(1023), + }, + { + Num: shandle.Handle(0xffffd105e9adaf70), + Name: `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, + Type: "ALPC Port", + Pid: uint32(1023), + MD: &htypes.AlpcPortInfo{ + Seqno: 1, + Context: 0x0, + Flags: 0x0, + }, + Object: 457488883434455544, + }, + { + Num: shandle.Handle(0xeaffd105e9adaf30), + Name: `C:\Users\bunny`, + Type: "File", + Pid: uint32(1023), + MD: &htypes.FileInfo{ + IsDirectory: true, + }, + Object: 357488883434455544, + }, + }, + }, + } + + kevt2 := &Kevent{ + Type: ktypes.CreateFile, + Tid: 2484, + PID: 829, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, + kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + }, + Metadata: map[string]string{"foo": "bar", "fooz": "baarz"}, + PS: &pstypes.PS{ + PID: 829, + Ppid: 6304, + Name: "firefox.exe", + Exe: `C:\Program Files\Mozilla Firefox\firefox.exe`, + Comm: `C:\Program Files\Mozilla Firefox\firefox.exe -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, + Cwd: `C:\Program Files\Mozilla Firefox\`, + SID: "archrabbit\\SYSTEM", + Args: []string{"-contentproc", `--channel=6304.3.1055809391\1014207667`, "-childID", "1", "-isForBrowser", "-prefsHandle", "2584", "-prefMapHandle", "2580", "-prefsLen", "70", "-prefMapSize", "216993", "-parentBuildID"}, + SessionID: 4, + Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, + Threads: map[uint32]pstypes.Thread{ + 3453: {Tid: 3453, Entrypoint: kparams.Hex("0x7ffe2557ff80"), IOPrio: 2, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + 3455: {Tid: 3455, Entrypoint: kparams.Hex("0x5efe2557ff80"), IOPrio: 3, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + }, + Handles: []htypes.Handle{ + {Num: shandle.Handle(0xffffd105e9baaf70), + Name: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, + Type: "Key", + Object: 777488883434455544, + Pid: uint32(1023), + }, + { + Num: shandle.Handle(0xffffd105e9adaf70), + Name: `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, + Type: "ALPC Port", + Pid: uint32(1023), + MD: &htypes.AlpcPortInfo{ + Seqno: 1, + Context: 0x0, + Flags: 0x0, + }, + Object: 457488883434455544, + }, + { + Num: shandle.Handle(0xeaffd105e9adaf30), + Name: `C:\Users\bunny`, + Type: "File", + Pid: uint32(1023), + MD: &htypes.FileInfo{ + IsDirectory: true, + }, + Object: 357488883434455544, + }, + }, + }, + } + + b := NewBatch(kevt, kevt1, kevt2) + require.Equal(t, int64(3), b.Len()) + + buf := b.MarshalJSON() + var kevts []*Kevent + + err := json.Unmarshal(buf, &kevts) + require.NoError(t, err) + require.Len(t, kevts, 3) + + assert.Equal(t, uint32(859), kevts[0].PID) + assert.Equal(t, uint32(459), kevts[1].PID) + assert.Equal(t, uint32(829), kevts[2].PID) +} diff --git a/pkg/kevent/doc.go b/pkg/kevent/doc.go new file mode 100644 index 000000000..8b601e5c4 --- /dev/null +++ b/pkg/kevent/doc.go @@ -0,0 +1,21 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package kevent defines the fundamental data structures that underpin the state of every kernel event pushed from the +// consumer. +package kevent diff --git a/pkg/kevent/formatter.go b/pkg/kevent/formatter.go new file mode 100644 index 000000000..18c0144f8 --- /dev/null +++ b/pkg/kevent/formatter.go @@ -0,0 +1,230 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kevent + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/util/fasttemplate" + "regexp" + "sort" + "strconv" + "strings" + "unicode" +) + +const ( + // startTag represents the leading tag surrounding field name + startTag = "{{" + // endTag represents the trailing tag surrounding field name + endTag = "}}" + + seq = ".Seq" + ts = ".Timestamp" + pid = ".Pid" + ppid = ".Ppid" + cwd = ".Cwd" + exe = ".Exe" + comm = ".Comm" + tid = ".Tid" + sid = ".Sid" + proc = ".Process" + cat = ".Category" + desc = ".Description" + cpu = ".CPU" + typ = ".Type" + kparameters = ".Kparams" + meta = ".Meta" + host = ".Host" + pe = ".PE" + kparsAccessor = ".Kparams." +) + +var ( + // tmplRegexp defines the regular expression for parsing template fields. + tmplRegexp = regexp.MustCompile(`({{2}.*?}{2})`) + // tmplNormRegepx defines the regular expression for normalizing the template. This basically consists in removing + // the brackets and trailing/leading spaces from the field name. + tmplNormRegexp = regexp.MustCompile(`({{2}\s*([A-Za-z.]+)\s*}{2})`) + // tmplExpandKparamsRegexp determines whether Kparams. fields are expanded + tmplExpandKparamsRegexp = regexp.MustCompile(`{{\s*.Kparams.\S+}}`) +) + +var kfields = map[string]bool{ + seq: true, + ts: true, + pid: true, + ppid: true, + cwd: true, + exe: true, + comm: true, + tid: true, + sid: true, + proc: true, + cat: true, + desc: true, + cpu: true, + typ: true, + kparameters: true, + meta: true, + host: true, + pe: true, +} + +func hintFields() string { + s := make([]string, 0, len(kfields)) + for field := range kfields { + s = append(s, field) + } + sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) + return strings.Join(s, " ") +} + +// Formatter deals with producing event's output that is dictated by the template. +type Formatter struct { + t *fasttemplate.Template + expandKparamsDot bool +} + +// NewFormatter builds a new instance of event's formatter. +func NewFormatter(template string) (*Formatter, error) { + // check basic template format and ensure all fields + // defined in the template are known to us + fields := tmplRegexp.FindAllStringSubmatch(template, -1) + if len(fields) == 0 { + return nil, fmt.Errorf("invalid template format: %q", template) + } + if ok, pos := isTemplateBalanced(template); !ok { + return nil, fmt.Errorf("template syntax error near field #%d: %q", pos, template) + } + for i, field := range fields { + if len(field) > 0 { + name := sanitize(field[0]) + if strings.HasPrefix(name, kparsAccessor) { + continue + } + if name == "" { + return nil, fmt.Errorf("empty field found at position %d", i+1) + } + if _, ok := kfields[name]; !ok { + return nil, fmt.Errorf("%s is not a known field name. Maybe you meant one "+ + "of the following fields: %s", name, hintFields()) + } + } + } + // user might define the tag such as `{{ .Seq }}` or {{ .Seq}}`. We have to make sure + // inner spaces are removed before building the fast template instance + norm := normalizeTemplate(template) + t, err := fasttemplate.NewTemplate(norm, startTag, endTag) + if err != nil { + return nil, fmt.Errorf("invalid template format %q: %v", norm, err) + } + return &Formatter{ + t: t, + expandKparamsDot: tmplExpandKparamsRegexp.MatchString(norm), + }, nil +} + +func sanitize(s string) string { + return strings.Map(func(r rune) rune { + if r == '{' || r == '}' || unicode.IsSpace(r) { + return -1 + } + return r + }, + s, + ) +} + +func normalizeTemplate(tmpl string) string { return tmplNormRegexp.ReplaceAllString(tmpl, "{{$2}}") } + +const expectedBracketsSeq = "{{}}" + +// isTemplateBalanced ensures the template string is balanced. This means that each tag in the template +// has its pair of leading/trailing brackets. +func isTemplateBalanced(tmpl string) (bool, int) { + // drop all but brackets + s := strings.Map(func(r rune) rune { + if r == '{' || r == '}' { + return r + } + return -1 + }, + tmpl, + ) + // partition slice into 4 groups. Each group must follow + // the correct sequence, otherwise it is an invalid field + partSize := 4 + partitions := len(s) / partSize + var i int + for ; i < partitions; i++ { + if s[i*partSize:(i+1)*partSize] != expectedBracketsSeq { + return false, i + 1 + } + } + if len(s)%partSize != 0 { + if s[i*partSize:] != expectedBracketsSeq { + return false, i + 1 + } + } + return true, -1 +} + +// Format applies the template on the provided kernel event. +func (f *Formatter) Format(kevt *Kevent) []byte { + if kevt == nil { + return []byte{} + } + values := map[string]interface{}{ + ts: kevt.Timestamp.String(), + pid: strconv.FormatUint(uint64(kevt.PID), 10), + tid: strconv.FormatUint(uint64(kevt.Tid), 10), + seq: strconv.FormatUint(kevt.Seq, 10), + cpu: strconv.FormatUint(uint64(kevt.CPU), 10), + typ: kevt.Name, + cat: kevt.Category, + desc: kevt.Description, + host: kevt.Host, + meta: kevt.Metadata.String(), + kparameters: kevt.Kparams.String(), + } + + // add process' metadata + ps := kevt.PS + if ps != nil { + values[proc] = ps.Name + values[ppid] = strconv.FormatUint(uint64(ps.Ppid), 10) + values[cwd] = ps.Cwd + values[exe] = ps.Exe + values[comm] = ps.Comm + values[sid] = ps.SID + if ps.PE != nil { + values[pe] = ps.PE.String() + } + } + + if f.expandKparamsDot { + // expand all parameters into the map so we can ask + // for specific parameter names in the template + for _, kpar := range kevt.Kparams { + values[".Kparams."+strings.Title(kpar.Name)] = kpar.String() + } + } + + return f.t.ExecuteString(values) +} diff --git a/pkg/kevent/formatter_test.go b/pkg/kevent/formatter_test.go new file mode 100644 index 000000000..39f628451 --- /dev/null +++ b/pkg/kevent/formatter_test.go @@ -0,0 +1,137 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kevent + +import ( + htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" + "github.com/stretchr/testify/assert" + + kpars "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/stretchr/testify/require" + "testing" +) + +func TestTemplateUnknownField(t *testing.T) { + template := "{{ .Seq }} {{NUllField1}} {{.Type}}" + _, err := NewFormatter(template) + require.Error(t, err, "NUllField1 is not a known field name. Maybe you meant one of the following fields: .CPU .Category .Comm .Cwd .Description .Exe .Handles .Host .Kparams .Meta .Pid .Ppid .Process .Seq .Sid .Tid .Timestamp .Type") +} + +func TestTemplateEmptyField(t *testing.T) { + template := "{{ .Seq }} {{}} {{.Type}}" + _, err := NewFormatter(template) + require.Error(t, err, "empty field found at position 2") + + template1 := "{{ .Seq }} {{.CPU}} - ({{.Type}}) -- pid: {{}} {{ .Kparams.Pid }} ({{.Kparams}}) {{ .Meta }}" + _, err = NewFormatter(template1) + require.Error(t, err, "empty field found at position 4") +} + +func TestTemplateSyntaxError(t *testing.T) { + template := "{{ .Seq }} {{.CPU}} {.Type}}" + _, err := NewFormatter(template) + require.Error(t, err, "template syntax error near field #3: {{ .Seq }} {{.CPU}} {.Type}}") +} + +func TestFormat(t *testing.T) { + template := "{{ .Seq }} {{.CPU}} - ({{.Type}}) -- pid: {{ .Kparams.Pid }} ({{.Kparams}}) {{ .Meta }}" + f, err := NewFormatter(template) + require.NoError(t, err) + params := Kparams{ + kpars.ProcessID: {Name: kpars.ProcessID, Type: kpars.HexInt32, Value: kpars.Hex("0x36c")}, + } + s := f.Format(&Kevent{CPU: uint8(4), Name: "CreateProcess", Seq: uint64(1999), Kparams: params, Metadata: map[string]string{"key1": "value1", "key2": "value2"}}) + assert.Equal(t, "1999 4 - (CreateProcess) -- pid: 0x36c (pidâžœ 0x36c) key1:value1, key2:value2", string(s)) +} + +func TestFormatPS(t *testing.T) { + template := "{{ .Seq }} {{ .Process }} ({{ .Cwd }}) {{ .Ppid }} ({{ .Sid }})" + f, err := NewFormatter(template) + require.NoError(t, err) + params := Kparams{ + kpars.ProcessID: {Name: kpars.ProcessID, Type: kpars.HexInt32, Value: kpars.Hex("0x36c")}, + } + s := f.Format(&Kevent{ + CPU: uint8(4), + Name: "CreateProcess", + Seq: uint64(1999), + Kparams: params, + PS: &pstypes.PS{ + Name: "cmd.exe", + Cwd: "C:/Windows/System32", + SID: "nedo/archrabbit", + Ppid: 2324, + Handles: htypes.Handles{ + {Name: "C:/Windows/notepad.exe", Type: "File"}, + {Name: "HKEY_LOCAL_MACHINE/Software", Type: "Key"}, + }, + }, + }) + assert.Equal(t, "1999 cmd.exe (C:/Windows/System32) 2324 (nedo/archrabbit)", string(s)) +} + +func TestNormalizeTemplate(t *testing.T) { + assert.Equal(t, "{{.Seq}} {{.CPU}}", normalizeTemplate("{{ .Seq }} {{ .CPU }}")) +} + +func TestIsTemplateBalanced(t *testing.T) { + ok, pos := isTemplateBalanced("{{ .Seq }} {{.CPU}}") + require.True(t, ok) + assert.Equal(t, -1, pos) + + ok, pos = isTemplateBalanced("{{ .Seq }} ({{.CPU}}) [] {{.Type}}") + require.True(t, ok) + assert.Equal(t, -1, pos) + + ok, pos = isTemplateBalanced("{{ .Seq }} {.CPU}} {{.Type}}") + require.False(t, ok) + assert.Equal(t, 2, pos) + + ok, pos = isTemplateBalanced("{.Seq}") + require.False(t, ok) + assert.Equal(t, 1, pos) + + ok, pos = isTemplateBalanced("{{ .Seq }} .CPU }}") + require.False(t, ok) + assert.Equal(t, 2, pos) + + ok, pos = isTemplateBalanced("{{{ .Seq }} {{.CPU}} {{} {{ .Kparams }} { .Kparams.pid}}") + require.False(t, ok) + assert.Equal(t, 1, pos) + + ok, pos = isTemplateBalanced("{{ .Seq }} {{.CPU}} {{} {{ .Kparams }} { .Kparams.pid}}") + require.False(t, ok) + assert.Equal(t, 3, pos) + + ok, pos = isTemplateBalanced("({{ .Seq }}) {{.CPU}} {{}} {{ .Kparams }} { .Kparams.pid}}") + require.False(t, ok) + assert.Equal(t, 5, pos) + + ok, pos = isTemplateBalanced("{{ .Seq } {{.CPU}} {.Type}}") + require.False(t, ok) + assert.Equal(t, 1, pos) + + ok, pos = isTemplateBalanced("{{ .Seq }} {{.CPU}} {.Type}}") + require.False(t, ok) + assert.Equal(t, 3, pos) + + ok, pos = isTemplateBalanced("{{ .Seq }} {{.CPU}} - ({{.Type}}) -- pid: {{]} {{ .Kparams.Pid }} ({{.Kparams}}) {{ .Meta }}") + require.False(t, ok) +} diff --git a/pkg/kevent/kevent.go b/pkg/kevent/kevent.go new file mode 100644 index 000000000..92a888350 --- /dev/null +++ b/pkg/kevent/kevent.go @@ -0,0 +1,191 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kevent + +import ( + "fmt" + kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" + "github.com/rabbitstack/fibratus/pkg/util/hostname" + "strings" + "sync" + "time" +) + +// pool is used to alleviate the pressure on the heap allocator +var pool = sync.Pool{ + New: func() interface{} { + return &Kevent{} + }, +} + +// TimestampFormat is the Go valid format for the kernel event timestamp +var TimestampFormat string + +// Metadata is a type alias for event metadata. Any tag, i.e. key/value pair could be attached to metadata. +type Metadata map[string]string + +// String turns kernel event's metadata into string. +func (md Metadata) String() string { + var sb strings.Builder + for k, v := range md { + sb.WriteString(k + ":" + v + ", ") + } + return strings.TrimSuffix(sb.String(), ", ") +} + +// Kevent encapsulates kernel event's payload. +type Kevent struct { + // Seq is monotonically incremented kernel event sequence. + Seq uint64 `json:"seq"` + // PID is the identifier of the process that generated the event. + PID uint32 `json:"pid"` + // Tid is the thread identifier of the thread that generated the event. + Tid uint32 `json:"tid"` + // Type is the internal representation of the kernel event. This field should be ignored by serializers. + Type ktypes.Ktype `json:"-"` + // CPU designates the processor logical core where the event was originated. + CPU uint8 `json:"cpu"` + // Name is the human friendly name of the kernel event. + Name string `json:"name"` + // Category designates the category to which this event pertains. + Category ktypes.Category `json:"category"` + // Description is the short explanation that describes the purpose of the event. + Description string `json:"description"` + // Host is the machine name that reported the generated event. + Host string `json:"host"` + // Timestamp represents the temporal occurrence of the event. + Timestamp time.Time `json:"timestamp"` + // Kparams stores the collection of kernel event parameters. + Kparams Kparams `json:"params"` + // Metadata represents any tags that are meaningful to this event. + Metadata Metadata `json:"metadata"` + // PS represents process' metadata and its allocated resources such as handles, DLLs, etc. + PS *pstypes.PS `json:"ps,omitempty"` +} + +// String returns event's string representation. +func (kevt *Kevent) String() string { + if kevt.PS != nil { + return fmt.Sprintf(` + Seq: %d + Pid: %d + Tid: %d + Type: %s + CPU: %d + Name: %s + Category: %s + Description: %s + Host: %s, + Timestamp: %s, + Kparams: %s, + Metadata: %s, + %s + `, + kevt.Seq, + kevt.PID, + kevt.Tid, + kevt.Type, + kevt.CPU, + kevt.Name, + kevt.Category, + kevt.Description, + kevt.Host, + kevt.Timestamp, + kevt.Kparams, + kevt.Metadata, + kevt.PS, + ) + } + return fmt.Sprintf(` + Seq: %d + Pid: %d + Tid: %d + Type: %s + CPU: %d + Name: %s + Category: %s + Description: %s + Host: %s, + Timestamp: %s, + Kparams: %s, + Metadata: %s + `, + kevt.Seq, + kevt.PID, + kevt.Tid, + kevt.Type, + kevt.CPU, + kevt.Name, + kevt.Category, + kevt.Description, + kevt.Host, + kevt.Timestamp, + kevt.Kparams, + kevt.Metadata, + ) +} + +// New constructs a new kernel event instance. +func New(seq uint64, pid, tid uint32, cpu uint8, ktype ktypes.Ktype, ts time.Time, kpars Kparams) *Kevent { + kevt := pool.Get().(*Kevent) + metainfo := ktypes.KtypeToKeventInfo(ktype) + *kevt = Kevent{ + Seq: seq, + PID: pid, + Tid: tid, + CPU: cpu, + Type: ktype, + Category: metainfo.Category, + Name: metainfo.Name, + Description: metainfo.Description, + Timestamp: ts, + Kparams: kpars, + Metadata: make(map[string]string), + Host: hostname.Get(), + } + return kevt +} + +// NewFromKcap recovers the kernel event instance from the kcapture byte buffer. +func NewFromKcap(buf []byte) (*Kevent, error) { + kevt := &Kevent{ + Kparams: make(Kparams), + Metadata: make(map[string]string), + } + if err := kevt.UnmarshalRaw(buf, kcapver.KevtSecV1); err != nil { + return nil, err + } + return kevt, nil +} + +// AddMeta appends a key/value pair to event's metadata. +func (kevt *Kevent) AddMeta(k, v string) { + kevt.Metadata[k] = v +} + +// Release returns an event to the pool. +func (kevt *Kevent) Release() { + for _, kpar := range kevt.Kparams { + kpar.Release() + } + *kevt = Kevent{} // clear kevent + pool.Put(kevt) +} diff --git a/pkg/kevent/kevent_test.go b/pkg/kevent/kevent_test.go new file mode 100644 index 000000000..81663ffae --- /dev/null +++ b/pkg/kevent/kevent_test.go @@ -0,0 +1,19 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kevent diff --git a/pkg/kevent/kparam.go b/pkg/kevent/kparam.go new file mode 100644 index 000000000..85b73ef52 --- /dev/null +++ b/pkg/kevent/kparam.go @@ -0,0 +1,554 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kevent + +import ( + "fmt" + kerrors "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/fs" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + knet "github.com/rabbitstack/fibratus/pkg/net" + "github.com/rabbitstack/fibratus/pkg/syscall/security" + "github.com/rabbitstack/fibratus/pkg/util/ip" + "net" + "reflect" + "sort" + "strconv" + "strings" + "sync" + "syscall" + "time" +) + +var kparamPool = sync.Pool{ + New: func() interface{} { + return &Kparam{} + }, +} + +// ParamCaseStyle is the type definition for parameter name case style +type ParamCaseStyle uint8 + +const ( + // Snake is the default parameter's name case style. Multi-word parameters are delimited by underscore symbol (e.g. process_object) + SnakeCase ParamCaseStyle = 1 + // Dot style uses a dot to separate multi-word parameter names (e.g. process.object) + DotCase ParamCaseStyle = 2 + // Camel renders parameter name with pascal case naming style (e.g. ProcessObject) + PascalCase ParamCaseStyle = 3 + // CamelCase represents parameter names with camel case naming style (e.g. processObject) + CamelCase ParamCaseStyle = 4 +) + +// ParamNameCaseStyle designates the case style for kernel parameter names +var ParamNameCaseStyle = SnakeCase + +// ParamKVDelimiter specifies the character that delimits parameter's key from its value +var ParamKVDelimiter = "âžœ " + +// Kparam defines the layout of the kernel event parameter. +type Kparam struct { + // Type is the type of the parameter. For example, `sport` parameter has the `Port` type although its value + // is the uint16 numeric type. + Type kparams.Type `json:"-"` + // Value is the container for parameter values. To access the underlying value use the appropriate `Get` methods. + Value kparams.Value `json:"value"` + // Name represents the name of the parameter (e.g. pid, sport). + Name string `json:"name"` +} + +// Kparams is the type that represents the sequence of kernel event parameters +type Kparams map[string]*Kparam + +// NewKparam creates a new event parameter. Since the parameter type is already categorized, +// we can coerce the value to the appropriate representation (e.g. hex, IP address, user security identifier, etc.) +func NewKparam(name string, typ kparams.Type, value kparams.Value) *Kparam { + var v kparams.Value + switch typ { + case kparams.HexInt8, kparams.HexInt16, kparams.HexInt32, kparams.HexInt64: + v = kparams.NewHex(value) + + case kparams.IPv4: + v = ip.ToIPv4(value.(uint32)) + + case kparams.IPv6: + v = ip.ToIPv6(value.([]byte)) + + case kparams.Port: + v = syscall.Ntohs(value.(uint16)) + + case kparams.SID: + account, domain := security.LookupAccount(value.([]byte), false) + if account != "" || domain != "" { + v = joinSID(account, domain) + } + + case kparams.WbemSID: + account, domain := security.LookupAccount(value.([]byte), true) + if account != "" || domain != "" { + v = joinSID(account, domain) + } + + default: + v = value + } + + kparam := kparamPool.Get().(*Kparam) + *kparam = Kparam{Name: name, Type: typ, Value: v} + + return kparam +} + +func NewKparamFromKcap(name string, typ kparams.Type, value kparams.Value) *Kparam { + return &Kparam{Name: name, Type: typ, Value: value} +} + +// String returns the string representation of the parameter value. +func (k Kparam) String() string { + if k.Value == nil { + return "" + } + switch k.Type { + case kparams.UnicodeString, kparams.AnsiString, kparams.SID, kparams.WbemSID: + return k.Value.(string) + case kparams.HexInt32, kparams.HexInt64, kparams.HexInt16, kparams.HexInt8: + return string(k.Value.(kparams.Hex)) + case kparams.Int8: + return strconv.Itoa(int(k.Value.(int8))) + case kparams.Uint8: + return strconv.Itoa(int(k.Value.(uint8))) + case kparams.Int16: + return strconv.Itoa(int(k.Value.(int16))) + case kparams.Uint16, kparams.Port: + return strconv.Itoa(int(k.Value.(uint16))) + case kparams.Uint32, kparams.PID, kparams.TID: + return strconv.Itoa(int(k.Value.(uint32))) + case kparams.Int32: + return strconv.Itoa(int(k.Value.(int32))) + case kparams.Uint64: + return strconv.FormatUint(k.Value.(uint64), 10) + case kparams.Int64: + return strconv.Itoa(int(k.Value.(int64))) + case kparams.IPv4, kparams.IPv6: + return k.Value.(net.IP).String() + case kparams.Bool: + return strconv.FormatBool(k.Value.(bool)) + case kparams.Float: + return strconv.FormatFloat(float64(k.Value.(float32)), 'f', 6, 32) + case kparams.Double: + return strconv.FormatFloat(k.Value.(float64), 'f', 6, 64) + case kparams.Time: + return k.Value.(time.Time).String() + case kparams.Enum: + switch typ := k.Value.(type) { + case fs.FileShareMode: + return typ.String() + case knet.L4Proto: + return typ.String() + case fs.FileDisposition: + return typ.String() + default: + return fmt.Sprintf("%v", k.Value) + } + default: + return fmt.Sprintf("%v", k.Value) + } +} + +// Release returns the param to the pool. +func (k *Kparam) Release() { + kparamPool.Put(k) +} + +// Append adds a new parameter with specified name, type and value. +func (kpars Kparams) Append(name string, typ kparams.Type, value kparams.Value) Kparams { + kpars[name] = NewKparam(name, typ, value) + return kpars +} + +func (kpars Kparams) AppendFromKcap(name string, typ kparams.Type, value kparams.Value) Kparams { + kpars[name] = NewKparamFromKcap(name, typ, value) + return kpars +} + +// Contains determines whether the specified parameter name exists. +func (kpars Kparams) Contains(name string) bool { + _, err := kpars.findParam(name) + return err == nil +} + +// Remove deletes the specified parameter from the map. +func (kpars Kparams) Remove(name string) { + delete(kpars, name) +} + +// Len returns the number of parameters. +func (kpars Kparams) Len() int { return len(kpars) } + +// Set replaces the value that is indexed at existing parameter name. It will return an error +// if the supplied parameter is not present. +func (kpars Kparams) Set(name string, value kparams.Value, typ kparams.Type) error { + _, err := kpars.findParam(name) + if err != nil { + return fmt.Errorf("setting the value on a missing %q parameter is not allowed", name) + } + kpars[name] = &Kparam{Name: name, Value: value, Type: typ} + return nil +} + +// Get returns the raw value for given parameter name. It is the responsibility of the caller to probe type assertion +// on the value before yielding its underlying type. +func (kpars Kparams) Get(name string) (kparams.Value, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return "", err + } + return kpar.Value, nil +} + +func (kpars Kparams) GetString(name string) (string, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return "", err + } + if _, ok := kpar.Value.(string); !ok { + return "", fmt.Errorf("unable to type cast %q parameter to string value", name) + } + return kpar.Value.(string), nil +} + +func (kpars Kparams) GetPid() (uint32, error) { + return kpars.getPid(kparams.ProcessID) +} + +func (kpars Kparams) GetPpid() (uint32, error) { + return kpars.getPid(kparams.ProcessParentID) +} + +func (kpars Kparams) getPid(name string) (uint32, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return uint32(0), err + } + if kpar.Type != kparams.PID { + return uint32(0), fmt.Errorf("%q parameter is not a PID", name) + } + v, ok := kpar.Value.(uint32) + if !ok { + return uint32(0), fmt.Errorf("unable to type cast %q parameter to uint32 value from pid", name) + } + return v, nil +} + +func (kpars Kparams) GetTid() (uint32, error) { + kpar, err := kpars.findParam(kparams.ThreadID) + if err != nil { + return uint32(0), err + } + if kpar.Type != kparams.TID { + return uint32(0), fmt.Errorf("%q parameter is not a TID", kparams.ThreadID) + } + v, ok := kpar.Value.(uint32) + if !ok { + return uint32(0), fmt.Errorf("unable to type cast %q parameter to uint32 value from tid", kparams.ThreadID) + } + return v, nil +} + +func (kpars Kparams) GetUint8(name string) (uint8, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return uint8(0), err + } + v, ok := kpar.Value.(uint8) + if !ok { + return uint8(0), fmt.Errorf("unable to type cast %q parameter to uint8 value", name) + } + return v, nil +} + +func (kpars Kparams) GetInt8(name string) (int8, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return int8(0), err + } + v, ok := kpar.Value.(int8) + if !ok { + return int8(0), fmt.Errorf("unable to type cast %q parameter to int8 value", name) + } + return v, nil + +} + +func (kpars Kparams) GetUint16(name string) (uint16, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return uint16(0), err + } + v, ok := kpar.Value.(uint16) + if !ok { + return uint16(0), fmt.Errorf("unable to type cast %q parameter to uint16 value", name) + } + return v, nil +} + +func (kpars Kparams) GetInt16(name string) (int16, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return int16(0), err + } + v, ok := kpar.Value.(int16) + if !ok { + return int16(0), fmt.Errorf("unable to type cast %q parameter to int16 value", name) + } + return v, nil +} + +func (kpars Kparams) GetUint32(name string) (uint32, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return uint32(0), err + } + v, ok := kpar.Value.(uint32) + if !ok { + return uint32(0), fmt.Errorf("unable to type cast %q parameter to uint32 value", name) + } + return v, nil +} + +func (kpars Kparams) GetInt32(name string) (int32, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return int32(0), err + } + v, ok := kpar.Value.(int32) + if !ok { + return int32(0), fmt.Errorf("unable to type cast %q parameter to int32 value", name) + } + return v, nil +} + +func (kpars Kparams) GetUint64(name string) (uint64, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return uint64(0), err + } + v, ok := kpar.Value.(uint64) + if !ok { + return uint64(0), fmt.Errorf("unable to type cast %q parameter to uint64 value", name) + } + return v, nil +} + +func (kpars Kparams) GetInt64(name string) (int64, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return int64(0), err + } + v, ok := kpar.Value.(int64) + if !ok { + return int64(0), fmt.Errorf("unable to type cast %q parameter to int64 value", name) + } + return v, nil +} + +func (kpars Kparams) GetFloat(name string) (float32, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return float32(0), err + } + v, ok := kpar.Value.(float32) + if !ok { + return float32(0), fmt.Errorf("unable to type cast %q parameter to float32 value", name) + } + return v, nil +} + +func (kpars Kparams) GetDouble(name string) (float64, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return float64(0), err + } + v, ok := kpar.Value.(float64) + if !ok { + return float64(0), fmt.Errorf("unable to type cast %q parameter to float64 value", name) + } + return v, nil + +} + +func (kpars Kparams) GetHexAsUint32(name string) (uint32, error) { + hex, err := kpars.GetHex(name) + if err != nil { + return uint32(0), err + } + return hex.Uint32(), nil +} + +func (kpars Kparams) GetHexAsUint8(name string) (uint8, error) { + hex, err := kpars.GetHex(name) + if err != nil { + return uint8(0), err + } + return hex.Uint8(), nil +} + +func (kpars Kparams) GetHexAsUint64(name string) (uint64, error) { + hex, err := kpars.GetHex(name) + if err != nil { + return uint64(0), err + } + return hex.Uint64(), nil +} + +func (kpars Kparams) GetHex(name string) (kparams.Hex, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return "", err + } + v, ok := kpar.Value.(kparams.Hex) + if !ok { + return "", fmt.Errorf("unable to type cast %q parameter to Hex value", name) + } + return v, nil +} + +func (kpars Kparams) GetIPv4(name string) (net.IP, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return net.IP{}, err + } + if kpar.Type != kparams.IPv4 { + return net.IP{}, fmt.Errorf("%q parameter is not an IPv4 address", name) + } + v, ok := kpar.Value.(net.IP) + if !ok { + return net.IP{}, fmt.Errorf("unable to type cast %q parameter to net.IP value", name) + } + return v, nil +} + +func (kpars Kparams) GetIPv6(name string) (net.IP, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return net.IP{}, err + } + if kpar.Type != kparams.IPv6 { + return net.IP{}, fmt.Errorf("%q parameter is not an IPv6 address", name) + } + v, ok := kpar.Value.(net.IP) + if !ok { + return net.IP{}, fmt.Errorf("unable to type cast %q parameter to net.IP value", name) + } + return v, nil +} + +func (kpars Kparams) GetIP(name string) (net.IP, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return net.IP{}, err + } + if kpar.Type != kparams.IPv4 && kpar.Type != kparams.IPv6 { + return net.IP{}, fmt.Errorf("%q parameter is not an IP address", name) + } + v, ok := kpar.Value.(net.IP) + if !ok { + return net.IP{}, fmt.Errorf("unable to type cast %q parameter to net.IP value", name) + } + return v, nil +} + +func (kpars Kparams) GetTime(name string) (time.Time, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return time.Unix(0, 0), err + } + v, ok := kpar.Value.(time.Time) + if !ok { + return time.Unix(0, 0), fmt.Errorf("unable to type cast %q parameter to Time value", name) + } + return v, nil +} + +func (kpars Kparams) GetStringSlice(name string) ([]string, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return nil, err + } + v, ok := kpar.Value.([]string) + if !ok { + return nil, fmt.Errorf("unable to type cast %q parameter to string slice", name) + } + return v, nil +} + +func (kpars Kparams) GetSlice(name string) (kparams.Value, error) { + kpar, err := kpars.findParam(name) + if err != nil { + return nil, err + } + if reflect.TypeOf(kpar.Value).Kind() != reflect.Slice { + return nil, fmt.Errorf("%q parameter is not a slice", name) + } + return kpar.Value, nil +} + +func (kpars Kparams) String() string { + var sb strings.Builder + // sort parameters by name + pars := make([]*Kparam, 0, len(kpars)) + for _, kpar := range kpars { + pars = append(pars, kpar) + } + sort.Slice(pars, func(i, j int) bool { return pars[i].Name < pars[j].Name }) + for i, kpar := range pars { + switch ParamNameCaseStyle { + case SnakeCase: + sb.WriteString(kpar.Name + ParamKVDelimiter + kpar.String()) + case DotCase: + sb.WriteString(strings.Replace(kpar.Name, "_", ".", -1) + ParamKVDelimiter + kpar.String()) + case PascalCase: + sb.WriteString(strings.Replace(strings.Title(strings.Replace(kpar.Name, "_", " ", -1)), " ", "", -1) + ParamKVDelimiter + kpar.String()) + case CamelCase: + } + if i != len(pars)-1 { + sb.WriteString(", ") + } + } + return sb.String() +} + +// Find returns the kparam with specified name. If it is not found, nil value is returned. +func (kpars Kparams) Find(name string) *Kparam { + kpar, err := kpars.findParam(name) + if err != nil { + return nil + } + return kpar +} + +// findParam lookups a parameter in the map and returns an error if it doesn't exist. +func (kpars Kparams) findParam(name string) (*Kparam, error) { + if _, ok := kpars[name]; !ok { + return nil, &kerrors.ErrKparamNotFound{Name: name} + } + return kpars[name], nil +} + +func joinSID(account, domain string) string { return fmt.Sprintf("%s\\%s", domain, account) } diff --git a/pkg/kevent/kparam_test.go b/pkg/kevent/kparam_test.go new file mode 100644 index 000000000..aaff8dc85 --- /dev/null +++ b/pkg/kevent/kparam_test.go @@ -0,0 +1,62 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kevent + +import ( + "github.com/rabbitstack/fibratus/pkg/fs" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestKparams(t *testing.T) { + kpars := Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(18446738026482168384)}, + kparams.ThreadID: {Name: kparams.ThreadID, Type: kparams.Uint32, Value: uint32(1484)}, + kparams.FileCreateOptions: {Name: kparams.FileCreateOptions, Type: kparams.Uint32, Value: uint32(1223456)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll"}, + kparams.FileShareMask: {Name: kparams.FileShareMask, Type: kparams.Uint32, Value: uint32(5)}, + } + + assert.True(t, kpars.Contains(kparams.FileObject)) + assert.False(t, kpars.Contains(kparams.FileOffset)) + + filename, err := kpars.GetString(kparams.FileName) + require.NoError(t, err) + assert.Equal(t, "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll", filename) + + filename, err = kpars.GetString(kparams.FileObject) + require.Error(t, err) + + assert.Equal(t, 5, kpars.Len()) + + kpars.Remove(kparams.ThreadID) + + assert.False(t, kpars.Contains(kparams.ThreadID)) + assert.Equal(t, 4, kpars.Len()) + + require.NoError(t, kpars.Set(kparams.FileShareMask, fs.FileShareMode(5), kparams.Enum)) + + filemode, err := kpars.Get(kparams.FileShareMask) + require.NoError(t, err) + mode := filemode.(fs.FileShareMode) + + assert.Equal(t, "r-d", mode.String()) +} diff --git a/pkg/kevent/kparams/canonicalize.go b/pkg/kevent/kparams/canonicalize.go new file mode 100644 index 000000000..d12e7dec2 --- /dev/null +++ b/pkg/kevent/kparams/canonicalize.go @@ -0,0 +1,191 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kparams + +const ( + uniqueProcessKey = "UniqueProcessKey" + processID = "ProcessId" + parentID = "ParentId" + sessionID = "SessionId" + exitStatus = "ExitStatus" + dirTableBase = "DirectoryTableBase" + userSID = "UserSID" + imageFileName = "ImageFileName" + commandLine = "CommandLine" + + tthreadID = "TThreadId" + issuingThreadId = "IssuingThreadId" + ttid = "TTID" + + pid = "PID" + + basePriority = "BasePriority" + ioPriority = "IoPriority" + pagePriority = "PagePriority" + stackBase = "StackBase" + stackLimit = "StackLimit" + userStackBase = "UserStackBase" + userStackLimit = "UserStackLimit" + win32StartAddr = "Win32StartAddr" + + fileObject = "FileObject" + fileName = "FileName" + openPath = "OpenPath" + fileCreateOptions = "CreateOptions" + fileShareAccess = "ShareAccess" + fileOffset = "Offset" + fileIoSize = "IoSize" + fileInfoClass = "InfoClass" + fileKey = "FileKey" + fileExtraInfo = "ExtraInfo" + fileIrpPtr = "IrpPtr" + + keyName = "KeyName" + keyHandle = "KeyHandle" + registryStatus = "Status" + ntStatus = "NtStatus" + + imageBase = "ImageBase" + imageSize = "ImageSize" + imageChecksum = "ImageCheckSum" + imageDefaultBase = "DefaultBase" + + netDaddr = "daddr" + netSaddr = "saddr" + netDport = "dport" + netSport = "sport" + netSize = "size" + + handleID = "Handle" + handleObject = "Object" + handleObjectName = "ObjectName" + handleObjectType = "ObjectType" +) + +// Ignored returns the collection of parameters that are ignored by kernel stream consumer. +func Ignored() map[string]bool { + return map[string]bool{ + "Reserved0": true, + "Reserved1": true, + "Reserved2": true, + "Reserved3": true, + "Reserved4": true, + "ThreadFlags": true, + "ApplicationId": true, + "PackageFullName": true, + "IoFlags": true, + } +} + +// Canonicalize takes an original kernel event property name and normalizes it +// to canonical parameter name. +func Canonicalize(name string) string { + switch name { + case tthreadID, issuingThreadId, ttid: + return ThreadID + case processID, pid: + return ProcessID + case uniqueProcessKey: + return ProcessObject + case parentID: + return ProcessParentID + case sessionID: + return SessionID + case imageFileName: + return ProcessName + case commandLine: + return Comm + case userSID: + return UserSID + case exitStatus: + return ExitStatus + case dirTableBase: + return DTB + case basePriority: + return BasePrio + case ioPriority: + return IOPrio + case pagePriority: + return PagePrio + case stackBase: + return KstackBase + case stackLimit: + return KstackLimit + case userStackBase: + return UstackBase + case userStackLimit: + return UstackLimit + case win32StartAddr: + return ThreadEntrypoint + case fileObject: + return FileObject + case fileName, openPath: + return FileName + case fileCreateOptions: + return FileCreateOptions + case fileShareAccess: + return FileShareMask + case fileOffset: + return FileOffset + case fileIoSize: + return FileIoSize + case fileInfoClass: + return FileInfoClass + case fileKey: + return FileKey + case fileExtraInfo: + return FileExtraInfo + case fileIrpPtr: + return FileIrpPtr + case keyName: + return RegKeyName + case keyHandle: + return RegKeyHandle + case imageBase: + return ImageBase + case imageDefaultBase: + return ImageDefaultBase + case imageSize: + return ImageSize + case imageChecksum: + return ImageCheckSum + case netDaddr: + return NetDIP + case netSaddr: + return NetSIP + case netDport: + return NetDport + case netSport: + return NetSport + case netSize: + return NetSize + case handleID: + return HandleID + case handleObject: + return HandleObject + case handleObjectName: + return HandleObjectName + case handleObjectType: + return HandleObjectTypeID + case registryStatus, ntStatus: + return NTStatus + default: + return "" + } +} diff --git a/pkg/kevent/kparams/canonicalize_test.go b/pkg/kevent/kparams/canonicalize_test.go new file mode 100644 index 000000000..06cd22b2c --- /dev/null +++ b/pkg/kevent/kparams/canonicalize_test.go @@ -0,0 +1,19 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kparams diff --git a/pkg/kevent/kparams/fields.go b/pkg/kevent/kparams/fields.go new file mode 100644 index 000000000..b3e5d8d7e --- /dev/null +++ b/pkg/kevent/kparams/fields.go @@ -0,0 +1,147 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kparams + +const ( + // Status is the parameter that identifies the NTSTATUS value. + NTStatus = "status" + + // ProcessID represents the process identifier. + ProcessID = "pid" + // ProcessObject field represents the address of the process object in the kernel. + ProcessObject = "kproc" + ThreadID = "tid" + ProcessParentID = "ppid" + SessionID = "session_id" + UserSID = "sid" + ProcessName = "name" + Exe = "exe" + Comm = "comm" + DTB = "directory_table_base" + ExitStatus = "exit_status" + StartTime = "start_time" + + BasePrio = "base_prio" + IOPrio = "io_prio" + PagePrio = "page_prio" + KstackBase = "kstack" + KstackLimit = "kstack_limit" + UstackBase = "ustack" + UstackLimit = "ustack_limit" + ThreadEntrypoint = "entrypoint" + + // FileObject determines the field name for the file object pointer. + FileObject = "file_object" + // FileName represents the field that designates the absolute path of the file. + FileName = "file_name" + // FileCreateOptions is the field that represents the values passed in the CreateDispositions parameter to the NtCreateFile function. + FileCreateOptions = "options" + // FileDisposition is the field that represents the values passed in the CreateOptions parameter to the NtCreateFile function. + FileOperation = "operation" + // FileCreated represents the name for the file creation field. + FileCreated = "created" + // FileAccessed represents the name for the file access field. + FileAccessed = "accessed" + // FileModified represents the name for the file modification field. + FileModified = "modified" + // FileShareMask represents the field name for the share access mask. + FileShareMask = "share_mask" + // FileType represents the field name that indicates the file type. + FileType = "type" + // FileAttributes is the field that represents file attribute values. + FileAttributes = "attributes" + // FileIoSize is the filed that represents the number of bytes in file read/write operations. + FileIoSize = "io_size" + // FileOffset represents the file for the file offset in read/write operations. + FileOffset = "offset" + // FileInfoClass represents the file information class. + FileInfoClass = "class" + // FileKey represents the directory key identifier in EnumDirectory events. + FileKey = "file_key" + // FileDirectory represents the filed for the directory name in EnumDirectory events. + FileDirectory = "dir" + // FileIrpPtr represents the I/O request packet id. + FileIrpPtr = "irp" + // FileExtraInfo is the parameter that represents extra information returned by the file system for the operation. For example for a read request, the actual number of bytes that were read. + FileExtraInfo = "extra_info" + + // RegKeyHandle identifies the parameter name for the registry key handle. + RegKeyHandle = "key_handle" + // RegKeyName represents the parameter name for the fully qualified key name. + RegKeyName = "key_name" + // RegValue identifies the parameter name that contains the value + RegValue = "value" + // RegValueType identifies the parameter that represents registry value type e.g (DWORD, BINARY) + RegValueType = "type" + + // ImageBase identifies the parameter name for the base address of the process in which the image is loaded. + ImageBase = "base_address" + // ImageSize represents the parameter name for the size of the image in bytes. + ImageSize = "image_size" + // ImageCheckSum is the parameter name for image checksum. + ImageCheckSum = "checksum" + // ImageDefaultBase is the parameter name that represents image's base address. + ImageDefaultBase = "default_address" + // ImageFilename is the parameter name that denotes file name and extension of the DLL/executable image. + ImageFilename = "file_name" + + // NetSize identifies the parameter name that represents the packet size. + NetSize = "size" + // NetDIP is the parameter name that denotes the destination IP address. + NetDIP = "dip" + // NetSIP is the parameter name that denotes the source IP address. + NetSIP = "sip" + // NetDport identifies the parameter name that represents destination port number. + NetDport = "dport" + // NetSport identifies the parameter name that represents source port number. + NetSport = "sport" + // NetMSS is the parameter name that represents the maximum TCP segment size. + NetMSS = "mss" + // NetRcvWin is the parameter name that represents TCP segment's receive window size. + NetRcvWin = "rcvwin" + // NetSAckopt is the parameter name that represents Selective Acknowledgment option in TCP header. + NetSAckopt = "sack_opt" + // NetTsopt is the parameter name that represents the time stamp option in TCP header. + NetTsopt = "timestamp_opt" + // NetWsopt is the parameter name that represents the window scale option in TCP header. + NetWsopt = "window_scale_opt" + // NetRcvWinScale is the parameter name that represents the TCP receive window scaling factor. + NetRcvWinScale = "recv_winscale" + // NetSendWinScale is the parameter name that represents the TCP send window scaling factor. + NetSendWinScale = "send_winscale" + // NetSeqNum is the parameter name that represents that represents the TCP sequence number. + NetSeqNum = "seqnum" + // NetConnID is the parameter name that represents a unique connection identifier. + NetConnID = "connid" + // NetL4Proto is the parameter name that identifies the Layer 4 protocol name. + NetL4Proto = "l4_proto" + NetDportName = "dport_name" + NetSportName = "sport_name" + + // HandleID identifies the parameter that specifies the handle identifier. + HandleID = "handle_id" + // HandleObject identifies the parameter that represents the kernel object to which handle is associated. + HandleObject = "handle_object" + // HandleObjectName identifies the parameter that represents the kernel object name. + HandleObjectName = "handle_name" + // HandleObjectType identifies the parameter that represents the kernel object type identifier. + HandleObjectTypeID = "type_id" + // HandleObjectTypeName identifies the parameter that represents the kernel object type name. + HandleObjectTypeName = "handle_type" +) diff --git a/pkg/kevent/kparams/types.go b/pkg/kevent/kparams/types.go new file mode 100644 index 000000000..4b9f3e167 --- /dev/null +++ b/pkg/kevent/kparams/types.go @@ -0,0 +1,167 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kparams + +import ( + "strconv" +) + +const ( + // NA defines absent parameter's value + NA = "na" +) + +// Value defines the container for parameter values +type Value interface{} + +// Type defines kernel event parameter type +type Type uint16 + +// Hex is the type alias for hexadecimal values +type Hex string + +// NewHex creates a new Hex type from the given integer value. +func NewHex(v Value) Hex { + switch n := v.(type) { + case uint8: + return Hex(strconv.FormatUint(uint64(n), 16)) + case uint16: + return Hex(strconv.FormatUint(uint64(n), 16)) + case uint32: + return Hex(strconv.FormatUint(uint64(n), 16)) + case uint64: + return Hex(strconv.FormatUint(uint64(n), 16)) + case int32: + return Hex(strconv.FormatInt(int64(n), 16)) + case int64: + return Hex(strconv.FormatInt(int64(n), 16)) + default: + return "" + } +} + +// Uint8 yields an uint8 value from its hex representation. +func (hex Hex) Uint8() uint8 { return uint8(hex.parseUint(8)) } + +// Uint16 yields an uint16 value from its hex representation. +func (hex Hex) Uint16() uint16 { return uint16(hex.parseUint(16)) } + +// Uint32 yields an uint32 value from its hex representation. +func (hex Hex) Uint32() uint32 { return uint32(hex.parseUint(32)) } + +// Uint64 yields an uint64 value from its hex representation. +func (hex Hex) Uint64() uint64 { return hex.parseUint(64) } + +func (hex Hex) parseUint(bitSize int) uint64 { + num, err := strconv.ParseUint(string(hex), 16, bitSize) + if err != nil { + return uint64(0) + } + return num +} + +// String returns a string representation of the hex value. +func (hex Hex) String() string { + return string(hex) +} + +const ( + Null Type = iota + UnicodeString + AnsiString + Int8 + Uint8 + Int16 + Uint16 + Int32 + Uint32 + Int64 + Uint64 + Float + Double + Bool + Binary + GUID + Pointer + SID + PID + TID + WbemSID + HexInt8 + HexInt16 + HexInt32 + HexInt64 + Port + IP + IPv4 + IPv6 + Time // timestamp + Slice // sequence of values + Enum // enumeration + Map + Object + Unknown +) + +func (t Type) String() string { + switch t { + case UnicodeString: + return "unicode" + case AnsiString: + return "ansi" + case Int8: + return "int8" + case Uint8: + return "uint8" + case HexInt8: + return "hex8" + case Int16: + return "int16" + case Uint16: + return "uint16" + case HexInt16: + return "hex16" + case Int32: + return "int32" + case Uint32: + return "uint32" + case Int64: + return "int64" + case Uint64: + return "uint64" + case HexInt32: + return "hex32" + case HexInt64: + return "hex64" + case SID, WbemSID: + return "sid" + case TID: + return "tid" + case PID: + return "pid" + case Port: + return "port" + case IPv6: + return "ipv6" + case IPv4: + return "ipv4" + default: + return "unknown" + } +} diff --git a/pkg/kevent/kparams/types_test.go b/pkg/kevent/kparams/types_test.go new file mode 100644 index 000000000..478991edb --- /dev/null +++ b/pkg/kevent/kparams/types_test.go @@ -0,0 +1,38 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kparams + +import ( + "github.com/stretchr/testify/assert" + + "testing" +) + +func TestNewHex(t *testing.T) { + hex := NewHex(uint32(7264)) + assert.Equal(t, Hex("1c60"), hex) + assert.Equal(t, uint32(7264), hex.Uint32()) + + hex = NewHex(uint32(4294967295)) + assert.Equal(t, Hex("ffffffff"), hex) + + hex = NewHex(uint64(18446744073709551615)) + assert.Equal(t, Hex("ffffffffffffffff"), hex) + assert.Equal(t, uint64(18446744073709551615), hex.Uint64()) +} diff --git a/pkg/kevent/ktypes/category.go b/pkg/kevent/ktypes/category.go new file mode 100644 index 000000000..02a11f9a5 --- /dev/null +++ b/pkg/kevent/ktypes/category.go @@ -0,0 +1,43 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ktypes + +// Category is the type alias for kernel event categories +type Category string + +const ( + // Registry is the category for registry related kernel events + Registry Category = "registry" + // File is the category for file system events + File Category = "file" + // Net is the category for network events + Net Category = "net" + // Process is the category for process events + Process Category = "process" + // Thread is the category for thread events + Thread Category = "thread" + // Image is the category for image events + Image Category = "image" + // Handle is the category for handle events + Handle Category = "handle" + // Other is the category for uncategorized events + Other Category = "other" + // Unknown is the category for events that couldn't match any of the previous categories + Unknown Category = "unknown" +) diff --git a/pkg/kevent/ktypes/ktypes.go b/pkg/kevent/ktypes/ktypes.go new file mode 100644 index 000000000..10141aef8 --- /dev/null +++ b/pkg/kevent/ktypes/ktypes.go @@ -0,0 +1,290 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ktypes + +import ( + "hash/fnv" + "syscall" +) + +// Ktype identifies a kernel event type. It comprises the kernel event GUID + one extra opcode byte to uniquely identify a kernel event +type Ktype [17]byte + +var ( + // CreateProcess identifies process creation kernel events + CreateProcess = Pack(syscall.GUID{Data1: 0x3d6fa8d0, Data2: 0xfe05, Data3: 0x11d0, Data4: [8]byte{0x9d, 0xda, 0x0, 0xc0, 0x4f, 0xd7, 0xba, 0x7c}}, 1) + // TerminateProcess identifies process termination kernel events + TerminateProcess = Pack(syscall.GUID{Data1: 0x3d6fa8d0, Data2: 0xfe05, Data3: 0x11d0, Data4: [8]byte{0x9d, 0xda, 0x0, 0xc0, 0x4f, 0xd7, 0xba, 0x7c}}, 2) + // EnumProcess represents the start data collection process event that enumerates processes that are currently running at the time the kernel session starts + EnumProcess = Pack(syscall.GUID{Data1: 0x3d6fa8d0, Data2: 0xfe05, Data3: 0x11d0, Data4: [8]byte{0x9d, 0xda, 0x0, 0xc0, 0x4f, 0xd7, 0xba, 0x7c}}, 3) + + // CreateThread identifies thread creation kernel events + CreateThread = Pack(syscall.GUID{Data1: 0x3d6fa8d1, Data2: 0xfe05, Data3: 0x11d0, Data4: [8]byte{0x9d, 0xda, 0x0, 0xc0, 0x4f, 0xd7, 0xba, 0x7c}}, 1) + // TerminateThread identifies thread termination kernel events + TerminateThread = Pack(syscall.GUID{Data1: 0x3d6fa8d1, Data2: 0xfe05, Data3: 0x11d0, Data4: [8]byte{0x9d, 0xda, 0x0, 0xc0, 0x4f, 0xd7, 0xba, 0x7c}}, 2) + // EnumThread represents the start data collection thread event that enumerates threads that are currently running at the time the kernel session starts + EnumThread = Pack(syscall.GUID{Data1: 0x3d6fa8d1, Data2: 0xfe05, Data3: 0x11d0, Data4: [8]byte{0x9d, 0xda, 0x0, 0xc0, 0x4f, 0xd7, 0xba, 0x7c}}, 3) + + // FileRundown events are generated by kernel rundown logger to enumerate all open files on the start of the kernel session + FileRundown = Pack(syscall.GUID{Data1: 0x90cbdc39, Data2: 0x4a3e, Data3: 0x11d1, Data4: [8]byte{0x84, 0xf4, 0x0, 0x0, 0xf8, 0x04, 0x64, 0xe3}}, 36) + // CreateFile represents events that create/open a file or I/O device + CreateFile = Pack(syscall.GUID{Data1: 0x90cbdc39, Data2: 0x4a3e, Data3: 0x11d1, Data4: [8]byte{0x84, 0xf4, 0x0, 0x0, 0xf8, 0x04, 0x64, 0xe3}}, 64) + // ReleaseFile represents events that occur when the last file handle is disposed + ReleaseFile = Pack(syscall.GUID{Data1: 0x90cbdc39, Data2: 0x4a3e, Data3: 0x11d1, Data4: [8]byte{0x84, 0xf4, 0x0, 0x0, 0xf8, 0x04, 0x64, 0xe3}}, 65) + // CloseFile represents events that dispose existing kernel file objects + CloseFile = Pack(syscall.GUID{Data1: 0x90cbdc39, Data2: 0x4a3e, Data3: 0x11d1, Data4: [8]byte{0x84, 0xf4, 0x0, 0x0, 0xf8, 0x04, 0x64, 0xe3}}, 66) + // ReadFile represents events that read data from the file or I/O device + ReadFile = Pack(syscall.GUID{Data1: 0x90cbdc39, Data2: 0x4a3e, Data3: 0x11d1, Data4: [8]byte{0x84, 0xf4, 0x0, 0x0, 0xf8, 0x04, 0x64, 0xe3}}, 67) + // WriteFile represents events that write data to the file or I/O device + WriteFile = Pack(syscall.GUID{Data1: 0x90cbdc39, Data2: 0x4a3e, Data3: 0x11d1, Data4: [8]byte{0x84, 0xf4, 0x0, 0x0, 0xf8, 0x04, 0x64, 0xe3}}, 68) + // SetFileInformation represents events that set file information + SetFileInformation = Pack(syscall.GUID{Data1: 0x90cbdc39, Data2: 0x4a3e, Data3: 0x11d1, Data4: [8]byte{0x84, 0xf4, 0x0, 0x0, 0xf8, 0x04, 0x64, 0xe3}}, 69) + // DeleteFile identifies file deletion events + DeleteFile = Pack(syscall.GUID{Data1: 0x90cbdc39, Data2: 0x4a3e, Data3: 0x11d1, Data4: [8]byte{0x84, 0xf4, 0x0, 0x0, 0xf8, 0x04, 0x64, 0xe3}}, 70) + // RenameFile identifies events that are responsible for renaming files + RenameFile = Pack(syscall.GUID{Data1: 0x90cbdc39, Data2: 0x4a3e, Data3: 0x11d1, Data4: [8]byte{0x84, 0xf4, 0x0, 0x0, 0xf8, 0x04, 0x64, 0xe3}}, 71) + // EnumDirectory identifies enumerate directory and directory notification events + EnumDirectory = Pack(syscall.GUID{Data1: 0x90cbdc39, Data2: 0x4a3e, Data3: 0x11d1, Data4: [8]byte{0x84, 0xf4, 0x0, 0x0, 0xf8, 0x04, 0x64, 0xe3}}, 72) + // FileOpEnd signals the finalization of the file operation + FileOpEnd = Pack(syscall.GUID{Data1: 0x90cbdc39, Data2: 0x4a3e, Data3: 0x11d1, Data4: [8]byte{0x84, 0xf4, 0x0, 0x0, 0xf8, 0x04, 0x64, 0xe3}}, 76) + + // RegCreateKey represents registry key creation kernel events + RegCreateKey = Pack(syscall.GUID{Data1: 0xae53722e, Data2: 0xc863, Data3: 0x11d2, Data4: [8]byte{0x86, 0x59, 0x0, 0xc0, 0x4f, 0xa3, 0x21, 0xa1}}, 10) + // RegOpenKey represents registry open key kernel events + RegOpenKey = Pack(syscall.GUID{Data1: 0xae53722e, Data2: 0xc863, Data3: 0x11d2, Data4: [8]byte{0x86, 0x59, 0x0, 0xc0, 0x4f, 0xa3, 0x21, 0xa1}}, 11) + // RegDeleteKey represents registry key deletion kernel events + RegDeleteKey = Pack(syscall.GUID{Data1: 0xae53722e, Data2: 0xc863, Data3: 0x11d2, Data4: [8]byte{0x86, 0x59, 0x0, 0xc0, 0x4f, 0xa3, 0x21, 0xa1}}, 12) + // RegQueryValue represents registry query key kernel events + RegQueryKey = Pack(syscall.GUID{Data1: 0xae53722e, Data2: 0xc863, Data3: 0x11d2, Data4: [8]byte{0x86, 0x59, 0x0, 0xc0, 0x4f, 0xa3, 0x21, 0xa1}}, 13) + // RegSetValue represents registry set value kernel events + RegSetValue = Pack(syscall.GUID{Data1: 0xae53722e, Data2: 0xc863, Data3: 0x11d2, Data4: [8]byte{0x86, 0x59, 0x0, 0xc0, 0x4f, 0xa3, 0x21, 0xa1}}, 14) + // RegDeleteValue are kernel events for registry value removals + RegDeleteValue = Pack(syscall.GUID{Data1: 0xae53722e, Data2: 0xc863, Data3: 0x11d2, Data4: [8]byte{0x86, 0x59, 0x0, 0xc0, 0x4f, 0xa3, 0x21, 0xa1}}, 15) + // RegQueryValue are kernel events for registry value queries + RegQueryValue = Pack(syscall.GUID{Data1: 0xae53722e, Data2: 0xc863, Data3: 0x11d2, Data4: [8]byte{0x86, 0x59, 0x0, 0xc0, 0x4f, 0xa3, 0x21, 0xa1}}, 16) + // RegCreateKCB represents kernel events for KCB (Key Control Block) creation requests + RegCreateKCB = Pack(syscall.GUID{Data1: 0xae53722e, Data2: 0xc863, Data3: 0x11d2, Data4: [8]byte{0x86, 0x59, 0x0, 0xc0, 0x4f, 0xa3, 0x21, 0xa1}}, 22) + // RegDeleteKCB represents kernel events for KCB(Key Control Block) closures + RegDeleteKCB = Pack(syscall.GUID{Data1: 0xae53722e, Data2: 0xc863, Data3: 0x11d2, Data4: [8]byte{0x86, 0x59, 0x0, 0xc0, 0x4f, 0xa3, 0x21, 0xa1}}, 23) + // RegKCBRundown enumerates the registry keys open at the start of the kernel session. + RegKCBRundown = Pack(syscall.GUID{Data1: 0xae53722e, Data2: 0xc863, Data3: 0x11d2, Data4: [8]byte{0x86, 0x59, 0x0, 0xc0, 0x4f, 0xa3, 0x21, 0xa1}}, 25) + // RegOpenKeyV1 represents registry open key kernel event. It looks like this event type defines identical kernel event type as RegOpenKey + RegOpenKeyV1 = Pack(syscall.GUID{Data1: 0xae53722e, Data2: 0xc863, Data3: 0x11d2, Data4: [8]byte{0x86, 0x59, 0x0, 0xc0, 0x4f, 0xa3, 0x21, 0xa1}}, 27) + + // UnloadImage represents unload image kernel events + UnloadImage = Pack(syscall.GUID{Data1: 0x2cb15d1d, Data2: 0x5fc1, Data3: 0x11d2, Data4: [8]byte{0xab, 0xe1, 0x0, 0xa0, 0xc9, 0x11, 0xf5, 0x18}}, 2) + // EnumImage represents kernel events that is triggered to enumerate all loaded images + EnumImage = Pack(syscall.GUID{Data1: 0x2cb15d1d, Data2: 0x5fc1, Data3: 0x11d2, Data4: [8]byte{0xab, 0xe1, 0x0, 0xa0, 0xc9, 0x11, 0xf5, 0x18}}, 3) + // LoadImage represents load image kernel events that are triggered when a DLL or executable file is loaded + LoadImage = Pack(syscall.GUID{Data1: 0x2cb15d1d, Data2: 0x5fc1, Data3: 0x11d2, Data4: [8]byte{0xab, 0xe1, 0x0, 0xa0, 0xc9, 0x11, 0xf5, 0x18}}, 10) + + // AcceptTCPv4 represents the TCPv4 kernel events for accepting connection requests from the socket queue. + AcceptTCPv4 = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xc8e0, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 15) + // AcceptTCPv6 represents the TCPv6 kernel events for accepting connection requests from the socket queue. + AcceptTCPv6 = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xc8e0, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 31) + // SendTCPv4 represents the TCPv4 kernel events for sending data to the connected socket. + SendTCPv4 = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xc8e0, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 10) + // SendTCPv6 represents the TCPv6 kernel events for sending data to the connected socket. + SendTCPv6 = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xc8e0, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 26) + // SendUDPv4 represents the UDPv4 kernel events for sending datagrams to connectionless sockets. + SendUDPv4 = Pack(syscall.GUID{Data1: 0xbf3a50c5, Data2: 0xa9c9, Data3: 0x4988, Data4: [8]byte{0xa0, 0x05, 0x2d, 0xc0, 0xb7, 0xc8, 0x0f, 0x80}}, 10) + // SendUDPv6 represents the UDPv6 kernel events for sending datagrams to connectionless sockets. + SendUDPv6 = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xc8e0, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 26) + + RecvTCPv4 = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xc8e0, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 11) + RecvTCPv6 = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xc8e0, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 27) + RecvUDPv4 = Pack(syscall.GUID{Data1: 0xbf3a50c5, Data2: 0xa9c9, Data3: 0x4988, Data4: [8]byte{0xa0, 0x05, 0x2d, 0xc0, 0xb7, 0xc8, 0x0f, 0x80}}, 10) + RecvUDPv6 = Pack(syscall.GUID{Data1: 0xbf3a50c5, Data2: 0xa9c9, Data3: 0x4988, Data4: [8]byte{0xa0, 0x05, 0x2d, 0xc0, 0xb7, 0xc8, 0x0f, 0x80}}, 27) + + ConnectTCPv4 = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xc8e0, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 12) + ConnectTCPv6 = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xc8e0, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 28) + + DisconnectTCPv4 = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xc8e0, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 13) + DisconnectTCPv6 = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xc8e0, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 29) + Disconnect = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xc8e0, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 42) + + ReconnectTCPv4 = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xc8e0, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 16) + ReconnectTCPv6 = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xc8e0, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 32) + + RetransmitTCPv4 = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xc8e0, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 14) + RetransmitTCPv6 = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xc8e0, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 30) + + // Accept represents the global kernel event type for both TCP v4/v6 connections. Note this is an artificial kernel event that is never published by the provider. + Accept = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xc8e0, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 46) + // Send represents the global kernel event for all variants of sending data to sockets. Note this is an artificial kernel event that is never published by the provider. + Send = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xa9c9, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 72) + Recv = Pack(syscall.GUID{Data1: 0xbf3a50c5, Data2: 0xc8e0, Data3: 0x4988, Data4: [8]byte{0xa0, 0x05, 0x2d, 0xc0, 0xb7, 0xc8, 0x0f, 0x80}}, 75) + Connect = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xc8e0, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 40) + Reconnect = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xc8e0, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 47) + Retransmit = Pack(syscall.GUID{Data1: 0x9a280ac0, Data2: 0xc8e0, Data3: 0x11d1, Data4: [8]byte{0x84, 0xe2, 0x0, 0xc0, 0x4f, 0xb9, 0x98, 0xa2}}, 44) + + // CreateHandle represents handle creation kernel event + CreateHandle = Pack(syscall.GUID{Data1: 0x89497f50, Data2: 0xeffe, Data3: 0x4440, Data4: [8]byte{0x8c, 0xf2, 0xce, 0x6b, 0x1c, 0xdc, 0xac, 0xa7}}, 32) + // CloseHandle represents handle closure kernel event + CloseHandle = Pack(syscall.GUID{Data1: 0x89497f50, Data2: 0xeffe, Data3: 0x4440, Data4: [8]byte{0x8c, 0xf2, 0xce, 0x6b, 0x1c, 0xdc, 0xac, 0xa7}}, 33) + + // UnknownKtype designates unknown kernel event type + UnknownKtype = Pack(syscall.GUID{}, 0) +) + +// String returns the string representation of the kernel event type. If event is unknown a GUID representation that includes the GUID of the event's provider + the opcode type is presented. +func (k Ktype) String() string { + switch k { + case CreateProcess: + return "CreateProcess" + case TerminateProcess: + return "TerminateProcess" + case CreateThread: + return "CreateThread" + case TerminateThread: + return "TerminateThread" + case CreateFile: + return "CreateFile" + case CloseFile: + return "CloseFile" + case ReleaseFile: + return "ReleaseFile" + case ReadFile: + return "ReadFile" + case WriteFile: + return "WriteFile" + case SetFileInformation: + return "SetFileInformation" + case DeleteFile: + return "DeleteFile" + case RenameFile: + return "RenameFile" + case EnumDirectory: + return "EnumDirectory" + case FileOpEnd: + return "FileOpEnd" + case FileRundown: + return "FileRundown" + case CreateHandle: + return "CreateHandle" + case CloseHandle: + return "CloseHandle" + case RegKCBRundown: + return "RegKCBRundown" + case RegOpenKey, RegOpenKeyV1: + return "RegOpenKey" + case RegCreateKey: + return "RegCreateKey" + case RegDeleteKey: + return "RegDeleteKey" + case RegDeleteValue: + return "RegDeleteValue" + case RegQueryKey: + return "RegQueryKey" + case RegQueryValue: + return "RegQueryValue" + case RegCreateKCB: + return "RegCreateKCB" + case LoadImage: + return "LoadImage" + case UnloadImage: + return "UnloadImage" + case Accept: + return "Accept" + case Send: + return "Send" + case Recv: + return "Recv" + case Connect: + return "Connect" + case Reconnect: + return "Reconnect" + case Disconnect: + return "Disconnect" + case Retransmit: + return "Retransmit" + default: + return string(k[:]) + } +} + +// Hash calculates the hash number of the ktype. +func (k Ktype) Hash() uint32 { + h := fnv.New32() + _, err := h.Write([]byte(k.String())) + if err != nil { + return 0 + } + return h.Sum32() +} + +// Exists determines whether particular ktype exists. +func (k Ktype) Exists() bool { + switch k { + case EnumProcess, EnumThread, FileRundown, FileOpEnd, ReleaseFile, EnumImage, RegCreateKCB, RegKCBRundown: + return true + default: + // for composite kernel events we match against a single global ktype. This way + // we use a unique kernel type to group several kernel events. For example, `Send` + // designates all network Send regardless of transport protocol or IP version + if k == AcceptTCPv4 || k == AcceptTCPv6 { + return true + } + if k == ConnectTCPv4 || k == ConnectTCPv6 { + return true + } + if k == ReconnectTCPv4 || k == ReconnectTCPv6 { + return true + } + if k == RetransmitTCPv4 || k == RetransmitTCPv6 { + return true + } + if k == DisconnectTCPv4 || k == DisconnectTCPv6 { + return true + } + if k == SendTCPv4 || k == SendTCPv6 || k == SendUDPv4 || k == SendUDPv6 { + return true + } + if k == RecvTCPv4 || k == RecvTCPv6 || k == RecvUDPv4 || k == RecvUDPv6 { + return true + } + _, ok := kevents[k] + return ok + } +} + +// Dropped determines whether certain events responsible for building the internal state are kept or dropped before hitting +// the output channel. +func (k Ktype) Dropped(capture bool) bool { + switch k { + case EnumProcess, EnumThread, FileRundown, FileOpEnd, ReleaseFile, EnumImage, RegCreateKCB, RegKCBRundown: + if capture { + return false + } + return true + default: + return false + } +} + +// Pack transforms event provider GUID and the op code into `Ktype` type. The type provides a convenient way +// to compare different kernel event types. +func Pack(g syscall.GUID, opcode uint8) Ktype { + return Ktype([17]byte{ + byte(g.Data1 >> 24), byte(g.Data1 >> 16), byte(g.Data1 >> 8), byte(g.Data1), + byte(g.Data2 >> 8), byte(g.Data2), byte(g.Data3 >> 8), byte(g.Data3), + g.Data4[0], g.Data4[1], g.Data4[2], g.Data4[3], g.Data4[4], g.Data4[5], g.Data4[6], g.Data4[7], + byte(opcode), + }) +} diff --git a/pkg/kevent/ktypes/ktypes_test.go b/pkg/kevent/ktypes/ktypes_test.go new file mode 100644 index 000000000..6a92e7206 --- /dev/null +++ b/pkg/kevent/ktypes/ktypes_test.go @@ -0,0 +1,69 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ktypes + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "syscall" + "testing" +) + +func TestPack(t *testing.T) { + assert.Equal(t, byte(0x3d), CreateProcess[0]) + assert.Equal(t, byte(0x6f), CreateProcess[1]) + assert.Equal(t, byte(0xa8), CreateProcess[2]) + assert.Equal(t, byte(0xd0), CreateProcess[3]) + + assert.Equal(t, byte(0xfe), CreateProcess[4]) + assert.Equal(t, byte(0x05), CreateProcess[5]) + + assert.Equal(t, byte(0x11), CreateProcess[6]) + assert.Equal(t, byte(0xd0), CreateProcess[7]) + + assert.Equal(t, byte(0x9d), CreateProcess[8]) + assert.Equal(t, byte(0xda), CreateProcess[9]) + assert.Equal(t, byte(0x0), CreateProcess[10]) + assert.Equal(t, byte(0xc0), CreateProcess[11]) + assert.Equal(t, byte(0x4f), CreateProcess[12]) + assert.Equal(t, byte(0xd7), CreateProcess[13]) + assert.Equal(t, byte(0xba), CreateProcess[14]) + assert.Equal(t, byte(0x7c), CreateProcess[15]) + + assert.Equal(t, byte(0x1), CreateProcess[16]) + + kt := Pack(syscall.GUID{Data1: 0x3d6fa8d0, Data2: 0xfe05, Data3: 0x11d0, Data4: [8]byte{0x9d, 0xda, 0x0, 0xc0, 0x4f, 0xd7, 0xba, 0x7c}}, 1) + assert.Equal(t, CreateProcess, kt) + + kt = Pack(syscall.GUID{Data1: 0x3d6fa8d0, Data2: 0xfe05, Data3: 0x11d0, Data4: [8]byte{0x9d, 0xda, 0x0, 0xc0, 0x4f, 0xd7, 0xba, 0x7c}}, 2) + assert.NotEqual(t, CreateProcess, kt) + assert.Equal(t, TerminateProcess, kt) + + switch kt { + case TerminateProcess: + default: + t.Fatal("expected TerminateProcess kernel event") + } +} + +func TestKtypeExists(t *testing.T) { + require.True(t, Accept.Exists()) + require.True(t, AcceptTCPv4.Exists()) + require.True(t, AcceptTCPv6.Exists()) +} diff --git a/pkg/kevent/ktypes/metainfo.go b/pkg/kevent/ktypes/metainfo.go new file mode 100644 index 000000000..f5a88d41e --- /dev/null +++ b/pkg/kevent/ktypes/metainfo.go @@ -0,0 +1,149 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ktypes + +import "sort" + +// KeventInfo describes the kernel event meta info such as human readable name, category +// and event's description. +type KeventInfo struct { + // Name is the human-readable representation of the kernel event (e.g. CreateProcess, DeleteFile). + Name string + // Category designates the category to which kernel event pertains. (e.g. process, net) + Category Category + // Description is the short explanation that describes the purpose of the kernel event. + Description string +} + +var kevents = map[Ktype]KeventInfo{ + CreateProcess: {"CreateProcess", Process, "Creates a new process and its primary thread"}, + TerminateProcess: {"TerminateProcess", Process, "Terminates the process and all of its threads"}, + CreateThread: {"CreateThread", Thread, "Creates a thread to execute within the virtual address space of the calling process"}, + TerminateThread: {"TerminateThread", Thread, "Terminates a thread within the process"}, + ReadFile: {"ReadFile", File, "Reads data from the file or I/O device"}, + WriteFile: {"WriteFile", File, "Writes data to the file or I/O device"}, + CreateFile: {"CreateFile", File, "Creates or opens a file or I/O device"}, + CloseFile: {"CloseFile", File, "Closes the file handle"}, + DeleteFile: {"DeleteFile", File, "Removes the file from the file system"}, + RenameFile: {"RenameFile", File, "Changes the file name"}, + SetFileInformation: {"SetFileInformation", File, "Sets the file meta information"}, + EnumDirectory: {"EnumDirectory", File, "Enumerates a directory or dispatches a directory change notification to registered listeners"}, + RegCreateKey: {"RegCreateKey", Registry, "Creates a registry key or opens it if the key already exists"}, + RegOpenKey: {"RegOpenKey", Registry, "Opens the registry key"}, + RegSetValue: {"RegSetValue", Registry, "Sets the data for the value of a registry key"}, + RegQueryValue: {"RegQueryValue", Registry, "Reads the data for the value of a registry key"}, + RegQueryKey: {"RegQueryKey", Registry, "Enumerates subkeys of the parent key"}, + RegDeleteKey: {"RegDeleteKey", Registry, "Removes the registry key"}, + RegDeleteValue: {"RegDeleteValue", Registry, "Removes the registry value"}, + Accept: {"Accept", Net, "Accepts the connection request from the socket queue"}, + Send: {"Send", Net, "Sends data over the wire"}, + Recv: {"Recv", Net, "Receives data from the socket"}, + Connect: {"Connect", Net, "Connects establishes a connection to the socket"}, + Disconnect: {"Disconnect", Net, "Terminates data reception on the socket"}, + Reconnect: {"Reconnect", Net, "Reconnects to the socket"}, + Retransmit: {"Retransmit", Net, "Retransmits unacknowledged TCP segments"}, + LoadImage: {"LoadImage", Image, "Loads the module into the address space of the calling process"}, + UnloadImage: {"UnloadImage", Image, "Unloads the module from the address space of the calling process"}, + CreateHandle: {"CreateHandle", Handle, "Creates a new handle"}, + CloseHandle: {"CloseHandle", Handle, "Closes the handle"}, +} + +var ktypes = map[string]Ktype{ + "CreateProcess": CreateProcess, + "TerminateProcess": TerminateProcess, + "CreateThread": CreateThread, + "TerminateThread": TerminateThread, + "LoadImage": LoadImage, + "UnloadImage": UnloadImage, + "CreateFile": CreateFile, + "CloseFile": CloseFile, + "ReadFile": ReadFile, + "WriteFile": WriteFile, + "SetFileInformation": SetFileInformation, + "DeleteFile": DeleteFile, + "RenameFile": RenameFile, + "EnumDirectory": EnumDirectory, + "RegCreateKey": RegCreateKey, + "RegOpenKey": RegOpenKey, + "RegSetValue": RegSetValue, + "RegQueryValue": RegQueryValue, + "RegQueryKey": RegQueryKey, + "RegDeleteKey": RegDeleteKey, + "RegDeleteValue": RegDeleteValue, + "Accept": Accept, + "Send": Send, + "Recv": Recv, + "Connect": Connect, + "Reconnect": Reconnect, + "Disconnect": Disconnect, + "Retransmit": Retransmit, + "CreateHandle": CreateHandle, + "CloseHandle": CloseHandle, +} + +// KtypeToKeventInfo maps the kernel event type to a structure that stores detailed information about the event. +func KtypeToKeventInfo(ktype Ktype) KeventInfo { + if ktype == RegOpenKeyV1 { + return kevents[RegOpenKey] + } + if ktype == AcceptTCPv4 || ktype == AcceptTCPv6 { + return kevents[Accept] + } + if ktype == ConnectTCPv4 || ktype == ConnectTCPv6 { + return kevents[Connect] + } + if ktype == ReconnectTCPv4 || ktype == ReconnectTCPv6 { + return kevents[Reconnect] + } + if ktype == RetransmitTCPv4 || ktype == RetransmitTCPv6 { + return kevents[Retransmit] + } + if ktype == DisconnectTCPv4 || ktype == DisconnectTCPv6 { + return kevents[Disconnect] + } + if ktype == SendTCPv4 || ktype == SendTCPv6 || ktype == SendUDPv4 || ktype == SendUDPv6 { + return kevents[Send] + } + if ktype == RecvTCPv4 || ktype == RecvTCPv6 || ktype == RecvUDPv4 || ktype == RecvUDPv6 { + return kevents[Recv] + } + + if kinfo, ok := kevents[ktype]; ok { + return kinfo + } + return KeventInfo{Name: "N/A", Category: Unknown} +} + +// KeventNameToKtype converts a human-readable kernel event name to its internal kernel type representation. +func KeventNameToKtype(name string) Ktype { + if ktype, ok := ktypes[name]; ok { + return ktype + } + return UnknownKtype +} + +// GetKtypesMeta returns kernel event types metadata. +func GetKtypesMeta() []KeventInfo { + ktypes := make([]KeventInfo, 0, len(kevents)) + for _, ktyp := range kevents { + ktypes = append(ktypes, ktyp) + } + sort.Slice(ktypes, func(i, j int) bool { return ktypes[i].Category < ktypes[j].Category }) + return ktypes +} diff --git a/pkg/kevent/ktypes/metainfo_test.go b/pkg/kevent/ktypes/metainfo_test.go new file mode 100644 index 000000000..7750174fa --- /dev/null +++ b/pkg/kevent/ktypes/metainfo_test.go @@ -0,0 +1,46 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ktypes + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestKeventNameToKtype(t *testing.T) { + ktype := KeventNameToKtype("CreateProcess") + + assert.Equal(t, CreateProcess, ktype) + + ktype = KeventNameToKtype("CreateRemoteThread") + assert.Equal(t, UnknownKtype, ktype) +} + +func TestKtypeToKeventInfo(t *testing.T) { + kinfo := KtypeToKeventInfo(CreateProcess) + + assert.Equal(t, "CreateProcess", kinfo.Name) + assert.Equal(t, Process, kinfo.Category) + assert.Equal(t, "Creates a new process and its primary thread", kinfo.Description) + + kinfo = KtypeToKeventInfo(UnknownKtype) + assert.Equal(t, "N/A", kinfo.Name) + assert.Equal(t, Unknown, kinfo.Category) + assert.Empty(t, kinfo.Description) +} diff --git a/pkg/kevent/marshaller.go b/pkg/kevent/marshaller.go new file mode 100644 index 000000000..722f3e1ba --- /dev/null +++ b/pkg/kevent/marshaller.go @@ -0,0 +1,1213 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kevent + +import ( + "expvar" + "fmt" + "github.com/rabbitstack/fibratus/pkg/fs" + "github.com/rabbitstack/fibratus/pkg/kcap/section" + kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + knet "github.com/rabbitstack/fibratus/pkg/net" + ptypes "github.com/rabbitstack/fibratus/pkg/ps/types" + "github.com/rabbitstack/fibratus/pkg/util/bytes" + "github.com/rabbitstack/fibratus/pkg/util/ip" + "math" + "net" + "strconv" + "time" + "unicode/utf8" + "unsafe" +) + +var ( + // SerializeHandles indicates if handles are serialized as part of the process' state + SerializeHandles bool + // SerializeThreads indicates if threads are serialized as part of the process' state + SerializeThreads bool + // SerializeImages indicates if images are serialized as part of the process' state + SerializeImages bool + // SerializePE indicates if PE metadata are serialized as part of the process' state + SerializePE bool + // SerializeEnvs indicates if the environment variables are serialized as part of the process's state + SerializeEnvs bool +) + +// unmarshalTimestampErrors counts timestamp unmarshal errors +var unmarshalTimestampErrors = expvar.NewInt("kevent.timestamp.unmarshal.errors") + +// Marshal produces a byte stream of the kernel event suitable for writing to disk. +func (kevt *Kevent) MarshalRaw() []byte { + b := make([]byte, 0) + + // write seq, pid, tid fields + b = append(b, bytes.WriteUint64(kevt.Seq)...) + b = append(b, bytes.WriteUint32(kevt.PID)...) + b = append(b, bytes.WriteUint32(kevt.Tid)...) + + // write ktype and CPU + b = append(b, kevt.Type[:]...) + b = append(b, kevt.CPU) + + // for the string fields we have to write the length prior to + // the string buffer itself so we can decode the string correctly + // + // write event name + b = append(b, bytes.WriteUint16(uint16(len(kevt.Name)))...) + b = append(b, kevt.Name...) + // write category + b = append(b, bytes.WriteUint16(uint16(len(kevt.Category)))...) + b = append(b, kevt.Category...) + // write description + b = append(b, bytes.WriteUint16(uint16(len(kevt.Description)))...) + b = append(b, kevt.Description...) + // write host name + b = append(b, bytes.WriteUint16(uint16(len(kevt.Host)))...) + b = append(b, kevt.Host...) + + // write event's timestamp + timestamp := make([]byte, 0) + timestamp = kevt.Timestamp.AppendFormat(timestamp, time.RFC3339Nano) + b = append(b, bytes.WriteUint16(uint16(len(timestamp)))...) + b = append(b, timestamp...) + + // write the number of kernel parameters followed by each parameter + b = append(b, bytes.WriteUint16(uint16(len(kevt.Kparams)))...) + for _, kpar := range kevt.Kparams { + // append the type, parameter size and name + b = append(b, bytes.WriteUint16(uint16(kpar.Type))...) + b = append(b, bytes.WriteUint16(uint16(len(kpar.Name)))...) + b = append(b, kpar.Name...) + switch kpar.Type { + case kparams.AnsiString, kparams.UnicodeString, kparams.SID, kparams.WbemSID: + b = append(b, bytes.WriteUint16(uint16(len(kpar.Value.(string))))...) + b = append(b, kpar.Value.(string)...) + case kparams.Uint8: + b = append(b, kpar.Value.(uint8)) + case kparams.Int8: + b = append(b, byte(kpar.Value.(int8))) + case kparams.HexInt8: + b = append(b, kpar.Value.(kparams.Hex).Uint8()) + case kparams.HexInt16: + b = append(b, bytes.WriteUint16(kpar.Value.(kparams.Hex).Uint16())...) + case kparams.HexInt32: + b = append(b, bytes.WriteUint32(kpar.Value.(kparams.Hex).Uint32())...) + case kparams.HexInt64: + b = append(b, bytes.WriteUint64(kpar.Value.(kparams.Hex).Uint64())...) + case kparams.Uint16, kparams.Port: + b = append(b, bytes.WriteUint16(kpar.Value.(uint16))...) + case kparams.Int16: + b = append(b, bytes.WriteUint16(uint16(kpar.Value.(int16)))...) + case kparams.Uint32: + b = append(b, bytes.WriteUint32(kpar.Value.(uint32))...) + case kparams.Int32: + b = append(b, bytes.WriteUint32(uint32(kpar.Value.(int32)))...) + case kparams.Uint64: + b = append(b, bytes.WriteUint64(kpar.Value.(uint64))...) + case kparams.Int64: + b = append(b, bytes.WriteUint64(uint64(kpar.Value.(int64)))...) + case kparams.Double: + b = append(b, bytes.WriteUint32(math.Float32bits(kpar.Value.(float32)))...) + case kparams.Float: + b = append(b, bytes.WriteUint64(math.Float64bits(kpar.Value.(float64)))...) + case kparams.IPv4: + b = append(b, kpar.Value.(net.IP).To4()...) + case kparams.IPv6: + b = append(b, kpar.Value.(net.IP).To16()...) + case kparams.PID, kparams.TID: + b = append(b, bytes.WriteUint32(kpar.Value.(uint32))...) + case kparams.Bool: + v := kpar.Value.(bool) + if v { + b = append(b, 1) + } else { + b = append(b, 0) + } + case kparams.Time: + v := kpar.Value.(time.Time) + ts := make([]byte, 0) + ts = v.AppendFormat(ts, time.RFC3339Nano) + b = append(b, bytes.WriteUint16(uint16(len(ts)))...) + b = append(b, ts...) + case kparams.Enum: + switch e := kpar.Value.(type) { + case fs.FileDisposition: + b = append(b, uint8(e)) + case fs.FileShareMode: + b = append(b, uint8(e)) + case knet.L4Proto: + b = append(b, uint8(e)) + } + } + } + // write metadata key/value pairs + b = append(b, bytes.WriteUint16(uint16(len(kevt.Metadata)))...) + for key, value := range kevt.Metadata { + b = append(b, bytes.WriteUint16(uint16(len(key)))...) + b = append(b, key...) + b = append(b, bytes.WriteUint16(uint16(len(value)))...) + b = append(b, value...) + } + + // write process state + if kevt.PS != nil && (kevt.Type == ktypes.CreateProcess || kevt.Type == ktypes.EnumProcess) { + buf := kevt.PS.Marshal() + sec := section.New(section.Process, kcapver.ProcessSecV1, 0, uint32(len(buf))) + b = append(b, sec[:]...) + b = append(b, buf...) + } else { + sec := section.New(section.Process, kcapver.ProcessSecV1, 0, 0) + b = append(b, sec[:]...) + } + + return b +} + +// Unmarshal recovers the state of the kernel event from the byte stream. +func (kevt *Kevent) UnmarshalRaw(b []byte, ver kcapver.Version) error { + if len(b) < 34 { + return fmt.Errorf("expected at least 34 bytes but got %d bytes", len(b)) + } + + // read seq, pid, tid + kevt.Seq = bytes.ReadUint64(b[0:]) + kevt.PID = bytes.ReadUint32(b[8:]) + kevt.Tid = bytes.ReadUint32(b[12:]) + + // read ktype and CPU + var ktype ktypes.Ktype + copy(ktype[:], b[16:33]) + kevt.Type = ktype + kevt.CPU = uint8(b[33:34][0]) + + // read event name + l := bytes.ReadUint16(b[34:]) + buf := b[36:] + offset := l + kevt.Name = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + + // read category + l = bytes.ReadUint16(b[36+offset:]) + buf = b[38+offset:] + offset += l + kevt.Category = ktypes.Category(string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l])) + + // read description + l = bytes.ReadUint16(b[38+offset:]) + buf = b[40+offset:] + offset += l + kevt.Description = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + + // read host name + l = bytes.ReadUint16(b[40+offset:]) + buf = b[42+offset:] + offset += l + kevt.Host = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + + // read timestamp + l = bytes.ReadUint16(b[42+offset:]) + buf = b[44+offset:] + offset += l + if len(buf) > 0 { + var err error + kevt.Timestamp, err = time.Parse(time.RFC3339Nano, string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l])) + if err != nil { + unmarshalTimestampErrors.Add(1) + } + } + + // read parameters + nbKparams := bytes.ReadUint16(b[44+offset:]) + var poffset uint16 + + for i := 0; i < int(nbKparams); i++ { + // read kparam type + typ := bytes.ReadUint16(b[46+offset+poffset:]) + // read kparam name + kparamNameLength := bytes.ReadUint16(b[48+offset+poffset:]) + buf = b[50+offset+poffset:] + kparamName := string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:kparamNameLength:kparamNameLength]) + + var kval kparams.Value + switch kparams.Type(typ) { + case kparams.AnsiString, kparams.UnicodeString, kparams.SID, kparams.WbemSID: + // read string parameter + l := bytes.ReadUint16(b[50+offset+kparamNameLength+poffset:]) + buf = b[52+offset+kparamNameLength+poffset:] + if len(buf) > 0 { + kval = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + } + // increment parameter offset by string by type length + name length bytes + length of + // the string parameter + string parameter size + poffset += kparamNameLength + 6 + l + case kparams.Uint64: + kval = bytes.ReadUint64(b[50+offset+kparamNameLength+poffset:]) + // increment parameter offset by type length + name length sizes + size of uint64 + poffset += kparamNameLength + 4 + 8 + case kparams.Int64: + kval = int64(bytes.ReadUint64(b[50+offset+kparamNameLength+poffset:])) + // increment parameter offset by type length + name length sizes + size of int64 + poffset += kparamNameLength + 4 + 8 + case kparams.Double: + kval = float64(bytes.ReadUint64(b[50+offset+kparamNameLength+poffset:])) + poffset += kparamNameLength + 4 + 8 + case kparams.Float: + kval = float32(bytes.ReadUint32(b[50+offset+kparamNameLength+poffset:])) + poffset += kparamNameLength + 4 + 4 + case kparams.IPv4: + kval = ip.ToIPv4(bytes.ReadUint32(b[50+offset+kparamNameLength+poffset:])) + // // increment by IPv4 length + poffset += kparamNameLength + 4 + 4 + case kparams.IPv6: + kval = ip.ToIPv6(b[50+offset+kparamNameLength+poffset : 50+offset+kparamNameLength+poffset+16]) + // increment by IPv6 length + poffset += kparamNameLength + 4 + 16 + case kparams.PID, kparams.TID: + kval = bytes.ReadUint32(b[50+offset+kparamNameLength+poffset:]) + poffset += kparamNameLength + 4 + 4 + case kparams.Int32: + kval = int32(bytes.ReadUint32(b[50+offset+kparamNameLength+poffset:])) + poffset += kparamNameLength + 4 + 4 + case kparams.Uint32: + kval = bytes.ReadUint32(b[50+offset+kparamNameLength+poffset:]) + poffset += kparamNameLength + 4 + 4 + case kparams.Uint16, kparams.Port: + kval = bytes.ReadUint16(b[50+offset+kparamNameLength+poffset:]) + poffset += kparamNameLength + 4 + 2 + case kparams.Int16: + kval = int16(bytes.ReadUint16(b[50+offset+kparamNameLength+poffset:])) + poffset += kparamNameLength + 4 + 2 + case kparams.Uint8, kparams.Enum: + switch kparamName { + case kparams.FileOperation: + kval = fs.FileDisposition(uint8(b[50+offset+kparamNameLength+poffset : 50+offset+kparamNameLength+poffset+1][0])) + case kparams.FileShareMask: + kval = fs.FileShareMode(uint8(b[50+offset+kparamNameLength+poffset : 50+offset+kparamNameLength+poffset+1][0])) + case kparams.NetL4Proto: + kval = knet.L4Proto(uint8(b[50+offset+kparamNameLength+poffset : 50+offset+kparamNameLength+poffset+1][0])) + default: + kval = uint8(b[50+offset+kparamNameLength+poffset : 50+offset+kparamNameLength+poffset+1][0]) + } + poffset += kparamNameLength + 4 + 1 + case kparams.Int8: + kval = int8(b[50+offset+kparamNameLength+poffset : 50+offset+kparamNameLength+poffset+1][0]) + poffset += kparamNameLength + 4 + 1 + case kparams.HexInt8: + v := uint8(b[50+offset+kparamNameLength+poffset : 50+offset+kparamNameLength+poffset+1][0]) + kval = kparams.NewHex(v) + poffset += kparamNameLength + 4 + 1 + case kparams.HexInt16: + v := bytes.ReadUint16(b[50+offset+kparamNameLength+poffset:]) + kval = kparams.NewHex(v) + poffset += kparamNameLength + 4 + 2 + case kparams.HexInt32: + v := bytes.ReadUint32(b[50+offset+kparamNameLength+poffset:]) + kval = kparams.NewHex(v) + poffset += kparamNameLength + 4 + 4 + case kparams.HexInt64: + v := bytes.ReadUint64(b[50+offset+kparamNameLength+poffset:]) + kval = kparams.NewHex(v) + poffset += kparamNameLength + 4 + 8 + case kparams.Bool: + v := uint8(b[50+offset+kparamNameLength+poffset : 50+offset+kparamNameLength+poffset+1][0]) + if v == 1 { + kval = true + } else { + kval = false + } + poffset += kparamNameLength + 4 + 1 + case kparams.Time: + // read ts length + l := bytes.ReadUint16(b[50+offset+kparamNameLength+poffset:]) + buf = b[52+offset+kparamNameLength+poffset:] + if len(buf) > 0 { + var err error + kval, err = time.Parse(time.RFC3339Nano, string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l])) + if err != nil { + unmarshalTimestampErrors.Add(1) + } + } + poffset += kparamNameLength + 6 + l + } + if kval != nil { + kevt.Kparams.AppendFromKcap(kparamName, kparams.Type(typ), kval) + } + } + + offset += poffset + + // read metadata tags + nbTags := bytes.ReadUint16(b[46+offset:]) + var moffset uint16 + for i := 0; i < int(nbTags); i++ { + // read key + klen := bytes.ReadUint16(b[48+offset+moffset:]) + buf = b[50+offset+moffset:] + key := string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:klen:klen]) + // read value + vlen := bytes.ReadUint16(b[50+offset+klen+moffset:]) + buf = b[52+offset+klen+moffset:] + value := string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:vlen:vlen]) + // increment the offset by the length of the key + length value + size of uint16 * 2 + // that corresponds to bytes storing the lengths of keys/values + moffset += klen + vlen + 4 + if key != "" { + kevt.AddMeta(key, value) + } + } + + offset += moffset + + // read process state + sec := section.Read(b[48+offset:]) + if sec.Size() != 0 { + ps, err := ptypes.NewFromKcap(b[58+offset:]) + if err != nil { + return err + } + kevt.PS = ps + } + + return nil +} + +var js = newJSONStream() + +func writePsResources() bool { + return SerializeHandles || SerializeThreads || SerializeImages || SerializePE +} + +// MarshalJSON produces a JSON payload for this kevent. +func (kevt *Kevent) MarshalJSON() []byte { + if kevt == nil { + return []byte{} + } + + // start of JSON + js.writeObjectStart() + + js.writeObjectField("seq").writeUint64(kevt.Seq).writeMore() + js.writeObjectField("pid").writeUint32(kevt.PID).writeMore() + js.writeObjectField("tid").writeUint32(kevt.Tid).writeMore() + js.writeObjectField("cpu").writeUint8(kevt.CPU).writeMore() + + js.writeObjectField("name").writeString(kevt.Name).writeMore() + js.writeObjectField("category").writeString(string(kevt.Category)).writeMore() + js.writeObjectField("description").writeString(kevt.Description).writeMore() + js.writeObjectField("host").writeString(kevt.Host).writeMore() + + timestamp := make([]byte, 0) + timestamp = kevt.Timestamp.AppendFormat(timestamp, time.RFC3339Nano) + js.writeObjectField("timestamp").writeString(string(timestamp)).writeMore() + + // start kparams + js.writeObjectField("kparams") + js.writeObjectStart() + + pars := make([]*Kparam, 0, len(kevt.Kparams)) + for _, kpar := range kevt.Kparams { + pars = append(pars, kpar) + } + for i, kpar := range pars { + writeMore := js.shouldWriteMore(i, len(pars)) + js.writeObjectField(kpar.Name) + switch kpar.Type { + case kparams.AnsiString, kparams.UnicodeString, kparams.SID, kparams.WbemSID: + js.writeEscapeString(kpar.Value.(string)) + case kparams.Int64: + js.writeInt64(kpar.Value.(int64)) + case kparams.Uint64: + js.writeUint64(kpar.Value.(uint64)) + case kparams.Int32: + js.writeInt32(kpar.Value.(int32)) + case kparams.Uint32: + js.writeUint32(kpar.Value.(uint32)) + case kparams.Int16: + js.writeInt16(kpar.Value.(int16)) + case kparams.Uint16, kparams.Port: + js.writeUint16(kpar.Value.(uint16)) + case kparams.Int8: + js.writeInt8(kpar.Value.(int8)) + case kparams.Uint8: + js.writeUint8(kpar.Value.(uint8)) + case kparams.Float: + js.writeFloat32(kpar.Value.(float32)) + case kparams.Double: + js.writeFloat64(kpar.Value.(float64)) + case kparams.PID, kparams.TID: + js.writeUint32(kpar.Value.(uint32)) + case kparams.IPv4, kparams.IPv6: + js.writeString(kpar.Value.(net.IP).String()) + case kparams.HexInt8, kparams.HexInt16, kparams.HexInt32, kparams.HexInt64: + js.writeString(kpar.Value.(kparams.Hex).String()) + case kparams.Enum: + switch kpar.Name { + case kparams.FileOperation: + js.writeString(kpar.Value.(fs.FileDisposition).String()) + case kparams.FileShareMask: + js.writeString(kpar.Value.(fs.FileShareMode).String()) + case kparams.NetL4Proto: + js.writeString(kpar.Value.(knet.L4Proto).String()) + default: + val, ok := kpar.Value.(uint8) + if !ok { + continue + } + js.writeUint8(val) + } + case kparams.Bool: + js.writeBool(kpar.Value.(bool)) + case kparams.Time: + js.writeString(kpar.Value.(time.Time).String()) + } + if writeMore { + js.writeMore() + } + } + // end kparams + js.writeObjectEnd().writeMore() + + // start metadata + js.writeObjectField("meta") + js.writeObjectStart() + var i int + for k, v := range kevt.Metadata { + writeMore := js.shouldWriteMore(i, len(kevt.Metadata)) + js.writeObjectField(k).writeEscapeString(v) + if writeMore { + js.writeMore() + } + i++ + } + + // end metadata + js.writeObjectEnd() + ps := kevt.PS + if ps != nil { + js.writeMore() + } + + // start process state + if ps != nil { + js.writeObjectField("ps") + js.writeObjectStart() + + js.writeObjectField("pid").writeUint32(ps.PID).writeMore() + js.writeObjectField("ppid").writeUint32(ps.Ppid).writeMore() + js.writeObjectField("name").writeString(ps.Name).writeMore() + js.writeObjectField("comm").writeEscapeString(ps.Comm).writeMore() + js.writeObjectField("exe").writeEscapeString(ps.Exe).writeMore() + js.writeObjectField("cwd").writeEscapeString(ps.Cwd).writeMore() + js.writeObjectField("sid").writeEscapeString(ps.SID).writeMore() + + js.writeObjectField("args") + js.writeArrayStart() + for i, arg := range ps.Args { + writeMore := js.shouldWriteMore(i, len(ps.Args)) + js.writeEscapeString(arg) + if writeMore { + js.writeMore() + } + } + js.writeArrayEnd().writeMore() + + js.writeObjectField("sessionid").writeUint8(ps.SessionID) + + if SerializeEnvs { + js.writeMore() + js.writeObjectField("envs") + js.writeObjectStart() + var i int + for k, v := range ps.Envs { + writeMore := js.shouldWriteMore(i, len(ps.Envs)) + js.writeObjectField(k).writeEscapeString(v) + if writeMore { + js.writeMore() + } + i++ + } + js.writeObjectEnd() + } + + if writePsResources() { + js.writeMore() + } + + if SerializeThreads { + // start threads + js.writeObjectField("threads") + js.writeArrayStart() + var i int + ps.RLock() + for _, thread := range ps.Threads { + writeMore := js.shouldWriteMore(i, len(ps.Threads)) + js.writeObjectStart() + js.writeObjectField("tid").writeUint32(thread.Tid).writeMore() + js.writeObjectField("ioprio").writeUint8(thread.IOPrio).writeMore() + js.writeObjectField("baseprio").writeUint8(thread.BasePrio).writeMore() + js.writeObjectField("pageprio").writeUint8(thread.PagePrio).writeMore() + js.writeObjectField("entrypoint").writeString(thread.Entrypoint.String()).writeMore() + js.writeObjectField("ustack_base").writeString(thread.UstackBase.String()).writeMore() + js.writeObjectField("ustack_limit").writeString(thread.UstackLimit.String()).writeMore() + js.writeObjectField("kstack_base").writeString(thread.KstackBase.String()).writeMore() + js.writeObjectField("kstack_limit").writeString(thread.KstackLimit.String()) + js.writeObjectEnd() + if writeMore { + js.writeMore() + } + i++ + } + ps.RUnlock() + // end threads + js.writeArrayEnd() + if SerializeImages || SerializeHandles { + js.writeMore() + } + } + + if SerializeImages { + // start modules + js.writeObjectField("modules") + js.writeArrayStart() + + for i, m := range ps.Modules { + writeMore := js.shouldWriteMore(i, len(ps.Modules)) + js.writeObjectStart() + js.writeObjectField("name").writeEscapeString(m.Name).writeMore() + js.writeObjectField("size").writeUint32(m.Size) + js.writeObjectEnd() + if writeMore { + js.writeMore() + } + } + + // end modules + js.writeArrayEnd() + if SerializeHandles { + js.writeMore() + } + } + + if SerializeHandles { + // start handles + js.writeObjectField("handles") + js.writeArrayStart() + + for i, handle := range ps.Handles { + writeMore := js.shouldWriteMore(i, len(ps.Handles)) + js.writeObjectStart() + js.writeObjectField("name").writeEscapeString(handle.Name).writeMore() + js.writeObjectField("type").writeString(handle.Type).writeMore() + js.writeObjectField("id").writeUint64(uint64(handle.Num)).writeMore() + js.writeObjectField("object").writeUint64(uint64(handle.Object)) + js.writeObjectEnd() + + if writeMore { + js.writeMore() + } + } + // end handles + js.writeArrayEnd() + if SerializePE && ps.PE != nil { + js.writeMore() + } + } + + pe := ps.PE + if SerializePE && pe != nil { + // start PE + js.writeObjectField("pe") + js.writeObjectStart() + + js.writeObjectField("nsections").writeUint16(pe.NumberOfSections).writeMore() + js.writeObjectField("nsymbols").writeUint32(pe.NumberOfSymbols).writeMore() + js.writeObjectField("image_base").writeString(pe.ImageBase).writeMore() + js.writeObjectField("entrypoint").writeString(pe.EntryPoint).writeMore() + + timestamp := make([]byte, 0) + timestamp = kevt.Timestamp.AppendFormat(timestamp, time.RFC3339Nano) + js.writeObjectField("link_time").writeString(string(timestamp)).writeMore() + + // sections + if len(pe.Sections) > 0 { + js.writeObjectField("sections") + js.writeArrayStart() + + for i, sec := range pe.Sections { + writeMore := js.shouldWriteMore(i, len(pe.Sections)) + js.writeObjectStart() + js.writeObjectField("name").writeEscapeString(sec.Name).writeMore() + js.writeObjectField("size").writeUint32(sec.Size).writeMore() + js.writeObjectField("entropy").writeFloat64(sec.Entropy).writeMore() + js.writeObjectField("md5").writeString(sec.Md5) + + js.writeObjectEnd() + if writeMore { + js.writeMore() + } + } + + js.writeArrayEnd() + if len(pe.Symbols) > 0 { + js.writeMore() + } + } + + // imported symbols + if len(pe.Symbols) > 0 { + js.writeObjectField("symbols") + js.writeArrayStart() + + for i, sym := range pe.Symbols { + writeMore := js.shouldWriteMore(i, len(pe.Symbols)) + js.writeEscapeString(sym) + + if writeMore { + js.writeMore() + } + } + + js.writeArrayEnd() + if len(pe.Imports) > 0 { + js.writeMore() + } + } + + // imports + if len(pe.Imports) > 0 { + js.writeObjectField("imports") + js.writeArrayStart() + + for i, imp := range pe.Imports { + writeMore := js.shouldWriteMore(i, len(pe.Imports)) + js.writeEscapeString(imp) + + if writeMore { + js.writeMore() + } + } + + js.writeArrayEnd() + if len(pe.VersionResources) > 0 { + js.writeMore() + } + } + + // version resources + if len(pe.VersionResources) > 0 { + js.writeObjectField("resources") + js.writeObjectStart() + + var i int + for k, v := range pe.VersionResources { + writeMore := js.shouldWriteMore(i, len(pe.VersionResources)) + js.writeObjectField(k).writeEscapeString(v) + + if writeMore { + js.writeMore() + } + i++ + } + js.writeObjectEnd() + + } + + // end PE + js.writeObjectEnd() + + } + + // end process state + js.writeObjectEnd() + } + + // end of JSON + js.writeObjectEnd() + + return js.flush() +} + +type jsonStream struct { + buf []byte +} + +func newJSONStream() *jsonStream { + return &jsonStream{buf: make([]byte, 0)} +} + +func (js *jsonStream) flush() []byte { + buf := js.buf + js.buf = nil + return buf +} + +func (js *jsonStream) writeByte(c byte) { + js.buf = append(js.buf, c) +} + +func (js *jsonStream) writeNewline() { + js.buf = append(js.buf, '\n') +} + +func (js *jsonStream) writeTwoBytes(c1, c2 byte) { + js.buf = append(js.buf, c1, c2) +} + +func (js *jsonStream) writeBytes(b []byte) *jsonStream { + js.buf = append(js.buf, b...) + return js +} + +func (js *jsonStream) writeString(s string) *jsonStream { + js.writeByte('"') + js.buf = append(js.buf, s...) + js.writeByte('"') + return js +} + +func (js *jsonStream) writeRaw(s string) { + js.buf = append(js.buf, s...) +} + +func (js *jsonStream) writeEscapeString(s string) *jsonStream { + valLen := len(s) + js.buf = append(js.buf, '"') + // write string, the fast path, without utf8 and escape support + i := 0 + for ; i < valLen; i++ { + c := s[i] + if c > 31 && c != '"' && c != '\\' { + js.buf = append(js.buf, c) + } else { + break + } + } + if i == valLen { + js.buf = append(js.buf, '"') + return js + } + + // write remaining part of the string with escape support + writeStringSlowPath(js, i, s, valLen) + return js +} + +func (js *jsonStream) writeObjectStart() *jsonStream { + js.writeByte('{') + return js +} + +func (js *jsonStream) writeArrayStart() *jsonStream { + js.writeByte('[') + return js +} + +func (js *jsonStream) writeArrayEnd() *jsonStream { + js.writeByte(']') + return js +} + +func (js *jsonStream) writeObjectField(f string) *jsonStream { + js.writeString(f) + js.writeTwoBytes(':', ' ') + return js +} + +func (js *jsonStream) writeBool(b bool) *jsonStream { + if b { + js.writeString("true") + return js + } + js.writeString("false") + return js +} + +func (js *jsonStream) writeObjectEnd() *jsonStream { + js.writeByte('}') + return js +} + +func (js *jsonStream) writeMore() *jsonStream { + js.writeByte(',') + return js +} + +func (js *jsonStream) shouldWriteMore(i, l int) bool { + return !(i == l-1) +} + +// borrowed from jsointer: https://github.com/json-iterator/go/blob/2fbdfbb5951116fb8bede4fd8b919a19e4a6b647/stream_int.go and https://github.com/json-iterator/go/blob/2fbdfbb5951116fb8bede4fd8b919a19e4a6b647/stream_float.go + +var digits []uint32 + +// safeSet holds the value true if the ASCII character with the given array +// position can be represented inside a JSON string without any further +// escaping. +// +// All values are true except for the ASCII control characters (0-31), the +// double quote ("), and the backslash character ("\"). +var safeSet = [utf8.RuneSelf]bool{ + ' ': true, + '!': true, + '"': false, + '#': true, + '$': true, + '%': true, + '&': true, + '\'': true, + '(': true, + ')': true, + '*': true, + '+': true, + ',': true, + '-': true, + '.': true, + '/': true, + '0': true, + '1': true, + '2': true, + '3': true, + '4': true, + '5': true, + '6': true, + '7': true, + '8': true, + '9': true, + ':': true, + ';': true, + '<': true, + '=': true, + '>': true, + '?': true, + '@': true, + 'A': true, + 'B': true, + 'C': true, + 'D': true, + 'E': true, + 'F': true, + 'G': true, + 'H': true, + 'I': true, + 'J': true, + 'K': true, + 'L': true, + 'M': true, + 'N': true, + 'O': true, + 'P': true, + 'Q': true, + 'R': true, + 'S': true, + 'T': true, + 'U': true, + 'V': true, + 'W': true, + 'X': true, + 'Y': true, + 'Z': true, + '[': true, + '\\': false, + ']': true, + '^': true, + '_': true, + '`': true, + 'a': true, + 'b': true, + 'c': true, + 'd': true, + 'e': true, + 'f': true, + 'g': true, + 'h': true, + 'i': true, + 'j': true, + 'k': true, + 'l': true, + 'm': true, + 'n': true, + 'o': true, + 'p': true, + 'q': true, + 'r': true, + 's': true, + 't': true, + 'u': true, + 'v': true, + 'w': true, + 'x': true, + 'y': true, + 'z': true, + '{': true, + '|': true, + '}': true, + '~': true, + '\u007f': true, +} + +var hex = "0123456789abcdef" + +func init() { + digits = make([]uint32, 1000) + for i := uint32(0); i < 1000; i++ { + digits[i] = (((i / 100) + '0') << 16) + ((((i / 10) % 10) + '0') << 8) + i%10 + '0' + if i < 10 { + digits[i] += 2 << 24 + } else if i < 100 { + digits[i] += 1 << 24 + } + } +} + +func writeStringSlowPath(stream *jsonStream, i int, s string, valLen int) { + start := i + // for the remaining parts, we process them char by char + for i < valLen { + if b := s[i]; b < utf8.RuneSelf { + if safeSet[b] { + i++ + continue + } + if start < i { + stream.writeRaw(s[start:i]) + } + switch b { + case '\\', '"': + stream.writeTwoBytes('\\', b) + case '\n': + stream.writeTwoBytes('\\', 'n') + case '\r': + stream.writeTwoBytes('\\', 'r') + case '\t': + stream.writeTwoBytes('\\', 't') + default: + // This encodes bytes < 0x20 except for \t, \n and \r. + // If escapeHTML is set, it also escapes <, >, and & + // because they can lead to security holes when + // user-controlled strings are rendered into JSON + // and served to some browsers. + stream.writeRaw(`\u00`) + stream.writeTwoBytes(hex[b>>4], hex[b&0xF]) + } + i++ + start = i + continue + } + i++ + continue + } + if start < len(s) { + stream.writeRaw(s[start:]) + } + stream.writeByte('"') +} + +func writeFirstBuf(space []byte, v uint32) []byte { + start := v >> 24 + if start == 0 { + space = append(space, byte(v>>16), byte(v>>8)) + } else if start == 1 { + space = append(space, byte(v>>8)) + } + space = append(space, byte(v)) + return space +} + +func writeBuf(buf []byte, v uint32) []byte { + return append(buf, byte(v>>16), byte(v>>8), byte(v)) +} + +func (js *jsonStream) writeUint8(val uint8) *jsonStream { + js.buf = writeFirstBuf(js.buf, digits[val]) + return js +} + +func (js *jsonStream) writeInt8(nval int8) *jsonStream { + var val uint8 + if nval < 0 { + val = uint8(-nval) + js.buf = append(js.buf, '-') + } else { + val = uint8(nval) + } + js.buf = writeFirstBuf(js.buf, digits[val]) + return js +} + +func (js *jsonStream) writeUint16(val uint16) *jsonStream { + q1 := val / 1000 + if q1 == 0 { + js.buf = writeFirstBuf(js.buf, digits[val]) + return js + } + r1 := val - q1*1000 + js.buf = writeFirstBuf(js.buf, digits[q1]) + js.buf = writeBuf(js.buf, digits[r1]) + return js +} + +func (js *jsonStream) writeInt16(nval int16) *jsonStream { + var val uint16 + if nval < 0 { + val = uint16(-nval) + js.buf = append(js.buf, '-') + } else { + val = uint16(nval) + } + js.writeUint16(val) + return js +} + +func (js *jsonStream) writeUint32(val uint32) *jsonStream { + q1 := val / 1000 + if q1 == 0 { + js.buf = writeFirstBuf(js.buf, digits[val]) + return js + } + r1 := val - q1*1000 + q2 := q1 / 1000 + if q2 == 0 { + js.buf = writeFirstBuf(js.buf, digits[q1]) + js.buf = writeBuf(js.buf, digits[r1]) + return js + } + r2 := q1 - q2*1000 + q3 := q2 / 1000 + if q3 == 0 { + js.buf = writeFirstBuf(js.buf, digits[q2]) + } else { + r3 := q2 - q3*1000 + js.buf = append(js.buf, byte(q3+'0')) + js.buf = writeBuf(js.buf, digits[r3]) + } + js.buf = writeBuf(js.buf, digits[r2]) + js.buf = writeBuf(js.buf, digits[r1]) + return js +} + +func (js *jsonStream) writeInt32(nval int32) *jsonStream { + var val uint32 + if nval < 0 { + val = uint32(-nval) + js.buf = append(js.buf, '-') + } else { + val = uint32(nval) + } + js.writeUint32(val) + return js +} + +func (js *jsonStream) writeUint64(val uint64) *jsonStream { + q1 := val / 1000 + if q1 == 0 { + js.buf = writeFirstBuf(js.buf, digits[val]) + return js + } + r1 := val - q1*1000 + q2 := q1 / 1000 + if q2 == 0 { + js.buf = writeFirstBuf(js.buf, digits[q1]) + js.buf = writeBuf(js.buf, digits[r1]) + return js + } + r2 := q1 - q2*1000 + q3 := q2 / 1000 + if q3 == 0 { + js.buf = writeFirstBuf(js.buf, digits[q2]) + js.buf = writeBuf(js.buf, digits[r2]) + js.buf = writeBuf(js.buf, digits[r1]) + return js + } + r3 := q2 - q3*1000 + q4 := q3 / 1000 + if q4 == 0 { + js.buf = writeFirstBuf(js.buf, digits[q3]) + js.buf = writeBuf(js.buf, digits[r3]) + js.buf = writeBuf(js.buf, digits[r2]) + js.buf = writeBuf(js.buf, digits[r1]) + return js + } + r4 := q3 - q4*1000 + q5 := q4 / 1000 + if q5 == 0 { + js.buf = writeFirstBuf(js.buf, digits[q4]) + js.buf = writeBuf(js.buf, digits[r4]) + js.buf = writeBuf(js.buf, digits[r3]) + js.buf = writeBuf(js.buf, digits[r2]) + js.buf = writeBuf(js.buf, digits[r1]) + return js + } + r5 := q4 - q5*1000 + q6 := q5 / 1000 + if q6 == 0 { + js.buf = writeFirstBuf(js.buf, digits[q5]) + } else { + js.buf = writeFirstBuf(js.buf, digits[q6]) + r6 := q5 - q6*1000 + js.buf = writeBuf(js.buf, digits[r6]) + } + js.buf = writeBuf(js.buf, digits[r5]) + js.buf = writeBuf(js.buf, digits[r4]) + js.buf = writeBuf(js.buf, digits[r3]) + js.buf = writeBuf(js.buf, digits[r2]) + js.buf = writeBuf(js.buf, digits[r1]) + return js +} + +func (js *jsonStream) writeInt64(nval int64) *jsonStream { + var val uint64 + if nval < 0 { + val = uint64(-nval) + js.buf = append(js.buf, '-') + } else { + val = uint64(nval) + } + js.writeUint64(val) + return js +} + +// writeFloat32 write float32 to stream +func (js *jsonStream) writeFloat32(val float32) *jsonStream { + abs := math.Abs(float64(val)) + format := byte('f') + // Note: Must use float32 comparisons for underlying float32 value to get precise cutoffs right. + if abs != 0 { + if float32(abs) < 1e-6 || float32(abs) >= 1e21 { + format = 'e' + } + } + js.buf = strconv.AppendFloat(js.buf, float64(val), format, -1, 32) + return js +} + +// writeFloat64 write float64 to stream +func (js *jsonStream) writeFloat64(val float64) *jsonStream { + abs := math.Abs(val) + format := byte('f') + // Note: Must use float32 comparisons for underlying float32 value to get precise cutoffs right. + if abs != 0 { + if abs < 1e-6 || abs >= 1e21 { + format = 'e' + } + } + js.buf = strconv.AppendFloat(js.buf, float64(val), format, -1, 64) + return js +} diff --git a/pkg/kevent/marshaller_test.go b/pkg/kevent/marshaller_test.go new file mode 100644 index 000000000..4496a3a24 --- /dev/null +++ b/pkg/kevent/marshaller_test.go @@ -0,0 +1,581 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kevent + +import ( + "encoding/json" + htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + pex "github.com/rabbitstack/fibratus/pkg/pe" + pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" + shandle "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "io/ioutil" + "testing" + "time" +) + +func init() { + SerializeThreads = true + SerializeImages = true + SerializeHandles = true + SerializePE = true + SerializeEnvs = true +} + +func TestMarshaller(t *testing.T) { + now, err := time.Parse(time.RFC3339Nano, time.Now().Format(time.RFC3339Nano)) + require.NoError(t, err) + + kevt := &Kevent{ + Type: ktypes.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: now, + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, + kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + kparams.KstackLimit: {Name: kparams.KstackLimit, Type: kparams.HexInt8, Value: kparams.Hex("ff")}, + kparams.StartTime: {Name: kparams.StartTime, Type: kparams.Time, Value: time.Now()}, + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(1204)}, + }, + Metadata: map[string]string{"foo": "bar", "fooz": "barzz"}, + } + + b := kevt.MarshalRaw() + require.NotEmpty(t, b) + + clone, err := NewFromKcap(b) + + assert.Equal(t, uint64(2), clone.Seq) + assert.Equal(t, uint32(859), clone.PID) + assert.Equal(t, uint32(2484), clone.Tid) + assert.Equal(t, ktypes.CreateFile, clone.Type) + assert.Equal(t, uint8(1), clone.CPU) + assert.Equal(t, "CreateFile", clone.Name) + assert.Equal(t, ktypes.File, clone.Category) + assert.Equal(t, "Creates or opens a new file, directory, I/O device, pipe, console", clone.Description) + assert.Equal(t, "archrabbit", clone.Host) + assert.Equal(t, now, clone.Timestamp) + + assert.Len(t, clone.Kparams, 9) + + filename, err := clone.Kparams.GetString(kparams.FileName) + require.NoError(t, err) + assert.Equal(t, "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll", filename) + fileobject, err := clone.Kparams.GetUint64(kparams.FileObject) + require.NoError(t, err) + assert.Equal(t, uint64(12456738026482168384), fileobject) + + assert.Len(t, clone.Metadata, 2) + + assert.Equal(t, "bar", clone.Metadata["foo"]) + assert.Equal(t, "barzz", clone.Metadata["fooz"]) +} + +func TestKeventMarshalJSON(t *testing.T) { + kevt := &Kevent{ + Type: ktypes.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, + kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + }, + Metadata: map[string]string{"foo": "bar", "fooz": "baarz"}, + PS: &pstypes.PS{ + PID: 2436, + Ppid: 6304, + Name: "firefox.exe", + Exe: `C:\Program Files\Mozilla Firefox\firefox.exe`, + Comm: `C:\Program Files\Mozilla Firefox\firefox.exe -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, + Cwd: `C:\Program Files\Mozilla Firefox\`, + SID: "archrabbit\\SYSTEM", + Args: []string{"-contentproc", `--channel=6304.3.1055809391\1014207667`, "-childID", "1", "-isForBrowser", "-prefsHandle", "2584", "-prefMapHandle", "2580", "-prefsLen", "70", "-prefMapSize", "216993", "-parentBuildID"}, + SessionID: 4, + Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, + Threads: map[uint32]pstypes.Thread{ + 3453: {Tid: 3453, Entrypoint: kparams.Hex("0x7ffe2557ff80"), IOPrio: 2, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + 3455: {Tid: 3455, Entrypoint: kparams.Hex("0x5efe2557ff80"), IOPrio: 3, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + }, + Handles: []htypes.Handle{ + {Num: shandle.Handle(0xffffd105e9baaf70), + Name: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, + Type: "Key", + Object: 777488883434455544, + Pid: uint32(1023), + }, + { + Num: shandle.Handle(0xffffd105e9adaf70), + Name: `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, + Type: "ALPC Port", + Pid: uint32(1023), + MD: &htypes.AlpcPortInfo{ + Seqno: 1, + Context: 0x0, + Flags: 0x0, + }, + Object: 457488883434455544, + }, + { + Num: shandle.Handle(0xeaffd105e9adaf30), + Name: `C:\Users\bunny`, + Type: "File", + Pid: uint32(1023), + MD: &htypes.FileInfo{ + IsDirectory: true, + }, + Object: 357488883434455544, + }, + }, + PE: &pex.PE{ + NumberOfSections: 2, + NumberOfSymbols: 10, + EntryPoint: "0x20110", + ImageBase: "0x140000000", + LinkTime: time.Now(), + Sections: []pex.Sec{ + {Name: ".text", Size: 132608, Entropy: 6.368381, Md5: "db23dce3911a42e987041d98abd4f7cd"}, + {Name: ".rdata", Size: 35840, Entropy: 5.996976, Md5: "ffa5c960b421ca9887e54966588e97e8"}, + }, + Symbols: []string{"SelectObject", "GetTextFaceW", "EnumFontsW", "TextOutW", "GetProcessHeap"}, + Imports: []string{"GDI32.dll", "USER32.dll", "msvcrt.dll", "api-ms-win-core-libraryloader-l1-2-0.dl"}, + VersionResources: map[string]string{"CompanyName": "Microsoft Corporation", "FileDescription": "Notepad", "FileVersion": "10.0.18362.693"}, + }, + }, + } + s := kevt.MarshalJSON() + var newKevt Kevent + err := json.Unmarshal(s, &newKevt) + require.NoError(t, err) + + assert.Equal(t, uint32(2484), newKevt.Tid) + assert.Equal(t, uint32(859), newKevt.PID) + assert.Equal(t, "archrabbit\\SYSTEM", newKevt.PS.SID) + assert.Len(t, newKevt.PS.Envs, 2) + assert.Len(t, newKevt.PS.Handles, 3) + + assert.NotNil(t, newKevt.PS.PE) + assert.Equal(t, uint32(10), newKevt.PS.PE.NumberOfSymbols) + assert.Equal(t, uint16(2), newKevt.PS.PE.NumberOfSections) + assert.Len(t, newKevt.PS.PE.Sections, 2) + assert.Len(t, newKevt.PS.PE.Symbols, 5) + assert.Len(t, newKevt.PS.PE.Imports, 4) + assert.Len(t, newKevt.PS.PE.VersionResources, 3) +} + +func TestUnmarshalHugeHandles(t *testing.T) { + b, err := ioutil.ReadFile("C:\\handles.json") + require.NoError(t, err) + handles := make([]htypes.Handle, 0) + err = json.Unmarshal(b, &handles) + require.NoError(t, err) + + kevt := &Kevent{ + Type: ktypes.CreateProcess, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateProcess", + Timestamp: time.Now(), + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates a new process", + Kparams: Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, + kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + }, + Metadata: map[string]string{"foo": "bar", "fooz": "baarz"}, + PS: &pstypes.PS{ + PID: 2436, + Ppid: 6304, + Name: "firefox.exe", + Exe: `C:\Program Files\Mozilla Firefox\firefox.exe`, + Comm: `C:\Program Files\Mozilla Firefox\firefox.exe -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, + Cwd: `C:\Program Files\Mozilla Firefox\`, + SID: "archrabbit\\SYSTEM", + Args: []string{"-contentproc", `--channel=6304.3.1055809391\1014207667`, "-childID", "1", "-isForBrowser", "-prefsHandle", "2584", "-prefMapHandle", "2580", "-prefsLen", "70", "-prefMapSize", "216993", "-parentBuildID"}, + SessionID: 4, + Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, + Threads: map[uint32]pstypes.Thread{ + 3453: {Tid: 3453, Entrypoint: kparams.Hex("0x7ffe2557ff80"), IOPrio: 2, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + 3455: {Tid: 3455, Entrypoint: kparams.Hex("0x5efe2557ff80"), IOPrio: 3, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + }, + Handles: handles, + PE: &pex.PE{ + NumberOfSections: 7, + NumberOfSymbols: 10, + EntryPoint: "0x20110", + ImageBase: "0x140000000", + LinkTime: time.Now(), + Sections: []pex.Sec{ + {Name: ".text", Size: 132608, Entropy: 6.368381, Md5: "db23dce3911a42e987041d98abd4f7cd"}, + {Name: ".rdata", Size: 35840, Entropy: 5.996976, Md5: "ffa5c960b421ca9887e54966588e97e8"}, + }, + Symbols: []string{"SelectObject", "GetTextFaceW", "EnumFontsW", "TextOutW", "GetProcessHeap"}, + Imports: []string{"GDI32.dll", "USER32.dll", "msvcrt.dll", "api-ms-win-core-libraryloader-l1-2-0.dl"}, + VersionResources: map[string]string{"CompanyName": "Microsoft Corporation", "FileDescription": "Notepad", "FileVersion": "10.0.18362.693"}, + }, + }, + } + + s := kevt.MarshalRaw() + clone, err := NewFromKcap(s) + require.NoError(t, err) + require.NotNil(t, clone) +} + +func TestKeventMarshalJSONMultiple(t *testing.T) { + for i := 0; i < 10; i++ { + seq := uint64(i + 1) + kevt := &Kevent{ + Type: ktypes.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: seq, + Name: "CreateFile", + Timestamp: time.Now(), + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, + kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + }, + Metadata: map[string]string{"foo": "bar", "fooz": "baarz"}, + PS: &pstypes.PS{ + PID: 2436, + Ppid: 6304, + Name: "firefox.exe", + Exe: `C:\Program Files\Mozilla Firefox\firefox.exe`, + Comm: `C:\Program Files\Mozilla Firefox\firefox.exe -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, + Cwd: `C:\Program Files\Mozilla Firefox\`, + SID: "archrabbit\\SYSTEM", + Args: []string{"-contentproc", `--channel=6304.3.1055809391\1014207667`, "-childID", "1", "-isForBrowser", "-prefsHandle", "2584", "-prefMapHandle", "2580", "-prefsLen", "70", "-prefMapSize", "216993", "-parentBuildID"}, + SessionID: 4, + Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, + Threads: map[uint32]pstypes.Thread{ + 3453: {Tid: 3453, Entrypoint: kparams.Hex("0x7ffe2557ff80"), IOPrio: 2, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + 3455: {Tid: 3455, Entrypoint: kparams.Hex("0x5efe2557ff80"), IOPrio: 3, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + }, + Handles: []htypes.Handle{ + {Num: shandle.Handle(0xffffd105e9baaf70), + Name: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, + Type: "Key", + Object: 777488883434455544, + Pid: uint32(1023), + }, + { + Num: shandle.Handle(0xffffd105e9adaf70), + Name: `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, + Type: "ALPC Port", + Pid: uint32(1023), + MD: &htypes.AlpcPortInfo{ + Seqno: 1, + Context: 0x0, + Flags: 0x0, + }, + Object: 457488883434455544, + }, + { + Num: shandle.Handle(0xeaffd105e9adaf30), + Name: `C:\Users\bunny`, + Type: "File", + Pid: uint32(1023), + MD: &htypes.FileInfo{ + IsDirectory: true, + }, + Object: 357488883434455544, + }, + }, + }, + } + s := kevt.MarshalJSON() + var newKevt Kevent + err := json.Unmarshal(s, &newKevt) + require.NoError(t, err) + + assert.Equal(t, uint32(2484), newKevt.Tid) + assert.Equal(t, uint32(859), newKevt.PID) + assert.Equal(t, seq, newKevt.Seq) + assert.Len(t, newKevt.PS.Handles, 3) + } +} + +func BenchmarkKeventMarshalJSON(b *testing.B) { + kevt := &Kevent{ + Type: ktypes.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, + kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + }, + Metadata: map[string]string{"foo": "bar", "fooz": "baarz"}, + PS: &pstypes.PS{ + PID: 2436, + Ppid: 6304, + Name: "firefox.exe", + Exe: `C:\Program Files\Mozilla Firefox\firefox.exe`, + Comm: `C:\Program Files\Mozilla Firefox\firefox.exe -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, + Cwd: `C:\Program Files\Mozilla Firefox\`, + SID: "archrabbit\\SYSTEM", + Args: []string{"-contentproc", `--channel=6304.3.1055809391\1014207667`, "-childID", "1", "-isForBrowser", "-prefsHandle", "2584", "-prefMapHandle", "2580", "-prefsLen", "70", "-prefMapSize", "216993", "-parentBuildID"}, + SessionID: 4, + Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, + Handles: []htypes.Handle{ + {Num: shandle.Handle(0xffffd105e9baaf70), + Name: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, + Type: "Key", + Object: 777488883434455544, + Pid: uint32(1023), + }, + { + Num: shandle.Handle(0xffffd105e9adaf70), + Name: `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, + Type: "ALPC Port", + Pid: uint32(1023), + MD: &htypes.AlpcPortInfo{ + Seqno: 1, + Context: 0x0, + Flags: 0x0, + }, + Object: 457488883434455544, + }, + { + Num: shandle.Handle(0xeaffd105e9adaf30), + Name: `C:\Users\bunny`, + Type: "File", + Pid: uint32(1023), + MD: &htypes.FileInfo{ + IsDirectory: true, + }, + Object: 357488883434455544, + }, + }, + PE: &pex.PE{ + NumberOfSections: 2, + NumberOfSymbols: 10, + EntryPoint: "0x20110", + ImageBase: "0x140000000", + LinkTime: time.Now(), + Sections: []pex.Sec{ + {Name: ".text", Size: 132608, Entropy: 6.368381, Md5: "db23dce3911a42e987041d98abd4f7cd"}, + {Name: ".rdata", Size: 35840, Entropy: 5.996976, Md5: "ffa5c960b421ca9887e54966588e97e8"}, + }, + Symbols: []string{"SelectObject", "GetTextFaceW", "EnumFontsW", "TextOutW", "GetProcessHeap"}, + Imports: []string{"GDI32.dll", "USER32.dll", "msvcrt.dll", "api-ms-win-core-libraryloader-l1-2-0.dl"}, + VersionResources: map[string]string{"CompanyName": "Microsoft Corporation", "FileDescription": "Notepad", "FileVersion": "10.0.18362.693"}, + }, + }, + } + b.ReportAllocs() + for i := 0; i < b.N; i++ { + kevt.MarshalJSON() + } +} + +func BenchmarkKeventMarshalJSONStdlib(b *testing.B) { + kevt := &Kevent{ + Type: ktypes.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, + kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + }, + Metadata: map[string]string{"foo": "bar", "fooz": "baarz"}, + PS: &pstypes.PS{ + PID: 2436, + Ppid: 6304, + Name: "firefox.exe", + Exe: `C:\Program Files\Mozilla Firefox\firefox.exe`, + Comm: `C:\Program Files\Mozilla Firefox\firefox.exe -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, + Cwd: `C:\Program Files\Mozilla Firefox\`, + SID: "archrabbit\\SYSTEM", + Args: []string{"-contentproc", `--channel=6304.3.1055809391\1014207667`, "-childID", "1", "-isForBrowser", "-prefsHandle", "2584", "-prefMapHandle", "2580", "-prefsLen", "70", "-prefMapSize", "216993", "-parentBuildID"}, + SessionID: 4, + Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, + Handles: []htypes.Handle{ + {Num: shandle.Handle(0xffffd105e9baaf70), + Name: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, + Type: "Key", + Object: 777488883434455544, + Pid: uint32(1023), + }, + { + Num: shandle.Handle(0xffffd105e9adaf70), + Name: `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, + Type: "ALPC Port", + Pid: uint32(1023), + MD: &htypes.AlpcPortInfo{ + Seqno: 1, + Context: 0x0, + Flags: 0x0, + }, + Object: 457488883434455544, + }, + { + Num: shandle.Handle(0xeaffd105e9adaf30), + Name: `C:\Users\bunny`, + Type: "File", + Pid: uint32(1023), + MD: &htypes.FileInfo{ + IsDirectory: true, + }, + Object: 357488883434455544, + }, + }, + PE: &pex.PE{ + NumberOfSections: 2, + NumberOfSymbols: 10, + EntryPoint: "0x20110", + ImageBase: "0x140000000", + LinkTime: time.Now(), + Sections: []pex.Sec{ + {Name: ".text", Size: 132608, Entropy: 6.368381, Md5: "db23dce3911a42e987041d98abd4f7cd"}, + {Name: ".rdata", Size: 35840, Entropy: 5.996976, Md5: "ffa5c960b421ca9887e54966588e97e8"}, + }, + Symbols: []string{"SelectObject", "GetTextFaceW", "EnumFontsW", "TextOutW", "GetProcessHeap"}, + Imports: []string{"GDI32.dll", "USER32.dll", "msvcrt.dll", "api-ms-win-core-libraryloader-l1-2-0.dl"}, + VersionResources: map[string]string{"CompanyName": "Microsoft Corporation", "FileDescription": "Notepad", "FileVersion": "10.0.18362.693"}, + }, + }, + } + b.ReportAllocs() + for i := 0; i < b.N; i++ { + json.Marshal(kevt) + } +} + +func BenchmarkMarshal(b *testing.B) { + kevt := &Kevent{ + Type: ktypes.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + }, + Metadata: map[string]string{"foo": "bar", "fooz": "barz"}, + } + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if buf := kevt.MarshalRaw(); len(buf) == 0 { + b.Fatal("empty buffer") + } + } +} + +func BenchmarkUnmarshal(b *testing.B) { + kevt := &Kevent{ + Type: ktypes.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + }, + Metadata: map[string]string{"foo": "bar", "fooz": "barz"}, + } + buf := kevt.MarshalRaw() + b.ReportAllocs() + for i := 0; i < b.N; i++ { + ke, err := NewFromKcap(buf) + if err != nil { + b.Fatal(err) + } + if ke.Name == "" { + b.Fatal("invalid unmarshal byte slice") + } + } +} diff --git a/pkg/kevent/sequencer.go b/pkg/kevent/sequencer.go new file mode 100644 index 000000000..73422c55d --- /dev/null +++ b/pkg/kevent/sequencer.go @@ -0,0 +1,128 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kevent + +import ( + "errors" + "expvar" + "fmt" + "golang.org/x/sys/windows/registry" + "sync/atomic" + "syscall" + "time" +) + +const ( + // seqVName is the name of the registry value that stores the QWORD sequence. + seqVName = "KeventSeq" + invalidKey = registry.Key(syscall.InvalidHandle) +) + +var seqStoreErrors = expvar.NewInt("kevent.seq.store.errors") +var seqInitErrors = expvar.NewMap("kevent.seq.init.errors") +var errInvalidVolatileKey = errors.New("couldn't open HKCU/Volatile Environment key") + +const flags = uint32(registry.QUERY_VALUE | registry.SET_VALUE) + +// Sequencer is responsible for incrementing, getting and persisting the kevent sequence number in the Windows registry. +type Sequencer struct { + key registry.Key + quit chan struct{} + seq uint64 +} + +// NewSequencer creates a fresh kevent sequencer. If the `KeventSeq` value is present under the volatile key, the current +// sequence number is initialized to the last stored sequence. The sequencer schedules a ticker that periodically dumps +// the current sequence number into the registry value. +func NewSequencer() *Sequencer { + key, err := registry.OpenKey(registry.CURRENT_USER, "Volatile Environment", flags) + if err != nil { + seqInitErrors.Add(err.Error(), 1) + return &Sequencer{key: invalidKey, quit: make(chan struct{}, 1)} + } + s := &Sequencer{ + key: key, + quit: make(chan struct{}, 1), + seq: uint64(0), + } + s.seq, _, _ = key.GetIntegerValue(seqVName) + + go s.store() + + return s +} + +// Store saves the current sequence value in the registry. +func (s *Sequencer) Store() error { + if s.key == invalidKey { + // try to open the key again + var err error + s.key, err = registry.OpenKey(registry.CURRENT_USER, "Volatile Environment", flags) + if err != nil { + return errInvalidVolatileKey + } + } + nextSeq := s.Get() + prevSeq, _, err := s.key.GetIntegerValue(seqVName) + if err == nil && nextSeq < prevSeq { + return fmt.Errorf("current sequence number %d is lower than registry value %d", nextSeq, prevSeq) + } + return s.key.SetQWordValue(seqVName, nextSeq) +} + +// Increment increments the sequence number atomically. +func (s *Sequencer) Increment() { + atomic.AddUint64(&s.seq, 1) +} + +// Get returns the current sequence number. +func (s *Sequencer) Get() uint64 { + return atomic.LoadUint64(&s.seq) +} + +// Reset removes the sequence value from the registry and sets the sequence number to zero. +func (s *Sequencer) Reset() error { + atomic.StoreUint64(&s.seq, 0) + if s.key == invalidKey { + return errInvalidVolatileKey + } + return s.key.DeleteValue(seqVName) +} + +// Close shutdowns the event sequencer. +func (s *Sequencer) Close() error { + s.quit <- struct{}{} + return s.key.Close() +} + +// store periodically dumps the sequence number into registry value. +func (s *Sequencer) store() { + ticker := time.NewTicker(time.Second * 5) + for { + select { + case <-ticker.C: + if err := s.Store(); err != nil { + seqStoreErrors.Add(1) + } + case <-s.quit: + ticker.Stop() + return + } + } +} diff --git a/pkg/kevent/sequencer_test.go b/pkg/kevent/sequencer_test.go new file mode 100644 index 000000000..bb17ab64c --- /dev/null +++ b/pkg/kevent/sequencer_test.go @@ -0,0 +1,60 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kevent + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestSequencer(t *testing.T) { + sequencer := NewSequencer() + defer sequencer.Close() + + for i := 0; i < 10; i++ { + sequencer.Increment() + } + assert.Equal(t, uint64(10), sequencer.Get()) + require.NoError(t, sequencer.Store()) + + sequencer = NewSequencer() + defer sequencer.Close() + assert.Equal(t, uint64(10), sequencer.Get()) + + require.NoError(t, sequencer.Reset()) + assert.Equal(t, uint64(0), sequencer.Get()) +} + +func TestSequencerMonotonic(t *testing.T) { + sequencer := NewSequencer() + defer sequencer.Close() + + for i := 0; i < 10; i++ { + sequencer.Increment() + } + require.NoError(t, sequencer.Store()) + + sequencer = NewSequencer() + defer sequencer.Close() + sequencer.seq = uint64(0) + + require.Error(t, sequencer.Store()) + require.NoError(t, sequencer.Reset()) +} diff --git a/pkg/kstream/README.md b/pkg/kstream/README.md new file mode 100644 index 000000000..8b73224ce --- /dev/null +++ b/pkg/kstream/README.md @@ -0,0 +1 @@ +### package `kstream` \ No newline at end of file diff --git a/pkg/kstream/_fixtures/snapshots/create-process.gob b/pkg/kstream/_fixtures/snapshots/create-process.gob new file mode 100644 index 0000000000000000000000000000000000000000..bf4a1b39c927c17cc871d8ea2b5dd8e23d8dad04 GIT binary patch literal 878 zcmZuvO=}Zj5T4m=c2i@&@KW&ZRg5A95vxhmV4$U@_8=aX>?B#5?1p#W#zeeWzrU+@ zui{_OgLv}XgCO`9`~x0J#(Cd-7}3CnnRjNMdFGkneZH4L%$Iu<>rJXBDn#A`^wGOY zK}ZVu0pR?l-A02{F^M(xHS!C<<#JzB98y>cbWluoV~rl*!d6Pkyi_S}>XvyrI-7{R z515-InQ-1X6JQSj_%q+1GMeF#NB4SkhfK|p(Q|FkwQlxuJ7}gRFmK1$P)V1R9+;P0 zt)*m+O365L5~)*~j=fscp;QN*uEmsCFOq>J$d3RQEmVopx{A6w5gnch{um%prWW~$ zaJg95Q7_ViQ60cJekkKZv3RCO!)H-9Z09N=hBIa(c-r9{~y=ohH zxCGGPrvmz4PCh!LdHfrs(kO`ys9$UaaZGL7ph~@w<*j$?NoX~5YeBkWGq2I$w%NMT z{sKRjHcRE&_P+~vaA7)qMHb&jekJ-=lV;P3X6je?rR86k@Rtk^U4V#R3-EEP>1cVg zS5eW_v6qp>xlNLL=uv3(@+E7sWxpE5J3&)ae=BW`q?2>-jkBSZ9pNOukxqXlJAYhr z1kyo?BB9v>K%4gv`KvR`tU1FIAr6<<@E3mj`EmQ~u>#`Z`Q*a^4u@F@9AJ*$@eklH n>-?QR-34^q3_s*fa2rc*x_<;O) literal 0 HcmV?d00001 diff --git a/pkg/kstream/controller.go b/pkg/kstream/controller.go new file mode 100644 index 000000000..032dda6dd --- /dev/null +++ b/pkg/kstream/controller.go @@ -0,0 +1,347 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kstream + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/config" + kerrors "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/syscall/etw" + log "github.com/sirupsen/logrus" + "golang.org/x/sys/windows/registry" + "runtime" + "time" + "unsafe" +) + +const ( + ktraceSession = etw.KernelLoggerSession + krundownSession = etw.KernelLoggerRundownSession + // maxBufferSize specifies the maximum buffer size for event tracing session buffer + maxBufferSize = 1024 + // etwMaxLoggersPath is the registry subkey that contains ETW logger preferences + etwMaxLoggersPath = `SYSTEM\CurrentControlSet\Control\WMI` + // etwMaxLoggersValue is the registry value that dictates the maximum number of loggers. Default value is 64 on most systems + etwMaxLoggersValue = "EtwMaxLoggers" + maxStringLen = 1024 +) + +// for testing purposes +var ( + startTrace = etw.StartTrace + controlTrace = etw.ControlTrace + enableTrace = etw.EnableTrace +) + +// KtraceController is responsible for managing the life cycle of the kernel traces. +type KtraceController interface { + // StartKtrace starts a new kernel tracing session. + StartKtrace() error + // CloseKtrace stops currently running kernel trace session. + CloseKtrace() error + // StartKtraceRundown initiates the kernel logger rundown session that will enumerate open file objects + // we can use to match file names in file system kernel events. + StartKtraceRundown() error + // IsKRundownStarted indicates if kernel logger rundown session is started. + IsKRundownStarted() bool + // GetTraceHandle returns the handle of the kernel trace session. + GetTraceHandle() etw.TraceHandle +} + +// ktraceController implements KtraceController. +type ktraceController struct { + // kstreamConfig stores kernel stream specific settings + kstreamConfig config.KstreamConfig + // handle is the pointer to kernel trace handle + handle etw.TraceHandle + rundownHandle etw.TraceHandle + // props is the pointer to event trace properties descriptor + props *etw.EventTraceProperties + rundownProps *etw.EventTraceProperties + krundownStarted bool +} + +// NewKtraceController spins up a new instance of kernel trace controller. +func NewKtraceController(kstreamConfig config.KstreamConfig) KtraceController { + return &ktraceController{kstreamConfig: kstreamConfig} +} + +// StartKtrace starts a new kernel tracing session. User has the ability to disable +// a specific subset of collected kernel events, even though by default most events +// are forwarded from the provider. +func (k *ktraceController) StartKtrace() error { + // at least process events have to be enabled + // for the purpose of building the state machine + flags := etw.Process + if k.kstreamConfig.EnableThreadKevents { + flags |= etw.Thread + } + if k.kstreamConfig.EnableImageKevents { + flags |= etw.ImageLoad + } + if k.kstreamConfig.EnableNetKevents { + flags |= etw.NetTCPIP + } + if k.kstreamConfig.EnableRegistryKevents { + flags |= etw.Registry + } + if k.kstreamConfig.EnableFileIOKevents { + flags |= etw.DiskFileIO | etw.FileIO | etw.FileIOInit + } + + bufferSize := k.kstreamConfig.BufferSize + if bufferSize > maxBufferSize { + bufferSize = maxBufferSize + } + // validate min/max buffers. The minimal + // number of buffers is 2 per CPU logical core + minBuffers := k.kstreamConfig.MinBuffers + if minBuffers < uint32(runtime.NumCPU()*2) { + minBuffers = uint32(runtime.NumCPU() * 2) + } + maxBuffers := k.kstreamConfig.MaxBuffers + maxBuffersAllowed := minBuffers + 20 + if maxBuffers > maxBuffersAllowed { + maxBuffers = maxBuffersAllowed + } + if minBuffers > maxBuffers { + minBuffers = maxBuffers + } + + flushTimer := k.kstreamConfig.FlushTimer + if flushTimer < time.Second { + flushTimer = time.Second + } + + props := &etw.EventTraceProperties{ + Wnode: etw.WnodeHeader{ + BufferSize: uint32(unsafe.Sizeof(etw.EventTraceProperties{})) + 2*maxStringLen, + Flags: etw.WnodeTraceFlagGUID, + GUID: etw.KernelTraceControlGUID, + }, + LoggerNameOffset: uint32(unsafe.Sizeof(etw.EventTraceProperties{})), + LogFileNameOffset: 0, + EnableFlags: flags, + BufferSize: bufferSize, + LogFileMode: etw.ProcessTraceModeRealtime, + MinimumBuffers: minBuffers, + MaximumBuffers: maxBuffers, + FlushTimer: uint32(flushTimer.Seconds()), + } + log.Debugf("starting kernel trace with %q event flags", flags) + handle, err := startTrace( + ktraceSession, + props, + ) + + if err == nil { + if !handle.IsValid() { + return kerrors.ErrInvalidTrace + } + handleCopy := handle + // poorly documented ETW feature that allows for enabling an extended set of + // kernel event tracing flags. According to the MSDN documentation, aside from + // invoking `EventTraceProperties` function to enable object manager tracking + // the `EventTraceProperties` structure's `EnableFlags` member needs to be set to PERF_OB_HANDLE (0x80000040). + // This actually results in an erroneous trace start. The documentation neither specifies how the function + // should be called (group mask array with its 4th element set to 0x80000040). + sysTraceFlags := make([]etw.EventTraceFlags, 8) + // when we call the`TraceSetInformation` with event empty group mask reserved for the + // flags that are bitvectored into `EventTraceProperties` structure's `EnableFlags` field, + // it will trigger the arrival of rundown events including open file objects and + // registry keys that are very valuable for us to construct the initial snapshot of + // these system resources and let us build the event's context + if err := etw.SetTraceInformation(handle, etw.TraceSystemTraceEnableFlagsInfo, sysTraceFlags); err != nil { + // enable rundown kernel logger to at least enumerate open file objects + if err := k.StartKtraceRundown(); err != nil { + log.Warn(err) + } + } + sysTraceFlags[0] = flags + // enable object manager tracking + if k.kstreamConfig.EnableHandleKevents { + sysTraceFlags[4] = etw.Handle + } + // call again to enable all kernel events. Just to recap. The first call to `TraceSetInformation` with empty + // group masks activates rundown events, while this second call enables the rest of the kernel events specified in flags. + if err := etw.SetTraceInformation(handle, etw.TraceSystemTraceEnableFlagsInfo, sysTraceFlags); err != nil { + log.Warnf("unable to set trace information: %v", err) + } + + k.handle = handleCopy + k.props = props + + return nil + } + + switch err { + case kerrors.ErrTraceAlreadyRunning: + if err := controlTrace(etw.TraceHandle(0), ktraceSession, props, etw.Query); err == kerrors.ErrKsessionNotRunning { + return kerrors.ErrCannotUpdateTrace + } + if err := controlTrace(etw.TraceHandle(0), ktraceSession, props, etw.Stop); err != nil { + return kerrors.ErrStopTrace + } + + time.Sleep(time.Millisecond * 100) + props := &etw.EventTraceProperties{ + Wnode: etw.WnodeHeader{ + BufferSize: uint32(unsafe.Sizeof(etw.EventTraceProperties{})) + 2*maxStringLen, + Flags: etw.WnodeTraceFlagGUID, + GUID: etw.KernelTraceControlGUID, + }, + LoggerNameOffset: uint32(unsafe.Sizeof(etw.EventTraceProperties{})), + LogFileNameOffset: 0, + EnableFlags: flags, + BufferSize: bufferSize, + LogFileMode: etw.ProcessTraceModeRealtime, + MinimumBuffers: minBuffers, + MaximumBuffers: maxBuffers, + FlushTimer: uint32(flushTimer.Seconds()), + } + handle, err := startTrace( + ktraceSession, + props, + ) + if err != nil { + return kerrors.ErrRestartTrace + } + if !handle.IsValid() { + return kerrors.ErrInvalidTrace + } + handleCopy := handle + + sysTraceFlags := make([]etw.EventTraceFlags, 8) + if err := etw.SetTraceInformation(handle, etw.TraceSystemTraceEnableFlagsInfo, sysTraceFlags); err != nil { + if err := k.StartKtraceRundown(); err != nil { + log.Warn(err) + } + } + sysTraceFlags[0] = flags + // enable object manager tracking + if k.kstreamConfig.EnableHandleKevents { + sysTraceFlags[4] = etw.Handle + } + // call again to enable all kernel events. Just to recap. The first call to `TraceSetInformation` with empty + // group masks activates rundown events, while this second call enables the rest of the kernel events specified in flags. + if err := etw.SetTraceInformation(handle, etw.TraceSystemTraceEnableFlagsInfo, sysTraceFlags); err != nil { + log.Warnf("unable to set trace information: %v", err) + } + + k.handle = handleCopy + k.props = props + return nil + + case kerrors.ErrTraceNoSysResources: + // get the number of maximum allowed loggers from registry + key, err := registry.OpenKey(registry.LOCAL_MACHINE, etwMaxLoggersPath, registry.QUERY_VALUE) + if err != nil { + return err + } + defer key.Close() + v, _, err := key.GetIntegerValue(etwMaxLoggersValue) + if err != nil { + return err + } + return fmt.Errorf(`the limit for logging sessions on your system is %d. Please consider increasing this number `+ + `by editing HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\WMI\EtwMaxLoggers key in registry. `+ + `Permissible values are 32 through 256 inclusive, and a reboot is required for any change to take effect`, v) + + default: + return err + } +} + +// StartKtraceRundown initiates the kernel logger rundown session that will enumerate open file objects +// we can use to match file names in file system kernel events. +func (k *ktraceController) StartKtraceRundown() error { + props := &etw.EventTraceProperties{ + Wnode: etw.WnodeHeader{ + BufferSize: uint32(unsafe.Sizeof(etw.EventTraceProperties{})) + 2*maxStringLen, + Flags: etw.WnodeTraceFlagGUID, + }, + LoggerNameOffset: uint32(unsafe.Sizeof(etw.EventTraceProperties{})), + LogFileNameOffset: 0, + BufferSize: maxBufferSize, + LogFileMode: etw.ProcessTraceModeRealtime, + } + k.rundownProps = props + handle, err := startTrace( + krundownSession, + props, + ) + if err == kerrors.ErrTraceAlreadyRunning { + if err := controlTrace(handle, krundownSession, props, etw.Stop); err != nil { + return kerrors.ErrStopTrace + } + props := &etw.EventTraceProperties{ + Wnode: etw.WnodeHeader{ + BufferSize: uint32(unsafe.Sizeof(etw.EventTraceProperties{})) + 2*maxStringLen, + Flags: etw.WnodeTraceFlagGUID, + }, + LoggerNameOffset: uint32(unsafe.Sizeof(etw.EventTraceProperties{})), + LogFileNameOffset: 0, + BufferSize: maxBufferSize, + LogFileMode: etw.ProcessTraceModeRealtime, + } + handle, err = startTrace( + krundownSession, + props, + ) + if err != nil { + return kerrors.ErrRestartTrace + } + } + if err := enableTrace(etw.KernelRundownGUID, handle, 0x10); err != nil { + return fmt.Errorf("couldn't activate kernel rundown logger: %v", err) + } + k.rundownHandle = handle + k.krundownStarted = true + + return nil +} + +// IsKRundownStarted indicates if kernel logger rundown session is started. +func (k *ktraceController) IsKRundownStarted() bool { + return k.krundownStarted +} + +// CloseKtrace stops currently running kernel trace session. +func (k *ktraceController) CloseKtrace() error { + // flush pending event buffers + if err := controlTrace(k.handle, ktraceSession, k.props, etw.Flush); err != nil { + log.Warnf("couldn't flush kernel trace session: %v", err) + } + time.Sleep(time.Millisecond * 100) + if err := controlTrace(k.handle, ktraceSession, k.props, etw.Stop); err != nil { + return fmt.Errorf("couldn't stop kernel trace session: %v", err) + } + if k.rundownHandle.IsValid() { + if err := controlTrace(k.rundownHandle, krundownSession, k.rundownProps, etw.Stop); err != nil { + log.Warn(err) + } + } + k.krundownStarted = false + return nil +} + +// GetTraceHandle returns the handle of the kernel trace session. +func (k *ktraceController) GetTraceHandle() etw.TraceHandle { + return k.handle +} diff --git a/pkg/kstream/controller_test.go b/pkg/kstream/controller_test.go new file mode 100644 index 000000000..28acd8870 --- /dev/null +++ b/pkg/kstream/controller_test.go @@ -0,0 +1,77 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kstream + +import ( + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/syscall/etw" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestStartKtraceSuccess(t *testing.T) { + startTrace = func(name string, flags *etw.EventTraceProperties) (etw.TraceHandle, error) { + return etw.TraceHandle(1), nil + } + + ktracec := NewKtraceController(config.KstreamConfig{ + EnableThreadKevents: true, + EnableNetKevents: true, + BufferSize: 1024, + FlushTimer: time.Millisecond * 2300, + }) + + err := ktracec.StartKtrace() + + require.NoError(t, err) + assert.Equal(t, etw.TraceHandle(1), ktracec.(*ktraceController).handle) + assert.Equal(t, etw.EventTraceFlags(0x10003), ktracec.(*ktraceController).props.EnableFlags) + assert.Equal(t, "TCPIP, Process, Thread", ktracec.(*ktraceController).props.EnableFlags.String()) + +} + +func TestStartKtraceNoSysResources(t *testing.T) { + + startTrace = func(name string, props *etw.EventTraceProperties) (etw.TraceHandle, error) { + return etw.TraceHandle(0), errors.ErrTraceNoSysResources + } + + ktracec := NewKtraceController(config.KstreamConfig{EnableThreadKevents: true, BufferSize: 1024}) + + err := ktracec.StartKtrace() + + require.Error(t, err) +} + +func TestStartKtraceBufferTooSmall(t *testing.T) { + + startTrace = func(name string, props *etw.EventTraceProperties) (etw.TraceHandle, error) { + return etw.TraceHandle(0), nil + } + + ktracec := NewKtraceController(config.KstreamConfig{EnableThreadKevents: true, BufferSize: 124}) + + err := ktracec.StartKtrace() + + require.Error(t, err) + assert.EqualError(t, err, "buffer size of 124 KB is too small") +} diff --git a/pkg/kstream/doc.go b/pkg/kstream/doc.go new file mode 100644 index 000000000..56cb2d362 --- /dev/null +++ b/pkg/kstream/doc.go @@ -0,0 +1,21 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package kstream contains facilities for controlling the kernel logger session and opening kernel event stream +// for the purpose of collecting and processing kernel events. +package kstream diff --git a/pkg/kstream/interceptors/chain.go b/pkg/kstream/interceptors/chain.go new file mode 100644 index 000000000..7c8d4c552 --- /dev/null +++ b/pkg/kstream/interceptors/chain.go @@ -0,0 +1,128 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package interceptors + +import ( + "expvar" + "fmt" + "github.com/rabbitstack/fibratus/pkg/config" + kerrors "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/fs" + "github.com/rabbitstack/fibratus/pkg/handle" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/util/multierror" + "github.com/rabbitstack/fibratus/pkg/yara" + log "github.com/sirupsen/logrus" +) + +// interceptorFailures counts the number of failures caused by interceptors while processing kernel events +var interceptorFailures = expvar.NewInt("kevent.interceptor.failures") + +// Chain defines the method that all chan interceptors have to satisfy. +type Chain interface { + // Dispatch pushes a kernel event into interceptor chain. Interceptors are applied sequentially, so we have to make + // sure that any interceptor providing additional context to the next interceptor is defined first in the chain. If + // one interceptor fails, the next interceptor in chain is invoked. + Dispatch(kevt *kevent.Kevent) (*kevent.Kevent, error) +} + +type chain struct { + interceptors []KstreamInterceptor +} + +// NewChain constructs the interceptor chain. It arranges all the interceptors according to enabled kernel event categories. +func NewChain(psnap ps.Snapshotter, hsnap handle.Snapshotter, rundownFn func() error, config *config.Config) Chain { + var ( + chain = &chain{interceptors: make([]KstreamInterceptor, 0)} + devMapper = fs.NewDevMapper() + scanner yara.Scanner + ) + + if config.Yara.Enabled { + var err error + scanner, err = yara.NewScanner(psnap, config.Yara) + if err != nil { + log.Warnf("unable to start YARA scanner: %v", err) + } + } + + chain.addInterceptor(newPsInterceptor(psnap, scanner)) + + if config.Kstream.EnableFileIOKevents { + chain.addInterceptor(newFsInterceptor(devMapper, hsnap, config, rundownFn)) + } + if config.Kstream.EnableRegistryKevents { + chain.addInterceptor(newRegistryInterceptor(hsnap)) + } + if config.Kstream.EnableImageKevents { + chain.addInterceptor(newImageInterceptor(psnap, devMapper, scanner)) + } + if config.Kstream.EnableNetKevents { + chain.addInterceptor(newNetInterceptor()) + } + if config.Kstream.EnableHandleKevents { + chain.addInterceptor(newHandleInterceptor(hsnap, handle.NewObjectTypeStore(), devMapper)) + } + + return chain +} + +func (c *chain) addInterceptor(interceptor KstreamInterceptor) { + if interceptor == nil { + return + } + c.interceptors = append(c.interceptors, interceptor) +} + +// Dispatch pushes a kernel event into interceptor chain. Interceptors are applied sequentially, so we have to make +// sure that any interceptor providing additional context to the next interceptor is defined first in the chain. If +// one interceptor fails, the next interceptor in chain is invoked. +func (c chain) Dispatch(kevt *kevent.Kevent) (*kevent.Kevent, error) { + var errs = make([]error, 0) + var cukerr error + + for _, interceptor := range c.interceptors { + var err error + var next bool + kevt, next, err = interceptor.Intercept(kevt) + if err != nil { + if !kerrors.IsCancelUpstreamKevent(err) { + interceptorFailures.Add(1) + errs = append(errs, fmt.Errorf("%q interceptor failed with error: %v", interceptor.Name(), err)) + continue + } else { + cukerr = err + } + } + if !next { + break + } + } + + if len(errs) > 0 { + return kevt, multierror.Wrap(errs) + } + + if cukerr != nil { + return kevt, cukerr + } + + return kevt, nil +} diff --git a/pkg/kstream/interceptors/fs.go b/pkg/kstream/interceptors/fs.go new file mode 100644 index 000000000..778243fd5 --- /dev/null +++ b/pkg/kstream/interceptors/fs.go @@ -0,0 +1,450 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package interceptors + +import ( + "expvar" + "github.com/rabbitstack/fibratus/pkg/config" + kerrors "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/fs" + "github.com/rabbitstack/fibratus/pkg/handle" + htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + log "github.com/sirupsen/logrus" + "sync" + "time" +) + +var ( + // totalRundownFiles counts the number of opened files + totalRundownFiles = expvar.NewInt("fs.total.rundown.files") + // fileObjectMisses computes file object cache misses + fileObjectMisses = expvar.NewInt("fs.file.objects.misses") + fileObjectHandleHits = expvar.NewInt("fs.file.object.handle.hits") + fileReleaseCount = expvar.NewInt("fs.file.releases") + rundownDeadlinePeriod = time.Minute + once sync.Once +) + +type fsInterceptor struct { + // files stores the file metadata indexed by file object + files map[uint64]*fileInfo + // devMapper translates DOS device names to regular drive letters + devMapper fs.DevMapper + rundownDeadline *time.Timer + hsnap handle.Snapshotter + config *config.Config + + pendingKevents map[uint64]*kevent.Kevent +} + +type fileInfo struct { + name string + typ fs.FileType +} + +// fileInfoClass contains the values that specify which structure to use to query or set information for a file object. +// For more information see https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ne-wdm-_file_information_class +var fileInfoClasses = map[uint32]string{ + 1: "Directory", + 2: "Full Directory", + 3: "Both Directory", + 4: "Basic", + 5: "Standard", + 6: "Internal", + 7: "EA", + 8: "Access", + 9: "Name", + 10: "Rename", + 11: "Link", + 12: "Names", + 13: "Disposition", + 14: "Position", + 15: "Full EA", + 16: "Mode", + 17: "Alignment", + 18: "All", + 19: "Allocation", + 20: "EOF", + 21: "Alternative Name", + 22: "Stream", + 23: "Pipe", + 24: "Pipe Local", + 25: "Pipe Remote", + 26: "Mailslot Query", + 27: "Mailslot Set", + 28: "Compression", + 29: "Object ID", + 30: "Completion", + 31: "Move Cluster", + 32: "Quota", + 33: "Reparse Point", + 34: "Network Open", + 35: "Attribute Tag", + 36: "Tracking", + 37: "ID Both Directory", + 38: "ID Full Directory", + 39: "Valid Data Length", + 40: "Short Name", + 41: "IO Completion Notification", + 42: "IO Status Block Range", + 43: "IO Priority Hint", + 44: "Sfio Reserve", + 45: "Sfio Volume", + 46: "Hard Link", + 47: "Process IDS Using File", + 48: "Normalized Name", + 49: "Network Physical Name", + 50: "ID Global Tx Directory", + 51: "Is Remote Device", + 52: "Unused", + 53: "Numa Node", + 54: "Standard Link", + 55: "Remote Protocol", + 56: "Rename Bypass Access Check", + 57: "Link Bypass Access Check", + 58: "Volume Name", + 59: "ID", + 60: "ID Extended Directory", + 61: "Replace Completion", + 62: "Hard Link Full ID", + 63: "ID Extended Both Directory", + 64: "Disposition Extended", + 65: "Rename Extended", + 66: "Rename Extended Bypass Access Check", + 67: "Desired Storage", + 68: "Stat", + 69: "Memory Partition", + 70: "Stat LX", + 71: "Case Sensitive", + 72: "Link Extended", + 73: "Link Extended Bypass Access Check", + 74: "Storage Reserve ID", + 75: "Case Sensitive Force Access Check", +} + +func infoClassFromID(klass uint32) string { + class, ok := fileInfoClasses[klass] + if !ok { + return "Unknown" + } + return class +} + +func newFsInterceptor(devMapper fs.DevMapper, hsnap handle.Snapshotter, config *config.Config, fn func() error) KstreamInterceptor { + interceptor := &fsInterceptor{ + files: make(map[uint64]*fileInfo, 0), + pendingKevents: make(map[uint64]*kevent.Kevent, 0), + devMapper: devMapper, + rundownDeadline: time.NewTimer(rundownDeadlinePeriod), + hsnap: hsnap, + config: config, + } + + // define a rundown deadline timer that will call into provided + // function if we didn't receive any rundown within the deadline + go func() { + <-interceptor.rundownDeadline.C + if fn != nil { + if err := fn(); err != nil { + log.Errorf("couldn't start kernel rundown session after deadline: %v", err) + } + } + }() + + return interceptor +} + +func (f *fsInterceptor) Intercept(kevt *kevent.Kevent) (*kevent.Kevent, bool, error) { + switch kevt.Type { + case ktypes.FileRundown, + ktypes.FileOpEnd, + ktypes.CreateFile, + ktypes.DeleteFile, + ktypes.CloseFile, + ktypes.WriteFile, + ktypes.ReadFile, + ktypes.RenameFile, + ktypes.ReleaseFile, + ktypes.SetFileInformation, + ktypes.EnumDirectory: + + fobj, err := kevt.Kparams.GetHexAsUint64(kparams.FileObject) + if err != nil && kevt.Type != ktypes.FileOpEnd { + return kevt, true, err + } + // when the file rundown event comes in we store the file info + // in the map in order to augment the rest of file events + // that lack the file name field + if kevt.Type == ktypes.FileRundown { + // cancel rundown deadline timer + once.Do(func() { + f.rundownDeadline.Stop() + }) + filename, err := kevt.Kparams.GetString(kparams.FileName) + if err != nil { + return kevt, true, err + } + totalRundownFiles.Add(1) + filename = f.devMapper.Convert(filename) + + f.files[fobj] = &fileInfo{name: filename, typ: fs.GetFileType(filename, 0)} + + return kevt, false, nil + } + // we'll update event's thread identifier with the one + // that's involved in the file system operation + kevt.Tid, err = kevt.Kparams.GetUint32(kparams.ThreadID) + if err != nil { + // tid is sometimes represented in hex format + kevt.Tid, err = kevt.Kparams.GetHexAsUint32(kparams.ThreadID) + } + + switch kevt.Type { + case ktypes.CreateFile: + // we defer the processing of the CreateFile event until we get + // the matching FileOpEnd event. This event contains the operation + // that was done on behalf of the file, e.g. create or open. + irp, err := kevt.Kparams.GetHexAsUint64(kparams.FileIrpPtr) + if err != nil { + return kevt, true, err + } + f.pendingKevents[irp] = kevt + return kevt, false, kerrors.ErrCancelUpstreamKevent + + case ktypes.FileOpEnd: + // get the CreateFile pending event by IRP identifier + irp, err := kevt.Kparams.GetHexAsUint64(kparams.FileIrpPtr) + if err != nil { + return kevt, true, err + } + fkevt, ok := f.pendingKevents[irp] + if !ok { + return kevt, true, kerrors.ErrCancelUpstreamKevent + } + extraInfo, err := kevt.Kparams.GetHexAsUint8(kparams.FileExtraInfo) + if err != nil { + return kevt, true, err + } + fkevt.Kparams.Append(kparams.FileExtraInfo, kparams.Uint8, extraInfo) + + delete(f.pendingKevents, irp) + + if err := f.processCreateFile(fkevt); err != nil { + return kevt, true, err + } + return fkevt, false, nil + + case ktypes.ReleaseFile: + fileReleaseCount.Add(1) + // delete both, the file object and the file key from files map + fileKey, err := kevt.Kparams.GetHexAsUint64(kparams.FileKey) + if err == nil { + delete(f.files, fileKey) + } + delete(f.files, fobj) + + case ktypes.DeleteFile, ktypes.RenameFile, + ktypes.CloseFile, ktypes.ReadFile, + ktypes.WriteFile, ktypes.SetFileInformation, ktypes.EnumDirectory: + + fileKey, err := kevt.Kparams.GetHexAsUint64(kparams.FileKey) + if err != nil { + return kevt, true, err + } + + // attempt to get the file by file key. If there is no such file referenced + // by the file key, then try to fetch it by file object. Even if file object + // references fails, we search in the file handles for such file + fileinfo, ok := f.files[fileKey] + if !ok { + fileinfo, ok = f.files[fobj] + if !ok { + // look in the system handles for file objects + var h htypes.Handle + h, ok = f.hsnap.FindByObject(fobj) + if ok && h.Type == handle.File { + fileObjectHandleHits.Add(1) + f.files[fobj] = &fileInfo{name: h.Name, typ: fs.GetFileType(h.Name, 0)} + } + } + } + + // ignore object misses that are produced by CloseFile + if !ok && kevt.Type != ktypes.CloseFile { + fileObjectMisses.Add(1) + } + + if kevt.Type == ktypes.DeleteFile { + delete(f.files, fobj) + } + + if kevt.Type == ktypes.SetFileInformation || kevt.Type == ktypes.EnumDirectory || kevt.Type == ktypes.RenameFile || kevt.Type == ktypes.DeleteFile { + // assign a human-readable information class from the class ID + infoClassID, err := kevt.Kparams.GetUint32(kparams.FileInfoClass) + if err == nil { + kevt.Kparams.Remove(kparams.FileInfoClass) + kevt.Kparams.Append(kparams.FileInfoClass, kparams.AnsiString, infoClassFromID(infoClassID)) + } + kevt.Kparams.Remove(kparams.FileExtraInfo) + } + + if kevt.Type == ktypes.EnumDirectory { + // the file key kparam contains the reference to the directory name + fileKey, err := kevt.Kparams.GetHexAsUint64(kparams.FileKey) + if err != nil { + kevt.Kparams.Remove(kparams.FileKey) + removeKparams(kevt) + return kevt, true, nil + } + kevt.Kparams.Remove(kparams.FileKey) + fileinfo, ok := f.files[fileKey] + if ok && fileinfo != nil { + kevt.Kparams.Append(kparams.FileDirectory, kparams.UnicodeString, fileinfo.name) + } + } + + removeKparams(kevt) + + if err := f.appendKparams(fileinfo, kevt); err != nil { + return kevt, true, err + } + } + default: + return kevt, true, nil + } + + return kevt, true, nil +} + +// removeKparams removes unwanted kparams, either because they are already present in kevent +// canonical attributes or are not very useful. +func removeKparams(kevt *kevent.Kevent) { + if kevt.Kparams.Contains(kparams.ProcessID) { + kevt.Kparams.Remove(kparams.ProcessID) + } + if kevt.Kparams.Contains(kparams.ThreadID) { + kevt.Kparams.Remove(kparams.ThreadID) + } + if kevt.Kparams.Contains(kparams.FileCreateOptions) { + kevt.Kparams.Remove(kparams.FileCreateOptions) + } + if kevt.Kparams.Contains(kparams.FileKey) { + kevt.Kparams.Remove(kparams.FileKey) + } +} + +func (fsInterceptor) Name() InterceptorType { return Fs } +func (f *fsInterceptor) getFileInfo(name string, opts uint32) *fileInfo { + return &fileInfo{name: name, typ: fs.GetFileType(name, opts)} +} + +func (f *fsInterceptor) processCreateFile(kevt *kevent.Kevent) error { + fobj, err := kevt.Kparams.GetHexAsUint64(kparams.FileObject) + if err != nil { + return err + } + + extraInfo, err := kevt.Kparams.GetUint8(kparams.FileExtraInfo) + if err != nil { + return err + } + + // delete raw event parameters + kevt.Kparams.Remove(kparams.FileExtraInfo) + // append human-readable file operation param + kevt.Kparams.Append(kparams.FileOperation, kparams.Enum, fs.FileDisposition(extraInfo)) + + filename, err := kevt.Kparams.GetString(kparams.FileName) + if err != nil { + return err + } + filename = f.devMapper.Convert(filename) + if err := kevt.Kparams.Set(kparams.FileName, filename, kparams.UnicodeString); err != nil { + return err + } + // figure out the file share mask that determines + // the type of share access that the caller thread + // would like to grant to other threads + mask, err := kevt.Kparams.GetUint32(kparams.FileShareMask) + if err != nil { + return err + } + if err := kevt.Kparams.Set(kparams.FileShareMask, fs.FileShareMode(mask), kparams.Enum); err != nil { + return err + } + // try to get extended file info. If the file object is already + // present in the map, we'll reuse the existing file information + fileinfo, ok := f.files[fobj] + if !ok { + opts, _ := kevt.Kparams.GetUint32(kparams.FileCreateOptions) + opts &= 0xFFFFFF + + fileinfo = f.getFileInfo(filename, opts) + // file type couldn't be resolved so we perform the lookup + // in system handles to determine whether file object is + // a directory + if fileinfo.typ == fs.Unknown { + fileinfo.typ = f.findDirHandle(fobj) + } + kevt.Kparams.Append(kparams.FileType, kparams.AnsiString, fileinfo.typ.String()) + + f.files[fobj] = fileinfo + } + + removeKparams(kevt) + + return f.appendKparams(fileinfo, kevt) +} + +func (f *fsInterceptor) findDirHandle(fobj uint64) fs.FileType { + fhandle, ok := f.hsnap.FindByObject(fobj) + if !ok || fhandle.Type != handle.File { + return fs.Unknown + } + if fhandle.MD == nil { + return fs.Unknown + } + md, ok := fhandle.MD.(*htypes.FileInfo) + if md.IsDirectory { + return fs.Directory + } + return fs.Unknown +} + +func (f *fsInterceptor) appendKparams(fileinfo *fileInfo, kevt *kevent.Kevent) error { + if kevt.Type == ktypes.EnumDirectory { + return nil + } + if fileinfo == nil { + if kevt.Type != ktypes.CreateFile { + kevt.Kparams.Append(kparams.FileName, kparams.UnicodeString, kparams.NA) + } + return nil + } + if fileinfo.typ != fs.Unknown { + kevt.Kparams.Append(kparams.FileType, kparams.AnsiString, fileinfo.typ.String()) + } + if kevt.Type != ktypes.CreateFile { + kevt.Kparams.Append(kparams.FileName, kparams.UnicodeString, fileinfo.name) + } + return nil +} diff --git a/pkg/kstream/interceptors/fs_test.go b/pkg/kstream/interceptors/fs_test.go new file mode 100644 index 000000000..2e2aa418e --- /dev/null +++ b/pkg/kstream/interceptors/fs_test.go @@ -0,0 +1,211 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package interceptors + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/fs" + "github.com/rabbitstack/fibratus/pkg/handle" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "os" + "testing" + "time" +) + +type devMapperMock struct { + mock.Mock +} + +func (dm *devMapperMock) Convert(filename string) string { + args := dm.Called(filename) + return args.String(0) +} + +func init() { + rundownDeadlinePeriod = time.Millisecond * 200 +} + +func TestCreateFile(t *testing.T) { + devMapper := new(devMapperMock) + hsnapMock := new(handle.SnapshotterMock) + + sysRoot := os.Getenv("SystemRoot") + devMapper.On("Convert", "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll").Return(fmt.Sprintf("%s\\system32\\user32.dll", sysRoot)) + + fsi := newFsInterceptor(devMapper, hsnapMock, &config.Config{}, nil) + + _, _, err := fsi.Intercept(&kevent.Kevent{ + Type: ktypes.FileRundown, + Tid: 2484, + PID: 859, + Kparams: kevent.Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + }, + }) + require.NoError(t, err) + + kevt1 := &kevent.Kevent{ + Type: ktypes.CreateFile, + Tid: 2484, + PID: 859, + Kparams: kevent.Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(18446738026482168384)}, + kparams.ThreadID: {Name: kparams.ThreadID, Type: kparams.Uint32, Value: uint32(1484)}, + kparams.FileCreateOptions: {Name: kparams.FileCreateOptions, Type: kparams.Uint32, Value: uint32(1223456)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll"}, + kparams.FileShareMask: {Name: kparams.FileShareMask, Type: kparams.Uint32, Value: uint32(5)}, + }, + } + devMapper.On("Convert", "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll").Return(fmt.Sprintf("%s\\system32\\kernel32.dll", sysRoot)) + + _, _, err = fsi.Intercept(kevt1) + require.NoError(t, err) + dispo, err := kevt1.Kparams.Get(kparams.FileOperation) + require.NoError(t, err) + assert.Equal(t, fs.Supersede, dispo.(fs.FileDisposition)) + filename, err := kevt1.Kparams.GetString(kparams.FileName) + require.NoError(t, err) + assert.Equal(t, fmt.Sprintf("%s\\system32\\kernel32.dll", sysRoot), filename) + assert.True(t, kevt1.Kparams.Contains(kparams.FileCreated)) + mask, err := kevt1.Kparams.GetString(kparams.FileShareMask) + assert.Equal(t, "r-d", mask) + + files := fsi.(*fsInterceptor).files + + require.Len(t, files, 2) + + fileinfo := files[18446738026482168384] + require.NotNil(t, fileinfo) + + assert.True(t, kevt1.Kparams.Contains(kparams.FileCreated)) + assert.Equal(t, fmt.Sprintf("%s\\system32\\kernel32.dll", sysRoot), fileinfo.name) + assert.Equal(t, fs.Regular, fileinfo.typ) + + typ, err := kevt1.Kparams.GetString(kparams.FileType) + require.NoError(t, err) + assert.Equal(t, "file", typ) +} + +func TestRundownFile(t *testing.T) { + devMapper := new(devMapperMock) + hsnapMock := new(handle.SnapshotterMock) + + sysRoot := os.Getenv("SystemRoot") + devMapper.On("Convert", "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll").Return(fmt.Sprintf("%s\\system32\\user32.dll", sysRoot)) + + fsi := newFsInterceptor(devMapper, hsnapMock, &config.Config{}, nil) + + _, _, err := fsi.Intercept(&kevent.Kevent{ + Type: ktypes.FileRundown, + Tid: 2484, + PID: 859, + Kparams: kevent.Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + }, + }) + require.NoError(t, err) + + files := fsi.(*fsInterceptor).files + + require.Len(t, files, 1) + + fileinfo := files[12456738026482168384] + require.NotNil(t, fileinfo) + + assert.Equal(t, fmt.Sprintf("%s\\system32\\user32.dll", sysRoot), fileinfo.name) + assert.Equal(t, fs.Regular, fileinfo.typ) +} + +func TestDeleteFile(t *testing.T) { + devMapper := new(devMapperMock) + hsnapMock := new(handle.SnapshotterMock) + + sysRoot := os.Getenv("SystemRoot") + devMapper.On("Convert", "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll").Return(fmt.Sprintf("%s\\system32\\user32.dll", sysRoot)) + + fsi := newFsInterceptor(devMapper, hsnapMock, &config.Config{}, nil) + + _, _, err := fsi.Intercept(&kevent.Kevent{ + Type: ktypes.FileRundown, + Tid: 2484, + PID: 859, + Kparams: kevent.Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + }, + }) + require.NoError(t, err) + + kevt := &kevent.Kevent{ + Type: ktypes.DeleteFile, + Tid: 2484, + PID: 859, + Kparams: kevent.Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.ThreadID: {Name: kparams.ThreadID, Type: kparams.Uint32, Value: uint32(1484)}, + }, + } + + files := fsi.(*fsInterceptor).files + require.Len(t, files, 1) + + _, _, err = fsi.Intercept(kevt) + require.NoError(t, err) + + require.Empty(t, files) + + filename, err := kevt.Kparams.GetString(kparams.FileName) + require.NoError(t, err) + + assert.Equal(t, fmt.Sprintf("%s\\system32\\user32.dll", sysRoot), filename) + typ, err := kevt.Kparams.GetString(kparams.FileType) + assert.Equal(t, "file", typ) + created, err := kevt.Kparams.GetTime(kparams.FileCreated) + require.NoError(t, err) + assert.NotEmpty(t, created) + attrs, err := kevt.Kparams.GetSlice(kparams.FileAttributes) + require.NoError(t, err) + require.Contains(t, attrs.([]fs.FileAttr), fs.FileArchive) +} + +func TestRundownFileDeadline(t *testing.T) { + devMapper := new(devMapperMock) + hsnapMock := new(handle.SnapshotterMock) + + sysRoot := os.Getenv("SystemRoot") + devMapper.On("Convert", "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll").Return(fmt.Sprintf("%s\\system32\\user32.dll", sysRoot)) + + ch := make(chan bool, 1) + fn := func() error { + ch <- true + return nil + } + + newFsInterceptor(devMapper, hsnapMock, &config.Config{}, fn) + + <-ch +} diff --git a/pkg/kstream/interceptors/handle.go b/pkg/kstream/interceptors/handle.go new file mode 100644 index 000000000..c8d9e4b5c --- /dev/null +++ b/pkg/kstream/interceptors/handle.go @@ -0,0 +1,162 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package interceptors + +import ( + "expvar" + kerrors "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/fs" + "github.com/rabbitstack/fibratus/pkg/handle" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + syshandle "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/rabbitstack/fibratus/pkg/syscall/registry" + "time" +) + +var ( + handleDeferEvictions = expvar.NewInt("handle.deferred.evictions") +) + +var joinElapsingPeriod = time.Second * 2 + +type handleInterceptor struct { + hsnap handle.Snapshotter + typeStore handle.ObjectTypeStore + devMapper fs.DevMapper + defers map[uint64]*kevent.Kevent + enqueue func(*kevent.Kevent) +} + +func newHandleInterceptor(hsnap handle.Snapshotter, typeStore handle.ObjectTypeStore, devMapper fs.DevMapper) KstreamInterceptor { + return &handleInterceptor{ + hsnap: hsnap, + typeStore: typeStore, + devMapper: devMapper, + defers: make(map[uint64]*kevent.Kevent), + } +} + +func (h *handleInterceptor) Intercept(kevt *kevent.Kevent) (*kevent.Kevent, bool, error) { + if kevt.Type == ktypes.CreateHandle || kevt.Type == ktypes.CloseHandle { + handleID, err := kevt.Kparams.GetHexAsUint32(kparams.HandleID) + if err == nil { + _ = kevt.Kparams.Set(kparams.HandleID, handleID, kparams.Uint32) + } + typeID, err := kevt.Kparams.GetUint16(kparams.HandleObjectTypeID) + if err != nil { + return kevt, true, err + } + object, err := kevt.Kparams.GetHexAsUint64(kparams.HandleObject) + if err != nil { + return kevt, true, err + } + // map object type identifier to its name. Query for object type if + // we didn't find in the object store + typeName := h.typeStore.FindByID(uint8(typeID)) + if typeName == "" { + rawHandle, err := kevt.Kparams.GetHexAsUint32(kparams.HandleID) + if err != nil { + return kevt, true, err + } + dup, err := handle.Duplicate(syshandle.Handle(rawHandle), kevt.PID, syshandle.AllAccess) + if err != nil { + return kevt, true, err + } + defer dup.Close() + typeName, err = handle.QueryType(dup) + if err != nil { + return kevt, true, err + } + h.typeStore.RegisterType(uint8(typeID), typeName) + } + + kevt.Kparams.Append(kparams.HandleObjectTypeName, kparams.AnsiString, typeName) + kevt.Kparams.Remove(kparams.HandleObjectTypeID) + // get the best possible object name according to its type + name, err := kevt.Kparams.GetString(kparams.HandleObjectName) + if err != nil { + return kevt, true, err + } + + switch typeName { + case handle.Key: + rootKey, keyName := handle.FormatKey(name) + if rootKey == registry.InvalidKey { + break + } + name = rootKey.String() + if keyName != "" { + name += "\\" + keyName + } + case handle.File: + name = h.devMapper.Convert(name) + } + + if kevt.Type == ktypes.CreateHandle { + // for some handle objects, the CreateHandle usually lacks the handle name + // but its counterpart CloseHandle kevent ships with the handle name. We'll + // defer emitting the CreateHandle kevent until we receive a CloseHandle targeting + // the same object + if name == "" && (typeName == handle.Key || typeName == handle.File || typeName == handle.Desktop) { + h.defers[object] = kevt + return kevt, false, kerrors.ErrCancelUpstreamKevent + } + return kevt, false, h.hsnap.Write(kevt) + } + if err := kevt.Kparams.Set(kparams.HandleObjectName, name, kparams.AnsiString); err != nil { + return kevt, true, err + } + + // at this point we hit CloseHandle kernel event and have waiting CreateHandle + // event reference. So we set handle object name to the name of its CloseHandle counterpart + if hkevt, ok := h.defers[object]; ok { + if err := hkevt.Kparams.Set(kparams.HandleObjectName, name, kparams.AnsiString); err != nil { + return kevt, true, err + } + kevt = hkevt + delete(h.defers, object) + err := h.hsnap.Write(hkevt) + if err != nil { + err = h.hsnap.Remove(kevt) + if err != nil { + return hkevt, false, err + } + } + return hkevt, false, h.hsnap.Remove(kevt) + } + // push pending CreateHandle kevents if they remained longer then expected. Possible + // cause could be that we lost the corresponding CloseHandle kernel event + for kobj, hkevt := range h.defers { + evict := kevt.Timestamp.Before(time.Now().Add(joinElapsingPeriod)) + if evict { + handleDeferEvictions.Add(1) + _ = kevt.Kparams.Set(kparams.HandleObjectName, kparams.NA, kparams.AnsiString) + kevt = hkevt + delete(h.defers, kobj) + } + } + return kevt, false, h.hsnap.Remove(kevt) + } + + return kevt, true, nil +} + +func (handleInterceptor) Name() InterceptorType { return Handle } diff --git a/pkg/kstream/interceptors/handle_test.go b/pkg/kstream/interceptors/handle_test.go new file mode 100644 index 000000000..15b8240af --- /dev/null +++ b/pkg/kstream/interceptors/handle_test.go @@ -0,0 +1,194 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package interceptors + +import ( + kerrors "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/handle" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +type objectTypeStoreMock struct { + mock.Mock +} + +func (s *objectTypeStoreMock) FindByID(id uint8) string { + args := s.Called(id) + return args.String(0) +} + +func (s *objectTypeStoreMock) TypeNames() []string { + args := s.Called() + return args.Get(0).([]string) +} + +func (s *objectTypeStoreMock) RegisterType(id uint8, typ string) {} + +func TestCloseHandle(t *testing.T) { + objectTypeStore := new(objectTypeStoreMock) + hsnapMock := new(handle.SnapshotterMock) + devMapper := new(devMapperMock) + + kevt := &kevent.Kevent{ + Type: ktypes.CloseHandle, + Tid: 2484, + PID: 859, + Kparams: kevent.Kparams{ + kparams.HandleObjectTypeID: {Name: kparams.HandleObjectTypeID, Type: kparams.Uint16, Value: uint16(23)}, + kparams.HandleObject: {Name: kparams.HandleObject, Type: kparams.HexInt64, Value: kparams.Hex("ffffd105e9baaf70")}, + kparams.HandleObjectName: {Name: kparams.HandleObjectName, Type: kparams.UnicodeString, Value: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`}, + }, + } + + hsnapMock.On("Remove", kevt).Return(nil) + + hi := newHandleInterceptor(hsnapMock, objectTypeStore, devMapper) + + objectTypeStore.On("FindByID", uint8(23)).Return(handle.Key) + + assert.Len(t, hi.(*handleInterceptor).defers, 0) + + _, _, err := hi.Intercept(kevt) + require.NoError(t, err) + + keyName, err := kevt.Kparams.GetString(kparams.HandleObjectName) + require.NoError(t, err) + typ, err := kevt.Kparams.GetString(kparams.HandleObjectTypeName) + require.NoError(t, err) + assert.Equal(t, handle.Key, typ) + assert.Equal(t, `HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, keyName) + +} + +func TestHandleCoalescing(t *testing.T) { + kevt := &kevent.Kevent{ + Type: ktypes.CreateHandle, + Tid: 2484, + PID: 859, + Kparams: kevent.Kparams{ + kparams.HandleObjectTypeID: {Name: kparams.HandleObjectTypeID, Type: kparams.Uint16, Value: uint16(23)}, + kparams.HandleObject: {Name: kparams.HandleObject, Type: kparams.HexInt64, Value: kparams.Hex("ffffd105e9baaf70")}, + kparams.HandleObjectName: {Name: kparams.HandleObjectName, Type: kparams.UnicodeString, Value: ""}, + }, + } + kevtsc := make(chan *kevent.Kevent, 1) + devMapper := new(devMapperMock) + + hsnapMock := new(handle.SnapshotterMock) + objectTypeStore := new(objectTypeStoreMock) + + hsnapMock.On("Write", mock.Anything).Return(nil) + hsnapMock.On("Remove", mock.Anything).Return(nil) + + hi := newHandleInterceptor(hsnapMock, objectTypeStore, devMapper) + + objectTypeStore.On("FindByID", uint8(23)).Return(handle.Key) + + _, _, err := hi.Intercept(kevt) + require.Error(t, err) + require.True(t, kerrors.IsCancelUpstreamKevent(err)) + + assert.Len(t, hi.(*handleInterceptor).defers, 1) + + kevt1 := &kevent.Kevent{ + Type: ktypes.CloseHandle, + Tid: 2484, + PID: 859, + Kparams: kevent.Kparams{ + kparams.HandleObjectTypeID: {Name: kparams.HandleObjectTypeID, Type: kparams.Uint16, Value: uint16(23)}, + kparams.HandleObject: {Name: kparams.HandleObject, Type: kparams.HexInt64, Value: kparams.Hex("ffffd105e9baaf70")}, + kparams.HandleObjectName: {Name: kparams.HandleObjectName, Type: kparams.UnicodeString, Value: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`}, + }, + } + + _, _, err = hi.Intercept(kevt1) + require.NoError(t, err) + + ckevt := <-kevtsc + keyName, err := ckevt.Kparams.GetString(kparams.HandleObjectName) + require.NoError(t, err) + assert.Equal(t, `HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, keyName) + + assert.Len(t, hi.(*handleInterceptor).defers, 0) +} + +func init() { + joinElapsingPeriod = time.Millisecond * 500 +} + +func TestHandleCoalescingWaiting(t *testing.T) { + kevt := &kevent.Kevent{ + Type: ktypes.CreateHandle, + Tid: 2484, + PID: 859, + Timestamp: time.Now(), + Kparams: kevent.Kparams{ + kparams.HandleObjectTypeID: {Name: kparams.HandleObjectTypeID, Type: kparams.Uint16, Value: uint16(23)}, + kparams.HandleObject: {Name: kparams.HandleObject, Type: kparams.HexInt64, Value: kparams.Hex("ffffd105e9baaf70")}, + kparams.HandleObjectName: {Name: kparams.HandleObjectName, Type: kparams.UnicodeString, Value: ""}, + }, + } + kevtsc := make(chan *kevent.Kevent, 1) + devMapper := new(devMapperMock) + objectTypeStore := new(objectTypeStoreMock) + hsnapMock := new(handle.SnapshotterMock) + + hsnapMock.On("Write", mock.Anything).Return(nil) + hsnapMock.On("Remove", mock.Anything).Return(nil) + + hi := newHandleInterceptor(hsnapMock, objectTypeStore, devMapper) + + objectTypeStore.On("FindByID", uint8(23)).Return(handle.Key) + + _, _, err := hi.Intercept(kevt) + require.Error(t, err) + require.True(t, kerrors.IsCancelUpstreamKevent(err)) + + assert.Len(t, hi.(*handleInterceptor).defers, 1) + + kevt1 := &kevent.Kevent{ + Type: ktypes.CloseHandle, + Tid: 2484, + PID: 859, + Kparams: kevent.Kparams{ + kparams.HandleObjectTypeID: {Name: kparams.HandleObjectTypeID, Type: kparams.Uint16, Value: uint16(23)}, + kparams.HandleObject: {Name: kparams.HandleObject, Type: kparams.HexInt64, Value: kparams.Hex("affdd155e9baaf70")}, + kparams.HandleObjectName: {Name: kparams.HandleObjectName, Type: kparams.UnicodeString, Value: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`}, + }, + } + + time.Sleep(time.Millisecond * 510) + + _, _, err = hi.Intercept(kevt1) + require.NoError(t, err) + + ckevt := <-kevtsc + keyName, err := ckevt.Kparams.GetString(kparams.HandleObjectName) + require.NoError(t, err) + assert.Equal(t, kparams.NA, keyName) + + assert.Len(t, hi.(*handleInterceptor).defers, 0) +} diff --git a/pkg/kstream/interceptors/image.go b/pkg/kstream/interceptors/image.go new file mode 100644 index 000000000..3a247360e --- /dev/null +++ b/pkg/kstream/interceptors/image.go @@ -0,0 +1,83 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package interceptors + +import ( + "expvar" + "github.com/rabbitstack/fibratus/pkg/fs" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/yara" + log "github.com/sirupsen/logrus" +) + +// imageYaraScans stores the total count of yara image scans +var imageYaraScans = expvar.NewInt("yara.image.scans") + +type imageInterceptor struct { + devMapper fs.DevMapper + snap ps.Snapshotter + yara yara.Scanner +} + +func newImageInterceptor(snap ps.Snapshotter, devMapper fs.DevMapper, yara yara.Scanner) KstreamInterceptor { + return &imageInterceptor{snap: snap, devMapper: devMapper, yara: yara} +} + +func (imageInterceptor) Name() InterceptorType { return Image } + +func (i *imageInterceptor) Intercept(kevt *kevent.Kevent) (*kevent.Kevent, bool, error) { + if kevt.Type == ktypes.LoadImage || kevt.Type == ktypes.UnloadImage || kevt.Type == ktypes.EnumImage { + // normalize image parameters to convert the size of hex to decimal representation + // and replace the DOS image path to regular drive-based file path + pid, err := kevt.Kparams.GetUint32(kparams.ProcessID) + if err != nil { + return kevt, true, err + } + if err := kevt.Kparams.Set(kparams.ProcessID, pid, kparams.PID); err != nil { + return kevt, true, err + } + size, _ := kevt.Kparams.GetHexAsUint32(kparams.ImageSize) + if err := kevt.Kparams.Set(kparams.ImageSize, size, kparams.Uint32); err != nil { + return kevt, true, err + } + filename, _ := kevt.Kparams.GetString(kparams.ImageFilename) + if err := kevt.Kparams.Set(kparams.ImageFilename, i.devMapper.Convert(filename), kparams.UnicodeString); err != nil { + return kevt, true, err + } + if i.yara != nil && kevt.Type == ktypes.LoadImage { + // scan the the target filename + go func() { + imageYaraScans.Add(1) + err := i.yara.ScanFile(filename) + if err != nil { + log.Warnf("unable to run yara scanner on %s image: %v", filename, err) + } + }() + } + if kevt.Type != ktypes.UnloadImage { + return kevt, false, i.snap.Write(kevt) + } + return kevt, false, i.snap.Remove(kevt) + } + + return kevt, true, nil +} diff --git a/pkg/kstream/interceptors/image_test.go b/pkg/kstream/interceptors/image_test.go new file mode 100644 index 000000000..64596f338 --- /dev/null +++ b/pkg/kstream/interceptors/image_test.go @@ -0,0 +1,19 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package interceptors diff --git a/pkg/kstream/interceptors/interceptor.go b/pkg/kstream/interceptors/interceptor.go new file mode 100644 index 000000000..8564886e3 --- /dev/null +++ b/pkg/kstream/interceptors/interceptor.go @@ -0,0 +1,66 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package interceptors + +import "github.com/rabbitstack/fibratus/pkg/kevent" + +// InterceptorType is an alias for the interceptor type +type InterceptorType uint8 + +const ( + Ps InterceptorType = iota + Fs + Registry + Image + Net + Handle +) + +// KstreamInterceptor is the minimal interface that each kernel stream interceptor has to satisfy. Kernel stream interceptor +// has the ability to augment kernel event with additional parameters. It is also capable of building a state machine +// from the flow of kernel events going through it. The interceptor can also decide to drop the inbound kernel event by +// returning an error via its `Intercept` method. +type KstreamInterceptor interface { + // Intercept receives an existing kernel event possibly mutating its state. The event is filtered out if + // this method returns an error. If it returns true, the next interceptor in the chain is evaluated. + Intercept(kevt *kevent.Kevent) (*kevent.Kevent, bool, error) + + // Name returns a human-readable name of this interceptor. + Name() InterceptorType +} + +// String returns a human-friendly interceptor name. +func (typ InterceptorType) String() string { + switch typ { + case Ps: + return "process" + case Fs: + return "file" + case Registry: + return "registry" + case Image: + return "image" + case Net: + return "net" + case Handle: + return "handle" + default: + return "unknown" + } +} diff --git a/pkg/kstream/interceptors/net.go b/pkg/kstream/interceptors/net.go new file mode 100644 index 000000000..c426e3031 --- /dev/null +++ b/pkg/kstream/interceptors/net.go @@ -0,0 +1,116 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package interceptors + +import ( + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/net" + "github.com/rabbitstack/fibratus/pkg/util/ports" +) + +type netInterceptor struct{} + +// newNetInterceptor creates a new instance of the network kernel stream interceptor. +func newNetInterceptor() KstreamInterceptor { + return &netInterceptor{} +} + +func (netInterceptor) Name() InterceptorType { + return Net +} + +// Intercpet overrides the kernel event type according to the transport layer +// and/or IP protocol version. At this point we also append the port names for all +// network kernel events. +func (n *netInterceptor) Intercept(kevt *kevent.Kevent) (*kevent.Kevent, bool, error) { + switch kevt.Type { + case ktypes.AcceptTCPv4, ktypes.AcceptTCPv6: + appendPortKparam(kevt, true) + kevt.Type = ktypes.Accept + return kevt, false, nil + + case ktypes.ConnectTCPv4, ktypes.ConnectTCPv6: + appendPortKparam(kevt, true) + kevt.Type = ktypes.Connect + return kevt, false, nil + + case ktypes.ReconnectTCPv4, ktypes.ReconnectTCPv6: + appendPortKparam(kevt, true) + kevt.Type = ktypes.Reconnect + return kevt, false, nil + + case ktypes.RetransmitTCPv4, ktypes.RetransmitTCPv6: + appendPortKparam(kevt, true) + kevt.Type = ktypes.Retransmit + return kevt, false, nil + + case ktypes.DisconnectTCPv4, ktypes.DisconnectTCPv6: + appendPortKparam(kevt, true) + kevt.Type = ktypes.Disconnect + return kevt, false, nil + + case ktypes.SendTCPv4, ktypes.SendTCPv6, ktypes.SendUDPv4, ktypes.SendUDPv6: + // append Layer 4 protocol name to Send events + if kevt.Type == ktypes.SendTCPv4 || kevt.Type == ktypes.SendTCPv6 { + kevt.Kparams.Append(kparams.NetL4Proto, kparams.Enum, net.TCP) + } else { + kevt.Kparams.Append(kparams.NetL4Proto, kparams.Enum, net.UDP) + } + appendPortKparam(kevt, kevt.Type == ktypes.SendTCPv4 || kevt.Type == ktypes.SendTCPv6) + kevt.Type = ktypes.Send + return kevt, false, nil + + case ktypes.RecvTCPv4, ktypes.RecvTCPv6, ktypes.RecvUDPv4, ktypes.RecvUDPv6: + // append Layer 4 protocol name to Recv events + if kevt.Type == ktypes.RecvTCPv4 || kevt.Type == ktypes.RecvTCPv6 { + kevt.Kparams.Append(kparams.NetL4Proto, kparams.Enum, net.TCP) + } else { + kevt.Kparams.Append(kparams.NetL4Proto, kparams.Enum, net.UDP) + } + appendPortKparam(kevt, kevt.Type == ktypes.RecvTCPv4 || kevt.Type == ktypes.RecvTCPv6) + kevt.Type = ktypes.Recv + return kevt, false, nil + } + + return kevt, true, nil +} + +// appendPortKparam resolves the IANA (https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml) +// service name for the particular port and transport protocol. +func appendPortKparam(kevt *kevent.Kevent, isTCP bool) { + dport, _ := kevt.Kparams.GetUint16(kparams.NetDport) + sport, _ := kevt.Kparams.GetUint16(kparams.NetSport) + if isTCP { + if name, ok := ports.TCPPortNames[dport]; ok { + kevt.Kparams.Append(kparams.NetDportName, kparams.AnsiString, name) + } + if name, ok := ports.TCPPortNames[sport]; ok { + kevt.Kparams.Append(kparams.NetSportName, kparams.AnsiString, name) + } + return + } + if name, ok := ports.UDPPortNames[dport]; ok { + kevt.Kparams.Append(kparams.NetDportName, kparams.AnsiString, name) + } + if name, ok := ports.UDPPortNames[sport]; ok { + kevt.Kparams.Append(kparams.NetSportName, kparams.AnsiString, name) + } +} diff --git a/pkg/kstream/interceptors/net_test.go b/pkg/kstream/interceptors/net_test.go new file mode 100644 index 000000000..eb0ad3731 --- /dev/null +++ b/pkg/kstream/interceptors/net_test.go @@ -0,0 +1,68 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package interceptors + +import ( + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + knet "github.com/rabbitstack/fibratus/pkg/net" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "net" + "testing" +) + +func TestNetInterceptorSend(t *testing.T) { + kevt := &kevent.Kevent{ + Type: ktypes.SendTCPv4, + Tid: 2484, + PID: 859, + Kparams: kevent.Kparams{ + kparams.NetDport: {Name: kparams.NetDport, Type: kparams.Uint16, Value: uint16(443)}, + kparams.NetSport: {Name: kparams.NetSport, Type: kparams.Uint16, Value: uint16(43123)}, + kparams.NetSIP: {Name: kparams.NetSIP, Type: kparams.IPv4, Value: net.ParseIP("127.0.0.1")}, + kparams.NetDIP: {Name: kparams.NetDIP, Type: kparams.IPv4, Value: net.ParseIP("216.58.201.174")}, + }, + } + ni := newNetInterceptor() + + _, _, err := ni.Intercept(kevt) + require.NoError(t, err) + + assert.Equal(t, ktypes.Send, kevt.Type) + + assert.Contains(t, kevt.Kparams, kparams.NetDportName) + dportName, err := kevt.Kparams.GetString(kparams.NetDportName) + require.NoError(t, err) + assert.Equal(t, "https", dportName) + + v, err := kevt.Kparams.Get(kparams.NetL4Proto) + require.NoError(t, err) + assert.IsType(t, knet.L4Proto(1), v) + assert.Equal(t, "tcp", v.(knet.L4Proto).String()) + + sip, err := kevt.Kparams.GetIPv4(kparams.NetSIP) + require.NoError(t, err) + assert.Equal(t, "127.0.0.1", sip.String()) + + dip, err := kevt.Kparams.GetIPv4(kparams.NetDIP) + require.NoError(t, err) + assert.Equal(t, "216.58.201.174", dip.String()) +} diff --git a/pkg/kstream/interceptors/ps.go b/pkg/kstream/interceptors/ps.go new file mode 100644 index 000000000..59d7ef405 --- /dev/null +++ b/pkg/kstream/interceptors/ps.go @@ -0,0 +1,171 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package interceptors + +import ( + "expvar" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/syscall/process" + "github.com/rabbitstack/fibratus/pkg/yara" + log "github.com/sirupsen/logrus" + "os" + "path/filepath" + "regexp" + "strings" + "time" +) + +// systemRootRegexp is the regular expression for detecting path with unexpanded SystemRoot environment variable +var systemRootRegexp = regexp.MustCompile(`%SystemRoot%|\\SystemRoot`) + +// procYaraScans stores the total count of yara process scans +var procYaraScans = expvar.NewInt("yara.proc.scans") + +type psInterceptor struct { + snap ps.Snapshotter + yara yara.Scanner +} + +var sysProcs = []string{ + "dwm.exe", + "wininit.exe", + "winlogon.exe", + "fontdrvhost.exe", + "sihost.exe", + "taskhostw.exe", + "dashost.exe", + "ctfmon.exe", +} + +// newPsInterceptor creates a new kstream interceptor for process events. +func newPsInterceptor(snap ps.Snapshotter, yara yara.Scanner) KstreamInterceptor { + return psInterceptor{snap: snap, yara: yara} +} + +func (ps psInterceptor) Intercept(kevt *kevent.Kevent) (*kevent.Kevent, bool, error) { + typ := kevt.Type + switch typ { + case ktypes.CreateProcess, ktypes.TerminateProcess, ktypes.EnumProcess: + comm, err := kevt.Kparams.GetString(kparams.Comm) + if err != nil { + return kevt, true, err + } + // some system processes are reported without the path in command line + if !strings.Contains(comm, `\\:`) { + for _, proc := range sysProcs { + if proc == comm { + _ = kevt.Kparams.Set(kparams.Comm, filepath.Join(os.Getenv("SystemRoot"), comm), kparams.UnicodeString) + } + } + } + // to compose the full executable string we extract the path + // from the process's command line by expanding the `SystemRoot` + // env variable accordingly and also removing rubbish characters + i := strings.Index(comm, "exe") + if i > 0 { + exe := strings.Replace(comm[0:i+3], "\"", "", -1) + if strings.Contains(exe, "SystemRoot") { + exe = systemRootRegexp.ReplaceAllString(exe, os.Getenv("SystemRoot")) + } + kevt.Kparams.Append(kparams.Exe, kparams.UnicodeString, exe) + } + // convert hexadecimal PID values to integers + pid, err := kevt.Kparams.GetHexAsUint32(kparams.ProcessID) + if err != nil { + return kevt, true, err + } + if err := kevt.Kparams.Set(kparams.ProcessID, pid, kparams.PID); err != nil { + return kevt, true, err + } + ppid, err := kevt.Kparams.GetHexAsUint32(kparams.ProcessParentID) + if err != nil { + return kevt, true, err + } + if err := kevt.Kparams.Set(kparams.ProcessParentID, ppid, kparams.PID); err != nil { + return kevt, true, err + } + + if typ != ktypes.TerminateProcess { + if pid != 0 { + // get the process's start time and append it to the parameters + started, err := getStartTime(pid) + if err != nil { + log.Warnf("couldn't get process (%d) start time: %v", pid, err) + } else { + _ = kevt.Kparams.Append(kparams.StartTime, kparams.Time, started) + } + } + if ps.yara != nil && typ == ktypes.CreateProcess { + // run yara scanner on the target process + go func() { + procYaraScans.Add(1) + err := ps.yara.ScanProc(pid) + if err != nil { + log.Warnf("unable to run yara scanner on pid %d: %v", pid, err) + } + }() + } + return kevt, false, ps.snap.Write(kevt) + } + + return kevt, false, ps.snap.Remove(kevt) + + case ktypes.CreateThread, ktypes.TerminateThread, ktypes.EnumThread: + pid, err := kevt.Kparams.GetHexAsUint32(kparams.ProcessID) + if err != nil { + return kevt, true, err + } + if err := kevt.Kparams.Set(kparams.ProcessID, pid, kparams.PID); err != nil { + return kevt, true, err + } + tid, err := kevt.Kparams.GetHexAsUint32(kparams.ThreadID) + if err != nil { + return kevt, true, err + } + if err := kevt.Kparams.Set(kparams.ThreadID, tid, kparams.TID); err != nil { + return kevt, true, err + } + + if typ != ktypes.TerminateThread { + return kevt, false, ps.snap.Write(kevt) + } + + return kevt, false, ps.snap.Remove(kevt) + } + + return kevt, true, nil +} + +func (psInterceptor) Name() InterceptorType { return Ps } + +func getStartTime(pid uint32) (time.Time, error) { + handle, err := process.Open(process.QueryLimitedInformation, false, pid) + if err != nil { + return time.Now(), err + } + defer handle.Close() + started, err := process.GetStartTime(handle) + if err != nil { + return time.Now(), err + } + return started, nil +} diff --git a/pkg/kstream/interceptors/ps_test.go b/pkg/kstream/interceptors/ps_test.go new file mode 100644 index 000000000..b417ed396 --- /dev/null +++ b/pkg/kstream/interceptors/ps_test.go @@ -0,0 +1,92 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package interceptors + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "testing" +) + +func TestPsInterceptorIntercept(t *testing.T) { + psnap := new(ps.SnapshotterMock) + psi := newPsInterceptor(psnap, nil) + + kpars := kevent.Kparams{ + kparams.Comm: {Name: kparams.Comm, Type: kparams.UnicodeString, Value: "C:\\Windows\\system32\\svchost.exe -k RPCSS"}, + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.HexInt32, Value: kparams.Hex("36c")}, + kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.HexInt32, Value: kparams.Hex("26c")}, + } + + kevt := &kevent.Kevent{ + Type: ktypes.CreateProcess, + Kparams: kpars, + } + _, _, err := psi.Intercept(kevt) + require.NoError(t, err) + + require.True(t, kevt.Kparams.Contains(kparams.Exe)) + exe, _ := kevt.Kparams.GetString(kparams.Exe) + assert.Equal(t, "C:\\Windows\\system32\\svchost.exe", exe) + pid, _ := kpars.GetPid() + assert.Equal(t, uint32(876), pid) + ppid, _ := kpars.GetPpid() + assert.Equal(t, uint32(620), ppid) + + kpars1 := kevent.Kparams{ + kparams.Comm: {Name: kparams.Comm, Type: kparams.UnicodeString, Value: "C:\\Windows\\System32\\smss.exe"}, + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.HexInt32, Value: kparams.Hex("36c")}, + kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.HexInt32, Value: kparams.Hex("26c")}, + } + + kevt1 := &kevent.Kevent{ + Type: ktypes.EnumProcess, + Kparams: kpars1, + } + err = os.Setenv("SystemRoot", "C:\\Windows") + if err != nil { + t.Fatal(err) + } + _, _, err = psi.Intercept(kevt1) + require.NoError(t, err) + exe, _ = kpars1.GetString(kparams.Exe) + assert.Equal(t, "C:\\Windows\\System32\\smss.exe", exe) + + tpid := fmt.Sprintf("%x", os.Getpid()) + kpars2 := kevent.Kparams{ + kparams.Comm: {Name: kparams.Comm, Type: kparams.UnicodeString, Value: "C:\\Windows\\System32\\smss.exe"}, + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.HexInt32, Value: kparams.Hex(tpid)}, + kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.HexInt32, Value: kparams.Hex("26c")}, + } + + kevt2 := &kevent.Kevent{ + Type: ktypes.CreateProcess, + Kparams: kpars2, + } + _, _, err = psi.Intercept(kevt2) + require.NoError(t, err) + + require.True(t, kevt2.Kparams.Contains(kparams.StartTime)) +} diff --git a/pkg/kstream/interceptors/registry.go b/pkg/kstream/interceptors/registry.go new file mode 100644 index 000000000..9a8d97fb1 --- /dev/null +++ b/pkg/kstream/interceptors/registry.go @@ -0,0 +1,276 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package interceptors + +import ( + "expvar" + "github.com/rabbitstack/fibratus/pkg/handle" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/syscall/registry" + "github.com/rabbitstack/fibratus/pkg/syscall/sys" + "golang.org/x/sys/windows" + reg "golang.org/x/sys/windows/registry" + "path/filepath" + "strings" + "sync/atomic" + "syscall" + "time" + "unicode/utf16" +) + +var ( + // kcbCount counts the total KCBs found during the duration of the kernel session + kcbCount = expvar.NewInt("registry.kcb.count") + kcbMissCount = expvar.NewInt("registry.kcb.misses") + unknownKeysCount = expvar.NewInt("registry.unknown.keys.count") + keyHandleHits = expvar.NewInt("registry.key.handle.hits") + + handleThrottleCount uint32 +) + +const ( + notFoundNTStatus = 3221225524 + maxHandleQueries = 200 +) + +type registryInterceptor struct { + // keys stores the mapping between the KCB (Key Control Block) and the key name. + keys map[uint64]string + hsnap handle.Snapshotter +} + +func newRegistryInterceptor(hsnap handle.Snapshotter) KstreamInterceptor { + // schedule a ticker that resets the throttle count every minute + tick := time.NewTicker(time.Minute) + go func() { + for { + <-tick.C + atomic.StoreUint32(&handleThrottleCount, 0) + } + }() + return ®istryInterceptor{keys: make(map[uint64]string), hsnap: hsnap} +} + +func (r *registryInterceptor) Intercept(kevt *kevent.Kevent) (*kevent.Kevent, bool, error) { + typ := kevt.Type + switch typ { + case ktypes.RegKCBRundown, ktypes.RegCreateKCB: + khandle, err := kevt.Kparams.GetHexAsUint64(kparams.RegKeyHandle) + if err != nil { + return kevt, true, err + } + if _, ok := r.keys[khandle]; !ok { + r.keys[khandle], _ = kevt.Kparams.GetString(kparams.RegKeyName) + } + kcbCount.Add(1) + return kevt, false, nil + + case ktypes.RegDeleteKCB: + khandle, err := kevt.Kparams.GetHexAsUint64(kparams.RegKeyHandle) + if err != nil { + return kevt, true, err + } + delete(r.keys, khandle) + kcbCount.Add(-1) + return kevt, false, nil + + case ktypes.RegCreateKey, + ktypes.RegDeleteKey, + ktypes.RegOpenKey, ktypes.RegOpenKeyV1, + ktypes.RegQueryKey, + ktypes.RegQueryValue, + ktypes.RegSetValue, + ktypes.RegDeleteValue: + khandle, err := kevt.Kparams.GetHexAsUint64(kparams.RegKeyHandle) + if err != nil { + return kevt, true, err + } + // we have to obey a straightforward algorithm to connect relative + // key names to their root keys. If key handle is equal to zero we + // have a full key name and don't have to go further resolving the + // missing part. Otherwise, we have to lookup existing KCBs to try + // find the matching base key name and concatenate to its relative + // path. If none of the aforementioned checks are successful, our + // last resort is to scan process' handles and check if any of the + // key handles contain the partial key name. In this case we assume + // the correct key is encountered. + var rootKey registry.Key + keyName, err := kevt.Kparams.GetString(kparams.RegKeyName) + if err != nil { + return kevt, true, err + } + if khandle != 0 { + if baseKey, ok := r.keys[khandle]; ok { + keyName = baseKey + "\\" + keyName + } else { + kcbMissCount.Add(1) + keyName = r.findMatchingKey(kevt.PID, keyName) + } + } + + if keyName != "" { + rootKey, keyName = handle.FormatKey(keyName) + k := rootKey.String() + if keyName != "" && rootKey != registry.InvalidKey { + k += "\\" + keyName + } + if rootKey == registry.InvalidKey { + unknownKeysCount.Add(1) + k = keyName + } + if err := kevt.Kparams.Set(kparams.RegKeyName, k, kparams.UnicodeString); err != nil { + return kevt, true, err + } + } + + // format registry operation status code + status, err := kevt.Kparams.GetUint32(kparams.NTStatus) + if err == nil { + _ = kevt.Kparams.Set(kparams.NTStatus, formatStatus(status), kparams.UnicodeString) + } + + // get the type/value of the registry key and append to parameters + if typ == ktypes.RegSetValue { + if rootKey != registry.InvalidKey { + subkey, value := filepath.Split(keyName) + key, err := reg.OpenKey(reg.Key(rootKey), subkey, reg.QUERY_VALUE) + if err != nil { + return kevt, true, nil + } + defer key.Close() + b := make([]byte, 0) + _, typ, err := key.GetValue(value, b) + if err != nil { + return kevt, true, nil + } + kevt.Kparams.Append(kparams.RegValueType, kparams.AnsiString, typToString(typ)) + switch typ { + case reg.SZ, reg.EXPAND_SZ: + v, _, err := key.GetStringValue(value) + if err != nil { + return kevt, true, nil + } + kevt.Kparams.Append(kparams.RegValue, kparams.UnicodeString, v) + + case reg.DWORD, reg.QWORD: + v, _, err := key.GetIntegerValue(value) + if err != nil { + return kevt, true, nil + } + kevt.Kparams.Append(kparams.RegValue, kparams.Uint64, v) + + case reg.MULTI_SZ: + v, _, err := key.GetStringsValue(value) + if err != nil { + return kevt, true, nil + } + kevt.Kparams.Append(kparams.RegValue, kparams.UnicodeString, strings.Join(v, "\n\r")) + + case reg.BINARY: + v, _, err := key.GetBinaryValue(value) + if err != nil { + return kevt, true, nil + } + kevt.Kparams.Append(kparams.RegValue, kparams.UnicodeString, string(v)) + } + return kevt, false, nil + } + } + } + + return kevt, true, nil +} + +func (registryInterceptor) Name() InterceptorType { return Registry } + +func typToString(typ uint32) string { + switch typ { + case reg.DWORD: + return "REG_DWORD" + case reg.QWORD: + return "REG_QWORD" + case reg.SZ: + return "REG_SZ" + case reg.EXPAND_SZ: + return "REG_EXPAND_SZ" + case reg.MULTI_SZ: + return "REG_MULTI_SZ" + case reg.BINARY: + return "REG_BINARY" + default: + return "UNKNOWN" + } +} + +var resolvedStatuses = map[uint32]string{} + +func formatStatus(status uint32) string { + if status == 0 { + return "success" + } + // this status code is return quite often so we can offload the FormatMessage call + if status == notFoundNTStatus { + return "key not found" + } + // pick resolved status + if s, ok := resolvedStatuses[status]; ok { + return s + } + var flags uint32 = syscall.FORMAT_MESSAGE_FROM_SYSTEM + b := make([]uint16, 300) + n, err := windows.FormatMessage(flags, 0, uint32(sys.CodeFromNtStatus(status)), 0, b, nil) + if err != nil { + return "unknown" + } + // trim terminating \r and \n + for ; n > 0 && (b[n-1] == '\n' || b[n-1] == '\r'); n-- { + } + + s := strings.ToLower(string(utf16.Decode(b[:n]))) + resolvedStatuses[status] = s + + return s +} + +func (r *registryInterceptor) findMatchingKey(pid uint32, relativeKeyName string) string { + // we want to prevent too frequent queries on the process' handles + // since that can cause significant performance overhead. When throttle + // count is greater than the max permitted value we'll just return the partial key + // and hold on querying the handles of target process + atomic.AddUint32(&handleThrottleCount, 1) + if handleThrottleCount > maxHandleQueries { + return relativeKeyName + } + handles, err := r.hsnap.FindHandles(pid) + if err != nil { + return relativeKeyName + } + for _, h := range handles { + if h.Type != handle.Key { + continue + } + if strings.HasSuffix(h.Name, relativeKeyName) { + keyHandleHits.Add(1) + return h.Name + } + } + return relativeKeyName +} diff --git a/pkg/kstream/interceptors/registry_test.go b/pkg/kstream/interceptors/registry_test.go new file mode 100644 index 000000000..46e8abeb3 --- /dev/null +++ b/pkg/kstream/interceptors/registry_test.go @@ -0,0 +1,72 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package interceptors + +import ( + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestRegistryInterceptor(t *testing.T) { + r := newRegistryInterceptor(nil) + + _, _, err := r.Intercept(&kevent.Kevent{ + Type: ktypes.RegKCBRundown, + Tid: 2484, + PID: 859, + Kparams: kevent.Kparams{ + kparams.RegKeyName: {Name: kparams.RegKeyName, Type: kparams.UnicodeString, Value: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\bthserv\Parameters`}, + kparams.RegKeyHandle: {Name: kparams.RegKeyHandle, Type: kparams.HexInt64, Value: kparams.NewHex(uint64(18446666033549154696))}, + }, + }) + require.NoError(t, err) + assert.Equal(t, int64(1), kcbCount.Value()) + + _, _, err = r.Intercept(&kevent.Kevent{ + Type: ktypes.RegCreateKCB, + Tid: 1484, + PID: 259, + Kparams: kevent.Kparams{ + kparams.RegKeyName: {Name: kparams.RegKeyName, Type: kparams.UnicodeString, Value: `\REGISTRY\MACHINE\SYSTEM\Setup`}, + kparams.RegKeyHandle: {Name: kparams.RegKeyHandle, Type: kparams.HexInt64, Value: kparams.NewHex(uint64(18446666033449935464))}, + }, + }) + require.NoError(t, err) + assert.Equal(t, int64(2), kcbCount.Value()) + + kevt := &kevent.Kevent{ + Type: ktypes.RegCreateKey, + Tid: 2484, + PID: 859, + Kparams: kevent.Kparams{ + kparams.RegKeyName: {Name: kparams.RegKeyName, Type: kparams.UnicodeString, Value: `Pid`}, + kparams.RegKeyHandle: {Name: kparams.RegKeyHandle, Type: kparams.HexInt64, Value: kparams.NewHex(uint64(18446666033449935464))}, + }, + } + _, _, err = r.Intercept(kevt) + require.NoError(t, err) + + keyName, err := kevt.Kparams.GetString(kparams.RegKeyName) + require.NoError(t, err) + assert.Equal(t, `HKEY_LOCAL_MACHINE\SYSTEM\Setup\Pid`, keyName) +} diff --git a/pkg/kstream/kstream_rundownc.go b/pkg/kstream/kstream_rundownc.go new file mode 100644 index 000000000..62f614c7a --- /dev/null +++ b/pkg/kstream/kstream_rundownc.go @@ -0,0 +1,176 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kstream + +import ( + "fmt" + kerrors "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/filter" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/syscall/etw" + "github.com/rabbitstack/fibratus/pkg/syscall/tdh" + "github.com/rabbitstack/fibratus/pkg/syscall/utf16" + "github.com/rabbitstack/fibratus/pkg/syscall/winerrno" + log "github.com/sirupsen/logrus" + "syscall" + "unsafe" +) + +// kstreamRundownConsumer publishes opened file objects as encountered at the beginning of the trace session. +type kstreamRundownConsumer struct { + handle etw.TraceHandle + errs chan error + krundowns chan *kevent.Kevent +} + +func newRundownConsumer() Consumer { + return &kstreamRundownConsumer{ + errs: make(chan error), + krundowns: make(chan *kevent.Kevent, 1000), + } +} + +func (k *kstreamRundownConsumer) OpenKstream() error { + ktrace := etw.EventTraceLogfile{ + LoggerName: utf16.StringToUTF16Ptr(etw.KernelLoggerRundownSession), + } + cb := syscall.NewCallback(k.processRundownCallback) + modes := uint32(etw.ProcessTraceModeRealtime | etw.ProcessTraceModeEventRecord) + *(*uint32)(unsafe.Pointer(&ktrace.LogFileMode[0])) = modes + *(*uintptr)(unsafe.Pointer(&ktrace.EventCallback[4])) = cb + + handle := openTrace(ktrace) + if uint64(handle) == winerrno.InvalidProcessTraceHandle { + return fmt.Errorf("unable to open kernel rundown logger trace: %v", syscall.GetLastError()) + } + k.handle = handle + go func() { + err := processTrace(handle) + if err != nil { + k.errs <- err + return + } + log.Info("successfully stopped kernel rundown session") + }() + return nil +} + +// SetFilter initializes the filter that's applied on the kernel events. +func (k *kstreamRundownConsumer) SetFilter(filter filter.Filter) {} + +// CloseKstream shutdowns the currently running kernel rundown consumer by closing the corresponding +// session. +func (k *kstreamRundownConsumer) CloseKstream() error { + return etw.CloseTrace(k.handle) +} + +// Errors returns a channel where errors are pushed. +func (k *kstreamRundownConsumer) Errors() chan error { + return k.errs +} + +// Events returns the buffered channel where enumerated system resources are pushed. +func (k *kstreamRundownConsumer) Events() chan *kevent.Kevent { + return k.krundowns +} + +func (k *kstreamRundownConsumer) processRundownCallback(evt *etw.EventRecord) uintptr { + if err := k.processRundown(evt); err != nil { + k.errs <- err + } + return callbackNext +} + +func (k *kstreamRundownConsumer) processRundown(evt *etw.EventRecord) error { + bufferSize := evtBufferSize + buffer := make([]byte, bufferSize) + + ktype := ktypes.Pack(evt.Header.ProviderID, evt.Header.EventDescriptor.Opcode) + + err := tdh.GetEventInformation(evt, buffer, bufferSize) + if err == kerrors.ErrInsufficentBuffer { + // not enough space to store the event, so we retry with bigger buffer + buffer = make([]byte, bufferSize) + if err = tdh.GetEventInformation(evt, buffer, bufferSize); err != nil { + return fmt.Errorf("failed to get rundown event after reallocating buffer size to %d KB: %v", bufferSize, err) + } + } + trace := (*tdh.TraceEventInfo)(unsafe.Pointer(&buffer[0])) + kpars := kevent.Kparams(produceParams(evt, trace)) + + krundown := &kevent.Kevent{ + Type: ktype, + Kparams: kpars, + } + + select { + case k.krundowns <- krundown: + default: + log.Warn("kernel rundown logger event queue is full") + } + + return nil +} + +// produceParams extracts event's parameters from the event descriptor. +func produceParams(evt *etw.EventRecord, trace *tdh.TraceEventInfo) map[string]*kevent.Kparam { + var ( + count = trace.PropertyCount + kpars = make(map[string]*kevent.Kparam, count) + // this yields a property array from unsized array + props = (*[1 << 30]tdh.EventPropertyInfo)(unsafe.Pointer(&trace.EventPropertyInfoArray[0]))[:count:count] + ) + + for _, property := range props { + // compute the pointer to each property name and get the size of the buffer + // that we'll allocate to accommodate the property value + propp := unsafe.Pointer(uintptr(unsafe.Pointer(trace)) + uintptr(property.NameOffset)) + kparName := syscall.UTF16ToString((*[1 << 20]uint16)(propp)[:]) + + descriptor := &tdh.PropertyDataDescriptor{ + PropertyName: propp, + ArrayIndex: 0xFFFFFFFF, + } + size, err := getPropertySize(evt, descriptor) + if err != nil || size == 0 { + continue + } + + buffer := make([]byte, size) + if err := getProperty(evt, descriptor, size, buffer); err != nil { + continue + } + + kparName = kparams.Canonicalize(kparName) + // discard unknown canonical names + if kparName == "" { + continue + } + nst := *(*tdh.NonStructType)(unsafe.Pointer(&property.Types[0])) + // obtain the parameter value from byte buffer + kpar, err := getParam(kparName, buffer, size, nst) + if err != nil { + continue + } + kpars[kparName] = kpar + } + return kpars +} diff --git a/pkg/kstream/kstreamc.go b/pkg/kstream/kstreamc.go new file mode 100644 index 000000000..aaec79769 --- /dev/null +++ b/pkg/kstream/kstreamc.go @@ -0,0 +1,611 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kstream + +import ( + "errors" + "expvar" + "fmt" + "github.com/rabbitstack/fibratus/pkg/config" + kerrors "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/filter" + "github.com/rabbitstack/fibratus/pkg/handle" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/kstream/interceptors" + "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/syscall/etw" + "github.com/rabbitstack/fibratus/pkg/syscall/process" + "github.com/rabbitstack/fibratus/pkg/syscall/tdh" + "github.com/rabbitstack/fibratus/pkg/syscall/thread" + "github.com/rabbitstack/fibratus/pkg/syscall/utf16" + "github.com/rabbitstack/fibratus/pkg/syscall/winerrno" + "github.com/rabbitstack/fibratus/pkg/util/filetime" + log "github.com/sirupsen/logrus" + "os" + "strings" + "syscall" + "unsafe" +) + +const ( + // callbackNext is the return callback value which designates that callback execution should progress + callbackNext = uintptr(1) + // evtBufferSize determines the default buffer size in kilobytes for the`TraceEventInfo` structure + evtBufferSize = uint32(4096) +) + +var ( + // failedKevents counts the number of kevents that failed to process + failedKevents = expvar.NewMap("kstream.kevents.failures") + failedKeventsByMissingSchema = expvar.NewMap("kstream.kevents.missing.schema.errors") + // keventsEnqueued counts the number of events that are pushed to the queue + keventsEnqueued = expvar.NewInt("kstream.kevents.enqueued") + // failedKparams counts the number of kernel event parameters that failed to process + failedKparams = expvar.NewInt("kstream.kevent.param.failures") + blacklistedKevents = expvar.NewMap("kstream.blacklist.dropped.kevents") + blacklistedProcs = expvar.NewMap("kstream.blacklist.dropped.procs") + + upstreamCancellations = expvar.NewInt("kstream.upstream.cancellations") + + buffersRead = expvar.NewInt("kstream.kbuffers.read") +) + +var ( + openTrace = etw.OpenTrace + processTrace = etw.ProcessTrace + getPropertySize = tdh.GetPropertySize + getProperty = tdh.GetProperty + + currentPid = uint32(os.Getpid()) +) + +// Consumer is the interface for the kernel event stream consumer. +type Consumer interface { + // OpenKstream initializes the kernel event stream by setting the event record callback and instructing it + // to consume events from log buffers. This operation can fail if opening the kernel logger session results + // in an invalid trace handler. Errors returned by `ProcessTrace` are sent to the channel since this function + // blocks the current thread and we schedule its execution in a separate goroutine. + OpenKstream() error + // CloseKstream shutdowns the currently running kernel event stream consumer by closing the corresponding + // session. + CloseKstream() error + // Errors returns the channel where errors are pushed. + Errors() chan error + // Events returns the buffered channel for pulling collected kernel events. + Events() chan *kevent.Kevent + // SetFilter initializes the filter that's applied on the kernel events. + SetFilter(filter filter.Filter) +} + +type blacklist map[ktypes.Ktype]string + +func (b blacklist) has(ktype ktypes.Ktype) bool { return b[ktype] != "" } + +type kstreamConsumer struct { + handle etw.TraceHandle + errs chan error + kevts chan *kevent.Kevent + interceptorChain interceptors.Chain + ignoredKparams map[string]bool + config *config.Config + + keventsBlacklist blacklist + procsBlacklist []string + + kstreamRundownConsumer Consumer + + ktraceController KtraceController + + psnapshotter ps.Snapshotter + sequencer *kevent.Sequencer + + filter filter.Filter + capture bool +} + +// NewConsumer constructs a new kernel event stream consumer. +func NewConsumer(ktraceController KtraceController, psnap ps.Snapshotter, hsnap handle.Snapshotter, config *config.Config) Consumer { + kconsumer := &kstreamConsumer{ + errs: make(chan error, 1000), + ignoredKparams: kparams.Ignored(), + config: config, + psnapshotter: psnap, + kstreamRundownConsumer: newRundownConsumer(), + ktraceController: ktraceController, + procsBlacklist: make([]string, len(config.Kstream.BlacklistImages)), + keventsBlacklist: make(map[ktypes.Ktype]string), + capture: config.KcapFile != "", + sequencer: kevent.NewSequencer(), + kevts: make(chan *kevent.Kevent), + } + + kconsumer.interceptorChain = interceptors.NewChain(psnap, hsnap, kconsumer.startRundown, config) + + return kconsumer +} + +func (k *kstreamConsumer) startRundown() error { + if err := k.ktraceController.StartKtraceRundown(); err != nil { + return err + } + go k.openRundownConsumer() + return nil +} + +func (k *kstreamConsumer) init() { + if k.ktraceController.IsKRundownStarted() { + go k.openRundownConsumer() + } + + for _, name := range k.config.Kstream.BlacklistKevents { + if ktype := ktypes.KeventNameToKtype(name); ktype != ktypes.UnknownKtype { + k.keventsBlacklist[ktype] = name + } + } + + for i, name := range k.config.Kstream.BlacklistImages { + k.procsBlacklist[i] = strings.ToLower(name) + } +} + +// SetFilter initializes the filter that's applied on the kernel events. +func (k *kstreamConsumer) SetFilter(filter filter.Filter) { k.filter = filter } + +// OpenKstream initializes the kernel event stream by setting the event record callback and instructing it +// to consume events from log buffers. This operation can fail if opening the kernel logger session results +// in an invalid trace handler. Errors returned by `ProcessTrace` are sent to the channel since this function +// blocks the current thread and we schedule its execution in a separate goroutine. +func (k *kstreamConsumer) OpenKstream() error { + ktrace := etw.EventTraceLogfile{ + LoggerName: utf16.StringToUTF16Ptr(etw.KernelLoggerSession), + BufferCallback: syscall.NewCallback(k.bufferStatsCallback), + } + cb := syscall.NewCallback(k.processKeventCallback) + modes := uint32(etw.ProcessTraceModeRealtime | etw.ProcessTraceModeEventRecord) + // initialize real time trace mode and event callback functions + // via these nasty pointer accesses to unions inside the structure + *(*uint32)(unsafe.Pointer(&ktrace.LogFileMode[0])) = modes + *(*uintptr)(unsafe.Pointer(&ktrace.EventCallback[4])) = cb + + h := openTrace(ktrace) + if uint64(h) == winerrno.InvalidProcessTraceHandle { + return fmt.Errorf("unable to open kernel trace: %v", syscall.GetLastError()) + } + k.handle = h + k.init() + // since `ProcessTrace` blocks the current thread + // we invoke it in a separate goroutine but send + // any possible errors to the channel + go func() { + err := processTrace(h) + log.Info("stopping kernel trace processing") + if err == nil { + log.Info("kernel trace processing successfully stopped") + return + } + switch err { + case kerrors.ErrTraceCancelled: + if uint64(h) != winerrno.InvalidProcessTraceHandle { + if err := etw.CloseTrace(h); err != nil { + k.errs <- err + } + } + default: + k.errs <- err + } + }() + return nil +} + +func (k *kstreamConsumer) openRundownConsumer() { + if err := k.kstreamRundownConsumer.OpenKstream(); err != nil { + log.Error(err) + return + } + for { + select { + case kevt := <-k.kstreamRundownConsumer.Events(): + if _, err := k.interceptorChain.Dispatch(kevt); err != nil { + log.Errorf("unable to dispatch rundown event to interceptors: %v", err) + } + case err := <-k.kstreamRundownConsumer.Errors(): + log.Errorf("got kernel rundown error: %v", err) + } + } +} + +// CloseKstream shutdowns the currently running kernel event stream consumer by closing the corresponding +// session. +func (k *kstreamConsumer) CloseKstream() error { + if err := etw.CloseTrace(k.handle); err != nil { + return err + } + if err := k.sequencer.Store(); err != nil { + log.Warn(err) + } + if err := k.sequencer.Close(); err != nil { + log.Warn(err) + } + if k.ktraceController.IsKRundownStarted() { + if err := k.kstreamRundownConsumer.CloseKstream(); err != nil { + return err + } + } + return nil +} + +// Errors returns a channel where errors are pushed. +func (k *kstreamConsumer) Errors() chan error { + return k.errs +} + +// Events returns the buffered channel for pulling collected kernel events. +func (k *kstreamConsumer) Events() chan *kevent.Kevent { + return k.kevts +} + +// bufferStatsCallback is periodically triggered by ETW subsystem for the purpose of reporting +// buffer statistics, such as the number of buffers processed. +func (k *kstreamConsumer) bufferStatsCallback(logfile *etw.EventTraceLogfile) uintptr { + buffersRead.Add(int64(logfile.BuffersRead)) + return callbackNext +} + +// processKeventCallback is the event callback function signature that delegates event processing +// to `processKevent`. +func (k *kstreamConsumer) processKeventCallback(evt *etw.EventRecord) uintptr { + if err := k.processKevent(evt); err != nil { + failedKevents.Add(err.Error(), 1) + k.errs <- err + } + return callbackNext +} + +// processKevent is the backbone of the kernel stream consumer. It does the heavy lifting of parsing inbound ETW events, +// iterating through event properties and pushing kernel events to the channel. +func (k *kstreamConsumer) processKevent(evt *etw.EventRecord) error { + var ( + pid = evt.Header.ProcessID + tid = evt.Header.ThreadID + // get the CPU core on which the event was generated + cpu = *(*uint8)(unsafe.Pointer(&evt.BufferContext.ProcessorIndex[0])) + ktype = ktypes.Pack(evt.Header.ProviderID, evt.Header.EventDescriptor.Opcode) + ) + + // drop any blacklisted process or unknown kernel event as earliest as possible + if k.dropBlacklistProc(pid) || !ktype.Exists() { + return nil + } + // it as required to initialize the size of the + // event trace buffer that we'll have to reallocate + // in case there's no enough room to store the whole trace + bufferSize := evtBufferSize + buffer := make([]byte, bufferSize) + + err := tdh.GetEventInformation(evt, buffer, bufferSize) + if err == kerrors.ErrInsufficentBuffer { + // not enough space to store the event, so we retry with bigger buffer + buffer = make([]byte, bufferSize) + if err = tdh.GetEventInformation(evt, buffer, bufferSize); err != nil { + return fmt.Errorf("failed to get event metadata after reallocating buffer size to %d KB: %v", bufferSize, err) + } + } + + if err != nil { + if err == kerrors.ErrEventSchemaNotFound { + // increment error count for events that lack the schema + failedKeventsByMissingSchema.Add(ktype.String(), 1) + return fmt.Errorf("schema not found for event %q", ktype) + } + return fmt.Errorf("unable to retrieve kernel event metadata for :%q: %v", ktype, err) + } + + trace := (*tdh.TraceEventInfo)(unsafe.Pointer(&buffer[0])) + kpars := kevent.Kparams(k.produceParams(ktype, evt, trace)) + ts := filetime.ToEpoch(evt.Header.Timestamp) + category := ktypes.KtypeToKeventInfo(ktype).Category + + switch category { + case ktypes.Image: + // sometimes the pid present in event header is invalid + // but we can get the valid one from the event parameters + if pid == winerrno.InvalidPID { + pid, _ = kpars.GetUint32(kparams.ProcessID) + } + + case ktypes.File: + // on some Windows versions the value of + // the PID attribute is invalid for the + // file system kernel events + if pid == winerrno.InvalidPID { + // try to resolve a valid pid from thread ID + threadID, err := kpars.GetHexAsUint32(kparams.ThreadID) + if err != nil { + break + } + h, err := thread.Open(thread.QueryLimitedInformation, false, threadID) + if err != nil { + break + } + defer h.Close() + pid, err = process.GetPIDFromThread(h) + } + if pid != winerrno.InvalidPID { + kpars.Append(kparams.ProcessID, kparams.PID, pid) + } + + case ktypes.Process: + // process and thread start events may be logged in the context of the parent process or thread. + // As a result, the ProcessId and ThreadId members of EVENT_TRACE_HEADER may not correspond to the + // process and thread being created so we set the event pid to be the one of the parent process + pid, _ = kpars.GetHexAsUint32(kparams.ProcessParentID) + + case ktypes.Net: + pid, _ = kpars.GetUint32(kparams.ProcessID) + kpars.Remove(kparams.ProcessID) + } + + // try to drop blacklist processes after pid readjustment + if k.dropBlacklistProc(pid) { + return nil + } + + // build a new kernel event with all required fields. Kevent is the fundamental data structure + // for propagating events to outputs sinks. + kevt := kevent.New( + k.sequencer.Get(), + pid, + tid, + cpu, + ktype, + ts, + kpars, + ) + + // dispatch each event to the interceptor chain that will further augment the kernel + // event with useful fields, route events to corresponding snapshotters or initialize + // open files/registry control blocks at the beginning of the kernel trace session + kevt, err = k.interceptorChain.Dispatch(kevt) + if err != nil { + if kerrors.IsCancelUpstreamKevent(err) { + upstreamCancellations.Add(1) + return nil + } + log.Errorf("interceptor chain error(s) occurred: %v", err) + } + // associate process' state with the kernel event. We only override the process' + // state if it hasn't been set previously like in the situation where captures + // are being taken. The kernel events that construct the process' snapshot also + // have attached process state, so simply by replaying the flow of these events + // we are able to reconstruct system-wide process state. + if kevt.PS == nil { + kevt.PS = k.psnapshotter.Find(kevt.PID) + } + if k.isDropped(kevt) { + kevt.Release() + return nil + } + + k.kevts <- kevt + + keventsEnqueued.Add(1) + if !kevt.Type.Dropped(false) { + k.sequencer.Increment() + } + + return nil +} + +var offsets = map[uint32]string{} + +// produceParams traverses ETW event's properties, gets the underlying property buffer and produces a kernel event +// parameter for the particular event. +func (k *kstreamConsumer) produceParams(ktype ktypes.Ktype, evt *etw.EventRecord, trace *tdh.TraceEventInfo) map[string]*kevent.Kparam { + var ( + count = trace.PropertyCount + kpars = make(map[string]*kevent.Kparam, count) + // this yields a property array from the unsized array + props = (*[1 << 30]tdh.EventPropertyInfo)(unsafe.Pointer(&trace.EventPropertyInfoArray[0]))[:count:count] + ) + + for _, property := range props { + hashKey := ktype.Hash() + property.NameOffset + // lookup resolved kparam names if the hash key is not located in offsets cache + kparName, ok := offsets[hashKey] + // compute the pointer to each property name and get the size of the buffer + // that we'll allocate to accommodate the property value + propp := unsafe.Pointer(uintptr(unsafe.Pointer(trace)) + uintptr(property.NameOffset)) + if !ok { + kparName = utf16.UTF16PtrToString(propp) + offsets[hashKey] = kparName + } + + // skip ignored parameters + if _, ok := k.ignoredKparams[kparName]; ok { + continue + } + + descriptor := &tdh.PropertyDataDescriptor{ + PropertyName: propp, + ArrayIndex: 0xFFFFFFFF, + } + size, err := getPropertySize(evt, descriptor) + if err != nil || size == 0 { + continue + } + + buffer := make([]byte, size) + if err := getProperty(evt, descriptor, size, buffer); err != nil { + continue + } + + kparName = kparams.Canonicalize(kparName) + // discard unknown canonical names + if kparName == "" { + continue + } + + nst := *(*tdh.NonStructType)(unsafe.Pointer(&property.Types[0])) + // obtain parameter value from the byte buffer + kpar, err := getParam(kparName, buffer, size, nst) + if err != nil { + failedKparams.Add(1) + continue + } + kpars[kparName] = kpar + } + + return kpars +} + +// getParam extracts parameter value from the property buffer and builds the kparam structure. +func getParam(name string, buffer []byte, size uint32, nonStructType tdh.NonStructType) (*kevent.Kparam, error) { + if buffer == nil || len(buffer) == 0 { + return nil, errors.New("property buffer is empty") + } + + var ( + typ kparams.Type + value kparams.Value + ) + + switch nonStructType.InType { + case tdh.IntypeUnicodeString: + typ, value = kparams.UnicodeString, utf16.UTF16PtrToString(unsafe.Pointer(&buffer[0])) + case tdh.IntypeAnsiString: + typ, value = kparams.AnsiString, string((*[1<<30 - 1]byte)(unsafe.Pointer(&buffer[0]))[:size-1:size-1]) + + case tdh.IntypeInt8: + typ, value = kparams.Int8, *(*int8)(unsafe.Pointer(&buffer[0])) + case tdh.IntypeUint8: + typ, value = kparams.Uint8, *(*uint8)(unsafe.Pointer(&buffer[0])) + if nonStructType.OutType == tdh.OutypeHexInt8 { + typ = kparams.HexInt8 + } + case tdh.IntypeBoolean: + typ, value = kparams.Bool, *(*bool)(unsafe.Pointer(&buffer[0])) + + case tdh.IntypeInt16: + typ, value = kparams.Int16, *(*int16)(unsafe.Pointer(&buffer[0])) + case tdh.IntypeUint16: + typ, value = kparams.Uint16, *(*uint16)(unsafe.Pointer(&buffer[0])) + switch nonStructType.OutType { + case tdh.OutypeHexInt16: + typ = kparams.HexInt16 + case tdh.OutypePort: + typ = kparams.Port + } + + case tdh.IntypeInt32: + typ, value = kparams.Int32, *(*int32)(unsafe.Pointer(&buffer[0])) + case tdh.IntypeUint32: + typ, value = kparams.Uint32, *(*uint32)(unsafe.Pointer(&buffer[0])) + switch nonStructType.OutType { + case tdh.OutypeHexInt32: + typ = kparams.HexInt32 + case tdh.OutypeIPv4: + typ = kparams.IPv4 + } + + case tdh.IntypeInt64: + typ, value = kparams.Int64, *(*int64)(unsafe.Pointer(&buffer[0])) + case tdh.IntypeUint64: + typ, value = kparams.Uint64, *(*uint64)(unsafe.Pointer(&buffer[0])) + if nonStructType.OutType == tdh.OutypeHexInt64 { + typ = kparams.HexInt64 + } + + case tdh.IntypeFloat: + typ, value = kparams.Float, *(*float32)(unsafe.Pointer(&buffer[0])) + case tdh.IntypeDouble: + typ, value = kparams.Double, *(*float64)(unsafe.Pointer(&buffer[0])) + + case tdh.IntypeHexInt32: + typ, value = kparams.HexInt32, *(*int32)(unsafe.Pointer(&buffer[0])) + case tdh.IntypeHexInt64: + typ, value = kparams.HexInt64, *(*int64)(unsafe.Pointer(&buffer[0])) + case tdh.IntypePointer, tdh.IntypeSizet: + typ, value = kparams.HexInt64, *(*uint64)(unsafe.Pointer(&buffer[0])) + case tdh.IntypeSID: + typ, value = kparams.SID, buffer + case tdh.IntypeWbemSID: + typ, value = kparams.WbemSID, buffer + case tdh.IntypeBinary: + if nonStructType.OutType == tdh.OutypeIPv6 { + typ, value = kparams.IPv6, buffer + } else { + typ, value = kparams.Binary, buffer + } + default: + return nil, fmt.Errorf("unknown type for %q parameter", name) + } + + return kevent.NewKparam(name, typ, value), nil +} + +// isDropped discards the kernel event before it hits the output channel. +// Dropping a kernel event occurs if any of the following conditions +// are met: +// +// - kernel event is used solely for building internal state of either +// needs to be stored in the capture file for the purpose of restoring +// the state +// - process that produced the kernel event is fibratus itself +// - kernel event is present in the blacklist, and thus it is always dropped +// - finally, the event is dropped by the filter engine +func (k *kstreamConsumer) isDropped(kevt *kevent.Kevent) bool { + if kevt.Type.Dropped(k.capture) { + return true + } + if kevt.PID == currentPid { + return true + } + if k.keventsBlacklist.has(kevt.Type) { + blacklistedKevents.Add(kevt.Name, 1) + return true + } + if k.filter == nil { + return false + } + filtered := k.filter.Run(kevt) + if !filtered { + return true + } + return false +} + +// dropBlacklistProc drops the events from the blacklist if it is linked to particular process name. +func (k *kstreamConsumer) dropBlacklistProc(pid uint32) bool { + if len(k.procsBlacklist) == 0 { + return false + } + proc := k.psnapshotter.Find(pid) + if proc == nil { + return false + } + for _, blacklistProc := range k.procsBlacklist { + if strings.ToLower(proc.Name) == blacklistProc { + blacklistedProcs.Add(proc.Name, int64(1)) + return true + } + } + return false +} diff --git a/pkg/kstream/kstreamc_test.go b/pkg/kstream/kstreamc_test.go new file mode 100644 index 000000000..4021f83b3 --- /dev/null +++ b/pkg/kstream/kstreamc_test.go @@ -0,0 +1,242 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kstream + +import ( + "encoding/gob" + "github.com/rabbitstack/fibratus/pkg/config" + kerrors "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/handle" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/ps/types" + "github.com/rabbitstack/fibratus/pkg/syscall/etw" + "github.com/rabbitstack/fibratus/pkg/syscall/tdh" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "net" + "os" + "testing" + "time" +) + +func TestOpenKstream(t *testing.T) { + psnap := new(ps.SnapshotterMock) + hsnap := new(handle.SnapshotterMock) + ktraceController := NewKtraceController(config.KstreamConfig{}) + kstreamc := NewConsumer(ktraceController, psnap, hsnap, &config.Config{}) + openTrace = func(ktrace etw.EventTraceLogfile) etw.TraceHandle { + return etw.TraceHandle(2) + } + processTrace = func(handle etw.TraceHandle) error { + return nil + } + err := kstreamc.OpenKstream() + require.NoError(t, err) +} + +func TestOpenKstreamInvalidHandle(t *testing.T) { + psnap := new(ps.SnapshotterMock) + hsnap := new(handle.SnapshotterMock) + ktraceController := NewKtraceController(config.KstreamConfig{}) + kstreamc := NewConsumer(ktraceController, psnap, hsnap, &config.Config{}) + openTrace = func(ktrace etw.EventTraceLogfile) etw.TraceHandle { + return etw.TraceHandle(0xffffffffffffffff) + } + err := kstreamc.OpenKstream() + require.Error(t, err) +} + +func TestOpenKstreamKsessionNotRunning(t *testing.T) { + psnap := new(ps.SnapshotterMock) + hsnap := new(handle.SnapshotterMock) + ktraceController := NewKtraceController(config.KstreamConfig{}) + kstreamc := NewConsumer(ktraceController, psnap, hsnap, &config.Config{}) + openTrace = func(ktrace etw.EventTraceLogfile) etw.TraceHandle { + return etw.TraceHandle(2) + } + processTrace = func(handle etw.TraceHandle) error { + return kerrors.ErrKsessionNotRunning + } + err := kstreamc.OpenKstream() + require.NoError(t, err) + err = <-kstreamc.Errors() + assert.EqualError(t, err, "kernel session from which you are trying to consume events in real time is not running") +} + +func TestProcessKevent(t *testing.T) { + psnap := new(ps.SnapshotterMock) + hsnap := new(handle.SnapshotterMock) + ktraceController := NewKtraceController(config.KstreamConfig{}) + kstreamc := NewConsumer(ktraceController, psnap, hsnap, &config.Config{}) + + psnap.On("Find", mock.Anything).Return(&types.PS{Name: "cmd.exe"}) + + openTrace = func(ktrace etw.EventTraceLogfile) etw.TraceHandle { + return etw.TraceHandle(2) + } + processTrace = func(handle etw.TraceHandle) error { + return nil + } + getPropertySize = func(evt *etw.EventRecord, descriptor *tdh.PropertyDataDescriptor) (uint32, error) { + return uint32(10), nil + } + getProperty = func(evt *etw.EventRecord, descriptor *tdh.PropertyDataDescriptor, size uint32, buffer []byte) error { + return nil + } + + psnap.On("Write", mock.Anything).Return(nil) + + f, err := os.Open("./_fixtures/snapshots/create-process.gob") + if err != nil { + t.Fatal(err) + } + + dec := gob.NewDecoder(f) + var evt etw.EventRecord + err = dec.Decode(&evt) + if err != nil { + t.Fatal(err) + } + err = kstreamc.(*kstreamConsumer).processKevent(&evt) + require.NoError(t, err) + + kevt := <-kstreamc.Events() + + assert.Equal(t, ktypes.Process, kevt.Category) + assert.Equal(t, uint32(9828), kevt.Tid) + assert.Equal(t, uint8(5), kevt.CPU) + assert.Equal(t, ktypes.CreateProcess, kevt.Type) + assert.Equal(t, "CreateProcess", kevt.Name) + assert.Equal(t, "Creates a new process and its primary thread", kevt.Description) + + ts, err := time.Parse("2006-01-02 15:04:05.0000000 -0700 CEST", "2019-04-05 16:10:36.5225778 +0200 CEST") + assert.Equal(t, ts, kevt.Timestamp) + assert.Len(t, kevt.Kparams, 9) + + assert.True(t, kevt.Kparams.Contains(kparams.DTB)) + assert.True(t, kevt.Kparams.Contains(kparams.ProcessName)) +} + +func TestGetParamEmptyBuffer(t *testing.T) { + _, err := getParam("sip", nil, 16, tdh.NonStructType{InType: tdh.IntypeBinary, OutType: tdh.OutypeIPv6}) + require.Error(t, err) + + _, err = getParam("sip", []byte{}, 16, tdh.NonStructType{InType: tdh.IntypeBinary, OutType: tdh.OutypeIPv6}) + require.Error(t, err) +} + +func TestGetParam(t *testing.T) { + kpar, err := getParam("comm", []byte{99, 0, 109, 0, 100, 0, 92, 0, 102, 0, 105, 0, 98, 0, 114, 0, 97, 0, 116, 0, 117, 0, 115, 0, 92, 0, 102, 0, 105, 0, 98, 0, 114, 0, 97, 0, 116, 0, 117, 0, 115, 0, 46, 0, 101, 0, 120, 0, 101, 0, 32, 0, 32, 0, 0, 0}, 16, tdh.NonStructType{InType: tdh.IntypeUnicodeString}) + require.NoError(t, err) + assert.Equal(t, kparams.UnicodeString, kpar.Type) + assert.Equal(t, "cmd\\fibratus\\fibratus.exe ", kpar.Value) + + kpar, err = getParam("exe", []byte{77, 105, 99, 114, 111, 115, 111, 102, 116, 46, 80, 104, 111, 116, 111, 115, 46, 101, 120, 101, 0}, 21, tdh.NonStructType{InType: tdh.IntypeAnsiString}) + assert.Equal(t, kparams.AnsiString, kpar.Type) + assert.Equal(t, "Microsoft.Photos.exe", kpar.Value) + + kpar, err = getParam("flag", []byte{127}, 1, tdh.NonStructType{InType: tdh.IntypeInt8}) + assert.Equal(t, kparams.Int8, kpar.Type) + assert.Equal(t, int8(127), kpar.Value) + + kpar, err = getParam("flag", []byte{255}, 1, tdh.NonStructType{InType: tdh.IntypeUint8}) + assert.Equal(t, kparams.Uint8, kpar.Type) + assert.Equal(t, uint8(255), kpar.Value) + + kpar, err = getParam("flag", []byte{255}, 1, tdh.NonStructType{InType: tdh.IntypeUint8, OutType: tdh.OutypeHexInt8}) + assert.Equal(t, kparams.HexInt8, kpar.Type) + assert.Equal(t, kparams.Hex("ff"), kpar.Value) + + kpar, err = getParam("enabled", []byte{1}, 1, tdh.NonStructType{InType: tdh.IntypeBoolean}) + assert.Equal(t, kparams.Bool, kpar.Type) + assert.Equal(t, true, kpar.Value) + + kpar, err = getParam("enabled", []byte{0}, 1, tdh.NonStructType{InType: tdh.IntypeBoolean}) + assert.Equal(t, kparams.Bool, kpar.Type) + assert.Equal(t, false, kpar.Value) + + kpar, err = getParam("addr", []byte{255, 169}, 2, tdh.NonStructType{InType: tdh.IntypeUint16, OutType: tdh.OutypeHexInt16}) + assert.Equal(t, kparams.HexInt16, kpar.Type) + assert.Equal(t, kparams.Hex("a9ff"), kpar.Value) + + kpar, err = getParam("sport", []byte{255, 169}, 2, tdh.NonStructType{InType: tdh.IntypeUint16, OutType: tdh.OutypePort}) + assert.Equal(t, kparams.Port, kpar.Type) + assert.Equal(t, uint16(65449), kpar.Value) + + kpar, err = getParam("pid", []byte{252, 26, 0, 0}, 4, tdh.NonStructType{InType: tdh.IntypeInt32}) + assert.Equal(t, kparams.Int32, kpar.Type) + assert.Equal(t, int32(6908), kpar.Value) + + kpar, err = getParam("kproc", []byte{108, 3, 0, 0}, 4, tdh.NonStructType{InType: tdh.IntypeUint32}) + assert.Equal(t, kparams.Uint32, kpar.Type) + assert.Equal(t, uint32(876), kpar.Value) + + kpar, err = getParam("kproc", []byte{108, 3, 0, 0}, 4, tdh.NonStructType{InType: tdh.IntypeUint32, OutType: tdh.OutypeHexInt32}) + assert.Equal(t, kparams.HexInt32, kpar.Type) + assert.Equal(t, kparams.Hex("36c"), kpar.Value) + + kpar, err = getParam("dip", []byte{192, 168, 1, 210}, 4, tdh.NonStructType{InType: tdh.IntypeUint32, OutType: tdh.OutypeIPv4}) + assert.Equal(t, kparams.IPv4, kpar.Type) + assert.Equal(t, net.ParseIP("192.168.1.210"), kpar.Value) + + kpar, err = getParam("syscall.addr", []byte{192, 168, 1, 210, 8, 1, 1, 1}, 8, tdh.NonStructType{InType: tdh.IntypeInt64}) + assert.Equal(t, kparams.Int64, kpar.Type) + assert.Equal(t, int64(72340206409328832), kpar.Value) + + kpar, err = getParam("syscall.addr", []byte{192, 168, 1, 210, 199, 100, 100, 100}, 8, tdh.NonStructType{InType: tdh.IntypeUint64}) + assert.Equal(t, kparams.Uint64, kpar.Type) + assert.Equal(t, uint64(7234017710848452800), kpar.Value) + + kpar, err = getParam("syscall.addr", []byte{192, 168, 1, 210, 199, 100, 100, 100}, 8, tdh.NonStructType{InType: tdh.IntypeUint64, OutType: tdh.OutypeHexInt64}) + assert.Equal(t, kparams.HexInt64, kpar.Type) + assert.Equal(t, kparams.Hex("646464c7d201a8c0"), kpar.Value) + + kpar, err = getParam("currency", []byte{0, 0, 0, 1}, 4, tdh.NonStructType{InType: tdh.IntypeFloat}) + assert.Equal(t, kparams.Float, kpar.Type) + assert.Equal(t, float32(2.3509887e-38), kpar.Value) + + kpar, err = getParam("currency", []byte{0, 0, 0, 0, 0, 0, 0, 1}, 8, tdh.NonStructType{InType: tdh.IntypeDouble}) + assert.Equal(t, kparams.Double, kpar.Type) + assert.Equal(t, float64(7.291122019556398e-304), kpar.Value) + + kpar, err = getParam("kproc", []byte{108, 3, 0, 0}, 4, tdh.NonStructType{InType: tdh.IntypeHexInt32}) + assert.Equal(t, kparams.HexInt32, kpar.Type) + assert.Equal(t, kparams.Hex("36c"), kpar.Value) + + kpar, err = getParam("syscall.addr", []byte{192, 168, 1, 210, 199, 100, 100, 100}, 8, tdh.NonStructType{InType: tdh.IntypeHexInt64}) + assert.Equal(t, kparams.HexInt64, kpar.Type) + assert.Equal(t, kparams.Hex("646464c7d201a8c0"), kpar.Value) + + kpar, err = getParam("sid", []byte{96, 12, 161, 104, 133, 219, 255, 255, 0, 0, 0, 0, 3, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0}, 8, tdh.NonStructType{InType: tdh.IntypeWbemSID}) + assert.Equal(t, kparams.WbemSID, kpar.Type) + assert.Equal(t, "NT AUTHORITY\\SYSTEM", kpar.Value) + + kpar, err = getParam("sip", []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, 16, tdh.NonStructType{InType: tdh.IntypeBinary, OutType: tdh.OutypeIPv6}) + assert.Equal(t, kparams.IPv6, kpar.Type) + assert.Equal(t, net.ParseIP("::1"), kpar.Value) +} + +func TestGetParamUnknownType(t *testing.T) { + kpar, err := getParam("comm", []byte{99, 0, 109, 0, 100, 0, 92, 0, 102, 0, 105, 0, 98, 0, 114, 0, 97, 0, 116, 0, 117, 0, 115, 0, 92, 0, 102, 0, 105, 0, 98, 0, 114, 0, 97, 0, 116, 0, 117, 0, 115, 0, 46, 0, 101, 0, 120, 0, 101, 0, 32, 0, 32, 0, 0, 0}, 16, tdh.NonStructType{InType: tdh.IntypeFiletime}) + require.Error(t, err) + assert.Equal(t, kparams.Unknown, kpar.Type) +} diff --git a/pkg/net/types.go b/pkg/net/types.go new file mode 100644 index 000000000..6b97af19e --- /dev/null +++ b/pkg/net/types.go @@ -0,0 +1,41 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net + +// L4Proto is the type alias for the Layer 4 protocol. +type L4Proto uint8 + +const ( + // TCP identifies TCP Layer 4 protocol. + TCP L4Proto = iota + 1 + // UDP identifies UDP Layer 4 protocol. + UDP +) + +// String returns the string representation of the Layer 4 protocol. +func (proto L4Proto) String() string { + switch proto { + case TCP: + return "tcp" + case UDP: + return "udp" + default: + return "unknown" + } +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/README.md b/pkg/outputs/amqp/_fixtures/garagemq/README.md new file mode 100644 index 000000000..9574f5d10 --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/README.md @@ -0,0 +1 @@ +This is the minimal [GarageMQ](https://github.com/valinurovam/garagemq) server merely used for testing the AMQP output. \ No newline at end of file diff --git a/pkg/outputs/amqp/_fixtures/garagemq/amqp/constants_generated.go b/pkg/outputs/amqp/_fixtures/garagemq/amqp/constants_generated.go new file mode 100644 index 000000000..d39cd4e6a --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/amqp/constants_generated.go @@ -0,0 +1,356 @@ +// Package amqp for read, write, parse amqp frames +// Autogenerated code. Do not edit. +package amqp + +// FrameMethod identifier +const FrameMethod = 1 + +// FrameHeader identifier +const FrameHeader = 2 + +// FrameBody identifier +const FrameBody = 3 + +// FrameHeartbeat identifier +const FrameHeartbeat = 8 + +// FrameMinSize identifier +const FrameMinSize = 4096 + +// FrameEnd identifier +const FrameEnd = 206 + +// ReplySuccess identifier Indicates that the method completed successfully. This reply code is +// reserved for future use - the current protocol design does not use positive +// confirmation and reply codes are sent only in case of an error. +const ReplySuccess = 200 + +// ContentTooLarge identifier The client attempted to transfer content larger than the server could accept +// at the present time. The client may retry at a later time. +const ContentTooLarge = 311 + +// NoConsumers identifier When the exchange cannot deliver to a consumer when the immediate flag is +// set. As a result of pending data on the queue or the absence of any +// consumers of the queue. +const NoConsumers = 313 + +// ConnectionForced identifier An operator intervened to close the connection for some reason. The client +// may retry at some later date. +const ConnectionForced = 320 + +// InvalidPath identifier The client tried to work with an unknown virtual host. +const InvalidPath = 402 + +// AccessRefused identifier The client attempted to work with a server entity to which it has no +// access due to security settings. +const AccessRefused = 403 + +// NotFound identifier The client attempted to work with a server entity that does not exist. +const NotFound = 404 + +// ResourceLocked identifier The client attempted to work with a server entity to which it has no +// access because another client is working with it. +const ResourceLocked = 405 + +// PreconditionFailed identifier The client requested a method that was not allowed because some precondition +// failed. +const PreconditionFailed = 406 + +// FrameError identifier The sender sent a malformed frame that the recipient could not decode. +// This strongly implies a programming error in the sending peer. +const FrameError = 501 + +// SyntaxError identifier The sender sent a frame that contained illegal values for one or more +// fields. This strongly implies a programming error in the sending peer. +const SyntaxError = 502 + +// CommandInvalid identifier The client sent an invalid sequence of frames, attempting to perform an +// operation that was considered invalid by the server. This usually implies +// a programming error in the client. +const CommandInvalid = 503 + +// ChannelError identifier The client attempted to work with a channel that had not been correctly +// opened. This most likely indicates a fault in the client layer. +const ChannelError = 504 + +// UnexpectedFrame identifier The peer sent a frame that was not expected, usually in the context of +// a content header and body. This strongly indicates a fault in the peer's +// content processing. +const UnexpectedFrame = 505 + +// ResourceError identifier The server could not complete the method because it lacked sufficient +// resources. This may be due to the client creating too many of some type +// of entity. +const ResourceError = 506 + +// NotAllowed identifier The client tried to work with some entity in a manner that is prohibited +// by the server, due to security settings or by some other criteria. +const NotAllowed = 530 + +// NotImplemented identifier The client tried to use functionality that is not implemented in the +// server. +const NotImplemented = 540 + +// InternalError identifier The server could not complete the method because of an internal error. +// The server may require intervention by an operator in order to resume +// normal operations. +const InternalError = 541 + +// ClassConnection identifier +const ClassConnection = 10 + +// MethodConnectionStart identifier +const MethodConnectionStart = 10 + +// MethodConnectionStartOk identifier +const MethodConnectionStartOk = 11 + +// MethodConnectionSecure identifier +const MethodConnectionSecure = 20 + +// MethodConnectionSecureOk identifier +const MethodConnectionSecureOk = 21 + +// MethodConnectionTune identifier +const MethodConnectionTune = 30 + +// MethodConnectionTuneOk identifier +const MethodConnectionTuneOk = 31 + +// MethodConnectionOpen identifier +const MethodConnectionOpen = 40 + +// MethodConnectionOpenOk identifier +const MethodConnectionOpenOk = 41 + +// MethodConnectionClose identifier +const MethodConnectionClose = 50 + +// MethodConnectionCloseOk identifier +const MethodConnectionCloseOk = 51 + +// MethodConnectionBlocked identifier +const MethodConnectionBlocked = 60 + +// MethodConnectionUnblocked identifier +const MethodConnectionUnblocked = 61 + +// ClassChannel identifier +const ClassChannel = 20 + +// MethodChannelOpen identifier +const MethodChannelOpen = 10 + +// MethodChannelOpenOk identifier +const MethodChannelOpenOk = 11 + +// MethodChannelFlow identifier +const MethodChannelFlow = 20 + +// MethodChannelFlowOk identifier +const MethodChannelFlowOk = 21 + +// MethodChannelClose identifier +const MethodChannelClose = 40 + +// MethodChannelCloseOk identifier +const MethodChannelCloseOk = 41 + +// ClassExchange identifier +const ClassExchange = 40 + +// MethodExchangeDeclare identifier +const MethodExchangeDeclare = 10 + +// MethodExchangeDeclareOk identifier +const MethodExchangeDeclareOk = 11 + +// MethodExchangeDelete identifier +const MethodExchangeDelete = 20 + +// MethodExchangeDeleteOk identifier +const MethodExchangeDeleteOk = 21 + +// MethodExchangeBind identifier +const MethodExchangeBind = 30 + +// MethodExchangeBindOk identifier +const MethodExchangeBindOk = 31 + +// MethodExchangeUnbind identifier +const MethodExchangeUnbind = 40 + +// MethodExchangeUnbindOk identifier +const MethodExchangeUnbindOk = 51 + +// ClassQueue identifier +const ClassQueue = 50 + +// MethodQueueDeclare identifier +const MethodQueueDeclare = 10 + +// MethodQueueDeclareOk identifier +const MethodQueueDeclareOk = 11 + +// MethodQueueBind identifier +const MethodQueueBind = 20 + +// MethodQueueBindOk identifier +const MethodQueueBindOk = 21 + +// MethodQueueUnbind identifier +const MethodQueueUnbind = 50 + +// MethodQueueUnbindOk identifier +const MethodQueueUnbindOk = 51 + +// MethodQueuePurge identifier +const MethodQueuePurge = 30 + +// MethodQueuePurgeOk identifier +const MethodQueuePurgeOk = 31 + +// MethodQueueDelete identifier +const MethodQueueDelete = 40 + +// MethodQueueDeleteOk identifier +const MethodQueueDeleteOk = 41 + +// ClassBasic identifier +const ClassBasic = 60 + +// MethodBasicQos identifier +const MethodBasicQos = 10 + +// MethodBasicQosOk identifier +const MethodBasicQosOk = 11 + +// MethodBasicConsume identifier +const MethodBasicConsume = 20 + +// MethodBasicConsumeOk identifier +const MethodBasicConsumeOk = 21 + +// MethodBasicCancel identifier +const MethodBasicCancel = 30 + +// MethodBasicCancelOk identifier +const MethodBasicCancelOk = 31 + +// MethodBasicPublish identifier +const MethodBasicPublish = 40 + +// MethodBasicReturn identifier +const MethodBasicReturn = 50 + +// MethodBasicDeliver identifier +const MethodBasicDeliver = 60 + +// MethodBasicGet identifier +const MethodBasicGet = 70 + +// MethodBasicGetOk identifier +const MethodBasicGetOk = 71 + +// MethodBasicGetEmpty identifier +const MethodBasicGetEmpty = 72 + +// MethodBasicAck identifier +const MethodBasicAck = 80 + +// MethodBasicReject identifier +const MethodBasicReject = 90 + +// MethodBasicRecoverAsync identifier +const MethodBasicRecoverAsync = 100 + +// MethodBasicRecover identifier +const MethodBasicRecover = 110 + +// MethodBasicRecoverOk identifier +const MethodBasicRecoverOk = 111 + +// MethodBasicNack identifier +const MethodBasicNack = 120 + +// ClassTx identifier +const ClassTx = 90 + +// MethodTxSelect identifier +const MethodTxSelect = 10 + +// MethodTxSelectOk identifier +const MethodTxSelectOk = 11 + +// MethodTxCommit identifier +const MethodTxCommit = 20 + +// MethodTxCommitOk identifier +const MethodTxCommitOk = 21 + +// MethodTxRollback identifier +const MethodTxRollback = 30 + +// MethodTxRollbackOk identifier +const MethodTxRollbackOk = 31 + +// ClassConfirm identifier +const ClassConfirm = 85 + +// MethodConfirmSelect identifier +const MethodConfirmSelect = 10 + +// MethodConfirmSelectOk identifier +const MethodConfirmSelectOk = 11 + +// ConstantsNameMap map for mapping error codes into error messages +var ConstantsNameMap = map[uint16]string{ + + 1: "FRAME_METHOD", + + 2: "FRAME_HEADER", + + 3: "FRAME_BODY", + + 8: "FRAME_HEARTBEAT", + + 4096: "FRAME_MIN_SIZE", + + 206: "FRAME_END", + + 200: "REPLY_SUCCESS", + + 311: "CONTENT_TOO_LARGE", + + 313: "NO_CONSUMERS", + + 320: "CONNECTION_FORCED", + + 402: "INVALID_PATH", + + 403: "ACCESS_REFUSED", + + 404: "NOT_FOUND", + + 405: "RESOURCE_LOCKED", + + 406: "PRECONDITION_FAILED", + + 501: "FRAME_ERROR", + + 502: "SYNTAX_ERROR", + + 503: "COMMAND_INVALID", + + 504: "CHANNEL_ERROR", + + 505: "UNEXPECTED_FRAME", + + 506: "RESOURCE_ERROR", + + 530: "NOT_ALLOWED", + + 540: "NOT_IMPLEMENTED", + + 541: "INTERNAL_ERROR", +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/amqp/extended_constants.go b/pkg/outputs/amqp/_fixtures/garagemq/amqp/extended_constants.go new file mode 100644 index 000000000..89656a626 --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/amqp/extended_constants.go @@ -0,0 +1,5 @@ +package amqp + +// NoRoute returns when a 'mandatory' message cannot be delivered to any queue. +// @see https://www.rabbitmq.com/amqp-0-9-1-errata.html#section_17 +const NoRoute = 312 diff --git a/pkg/outputs/amqp/_fixtures/garagemq/amqp/methods_generated.go b/pkg/outputs/amqp/_fixtures/garagemq/amqp/methods_generated.go new file mode 100644 index 000000000..ae563d707 --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/amqp/methods_generated.go @@ -0,0 +1,4899 @@ +// Package amqp for read, write, parse amqp frames +// Autogenerated code. Do not edit. +package amqp + +import ( + "fmt" + "io" + "time" +) + +// Method represents base method interface +type Method interface { + Name() string + FrameType() byte + ClassIdentifier() uint16 + MethodIdentifier() uint16 + Read(reader io.Reader, protoVersion string) (err error) + Write(writer io.Writer, protoVersion string) (err error) + Sync() bool +} + +// Connection methods + +// ConnectionStart This method starts the connection negotiation process by telling the client the +// protocol version that the server proposes, along with a list of security mechanisms +// which the client can use for authentication. +type ConnectionStart struct { + VersionMajor byte + VersionMinor byte + ServerProperties *Table + Mechanisms []byte + Locales []byte +} + +// Name returns method name as string, usefully for logging +func (method *ConnectionStart) Name() string { + return "ConnectionStart" +} + +// FrameType returns method frame type +func (method *ConnectionStart) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ConnectionStart) ClassIdentifier() uint16 { + return 10 +} + +// MethodIdentifier returns method methodID +func (method *ConnectionStart) MethodIdentifier() uint16 { + return 10 +} + +// Sync is method should me sent synchronous +func (method *ConnectionStart) Sync() bool { + return true +} + +// Read method from io reader +func (method *ConnectionStart) Read(reader io.Reader, protoVersion string) (err error) { + + method.VersionMajor, err = ReadOctet(reader) + if err != nil { + return err + } + + method.VersionMinor, err = ReadOctet(reader) + if err != nil { + return err + } + + method.ServerProperties, err = ReadTable(reader, protoVersion) + if err != nil { + return err + } + + method.Mechanisms, err = ReadLongstr(reader) + if err != nil { + return err + } + + method.Locales, err = ReadLongstr(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *ConnectionStart) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteOctet(writer, method.VersionMajor); err != nil { + return err + } + + if err = WriteOctet(writer, method.VersionMinor); err != nil { + return err + } + + if err = WriteTable(writer, method.ServerProperties, protoVersion); err != nil { + return err + } + + if err = WriteLongstr(writer, method.Mechanisms); err != nil { + return err + } + + if err = WriteLongstr(writer, method.Locales); err != nil { + return err + } + + return +} + +// ConnectionStartOk This method selects a SASL security mechanism. +type ConnectionStartOk struct { + ClientProperties *Table + Mechanism string + Response []byte + Locale string +} + +// Name returns method name as string, usefully for logging +func (method *ConnectionStartOk) Name() string { + return "ConnectionStartOk" +} + +// FrameType returns method frame type +func (method *ConnectionStartOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ConnectionStartOk) ClassIdentifier() uint16 { + return 10 +} + +// MethodIdentifier returns method methodID +func (method *ConnectionStartOk) MethodIdentifier() uint16 { + return 11 +} + +// Sync is method should me sent synchronous +func (method *ConnectionStartOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *ConnectionStartOk) Read(reader io.Reader, protoVersion string) (err error) { + + method.ClientProperties, err = ReadTable(reader, protoVersion) + if err != nil { + return err + } + + method.Mechanism, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.Response, err = ReadLongstr(reader) + if err != nil { + return err + } + + method.Locale, err = ReadShortstr(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *ConnectionStartOk) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteTable(writer, method.ClientProperties, protoVersion); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Mechanism); err != nil { + return err + } + + if err = WriteLongstr(writer, method.Response); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Locale); err != nil { + return err + } + + return +} + +// ConnectionSecure The SASL protocol works by exchanging challenges and responses until both peers have +// received sufficient information to authenticate each other. This method challenges +// the client to provide more information. +type ConnectionSecure struct { + Challenge []byte +} + +// Name returns method name as string, usefully for logging +func (method *ConnectionSecure) Name() string { + return "ConnectionSecure" +} + +// FrameType returns method frame type +func (method *ConnectionSecure) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ConnectionSecure) ClassIdentifier() uint16 { + return 10 +} + +// MethodIdentifier returns method methodID +func (method *ConnectionSecure) MethodIdentifier() uint16 { + return 20 +} + +// Sync is method should me sent synchronous +func (method *ConnectionSecure) Sync() bool { + return true +} + +// Read method from io reader +func (method *ConnectionSecure) Read(reader io.Reader, protoVersion string) (err error) { + + method.Challenge, err = ReadLongstr(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *ConnectionSecure) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteLongstr(writer, method.Challenge); err != nil { + return err + } + + return +} + +// ConnectionSecureOk This method attempts to authenticate, passing a block of SASL data for the security +// mechanism at the server side. +type ConnectionSecureOk struct { + Response []byte +} + +// Name returns method name as string, usefully for logging +func (method *ConnectionSecureOk) Name() string { + return "ConnectionSecureOk" +} + +// FrameType returns method frame type +func (method *ConnectionSecureOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ConnectionSecureOk) ClassIdentifier() uint16 { + return 10 +} + +// MethodIdentifier returns method methodID +func (method *ConnectionSecureOk) MethodIdentifier() uint16 { + return 21 +} + +// Sync is method should me sent synchronous +func (method *ConnectionSecureOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *ConnectionSecureOk) Read(reader io.Reader, protoVersion string) (err error) { + + method.Response, err = ReadLongstr(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *ConnectionSecureOk) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteLongstr(writer, method.Response); err != nil { + return err + } + + return +} + +// ConnectionTune This method proposes a set of connection configuration values to the client. The +// client can accept and/or adjust these. +type ConnectionTune struct { + ChannelMax uint16 + FrameMax uint32 + Heartbeat uint16 +} + +// Name returns method name as string, usefully for logging +func (method *ConnectionTune) Name() string { + return "ConnectionTune" +} + +// FrameType returns method frame type +func (method *ConnectionTune) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ConnectionTune) ClassIdentifier() uint16 { + return 10 +} + +// MethodIdentifier returns method methodID +func (method *ConnectionTune) MethodIdentifier() uint16 { + return 30 +} + +// Sync is method should me sent synchronous +func (method *ConnectionTune) Sync() bool { + return true +} + +// Read method from io reader +func (method *ConnectionTune) Read(reader io.Reader, protoVersion string) (err error) { + + method.ChannelMax, err = ReadShort(reader) + if err != nil { + return err + } + + method.FrameMax, err = ReadLong(reader) + if err != nil { + return err + } + + method.Heartbeat, err = ReadShort(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *ConnectionTune) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShort(writer, method.ChannelMax); err != nil { + return err + } + + if err = WriteLong(writer, method.FrameMax); err != nil { + return err + } + + if err = WriteShort(writer, method.Heartbeat); err != nil { + return err + } + + return +} + +// ConnectionTuneOk This method sends the client's connection tuning parameters to the server. +// Certain fields are negotiated, others provide capability information. +type ConnectionTuneOk struct { + ChannelMax uint16 + FrameMax uint32 + Heartbeat uint16 +} + +// Name returns method name as string, usefully for logging +func (method *ConnectionTuneOk) Name() string { + return "ConnectionTuneOk" +} + +// FrameType returns method frame type +func (method *ConnectionTuneOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ConnectionTuneOk) ClassIdentifier() uint16 { + return 10 +} + +// MethodIdentifier returns method methodID +func (method *ConnectionTuneOk) MethodIdentifier() uint16 { + return 31 +} + +// Sync is method should me sent synchronous +func (method *ConnectionTuneOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *ConnectionTuneOk) Read(reader io.Reader, protoVersion string) (err error) { + + method.ChannelMax, err = ReadShort(reader) + if err != nil { + return err + } + + method.FrameMax, err = ReadLong(reader) + if err != nil { + return err + } + + method.Heartbeat, err = ReadShort(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *ConnectionTuneOk) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShort(writer, method.ChannelMax); err != nil { + return err + } + + if err = WriteLong(writer, method.FrameMax); err != nil { + return err + } + + if err = WriteShort(writer, method.Heartbeat); err != nil { + return err + } + + return +} + +// ConnectionOpen This method opens a connection to a virtual host, which is a collection of +// resources, and acts to separate multiple application domains within a server. +// The server may apply arbitrary limits per virtual host, such as the number +// of each type of entity that may be used, per connection and/or in total. +type ConnectionOpen struct { + VirtualHost string + Reserved1 string + Reserved2 bool +} + +// Name returns method name as string, usefully for logging +func (method *ConnectionOpen) Name() string { + return "ConnectionOpen" +} + +// FrameType returns method frame type +func (method *ConnectionOpen) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ConnectionOpen) ClassIdentifier() uint16 { + return 10 +} + +// MethodIdentifier returns method methodID +func (method *ConnectionOpen) MethodIdentifier() uint16 { + return 40 +} + +// Sync is method should me sent synchronous +func (method *ConnectionOpen) Sync() bool { + return true +} + +// Read method from io reader +func (method *ConnectionOpen) Read(reader io.Reader, protoVersion string) (err error) { + + method.VirtualHost, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.Reserved1, err = ReadShortstr(reader) + if err != nil { + return err + } + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.Reserved2 = bits&(1<<0) != 0 + + return +} + +// Write method from io reader +func (method *ConnectionOpen) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShortstr(writer, method.VirtualHost); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Reserved1); err != nil { + return err + } + + var bits byte + + if method.Reserved2 { + bits |= 1 << 0 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + return +} + +// ConnectionOpenOk This method signals to the client that the connection is ready for use. +type ConnectionOpenOk struct { + Reserved1 string +} + +// Name returns method name as string, usefully for logging +func (method *ConnectionOpenOk) Name() string { + return "ConnectionOpenOk" +} + +// FrameType returns method frame type +func (method *ConnectionOpenOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ConnectionOpenOk) ClassIdentifier() uint16 { + return 10 +} + +// MethodIdentifier returns method methodID +func (method *ConnectionOpenOk) MethodIdentifier() uint16 { + return 41 +} + +// Sync is method should me sent synchronous +func (method *ConnectionOpenOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *ConnectionOpenOk) Read(reader io.Reader, protoVersion string) (err error) { + + method.Reserved1, err = ReadShortstr(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *ConnectionOpenOk) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShortstr(writer, method.Reserved1); err != nil { + return err + } + + return +} + +// ConnectionClose This method indicates that the sender wants to close the connection. This may be +// due to internal conditions (e.g. a forced shut-down) or due to an error handling +// a specific method, i.e. an exception. When a close is due to an exception, the +// sender provides the class and method id of the method which caused the exception. +type ConnectionClose struct { + ReplyCode uint16 + ReplyText string + ClassID uint16 + MethodID uint16 +} + +// Name returns method name as string, usefully for logging +func (method *ConnectionClose) Name() string { + return "ConnectionClose" +} + +// FrameType returns method frame type +func (method *ConnectionClose) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ConnectionClose) ClassIdentifier() uint16 { + return 10 +} + +// MethodIdentifier returns method methodID +func (method *ConnectionClose) MethodIdentifier() uint16 { + return 50 +} + +// Sync is method should me sent synchronous +func (method *ConnectionClose) Sync() bool { + return true +} + +// Read method from io reader +func (method *ConnectionClose) Read(reader io.Reader, protoVersion string) (err error) { + + method.ReplyCode, err = ReadShort(reader) + if err != nil { + return err + } + + method.ReplyText, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.ClassID, err = ReadShort(reader) + if err != nil { + return err + } + + method.MethodID, err = ReadShort(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *ConnectionClose) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShort(writer, method.ReplyCode); err != nil { + return err + } + + if err = WriteShortstr(writer, method.ReplyText); err != nil { + return err + } + + if err = WriteShort(writer, method.ClassID); err != nil { + return err + } + + if err = WriteShort(writer, method.MethodID); err != nil { + return err + } + + return +} + +// ConnectionCloseOk This method confirms a Connection.Close method and tells the recipient that it is +// safe to release resources for the connection and close the socket. +type ConnectionCloseOk struct { +} + +// Name returns method name as string, usefully for logging +func (method *ConnectionCloseOk) Name() string { + return "ConnectionCloseOk" +} + +// FrameType returns method frame type +func (method *ConnectionCloseOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ConnectionCloseOk) ClassIdentifier() uint16 { + return 10 +} + +// MethodIdentifier returns method methodID +func (method *ConnectionCloseOk) MethodIdentifier() uint16 { + return 51 +} + +// Sync is method should me sent synchronous +func (method *ConnectionCloseOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *ConnectionCloseOk) Read(reader io.Reader, protoVersion string) (err error) { + + return +} + +// Write method from io reader +func (method *ConnectionCloseOk) Write(writer io.Writer, protoVersion string) (err error) { + + return +} + +// ConnectionBlocked This method indicates that a connection has been blocked +// and does not accept new publishes. +type ConnectionBlocked struct { + Reason string +} + +// Name returns method name as string, usefully for logging +func (method *ConnectionBlocked) Name() string { + return "ConnectionBlocked" +} + +// FrameType returns method frame type +func (method *ConnectionBlocked) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ConnectionBlocked) ClassIdentifier() uint16 { + return 10 +} + +// MethodIdentifier returns method methodID +func (method *ConnectionBlocked) MethodIdentifier() uint16 { + return 60 +} + +// Sync is method should me sent synchronous +func (method *ConnectionBlocked) Sync() bool { + return false +} + +// Read method from io reader +func (method *ConnectionBlocked) Read(reader io.Reader, protoVersion string) (err error) { + + method.Reason, err = ReadShortstr(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *ConnectionBlocked) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShortstr(writer, method.Reason); err != nil { + return err + } + + return +} + +// ConnectionUnblocked This method indicates that a connection has been unblocked +// and now accepts publishes. +type ConnectionUnblocked struct { +} + +// Name returns method name as string, usefully for logging +func (method *ConnectionUnblocked) Name() string { + return "ConnectionUnblocked" +} + +// FrameType returns method frame type +func (method *ConnectionUnblocked) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ConnectionUnblocked) ClassIdentifier() uint16 { + return 10 +} + +// MethodIdentifier returns method methodID +func (method *ConnectionUnblocked) MethodIdentifier() uint16 { + return 61 +} + +// Sync is method should me sent synchronous +func (method *ConnectionUnblocked) Sync() bool { + return false +} + +// Read method from io reader +func (method *ConnectionUnblocked) Read(reader io.Reader, protoVersion string) (err error) { + + return +} + +// Write method from io reader +func (method *ConnectionUnblocked) Write(writer io.Writer, protoVersion string) (err error) { + + return +} + +// Channel methods + +// ChannelOpen This method opens a channel to the server. +type ChannelOpen struct { + Reserved1 string +} + +// Name returns method name as string, usefully for logging +func (method *ChannelOpen) Name() string { + return "ChannelOpen" +} + +// FrameType returns method frame type +func (method *ChannelOpen) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ChannelOpen) ClassIdentifier() uint16 { + return 20 +} + +// MethodIdentifier returns method methodID +func (method *ChannelOpen) MethodIdentifier() uint16 { + return 10 +} + +// Sync is method should me sent synchronous +func (method *ChannelOpen) Sync() bool { + return true +} + +// Read method from io reader +func (method *ChannelOpen) Read(reader io.Reader, protoVersion string) (err error) { + + method.Reserved1, err = ReadShortstr(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *ChannelOpen) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShortstr(writer, method.Reserved1); err != nil { + return err + } + + return +} + +// ChannelOpenOk This method signals to the client that the channel is ready for use. +type ChannelOpenOk struct { + Reserved1 []byte +} + +// Name returns method name as string, usefully for logging +func (method *ChannelOpenOk) Name() string { + return "ChannelOpenOk" +} + +// FrameType returns method frame type +func (method *ChannelOpenOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ChannelOpenOk) ClassIdentifier() uint16 { + return 20 +} + +// MethodIdentifier returns method methodID +func (method *ChannelOpenOk) MethodIdentifier() uint16 { + return 11 +} + +// Sync is method should me sent synchronous +func (method *ChannelOpenOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *ChannelOpenOk) Read(reader io.Reader, protoVersion string) (err error) { + + method.Reserved1, err = ReadLongstr(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *ChannelOpenOk) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteLongstr(writer, method.Reserved1); err != nil { + return err + } + + return +} + +// ChannelFlow This method asks the peer to pause or restart the flow of content data sent by +// a consumer. This is a simple flow-control mechanism that a peer can use to avoid +// overflowing its queues or otherwise finding itself receiving more messages than +// it can process. Note that this method is not intended for window control. It does +// not affect contents returned by Basic.Get-Ok methods. +type ChannelFlow struct { + Active bool +} + +// Name returns method name as string, usefully for logging +func (method *ChannelFlow) Name() string { + return "ChannelFlow" +} + +// FrameType returns method frame type +func (method *ChannelFlow) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ChannelFlow) ClassIdentifier() uint16 { + return 20 +} + +// MethodIdentifier returns method methodID +func (method *ChannelFlow) MethodIdentifier() uint16 { + return 20 +} + +// Sync is method should me sent synchronous +func (method *ChannelFlow) Sync() bool { + return true +} + +// Read method from io reader +func (method *ChannelFlow) Read(reader io.Reader, protoVersion string) (err error) { + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.Active = bits&(1<<0) != 0 + + return +} + +// Write method from io reader +func (method *ChannelFlow) Write(writer io.Writer, protoVersion string) (err error) { + + var bits byte + + if method.Active { + bits |= 1 << 0 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + return +} + +// ChannelFlowOk Confirms to the peer that a flow command was received and processed. +type ChannelFlowOk struct { + Active bool +} + +// Name returns method name as string, usefully for logging +func (method *ChannelFlowOk) Name() string { + return "ChannelFlowOk" +} + +// FrameType returns method frame type +func (method *ChannelFlowOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ChannelFlowOk) ClassIdentifier() uint16 { + return 20 +} + +// MethodIdentifier returns method methodID +func (method *ChannelFlowOk) MethodIdentifier() uint16 { + return 21 +} + +// Sync is method should me sent synchronous +func (method *ChannelFlowOk) Sync() bool { + return false +} + +// Read method from io reader +func (method *ChannelFlowOk) Read(reader io.Reader, protoVersion string) (err error) { + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.Active = bits&(1<<0) != 0 + + return +} + +// Write method from io reader +func (method *ChannelFlowOk) Write(writer io.Writer, protoVersion string) (err error) { + + var bits byte + + if method.Active { + bits |= 1 << 0 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + return +} + +// ChannelClose This method indicates that the sender wants to close the channel. This may be due to +// internal conditions (e.g. a forced shut-down) or due to an error handling a specific +// method, i.e. an exception. When a close is due to an exception, the sender provides +// the class and method id of the method which caused the exception. +type ChannelClose struct { + ReplyCode uint16 + ReplyText string + ClassID uint16 + MethodID uint16 +} + +// Name returns method name as string, usefully for logging +func (method *ChannelClose) Name() string { + return "ChannelClose" +} + +// FrameType returns method frame type +func (method *ChannelClose) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ChannelClose) ClassIdentifier() uint16 { + return 20 +} + +// MethodIdentifier returns method methodID +func (method *ChannelClose) MethodIdentifier() uint16 { + return 40 +} + +// Sync is method should me sent synchronous +func (method *ChannelClose) Sync() bool { + return true +} + +// Read method from io reader +func (method *ChannelClose) Read(reader io.Reader, protoVersion string) (err error) { + + method.ReplyCode, err = ReadShort(reader) + if err != nil { + return err + } + + method.ReplyText, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.ClassID, err = ReadShort(reader) + if err != nil { + return err + } + + method.MethodID, err = ReadShort(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *ChannelClose) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShort(writer, method.ReplyCode); err != nil { + return err + } + + if err = WriteShortstr(writer, method.ReplyText); err != nil { + return err + } + + if err = WriteShort(writer, method.ClassID); err != nil { + return err + } + + if err = WriteShort(writer, method.MethodID); err != nil { + return err + } + + return +} + +// ChannelCloseOk This method confirms a Channel.Close method and tells the recipient that it is safe +// to release resources for the channel. +type ChannelCloseOk struct { +} + +// Name returns method name as string, usefully for logging +func (method *ChannelCloseOk) Name() string { + return "ChannelCloseOk" +} + +// FrameType returns method frame type +func (method *ChannelCloseOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ChannelCloseOk) ClassIdentifier() uint16 { + return 20 +} + +// MethodIdentifier returns method methodID +func (method *ChannelCloseOk) MethodIdentifier() uint16 { + return 41 +} + +// Sync is method should me sent synchronous +func (method *ChannelCloseOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *ChannelCloseOk) Read(reader io.Reader, protoVersion string) (err error) { + + return +} + +// Write method from io reader +func (method *ChannelCloseOk) Write(writer io.Writer, protoVersion string) (err error) { + + return +} + +// Exchange methods + +// ExchangeDeclare This method creates an exchange if it does not already exist, and if the exchange +// exists, verifies that it is of the correct and expected class. +type ExchangeDeclare struct { + Reserved1 uint16 + Exchange string + Type string + Passive bool + Durable bool + AutoDelete bool + Internal bool + NoWait bool + Arguments *Table +} + +// Name returns method name as string, usefully for logging +func (method *ExchangeDeclare) Name() string { + return "ExchangeDeclare" +} + +// FrameType returns method frame type +func (method *ExchangeDeclare) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ExchangeDeclare) ClassIdentifier() uint16 { + return 40 +} + +// MethodIdentifier returns method methodID +func (method *ExchangeDeclare) MethodIdentifier() uint16 { + return 10 +} + +// Sync is method should me sent synchronous +func (method *ExchangeDeclare) Sync() bool { + return true +} + +// Read method from io reader +func (method *ExchangeDeclare) Read(reader io.Reader, protoVersion string) (err error) { + + method.Reserved1, err = ReadShort(reader) + if err != nil { + return err + } + + method.Exchange, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.Type, err = ReadShortstr(reader) + if err != nil { + return err + } + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.Passive = bits&(1<<0) != 0 + + method.Durable = bits&(1<<1) != 0 + + method.AutoDelete = bits&(1<<2) != 0 + + method.Internal = bits&(1<<3) != 0 + + method.NoWait = bits&(1<<4) != 0 + + method.Arguments, err = ReadTable(reader, protoVersion) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *ExchangeDeclare) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShort(writer, method.Reserved1); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Exchange); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Type); err != nil { + return err + } + + var bits byte + + if method.Passive { + bits |= 1 << 0 + } + + if method.Durable { + bits |= 1 << 1 + } + + if method.AutoDelete { + bits |= 1 << 2 + } + + if method.Internal { + bits |= 1 << 3 + } + + if method.NoWait { + bits |= 1 << 4 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + if err = WriteTable(writer, method.Arguments, protoVersion); err != nil { + return err + } + + return +} + +// ExchangeDeclareOk This method confirms a Declare method and confirms the name of the exchange, +// essential for automatically-named exchanges. +type ExchangeDeclareOk struct { +} + +// Name returns method name as string, usefully for logging +func (method *ExchangeDeclareOk) Name() string { + return "ExchangeDeclareOk" +} + +// FrameType returns method frame type +func (method *ExchangeDeclareOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ExchangeDeclareOk) ClassIdentifier() uint16 { + return 40 +} + +// MethodIdentifier returns method methodID +func (method *ExchangeDeclareOk) MethodIdentifier() uint16 { + return 11 +} + +// Sync is method should me sent synchronous +func (method *ExchangeDeclareOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *ExchangeDeclareOk) Read(reader io.Reader, protoVersion string) (err error) { + + return +} + +// Write method from io reader +func (method *ExchangeDeclareOk) Write(writer io.Writer, protoVersion string) (err error) { + + return +} + +// ExchangeDelete This method deletes an exchange. When an exchange is deleted all queue bindings on +// the exchange are cancelled. +type ExchangeDelete struct { + Reserved1 uint16 + Exchange string + IfUnused bool + NoWait bool +} + +// Name returns method name as string, usefully for logging +func (method *ExchangeDelete) Name() string { + return "ExchangeDelete" +} + +// FrameType returns method frame type +func (method *ExchangeDelete) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ExchangeDelete) ClassIdentifier() uint16 { + return 40 +} + +// MethodIdentifier returns method methodID +func (method *ExchangeDelete) MethodIdentifier() uint16 { + return 20 +} + +// Sync is method should me sent synchronous +func (method *ExchangeDelete) Sync() bool { + return true +} + +// Read method from io reader +func (method *ExchangeDelete) Read(reader io.Reader, protoVersion string) (err error) { + + method.Reserved1, err = ReadShort(reader) + if err != nil { + return err + } + + method.Exchange, err = ReadShortstr(reader) + if err != nil { + return err + } + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.IfUnused = bits&(1<<0) != 0 + + method.NoWait = bits&(1<<1) != 0 + + return +} + +// Write method from io reader +func (method *ExchangeDelete) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShort(writer, method.Reserved1); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Exchange); err != nil { + return err + } + + var bits byte + + if method.IfUnused { + bits |= 1 << 0 + } + + if method.NoWait { + bits |= 1 << 1 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + return +} + +// ExchangeDeleteOk This method confirms the deletion of an exchange. +type ExchangeDeleteOk struct { +} + +// Name returns method name as string, usefully for logging +func (method *ExchangeDeleteOk) Name() string { + return "ExchangeDeleteOk" +} + +// FrameType returns method frame type +func (method *ExchangeDeleteOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ExchangeDeleteOk) ClassIdentifier() uint16 { + return 40 +} + +// MethodIdentifier returns method methodID +func (method *ExchangeDeleteOk) MethodIdentifier() uint16 { + return 21 +} + +// Sync is method should me sent synchronous +func (method *ExchangeDeleteOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *ExchangeDeleteOk) Read(reader io.Reader, protoVersion string) (err error) { + + return +} + +// Write method from io reader +func (method *ExchangeDeleteOk) Write(writer io.Writer, protoVersion string) (err error) { + + return +} + +// ExchangeBind This method binds an exchange to an exchange. +type ExchangeBind struct { + Reserved1 uint16 + Destination string + Source string + RoutingKey string + NoWait bool + Arguments *Table +} + +// Name returns method name as string, usefully for logging +func (method *ExchangeBind) Name() string { + return "ExchangeBind" +} + +// FrameType returns method frame type +func (method *ExchangeBind) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ExchangeBind) ClassIdentifier() uint16 { + return 40 +} + +// MethodIdentifier returns method methodID +func (method *ExchangeBind) MethodIdentifier() uint16 { + return 30 +} + +// Sync is method should me sent synchronous +func (method *ExchangeBind) Sync() bool { + return true +} + +// Read method from io reader +func (method *ExchangeBind) Read(reader io.Reader, protoVersion string) (err error) { + + method.Reserved1, err = ReadShort(reader) + if err != nil { + return err + } + + method.Destination, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.Source, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.RoutingKey, err = ReadShortstr(reader) + if err != nil { + return err + } + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.NoWait = bits&(1<<0) != 0 + + method.Arguments, err = ReadTable(reader, protoVersion) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *ExchangeBind) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShort(writer, method.Reserved1); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Destination); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Source); err != nil { + return err + } + + if err = WriteShortstr(writer, method.RoutingKey); err != nil { + return err + } + + var bits byte + + if method.NoWait { + bits |= 1 << 0 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + if err = WriteTable(writer, method.Arguments, protoVersion); err != nil { + return err + } + + return +} + +// ExchangeBindOk This method confirms that the bind was successful. +type ExchangeBindOk struct { +} + +// Name returns method name as string, usefully for logging +func (method *ExchangeBindOk) Name() string { + return "ExchangeBindOk" +} + +// FrameType returns method frame type +func (method *ExchangeBindOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ExchangeBindOk) ClassIdentifier() uint16 { + return 40 +} + +// MethodIdentifier returns method methodID +func (method *ExchangeBindOk) MethodIdentifier() uint16 { + return 31 +} + +// Sync is method should me sent synchronous +func (method *ExchangeBindOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *ExchangeBindOk) Read(reader io.Reader, protoVersion string) (err error) { + + return +} + +// Write method from io reader +func (method *ExchangeBindOk) Write(writer io.Writer, protoVersion string) (err error) { + + return +} + +// ExchangeUnbind This method unbinds an exchange from an exchange. +type ExchangeUnbind struct { + Reserved1 uint16 + Destination string + Source string + RoutingKey string + NoWait bool + Arguments *Table +} + +// Name returns method name as string, usefully for logging +func (method *ExchangeUnbind) Name() string { + return "ExchangeUnbind" +} + +// FrameType returns method frame type +func (method *ExchangeUnbind) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ExchangeUnbind) ClassIdentifier() uint16 { + return 40 +} + +// MethodIdentifier returns method methodID +func (method *ExchangeUnbind) MethodIdentifier() uint16 { + return 40 +} + +// Sync is method should me sent synchronous +func (method *ExchangeUnbind) Sync() bool { + return true +} + +// Read method from io reader +func (method *ExchangeUnbind) Read(reader io.Reader, protoVersion string) (err error) { + + method.Reserved1, err = ReadShort(reader) + if err != nil { + return err + } + + method.Destination, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.Source, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.RoutingKey, err = ReadShortstr(reader) + if err != nil { + return err + } + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.NoWait = bits&(1<<0) != 0 + + method.Arguments, err = ReadTable(reader, protoVersion) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *ExchangeUnbind) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShort(writer, method.Reserved1); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Destination); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Source); err != nil { + return err + } + + if err = WriteShortstr(writer, method.RoutingKey); err != nil { + return err + } + + var bits byte + + if method.NoWait { + bits |= 1 << 0 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + if err = WriteTable(writer, method.Arguments, protoVersion); err != nil { + return err + } + + return +} + +// ExchangeUnbindOk This method confirms that the unbind was successful. +type ExchangeUnbindOk struct { +} + +// Name returns method name as string, usefully for logging +func (method *ExchangeUnbindOk) Name() string { + return "ExchangeUnbindOk" +} + +// FrameType returns method frame type +func (method *ExchangeUnbindOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ExchangeUnbindOk) ClassIdentifier() uint16 { + return 40 +} + +// MethodIdentifier returns method methodID +func (method *ExchangeUnbindOk) MethodIdentifier() uint16 { + return 51 +} + +// Sync is method should me sent synchronous +func (method *ExchangeUnbindOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *ExchangeUnbindOk) Read(reader io.Reader, protoVersion string) (err error) { + + return +} + +// Write method from io reader +func (method *ExchangeUnbindOk) Write(writer io.Writer, protoVersion string) (err error) { + + return +} + +// Queue methods + +// QueueDeclare This method creates or checks a queue. When creating a new queue the client can +// specify various properties that control the durability of the queue and its +// contents, and the level of sharing for the queue. +type QueueDeclare struct { + Reserved1 uint16 + Queue string + Passive bool + Durable bool + Exclusive bool + AutoDelete bool + NoWait bool + Arguments *Table +} + +// Name returns method name as string, usefully for logging +func (method *QueueDeclare) Name() string { + return "QueueDeclare" +} + +// FrameType returns method frame type +func (method *QueueDeclare) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *QueueDeclare) ClassIdentifier() uint16 { + return 50 +} + +// MethodIdentifier returns method methodID +func (method *QueueDeclare) MethodIdentifier() uint16 { + return 10 +} + +// Sync is method should me sent synchronous +func (method *QueueDeclare) Sync() bool { + return true +} + +// Read method from io reader +func (method *QueueDeclare) Read(reader io.Reader, protoVersion string) (err error) { + + method.Reserved1, err = ReadShort(reader) + if err != nil { + return err + } + + method.Queue, err = ReadShortstr(reader) + if err != nil { + return err + } + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.Passive = bits&(1<<0) != 0 + + method.Durable = bits&(1<<1) != 0 + + method.Exclusive = bits&(1<<2) != 0 + + method.AutoDelete = bits&(1<<3) != 0 + + method.NoWait = bits&(1<<4) != 0 + + method.Arguments, err = ReadTable(reader, protoVersion) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *QueueDeclare) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShort(writer, method.Reserved1); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Queue); err != nil { + return err + } + + var bits byte + + if method.Passive { + bits |= 1 << 0 + } + + if method.Durable { + bits |= 1 << 1 + } + + if method.Exclusive { + bits |= 1 << 2 + } + + if method.AutoDelete { + bits |= 1 << 3 + } + + if method.NoWait { + bits |= 1 << 4 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + if err = WriteTable(writer, method.Arguments, protoVersion); err != nil { + return err + } + + return +} + +// QueueDeclareOk This method confirms a Declare method and confirms the name of the queue, essential +// for automatically-named queues. +type QueueDeclareOk struct { + Queue string + MessageCount uint32 + ConsumerCount uint32 +} + +// Name returns method name as string, usefully for logging +func (method *QueueDeclareOk) Name() string { + return "QueueDeclareOk" +} + +// FrameType returns method frame type +func (method *QueueDeclareOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *QueueDeclareOk) ClassIdentifier() uint16 { + return 50 +} + +// MethodIdentifier returns method methodID +func (method *QueueDeclareOk) MethodIdentifier() uint16 { + return 11 +} + +// Sync is method should me sent synchronous +func (method *QueueDeclareOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *QueueDeclareOk) Read(reader io.Reader, protoVersion string) (err error) { + + method.Queue, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.MessageCount, err = ReadLong(reader) + if err != nil { + return err + } + + method.ConsumerCount, err = ReadLong(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *QueueDeclareOk) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShortstr(writer, method.Queue); err != nil { + return err + } + + if err = WriteLong(writer, method.MessageCount); err != nil { + return err + } + + if err = WriteLong(writer, method.ConsumerCount); err != nil { + return err + } + + return +} + +// QueueBind This method binds a queue to an exchange. Until a queue is bound it will not +// receive any messages. In a classic messaging model, store-and-forward queues +// are bound to a direct exchange and subscription queues are bound to a topic +// exchange. +type QueueBind struct { + Reserved1 uint16 + Queue string + Exchange string + RoutingKey string + NoWait bool + Arguments *Table +} + +// Name returns method name as string, usefully for logging +func (method *QueueBind) Name() string { + return "QueueBind" +} + +// FrameType returns method frame type +func (method *QueueBind) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *QueueBind) ClassIdentifier() uint16 { + return 50 +} + +// MethodIdentifier returns method methodID +func (method *QueueBind) MethodIdentifier() uint16 { + return 20 +} + +// Sync is method should me sent synchronous +func (method *QueueBind) Sync() bool { + return true +} + +// Read method from io reader +func (method *QueueBind) Read(reader io.Reader, protoVersion string) (err error) { + + method.Reserved1, err = ReadShort(reader) + if err != nil { + return err + } + + method.Queue, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.Exchange, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.RoutingKey, err = ReadShortstr(reader) + if err != nil { + return err + } + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.NoWait = bits&(1<<0) != 0 + + method.Arguments, err = ReadTable(reader, protoVersion) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *QueueBind) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShort(writer, method.Reserved1); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Queue); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Exchange); err != nil { + return err + } + + if err = WriteShortstr(writer, method.RoutingKey); err != nil { + return err + } + + var bits byte + + if method.NoWait { + bits |= 1 << 0 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + if err = WriteTable(writer, method.Arguments, protoVersion); err != nil { + return err + } + + return +} + +// QueueBindOk This method confirms that the bind was successful. +type QueueBindOk struct { +} + +// Name returns method name as string, usefully for logging +func (method *QueueBindOk) Name() string { + return "QueueBindOk" +} + +// FrameType returns method frame type +func (method *QueueBindOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *QueueBindOk) ClassIdentifier() uint16 { + return 50 +} + +// MethodIdentifier returns method methodID +func (method *QueueBindOk) MethodIdentifier() uint16 { + return 21 +} + +// Sync is method should me sent synchronous +func (method *QueueBindOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *QueueBindOk) Read(reader io.Reader, protoVersion string) (err error) { + + return +} + +// Write method from io reader +func (method *QueueBindOk) Write(writer io.Writer, protoVersion string) (err error) { + + return +} + +// QueueUnbind This method unbinds a queue from an exchange. +type QueueUnbind struct { + Reserved1 uint16 + Queue string + Exchange string + RoutingKey string + Arguments *Table +} + +// Name returns method name as string, usefully for logging +func (method *QueueUnbind) Name() string { + return "QueueUnbind" +} + +// FrameType returns method frame type +func (method *QueueUnbind) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *QueueUnbind) ClassIdentifier() uint16 { + return 50 +} + +// MethodIdentifier returns method methodID +func (method *QueueUnbind) MethodIdentifier() uint16 { + return 50 +} + +// Sync is method should me sent synchronous +func (method *QueueUnbind) Sync() bool { + return true +} + +// Read method from io reader +func (method *QueueUnbind) Read(reader io.Reader, protoVersion string) (err error) { + + method.Reserved1, err = ReadShort(reader) + if err != nil { + return err + } + + method.Queue, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.Exchange, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.RoutingKey, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.Arguments, err = ReadTable(reader, protoVersion) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *QueueUnbind) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShort(writer, method.Reserved1); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Queue); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Exchange); err != nil { + return err + } + + if err = WriteShortstr(writer, method.RoutingKey); err != nil { + return err + } + + if err = WriteTable(writer, method.Arguments, protoVersion); err != nil { + return err + } + + return +} + +// QueueUnbindOk This method confirms that the unbind was successful. +type QueueUnbindOk struct { +} + +// Name returns method name as string, usefully for logging +func (method *QueueUnbindOk) Name() string { + return "QueueUnbindOk" +} + +// FrameType returns method frame type +func (method *QueueUnbindOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *QueueUnbindOk) ClassIdentifier() uint16 { + return 50 +} + +// MethodIdentifier returns method methodID +func (method *QueueUnbindOk) MethodIdentifier() uint16 { + return 51 +} + +// Sync is method should me sent synchronous +func (method *QueueUnbindOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *QueueUnbindOk) Read(reader io.Reader, protoVersion string) (err error) { + + return +} + +// Write method from io reader +func (method *QueueUnbindOk) Write(writer io.Writer, protoVersion string) (err error) { + + return +} + +// QueuePurge This method removes all messages from a queue which are not awaiting +// acknowledgment. +type QueuePurge struct { + Reserved1 uint16 + Queue string + NoWait bool +} + +// Name returns method name as string, usefully for logging +func (method *QueuePurge) Name() string { + return "QueuePurge" +} + +// FrameType returns method frame type +func (method *QueuePurge) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *QueuePurge) ClassIdentifier() uint16 { + return 50 +} + +// MethodIdentifier returns method methodID +func (method *QueuePurge) MethodIdentifier() uint16 { + return 30 +} + +// Sync is method should me sent synchronous +func (method *QueuePurge) Sync() bool { + return true +} + +// Read method from io reader +func (method *QueuePurge) Read(reader io.Reader, protoVersion string) (err error) { + + method.Reserved1, err = ReadShort(reader) + if err != nil { + return err + } + + method.Queue, err = ReadShortstr(reader) + if err != nil { + return err + } + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.NoWait = bits&(1<<0) != 0 + + return +} + +// Write method from io reader +func (method *QueuePurge) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShort(writer, method.Reserved1); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Queue); err != nil { + return err + } + + var bits byte + + if method.NoWait { + bits |= 1 << 0 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + return +} + +// QueuePurgeOk This method confirms the purge of a queue. +type QueuePurgeOk struct { + MessageCount uint32 +} + +// Name returns method name as string, usefully for logging +func (method *QueuePurgeOk) Name() string { + return "QueuePurgeOk" +} + +// FrameType returns method frame type +func (method *QueuePurgeOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *QueuePurgeOk) ClassIdentifier() uint16 { + return 50 +} + +// MethodIdentifier returns method methodID +func (method *QueuePurgeOk) MethodIdentifier() uint16 { + return 31 +} + +// Sync is method should me sent synchronous +func (method *QueuePurgeOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *QueuePurgeOk) Read(reader io.Reader, protoVersion string) (err error) { + + method.MessageCount, err = ReadLong(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *QueuePurgeOk) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteLong(writer, method.MessageCount); err != nil { + return err + } + + return +} + +// QueueDelete This method deletes a queue. When a queue is deleted any pending messages are sent +// to a dead-letter queue if this is defined in the server configuration, and all +// consumers on the queue are cancelled. +type QueueDelete struct { + Reserved1 uint16 + Queue string + IfUnused bool + IfEmpty bool + NoWait bool +} + +// Name returns method name as string, usefully for logging +func (method *QueueDelete) Name() string { + return "QueueDelete" +} + +// FrameType returns method frame type +func (method *QueueDelete) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *QueueDelete) ClassIdentifier() uint16 { + return 50 +} + +// MethodIdentifier returns method methodID +func (method *QueueDelete) MethodIdentifier() uint16 { + return 40 +} + +// Sync is method should me sent synchronous +func (method *QueueDelete) Sync() bool { + return true +} + +// Read method from io reader +func (method *QueueDelete) Read(reader io.Reader, protoVersion string) (err error) { + + method.Reserved1, err = ReadShort(reader) + if err != nil { + return err + } + + method.Queue, err = ReadShortstr(reader) + if err != nil { + return err + } + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.IfUnused = bits&(1<<0) != 0 + + method.IfEmpty = bits&(1<<1) != 0 + + method.NoWait = bits&(1<<2) != 0 + + return +} + +// Write method from io reader +func (method *QueueDelete) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShort(writer, method.Reserved1); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Queue); err != nil { + return err + } + + var bits byte + + if method.IfUnused { + bits |= 1 << 0 + } + + if method.IfEmpty { + bits |= 1 << 1 + } + + if method.NoWait { + bits |= 1 << 2 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + return +} + +// QueueDeleteOk This method confirms the deletion of a queue. +type QueueDeleteOk struct { + MessageCount uint32 +} + +// Name returns method name as string, usefully for logging +func (method *QueueDeleteOk) Name() string { + return "QueueDeleteOk" +} + +// FrameType returns method frame type +func (method *QueueDeleteOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *QueueDeleteOk) ClassIdentifier() uint16 { + return 50 +} + +// MethodIdentifier returns method methodID +func (method *QueueDeleteOk) MethodIdentifier() uint16 { + return 41 +} + +// Sync is method should me sent synchronous +func (method *QueueDeleteOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *QueueDeleteOk) Read(reader io.Reader, protoVersion string) (err error) { + + method.MessageCount, err = ReadLong(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *QueueDeleteOk) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteLong(writer, method.MessageCount); err != nil { + return err + } + + return +} + +// Basic methods + +// BasicPropertyList represents properties for Basic method +type BasicPropertyList struct { + ContentType *string + ContentEncoding *string + Headers *Table + DeliveryMode *byte + Priority *byte + CorrelationID *string + ReplyTo *string + Expiration *string + MessageID *string + Timestamp *time.Time + Type *string + UserID *string + AppID *string + Reserved *string +} + +// BasicPropertyList reads properties from io reader +func (pList *BasicPropertyList) Read(reader io.Reader, propertyFlags uint16, protoVersion string) (err error) { + + if propertyFlags&(1<<15) != 0 { + value, err := ReadShortstr(reader) + if err != nil { + return err + } + pList.ContentType = &value + } + + if propertyFlags&(1<<14) != 0 { + value, err := ReadShortstr(reader) + if err != nil { + return err + } + pList.ContentEncoding = &value + } + + if propertyFlags&(1<<13) != 0 { + value, err := ReadTable(reader, protoVersion) + if err != nil { + return err + } + pList.Headers = value + } + + if propertyFlags&(1<<12) != 0 { + value, err := ReadOctet(reader) + if err != nil { + return err + } + pList.DeliveryMode = &value + } + + if propertyFlags&(1<<11) != 0 { + value, err := ReadOctet(reader) + if err != nil { + return err + } + pList.Priority = &value + } + + if propertyFlags&(1<<10) != 0 { + value, err := ReadShortstr(reader) + if err != nil { + return err + } + pList.CorrelationID = &value + } + + if propertyFlags&(1<<9) != 0 { + value, err := ReadShortstr(reader) + if err != nil { + return err + } + pList.ReplyTo = &value + } + + if propertyFlags&(1<<8) != 0 { + value, err := ReadShortstr(reader) + if err != nil { + return err + } + pList.Expiration = &value + } + + if propertyFlags&(1<<7) != 0 { + value, err := ReadShortstr(reader) + if err != nil { + return err + } + pList.MessageID = &value + } + + if propertyFlags&(1<<6) != 0 { + value, err := ReadTimestamp(reader) + if err != nil { + return err + } + pList.Timestamp = &value + } + + if propertyFlags&(1<<5) != 0 { + value, err := ReadShortstr(reader) + if err != nil { + return err + } + pList.Type = &value + } + + if propertyFlags&(1<<4) != 0 { + value, err := ReadShortstr(reader) + if err != nil { + return err + } + pList.UserID = &value + } + + if propertyFlags&(1<<3) != 0 { + value, err := ReadShortstr(reader) + if err != nil { + return err + } + pList.AppID = &value + } + + if propertyFlags&(1<<2) != 0 { + value, err := ReadShortstr(reader) + if err != nil { + return err + } + pList.Reserved = &value + } + + return +} + +// BasicPropertyList wiretes properties into io writer +func (pList *BasicPropertyList) Write(writer io.Writer, protoVersion string) (propertyFlags uint16, err error) { + + if pList.ContentType != nil { + propertyFlags |= 1 << 15 + if err = WriteShortstr(writer, *pList.ContentType); err != nil { + return + } + } + + if pList.ContentEncoding != nil { + propertyFlags |= 1 << 14 + if err = WriteShortstr(writer, *pList.ContentEncoding); err != nil { + return + } + } + + if pList.Headers != nil { + propertyFlags |= 1 << 13 + if err = WriteTable(writer, pList.Headers, protoVersion); err != nil { + return + } + } + + if pList.DeliveryMode != nil { + propertyFlags |= 1 << 12 + if err = WriteOctet(writer, *pList.DeliveryMode); err != nil { + return + } + } + + if pList.Priority != nil { + propertyFlags |= 1 << 11 + if err = WriteOctet(writer, *pList.Priority); err != nil { + return + } + } + + if pList.CorrelationID != nil { + propertyFlags |= 1 << 10 + if err = WriteShortstr(writer, *pList.CorrelationID); err != nil { + return + } + } + + if pList.ReplyTo != nil { + propertyFlags |= 1 << 9 + if err = WriteShortstr(writer, *pList.ReplyTo); err != nil { + return + } + } + + if pList.Expiration != nil { + propertyFlags |= 1 << 8 + if err = WriteShortstr(writer, *pList.Expiration); err != nil { + return + } + } + + if pList.MessageID != nil { + propertyFlags |= 1 << 7 + if err = WriteShortstr(writer, *pList.MessageID); err != nil { + return + } + } + + if pList.Timestamp != nil { + propertyFlags |= 1 << 6 + if err = WriteTimestamp(writer, *pList.Timestamp); err != nil { + return + } + } + + if pList.Type != nil { + propertyFlags |= 1 << 5 + if err = WriteShortstr(writer, *pList.Type); err != nil { + return + } + } + + if pList.UserID != nil { + propertyFlags |= 1 << 4 + if err = WriteShortstr(writer, *pList.UserID); err != nil { + return + } + } + + if pList.AppID != nil { + propertyFlags |= 1 << 3 + if err = WriteShortstr(writer, *pList.AppID); err != nil { + return + } + } + + if pList.Reserved != nil { + propertyFlags |= 1 << 2 + if err = WriteShortstr(writer, *pList.Reserved); err != nil { + return + } + } + + return +} + +// BasicQos This method requests a specific quality of service. The QoS can be specified for the +// current channel or for all channels on the connection. The particular properties and +// semantics of a qos method always depend on the content class semantics. Though the +// qos method could in principle apply to both peers, it is currently meaningful only +// for the server. +type BasicQos struct { + PrefetchSize uint32 + PrefetchCount uint16 + Global bool +} + +// Name returns method name as string, usefully for logging +func (method *BasicQos) Name() string { + return "BasicQos" +} + +// FrameType returns method frame type +func (method *BasicQos) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *BasicQos) ClassIdentifier() uint16 { + return 60 +} + +// MethodIdentifier returns method methodID +func (method *BasicQos) MethodIdentifier() uint16 { + return 10 +} + +// Sync is method should me sent synchronous +func (method *BasicQos) Sync() bool { + return true +} + +// Read method from io reader +func (method *BasicQos) Read(reader io.Reader, protoVersion string) (err error) { + + method.PrefetchSize, err = ReadLong(reader) + if err != nil { + return err + } + + method.PrefetchCount, err = ReadShort(reader) + if err != nil { + return err + } + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.Global = bits&(1<<0) != 0 + + return +} + +// Write method from io reader +func (method *BasicQos) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteLong(writer, method.PrefetchSize); err != nil { + return err + } + + if err = WriteShort(writer, method.PrefetchCount); err != nil { + return err + } + + var bits byte + + if method.Global { + bits |= 1 << 0 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + return +} + +// BasicQosOk This method tells the client that the requested QoS levels could be handled by the +// server. The requested QoS applies to all active consumers until a new QoS is +// defined. +type BasicQosOk struct { +} + +// Name returns method name as string, usefully for logging +func (method *BasicQosOk) Name() string { + return "BasicQosOk" +} + +// FrameType returns method frame type +func (method *BasicQosOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *BasicQosOk) ClassIdentifier() uint16 { + return 60 +} + +// MethodIdentifier returns method methodID +func (method *BasicQosOk) MethodIdentifier() uint16 { + return 11 +} + +// Sync is method should me sent synchronous +func (method *BasicQosOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *BasicQosOk) Read(reader io.Reader, protoVersion string) (err error) { + + return +} + +// Write method from io reader +func (method *BasicQosOk) Write(writer io.Writer, protoVersion string) (err error) { + + return +} + +// BasicConsume This method asks the server to start a "consumer", which is a transient request for +// messages from a specific queue. Consumers last as long as the channel they were +// declared on, or until the client cancels them. +type BasicConsume struct { + Reserved1 uint16 + Queue string + ConsumerTag string + NoLocal bool + NoAck bool + Exclusive bool + NoWait bool + Arguments *Table +} + +// Name returns method name as string, usefully for logging +func (method *BasicConsume) Name() string { + return "BasicConsume" +} + +// FrameType returns method frame type +func (method *BasicConsume) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *BasicConsume) ClassIdentifier() uint16 { + return 60 +} + +// MethodIdentifier returns method methodID +func (method *BasicConsume) MethodIdentifier() uint16 { + return 20 +} + +// Sync is method should me sent synchronous +func (method *BasicConsume) Sync() bool { + return true +} + +// Read method from io reader +func (method *BasicConsume) Read(reader io.Reader, protoVersion string) (err error) { + + method.Reserved1, err = ReadShort(reader) + if err != nil { + return err + } + + method.Queue, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.ConsumerTag, err = ReadShortstr(reader) + if err != nil { + return err + } + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.NoLocal = bits&(1<<0) != 0 + + method.NoAck = bits&(1<<1) != 0 + + method.Exclusive = bits&(1<<2) != 0 + + method.NoWait = bits&(1<<3) != 0 + + method.Arguments, err = ReadTable(reader, protoVersion) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *BasicConsume) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShort(writer, method.Reserved1); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Queue); err != nil { + return err + } + + if err = WriteShortstr(writer, method.ConsumerTag); err != nil { + return err + } + + var bits byte + + if method.NoLocal { + bits |= 1 << 0 + } + + if method.NoAck { + bits |= 1 << 1 + } + + if method.Exclusive { + bits |= 1 << 2 + } + + if method.NoWait { + bits |= 1 << 3 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + if err = WriteTable(writer, method.Arguments, protoVersion); err != nil { + return err + } + + return +} + +// BasicConsumeOk The server provides the client with a consumer tag, which is used by the client +// for methods called on the consumer at a later stage. +type BasicConsumeOk struct { + ConsumerTag string +} + +// Name returns method name as string, usefully for logging +func (method *BasicConsumeOk) Name() string { + return "BasicConsumeOk" +} + +// FrameType returns method frame type +func (method *BasicConsumeOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *BasicConsumeOk) ClassIdentifier() uint16 { + return 60 +} + +// MethodIdentifier returns method methodID +func (method *BasicConsumeOk) MethodIdentifier() uint16 { + return 21 +} + +// Sync is method should me sent synchronous +func (method *BasicConsumeOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *BasicConsumeOk) Read(reader io.Reader, protoVersion string) (err error) { + + method.ConsumerTag, err = ReadShortstr(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *BasicConsumeOk) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShortstr(writer, method.ConsumerTag); err != nil { + return err + } + + return +} + +// BasicCancel This method cancels a consumer. This does not affect already delivered +// messages, but it does mean the server will not send any more messages for +// that consumer. The client may receive an arbitrary number of messages in +// between sending the cancel method and receiving the cancel-ok reply. +// +// It may also be sent from the server to the client in the event +// of the consumer being unexpectedly cancelled (i.e. cancelled +// for any reason other than the server receiving the +// corresponding basic.cancel from the client). This allows +// clients to be notified of the loss of consumers due to events +// such as queue deletion. Note that as it is not a MUST for +// clients to accept this method from the client, it is advisable +// for the broker to be able to identify those clients that are +// capable of accepting the method, through some means of +// capability negotiation. +type BasicCancel struct { + ConsumerTag string + NoWait bool +} + +// Name returns method name as string, usefully for logging +func (method *BasicCancel) Name() string { + return "BasicCancel" +} + +// FrameType returns method frame type +func (method *BasicCancel) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *BasicCancel) ClassIdentifier() uint16 { + return 60 +} + +// MethodIdentifier returns method methodID +func (method *BasicCancel) MethodIdentifier() uint16 { + return 30 +} + +// Sync is method should me sent synchronous +func (method *BasicCancel) Sync() bool { + return true +} + +// Read method from io reader +func (method *BasicCancel) Read(reader io.Reader, protoVersion string) (err error) { + + method.ConsumerTag, err = ReadShortstr(reader) + if err != nil { + return err + } + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.NoWait = bits&(1<<0) != 0 + + return +} + +// Write method from io reader +func (method *BasicCancel) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShortstr(writer, method.ConsumerTag); err != nil { + return err + } + + var bits byte + + if method.NoWait { + bits |= 1 << 0 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + return +} + +// BasicCancelOk This method confirms that the cancellation was completed. +type BasicCancelOk struct { + ConsumerTag string +} + +// Name returns method name as string, usefully for logging +func (method *BasicCancelOk) Name() string { + return "BasicCancelOk" +} + +// FrameType returns method frame type +func (method *BasicCancelOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *BasicCancelOk) ClassIdentifier() uint16 { + return 60 +} + +// MethodIdentifier returns method methodID +func (method *BasicCancelOk) MethodIdentifier() uint16 { + return 31 +} + +// Sync is method should me sent synchronous +func (method *BasicCancelOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *BasicCancelOk) Read(reader io.Reader, protoVersion string) (err error) { + + method.ConsumerTag, err = ReadShortstr(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *BasicCancelOk) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShortstr(writer, method.ConsumerTag); err != nil { + return err + } + + return +} + +// BasicPublish This method publishes a message to a specific exchange. The message will be routed +// to queues as defined by the exchange configuration and distributed to any active +// consumers when the transaction, if any, is committed. +type BasicPublish struct { + Reserved1 uint16 + Exchange string + RoutingKey string + Mandatory bool + Immediate bool +} + +// Name returns method name as string, usefully for logging +func (method *BasicPublish) Name() string { + return "BasicPublish" +} + +// FrameType returns method frame type +func (method *BasicPublish) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *BasicPublish) ClassIdentifier() uint16 { + return 60 +} + +// MethodIdentifier returns method methodID +func (method *BasicPublish) MethodIdentifier() uint16 { + return 40 +} + +// Sync is method should me sent synchronous +func (method *BasicPublish) Sync() bool { + return false +} + +// Read method from io reader +func (method *BasicPublish) Read(reader io.Reader, protoVersion string) (err error) { + + method.Reserved1, err = ReadShort(reader) + if err != nil { + return err + } + + method.Exchange, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.RoutingKey, err = ReadShortstr(reader) + if err != nil { + return err + } + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.Mandatory = bits&(1<<0) != 0 + + method.Immediate = bits&(1<<1) != 0 + + return +} + +// Write method from io reader +func (method *BasicPublish) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShort(writer, method.Reserved1); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Exchange); err != nil { + return err + } + + if err = WriteShortstr(writer, method.RoutingKey); err != nil { + return err + } + + var bits byte + + if method.Mandatory { + bits |= 1 << 0 + } + + if method.Immediate { + bits |= 1 << 1 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + return +} + +// BasicReturn This method returns an undeliverable message that was published with the "immediate" +// flag set, or an unroutable message published with the "mandatory" flag set. The +// reply code and text provide information about the reason that the message was +// undeliverable. +type BasicReturn struct { + ReplyCode uint16 + ReplyText string + Exchange string + RoutingKey string +} + +// Name returns method name as string, usefully for logging +func (method *BasicReturn) Name() string { + return "BasicReturn" +} + +// FrameType returns method frame type +func (method *BasicReturn) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *BasicReturn) ClassIdentifier() uint16 { + return 60 +} + +// MethodIdentifier returns method methodID +func (method *BasicReturn) MethodIdentifier() uint16 { + return 50 +} + +// Sync is method should me sent synchronous +func (method *BasicReturn) Sync() bool { + return false +} + +// Read method from io reader +func (method *BasicReturn) Read(reader io.Reader, protoVersion string) (err error) { + + method.ReplyCode, err = ReadShort(reader) + if err != nil { + return err + } + + method.ReplyText, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.Exchange, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.RoutingKey, err = ReadShortstr(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *BasicReturn) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShort(writer, method.ReplyCode); err != nil { + return err + } + + if err = WriteShortstr(writer, method.ReplyText); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Exchange); err != nil { + return err + } + + if err = WriteShortstr(writer, method.RoutingKey); err != nil { + return err + } + + return +} + +// BasicDeliver This method delivers a message to the client, via a consumer. In the asynchronous +// message delivery model, the client starts a consumer using the Consume method, then +// the server responds with Deliver methods as and when messages arrive for that +// consumer. +type BasicDeliver struct { + ConsumerTag string + DeliveryTag uint64 + Redelivered bool + Exchange string + RoutingKey string +} + +// Name returns method name as string, usefully for logging +func (method *BasicDeliver) Name() string { + return "BasicDeliver" +} + +// FrameType returns method frame type +func (method *BasicDeliver) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *BasicDeliver) ClassIdentifier() uint16 { + return 60 +} + +// MethodIdentifier returns method methodID +func (method *BasicDeliver) MethodIdentifier() uint16 { + return 60 +} + +// Sync is method should me sent synchronous +func (method *BasicDeliver) Sync() bool { + return false +} + +// Read method from io reader +func (method *BasicDeliver) Read(reader io.Reader, protoVersion string) (err error) { + + method.ConsumerTag, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.DeliveryTag, err = ReadLonglong(reader) + if err != nil { + return err + } + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.Redelivered = bits&(1<<0) != 0 + + method.Exchange, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.RoutingKey, err = ReadShortstr(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *BasicDeliver) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShortstr(writer, method.ConsumerTag); err != nil { + return err + } + + if err = WriteLonglong(writer, method.DeliveryTag); err != nil { + return err + } + + var bits byte + + if method.Redelivered { + bits |= 1 << 0 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Exchange); err != nil { + return err + } + + if err = WriteShortstr(writer, method.RoutingKey); err != nil { + return err + } + + return +} + +// BasicGet This method provides a direct access to the messages in a queue using a synchronous +// dialogue that is designed for specific types of application where synchronous +// functionality is more important than performance. +type BasicGet struct { + Reserved1 uint16 + Queue string + NoAck bool +} + +// Name returns method name as string, usefully for logging +func (method *BasicGet) Name() string { + return "BasicGet" +} + +// FrameType returns method frame type +func (method *BasicGet) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *BasicGet) ClassIdentifier() uint16 { + return 60 +} + +// MethodIdentifier returns method methodID +func (method *BasicGet) MethodIdentifier() uint16 { + return 70 +} + +// Sync is method should me sent synchronous +func (method *BasicGet) Sync() bool { + return true +} + +// Read method from io reader +func (method *BasicGet) Read(reader io.Reader, protoVersion string) (err error) { + + method.Reserved1, err = ReadShort(reader) + if err != nil { + return err + } + + method.Queue, err = ReadShortstr(reader) + if err != nil { + return err + } + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.NoAck = bits&(1<<0) != 0 + + return +} + +// Write method from io reader +func (method *BasicGet) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShort(writer, method.Reserved1); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Queue); err != nil { + return err + } + + var bits byte + + if method.NoAck { + bits |= 1 << 0 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + return +} + +// BasicGetOk This method delivers a message to the client following a get method. A message +// delivered by 'get-ok' must be acknowledged unless the no-ack option was set in the +// get method. +type BasicGetOk struct { + DeliveryTag uint64 + Redelivered bool + Exchange string + RoutingKey string + MessageCount uint32 +} + +// Name returns method name as string, usefully for logging +func (method *BasicGetOk) Name() string { + return "BasicGetOk" +} + +// FrameType returns method frame type +func (method *BasicGetOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *BasicGetOk) ClassIdentifier() uint16 { + return 60 +} + +// MethodIdentifier returns method methodID +func (method *BasicGetOk) MethodIdentifier() uint16 { + return 71 +} + +// Sync is method should me sent synchronous +func (method *BasicGetOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *BasicGetOk) Read(reader io.Reader, protoVersion string) (err error) { + + method.DeliveryTag, err = ReadLonglong(reader) + if err != nil { + return err + } + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.Redelivered = bits&(1<<0) != 0 + + method.Exchange, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.RoutingKey, err = ReadShortstr(reader) + if err != nil { + return err + } + + method.MessageCount, err = ReadLong(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *BasicGetOk) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteLonglong(writer, method.DeliveryTag); err != nil { + return err + } + + var bits byte + + if method.Redelivered { + bits |= 1 << 0 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + if err = WriteShortstr(writer, method.Exchange); err != nil { + return err + } + + if err = WriteShortstr(writer, method.RoutingKey); err != nil { + return err + } + + if err = WriteLong(writer, method.MessageCount); err != nil { + return err + } + + return +} + +// BasicGetEmpty This method tells the client that the queue has no messages available for the +// client. +type BasicGetEmpty struct { + Reserved1 string +} + +// Name returns method name as string, usefully for logging +func (method *BasicGetEmpty) Name() string { + return "BasicGetEmpty" +} + +// FrameType returns method frame type +func (method *BasicGetEmpty) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *BasicGetEmpty) ClassIdentifier() uint16 { + return 60 +} + +// MethodIdentifier returns method methodID +func (method *BasicGetEmpty) MethodIdentifier() uint16 { + return 72 +} + +// Sync is method should me sent synchronous +func (method *BasicGetEmpty) Sync() bool { + return true +} + +// Read method from io reader +func (method *BasicGetEmpty) Read(reader io.Reader, protoVersion string) (err error) { + + method.Reserved1, err = ReadShortstr(reader) + if err != nil { + return err + } + + return +} + +// Write method from io reader +func (method *BasicGetEmpty) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteShortstr(writer, method.Reserved1); err != nil { + return err + } + + return +} + +// BasicAck When sent by the client, this method acknowledges one or more +// messages delivered via the Deliver or Get-Ok methods. +// +// When sent by server, this method acknowledges one or more +// messages published with the Publish method on a channel in +// confirm mode. +// +// The acknowledgement can be for a single message or a set of +// messages up to and including a specific message. +type BasicAck struct { + DeliveryTag uint64 + Multiple bool +} + +// Name returns method name as string, usefully for logging +func (method *BasicAck) Name() string { + return "BasicAck" +} + +// FrameType returns method frame type +func (method *BasicAck) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *BasicAck) ClassIdentifier() uint16 { + return 60 +} + +// MethodIdentifier returns method methodID +func (method *BasicAck) MethodIdentifier() uint16 { + return 80 +} + +// Sync is method should me sent synchronous +func (method *BasicAck) Sync() bool { + return false +} + +// Read method from io reader +func (method *BasicAck) Read(reader io.Reader, protoVersion string) (err error) { + + method.DeliveryTag, err = ReadLonglong(reader) + if err != nil { + return err + } + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.Multiple = bits&(1<<0) != 0 + + return +} + +// Write method from io reader +func (method *BasicAck) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteLonglong(writer, method.DeliveryTag); err != nil { + return err + } + + var bits byte + + if method.Multiple { + bits |= 1 << 0 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + return +} + +// BasicReject This method allows a client to reject a message. It can be used to interrupt and +// cancel large incoming messages, or return untreatable messages to their original +// queue. +type BasicReject struct { + DeliveryTag uint64 + Requeue bool +} + +// Name returns method name as string, usefully for logging +func (method *BasicReject) Name() string { + return "BasicReject" +} + +// FrameType returns method frame type +func (method *BasicReject) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *BasicReject) ClassIdentifier() uint16 { + return 60 +} + +// MethodIdentifier returns method methodID +func (method *BasicReject) MethodIdentifier() uint16 { + return 90 +} + +// Sync is method should me sent synchronous +func (method *BasicReject) Sync() bool { + return false +} + +// Read method from io reader +func (method *BasicReject) Read(reader io.Reader, protoVersion string) (err error) { + + method.DeliveryTag, err = ReadLonglong(reader) + if err != nil { + return err + } + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.Requeue = bits&(1<<0) != 0 + + return +} + +// Write method from io reader +func (method *BasicReject) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteLonglong(writer, method.DeliveryTag); err != nil { + return err + } + + var bits byte + + if method.Requeue { + bits |= 1 << 0 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + return +} + +// BasicRecoverAsync This method asks the server to redeliver all unacknowledged messages on a +// specified channel. Zero or more messages may be redelivered. This method +// is deprecated in favour of the synchronous Recover/Recover-Ok. +type BasicRecoverAsync struct { + Requeue bool +} + +// Name returns method name as string, usefully for logging +func (method *BasicRecoverAsync) Name() string { + return "BasicRecoverAsync" +} + +// FrameType returns method frame type +func (method *BasicRecoverAsync) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *BasicRecoverAsync) ClassIdentifier() uint16 { + return 60 +} + +// MethodIdentifier returns method methodID +func (method *BasicRecoverAsync) MethodIdentifier() uint16 { + return 100 +} + +// Sync is method should me sent synchronous +func (method *BasicRecoverAsync) Sync() bool { + return false +} + +// Read method from io reader +func (method *BasicRecoverAsync) Read(reader io.Reader, protoVersion string) (err error) { + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.Requeue = bits&(1<<0) != 0 + + return +} + +// Write method from io reader +func (method *BasicRecoverAsync) Write(writer io.Writer, protoVersion string) (err error) { + + var bits byte + + if method.Requeue { + bits |= 1 << 0 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + return +} + +// BasicRecover This method asks the server to redeliver all unacknowledged messages on a +// specified channel. Zero or more messages may be redelivered. This method +// replaces the asynchronous Recover. +type BasicRecover struct { + Requeue bool +} + +// Name returns method name as string, usefully for logging +func (method *BasicRecover) Name() string { + return "BasicRecover" +} + +// FrameType returns method frame type +func (method *BasicRecover) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *BasicRecover) ClassIdentifier() uint16 { + return 60 +} + +// MethodIdentifier returns method methodID +func (method *BasicRecover) MethodIdentifier() uint16 { + return 110 +} + +// Sync is method should me sent synchronous +func (method *BasicRecover) Sync() bool { + return false +} + +// Read method from io reader +func (method *BasicRecover) Read(reader io.Reader, protoVersion string) (err error) { + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.Requeue = bits&(1<<0) != 0 + + return +} + +// Write method from io reader +func (method *BasicRecover) Write(writer io.Writer, protoVersion string) (err error) { + + var bits byte + + if method.Requeue { + bits |= 1 << 0 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + return +} + +// BasicRecoverOk This method acknowledges a Basic.Recover method. +type BasicRecoverOk struct { +} + +// Name returns method name as string, usefully for logging +func (method *BasicRecoverOk) Name() string { + return "BasicRecoverOk" +} + +// FrameType returns method frame type +func (method *BasicRecoverOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *BasicRecoverOk) ClassIdentifier() uint16 { + return 60 +} + +// MethodIdentifier returns method methodID +func (method *BasicRecoverOk) MethodIdentifier() uint16 { + return 111 +} + +// Sync is method should me sent synchronous +func (method *BasicRecoverOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *BasicRecoverOk) Read(reader io.Reader, protoVersion string) (err error) { + + return +} + +// Write method from io reader +func (method *BasicRecoverOk) Write(writer io.Writer, protoVersion string) (err error) { + + return +} + +// BasicNack This method allows a client to reject one or more incoming messages. It can be +// used to interrupt and cancel large incoming messages, or return untreatable +// messages to their original queue. +// +// This method is also used by the server to inform publishers on channels in +// confirm mode of unhandled messages. If a publisher receives this method, it +// probably needs to republish the offending messages. +type BasicNack struct { + DeliveryTag uint64 + Multiple bool + Requeue bool +} + +// Name returns method name as string, usefully for logging +func (method *BasicNack) Name() string { + return "BasicNack" +} + +// FrameType returns method frame type +func (method *BasicNack) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *BasicNack) ClassIdentifier() uint16 { + return 60 +} + +// MethodIdentifier returns method methodID +func (method *BasicNack) MethodIdentifier() uint16 { + return 120 +} + +// Sync is method should me sent synchronous +func (method *BasicNack) Sync() bool { + return false +} + +// Read method from io reader +func (method *BasicNack) Read(reader io.Reader, protoVersion string) (err error) { + + method.DeliveryTag, err = ReadLonglong(reader) + if err != nil { + return err + } + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.Multiple = bits&(1<<0) != 0 + + method.Requeue = bits&(1<<1) != 0 + + return +} + +// Write method from io reader +func (method *BasicNack) Write(writer io.Writer, protoVersion string) (err error) { + + if err = WriteLonglong(writer, method.DeliveryTag); err != nil { + return err + } + + var bits byte + + if method.Multiple { + bits |= 1 << 0 + } + + if method.Requeue { + bits |= 1 << 1 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + return +} + +// Tx methods + +// TxSelect This method sets the channel to use standard transactions. The client must use this +// method at least once on a channel before using the Commit or Rollback methods. +type TxSelect struct { +} + +// Name returns method name as string, usefully for logging +func (method *TxSelect) Name() string { + return "TxSelect" +} + +// FrameType returns method frame type +func (method *TxSelect) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *TxSelect) ClassIdentifier() uint16 { + return 90 +} + +// MethodIdentifier returns method methodID +func (method *TxSelect) MethodIdentifier() uint16 { + return 10 +} + +// Sync is method should me sent synchronous +func (method *TxSelect) Sync() bool { + return true +} + +// Read method from io reader +func (method *TxSelect) Read(reader io.Reader, protoVersion string) (err error) { + + return +} + +// Write method from io reader +func (method *TxSelect) Write(writer io.Writer, protoVersion string) (err error) { + + return +} + +// TxSelectOk This method confirms to the client that the channel was successfully set to use +// standard transactions. +type TxSelectOk struct { +} + +// Name returns method name as string, usefully for logging +func (method *TxSelectOk) Name() string { + return "TxSelectOk" +} + +// FrameType returns method frame type +func (method *TxSelectOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *TxSelectOk) ClassIdentifier() uint16 { + return 90 +} + +// MethodIdentifier returns method methodID +func (method *TxSelectOk) MethodIdentifier() uint16 { + return 11 +} + +// Sync is method should me sent synchronous +func (method *TxSelectOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *TxSelectOk) Read(reader io.Reader, protoVersion string) (err error) { + + return +} + +// Write method from io reader +func (method *TxSelectOk) Write(writer io.Writer, protoVersion string) (err error) { + + return +} + +// TxCommit This method commits all message publications and acknowledgments performed in +// the current transaction. A new transaction starts immediately after a commit. +type TxCommit struct { +} + +// Name returns method name as string, usefully for logging +func (method *TxCommit) Name() string { + return "TxCommit" +} + +// FrameType returns method frame type +func (method *TxCommit) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *TxCommit) ClassIdentifier() uint16 { + return 90 +} + +// MethodIdentifier returns method methodID +func (method *TxCommit) MethodIdentifier() uint16 { + return 20 +} + +// Sync is method should me sent synchronous +func (method *TxCommit) Sync() bool { + return true +} + +// Read method from io reader +func (method *TxCommit) Read(reader io.Reader, protoVersion string) (err error) { + + return +} + +// Write method from io reader +func (method *TxCommit) Write(writer io.Writer, protoVersion string) (err error) { + + return +} + +// TxCommitOk This method confirms to the client that the commit succeeded. Note that if a commit +// fails, the server raises a channel exception. +type TxCommitOk struct { +} + +// Name returns method name as string, usefully for logging +func (method *TxCommitOk) Name() string { + return "TxCommitOk" +} + +// FrameType returns method frame type +func (method *TxCommitOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *TxCommitOk) ClassIdentifier() uint16 { + return 90 +} + +// MethodIdentifier returns method methodID +func (method *TxCommitOk) MethodIdentifier() uint16 { + return 21 +} + +// Sync is method should me sent synchronous +func (method *TxCommitOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *TxCommitOk) Read(reader io.Reader, protoVersion string) (err error) { + + return +} + +// Write method from io reader +func (method *TxCommitOk) Write(writer io.Writer, protoVersion string) (err error) { + + return +} + +// TxRollback This method abandons all message publications and acknowledgments performed in +// the current transaction. A new transaction starts immediately after a rollback. +// Note that unacked messages will not be automatically redelivered by rollback; +// if that is required an explicit recover call should be issued. +type TxRollback struct { +} + +// Name returns method name as string, usefully for logging +func (method *TxRollback) Name() string { + return "TxRollback" +} + +// FrameType returns method frame type +func (method *TxRollback) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *TxRollback) ClassIdentifier() uint16 { + return 90 +} + +// MethodIdentifier returns method methodID +func (method *TxRollback) MethodIdentifier() uint16 { + return 30 +} + +// Sync is method should me sent synchronous +func (method *TxRollback) Sync() bool { + return true +} + +// Read method from io reader +func (method *TxRollback) Read(reader io.Reader, protoVersion string) (err error) { + + return +} + +// Write method from io reader +func (method *TxRollback) Write(writer io.Writer, protoVersion string) (err error) { + + return +} + +// TxRollbackOk This method confirms to the client that the rollback succeeded. Note that if an +// rollback fails, the server raises a channel exception. +type TxRollbackOk struct { +} + +// Name returns method name as string, usefully for logging +func (method *TxRollbackOk) Name() string { + return "TxRollbackOk" +} + +// FrameType returns method frame type +func (method *TxRollbackOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *TxRollbackOk) ClassIdentifier() uint16 { + return 90 +} + +// MethodIdentifier returns method methodID +func (method *TxRollbackOk) MethodIdentifier() uint16 { + return 31 +} + +// Sync is method should me sent synchronous +func (method *TxRollbackOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *TxRollbackOk) Read(reader io.Reader, protoVersion string) (err error) { + + return +} + +// Write method from io reader +func (method *TxRollbackOk) Write(writer io.Writer, protoVersion string) (err error) { + + return +} + +// Confirm methods + +// ConfirmSelect This method sets the channel to use publisher acknowledgements. +// The client can only use this method on a non-transactional +// channel. +type ConfirmSelect struct { + Nowait bool +} + +// Name returns method name as string, usefully for logging +func (method *ConfirmSelect) Name() string { + return "ConfirmSelect" +} + +// FrameType returns method frame type +func (method *ConfirmSelect) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ConfirmSelect) ClassIdentifier() uint16 { + return 85 +} + +// MethodIdentifier returns method methodID +func (method *ConfirmSelect) MethodIdentifier() uint16 { + return 10 +} + +// Sync is method should me sent synchronous +func (method *ConfirmSelect) Sync() bool { + return true +} + +// Read method from io reader +func (method *ConfirmSelect) Read(reader io.Reader, protoVersion string) (err error) { + + bits, err := ReadOctet(reader) + if err != nil { + return err + } + + method.Nowait = bits&(1<<0) != 0 + + return +} + +// Write method from io reader +func (method *ConfirmSelect) Write(writer io.Writer, protoVersion string) (err error) { + + var bits byte + + if method.Nowait { + bits |= 1 << 0 + } + + if err = WriteOctet(writer, bits); err != nil { + return err + } + + return +} + +// ConfirmSelectOk This method confirms to the client that the channel was successfully +// set to use publisher acknowledgements. +type ConfirmSelectOk struct { +} + +// Name returns method name as string, usefully for logging +func (method *ConfirmSelectOk) Name() string { + return "ConfirmSelectOk" +} + +// FrameType returns method frame type +func (method *ConfirmSelectOk) FrameType() byte { + return 1 +} + +// ClassIdentifier returns method classID +func (method *ConfirmSelectOk) ClassIdentifier() uint16 { + return 85 +} + +// MethodIdentifier returns method methodID +func (method *ConfirmSelectOk) MethodIdentifier() uint16 { + return 11 +} + +// Sync is method should me sent synchronous +func (method *ConfirmSelectOk) Sync() bool { + return true +} + +// Read method from io reader +func (method *ConfirmSelectOk) Read(reader io.Reader, protoVersion string) (err error) { + + return +} + +// Write method from io reader +func (method *ConfirmSelectOk) Write(writer io.Writer, protoVersion string) (err error) { + + return +} + +/* +ReadMethod reads method from frame's payload + +Method frames carry the high-level protocol commands (which we call "methods"). +One method frame carries one command. The method frame payload has this format: + + 0 2 4 + +----------+-----------+-------------- - - + | class-id | method-id | arguments... + +----------+-----------+-------------- - - + short short ... + +*/ +func ReadMethod(reader io.Reader, protoVersion string) (Method, error) { + classID, err := ReadShort(reader) + if err != nil { + return nil, err + } + + methodID, err := ReadShort(reader) + if err != nil { + return nil, err + } + switch classID { + + case 10: + switch methodID { + + case 10: + var method = &ConnectionStart{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 11: + var method = &ConnectionStartOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 20: + var method = &ConnectionSecure{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 21: + var method = &ConnectionSecureOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 30: + var method = &ConnectionTune{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 31: + var method = &ConnectionTuneOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 40: + var method = &ConnectionOpen{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 41: + var method = &ConnectionOpenOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 50: + var method = &ConnectionClose{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 51: + var method = &ConnectionCloseOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 60: + var method = &ConnectionBlocked{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 61: + var method = &ConnectionUnblocked{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + } + case 20: + switch methodID { + + case 10: + var method = &ChannelOpen{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 11: + var method = &ChannelOpenOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 20: + var method = &ChannelFlow{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 21: + var method = &ChannelFlowOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 40: + var method = &ChannelClose{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 41: + var method = &ChannelCloseOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + } + case 40: + switch methodID { + + case 10: + var method = &ExchangeDeclare{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 11: + var method = &ExchangeDeclareOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 20: + var method = &ExchangeDelete{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 21: + var method = &ExchangeDeleteOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 30: + var method = &ExchangeBind{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 31: + var method = &ExchangeBindOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 40: + var method = &ExchangeUnbind{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 51: + var method = &ExchangeUnbindOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + } + case 50: + switch methodID { + + case 10: + var method = &QueueDeclare{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 11: + var method = &QueueDeclareOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 20: + var method = &QueueBind{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 21: + var method = &QueueBindOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 50: + var method = &QueueUnbind{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 51: + var method = &QueueUnbindOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 30: + var method = &QueuePurge{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 31: + var method = &QueuePurgeOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 40: + var method = &QueueDelete{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 41: + var method = &QueueDeleteOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + } + case 60: + switch methodID { + + case 10: + var method = &BasicQos{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 11: + var method = &BasicQosOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 20: + var method = &BasicConsume{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 21: + var method = &BasicConsumeOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 30: + var method = &BasicCancel{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 31: + var method = &BasicCancelOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 40: + var method = &BasicPublish{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 50: + var method = &BasicReturn{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 60: + var method = &BasicDeliver{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 70: + var method = &BasicGet{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 71: + var method = &BasicGetOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 72: + var method = &BasicGetEmpty{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 80: + var method = &BasicAck{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 90: + var method = &BasicReject{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 100: + var method = &BasicRecoverAsync{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 110: + var method = &BasicRecover{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 111: + var method = &BasicRecoverOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 120: + var method = &BasicNack{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + } + case 90: + switch methodID { + + case 10: + var method = &TxSelect{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 11: + var method = &TxSelectOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 20: + var method = &TxCommit{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 21: + var method = &TxCommitOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 30: + var method = &TxRollback{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 31: + var method = &TxRollbackOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + } + case 85: + switch methodID { + + case 10: + var method = &ConfirmSelect{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + case 11: + var method = &ConfirmSelectOk{} + if err := method.Read(reader, protoVersion); err != nil { + return nil, err + } + return method, nil + } + } + + return nil, fmt.Errorf("unknown classID and methodID: [%d. %d]", classID, methodID) +} + +// WriteMethod writes method into frame's payload +func WriteMethod(writer io.Writer, method Method, protoVersion string) (err error) { + if err = WriteShort(writer, method.ClassIdentifier()); err != nil { + return err + } + if err = WriteShort(writer, method.MethodIdentifier()); err != nil { + return err + } + + if err = method.Write(writer, protoVersion); err != nil { + return err + } + + return +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/amqp/readers_writers.go b/pkg/outputs/amqp/_fixtures/garagemq/amqp/readers_writers.go new file mode 100644 index 000000000..22e8b2ddd --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/amqp/readers_writers.go @@ -0,0 +1,882 @@ +package amqp + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/pool" + "io" + "time" +) + +var emptyBufferPool = pool.NewBufferPool(0) + +// 14 bytes for class-id | weight | body size | property flags +var headerBufferPool = pool.NewBufferPool(14) + +// AmqpHeader standard AMQP header +var AmqpHeader = []byte{'A', 'M', 'Q', 'P', 0, 0, 9, 1} + +// supported protocol identifiers +const ( + Proto091 = "amqp-0-9-1" + ProtoRabbit = "amqp-rabbit" +) + +func writeSlice(wr io.Writer, data []byte) error { + _, err := wr.Write(data[:]) + return err +} + +/* +ReadFrame reads and parses raw data from conn reader and returns amqp frame + +@spec-note +All frames consist of a header (7 octets), a payload of arbitrary size, and a +'frame-end' octet that detects malformed frames: + + 0 1 3 7 size+7 size+8 + +------+---------+-------------+ +------------+ +-----------+ + | type | channel | size | | payload | | frame-end | + +------+---------+-------------+ +------------+ +-----------+ + octet short long size octets octet + +To read a frame, we: + 1. Read the header and check the frame type and channel. + 2. Depending on the frame size, we read the payload + 3. Read the frame-end octet. +*/ +func ReadFrame(r io.Reader) (frame *Frame, err error) { + // It does not matter that we call read methods 3 time + // Because net.TCPConn connection buffered by bufio.NewReader + frame = &Frame{} + if frame.Type, err = ReadOctet(r); err != nil { + return nil, err + } + if frame.ChannelID, err = ReadShort(r); err != nil { + return nil, err + } + var payloadSize uint32 + if payloadSize, err = ReadLong(r); err != nil { + return nil, err + } + + var payload = make([]byte, payloadSize+1) + if _, err := io.ReadFull(r, payload); err != nil { + return nil, err + } + frame.Payload = payload[0:payloadSize] + + // check frame end + if payload[payloadSize] != FrameEnd { + return nil, fmt.Errorf( + "the frame-end octet MUST always be the hexadecimal value 'xCE', %x given", + payload[payloadSize]) + } + + return frame, nil +} + +// WriteFrame pack amqp Frame as bytes and write to conn writer +func WriteFrame(wr io.Writer, frame *Frame) (err error) { + if err = WriteOctet(wr, frame.Type); err != nil { + return err + } + if err = WriteShort(wr, frame.ChannelID); err != nil { + return err + } + + // size + payload + if err = WriteLongstr(wr, frame.Payload); err != nil { + return err + } + // frame end + if err = WriteOctet(wr, FrameEnd); err != nil { + return err + } + + return nil +} + +// ReadOctet reads octet (byte) +func ReadOctet(r io.Reader) (data byte, err error) { + var b [1]byte + if _, err = io.ReadFull(r, b[:]); err != nil { + return + } + data = b[0] + return +} + +// WriteOctet writes octet (byte) +func WriteOctet(wr io.Writer, data byte) error { + var b [1]byte + b[0] = data + return writeSlice(wr, b[:]) +} + +// ReadShort reads 2 bytes +func ReadShort(r io.Reader) (data uint16, err error) { + err = binary.Read(r, binary.BigEndian, &data) + return +} + +// WriteShort writes 2 bytes +func WriteShort(wr io.Writer, data uint16) error { + var b [2]byte + binary.BigEndian.PutUint16(b[:], data) + return writeSlice(wr, b[:]) +} + +// ReadLong reads 4 bytes +func ReadLong(r io.Reader) (data uint32, err error) { + err = binary.Read(r, binary.BigEndian, &data) + return +} + +// WriteLong writes 4 bytes +func WriteLong(wr io.Writer, data uint32) error { + var b [4]byte + binary.BigEndian.PutUint32(b[:], data) + return writeSlice(wr, b[:]) +} + +// ReadLonglong reads 8 bytes +func ReadLonglong(r io.Reader) (data uint64, err error) { + err = binary.Read(r, binary.BigEndian, &data) + return +} + +// WriteLonglong writes 8 bytes +func WriteLonglong(wr io.Writer, data uint64) error { + var b [8]byte + binary.BigEndian.PutUint64(b[:], data) + return writeSlice(wr, b[:]) +} + +// ReadTimestamp reads timestamp +// amqp presents timestamp as 8byte int +func ReadTimestamp(r io.Reader) (data time.Time, err error) { + var seconds uint64 + if seconds, err = ReadLonglong(r); err != nil { + return + } + return time.Unix(int64(seconds), 0), nil +} + +// WriteTimestamp writes timestamp +func WriteTimestamp(wr io.Writer, data time.Time) error { + return WriteLonglong(wr, uint64(data.Unix())) +} + +// ReadShortstr reads string +func ReadShortstr(r io.Reader) (data string, err error) { + var length byte + + length, err = ReadOctet(r) + if err != nil { + return "", err + } + + strBytes := make([]byte, length) + + _, err = io.ReadFull(r, strBytes) + if err != nil { + return "", err + } + data = string(strBytes) + return +} + +// WriteShortstr writes string +func WriteShortstr(wr io.Writer, data string) error { + if err := WriteOctet(wr, byte(len(data))); err != nil { + return err + } + if _, err := wr.Write([]byte(data)); err != nil { + return err + } + + return nil +} + +// ReadLongstr reads long string +// Long string is just array of bytes +func ReadLongstr(r io.Reader) (data []byte, err error) { + var length uint32 + + length, err = ReadLong(r) + if err != nil { + return nil, err + } + + data = make([]byte, length) + + _, err = io.ReadFull(r, data) + if err != nil { + return nil, err + } + return +} + +// WriteLongstr writes long string +func WriteLongstr(wr io.Writer, data []byte) error { + err := WriteLong(wr, uint32(len(data))) + if err != nil { + return err + } + _, err = wr.Write(data) + if err != nil { + return err + } + return nil +} + +// ReadTable reads amqp table +// Standard amqp table and rabbitmq table are little different +// So we have second argument protoVersion to handle that issue +func ReadTable(r io.Reader, protoVersion string) (data *Table, err error) { + tmpData := Table{} + tableData, err := ReadLongstr(r) + if err != nil { + return nil, err + } + + tableReader := bytes.NewReader(tableData) + for tableReader.Len() > 0 { + var key string + var value interface{} + if key, err = ReadShortstr(tableReader); err != nil { + return nil, errors.New("Unable to read key from table: " + err.Error()) + } + + if value, err = readV(tableReader, protoVersion); err != nil { + return nil, errors.New("Unable to read value from table: " + err.Error()) + } + + tmpData[key] = value + } + + return &tmpData, nil +} + +func readV(r io.Reader, protoVersion string) (data interface{}, err error) { + switch protoVersion { + case Proto091: + return readValue091(r) + case ProtoRabbit: + return readValueRabbit(r) + } + + return nil, fmt.Errorf("unknown proto version [%s]", protoVersion) +} + +/* +Standard amqp-0-9-1 table fields + +'t' bool boolean +'b' int8 short-short-int +'B' uint8 short-short-uint +'U' int16 short-int +'u' uint16 short-uint +'I' int32 long-int +'i' uint32 long-uint +'L' int64 long-long-int +'l' uint64 long-long-uint +'f' float float +'d' double double +'D' Decimal decimal-value +'s' string short-string +'S' []byte long-string +'A' []interface{} field-array +'T' time.Time timestamp +'F' Table field-table +'V' nil no-field +*/ +func readValue091(r io.Reader) (data interface{}, err error) { + vType, err := ReadOctet(r) + if err != nil { + return nil, err + } + + switch vType { + case 't': + var rData byte + rData, err = ReadOctet(r) + if err != nil { + return nil, err + } + return rData != 0, nil + case 'b': + var rData int8 + if err = binary.Read(r, binary.BigEndian, &rData); err != nil { + return nil, err + } + return rData, nil + case 'B': + var rData uint8 + if err = binary.Read(r, binary.BigEndian, &rData); err != nil { + return nil, err + } + return rData, nil + case 'U': + var rData int16 + if err = binary.Read(r, binary.BigEndian, &rData); err != nil { + return nil, err + } + return rData, nil + case 'u': + var rData uint16 + if err = binary.Read(r, binary.BigEndian, &rData); err != nil { + return nil, err + } + return rData, nil + case 'I': + var rData int32 + if err = binary.Read(r, binary.BigEndian, &rData); err != nil { + return nil, err + } + return rData, nil + case 'i': + var rData uint32 + if err = binary.Read(r, binary.BigEndian, &rData); err != nil { + return nil, err + } + return rData, nil + case 'L': + var rData int64 + if err = binary.Read(r, binary.BigEndian, &rData); err != nil { + return nil, err + } + return rData, nil + case 'l': + var rData uint64 + if err = binary.Read(r, binary.BigEndian, &rData); err != nil { + return nil, err + } + return rData, nil + case 'f': + var rData float32 + if err = binary.Read(r, binary.BigEndian, &rData); err != nil { + return nil, err + } + return rData, nil + case 'd': + var rData float64 + if err = binary.Read(r, binary.BigEndian, &rData); err != nil { + return nil, err + } + return rData, nil + case 'D': + var rData = Decimal{0, 0} + + if err = binary.Read(r, binary.BigEndian, &rData.Scale); err != nil { + return nil, err + } + if err = binary.Read(r, binary.BigEndian, &rData.Value); err != nil { + return nil, err + } + return rData, nil + case 's': + var rData string + if rData, err = ReadShortstr(r); err == nil { + return nil, err + } + + return rData, nil + case 'S': + var rData []byte + if rData, err = ReadLongstr(r); err == nil { + return nil, err + } + + return rData, nil + case 'T': + var rData time.Time + if rData, err = ReadTimestamp(r); err == nil { + return nil, err + } + + return rData, nil + case 'A': + var rData []interface{} + if rData, err = readArray(r, Proto091); err == nil { + return nil, err + } + return rData, nil + case 'F': + var rData *Table + if rData, err = ReadTable(r, Proto091); err == nil { + return nil, err + } + return rData, nil + case 'V': + return nil, nil + } + + return nil, fmt.Errorf("unsupported type %c (%d) by %s protocol", vType, vType, Proto091) +} + +/* +Rabbitmq table fields + +'t' bool boolean +'b' int8 short-short-int +'s' int16 short-int +'I' int32 long-int +'l' int64 long-long-int +'f' float float +'d' double double +'D' Decimal decimal-value +'S' []byte long-string +'T' time.Time timestamp +'F' Table field-table +'V' nil no-field +'x' []interface{} field-array +*/ +func readValueRabbit(r io.Reader) (data interface{}, err error) { + vType, err := ReadOctet(r) + if err != nil { + return nil, err + } + + switch vType { + case 't': + var rData byte + rData, err = ReadOctet(r) + if err != nil { + return nil, err + } + return rData != 0, nil + case 'b': + var rData int8 + if err = binary.Read(r, binary.BigEndian, &rData); err != nil { + return nil, err + } + return rData, nil + case 's': + var rData int16 + if err = binary.Read(r, binary.BigEndian, &rData); err != nil { + return nil, err + } + return rData, nil + case 'I': + var rData int32 + if err = binary.Read(r, binary.BigEndian, &rData); err != nil { + return nil, err + } + return rData, nil + case 'l': + var rData int64 + if err = binary.Read(r, binary.BigEndian, &rData); err != nil { + return nil, err + } + return rData, nil + case 'f': + var rData float32 + if err = binary.Read(r, binary.BigEndian, &rData); err != nil { + return nil, err + } + return rData, nil + case 'd': + var rData float64 + if err = binary.Read(r, binary.BigEndian, &rData); err != nil { + return nil, err + } + return rData, nil + case 'D': + var rData = Decimal{0, 0} + + if err = binary.Read(r, binary.BigEndian, &rData.Scale); err != nil { + return nil, err + } + if err = binary.Read(r, binary.BigEndian, &rData.Value); err != nil { + return nil, err + } + return rData, nil + case 'S': + var rData []byte + if rData, err = ReadLongstr(r); err != nil { + return nil, err + } + + return string(rData), nil + case 'T': + var rData time.Time + if rData, err = ReadTimestamp(r); err != nil { + return nil, err + } + + return rData, nil + case 'x': + var rData []interface{} + if rData, err = readArray(r, ProtoRabbit); err != nil { + return nil, err + } + return rData, nil + case 'F': + var rData *Table + if rData, err = ReadTable(r, ProtoRabbit); err != nil { + return nil, err + } + return rData, nil + case 'V': + return nil, nil + } + + return nil, fmt.Errorf("unsupported type %c (%d) by %s protocol", vType, vType, ProtoRabbit) +} + +// WriteTable writes amqp table +// Standard amqp table and rabbitmq table are little different +// So we have second argument protoVersion to handle that issue +func WriteTable(writer io.Writer, table *Table, protoVersion string) (err error) { + var buf = emptyBufferPool.Get() + defer emptyBufferPool.Put(buf) + for key, v := range *table { + if err := WriteShortstr(buf, key); err != nil { + return err + } + if err := writeV(buf, v, protoVersion); err != nil { + return err + } + } + return WriteLongstr(writer, buf.Bytes()) +} + +func writeV(writer io.Writer, v interface{}, protoVersion string) (err error) { + switch protoVersion { + case Proto091: + return writeValue091(writer, v) + case ProtoRabbit: + return writeValueRabbit(writer, v) + } + + return fmt.Errorf("unknown proto version [%s]", protoVersion) +} + +/* +Standard amqp-0-9-1 table fields + +'t' bool boolean +'b' int8 short-short-int +'B' uint8 short-short-uint +'U' int16 short-int +'u' uint16 short-uint +'I' int32 long-int +'i' uint32 long-uint +'L' int64 long-long-int +'l' uint64 long-long-uint +'f' float float +'d' double double +'D' Decimal decimal-value +'s' string short-string +'S' []byte long-string +'A' []interface{} field-array +'T' time.Time timestamp +'F' Table field-table +'V' nil no-field +*/ +func writeValue091(writer io.Writer, v interface{}) (err error) { + switch value := v.(type) { + case bool: + if err = WriteOctet(writer, byte('t')); err == nil { + if value { + err = binary.Write(writer, binary.BigEndian, uint8(1)) + } else { + err = binary.Write(writer, binary.BigEndian, uint8(0)) + } + } + case int8: + if err = WriteOctet(writer, byte('b')); err == nil { + err = binary.Write(writer, binary.BigEndian, value) + } + case uint8: + if err = WriteOctet(writer, byte('B')); err == nil { + err = binary.Write(writer, binary.BigEndian, value) + } + case int16: + if err = WriteOctet(writer, byte('U')); err == nil { + err = binary.Write(writer, binary.BigEndian, value) + } + case uint16: + if err = binary.Write(writer, binary.BigEndian, byte('u')); err == nil { + err = binary.Write(writer, binary.BigEndian, value) + } + case int32: + if err = binary.Write(writer, binary.BigEndian, byte('I')); err == nil { + err = binary.Write(writer, binary.BigEndian, value) + } + case uint32: + if err = binary.Write(writer, binary.BigEndian, byte('i')); err == nil { + err = binary.Write(writer, binary.BigEndian, value) + } + case int64: + if err = binary.Write(writer, binary.BigEndian, byte('L')); err == nil { + err = binary.Write(writer, binary.BigEndian, value) + } + case uint64: + if err = binary.Write(writer, binary.BigEndian, byte('l')); err == nil { + err = binary.Write(writer, binary.BigEndian, value) + } + case float32: + if err = binary.Write(writer, binary.BigEndian, byte('f')); err == nil { + err = binary.Write(writer, binary.BigEndian, value) + } + case float64: + if err = binary.Write(writer, binary.BigEndian, byte('d')); err == nil { + err = binary.Write(writer, binary.BigEndian, value) + } + case Decimal: + if err = binary.Write(writer, binary.BigEndian, byte('D')); err == nil { + if err = binary.Write(writer, binary.BigEndian, byte(value.Scale)); err == nil { + err = binary.Write(writer, binary.BigEndian, uint32(value.Value)) + } + } + case string: + if err = WriteOctet(writer, byte('s')); err == nil { + err = WriteShortstr(writer, value) + } + case []byte: + if err = WriteOctet(writer, byte('S')); err == nil { + err = WriteLongstr(writer, value) + } + case time.Time: + if err = WriteOctet(writer, byte('T')); err == nil { + err = WriteTimestamp(writer, value) + } + case []interface{}: + if err = WriteOctet(writer, byte('A')); err == nil { + err = writeArray(writer, value, Proto091) + } + + case Table: + if err = WriteOctet(writer, byte('F')); err == nil { + err = WriteTable(writer, &value, Proto091) + } + case nil: + err = binary.Write(writer, binary.BigEndian, byte('V')) + default: + err = fmt.Errorf("unsupported type by %s protocol", Proto091) + } + + return +} + +/* +Rabbitmq table fields + +'t' bool boolean +'b' int8 short-short-int +'s' int16 short-int +'I' int32 long-int +'l' int64 long-long-int +'f' float float +'d' double double +'D' Decimal decimal-value +'S' []byte long-string +'T' time.Time timestamp +'F' Table field-table +'V' nil no-field +'x' []interface{} field-array +*/ +func writeValueRabbit(writer io.Writer, v interface{}) (err error) { + switch value := v.(type) { + case bool: + if err = WriteOctet(writer, byte('t')); err == nil { + if value { + err = binary.Write(writer, binary.BigEndian, uint8(1)) + } else { + err = binary.Write(writer, binary.BigEndian, uint8(0)) + } + } + case int8: + if err = WriteOctet(writer, byte('b')); err == nil { + err = binary.Write(writer, binary.BigEndian, value) + } + case uint8: + if err = WriteOctet(writer, byte('b')); err == nil { + err = binary.Write(writer, binary.BigEndian, int8(value)) + } + case int16: + if err = WriteOctet(writer, byte('s')); err == nil { + err = binary.Write(writer, binary.BigEndian, value) + } + case uint16: + if err = binary.Write(writer, binary.BigEndian, byte('s')); err == nil { + err = binary.Write(writer, binary.BigEndian, int16(value)) + } + case int32: + if err = binary.Write(writer, binary.BigEndian, byte('I')); err == nil { + err = binary.Write(writer, binary.BigEndian, value) + } + case uint32: + if err = binary.Write(writer, binary.BigEndian, byte('I')); err == nil { + err = binary.Write(writer, binary.BigEndian, int32(value)) + } + case int64: + if err = binary.Write(writer, binary.BigEndian, byte('l')); err == nil { + err = binary.Write(writer, binary.BigEndian, value) + } + case uint64: + if err = binary.Write(writer, binary.BigEndian, byte('l')); err == nil { + err = binary.Write(writer, binary.BigEndian, int64(value)) + } + case float32: + if err = binary.Write(writer, binary.BigEndian, byte('f')); err == nil { + err = binary.Write(writer, binary.BigEndian, value) + } + case float64: + if err = binary.Write(writer, binary.BigEndian, byte('d')); err == nil { + err = binary.Write(writer, binary.BigEndian, value) + } + case Decimal: + if err = binary.Write(writer, binary.BigEndian, byte('D')); err == nil { + if err = binary.Write(writer, binary.BigEndian, byte(value.Scale)); err == nil { + err = binary.Write(writer, binary.BigEndian, uint32(value.Value)) + } + } + case []byte: + if err = WriteOctet(writer, byte('S')); err == nil { + err = WriteLongstr(writer, value) + } + case string: + if err = WriteOctet(writer, byte('S')); err == nil { + err = WriteLongstr(writer, []byte(value)) + } + case time.Time: + if err = WriteOctet(writer, byte('T')); err == nil { + err = WriteTimestamp(writer, value) + } + case []interface{}: + if err = WriteOctet(writer, byte('x')); err == nil { + err = writeArray(writer, value, ProtoRabbit) + } + case Table: + if err = WriteOctet(writer, byte('F')); err == nil { + err = WriteTable(writer, &value, ProtoRabbit) + } + case nil: + err = binary.Write(writer, binary.BigEndian, byte('V')) + default: + err = fmt.Errorf("unsupported type by %s protocol", Proto091) + } + + return +} + +func writeArray(writer io.Writer, array []interface{}, protoVersion string) error { + var buf = emptyBufferPool.Get() + defer emptyBufferPool.Put(buf) + + for _, v := range array { + if err := writeV(buf, v, protoVersion); err != nil { + return err + } + } + return WriteLongstr(writer, buf.Bytes()) +} + +func readArray(r io.Reader, protoVersion string) (data []interface{}, err error) { + data = make([]interface{}, 0) + var arrayData []byte + if arrayData, err = ReadLongstr(r); err != nil { + return nil, err + } + + arrayBuffer := bytes.NewBuffer(arrayData) + for arrayBuffer.Len() > 0 { + var itemV interface{} + if itemV, err = readV(arrayBuffer, protoVersion); err != nil { + return nil, err + } + + data = append(data, itemV) + } + + return data, nil +} + +/* +ReadContentHeader reads amqp content header + +Certain methods (such as Basic.Publish, Basic.Deliver, etc.) are formally +defined as carrying content. When a peer sends such a method frame, it always +follows it with a content header and zero or more content body frames. + +A content header frame has this format: + + 0 2 4 12 14 + +----------+--------+-----------+----------------+------------- - - + | class-id | weight | body size | property flags | property list... + +----------+--------+-----------+----------------+------------- - - + short short long long short remainder... +*/ +func ReadContentHeader(r io.Reader, protoVersion string) (*ContentHeader, error) { + var err error + // 14 bytes for class-id | weight | body size | property flags + headerBuf := headerBufferPool.Get() + defer headerBufferPool.Put(headerBuf) + + var header [14]byte + if _, err = io.ReadFull(r, header[:]); err != nil { + return nil, err + } + if _, err = headerBuf.Write(header[:]); err != nil { + return nil, err + } + + contentHeader := &ContentHeader{} + + if contentHeader.ClassID, err = ReadShort(headerBuf); err != nil { + return nil, err + } + if contentHeader.Weight, err = ReadShort(headerBuf); err != nil { + return nil, err + } + if contentHeader.BodySize, err = ReadLonglong(headerBuf); err != nil { + return nil, err + } + if contentHeader.propertyFlags, err = ReadShort(headerBuf); err != nil { + return nil, err + } + + contentHeader.PropertyList = &BasicPropertyList{} + if err = contentHeader.PropertyList.Read(r, contentHeader.propertyFlags, protoVersion); err != nil { + return nil, err + } + + return contentHeader, nil +} + +// WriteContentHeader writes amqp content header +func WriteContentHeader(writer io.Writer, header *ContentHeader, protoVersion string) (err error) { + if err = WriteShort(writer, header.ClassID); err != nil { + return err + } + if err = WriteShort(writer, header.Weight); err != nil { + return err + } + if err = WriteLonglong(writer, header.BodySize); err != nil { + return err + } + + var propertyBuf = emptyBufferPool.Get() + defer emptyBufferPool.Put(propertyBuf) + + properyFlags, err := header.PropertyList.Write(propertyBuf, protoVersion) + if err != nil { + return err + } + + header.propertyFlags = properyFlags + if err = WriteShort(writer, header.propertyFlags); err != nil { + return err + } + if _, err = writer.Write(propertyBuf.Bytes()); err != nil { + return err + } + + return +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/amqp/types.go b/pkg/outputs/amqp/_fixtures/garagemq/amqp/types.go new file mode 100644 index 000000000..3dd451d82 --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/amqp/types.go @@ -0,0 +1,198 @@ +package amqp + +import ( + "bytes" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/pool" + "sync/atomic" + "time" +) + +var emptyMessageBufferPool = pool.NewBufferPool(0) + +// Table - simple amqp-table implementation +type Table map[string]interface{} + +// Decimal represents amqp-decimal data +type Decimal struct { + Scale uint8 + Value int32 +} + +// Frame is raw frame +type Frame struct { + ChannelID uint16 + Type byte + CloseAfter bool + Sync bool + Payload []byte +} + +// ContentHeader represents amqp-message content-header +type ContentHeader struct { + BodySize uint64 + ClassID uint16 + Weight uint16 + propertyFlags uint16 + PropertyList *BasicPropertyList +} + +// ConfirmMeta store information for check confirms and send confirm-acks +type ConfirmMeta struct { + ChanID uint16 + ConnID uint64 + DeliveryTag uint64 + ExpectedConfirms int + ActualConfirms int +} + +// CanConfirm returns is message can be confirmed +func (meta *ConfirmMeta) CanConfirm() bool { + return meta.ActualConfirms == meta.ExpectedConfirms +} + +// Message represents amqp-message and meta-data +type Message struct { + ID uint64 + BodySize uint64 + DeliveryCount uint32 + Mandatory bool + Immediate bool + Exchange string + RoutingKey string + ConfirmMeta *ConfirmMeta + Header *ContentHeader + Body []*Frame +} + +// when server restart we can't start again count messages from 0 +var msgID = uint64(time.Now().UnixNano()) + +// NewMessage returns new message instance +func NewMessage(method *BasicPublish) *Message { + return &Message{ + Exchange: method.Exchange, + RoutingKey: method.RoutingKey, + Mandatory: method.Mandatory, + Immediate: method.Immediate, + BodySize: 0, + DeliveryCount: 0, + } +} + +// IsPersistent check if message should be persisted +func (m *Message) IsPersistent() bool { + deliveryMode := m.Header.PropertyList.DeliveryMode + return deliveryMode != nil && *deliveryMode == 2 +} + +// GenerateSeq returns next message ID +func (m *Message) GenerateSeq() { + if m.ID == 0 { + m.ID = atomic.AddUint64(&msgID, 1) + } +} + +// Append appends new body-frame into message and increase bodySize +func (m *Message) Append(body *Frame) { + m.Body = append(m.Body, body) + m.BodySize += uint64(len(body.Payload)) +} + +// Marshal converts message into bytes to store into db +func (m *Message) Marshal(protoVersion string) (data []byte, err error) { + buffer := emptyMessageBufferPool.Get() + defer emptyMessageBufferPool.Put(buffer) + + if err = WriteLonglong(buffer, m.ID); err != nil { + return nil, err + } + + if err = WriteContentHeader(buffer, m.Header, protoVersion); err != nil { + return nil, err + } + if err = WriteShortstr(buffer, m.Exchange); err != nil { + return nil, err + } + if err = WriteShortstr(buffer, m.RoutingKey); err != nil { + return nil, err + } + + for _, frame := range m.Body { + if err = WriteFrame(buffer, frame); err != nil { + return nil, err + } + } + + data = make([]byte, buffer.Len()) + copy(data, buffer.Bytes()) + return +} + +// Unmarshal restore message entity from bytes +func (m *Message) Unmarshal(buffer []byte, protoVersion string) (err error) { + reader := bytes.NewReader(buffer) + if m.ID, err = ReadLonglong(reader); err != nil { + return err + } + + if m.Header, err = ReadContentHeader(reader, protoVersion); err != nil { + return err + } + if m.Exchange, err = ReadShortstr(reader); err != nil { + return err + } + if m.RoutingKey, err = ReadShortstr(reader); err != nil { + return err + } + + for m.BodySize < m.Header.BodySize { + body, errFrame := ReadFrame(reader) + if errFrame != nil { + return errFrame + } + m.Append(body) + } + + return nil +} + +// Constants to detect connection or channel error thrown +const ( + ErrorOnConnection = iota + ErrorOnChannel +) + +// Error represents AMQP-error data +type Error struct { + ReplyCode uint16 + ReplyText string + ClassID uint16 + MethodID uint16 + ErrorType int +} + +// NewConnectionError returns new connection error. If caused - connection should be closed +func NewConnectionError(code uint16, text string, classID uint16, methodID uint16) *Error { + err := &Error{ + ReplyCode: code, + ReplyText: ConstantsNameMap[code] + " - " + text, + ClassID: classID, + MethodID: methodID, + ErrorType: ErrorOnConnection, + } + + return err +} + +// NewChannelError returns new channel error& If caused - channel should be closed +func NewChannelError(code uint16, text string, classID uint16, methodID uint16) *Error { + err := &Error{ + ReplyCode: code, + ReplyText: ConstantsNameMap[code] + " - " + text, + ClassID: classID, + MethodID: methodID, + ErrorType: ErrorOnChannel, + } + + return err +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/auth/auth.go b/pkg/outputs/amqp/_fixtures/garagemq/auth/auth.go new file mode 100644 index 000000000..97216eb93 --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/auth/auth.go @@ -0,0 +1,51 @@ +package auth + +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "errors" +) + +// SaslPlain method +const SaslPlain = "PLAIN" + +// SaslData represents standard SASL properties +type SaslData struct { + Identity string + Username string + Password string +} + +// ParsePlain check and parse SASL-raw data and return SaslData structure +func ParsePlain(response []byte) (SaslData, error) { + parts := bytes.Split(response, []byte{0}) + if len(parts) != 3 { + return SaslData{}, errors.New("Unable to parse PLAIN SALS response") + } + + saslData := SaslData{} + saslData.Identity = string(parts[0]) + saslData.Username = string(parts[1]) + saslData.Password = string(parts[2]) + + return saslData, nil +} + +// HashPassword hash raw password and return hash for check +func HashPassword(password string, isMd5 bool) (string, error) { + h := md5.New() + h.Write([]byte(password)) + return hex.EncodeToString(h.Sum(nil)), nil +} + +// CheckPasswordHash check given password and hash +func CheckPasswordHash(password, hash string, isMd5 bool) bool { + + h := md5.New() + // digest.Write never return any error, so skip error ckeck + h.Write([]byte(password)) + + return hash == hex.EncodeToString(h.Sum(nil)) + +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/binding/binding.go b/pkg/outputs/amqp/_fixtures/garagemq/binding/binding.go new file mode 100644 index 000000000..9236d326a --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/binding/binding.go @@ -0,0 +1,311 @@ +package binding + +import ( + "bytes" + "fmt" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/amqp" + "reflect" + "regexp" + "strings" +) + +// MatchType is the x-match attribute in a binding argument table +type MatchType int + +const ( + // MatchAll requires all registered arguments to match for routing + MatchAll MatchType = iota + // MatchAny requires any registered arguments to match for routing + MatchAny +) + +// Binding represents AMQP-binding +type Binding struct { + Queue string + Exchange string + RoutingKey string + Arguments *amqp.Table + regexp *regexp.Regexp + topic bool + MatchType MatchType +} + +// NewBinding returns new instance of Binding +func NewBinding(queue string, exchange string, routingKey string, arguments *amqp.Table, topic bool) (*Binding, error) { + binding := &Binding{ + Queue: queue, + Exchange: exchange, + RoutingKey: routingKey, + Arguments: arguments, + topic: topic, + } + + if topic { + var err error + if binding.regexp, err = buildRegexp(routingKey); err != nil { + return nil, fmt.Errorf("bad topic routing key %s -- %s", + routingKey, + err.Error()) + } + } + + if arguments == nil { + return binding, nil + } + + // @spec-note AMQP 0.9.1 + // + // Any field starting with 'x-' other than 'x-match' is + // reserved for future use and will be ignored. + // + // * 'all' implies that all the other pairs must match the headers + // property of a message for that message to be routed (i.e. and AND match) + // * 'any' implies that the message should be routed if any of the + // fields in the headers property match one of the fields in the + // arguments table (i.e. an OR match) + // + // We arbitrarily choose `all` as the default if none was provided + // at binding time. + xmatch, ok := (*arguments)["x-match"] + if ok { + if xmatch == "all" { + binding.MatchType = MatchAll + } else if xmatch == "any" { + binding.MatchType = MatchAny + } else { + return nil, fmt.Errorf("Invalid x-match field value %s, expected all or any", + xmatch) + } + } else { + binding.MatchType = MatchAll + } + + return binding, nil +} + +// @todo may be better will be trie or dfa than regexp +// @see http://www.rabbitmq.com/blog/2010/09/14/very-fast-and-scalable-topic-routing-part-1/ +// @see http://www.rabbitmq.com/blog/2011/03/28/very-fast-and-scalable-topic-routing-part-2/ +// +// buildRegexp generate regexp from topic-match string +func buildRegexp(routingKey string) (*regexp.Regexp, error) { + routingKey = strings.TrimSpace(routingKey) + routingParts := strings.Split(routingKey, ".") + + for idx, routingPart := range routingParts { + if routingPart == "*" { + routingParts[idx] = "*" + } else if routingPart == "#" { + routingParts[idx] = "#" + } else { + routingParts[idx] = regexp.QuoteMeta(routingPart) + } + } + + routingKey = strings.Join(routingParts, "\\.") + routingKey = strings.Replace(routingKey, "*", `([^\.]+)`, -1) + + for strings.HasPrefix(routingKey, "#\\.") { + routingKey = strings.TrimPrefix(routingKey, "#\\.") + if strings.HasPrefix(routingKey, "#\\.") { + continue + } + routingKey = `(.*\.?)+` + routingKey + } + + for strings.HasSuffix(routingKey, "\\.#") { + routingKey = strings.TrimSuffix(routingKey, "\\.#") + if strings.HasSuffix(routingKey, "\\.#") { + continue + } + routingKey = routingKey + `(.*\.?)+` + } + routingKey = strings.Replace(routingKey, "\\.#\\.", `(.*\.?)+`, -1) + routingKey = strings.Replace(routingKey, "#", `(.*\.?)+`, -1) + pattern := "^" + routingKey + "$" + + return regexp.Compile(pattern) +} + +// MatchDirect check is message can be routed from direct-exchange to queue +// with compare exchange and routing key +func (b *Binding) MatchDirect(exchange string, routingKey string) bool { + return b.Exchange == exchange && b.RoutingKey == routingKey +} + +// MatchFanout check is message can be routed from fanout-exchange to queue +// with compare only exchange +func (b *Binding) MatchFanout(exchange string) bool { + return b.Exchange == exchange +} + +// MatchTopic check is message can be routed from topic-exchange to queue +// with compare exchange and match topic-pattern with routing key +func (b *Binding) MatchTopic(exchange string, routingKey string) bool { + return b.Exchange == exchange && b.regexp.MatchString(routingKey) +} + +// MatchHeader checks whether the message can be routed on `b` for a +// header exchange type. +func (b *Binding) MatchHeader(exchange string, headers *amqp.Table) bool { + if b.Exchange != exchange { + return false + } + + // If no arguments were declared by the exchange, + // consider it is an always true route. + if b.Arguments == nil { + return true + } + + if headers == nil { + return false + } + + bindingArgTable := *b.Arguments + cliHeaders := *headers + + matchType := b.MatchType + + // Fallback solution for the x-match any case, and no other + // argument in the table + // + // If no match is found in the loop, and arguments other than + // x-match were specified, it should not return a positive + // value in the end. + hasNonXArgs := false + + for key, value := range bindingArgTable { + // Any field starting with 'x-' shall be ignored + if strings.HasPrefix(key, "x-") { + continue + } + + hasNonXArgs = true + + val, ok := cliHeaders[key] + + if !ok { + if matchType == MatchAll { + return false + } + continue + } + + // @spec-note AMQP 0.9.1 + // + // A message queue is bound to the exchange with a table of + // arguments containing the headers to be matched for that + // binding and optionally the values they should hold + if value == nil { + if matchType == MatchAny { + return true + } + continue + } + + if value == val { + if matchType == MatchAny { + return true + } + continue + } + + if matchType == MatchAll { + return false + } + } + + return matchType == MatchAll || + !hasNonXArgs && matchType == MatchAny +} + +// GetExchange returns binding's exchange +func (b *Binding) GetExchange() string { + return b.Exchange +} + +// GetRoutingKey returns binding's routing key +func (b *Binding) GetRoutingKey() string { + return b.RoutingKey +} + +// GetQueue returns binding's queue +func (b *Binding) GetQueue() string { + return b.Queue +} + +// Equal returns is given binding equal to current +// with compare exchange, routing key and queue +func (b *Binding) Equal(bind *Binding) bool { + return b.Exchange == bind.GetExchange() && + b.Queue == bind.GetQueue() && + b.RoutingKey == bind.GetRoutingKey() && + reflect.DeepEqual(b.Arguments, bind.Arguments) +} + +// GetName generate binding name by concatenating its params +func (b *Binding) GetName() string { + return strings.Join( + []string{b.Queue, b.Exchange, b.RoutingKey}, + "_", + ) +} + +// Marshal returns raw representation of binding to store into storage +func (b *Binding) Marshal(protoVersion string) (data []byte, err error) { + buf := bytes.NewBuffer(make([]byte, 0)) + if err = amqp.WriteShortstr(buf, b.Queue); err != nil { + return nil, err + } + if err = amqp.WriteShortstr(buf, b.Exchange); err != nil { + return nil, err + } + if err = amqp.WriteShortstr(buf, b.RoutingKey); err != nil { + return nil, err + } + // Since marshalling is used for storage only, we can + // simplify the Marshal/Unmarshal of arguments by + // writing them in Rabbit format, and reading them as such + if err = amqp.WriteTable(buf, b.Arguments, protoVersion); err != nil { + return nil, err + } + var topic byte + if b.topic { + topic = 1 + } + if err = amqp.WriteOctet(buf, topic); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// Unmarshal returns binding from storage raw bytes data +func (b *Binding) Unmarshal(data []byte, protoVersion string) (err error) { + buf := bytes.NewReader(data) + if b.Queue, err = amqp.ReadShortstr(buf); err != nil { + return err + } + if b.Exchange, err = amqp.ReadShortstr(buf); err != nil { + return err + } + if b.RoutingKey, err = amqp.ReadShortstr(buf); err != nil { + return err + } + if b.Arguments, err = amqp.ReadTable(buf, protoVersion); err != nil { + return err + } + var topic byte + if topic, err = amqp.ReadOctet(buf); err != nil { + return err + } + b.topic = topic == 1 + + if b.topic { + if b.regexp, err = buildRegexp(b.RoutingKey); err != nil { + return err + } + } + + return +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/config/config.go b/pkg/outputs/amqp/_fixtures/garagemq/config/config.go new file mode 100644 index 000000000..3f232a216 --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/config/config.go @@ -0,0 +1,89 @@ +package config + +import ( + "io/ioutil" + + "gopkg.in/yaml.v2" +) + +// Config represents server changeable se +type Config struct { + Proto string + Users []User + TCP TCPConfig + Queue Queue + Db Db + Vhost Vhost + Security Security + Connection Connection + Admin AdminConfig +} + +// User for auth check +type User struct { + Username string + Password string +} + +// TCPConfig represents properties for tune network connections +type TCPConfig struct { + IP string `yaml:"ip"` + Port string + Nodelay bool + ReadBufSize int `yaml:"readBufSize"` + WriteBufSize int `yaml:"writeBufSize"` +} + +// AdminConfig represents properties for admin server +type AdminConfig struct { + IP string `yaml:"ip"` + Port string +} + +// Queue settings +type Queue struct { + ShardSize int `yaml:"shardSize"` + MaxMessagesInRAM uint64 `yaml:"maxMessagesInRam"` +} + +// Db settings, such as path to load/save and engine +type Db struct { + DefaultPath string `yaml:"defaultPath"` + Engine string `yaml:"engine"` +} + +// Vhost settings +type Vhost struct { + DefaultPath string `yaml:"defaultPath"` +} + +// Security settings +type Security struct { + PasswordCheck string `yaml:"passwordCheck"` +} + +// Connection settings for AMQP-connection +type Connection struct { + ChannelsMax uint16 `yaml:"channelsMax"` + FrameMaxSize uint32 `yaml:"frameMaxSize"` +} + +// CreateFromFile creates config from file +func CreateFromFile(path string) (*Config, error) { + cfg := &Config{} + file, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + err = yaml.Unmarshal(file, &cfg) + if err != nil { + return nil, err + } + + return cfg, nil +} + +// CreateDefault creates config from default values +func CreateDefault() (*Config, error) { + return Default(), nil +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/config/default.go b/pkg/outputs/amqp/_fixtures/garagemq/config/default.go new file mode 100644 index 000000000..6a85930da --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/config/default.go @@ -0,0 +1,47 @@ +package config + +const ( + dbBuntDB = "buntdb" + dbBadger = "badger" +) + +func Default() *Config { + return &Config{ + Proto: "amqp-rabbit", + Users: []User{ + { + Username: "guest", + Password: "084e0343a0486ff05530df6c705c8bb4", // guest md5 hash + }, + }, + TCP: TCPConfig{ + IP: "0.0.0.0", + Port: "5672", + Nodelay: false, + ReadBufSize: 128 << 10, // 128Kb + WriteBufSize: 128 << 10, // 128Kb + }, + Admin: AdminConfig{ + IP: "0.0.0.0", + Port: "15672", + }, + Queue: Queue{ + ShardSize: 8 << 10, // 8k + MaxMessagesInRAM: 10 * 8 << 10, // 10 buckets + }, + Db: Db{ + DefaultPath: "db", + Engine: dbBadger, + }, + Vhost: Vhost{ + DefaultPath: "/", + }, + Security: Security{ + PasswordCheck: "md5", + }, + Connection: Connection{ + ChannelsMax: 4096, + FrameMaxSize: 65536, + }, + } +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/consumer/consumer.go b/pkg/outputs/amqp/_fixtures/garagemq/consumer/consumer.go new file mode 100644 index 000000000..68a8a4b58 --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/consumer/consumer.go @@ -0,0 +1,171 @@ +package consumer + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/amqp" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/interfaces" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/qos" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/queue" + "sync" + "sync/atomic" + "time" +) + +const ( + started = iota + stopped + paused +) + +var cid uint64 + +// Consumer implements AMQP consumer +type Consumer struct { + ID uint64 + Queue string + ConsumerTag string + noAck bool + channel interfaces.Channel + queue *queue.Queue + statusLock sync.RWMutex + status int + qos []*qos.AmqpQos + consume chan struct{} +} + +// NewConsumer returns new instance of Consumer +func NewConsumer(queueName string, consumerTag string, noAck bool, channel interfaces.Channel, queue *queue.Queue, qos []*qos.AmqpQos) *Consumer { + id := atomic.AddUint64(&cid, 1) + if consumerTag == "" { + consumerTag = generateTag(id) + } + return &Consumer{ + ID: id, + Queue: queueName, + ConsumerTag: consumerTag, + noAck: noAck, + channel: channel, + queue: queue, + qos: qos, + consume: make(chan struct{}, 1), + } +} + +func generateTag(id uint64) string { + return fmt.Sprintf("%d_%d", time.Now().Unix(), id) +} + +// Start starting consumer to fetch messages from queue +func (consumer *Consumer) Start() { + consumer.status = started + go consumer.startConsume() + consumer.Consume() +} + +// startConsume waiting a signal from consume channel and try to pop message from queue +// if not set noAck consumer pop message with qos rules and add message to unacked message queue +func (consumer *Consumer) startConsume() { + for range consumer.consume { + consumer.retrieveAndSendMessage() + } +} + +func (consumer *Consumer) retrieveAndSendMessage() { + var message *amqp.Message + consumer.statusLock.RLock() + defer consumer.statusLock.RUnlock() + if consumer.status == stopped { + return + } + + if consumer.noAck { + message = consumer.queue.Pop() + } else { + message = consumer.queue.PopQos(consumer.qos) + } + + if message == nil { + return + } + + dTag := consumer.channel.NextDeliveryTag() + if !consumer.noAck { + consumer.channel.AddUnackedMessage(dTag, consumer.ConsumerTag, consumer.queue.GetName(), message) + } + + consumer.channel.SendContent(&amqp.BasicDeliver{ + ConsumerTag: consumer.ConsumerTag, + DeliveryTag: dTag, + Redelivered: message.DeliveryCount > 1, + Exchange: message.Exchange, + RoutingKey: message.RoutingKey, + }, message) + + consumer.consumeMsg() + + return +} + +// Pause pause consumer, used by channel.flow change +func (consumer *Consumer) Pause() { + consumer.statusLock.Lock() + defer consumer.statusLock.Unlock() + consumer.status = paused +} + +// UnPause unpause consumer, used by channel.flow change +func (consumer *Consumer) UnPause() { + consumer.statusLock.Lock() + defer consumer.statusLock.Unlock() + consumer.status = started +} + +// Consume send signal into consumer channel, than consumer can try to pop message from queue +func (consumer *Consumer) Consume() bool { + consumer.statusLock.RLock() + defer consumer.statusLock.RUnlock() + + return consumer.consumeMsg() +} + +func (consumer *Consumer) consumeMsg() bool { + if consumer.status == stopped || consumer.status == paused { + return false + } + + select { + case consumer.consume <- struct{}{}: + return true + default: + return false + } +} + +// Stop stops consumer and remove it from queue consumers list +func (consumer *Consumer) Stop() { + consumer.statusLock.Lock() + if consumer.status == stopped { + consumer.statusLock.Unlock() + return + } + consumer.status = stopped + consumer.statusLock.Unlock() + consumer.queue.RemoveConsumer(consumer.ConsumerTag) + close(consumer.consume) +} + +// Cancel stops consumer and send basic.cancel method to the client +func (consumer *Consumer) Cancel() { + consumer.Stop() + consumer.channel.SendMethod(&amqp.BasicCancel{ConsumerTag: consumer.ConsumerTag, NoWait: true}) +} + +// Tag returns consumer tag +func (consumer *Consumer) Tag() string { + return consumer.ConsumerTag +} + +// Qos returns consumer qos rules +func (consumer *Consumer) Qos() []*qos.AmqpQos { + return consumer.qos +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/exchange/exchange.go b/pkg/outputs/amqp/_fixtures/garagemq/exchange/exchange.go new file mode 100644 index 000000000..ee412d478 --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/exchange/exchange.go @@ -0,0 +1,270 @@ +package exchange + +import ( + "bytes" + "fmt" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/amqp" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/binding" + "sync" +) + +// available exchange types +const ( + ExTypeDirect = iota + 1 + ExTypeFanout + ExTypeTopic + ExTypeHeaders +) + +var exchangeTypeIDAliasMap = map[byte]string{ + ExTypeDirect: "direct", + ExTypeFanout: "fanout", + ExTypeTopic: "topic", + ExTypeHeaders: "headers", +} + +var exchangeTypeAliasIDMap = map[string]byte{ + "direct": ExTypeDirect, + "fanout": ExTypeFanout, + "topic": ExTypeTopic, + "headers": ExTypeHeaders, +} + +// MetricsState implements exchange's metrics state +type MetricsState struct { +} + +// Exchange implements AMQP-exchange +type Exchange struct { + Name string + exType byte + durable bool + autoDelete bool + internal bool + system bool + bindLock sync.Mutex + bindings []*binding.Binding +} + +// NewExchange returns new instance of Exchange +func NewExchange(name string, exType byte, durable bool, autoDelete bool, internal bool, system bool) *Exchange { + return &Exchange{ + Name: name, + exType: exType, + durable: durable, + autoDelete: autoDelete, + internal: internal, + system: system, + } +} + +// GetExchangeTypeAlias returns exchange type alias by id +func GetExchangeTypeAlias(id byte) (alias string, err error) { + if alias, ok := exchangeTypeIDAliasMap[id]; ok { + return alias, nil + } + return "", fmt.Errorf("undefined exchange type '%d'", id) +} + +// GetExchangeTypeID returns exchange type id by alias +func GetExchangeTypeID(alias string) (id byte, err error) { + if id, ok := exchangeTypeAliasIDMap[alias]; ok { + return id, nil + } + return 0, fmt.Errorf("undefined exchange alias '%s'", alias) +} + +// GetTypeAlias returns exchange type alias by id +func (ex *Exchange) GetTypeAlias() string { + alias, _ := GetExchangeTypeAlias(ex.exType) + + return alias +} + +// AppendBinding check and append binding +// method check if binding already exists and ignore it +func (ex *Exchange) AppendBinding(newBind *binding.Binding) { + ex.bindLock.Lock() + defer ex.bindLock.Unlock() + + // @spec-note + // A server MUST allow ignore duplicate bindings ­ that is, two or more bind methods for a specific queue, + // with identical arguments ­ without treating these as an error. + for _, bind := range ex.bindings { + if bind.Equal(newBind) { + return + } + } + ex.bindings = append(ex.bindings, newBind) +} + +// RemoveBinding remove binding +func (ex *Exchange) RemoveBinding(rmBind *binding.Binding) { + ex.bindLock.Lock() + defer ex.bindLock.Unlock() + for i, bind := range ex.bindings { + if bind.Equal(rmBind) { + ex.bindings = append(ex.bindings[:i], ex.bindings[i+1:]...) + return + } + } +} + +// RemoveQueueBindings remove bindings for queue and return removed bindings +func (ex *Exchange) RemoveQueueBindings(queueName string) []*binding.Binding { + var newBindings []*binding.Binding + var removedBindings []*binding.Binding + ex.bindLock.Lock() + defer ex.bindLock.Unlock() + for _, bind := range ex.bindings { + if bind.GetQueue() != queueName { + newBindings = append(newBindings, bind) + } else { + removedBindings = append(removedBindings, bind) + } + } + + ex.bindings = newBindings + return removedBindings +} + +// GetMatchedQueues returns queues matched for message routing key +func (ex *Exchange) GetMatchedQueues(message *amqp.Message) (matchedQueues map[string]bool) { + // @spec-note + // The server MUST implement these standard exchange types: fanout, direct. + // The server SHOULD implement these standard exchange types: topic, headers. + + // TODO implement "headers" exchange + matchedQueues = make(map[string]bool) + switch ex.exType { + case ExTypeDirect: + for _, bind := range ex.bindings { + if bind.MatchDirect(message.Exchange, message.RoutingKey) { + matchedQueues[bind.GetQueue()] = true + return + } + } + case ExTypeFanout: + for _, bind := range ex.bindings { + if bind.MatchFanout(message.Exchange) { + matchedQueues[bind.GetQueue()] = true + } + } + case ExTypeTopic: + for _, bind := range ex.bindings { + if bind.MatchTopic(message.Exchange, message.RoutingKey) { + matchedQueues[bind.GetQueue()] = true + } + } + case ExTypeHeaders: + if message.Header == nil { + return + } + props := message.Header.PropertyList + if props == nil { + return + } + header := props.Headers + for _, bind := range ex.bindings { + if bind.MatchHeader(message.Exchange, header) { + matchedQueues[bind.GetQueue()] = true + } + } + } + return +} + +// EqualWithErr returns is given exchange equal to current +func (ex *Exchange) EqualWithErr(exB *Exchange) error { + errTemplate := "inequivalent arg '%s' for exchange '%s': received '%s' but current is '%s'" + if ex.exType != exB.ExType() { + aliasA, err := GetExchangeTypeAlias(ex.exType) + if err != nil { + return err + } + aliasB, err := GetExchangeTypeAlias(exB.ExType()) + if err != nil { + return err + } + return fmt.Errorf( + errTemplate, + "type", + ex.Name, + aliasB, + aliasA, + ) + } + if ex.durable != exB.IsDurable() { + return fmt.Errorf(errTemplate, "durable", ex.Name, exB.IsDurable(), ex.durable) + } + if ex.autoDelete != exB.IsAutoDelete() { + return fmt.Errorf(errTemplate, "autoDelete", ex.Name, exB.IsAutoDelete(), ex.autoDelete) + } + if ex.internal != exB.IsInternal() { + return fmt.Errorf(errTemplate, "internal", ex.Name, exB.IsInternal(), ex.internal) + } + return nil +} + +// GetBindings returns exchange's bindings +func (ex *Exchange) GetBindings() []*binding.Binding { + ex.bindLock.Lock() + defer ex.bindLock.Unlock() + return ex.bindings +} + +// IsDurable returns is exchange durable +func (ex *Exchange) IsDurable() bool { + return ex.durable +} + +// IsSystem returns is exchange system +func (ex *Exchange) IsSystem() bool { + return ex.system +} + +// IsAutoDelete returns should be exchange deleted when all queues have finished using it +func (ex *Exchange) IsAutoDelete() bool { + return ex.autoDelete +} + +// IsInternal returns that the exchange may not be used directly by publishers, +// but only when bound to other exchanges +func (ex *Exchange) IsInternal() bool { + return ex.internal +} + +// Marshal returns raw representation of exchange to store into storage +func (ex *Exchange) Marshal(protoVersion string) (data []byte, err error) { + buf := bytes.NewBuffer(make([]byte, 0)) + if err = amqp.WriteShortstr(buf, ex.Name); err != nil { + return nil, err + } + if err = amqp.WriteOctet(buf, ex.exType); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// Unmarshal returns exchange from storage raw bytes data +func (ex *Exchange) Unmarshal(data []byte) (err error) { + buf := bytes.NewReader(data) + if ex.Name, err = amqp.ReadShortstr(buf); err != nil { + return err + } + if ex.exType, err = amqp.ReadOctet(buf); err != nil { + return err + } + ex.durable = true + return +} + +// GetName returns exchange name +func (ex *Exchange) GetName() string { + return ex.Name +} + +// ExType returns exchange type +func (ex *Exchange) ExType() byte { + return ex.exType +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/interfaces/interfaces.go b/pkg/outputs/amqp/_fixtures/garagemq/interfaces/interfaces.go new file mode 100644 index 000000000..b950c93d9 --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/interfaces/interfaces.go @@ -0,0 +1,57 @@ +package interfaces + +import ( + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/amqp" +) + +// Channel represents base channel public interface +type Channel interface { + SendContent(method amqp.Method, message *amqp.Message) + SendMethod(method amqp.Method) + NextDeliveryTag() uint64 + AddUnackedMessage(dTag uint64, cTag string, queue string, message *amqp.Message) +} + +// Consumer represents base consumer public interface +type Consumer interface { + Consume() bool + Tag() string + Cancel() +} + +// OpSet identifier for set data into storeage +const OpSet = 1 + +// OpDel identifier for delete data from storage +const OpDel = 2 + +// Operation represents structure to set/del from storage +type Operation struct { + Key string + Value []byte + Op byte +} + +// DbStorage represent base db storage interface +type DbStorage interface { + Set(key string, value []byte) (err error) + Del(key string) (err error) + Get(key string) (value []byte, err error) + Iterate(fn func(key []byte, value []byte)) + IterateByPrefix(prefix []byte, limit uint64, fn func(key []byte, value []byte)) uint64 + IterateByPrefixFrom(prefix []byte, from []byte, limit uint64, fn func(key []byte, value []byte)) uint64 + DeleteByPrefix(prefix []byte) + KeysByPrefixCount(prefix []byte) uint64 + ProcessBatch(batch []*Operation) (err error) + Close() error +} + +// MsgStorage represent interface for messages storage +type MsgStorage interface { + Del(message *amqp.Message, queue string) error + PurgeQueue(queue string) + Add(message *amqp.Message, queue string) error + Update(message *amqp.Message, queue string) error + IterateByQueueFromMsgID(queue string, msgID uint64, limit uint64, fn func(message *amqp.Message)) uint64 + GetQueueLength(queue string) uint64 +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/pool/pool.go b/pkg/outputs/amqp/_fixtures/garagemq/pool/pool.go new file mode 100644 index 000000000..b703cd8a2 --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/pool/pool.go @@ -0,0 +1,51 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package pool + +import ( + "bytes" + "sync" +) + +// BufferPool represents a thread safe buffer pool +type BufferPool struct { + sync.Pool +} + +// NewBufferPool returns a new BufferPool +func NewBufferPool(bufferSize int) *BufferPool { + return &BufferPool{ + sync.Pool{ + New: func() interface{} { + return bytes.NewBuffer(make([]byte, 0, bufferSize)) + }, + }, + } +} + +// Get gets a Buffer from the pool +func (bp *BufferPool) Get() *bytes.Buffer { + return bp.Pool.Get().(*bytes.Buffer) +} + +// Put returns the given Buffer to the pool. +func (bp *BufferPool) Put(b *bytes.Buffer) { + b.Reset() + bp.Pool.Put(b) +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/qos/qos.go b/pkg/outputs/amqp/_fixtures/garagemq/qos/qos.go new file mode 100644 index 000000000..9b87b95c8 --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/qos/qos.go @@ -0,0 +1,101 @@ +package qos + +import "sync" + +// AmqpQos represents qos system +type AmqpQos struct { + sync.Mutex + prefetchCount uint16 + currentCount uint16 + prefetchSize uint32 + currentSize uint32 +} + +// NewAmqpQos returns new instance of AmqpQos +func NewAmqpQos(prefetchCount uint16, prefetchSize uint32) *AmqpQos { + return &AmqpQos{ + prefetchCount: prefetchCount, + prefetchSize: prefetchSize, + currentCount: 0, + currentSize: 0, + } +} + +// PrefetchCount returns prefetchCount +func (qos *AmqpQos) PrefetchCount() uint16 { + return qos.prefetchCount +} + +// PrefetchSize returns prefetchSize +func (qos *AmqpQos) PrefetchSize() uint32 { + return qos.prefetchSize +} + +// Update set new prefetchCount and prefetchSize +func (qos *AmqpQos) Update(prefetchCount uint16, prefetchSize uint32) { + qos.prefetchCount = prefetchCount + qos.prefetchSize = prefetchSize +} + +// IsActive check is qos rules are active +// both prefetchSize and prefetchCount must be 0 +func (qos *AmqpQos) IsActive() bool { + return qos.prefetchCount != 0 || qos.prefetchSize != 0 +} + +// Inc increment current count and size +// Returns true if increment success +// Returns false if after increment size or count will be more than prefetchCount or prefetchSize +func (qos *AmqpQos) Inc(count uint16, size uint32) bool { + qos.Lock() + defer qos.Unlock() + + newCount := qos.currentCount + count + newSize := qos.currentSize + size + + if (qos.prefetchCount == 0 || newCount <= qos.prefetchCount) && (qos.prefetchSize == 0 || newSize <= qos.prefetchSize) { + qos.currentCount = newCount + qos.currentSize = newSize + return true + } + + return false +} + +// Dec decrement current count and size +func (qos *AmqpQos) Dec(count uint16, size uint32) { + qos.Lock() + defer qos.Unlock() + + if qos.currentCount < count { + qos.currentCount = 0 + } else { + qos.currentCount = qos.currentCount - count + } + + if qos.currentSize < size { + qos.currentSize = 0 + } else { + qos.currentSize = qos.currentSize - size + } +} + +// Release reset current count and size +func (qos *AmqpQos) Release() { + qos.Lock() + defer qos.Unlock() + qos.currentCount = 0 + qos.currentSize = 0 +} + +// Copy safe copy current qos instance to new one +func (qos *AmqpQos) Copy() *AmqpQos { + qos.Lock() + defer qos.Unlock() + return &AmqpQos{ + prefetchCount: qos.prefetchCount, + prefetchSize: qos.prefetchSize, + currentCount: qos.currentCount, + currentSize: qos.currentSize, + } +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/queue/queue.go b/pkg/outputs/amqp/_fixtures/garagemq/queue/queue.go new file mode 100644 index 000000000..cc8c81440 --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/queue/queue.go @@ -0,0 +1,529 @@ +package queue + +import ( + "bytes" + "errors" + "fmt" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/amqp" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/config" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/interfaces" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/qos" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/safequeue" + "sync" + "sync/atomic" +) + +// Queue is an implementation of the AMQP-queue entity +type Queue struct { + safequeue.SafeQueue + name string + connID uint64 + exclusive bool + autoDelete bool + durable bool + cmrLock sync.RWMutex + consumers []interfaces.Consumer + consumeExcl bool + call chan struct{} + wasConsumed bool + shardSize int + actLock sync.RWMutex + active bool + // persistent storage + // transient storage + currentConsumer int + autoDeleteQueue chan string + queueLength int64 + + // lock for sync load swapped-messages from disk + loadSwapLock sync.Mutex + maxMessagesInRAM uint64 + lastStoredMsgID uint64 + lastMemMsgID uint64 + swappedToDisk bool + maybeLoadFromStorageCh chan struct{} + wg *sync.WaitGroup +} + +// NewQueue returns new instance of Queue +func NewQueue(name string, connID uint64, exclusive bool, autoDelete bool, durable bool, config config.Queue, msgStorageP interfaces.MsgStorage, msgStorageT interfaces.MsgStorage, autoDeleteQueue chan string) *Queue { + return &Queue{ + SafeQueue: *safequeue.NewSafeQueue(config.ShardSize), + name: name, + connID: connID, + exclusive: exclusive, + autoDelete: autoDelete, + durable: durable, + call: make(chan struct{}, 1), + maybeLoadFromStorageCh: make(chan struct{}, 1), + wasConsumed: false, + active: false, + shardSize: 1, + maxMessagesInRAM: 1500, + currentConsumer: 0, + autoDeleteQueue: autoDeleteQueue, + swappedToDisk: false, + wg: &sync.WaitGroup{}, + } + +} + +// Start starts base queue loop to send events to consumers +// Current consumer to handle message from queue selected by round robin +func (queue *Queue) Start() error { + queue.actLock.Lock() + defer queue.actLock.Unlock() + + if queue.active { + return nil + } + + queue.active = true + queue.wg.Add(1) + go func() { + defer queue.wg.Done() + for range queue.call { + func() { + queue.cmrLock.RLock() + defer queue.cmrLock.RUnlock() + cmrCount := len(queue.consumers) + for i := 0; i < cmrCount; i++ { + if !queue.active { + return + } + queue.currentConsumer = (queue.currentConsumer + 1) % cmrCount + cmr := queue.consumers[queue.currentConsumer] + if cmr.Consume() { + return + } + } + }() + } + }() + + queue.wg.Add(1) + go func() { + defer queue.wg.Done() + for range queue.maybeLoadFromStorageCh { + queue.mayBeLoadFromStorage() + } + }() + + return nil +} + +// Stop stops main queue loop +// After stop no one can send or receive messages from queue +func (queue *Queue) Stop() error { + queue.actLock.Lock() + defer queue.actLock.Unlock() + + queue.active = false + close(queue.maybeLoadFromStorageCh) + close(queue.call) + queue.wg.Wait() + return nil +} + +// GetName returns queue name +func (queue *Queue) GetName() string { + return queue.name +} + +// Push append message into queue tail and put it into message storage +// if queue is durable and message's persistent flag is true +func (queue *Queue) Push(message *amqp.Message) { + queue.actLock.Lock() + defer queue.actLock.Unlock() + + if !queue.active { + return + } + + atomic.AddInt64(&queue.queueLength, 1) + + message.GenerateSeq() + + persisted := false + if queue.durable && message.IsPersistent() { + persisted = true + } else { + if queue.SafeQueue.Length() > queue.maxMessagesInRAM || queue.swappedToDisk { + persisted = true + } + + if message.ConfirmMeta != nil { + message.ConfirmMeta.ActualConfirms++ + } + } + + if persisted && !queue.swappedToDisk && queue.SafeQueue.Length() > queue.maxMessagesInRAM { + queue.swappedToDisk = true + queue.lastStoredMsgID = message.ID + } + + if queue.SafeQueue.Length() <= queue.maxMessagesInRAM && !queue.swappedToDisk { + queue.SafeQueue.Push(message) + queue.lastMemMsgID = message.ID + } + + queue.callConsumers() +} + +// Pop returns message from queue head without QOS check +func (queue *Queue) Pop() *amqp.Message { + return queue.PopQos([]*qos.AmqpQos{}) +} + +// PopQos returns message from queue head with QOS check +func (queue *Queue) PopQos(qosList []*qos.AmqpQos) *amqp.Message { + queue.actLock.RLock() + if !queue.active { + queue.actLock.RUnlock() + return nil + } + queue.actLock.RUnlock() + + select { + case queue.maybeLoadFromStorageCh <- struct{}{}: + default: + } + + queue.SafeQueue.Lock() + var message *amqp.Message + if message = queue.SafeQueue.HeadItem(); message != nil { + allowed := true + for _, q := range qosList { + if !q.IsActive() { + continue + } + if !q.Inc(1, uint32(message.BodySize)) { + allowed = false + break + } + } + + if allowed { + queue.SafeQueue.DirtyPop() + atomic.AddInt64(&queue.queueLength, -1) + } else { + message = nil + } + } + queue.SafeQueue.Unlock() + + return message +} + +func (queue *Queue) mayBeLoadFromStorage() { + swappedToPersistent := true + swappedToTransient := true + + currentLength := queue.SafeQueue.Length() + needle := queue.maxMessagesInRAM - currentLength + + if currentLength >= queue.maxMessagesInRAM/2 || needle <= 0 || !queue.swappedToDisk { + return + } + + pMessages := make([]*amqp.Message, 0, needle) + tMessages := make([]*amqp.Message, 0, needle) + + lastMemMsgID := queue.lastMemMsgID + + var wg sync.WaitGroup + // 2 - search for transient and persistent + wg.Add(2) + + go func() { + wg.Done() + }() + + go func() { + wg.Done() + }() + + wg.Wait() + + sortedMessages := queue.mergeSortedMessageSlices(pMessages, tMessages) + sortedMessageslength := uint64(len(sortedMessages)) + + var pos uint64 + if sortedMessageslength <= needle { + pos = sortedMessageslength + } else { + pos = needle + } + + for _, message := range sortedMessages[0:pos] { + if message.ID == lastMemMsgID { + continue + } + queue.SafeQueue.Push(message) + queue.lastMemMsgID = message.ID + queue.lastStoredMsgID = message.ID + queue.callConsumers() + } + + queue.swappedToDisk = swappedToPersistent || swappedToTransient +} + +func (queue *Queue) mergeSortedMessageSlices(A, B []*amqp.Message) []*amqp.Message { + result := make([]*amqp.Message, len(A)+len(B)) + + idxA, idxB := 0, 0 + + for i := 0; i < len(result); i++ { + if idxA >= len(A) { + result[i] = B[idxB] + idxB++ + continue + } else if idxB >= len(B) { + result[i] = A[idxA] + idxA++ + continue + } + + if A[idxA].ID < B[idxB].ID { + result[i] = A[idxA] + idxA++ + } else { + result[i] = B[idxB] + idxB++ + } + } + + return result +} + +// AckMsg accept ack event for message +func (queue *Queue) AckMsg(message *amqp.Message) { + queue.actLock.RLock() + if !queue.active { + queue.actLock.RUnlock() + return + } + queue.actLock.RUnlock() + + if queue.durable && message.IsPersistent() { + // TODO handle error + } + +} + +// Requeue add message into queue head +func (queue *Queue) Requeue(message *amqp.Message) { + queue.actLock.RLock() + if !queue.active { + queue.actLock.RUnlock() + return + } + queue.actLock.RUnlock() + + message.DeliveryCount++ + queue.SafeQueue.PushHead(message) + if queue.durable && message.IsPersistent() { + // TODO handle error + } + + atomic.AddInt64(&queue.queueLength, 1) + + queue.callConsumers() +} + +// Purge clean queue and message storage for durable queues +func (queue *Queue) Purge() (length uint64) { + queue.SafeQueue.Lock() + defer queue.SafeQueue.Unlock() + length = uint64(atomic.LoadInt64(&queue.queueLength)) + queue.SafeQueue.DirtyPurge() + + if queue.durable { + } + + atomic.StoreInt64(&queue.queueLength, 0) + return +} + +// Delete cancel consumers and delete its messages from storage +func (queue *Queue) Delete(ifUnused bool, ifEmpty bool) (uint64, error) { + queue.actLock.Lock() + queue.cmrLock.Lock() + queue.SafeQueue.Lock() + defer queue.actLock.Unlock() + defer queue.cmrLock.Unlock() + defer queue.SafeQueue.Unlock() + + queue.active = false + + if ifUnused && len(queue.consumers) != 0 { + return 0, errors.New("queue has consumers") + } + + if ifEmpty && queue.SafeQueue.DirtyLength() != 0 { + return 0, errors.New("queue has messages") + } + + queue.cancelConsumers() + length := uint64(atomic.LoadInt64(&queue.queueLength)) + + if queue.durable { + } + + return length, nil +} + +// AddConsumer add consumer to consumer messages with exclusive check +func (queue *Queue) AddConsumer(consumer interfaces.Consumer, exclusive bool) error { + queue.cmrLock.Lock() + defer queue.cmrLock.Unlock() + + if !queue.active { + return fmt.Errorf("queue is not active") + } + queue.wasConsumed = true + + if len(queue.consumers) != 0 && (queue.consumeExcl || exclusive) { + return fmt.Errorf("queue is busy by %d consumers", len(queue.consumers)) + } + + if exclusive { + queue.consumeExcl = true + } + + queue.consumers = append(queue.consumers, consumer) + + queue.callConsumers() + return nil +} + +// RemoveConsumer remove consumer +// If it was last consumer and queue is auto-delete - queue will be removed +func (queue *Queue) RemoveConsumer(cTag string) { + queue.cmrLock.Lock() + defer queue.cmrLock.Unlock() + + for i, cmr := range queue.consumers { + if cmr.Tag() == cTag { + queue.consumers = append(queue.consumers[:i], queue.consumers[i+1:]...) + break + } + } + cmrCount := len(queue.consumers) + if cmrCount == 0 { + queue.currentConsumer = 0 + queue.consumeExcl = false + } else { + queue.currentConsumer = (queue.currentConsumer + 1) % cmrCount + } + + if cmrCount == 0 && queue.wasConsumed && queue.autoDelete { + queue.autoDeleteQueue <- queue.name + } +} + +// Send event to call next consumer, that it can receive next message +func (queue *Queue) callConsumers() { + if !queue.active { + return + } + select { + case queue.call <- struct{}{}: + default: + } +} + +func (queue *Queue) cancelConsumers() { + for _, cmr := range queue.consumers { + cmr.Cancel() + } +} + +// Length returns queue length +func (queue *Queue) Length() uint64 { + return uint64(atomic.LoadInt64(&queue.queueLength)) +} + +// ConsumersCount returns consumers count +func (queue *Queue) ConsumersCount() int { + queue.cmrLock.RLock() + defer queue.cmrLock.RUnlock() + return len(queue.consumers) +} + +// EqualWithErr returns is given queue equal to current +func (queue *Queue) EqualWithErr(qB *Queue) error { + errTemplate := "inequivalent arg '%s' for queue '%s': received '%t' but current is '%t'" + if queue.durable != qB.IsDurable() { + return fmt.Errorf(errTemplate, "durable", queue.name, qB.IsDurable(), queue.durable) + } + if queue.autoDelete != qB.autoDelete { + return fmt.Errorf(errTemplate, "autoDelete", queue.name, qB.autoDelete, queue.autoDelete) + } + if queue.exclusive != qB.IsExclusive() { + return fmt.Errorf(errTemplate, "exclusive", queue.name, qB.IsExclusive(), queue.exclusive) + } + return nil +} + +// Marshal returns raw representation of queue to store into storage +func (queue *Queue) Marshal(protoVersion string) (data []byte, err error) { + buf := bytes.NewBuffer(make([]byte, 0)) + if err = amqp.WriteShortstr(buf, queue.name); err != nil { + return nil, err + } + + var autoDelete byte + if queue.autoDelete { + autoDelete = 1 + } else { + autoDelete = 0 + } + + if err = amqp.WriteOctet(buf, autoDelete); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// Unmarshal returns queue from storage raw bytes data +func (queue *Queue) Unmarshal(data []byte, protoVersion string) (err error) { + buf := bytes.NewReader(data) + if queue.name, err = amqp.ReadShortstr(buf); err != nil { + return err + } + + var autoDelete byte + + if autoDelete, err = amqp.ReadOctet(buf); err != nil { + return err + } + queue.autoDelete = autoDelete > 0 + queue.durable = true + return +} + +// IsDurable returns is queue durable +func (queue *Queue) IsDurable() bool { + return queue.durable +} + +// IsExclusive returns is queue exclusive +func (queue *Queue) IsExclusive() bool { + return queue.exclusive +} + +// IsAutoDelete returns is queue should be deleted automatically +func (queue *Queue) IsAutoDelete() bool { + return queue.autoDelete +} + +// ConnID returns ID of connection that create this queue +func (queue *Queue) ConnID() uint64 { + return queue.connID +} + +// IsActive returns is queue's main loop is active +func (queue *Queue) IsActive() bool { + return queue.active +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/safequeue/safequeue.go b/pkg/outputs/amqp/_fixtures/garagemq/safequeue/safequeue.go new file mode 100644 index 000000000..31184aa6d --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/safequeue/safequeue.go @@ -0,0 +1,148 @@ +package safequeue + +import ( + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/amqp" + "sync" +) + +// We change item's type from {}interface to *amqp.Message, cause we know that we use safequeue only in AMQP context +// TODO Move safe queue into amqp package + +// SafeQueue represents simple FIFO queue +// TODO Is that implementation faster? test simple slice queue +type SafeQueue struct { + sync.RWMutex + shards [][]*amqp.Message + shardSize int + tailIdx int + tail []*amqp.Message + tailPos int + headIdx int + head []*amqp.Message + headPos int + length uint64 +} + +// NewSafeQueue returns new instance of queue +func NewSafeQueue(shardSize int) *SafeQueue { + queue := &SafeQueue{ + shardSize: shardSize, + shards: [][]*amqp.Message{make([]*amqp.Message, shardSize)}, + } + + queue.tailIdx = 0 + queue.tail = queue.shards[queue.tailIdx] + queue.headIdx = 0 + queue.head = queue.shards[queue.headIdx] + return queue +} + +// Push adds message into queue tail +func (queue *SafeQueue) Push(item *amqp.Message) { + queue.Lock() + defer queue.Unlock() + + queue.tail[queue.tailPos] = item + queue.tailPos++ + queue.length++ + + if queue.tailPos == queue.shardSize { + queue.tailPos = 0 + queue.tailIdx = len(queue.shards) + + buffer := make([][]*amqp.Message, len(queue.shards)+1) + buffer[queue.tailIdx] = make([]*amqp.Message, queue.shardSize) + copy(buffer, queue.shards) + + queue.shards = buffer + queue.tail = queue.shards[queue.tailIdx] + } +} + +// PushHead adds message into queue head +func (queue *SafeQueue) PushHead(item *amqp.Message) { + queue.Lock() + defer queue.Unlock() + + if queue.headPos == 0 { + buffer := make([][]*amqp.Message, len(queue.shards)+1) + copy(buffer[1:], queue.shards) + buffer[queue.headIdx] = make([]*amqp.Message, queue.shardSize) + + queue.shards = buffer + queue.tailIdx++ + queue.headPos = queue.shardSize + queue.tail = queue.shards[queue.tailIdx] + queue.head = queue.shards[queue.headIdx] + } + queue.length++ + queue.headPos-- + queue.head[queue.headPos] = item +} + +// Pop retrieves message from head +func (queue *SafeQueue) Pop() (item *amqp.Message) { + queue.Lock() + item = queue.DirtyPop() + queue.Unlock() + return +} + +// DirtyPop retrieves message from head +// This method is not thread safe +func (queue *SafeQueue) DirtyPop() (item *amqp.Message) { + item, queue.head[queue.headPos] = queue.head[queue.headPos], nil + if item == nil { + return item + } + queue.headPos++ + queue.length-- + if queue.headPos == queue.shardSize { + buffer := make([][]*amqp.Message, len(queue.shards)-1) + copy(buffer, queue.shards[queue.headIdx+1:]) + + queue.shards = buffer + + queue.headPos = 0 + queue.tailIdx-- + queue.head = queue.shards[queue.headIdx] + } + return +} + +// Length returns queue length +func (queue *SafeQueue) Length() uint64 { + queue.RLock() + defer queue.RUnlock() + return queue.length +} + +// DirtyLength returns queue length +// This method is not thread safe +func (queue *SafeQueue) DirtyLength() uint64 { + return queue.length +} + +// HeadItem returns current head message +// This method is not thread safe +func (queue *SafeQueue) HeadItem() (res *amqp.Message) { + return queue.head[queue.headPos] +} + +// DirtyPurge clear queue +// This method is not thread safe +func (queue *SafeQueue) DirtyPurge() { + queue.shards = [][]*amqp.Message{make([]*amqp.Message, queue.shardSize)} + queue.tailIdx = 0 + queue.tail = queue.shards[queue.tailIdx] + queue.headIdx = 0 + queue.head = queue.shards[queue.headIdx] + queue.length = 0 +} + +// Purge clear queue +func (queue *SafeQueue) Purge() { + queue.Lock() + defer queue.Unlock() + queue.DirtyPurge() +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/server/basicMethods.go b/pkg/outputs/amqp/_fixtures/garagemq/server/basicMethods.go new file mode 100644 index 000000000..418f94d52 --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/server/basicMethods.go @@ -0,0 +1,130 @@ +package server + +import ( + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/amqp" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/consumer" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/qos" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/queue" +) + +func (channel *Channel) basicRoute(method amqp.Method) *amqp.Error { + switch method := method.(type) { + case *amqp.BasicQos: + return channel.basicQos(method) + case *amqp.BasicPublish: + return channel.basicPublish(method) + case *amqp.BasicConsume: + return channel.basicConsume(method) + case *amqp.BasicAck: + return channel.basicAck(method) + case *amqp.BasicNack: + return channel.basicNack(method) + case *amqp.BasicReject: + return channel.basicReject(method) + case *amqp.BasicCancel: + return channel.basicCancel(method) + case *amqp.BasicGet: + return channel.basicGet(method) + } + + return amqp.NewConnectionError(amqp.NotImplemented, "unable to route basic method "+method.Name(), method.ClassIdentifier(), method.MethodIdentifier()) +} + +func (channel *Channel) basicQos(method *amqp.BasicQos) (err *amqp.Error) { + channel.updateQos(method.PrefetchCount, method.PrefetchSize, method.Global) + channel.SendMethod(&amqp.BasicQosOk{}) + + return nil +} + +func (channel *Channel) basicAck(method *amqp.BasicAck) (err *amqp.Error) { + return channel.handleAck(method) +} + +func (channel *Channel) basicNack(method *amqp.BasicNack) (err *amqp.Error) { + return channel.handleReject(method.DeliveryTag, method.Multiple, method.Requeue, method) +} + +func (channel *Channel) basicReject(method *amqp.BasicReject) (err *amqp.Error) { + return channel.handleReject(method.DeliveryTag, false, method.Requeue, method) +} + +func (channel *Channel) basicPublish(method *amqp.BasicPublish) (err *amqp.Error) { + if method.Immediate { + return amqp.NewChannelError(amqp.NotImplemented, "Immediate = true", method.ClassIdentifier(), method.MethodIdentifier()) + } + + if _, err = channel.getExchangeWithError(method.Exchange, method); err != nil { + return err + } + + channel.currentMessage = amqp.NewMessage(method) + if channel.confirmMode { + channel.currentMessage.ConfirmMeta = &amqp.ConfirmMeta{ + ChanID: channel.id, + ConnID: channel.conn.id, + DeliveryTag: channel.nextConfirmDeliveryTag(), + } + } + return nil +} + +func (channel *Channel) basicConsume(method *amqp.BasicConsume) (err *amqp.Error) { + var cmr *consumer.Consumer + if cmr, err = channel.addConsumer(method); err != nil { + return err + } + + if !method.NoWait { + channel.SendMethod(&amqp.BasicConsumeOk{ConsumerTag: cmr.Tag()}) + } + + cmr.Start() + + return nil +} + +func (channel *Channel) basicCancel(method *amqp.BasicCancel) (err *amqp.Error) { + if _, ok := channel.consumers[method.ConsumerTag]; !ok { + return amqp.NewChannelError(amqp.NotFound, "Consumer not found", method.ClassIdentifier(), method.MethodIdentifier()) + } + channel.removeConsumer(method.ConsumerTag) + channel.SendMethod(&amqp.BasicCancelOk{ConsumerTag: method.ConsumerTag}) + return nil +} + +func (channel *Channel) basicGet(method *amqp.BasicGet) (err *amqp.Error) { + var qu *queue.Queue + var message *amqp.Message + if qu, err = channel.getQueueWithError(method.Queue, method); err != nil { + return err + } + + if method.NoAck { + message = qu.Pop() + } else { + message = qu.PopQos([]*qos.AmqpQos{channel.qos, channel.conn.qos}) + } + + // how to handle if queue is not empty, but qos triggered and message is nil + if message == nil { + channel.SendMethod(&amqp.BasicGetEmpty{}) + return nil + } + + dTag := channel.NextDeliveryTag() + if !method.NoAck { + channel.AddUnackedMessage(dTag, "", qu.GetName(), message) + } else { + } + + channel.SendContent(&amqp.BasicGetOk{ + DeliveryTag: dTag, + Redelivered: false, + Exchange: message.Exchange, + RoutingKey: message.RoutingKey, + MessageCount: 1, + }, message) + + return nil +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/server/channel.go b/pkg/outputs/amqp/_fixtures/garagemq/server/channel.go new file mode 100644 index 000000000..f8ebf6fff --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/server/channel.go @@ -0,0 +1,638 @@ +package server + +import ( + "bytes" + "fmt" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/amqp" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/consumer" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/exchange" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/pool" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/qos" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/queue" + "sort" + "sync" + "sync/atomic" + "time" + + "github.com/sirupsen/logrus" + log "github.com/sirupsen/logrus" +) + +const ( + channelNew = iota + channelOpen + channelClosing + channelClosed + channelDelete +) + +// Channel is an implementation of the AMQP-channel entity +// Within a single socket connection, there can be multiple +// independent threads of control, called "channels" +type Channel struct { + active bool + confirmMode bool + id uint16 + conn *Connection + server *Server + incoming chan *amqp.Frame + outgoing chan *amqp.Frame + logger *log.Entry + status int + protoVersion string + currentMessage *amqp.Message + cmrLock sync.RWMutex + consumers map[string]*consumer.Consumer + qos *qos.AmqpQos + consumerQos *qos.AmqpQos + deliveryTag uint64 + confirmDeliveryTag uint64 + confirmLock sync.Mutex + confirmQueue []*amqp.ConfirmMeta + ackLock sync.Mutex + ackStore map[uint64]*UnackedMessage + + bufferPool *pool.BufferPool + + closeCh chan bool +} + +// UnackedMessage represents the unacknowledged message +type UnackedMessage struct { + cTag string + msg *amqp.Message + queue string +} + +// NewChannel returns new instance of Channel +func NewChannel(id uint16, conn *Connection) *Channel { + channel := &Channel{ + active: true, + id: id, + conn: conn, + server: conn.server, + // for incoming channel much capacity is good for performance + // but it is difficult to implement processing already queued frames on shutdown or connection close + incoming: make(chan *amqp.Frame, 128), + outgoing: conn.outgoing, + status: channelNew, + protoVersion: conn.server.protoVersion, + consumers: make(map[string]*consumer.Consumer), + qos: qos.NewAmqpQos(0, 0), + consumerQos: qos.NewAmqpQos(0, 0), + ackStore: make(map[uint64]*UnackedMessage), + confirmQueue: make([]*amqp.ConfirmMeta, 0), + closeCh: make(chan bool), + bufferPool: pool.NewBufferPool(0), + } + + channel.logger = log.WithFields(log.Fields{ + "connectionId": conn.id, + "channelId": id, + }) + + return channel +} + +func (channel *Channel) start() { + if channel.id == 0 { + go channel.connectionStart() + } + + go channel.handleIncoming() +} + +func (channel *Channel) handleIncoming() { + buffer := bytes.NewReader([]byte{}) + + // TODO + // @spec-note + // After sending channel.close, any received methods except Close and Close­OK MUST be discarded. + // The response to receiving a Close after sending Close must be to send Close­Ok. + for { + select { + case <-channel.closeCh: + channel.close() + return + case frame := <-channel.incoming: + if frame == nil { + // channel.incoming closed by connection + return + } + + switch frame.Type { + case amqp.FrameMethod: + buffer.Reset(frame.Payload) + method, err := amqp.ReadMethod(buffer, channel.protoVersion) + channel.logger.Debug("Incoming method <- " + method.Name()) + if err != nil { + channel.logger.WithError(err).Error("Error on handling frame") + channel.sendError(amqp.NewConnectionError(amqp.FrameError, err.Error(), 0, 0)) + } + + if err := channel.handleMethod(method); err != nil { + channel.sendError(err) + } + case amqp.FrameHeader: + if err := channel.handleContentHeader(frame); err != nil { + channel.sendError(err) + } + case amqp.FrameBody: + if err := channel.handleContentBody(frame); err != nil { + channel.sendError(err) + } + } + } + } +} + +func (channel *Channel) sendError(err *amqp.Error) { + channel.logger.Error(err) + switch err.ErrorType { + case amqp.ErrorOnChannel: + channel.status = channelClosing + channel.SendMethod(&amqp.ChannelClose{ + ReplyCode: err.ReplyCode, + ReplyText: err.ReplyText, + ClassID: err.ClassID, + MethodID: err.MethodID, + }) + case amqp.ErrorOnConnection: + ch := channel.conn.getChannel(0) + if ch != nil { + ch.SendMethod(&amqp.ConnectionClose{ + ReplyCode: err.ReplyCode, + ReplyText: err.ReplyText, + ClassID: err.ClassID, + MethodID: err.MethodID, + }) + } + } +} + +func (channel *Channel) handleMethod(method amqp.Method) *amqp.Error { + switch method.ClassIdentifier() { + case amqp.ClassConnection: + return channel.connectionRoute(method) + case amqp.ClassChannel: + return channel.channelRoute(method) + case amqp.ClassBasic: + return channel.basicRoute(method) + case amqp.ClassExchange: + return channel.exchangeRoute(method) + case amqp.ClassQueue: + return channel.queueRoute(method) + case amqp.ClassConfirm: + return channel.confirmRoute(method) + } + + return nil +} + +func (channel *Channel) handleContentHeader(headerFrame *amqp.Frame) *amqp.Error { + reader := bytes.NewReader(headerFrame.Payload) + var err error + if channel.currentMessage == nil { + return amqp.NewConnectionError(amqp.FrameError, "unexpected content header frame", 0, 0) + } + + if channel.currentMessage.Header != nil { + return amqp.NewConnectionError(amqp.FrameError, "unexpected content header frame - header already exists", 0, 0) + } + + if channel.currentMessage.Header, err = amqp.ReadContentHeader(reader, channel.protoVersion); err != nil { + return amqp.NewConnectionError(amqp.FrameError, "error on parsing content header frame", 0, 0) + } + + return nil +} + +func (channel *Channel) handleContentBody(bodyFrame *amqp.Frame) *amqp.Error { + if channel.currentMessage == nil { + return amqp.NewConnectionError(amqp.FrameError, "unexpected content body frame", 0, 0) + } + + if channel.currentMessage.Header == nil { + return amqp.NewConnectionError(amqp.FrameError, "unexpected content body frame - no header yet", 0, 0) + } + + channel.currentMessage.Append(bodyFrame) + + if channel.currentMessage.BodySize < channel.currentMessage.Header.BodySize { + return nil + } + + vhost := channel.conn.GetVirtualHost() + message := channel.currentMessage + ex := vhost.GetExchange(message.Exchange) + if ex == nil { + channel.SendContent( + &amqp.BasicReturn{ReplyCode: amqp.NoRoute, ReplyText: "No route", Exchange: message.Exchange, RoutingKey: message.RoutingKey}, + message, + ) + + channel.addConfirm(message.ConfirmMeta) + + return nil + } + matchedQueues := ex.GetMatchedQueues(message) + + if len(matchedQueues) == 0 { + if message.Mandatory { + channel.SendContent( + &amqp.BasicReturn{ReplyCode: amqp.NoRoute, ReplyText: "No route", Exchange: message.Exchange, RoutingKey: message.RoutingKey}, + message, + ) + } + + channel.addConfirm(message.ConfirmMeta) + + return nil + } + + if channel.confirmMode { + message.ConfirmMeta.ExpectedConfirms = len(matchedQueues) + } + + for queueName := range matchedQueues { + qu := channel.conn.GetVirtualHost().GetQueue(queueName) + if qu == nil { + if message.Mandatory { + channel.SendContent( + &amqp.BasicReturn{ReplyCode: amqp.NoRoute, ReplyText: "No route", Exchange: message.Exchange, RoutingKey: message.RoutingKey}, + message, + ) + } + + channel.addConfirm(message.ConfirmMeta) + + return nil + } + + qu.Push(message) + + if channel.confirmMode && message.ConfirmMeta.CanConfirm() && !message.IsPersistent() { + channel.addConfirm(message.ConfirmMeta) + } + } + return nil +} + +// SendMethod send method to client +// Method will be packed into frame and send to outgoing channel +func (channel *Channel) SendMethod(method amqp.Method) { + var rawMethod = channel.bufferPool.Get() + if err := amqp.WriteMethod(rawMethod, method, channel.server.protoVersion); err != nil { + logrus.WithError(err).Error("Error") + } + + closeAfter := method.ClassIdentifier() == amqp.ClassConnection && method.MethodIdentifier() == amqp.MethodConnectionCloseOk + + channel.logger.Debug("Outgoing -> " + method.Name()) + + payload := make([]byte, rawMethod.Len()) + copy(payload, rawMethod.Bytes()) + channel.bufferPool.Put(rawMethod) + + channel.sendOutgoing(&amqp.Frame{Type: byte(amqp.FrameMethod), ChannelID: channel.id, Payload: payload, CloseAfter: closeAfter, Sync: method.Sync()}) +} + +func (channel *Channel) sendOutgoing(frame *amqp.Frame) { + select { + case <-channel.conn.ctx.Done(): + if channel.id == 0 { + close(channel.outgoing) + } + case channel.outgoing <- frame: + } +} + +// SendContent send message to consumers or returns to publishers +func (channel *Channel) SendContent(method amqp.Method, message *amqp.Message) { + channel.SendMethod(method) + + var rawHeader = channel.bufferPool.Get() + amqp.WriteContentHeader(rawHeader, message.Header, channel.server.protoVersion) + + payload := make([]byte, rawHeader.Len()) + copy(payload, rawHeader.Bytes()) + channel.bufferPool.Put(rawHeader) + + channel.sendOutgoing(&amqp.Frame{Type: byte(amqp.FrameHeader), ChannelID: channel.id, Payload: payload, CloseAfter: false}) + + for _, payload := range message.Body { + payload.ChannelID = channel.id + channel.sendOutgoing(payload) + } + + switch method.(type) { + case *amqp.BasicDeliver: + } +} + +func (channel *Channel) addConfirm(meta *amqp.ConfirmMeta) { + if !channel.confirmMode { + return + } + channel.confirmLock.Lock() + defer channel.confirmLock.Unlock() + + if channel.status == channelClosed { + return + } + channel.confirmQueue = append(channel.confirmQueue, meta) +} + +func (channel *Channel) sendConfirms() { + tick := time.Tick(20 * time.Millisecond) + for range tick { + if channel.status == channelClosed { + return + } + channel.confirmLock.Lock() + currentConfirms := channel.confirmQueue + channel.confirmQueue = make([]*amqp.ConfirmMeta, 0) + channel.confirmLock.Unlock() + + for _, confirm := range currentConfirms { + channel.SendMethod(&amqp.BasicAck{ + DeliveryTag: confirm.DeliveryTag, + Multiple: false, + }) + } + } +} + +func (channel *Channel) addConsumer(method *amqp.BasicConsume) (cmr *consumer.Consumer, err *amqp.Error) { + channel.cmrLock.Lock() + defer channel.cmrLock.Unlock() + + var qu *queue.Queue + if qu, err = channel.getQueueWithError(method.Queue, method); err != nil { + return nil, err + } + + var consumerQos []*qos.AmqpQos + if channel.server.protoVersion == amqp.Proto091 { + consumerQos = []*qos.AmqpQos{channel.qos, channel.conn.qos} + } else { + cmrQos := channel.consumerQos.Copy() + consumerQos = []*qos.AmqpQos{channel.qos, cmrQos} + } + + cmr = consumer.NewConsumer(method.Queue, method.ConsumerTag, method.NoAck, channel, qu, consumerQos) + if _, ok := channel.consumers[cmr.Tag()]; ok { + return nil, amqp.NewChannelError(amqp.NotAllowed, fmt.Sprintf("Consumer with tag '%s' already exists", cmr.Tag()), method.ClassIdentifier(), method.MethodIdentifier()) + } + + if quErr := qu.AddConsumer(cmr, method.Exclusive); quErr != nil { + return nil, amqp.NewChannelError(amqp.AccessRefused, quErr.Error(), method.ClassIdentifier(), method.MethodIdentifier()) + } + channel.consumers[cmr.Tag()] = cmr + + return cmr, nil +} + +func (channel *Channel) removeConsumer(cTag string) { + channel.cmrLock.Lock() + defer channel.cmrLock.Unlock() + if cmr, ok := channel.consumers[cTag]; ok { + cmr.Stop() + delete(channel.consumers, cmr.Tag()) + } +} + +func (channel *Channel) close() { + channel.cmrLock.Lock() + for _, cmr := range channel.consumers { + cmr.Stop() + delete(channel.consumers, cmr.Tag()) + channel.logger.WithFields(log.Fields{ + "consumerTag": cmr.Tag(), + }).Info("Consumer stopped") + } + channel.cmrLock.Unlock() + if channel.id > 0 { + channel.handleReject(0, true, true, &amqp.BasicNack{}) + } + channel.status = channelClosed + channel.logger.Info("Channel closed") +} + +func (channel *Channel) delete() { + channel.closeCh <- true + channel.status = channelDelete +} + +func (channel *Channel) updateQos(prefetchCount uint16, prefetchSize uint32, global bool) { + if channel.server.protoVersion == amqp.Proto091 { + if global { + channel.conn.qos.Update(prefetchCount, prefetchSize) + } else { + channel.qos.Update(prefetchCount, prefetchSize) + } + } else { + if global { + channel.qos.Update(prefetchCount, prefetchSize) + } else { + channel.consumerQos.Update(prefetchCount, prefetchSize) + } + } +} + +func (channel *Channel) GetQos() *qos.AmqpQos { + return channel.qos +} + +// NextDeliveryTag returns next delivery tag for current channel +func (channel *Channel) NextDeliveryTag() uint64 { + return atomic.AddUint64(&channel.deliveryTag, 1) +} + +func (channel *Channel) nextConfirmDeliveryTag() uint64 { + return atomic.AddUint64(&channel.confirmDeliveryTag, 1) +} + +// AddUnackedMessage add message to unacked queue +func (channel *Channel) AddUnackedMessage(dTag uint64, cTag string, queue string, message *amqp.Message) { + channel.ackLock.Lock() + defer channel.ackLock.Unlock() + channel.ackStore[dTag] = &UnackedMessage{ + cTag: cTag, + msg: message, + queue: queue, + } +} + +func (channel *Channel) handleAck(method *amqp.BasicAck) *amqp.Error { + channel.ackLock.Lock() + defer channel.ackLock.Unlock() + var uMsg *UnackedMessage + var msgFound bool + + if method.Multiple { + for tag, uMsg := range channel.ackStore { + if method.DeliveryTag == 0 || tag <= method.DeliveryTag { + channel.ackMsg(uMsg, tag) + } + } + + return nil + } + + if uMsg, msgFound = channel.ackStore[method.DeliveryTag]; !msgFound { + return amqp.NewChannelError(amqp.PreconditionFailed, fmt.Sprintf("Delivery tag [%d] not found", method.DeliveryTag), method.ClassIdentifier(), method.MethodIdentifier()) + } + + channel.ackMsg(uMsg, method.DeliveryTag) + + return nil +} + +func (channel *Channel) ackMsg(unackedMessage *UnackedMessage, deliveryTag uint64) { + delete(channel.ackStore, deliveryTag) + q := channel.conn.GetVirtualHost().GetQueue(unackedMessage.queue) + if q != nil { + q.AckMsg(unackedMessage.msg) + } + + channel.decQosAndConsumerNext(unackedMessage) +} + +func (channel *Channel) handleReject(deliveryTag uint64, multiple bool, requeue bool, method amqp.Method) *amqp.Error { + channel.ackLock.Lock() + defer channel.ackLock.Unlock() + var uMsg *UnackedMessage + var msgFound bool + + if multiple { + deliveryTags := make([]uint64, 0) + for dTag := range channel.ackStore { + deliveryTags = append(deliveryTags, dTag) + } + sort.Slice( + deliveryTags, + func(i, j int) bool { + return deliveryTags[i] > deliveryTags[j] + }, + ) + for _, tag := range deliveryTags { + if deliveryTag == 0 || tag <= deliveryTag { + channel.rejectMsg(channel.ackStore[tag], tag, requeue) + } + } + + return nil + } + + if uMsg, msgFound = channel.ackStore[deliveryTag]; !msgFound { + return amqp.NewChannelError(amqp.PreconditionFailed, fmt.Sprintf("Delivery tag [%d] not found", deliveryTag), method.ClassIdentifier(), method.MethodIdentifier()) + } + + channel.rejectMsg(uMsg, deliveryTag, requeue) + + return nil +} + +func (channel *Channel) rejectMsg(unackedMessage *UnackedMessage, deliveryTag uint64, requeue bool) { + delete(channel.ackStore, deliveryTag) + qu := channel.conn.GetVirtualHost().GetQueue(unackedMessage.queue) + + if qu != nil { + if requeue { + qu.Requeue(unackedMessage.msg) + } else { + qu.AckMsg(unackedMessage.msg) + } + } else { + // TODO When a queue is deleted any pending messages are sent to a dead­letter + } + + channel.decQosAndConsumerNext(unackedMessage) +} + +func (channel *Channel) decQosAndConsumerNext(unackedMessage *UnackedMessage) { + channel.cmrLock.RLock() + if cmr, ok := channel.consumers[unackedMessage.cTag]; ok { + cmr.Consume() + + for _, amqpQos := range cmr.Qos() { + amqpQos.Dec(1, uint32(unackedMessage.msg.BodySize)) + } + } else { + channel.qos.Dec(1, uint32(unackedMessage.msg.BodySize)) + channel.conn.qos.Dec(1, uint32(unackedMessage.msg.BodySize)) + } + channel.cmrLock.RUnlock() +} + +func (channel *Channel) getExchangeWithError(exchangeName string, method amqp.Method) (ex *exchange.Exchange, err *amqp.Error) { + ex = channel.conn.GetVirtualHost().GetExchange(exchangeName) + if ex == nil { + return nil, amqp.NewChannelError( + amqp.NotFound, + fmt.Sprintf("exchange '%s' not found", exchangeName), + method.ClassIdentifier(), + method.MethodIdentifier(), + ) + } + return ex, nil +} + +func (channel *Channel) getQueueWithError(queueName string, method amqp.Method) (queue *queue.Queue, err *amqp.Error) { + qu := channel.conn.GetVirtualHost().GetQueue(queueName) + if qu == nil || !qu.IsActive() { + return nil, amqp.NewChannelError( + amqp.NotFound, + fmt.Sprintf("queue '%s' not found", queueName), + method.ClassIdentifier(), + method.MethodIdentifier(), + ) + } + return qu, nil +} + +func (channel *Channel) checkQueueLockWithError(qu *queue.Queue, method amqp.Method) *amqp.Error { + if qu == nil { + return nil + } + if qu.IsExclusive() && qu.ConnID() != channel.conn.id { + return amqp.NewChannelError( + amqp.ResourceLocked, + fmt.Sprintf("queue '%s' is locked to another connection", qu.GetName()), + method.ClassIdentifier(), + method.MethodIdentifier(), + ) + } + + return nil +} + +func (channel *Channel) isActive() bool { + return channel.active +} + +func (channel *Channel) changeFlow(active bool) { + if channel.active == active { + return + } + channel.active = active + + channel.cmrLock.RLock() + if channel.active { + for _, cmr := range channel.consumers { + cmr.UnPause() + cmr.Consume() + } + } else { + for _, cmr := range channel.consumers { + cmr.Pause() + } + } + channel.cmrLock.RUnlock() +} + +// GetConsumersCount returns consumers count on channel +func (channel *Channel) GetConsumersCount() int { + return len(channel.consumers) +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/server/channelMethods.go b/pkg/outputs/amqp/_fixtures/garagemq/server/channelMethods.go new file mode 100644 index 000000000..86df8c798 --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/server/channelMethods.go @@ -0,0 +1,51 @@ +package server + +import ( + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/amqp" +) + +func (channel *Channel) channelRoute(method amqp.Method) *amqp.Error { + switch method := method.(type) { + case *amqp.ChannelOpen: + return channel.channelOpen(method) + case *amqp.ChannelClose: + return channel.channelClose(method) + case *amqp.ChannelCloseOk: + return channel.channelCloseOk(method) + case *amqp.ChannelFlow: + return channel.channelFlow(method) + } + + return amqp.NewConnectionError(amqp.NotImplemented, "unable to route channel method "+method.Name(), method.ClassIdentifier(), method.MethodIdentifier()) +} + +func (channel *Channel) channelOpen(method *amqp.ChannelOpen) (err *amqp.Error) { + // @spec-note + // The client MUST NOT use this method on an already­opened channel. + if channel.status == channelOpen { + return amqp.NewConnectionError(amqp.ChannelError, "channel already open", method.ClassIdentifier(), method.MethodIdentifier()) + } + + channel.SendMethod(&amqp.ChannelOpenOk{}) + channel.status = channelOpen + + return nil +} + +func (channel *Channel) channelClose(method *amqp.ChannelClose) (err *amqp.Error) { + channel.status = channelClosed + channel.SendMethod(&amqp.ChannelCloseOk{}) + channel.close() + return nil +} + +func (channel *Channel) channelCloseOk(method *amqp.ChannelCloseOk) (err *amqp.Error) { + channel.status = channelClosed + return nil +} + +func (channel *Channel) channelFlow(method *amqp.ChannelFlow) (err *amqp.Error) { + channel.changeFlow(method.Active) + channel.SendMethod(&amqp.ChannelFlowOk{Active: method.Active}) + return nil +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/server/confirmMethods.go b/pkg/outputs/amqp/_fixtures/garagemq/server/confirmMethods.go new file mode 100644 index 000000000..73af1a314 --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/server/confirmMethods.go @@ -0,0 +1,23 @@ +package server + +import ( + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/amqp" +) + +func (channel *Channel) confirmRoute(method amqp.Method) *amqp.Error { + switch method := method.(type) { + case *amqp.ConfirmSelect: + return channel.confirmSelect(method) + } + + return amqp.NewConnectionError(amqp.NotImplemented, "unable to route channel method "+method.Name(), method.ClassIdentifier(), method.MethodIdentifier()) +} + +func (channel *Channel) confirmSelect(method *amqp.ConfirmSelect) (err *amqp.Error) { + channel.confirmMode = true + go channel.sendConfirms() + if !method.Nowait { + channel.SendMethod(&amqp.ConfirmSelectOk{}) + } + return nil +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/server/connection.go b/pkg/outputs/amqp/_fixtures/garagemq/server/connection.go new file mode 100644 index 000000000..4b92bf0d4 --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/server/connection.go @@ -0,0 +1,409 @@ +package server + +import ( + "bufio" + "bytes" + "context" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/amqp" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/qos" + "net" + "sort" + "strings" + "sync" + "sync/atomic" + "time" + + log "github.com/sirupsen/logrus" +) + +// connection status list +const ( + ConnStart = iota + ConnStartOK + ConnSecure + ConnSecureOK + ConnTune + ConnTuneOK + ConnOpen + ConnOpenOK + ConnCloseOK + ConnClosed +) + +// From https://github.com/rabbitmq/rabbitmq-common/blob/master/src/rabbit_writer.erl +// When the amount of protocol method data buffered exceeds +// this threshold, a socket flush is performed. +// +// This magic number is the tcp-over-ethernet MSS (1460) minus the +// minimum size of a AMQP 0-9-1 basic.deliver method frame (24) plus basic +// content header (22). The idea is that we want to flush just before +// exceeding the MSS. +const flushThreshold = 1414 + +// Connection represents AMQP-connection +type Connection struct { + id uint64 + server *Server + netConn *net.TCPConn + logger *log.Entry + channelsLock sync.RWMutex + channels map[uint16]*Channel + outgoing chan *amqp.Frame + clientProperties *amqp.Table + maxChannels uint16 + maxFrameSize uint32 + statusLock sync.RWMutex + status int + qos *qos.AmqpQos + virtualHost *VirtualHost + vhostName string + closeCh chan bool + userName string + + wg *sync.WaitGroup + ctx context.Context + cancelCtx context.CancelFunc + + heartbeatInterval uint16 + heartbeatTimeout uint16 + heartbeatTimer *time.Ticker + + lastOutgoingTS chan time.Time +} + +// NewConnection returns new instance of amqp Connection +func NewConnection(server *Server, netConn *net.TCPConn) (connection *Connection) { + connection = &Connection{ + id: atomic.AddUint64(&server.connSeq, 1), + server: server, + netConn: netConn, + channels: make(map[uint16]*Channel), + outgoing: make(chan *amqp.Frame, 128), + maxChannels: server.config.Connection.ChannelsMax, + maxFrameSize: server.config.Connection.FrameMaxSize, + qos: qos.NewAmqpQos(0, 0), + closeCh: make(chan bool, 2), + wg: &sync.WaitGroup{}, + lastOutgoingTS: make(chan time.Time), + heartbeatInterval: 10, + } + + connection.logger = log.WithFields(log.Fields{ + "connectionId": connection.id, + }) + + return +} + +func (conn *Connection) close() { + conn.statusLock.Lock() + if conn.status == ConnClosed { + conn.statusLock.Unlock() + return + } + + if conn.heartbeatTimer != nil { + conn.heartbeatTimer.Stop() + } + + conn.status = ConnClosed + conn.statusLock.Unlock() + + // @todo should we chech for errors here? And what should we do if error occur + _ = conn.netConn.Close() + + if conn.cancelCtx != nil { + conn.cancelCtx() + } + + conn.wg.Wait() + + // channel0 should we be closed at the end + channelIds := make([]int, 0) + conn.channelsLock.Lock() + for chID := range conn.channels { + channelIds = append(channelIds, int(chID)) + } + sort.Sort(sort.Reverse(sort.IntSlice(channelIds))) + for _, chID := range channelIds { + channel := conn.channels[uint16(chID)] + channel.delete() + delete(conn.channels, uint16(chID)) + } + conn.channelsLock.Unlock() + conn.clearQueues() + + conn.logger.WithFields(log.Fields{ + "vhost": conn.vhostName, + "from": conn.netConn.RemoteAddr(), + }).Info("Connection closed") + conn.server.removeConnection(conn.id) + + conn.closeCh <- true +} + +func (conn *Connection) getChannel(id uint16) *Channel { + conn.channelsLock.Lock() + channel := conn.channels[id] + conn.channelsLock.Unlock() + return channel +} + +func (conn *Connection) safeClose(wg *sync.WaitGroup) { + defer wg.Done() + + ch := conn.getChannel(0) + if ch == nil { + return + } + ch.SendMethod(&amqp.ConnectionClose{ + ReplyCode: amqp.ConnectionForced, + ReplyText: "Server shutdown", + ClassID: 0, + MethodID: 0, + }) + + // let clients proper handle connection closing in 10 sec + timeOut := time.After(10 * time.Second) + + select { + case <-timeOut: + conn.close() + return + case <-conn.closeCh: + return + } +} + +func (conn *Connection) clearQueues() { + virtualHost := conn.GetVirtualHost() + if virtualHost == nil { + // it is possible when conn close before open, for example login failure + return + } + for _, queue := range virtualHost.GetQueues() { + if queue.IsExclusive() && queue.ConnID() == conn.id { + virtualHost.DeleteQueue(queue.GetName(), false, false) + } + } +} + +func (conn *Connection) handleConnection() { + buf := make([]byte, 8) + _, err := conn.netConn.Read(buf) + if err != nil { + conn.logger.WithError(err).WithFields(log.Fields{ + "read buffer": buf, + }).Error("Error on read protocol header") + conn.close() + return + } + + // @spec-note + // If the server cannot support the protocol specified in the protocol header, + // it MUST respond with a valid protocol header and then close the socket connection. + // The client MUST start a new connection by sending a protocol header + if !bytes.Equal(buf, amqp.AmqpHeader) { + conn.logger.WithFields(log.Fields{ + "given": buf, + "supported": amqp.AmqpHeader, + }).Warn("Unsupported protocol") + _, _ = conn.netConn.Write(amqp.AmqpHeader) + conn.close() + return + } + + conn.ctx, conn.cancelCtx = context.WithCancel(context.Background()) + + channel := NewChannel(0, conn) + conn.channelsLock.Lock() + conn.channels[channel.id] = channel + conn.channelsLock.Unlock() + + channel.start() + conn.wg.Add(1) + go conn.handleOutgoing() + conn.wg.Add(1) + go conn.handleIncoming() +} + +func (conn *Connection) handleOutgoing() { + defer func() { + close(conn.lastOutgoingTS) + conn.wg.Done() + conn.close() + }() + + var err error + buffer := bufio.NewWriterSize(conn.netConn, 128<<10) + for { + select { + case <-conn.ctx.Done(): + return + case frame := <-conn.outgoing: + if frame == nil { + return + } + + if err = amqp.WriteFrame(buffer, frame); err != nil && !conn.isClosedError(err) { + conn.logger.WithError(err).Warn("writing frame") + return + } + + if frame.CloseAfter { + if err = buffer.Flush(); err != nil && !conn.isClosedError(err) { + conn.logger.WithError(err).Warn("writing frame") + } + return + } + + if frame.Sync { + if err = buffer.Flush(); err != nil && !conn.isClosedError(err) { + conn.logger.WithError(err).Warn("writing frame") + return + } + } else { + if err = conn.mayBeFlushBuffer(buffer); err != nil && !conn.isClosedError(err) { + conn.logger.WithError(err).Warn("writing frame") + return + } + } + + select { + case conn.lastOutgoingTS <- time.Now(): + default: + } + } + } +} + +func (conn *Connection) mayBeFlushBuffer(buffer *bufio.Writer) (err error) { + if buffer.Buffered() >= flushThreshold { + if err = buffer.Flush(); err != nil { + return err + } + } + + if len(conn.outgoing) == 0 { + // outgoing channel is buffered and we can check is here more messages for store into buffer + // if nothing to store into buffer - we flush + if err = buffer.Flush(); err != nil { + return err + } + } + return +} + +func (conn *Connection) handleIncoming() { + defer func() { + conn.wg.Done() + conn.close() + }() + + buffer := bufio.NewReaderSize(conn.netConn, 128<<10) + + for { + // TODO + // @spec-note + // After sending connection.close , any received methods except Close and Close­OK MUST be discarded. + // The response to receiving a Close after sending Close must be to send Close­Ok. + frame, err := amqp.ReadFrame(buffer) + if err != nil { + if err.Error() != "EOF" && !conn.isClosedError(err) { + conn.logger.WithError(err).Warn("reading frame") + } + return + } + + if conn.status < ConnOpen && frame.ChannelID != 0 { + conn.logger.WithError(err).Error("Frame not allowed for unopened connection") + return + } + + conn.channelsLock.RLock() + channel, ok := conn.channels[frame.ChannelID] + conn.channelsLock.RUnlock() + + if !ok { + channel = NewChannel(frame.ChannelID, conn) + + conn.channelsLock.Lock() + conn.channels[frame.ChannelID] = channel + conn.channelsLock.Unlock() + + channel.start() + } + + if conn.heartbeatTimeout > 0 { + if err = conn.netConn.SetReadDeadline(time.Now().Add(time.Duration(conn.heartbeatTimeout) * time.Second)); err != nil { + conn.logger.WithError(err).Warn("reading frame") + return + } + } + + if frame.Type == amqp.FrameHeartbeat && frame.ChannelID != 0 { + return + } + + select { + case <-conn.ctx.Done(): + close(channel.incoming) + return + case channel.incoming <- frame: + } + } +} + +func (conn *Connection) heartBeater() { + interval := time.Duration(conn.heartbeatInterval) * time.Second + conn.heartbeatTimer = time.NewTicker(interval) + + var ( + ok bool + lastTs = time.Now() + ) + + heartbeatFrame := &amqp.Frame{Type: byte(amqp.FrameHeartbeat), ChannelID: 0, Payload: []byte{}, CloseAfter: false, Sync: true} + + go func() { + for { + select { + case lastTs, ok = <-conn.lastOutgoingTS: + if !ok { + return + } + } + } + }() + + for tickTime := range conn.heartbeatTimer.C { + if tickTime.Sub(lastTs) >= interval-time.Second { + conn.outgoing <- heartbeatFrame + } + } +} + +func (conn *Connection) isClosedError(err error) bool { + // See: https://github.com/golang/go/issues/4373 + return err != nil && strings.Contains(err.Error(), "use of closed network connection") +} + +func (conn *Connection) GetVirtualHost() *VirtualHost { + return conn.virtualHost +} + +func (conn *Connection) GetRemoteAddr() net.Addr { + return conn.netConn.RemoteAddr() +} + +func (conn *Connection) GetChannels() map[uint16]*Channel { + return conn.channels +} + +func (conn *Connection) GetID() uint64 { + return conn.id +} + +func (conn *Connection) GetUsername() string { + return conn.userName +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/server/connectionMethods.go b/pkg/outputs/amqp/_fixtures/garagemq/server/connectionMethods.go new file mode 100644 index 000000000..869bebfd3 --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/server/connectionMethods.go @@ -0,0 +1,134 @@ +package server + +import ( + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/amqp" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/auth" + "os" + "runtime" +) + +func (channel *Channel) connectionRoute(method amqp.Method) *amqp.Error { + switch method := method.(type) { + case *amqp.ConnectionStartOk: + return channel.connectionStartOk(method) + case *amqp.ConnectionTuneOk: + return channel.connectionTuneOk(method) + case *amqp.ConnectionOpen: + return channel.connectionOpen(method) + case *amqp.ConnectionClose: + return channel.connectionClose(method) + case *amqp.ConnectionCloseOk: + return channel.connectionCloseOk(method) + } + + return amqp.NewConnectionError(amqp.NotImplemented, "unable to route connection method", method.ClassIdentifier(), method.MethodIdentifier()) +} + +func (channel *Channel) connectionStart() { + var capabilities = amqp.Table{} + capabilities["publisher_confirms"] = true + capabilities["exchange_exchange_bindings"] = false + capabilities["basic.nack"] = true + capabilities["consumer_cancel_notify"] = true + capabilities["connection.blocked"] = false + capabilities["consumer_priorities"] = false + capabilities["authentication_failure_close"] = true + capabilities["per_consumer_qos"] = true + + var serverProps = amqp.Table{} + serverProps["product"] = "garagemq" + serverProps["version"] = "0.1" + serverProps["copyright"] = "Alexander Valinurov, 2018" + serverProps["platform"] = runtime.GOARCH + serverProps["capabilities"] = capabilities + host, err := os.Hostname() + if err != nil { + serverProps["host"] = "UnknownHostError" + } else { + serverProps["host"] = host + } + + var method = amqp.ConnectionStart{VersionMajor: 0, VersionMinor: 9, ServerProperties: &serverProps, Mechanisms: []byte("PLAIN"), Locales: []byte("en_US")} + channel.SendMethod(&method) + + channel.conn.status = ConnStart +} + +func (channel *Channel) connectionStartOk(method *amqp.ConnectionStartOk) *amqp.Error { + channel.conn.status = ConnStartOK + + var saslData auth.SaslData + var err error + if saslData, err = auth.ParsePlain(method.Response); err != nil { + return amqp.NewConnectionError(amqp.NotAllowed, "login failure", method.ClassIdentifier(), method.MethodIdentifier()) + } + + if method.Mechanism != auth.SaslPlain { + channel.conn.close() + } + + if !channel.server.checkAuth(saslData) { + return amqp.NewConnectionError(amqp.NotAllowed, "login failure", method.ClassIdentifier(), method.MethodIdentifier()) + } + channel.conn.userName = saslData.Username + channel.conn.clientProperties = method.ClientProperties + + // @todo Send HeartBeat 0 cause not supported yet + channel.SendMethod(&amqp.ConnectionTune{ + ChannelMax: channel.conn.maxChannels, + FrameMax: channel.conn.maxFrameSize, + Heartbeat: channel.conn.heartbeatInterval, + }) + channel.conn.status = ConnTune + + return nil +} + +func (channel *Channel) connectionTuneOk(method *amqp.ConnectionTuneOk) *amqp.Error { + channel.conn.status = ConnTuneOK + + if method.ChannelMax > channel.conn.maxChannels || method.FrameMax > channel.conn.maxFrameSize { + channel.conn.close() + return nil + } + + channel.conn.maxChannels = method.ChannelMax + channel.conn.maxFrameSize = method.FrameMax + + if method.Heartbeat > 0 { + if method.Heartbeat < channel.conn.heartbeatInterval { + channel.conn.heartbeatInterval = method.Heartbeat + } + channel.conn.heartbeatTimeout = channel.conn.heartbeatInterval * 3 + go channel.conn.heartBeater() + } + + return nil +} + +func (channel *Channel) connectionOpen(method *amqp.ConnectionOpen) *amqp.Error { + channel.conn.status = ConnOpen + var vhostFound bool + if channel.conn.virtualHost, vhostFound = channel.server.vhosts[method.VirtualHost]; !vhostFound { + return amqp.NewConnectionError(amqp.InvalidPath, "virtualHost '"+method.VirtualHost+"' does not exist", method.ClassIdentifier(), method.MethodIdentifier()) + } + + channel.conn.vhostName = method.VirtualHost + + channel.SendMethod(&amqp.ConnectionOpenOk{}) + channel.conn.status = ConnOpenOK + + channel.logger.Info("AMQP connection open") + return nil +} + +func (channel *Channel) connectionClose(method *amqp.ConnectionClose) *amqp.Error { + channel.logger.Infof("Connection closed by client, reason - [%d] %s", method.ReplyCode, method.ReplyText) + channel.SendMethod(&amqp.ConnectionCloseOk{}) + return nil +} + +func (channel *Channel) connectionCloseOk(method *amqp.ConnectionCloseOk) *amqp.Error { + go channel.conn.close() + return nil +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/server/exchangeMethods.go b/pkg/outputs/amqp/_fixtures/garagemq/server/exchangeMethods.go new file mode 100644 index 000000000..89c145e41 --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/server/exchangeMethods.go @@ -0,0 +1,97 @@ +package server + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/amqp" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/exchange" + "strings" +) + +func (channel *Channel) exchangeRoute(method amqp.Method) *amqp.Error { + switch method := method.(type) { + case *amqp.ExchangeDeclare: + return channel.exchangeDeclare(method) + case *amqp.ExchangeDelete: + return channel.exchangeDelete(method) + } + + return amqp.NewConnectionError(amqp.NotImplemented, "unable to route queue method "+method.Name(), method.ClassIdentifier(), method.MethodIdentifier()) +} + +func (channel *Channel) exchangeDeclare(method *amqp.ExchangeDeclare) *amqp.Error { + exTypeId, err := exchange.GetExchangeTypeID(method.Type) + if err != nil { + return amqp.NewChannelError(amqp.NotImplemented, err.Error(), method.ClassIdentifier(), method.MethodIdentifier()) + } + + if method.Exchange == "" { + return amqp.NewChannelError( + amqp.CommandInvalid, + "exchange name is required", + method.ClassIdentifier(), + method.MethodIdentifier(), + ) + } + + existingExchange := channel.conn.GetVirtualHost().GetExchange(method.Exchange) + if method.Passive { + if method.NoWait { + return nil + } + + if existingExchange == nil { + return amqp.NewChannelError( + amqp.NotFound, + fmt.Sprintf("exchange '%s' not found", method.Exchange), + method.ClassIdentifier(), + method.MethodIdentifier(), + ) + } + + channel.SendMethod(&amqp.ExchangeDeclareOk{}) + + return nil + } + + if strings.HasPrefix(method.Exchange, "amq.") { + return amqp.NewChannelError( + amqp.AccessRefused, + fmt.Sprintf("exchange name '%s' contains reserved prefix 'amq.*'", method.Exchange), + method.ClassIdentifier(), + method.MethodIdentifier(), + ) + } + + newExchange := exchange.NewExchange( + method.Exchange, + exTypeId, + method.Durable, + method.AutoDelete, + method.Internal, + false, + ) + + if existingExchange != nil { + if err := existingExchange.EqualWithErr(newExchange); err != nil { + return amqp.NewChannelError( + amqp.PreconditionFailed, + err.Error(), + method.ClassIdentifier(), + method.MethodIdentifier(), + ) + } + channel.SendMethod(&amqp.ExchangeDeclareOk{}) + return nil + } + + channel.conn.GetVirtualHost().AppendExchange(newExchange) + if !method.NoWait { + channel.SendMethod(&amqp.ExchangeDeclareOk{}) + } + + return nil +} + +func (channel *Channel) exchangeDelete(method *amqp.ExchangeDelete) *amqp.Error { + return nil +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/server/queueMethods.go b/pkg/outputs/amqp/_fixtures/garagemq/server/queueMethods.go new file mode 100644 index 000000000..682f77a7b --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/server/queueMethods.go @@ -0,0 +1,244 @@ +package server + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/amqp" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/binding" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/exchange" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/queue" +) + +func (channel *Channel) queueRoute(method amqp.Method) *amqp.Error { + switch method := method.(type) { + case *amqp.QueueDeclare: + return channel.queueDeclare(method) + case *amqp.QueueBind: + return channel.queueBind(method) + case *amqp.QueueUnbind: + return channel.queueUnbind(method) + case *amqp.QueuePurge: + return channel.queuePurge(method) + case *amqp.QueueDelete: + return channel.queueDelete(method) + } + + return amqp.NewConnectionError(amqp.NotImplemented, "unable to route queue method "+method.Name(), method.ClassIdentifier(), method.MethodIdentifier()) +} + +func (channel *Channel) queueDeclare(method *amqp.QueueDeclare) *amqp.Error { + var existingQueue *queue.Queue + var notFoundErr, exclusiveErr *amqp.Error + + if method.Queue == "" { + return amqp.NewChannelError( + amqp.CommandInvalid, + "queue name is required", + method.ClassIdentifier(), + method.MethodIdentifier(), + ) + } + + existingQueue, notFoundErr = channel.getQueueWithError(method.Queue, method) + exclusiveErr = channel.checkQueueLockWithError(existingQueue, method) + + if method.Passive { + if method.NoWait { + return nil + } + + if existingQueue == nil { + return notFoundErr + } + + if exclusiveErr != nil { + return exclusiveErr + } + + channel.SendMethod(&amqp.QueueDeclareOk{ + Queue: method.Queue, + MessageCount: uint32(existingQueue.Length()), + ConsumerCount: uint32(existingQueue.ConsumersCount()), + }) + + return nil + } + + newQueue := channel.conn.GetVirtualHost().NewQueue( + method.Queue, + channel.conn.id, + method.Exclusive, + method.AutoDelete, + method.Durable, + channel.server.config.Queue.ShardSize, + ) + + if existingQueue != nil { + if exclusiveErr != nil { + return exclusiveErr + } + + if err := existingQueue.EqualWithErr(newQueue); err != nil { + return amqp.NewChannelError( + amqp.PreconditionFailed, + err.Error(), + method.ClassIdentifier(), + method.MethodIdentifier(), + ) + } + + channel.SendMethod(&amqp.QueueDeclareOk{ + Queue: method.Queue, + MessageCount: uint32(existingQueue.Length()), + ConsumerCount: uint32(existingQueue.ConsumersCount()), + }) + return nil + } + + newQueue.Start() + err := channel.conn.GetVirtualHost().AppendQueue(newQueue) + if err != nil { + return amqp.NewChannelError( + amqp.PreconditionFailed, + err.Error(), + method.ClassIdentifier(), + method.MethodIdentifier(), + ) + } + channel.SendMethod(&amqp.QueueDeclareOk{ + Queue: method.Queue, + MessageCount: 0, + ConsumerCount: 0, + }) + + return nil +} + +func (channel *Channel) queueBind(method *amqp.QueueBind) *amqp.Error { + var ex *exchange.Exchange + var qu *queue.Queue + var err *amqp.Error + + if ex, err = channel.getExchangeWithError(method.Exchange, method); err != nil { + return err + } + + // @spec-note + // The server MUST NOT allow clients to access the default exchange except by specifying an empty exchange name in the Queue.Bind and content Publish methods. + if ex.GetName() == exDefaultName { + return amqp.NewChannelError( + amqp.AccessRefused, + fmt.Sprintf("operation not permitted on the default exchange"), + method.ClassIdentifier(), + method.MethodIdentifier(), + ) + } + + if qu, err = channel.getQueueWithError(method.Queue, method); err != nil { + return err + } + + if err = channel.checkQueueLockWithError(qu, method); err != nil { + return err + } + + bind, bindErr := binding.NewBinding(method.Queue, method.Exchange, + method.RoutingKey, method.Arguments, ex.ExType() == exchange.ExTypeTopic) + if bindErr != nil { + return amqp.NewChannelError( + amqp.PreconditionFailed, + bindErr.Error(), + method.ClassIdentifier(), + method.MethodIdentifier(), + ) + + } + + ex.AppendBinding(bind) + + // @spec-note + // Bindings of durable queues to durable exchanges are automatically durable and the server MUST restore such bindings after a server restart. + if ex.IsDurable() && qu.IsDurable() { + channel.conn.GetVirtualHost().PersistBinding(bind) + } + + if !method.NoWait { + channel.SendMethod(&amqp.QueueBindOk{}) + } + + return nil +} + +func (channel *Channel) queueUnbind(method *amqp.QueueUnbind) *amqp.Error { + var ex *exchange.Exchange + var qu *queue.Queue + var err *amqp.Error + + if ex, err = channel.getExchangeWithError(method.Exchange, method); err != nil { + return err + } + + if qu, err = channel.getQueueWithError(method.Queue, method); err != nil { + return err + } + + if err = channel.checkQueueLockWithError(qu, method); err != nil { + return err + } + + bind, bindErr := binding.NewBinding(method.Queue, method.Exchange, method.RoutingKey, method.Arguments, ex.ExType() == exchange.ExTypeTopic) + + if bindErr != nil { + return amqp.NewConnectionError( + amqp.PreconditionFailed, + bindErr.Error(), + method.ClassIdentifier(), + method.MethodIdentifier(), + ) + } + + ex.RemoveBinding(bind) + channel.conn.GetVirtualHost().RemoveBindings([]*binding.Binding{bind}) + channel.SendMethod(&amqp.QueueUnbindOk{}) + + return nil +} + +func (channel *Channel) queuePurge(method *amqp.QueuePurge) *amqp.Error { + var qu *queue.Queue + var err *amqp.Error + + if qu, err = channel.getQueueWithError(method.Queue, method); err != nil { + return err + } + + if err = channel.checkQueueLockWithError(qu, method); err != nil { + return err + } + + msgCnt := qu.Purge() + if !method.NoWait { + channel.SendMethod(&amqp.QueuePurgeOk{MessageCount: uint32(msgCnt)}) + } + return nil +} + +func (channel *Channel) queueDelete(method *amqp.QueueDelete) *amqp.Error { + var qu *queue.Queue + var err *amqp.Error + + if qu, err = channel.getQueueWithError(method.Queue, method); err != nil { + return err + } + + if err = channel.checkQueueLockWithError(qu, method); err != nil { + return err + } + + var length, errDel = channel.conn.GetVirtualHost().DeleteQueue(method.Queue, method.IfUnused, method.IfEmpty) + if errDel != nil { + return amqp.NewChannelError(amqp.PreconditionFailed, errDel.Error(), method.ClassIdentifier(), method.MethodIdentifier()) + } + + channel.SendMethod(&amqp.QueueDeleteOk{MessageCount: uint32(length)}) + return nil +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/server/server.go b/pkg/outputs/amqp/_fixtures/garagemq/server/server.go new file mode 100644 index 000000000..dea86b26e --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/server/server.go @@ -0,0 +1,255 @@ +package server + +import ( + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/amqp" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/auth" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/config" + "net" + "os" + "os/signal" + "sync" + "syscall" + + log "github.com/sirupsen/logrus" +) + +type ServerState int + +// server state statuses +const ( + Stopped ServerState = iota + Running + Stopping +) + +// Server implements AMQP server +type Server struct { + host string + port string + protoVersion string + listener *net.TCPListener + connSeq uint64 + connLock sync.Mutex + connections map[uint64]*Connection + config *config.Config + users map[string]string + vhostsLock sync.Mutex + vhosts map[string]*VirtualHost + status ServerState +} + +// NewServer returns new instance of AMQP Server +func NewServer(host string, port string, protoVersion string, config *config.Config) (server *Server) { + server = &Server{ + host: host, + port: port, + connections: make(map[uint64]*Connection), + protoVersion: protoVersion, + config: config, + users: make(map[string]string), + vhosts: make(map[string]*VirtualHost), + connSeq: 0, + } + + return +} + +// Start start main server loop +func (srv *Server) Start() { + log.WithFields(log.Fields{ + "pid": os.Getpid(), + }).Info("Server starting") + + go srv.hookSignals() + + srv.initUsers() + srv.initDefaultVirtualHosts() + + go srv.listen() + + srv.status = Running + select {} +} + +// Stop stop server and all vhosts +func (srv *Server) Stop() { + srv.vhostsLock.Lock() + defer srv.vhostsLock.Unlock() + srv.status = Stopping + + // stop accept new connections + srv.listener.Close() + + var wg sync.WaitGroup + srv.connLock.Lock() + for _, conn := range srv.connections { + wg.Add(1) + go conn.safeClose(&wg) + } + srv.connLock.Unlock() + wg.Wait() + log.Info("All connections safe closed") + + // stop exchanges and queues + for _, virtualHost := range srv.vhosts { + virtualHost.Stop() + } + + srv.status = Stopped +} + +func (srv *Server) getVhost(name string) *VirtualHost { + srv.vhostsLock.Lock() + defer srv.vhostsLock.Unlock() + + return srv.vhosts[name] +} + +func (srv *Server) listen() { + address := srv.host + ":" + srv.port + tcpAddr, err := net.ResolveTCPAddr("tcp4", address) + srv.listener, err = net.ListenTCP("tcp", tcpAddr) + if err != nil { + log.WithError(err).WithFields(log.Fields{ + "address": address, + }).Error("Error on listener start") + os.Exit(1) + } + + log.WithFields(log.Fields{ + "address": address, + }).Info("Server started") + + for { + conn, err := srv.listener.AcceptTCP() + if err != nil { + if srv.status != Running { + return + } + srv.stopWithError(err, "accepting connection") + } + log.WithFields(log.Fields{ + "from": conn.RemoteAddr().String(), + "to": conn.LocalAddr().String(), + }).Info("accepting connection") + + conn.SetReadBuffer(srv.config.TCP.ReadBufSize) + conn.SetWriteBuffer(srv.config.TCP.WriteBufSize) + conn.SetNoDelay(srv.config.TCP.Nodelay) + + srv.acceptConnection(conn) + } +} + +func (srv *Server) stopWithError(err error, msg string) { + log.WithError(err).Error(msg) + srv.Stop() + os.Exit(1) +} + +func (srv *Server) acceptConnection(conn *net.TCPConn) { + srv.connLock.Lock() + defer srv.connLock.Unlock() + + connection := NewConnection(srv, conn) + srv.connections[connection.id] = connection + go connection.handleConnection() +} + +func (srv *Server) removeConnection(connID uint64) { + srv.connLock.Lock() + defer srv.connLock.Unlock() + + delete(srv.connections, connID) +} + +func (srv *Server) checkAuth(saslData auth.SaslData) bool { + for userName, passwordHash := range srv.users { + if userName != saslData.Username { + continue + } + + return auth.CheckPasswordHash( + saslData.Password, + passwordHash, + srv.config.Security.PasswordCheck == "md5", + ) + } + return false +} + +func (srv *Server) initUsers() { + for _, user := range srv.config.Users { + srv.users[user.Username] = user.Password + } +} + +func (srv *Server) initDefaultVirtualHosts() { + log.WithFields(log.Fields{ + "vhost": srv.config.Vhost.DefaultPath, + }).Info("Initialize default vhost") + + log.Info("Initialize host message msgStorage") + + srv.vhostsLock.Lock() + defer srv.vhostsLock.Unlock() + srv.vhosts[srv.config.Vhost.DefaultPath] = NewVhost(srv.config.Vhost.DefaultPath, true, srv) +} + +func (srv *Server) onSignal(sig os.Signal) { + switch sig { + case syscall.SIGTERM, syscall.SIGINT: + srv.Stop() + os.Exit(0) + } +} + +// Special method for calling in tests without os.Exit(0) +func (srv *Server) testOnSignal(sig os.Signal) { + switch sig { + case syscall.SIGTERM, syscall.SIGINT: + srv.Stop() + } +} + +func (srv *Server) hookSignals() { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + go func() { + for sig := range c { + log.Infof("Received [%d:%s] signal from OS", sig, sig.String()) + srv.onSignal(sig) + } + }() +} + +func (srv *Server) getConfirmChannel(meta *amqp.ConfirmMeta) *Channel { + srv.connLock.Lock() + defer srv.connLock.Unlock() + conn := srv.connections[meta.ConnID] + if conn == nil { + return nil + } + + return conn.getChannel(meta.ChanID) +} + +func (srv *Server) GetVhost(name string) *VirtualHost { + return srv.getVhost(name) +} + +func (srv *Server) GetVhosts() map[string]*VirtualHost { + return srv.vhosts +} + +func (srv *Server) GetConnections() map[uint64]*Connection { + return srv.connections +} + +func (srv *Server) GetProtoVersion() string { + return srv.protoVersion +} + +func (srv *Server) GetStatus() ServerState { + return srv.status +} diff --git a/pkg/outputs/amqp/_fixtures/garagemq/server/vhost.go b/pkg/outputs/amqp/_fixtures/garagemq/server/vhost.go new file mode 100644 index 000000000..025c4eca7 --- /dev/null +++ b/pkg/outputs/amqp/_fixtures/garagemq/server/vhost.go @@ -0,0 +1,304 @@ +package server + +import ( + "errors" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/amqp" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/binding" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/config" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/exchange" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/queue" + "sync" + + log "github.com/sirupsen/logrus" +) + +const exDefaultName = "" + +// VirtualHost represents AMQP virtual host +// Each virtual host is "parent" for its queues and exchanges +type VirtualHost struct { + name string + system bool + exLock sync.RWMutex + exchanges map[string]*exchange.Exchange + quLock sync.RWMutex + queues map[string]*queue.Queue + srv *Server + srvConfig *config.Config + logger *log.Entry + autoDeleteQueue chan string +} + +// NewVhost returns instance of VirtualHost +// When instantiating virtual host we +// 1) init system exchanges +// 2) load durable exchanges, queues and bindings from server storage +// 3) load persisted messages from message store into all initiated queues +// 4) run confirm loop +// Only after that vhost is in state running msgStoragePersistent, msgStorageTransient +func NewVhost(name string, system bool, srv *Server) *VirtualHost { + vhost := &VirtualHost{ + name: name, + system: system, + exchanges: make(map[string]*exchange.Exchange), + queues: make(map[string]*queue.Queue), + srvConfig: srv.config, + srv: srv, + autoDeleteQueue: make(chan string, 1), + } + + vhost.logger = log.WithFields(log.Fields{ + "vhost": name, + }) + + vhost.initSystemExchanges() + vhost.loadExchanges() + vhost.loadQueues() + vhost.loadBindings() + + vhost.logger.Info("Load messages into queues") + + vhost.loadMessagesIntoQueues() + for _, q := range vhost.GetQueues() { + q.Start() + vhost.logger.WithFields(log.Fields{ + "name": q.GetName(), + "length": q.Length(), + }).Info("Messages loaded into queue") + } + + go vhost.handleConfirms() + go vhost.handleAutoDeleteQueue() + + return vhost +} + +func (vhost *VirtualHost) handleAutoDeleteQueue() { + for queueName := range vhost.autoDeleteQueue { + //time.Sleep(5 * time.Second) + vhost.DeleteQueue(queueName, false, false) + } +} + +func (vhost *VirtualHost) handleConfirms() { + +} + +func (vhost *VirtualHost) initSystemExchanges() { + // @spec-note + // The server MUST, in each virtual host, pre­declare an exchange instance for each standard exchange type that it + // implements, where the name of the exchange instance, if defined, is "amq." followed by the exchange type name. + + // The server MUST, in each virtual host, pre­declare at least two direct exchange instances: one named "amq.direct", + // the other with no public name that serves as a default exchange for Publish methods. + + // The server MUST pre­declare a direct exchange with no public name to act as the default exchange for content Publish methods and for default queue bindings. + + vhost.logger.Info("Initialize host default exchanges...") + for _, exType := range []byte{ + exchange.ExTypeDirect, + exchange.ExTypeFanout, + exchange.ExTypeTopic, + } { + exTypeAlias, _ := exchange.GetExchangeTypeAlias(exType) + exName := "amq." + exTypeAlias + vhost.AppendExchange(exchange.NewExchange(exName, exType, true, false, false, true)) + } + + // Special case for exchange.ExTypeHeaders + // + // AMQP specifies that the default exchange for headers shall be called + // amq.match, but RabbitMQ declares it as amq.header + // + // To be compatible, we change its name depending on protoVersion + protoVer := vhost.srv.protoVersion + + exTypeAlias, _ := exchange.GetExchangeTypeAlias(exchange.ExTypeHeaders) + exName := "amq." + exTypeAlias + + if protoVer == amqp.ProtoRabbit { + exName = "amq.header" + } + vhost.AppendExchange(exchange.NewExchange(exName, exchange.ExTypeHeaders, true, false, false, true)) + + systemExchange := exchange.NewExchange(exDefaultName, exchange.ExTypeDirect, true, false, false, true) + vhost.AppendExchange(systemExchange) +} + +// GetQueue returns queue by name or nil if not exists +func (vhost *VirtualHost) GetQueue(name string) *queue.Queue { + vhost.quLock.RLock() + defer vhost.quLock.RUnlock() + return vhost.getQueue(name) +} + +// GetQueues return all vhost's queues +func (vhost *VirtualHost) GetQueues() map[string]*queue.Queue { + vhost.quLock.RLock() + defer vhost.quLock.RUnlock() + return vhost.queues +} + +func (vhost *VirtualHost) getQueue(name string) *queue.Queue { + return vhost.queues[name] +} + +// GetExchange returns exchange by name or nil if not exists +func (vhost *VirtualHost) GetExchange(name string) *exchange.Exchange { + vhost.exLock.RLock() + defer vhost.exLock.RUnlock() + return vhost.getExchange(name) +} + +func (vhost *VirtualHost) getExchange(name string) *exchange.Exchange { + return vhost.exchanges[name] +} + +func (vhost *VirtualHost) GetExchanges() map[string]*exchange.Exchange { + return vhost.exchanges +} + +// GetDefaultExchange returns default exchange +func (vhost *VirtualHost) GetDefaultExchange() *exchange.Exchange { + return vhost.exchanges[exDefaultName] +} + +// AppendExchange append new exchange and persist if it is durable +func (vhost *VirtualHost) AppendExchange(ex *exchange.Exchange) { + vhost.exLock.Lock() + defer vhost.exLock.Unlock() + exTypeAlias, _ := exchange.GetExchangeTypeAlias(ex.ExType()) + vhost.logger.WithFields(log.Fields{ + "name": ex.GetName(), + "type": exTypeAlias, + }).Info("Append exchange") + vhost.exchanges[ex.GetName()] = ex + +} + +// NewQueue returns new instance of queue by params +// we can't use just queue.NewQueue, cause we need to set msgStorage to queue +func (vhost *VirtualHost) NewQueue(name string, connID uint64, exclusive bool, autoDelete bool, durable bool, shardSize int) *queue.Queue { + return queue.NewQueue( + name, + connID, + exclusive, + autoDelete, + durable, + vhost.srvConfig.Queue, + nil, + nil, + vhost.autoDeleteQueue, + ) +} + +// AppendQueue append new queue and persist if it is durable and +// bindings into default exchange +func (vhost *VirtualHost) AppendQueue(qu *queue.Queue) error { + vhost.quLock.Lock() + defer vhost.quLock.Unlock() + vhost.logger.WithFields(log.Fields{ + "queueName": qu.GetName(), + }).Info("Append queue") + + vhost.queues[qu.GetName()] = qu + + // @spec-note + // The server MUST create a default binding for a newly­declared queue to the default exchange, + // which is an exchange of type 'direct' and use the queue name as the routing key. + ex := vhost.GetDefaultExchange() + bind, bindErr := binding.NewBinding(qu.GetName(), exDefaultName, + qu.GetName(), &amqp.Table{}, false) + if bindErr != nil { + // Should not happen since the only error paths are on `topic` and + // `headers` + return bindErr + } + + ex.AppendBinding(bind) + + if qu.IsDurable() { + + } + + return nil +} + +// PersistBinding store binding into server storage +func (vhost *VirtualHost) PersistBinding(binding *binding.Binding) { +} + +// RemoveBindings remove given bindings from server storage +func (vhost *VirtualHost) RemoveBindings(bindings []*binding.Binding) { +} + +func (vhost *VirtualHost) loadQueues() { +} + +func (vhost *VirtualHost) loadMessagesIntoQueues() { + var wg sync.WaitGroup + for queueName, q := range vhost.queues { + wg.Add(1) + go func(queueName string, queue *queue.Queue) { + wg.Done() + }(queueName, q) + } + wg.Wait() +} + +func (vhost *VirtualHost) loadExchanges() { +} + +func (vhost *VirtualHost) loadBindings() { +} + +// DeleteQueue delete queue from virtual host and all bindings to that queue +// Also queue will be removed from server storage +func (vhost *VirtualHost) DeleteQueue(queueName string, ifUnused bool, ifEmpty bool) (uint64, error) { + vhost.quLock.Lock() + defer vhost.quLock.Unlock() + + qu := vhost.getQueue(queueName) + if qu == nil { + return 0, errors.New("not found") + } + + var length, err = qu.Delete(ifUnused, ifEmpty) + if err != nil { + return 0, err + } + + qu.Stop() + + for _, ex := range vhost.exchanges { + removedBindings := ex.RemoveQueueBindings(queueName) + vhost.RemoveBindings(removedBindings) + } + delete(vhost.queues, queueName) + + return length, nil +} + +// Stop properly stop virtual host +// TODO: properly stop confirm loop +func (vhost *VirtualHost) Stop() error { + vhost.quLock.Lock() + vhost.exLock.Lock() + defer vhost.quLock.Unlock() + defer vhost.exLock.Unlock() + vhost.logger.Info("Stop virtual host") + for _, qu := range vhost.queues { + qu.Stop() + vhost.logger.WithFields(log.Fields{ + "queueName": qu.GetName(), + }).Info("Queue stopped") + } + + vhost.logger.Info("Storage closed") + close(vhost.autoDeleteQueue) + return nil +} + +func (vhost *VirtualHost) GetName() string { + return vhost.name +} diff --git a/pkg/outputs/amqp/amqp.go b/pkg/outputs/amqp/amqp.go new file mode 100644 index 000000000..fe2f3b23f --- /dev/null +++ b/pkg/outputs/amqp/amqp.go @@ -0,0 +1,81 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package amqp + +import ( + "expvar" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/outputs" +) + +var ( + // amqpErrors counts AMQP delivery errors + amqpErrors = expvar.NewInt("output.amqp.publish.errors") + // amqpMessages counts the total number of published messages + amqpMessages = expvar.NewInt("output.amqp.publish.messages") +) + +type rabbitmq struct { + client *client +} + +func init() { + outputs.Register(outputs.AMQP, initAmqp) +} + +func initAmqp(config outputs.Config) (outputs.OutputGroup, error) { + cfg, ok := config.Output.(Config) + if !ok { + return outputs.Fail(outputs.ErrInvalidConfig(outputs.AMQP, config.Output)) + } + + q := &rabbitmq{client: newClient(cfg)} + + return outputs.Success(q), nil +} + +func (q *rabbitmq) Connect() error { + err := q.client.connect(true) + if err != nil { + return err + } + return q.client.declareExchange() +} + +func (q *rabbitmq) Close() error { + if q.client == nil { + return nil + } + return q.client.close() +} + +func (q *rabbitmq) Publish(batch *kevent.Batch) error { + body := batch.MarshalJSON() + defer batch.Release() + + err := q.client.publish(body) + if err != nil { + amqpErrors.Add(1) + return err + } + + amqpMessages.Add(1) + + return nil +} diff --git a/pkg/outputs/amqp/amqp_test.go b/pkg/outputs/amqp/amqp_test.go new file mode 100644 index 000000000..97a8f4f14 --- /dev/null +++ b/pkg/outputs/amqp/amqp_test.go @@ -0,0 +1,384 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package amqp + +import ( + "encoding/json" + "fmt" + "github.com/phayes/freeport" + htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/config" + broker "github.com/rabbitstack/fibratus/pkg/outputs/amqp/_fixtures/garagemq/server" + pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" + shandle "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/streadway/amqp" + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestPublishAmqpOutput(t *testing.T) { + port, err := freeport.GetFreePort() + require.NoError(t, err) + amqpBroker := broker.NewServer("127.0.0.1", fmt.Sprintf("%d", port), "amqp-rabbit", config.Default()) + + done := make(chan struct{}) + + go func() { + amqpBroker.Start() + }() + defer amqpBroker.Stop() + + q := rabbitmq{client: newClient(Config{ + URL: amqpURL(port), + Exchange: "fibratus", + ExchangeType: "topic", + RoutingKey: "fibratus", + })} + require.NoError(t, q.Connect()) + defer q.Close() + + err = consumeKevents(t, amqpURL(port), done) + require.NoError(t, err) + + err = q.Publish(getBatch()) + require.NoError(t, err) + + <-done +} + +func TestHealthcheck(t *testing.T) { + port, err := freeport.GetFreePort() + require.NoError(t, err) + amqpBroker := broker.NewServer("127.0.0.1", fmt.Sprintf("%d", port), "amqp-rabbit", config.Default()) + + go func() { + amqpBroker.Start() + }() + + q := rabbitmq{client: newClient(Config{ + URL: amqpURL(port), + Exchange: "fibratus", + ExchangeType: "topic", + RoutingKey: "fibratus", + Timeout: time.Second, + })} + require.NoError(t, q.Connect()) + defer q.Close() + + time.Sleep(time.Millisecond * 400) + + amqpBroker.Stop() + + err = q.Publish(getBatch()) + require.Error(t, err) + + time.Sleep(time.Millisecond * 100) + + go func() { + amqpBroker.Start() + }() + + time.Sleep(time.Millisecond * 2000) + require.NoError(t, q.client.declareExchange()) + err = q.Publish(getBatch()) + require.NoError(t, err) + +} + +func consumeKevents(t *testing.T, amqpURI string, done chan struct{}) error { + conn, err := amqp.Dial(amqpURI) + if err != nil { + return err + } + + channel, err := conn.Channel() + if err != nil { + return err + } + queue, err := channel.QueueDeclare( + "fibratus", // name of the queue + true, // durable + false, // delete when unused + false, // exclusive + false, // noWait + nil, // arguments + ) + if err != nil { + return err + } + if err = channel.QueueBind( + queue.Name, // name of the queue + "fibratus", // bindingKey + "fibratus", // sourceExchange + false, // noWait + nil, // arguments + ); err != nil { + return err + } + + deliveries, err := channel.Consume( + queue.Name, // name + "kevents-consumer", // consumerTag, + false, // noAck + false, // exclusive + false, // noLocal + false, // noWait + nil, // arguments + ) + + go func() { + for d := range deliveries { + body := d.Body + if len(body) == 0 { + done <- struct{}{} + t.Fatal("got empty AMQP message") + } + var kevents []*kevent.Kevent + err := json.Unmarshal(body, &kevents) + if err != nil { + done <- struct{}{} + t.Fatal(err) + } + if len(kevents) != 3 { + done <- struct{}{} + t.Fatalf("expected 3 events in body but got %d", len(kevents)) + } + d.Ack(false) + done <- struct{}{} + } + }() + + return nil +} + +func amqpURL(port int) string { + return fmt.Sprintf("amqp://localhost:%d", port) +} + +func getBatch() *kevent.Batch { + kevt := &kevent.Kevent{ + Type: ktypes.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: kevent.Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, + kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + }, + Metadata: map[string]string{"foo": "bar", "fooz": "baarz"}, + PS: &pstypes.PS{ + PID: 2436, + Ppid: 6304, + Name: "firefox.exe", + Exe: `C:\Program Files\Mozilla Firefox\firefox.exe`, + Comm: `C:\Program Files\Mozilla Firefox\firefox.exe -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, + Cwd: `C:\Program Files\Mozilla Firefox\`, + SID: "archrabbit\\SYSTEM", + Args: []string{"-contentproc", `--channel=6304.3.1055809391\1014207667`, "-childID", "1", "-isForBrowser", "-prefsHandle", "2584", "-prefMapHandle", "2580", "-prefsLen", "70", "-prefMapSize", "216993", "-parentBuildID"}, + SessionID: 4, + Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, + Threads: map[uint32]pstypes.Thread{ + 3453: {Tid: 3453, Entrypoint: kparams.Hex("0x7ffe2557ff80"), IOPrio: 2, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + 3455: {Tid: 3455, Entrypoint: kparams.Hex("0x5efe2557ff80"), IOPrio: 3, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + }, + Handles: []htypes.Handle{ + {Num: shandle.Handle(0xffffd105e9baaf70), + Name: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, + Type: "Key", + Object: 777488883434455544, + Pid: uint32(1023), + }, + { + Num: shandle.Handle(0xffffd105e9adaf70), + Name: `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, + Type: "ALPC Port", + Pid: uint32(1023), + MD: &htypes.AlpcPortInfo{ + Seqno: 1, + Context: 0x0, + Flags: 0x0, + }, + Object: 457488883434455544, + }, + { + Num: shandle.Handle(0xeaffd105e9adaf30), + Name: `C:\Users\bunny`, + Type: "File", + Pid: uint32(1023), + MD: &htypes.FileInfo{ + IsDirectory: true, + }, + Object: 357488883434455544, + }, + }, + }, + } + + kevt1 := &kevent.Kevent{ + Type: ktypes.CreateFile, + Tid: 2484, + PID: 459, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: kevent.Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, + kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + }, + Metadata: map[string]string{"foo": "bar", "fooz": "baarz"}, + PS: &pstypes.PS{ + PID: 2436, + Ppid: 6304, + Name: "firefox.exe", + Exe: `C:\Program Files\Mozilla Firefox\firefox.exe`, + Comm: `C:\Program Files\Mozilla Firefox\firefox.exe -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, + Cwd: `C:\Program Files\Mozilla Firefox\`, + SID: "archrabbit\\SYSTEM", + Args: []string{"-contentproc", `--channel=6304.3.1055809391\1014207667`, "-childID", "1", "-isForBrowser", "-prefsHandle", "2584", "-prefMapHandle", "2580", "-prefsLen", "70", "-prefMapSize", "216993", "-parentBuildID"}, + SessionID: 4, + Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, + Threads: map[uint32]pstypes.Thread{ + 3453: {Tid: 3453, Entrypoint: kparams.Hex("0x7ffe2557ff80"), IOPrio: 2, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + 3455: {Tid: 3455, Entrypoint: kparams.Hex("0x5efe2557ff80"), IOPrio: 3, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + }, + Handles: []htypes.Handle{ + {Num: shandle.Handle(0xffffd105e9baaf70), + Name: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, + Type: "Key", + Object: 777488883434455544, + Pid: uint32(1023), + }, + { + Num: shandle.Handle(0xffffd105e9adaf70), + Name: `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, + Type: "ALPC Port", + Pid: uint32(1023), + MD: &htypes.AlpcPortInfo{ + Seqno: 1, + Context: 0x0, + Flags: 0x0, + }, + Object: 457488883434455544, + }, + { + Num: shandle.Handle(0xeaffd105e9adaf30), + Name: `C:\Users\bunny`, + Type: "File", + Pid: uint32(1023), + MD: &htypes.FileInfo{ + IsDirectory: true, + }, + Object: 357488883434455544, + }, + }, + }, + } + + kevt2 := &kevent.Kevent{ + Type: ktypes.CreateFile, + Tid: 2484, + PID: 829, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: time.Now(), + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: kevent.Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, + kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + }, + Metadata: map[string]string{"foo": "bar", "fooz": "baarz"}, + PS: &pstypes.PS{ + PID: 829, + Ppid: 6304, + Name: "firefox.exe", + Exe: `C:\Program Files\Mozilla Firefox\firefox.exe`, + Comm: `C:\Program Files\Mozilla Firefox\firefox.exe -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, + Cwd: `C:\Program Files\Mozilla Firefox\`, + SID: "archrabbit\\SYSTEM", + Args: []string{"-contentproc", `--channel=6304.3.1055809391\1014207667`, "-childID", "1", "-isForBrowser", "-prefsHandle", "2584", "-prefMapHandle", "2580", "-prefsLen", "70", "-prefMapSize", "216993", "-parentBuildID"}, + SessionID: 4, + Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, + Threads: map[uint32]pstypes.Thread{ + 3453: {Tid: 3453, Entrypoint: kparams.Hex("0x7ffe2557ff80"), IOPrio: 2, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + 3455: {Tid: 3455, Entrypoint: kparams.Hex("0x5efe2557ff80"), IOPrio: 3, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + }, + Handles: []htypes.Handle{ + {Num: shandle.Handle(0xffffd105e9baaf70), + Name: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, + Type: "Key", + Object: 777488883434455544, + Pid: uint32(1023), + }, + { + Num: shandle.Handle(0xffffd105e9adaf70), + Name: `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, + Type: "ALPC Port", + Pid: uint32(1023), + MD: &htypes.AlpcPortInfo{ + Seqno: 1, + Context: 0x0, + Flags: 0x0, + }, + Object: 457488883434455544, + }, + { + Num: shandle.Handle(0xeaffd105e9adaf30), + Name: `C:\Users\bunny`, + Type: "File", + Pid: uint32(1023), + MD: &htypes.FileInfo{ + IsDirectory: true, + }, + Object: 357488883434455544, + }, + }, + }, + } + + return kevent.NewBatch(kevt, kevt1, kevt2) +} diff --git a/pkg/outputs/amqp/client.go b/pkg/outputs/amqp/client.go new file mode 100644 index 000000000..cb55b170a --- /dev/null +++ b/pkg/outputs/amqp/client.go @@ -0,0 +1,193 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package amqp + +import ( + "expvar" + "fmt" + "github.com/rabbitstack/fibratus/pkg/util/tls" + log "github.com/sirupsen/logrus" + "github.com/streadway/amqp" + "net" + "time" +) + +var ( + connectionFailures = expvar.NewInt("output.amqp.connection.failures") + channelFailures = expvar.NewInt("output.amqp.channel.failures") +) + +// client encapsulates the AMQP connection/channel and deals with configuring, establishing the connection +// and publishing messages to the exchange. +type client struct { + conn *amqp.Connection + channel *amqp.Channel + config Config + quit chan struct{} +} + +// newClient creates a new AMQP client and setups the connection/channel. +func newClient(config Config) *client { + return &client{config: config, quit: make(chan struct{})} +} + +// connect opens a connection to the AMQP broker honoring the preferences that were passed in the config. +func (c *client) connect(healthcheck bool) error { + amqpConfig := amqp.Config{ + Vhost: c.config.Vhost, + Dial: func(network, addr string) (net.Conn, error) { + return net.DialTimeout(network, addr, c.config.Timeout) + }, + SASL: c.config.auth(), + } + tlsConfig, err := tls.MakeConfig(c.config.TLSCert, c.config.TLSKey, c.config.TLSCA, c.config.TLSInsecureSkipVerify) + if err != nil { + return fmt.Errorf("invalid TLS config: %v", err) + } + amqpConfig.TLSClientConfig = tlsConfig + + c.conn, err = amqp.DialConfig(c.config.URL, amqpConfig) + if err != nil { + return err + } + c.channel, err = c.conn.Channel() + if err != nil { + return fmt.Errorf("unable to open AMQP channel: %v", err) + } + + log.Infof("established connection to AMQP broker on %s", c.config.URL) + + if healthcheck { + go c.doHealthcheck() + } + + return nil +} + +// declareExchange creates the exchange in the broker where messages are published. +func (c *client) declareExchange() error { + var err error + if c.config.Passive { + err = c.channel.ExchangeDeclarePassive( + c.config.Exchange, + c.config.ExchangeType, + c.config.Durable, + false, + false, + false, + nil, + ) + } else { + err = c.channel.ExchangeDeclare( + c.config.Exchange, + c.config.ExchangeType, + c.config.Durable, + false, + false, + false, + nil, + ) + } + if err != nil { + return fmt.Errorf("unable to declare %s exchange: %v", c.config.Exchange, err) + } + return nil +} + +// publish sends the byte stream to the exchange. +func (c *client) publish(body []byte) error { + return c.channel.Publish(c.config.Exchange, c.config.RoutingKey, false, false, c.msg(body)) +} + +func (c *client) msg(body []byte) amqp.Publishing { + return amqp.Publishing{ + Body: body, + ContentType: "text/json", + Headers: c.config.amqpHeaders(), + DeliveryMode: c.config.deliveryMode(), + } +} + +// healthcheck monitors the state of the AMQP connection and its corresponding channel. Since AMQP channel is +// shutdown if an error occurs on it, we'll have to handle this situation properly and try to reopen the channel. +// Similarly if the connection is lost, the reconnect loop kicks in and tries to reconcile the connection state. +func (c *client) doHealthcheck() { + notify := c.conn.NotifyClose(make(chan *amqp.Error)) + cnotify := c.channel.NotifyClose(make(chan *amqp.Error)) + go func() { + for { + select { + case err := <-cnotify: + if err != nil { + channelFailures.Add(1) + log.Warnf("channel error: %v. Trying to reopen...", err) + if c.conn != nil && !c.conn.IsClosed() { + for { + var err error + c.channel, err = c.conn.Channel() + if err == nil { + log.Info("channel reopened") + cnotify = c.channel.NotifyClose(make(chan *amqp.Error)) + break + } + // sleep a bit before retrying + time.Sleep(time.Millisecond * 500) + } + } + } + case <-c.quit: + return + } + } + }() + + for { + select { + case err := <-notify: + if err != nil { + for { + connectionFailures.Add(1) + log.Warnf("connection error: %v. Trying to reconnect...", err) + e := c.connect(false) + if e == nil { + log.Info("connection recovered") + notify = c.conn.NotifyClose(make(chan *amqp.Error)) + break + } + } + } + case <-c.quit: + return + } + } +} + +// close tears down the underlying AMQP connection. +func (c *client) close() error { + if c.conn == nil { + return nil + } + c.quit <- struct{}{} + c.quit <- struct{}{} + err := c.conn.Close() + if err != nil && err != amqp.ErrClosed { + return err + } + return nil +} diff --git a/pkg/outputs/amqp/config.go b/pkg/outputs/amqp/config.go new file mode 100644 index 000000000..97ad336ac --- /dev/null +++ b/pkg/outputs/amqp/config.go @@ -0,0 +1,120 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package amqp + +import ( + "github.com/rabbitstack/fibratus/pkg/outputs" + "github.com/spf13/pflag" + "github.com/streadway/amqp" + "time" +) + +const ( + amqpURI = "output.amqp.url" + amqpTimeout = "output.amqp.timeout" + amqpVhost = "output.amqp.vhost" + amqpExchange = "output.amqp.exchange" + amqpRoutingKey = "output.amqp.routing-key" + amqpExchangeType = "output.amqp.exchange-type" + amqpEnabled = "output.amqp.enabled" + amqpPassive = "output.amqp.passive" + amqpDurable = "output.amqp.durable" + amqpDeliveryMode = "output.amqp.delivery-mode" + amqpUsername = "output.amqp.username" + amqpPassword = "output.amqp.password" +) + +// Config contains the tweaks that influence the behaviour of the AMQP output. +type Config struct { + outputs.TLSConfig + // Enabled indicates if the AMQP output is enabled + Enabled bool `mapstructure:"enabled"` + // URL represents the AMQP connection string. + URL string `mapstructure:"url"` + // Timeout specifies the AMQP connection timeout. + Timeout time.Duration `mapstructure:"timeout"` + // Exchange is the AMQP exchange for publishing events. + Exchange string `mapstructure:"exchange"` + // ExchangeType is the AMQP exchange type. + ExchangeType string `mapstructure:"exchange-type"` + // Passive indicates that the server checks whether the exchange already exists and raises an error if it doesn't exist. + Passive bool `mapstructure:"passive"` + // Durable indicates that the exchange is marked as durable. Durable exchanges can survive server restarts. + Durable bool `mapstructure:"durable"` + // DeliveryMode determines if a published message is persistent or transient. + DeliveryMode string `mapstructure:"delivery-mode"` + // RoutingKey represents the static routing key to link exchanges with queues. + RoutingKey string `mapstructure:"routing-key"` + // Username is the username for the plain authentication method. + Username string `mapstructure:"username"` + // Password is the password for the plain authentication method. + Password string `mapstructure:"password"` + // Vhost represents the virtual host name. + Vhost string `mapstructure:"vhost"` + // Headers contains a list of headers that are added to AMQP message + Headers map[string]string `mapstructure:"headers"` +} + +// AddFlags registers persistent flags. +func AddFlags(flags *pflag.FlagSet) { + flags.String(amqpURI, "amqp://localhost:5672", "Represents the AMQP broker address") + flags.Duration(amqpTimeout, time.Second*5, "Specifies the AMQP connection timeout") + flags.String(amqpVhost, "/", "The virtual host that provides logical grouping and separation of broker's resources") + flags.String(amqpExchange, "fibratus", "Specifies the target exchange name") + flags.String(amqpExchangeType, "topic", "Defines the AMQP exchange type") + flags.String(amqpRoutingKey, "fibratus", "Specifies the routing key") + flags.Bool(amqpDurable, false, "Indicates if the exchange is marked as durable. Durable exchanges can survive server restarts.") + flags.Bool(amqpPassive, false, "Indicates if the server checks whether the exchange already exists and raises an error if it doesn't exist.") + flags.Bool(amqpEnabled, false, "Indicates if the AMQP output is enabled") + flags.String(amqpDeliveryMode, "transient", "Determines if a published message is persistent or transient") + flags.String(amqpUsername, "", "The username for the plain authentication method") + flags.String(amqpPassword, "", "The password for the plain authentication method") + outputs.AddTLSFlags(flags, outputs.AMQP) +} + +func (c Config) amqpHeaders() amqp.Table { + headers := make(amqp.Table) + for k, v := range c.Headers { + headers[k] = v + } + return headers +} + +func (c Config) deliveryMode() uint8 { + switch c.DeliveryMode { + case "transient": + return amqp.Transient + case "persistent": + return amqp.Persistent + default: + return amqp.Transient + } +} + +func (c Config) auth() []amqp.Authentication { + if c.Username == "" && c.Password == "" { + return nil + } + return []amqp.Authentication{ + &amqp.PlainAuth{ + Username: c.Username, + Password: c.Password, + }, + } +} diff --git a/pkg/outputs/client.go b/pkg/outputs/client.go new file mode 100644 index 000000000..be98bdea8 --- /dev/null +++ b/pkg/outputs/client.go @@ -0,0 +1,30 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package outputs + +import ( + "github.com/rabbitstack/fibratus/pkg/kevent" +) + +// Client represents the minimal interface all output implementors have to satisfy. +type Client interface { + Close() error + Publish(*kevent.Batch) error + Connect() error +} diff --git a/pkg/outputs/config.go b/pkg/outputs/config.go new file mode 100644 index 000000000..41850daa6 --- /dev/null +++ b/pkg/outputs/config.go @@ -0,0 +1,52 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package outputs + +import ( + "fmt" + "github.com/spf13/pflag" +) + +// Config contains the output configuration. +type Config struct { + Type Type + Output interface{} +} + +// TLSConfig stores the client TLS parameters. +type TLSConfig struct { + // TLSCA represents the path of the certificate file that is associated with the Certification Authority (CA). + TLSCA string `mapstructure:"tls-ca"` + // TLSCert is the path to the certificate file. + TLSCert string `mapstructure:"tls-cert"` + // TLSKey represents the path to the public/private key file. + TLSKey string `mapstructure:"tls-key"` + // TLSInsecureSkipVerify skips the chain and host verification. + TLSInsecureSkipVerify bool `mapstructure:"tls-insecure-skip-verify"` +} + +// AddTLSFlags register the TLS flags for the specified output type. +func AddTLSFlags(flags *pflag.FlagSet, typ Type) { + flags.String(tlsForOutput("tls-ca", typ), "", "Represents the path of the certificate file that is associated with the Certification Authority (CA)") + flags.String(tlsForOutput("tls-cert", typ), "", "Path to certificate file") + flags.String(tlsForOutput("tls-key", typ), "", "Path to the public/private key file") + flags.Bool(tlsForOutput("tls-insecure-skip-verify", typ), false, "Indicates if the chain and host verification stage is skipped") +} + +func tlsForOutput(name string, typ Type) string { return fmt.Sprintf("output.%s.%s", typ, name) } diff --git a/pkg/outputs/console/config.go b/pkg/outputs/console/config.go new file mode 100644 index 000000000..583b894bd --- /dev/null +++ b/pkg/outputs/console/config.go @@ -0,0 +1,44 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package console + +import "github.com/spf13/pflag" + +const ( + frmt = "output.console.format" + tmpl = "output.console.template" + paramKVDelimiter = "output.console.kv-delimiter" + enabled = "output.console.enabled" +) + +// Config contains the tweaks that influence the behaviour of the console output. +type Config struct { + Format string `mapstructure:"format"` + Template string `mapstructure:"template"` + ParamKVDelimiter string `mapstructure:"kv-delimiter"` + Enabled bool `mapstructure:"enabled"` +} + +// AddFlags registers persistent flags. +func AddFlags(flags *pflag.FlagSet) { + flags.String(frmt, string(pretty), "Specifies the output format. Choose between pretty|json") + flags.String(paramKVDelimiter, "", "The delimiter symbol for the kparams key/value pairs") + flags.String(tmpl, "", "Event formatting template") + flags.Bool(enabled, true, "Indicates if the console output is enabled") +} diff --git a/pkg/outputs/console/console.go b/pkg/outputs/console/console.go new file mode 100644 index 000000000..371c5daa1 --- /dev/null +++ b/pkg/outputs/console/console.go @@ -0,0 +1,124 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package console + +import ( + "bufio" + "expvar" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/outputs" + "os" +) + +var ( + consoleErrors = expvar.NewInt("output.console.errors") +) + +type format string + +const ( + pretty format = "pretty" + json format = "json" + // template represents the default template used in pretty rendering mode + template = "{{ .Seq }} {{ .Timestamp }} - {{ .CPU }} {{ .Process }} ({{ .Pid }}) - {{ .Type }} ({{ .Kparams }})" +) + +type console struct { + writer *bufio.Writer + formatter *kevent.Formatter + format format +} + +func init() { + outputs.Register(outputs.Console, initConsole) +} + +func initConsole(config outputs.Config) (outputs.OutputGroup, error) { + stdout := os.Stdout + cfg, ok := config.Output.(Config) + if !ok { + return outputs.Fail(outputs.ErrInvalidConfig(outputs.Console, config.Output)) + } + tmpl := cfg.Template + if tmpl == "" { + tmpl = template + } + formatter, err := kevent.NewFormatter(tmpl) + if err != nil { + return outputs.Fail(err) + } + if cfg.ParamKVDelimiter != "" { + kevent.ParamKVDelimiter = cfg.ParamKVDelimiter + } + + c := &console{ + writer: bufio.NewWriterSize(stdout, 8*1024), + formatter: formatter, + format: format(cfg.Format), + } + return outputs.Success(c), nil +} + +func (c *console) Close() error { return c.writer.Flush() } +func (c *console) Connect() error { return nil } +func (c *console) Publish(batch *kevent.Batch) error { + defer batch.Release() + + for _, kevt := range batch.Events { + var buf []byte + switch c.format { + case json: + buf = kevt.MarshalJSON() + case pretty: + buf = c.formatter.Format(kevt) + default: + return nil + } + + if err := c.write(buf); err != nil { + consoleErrors.Add(1) + continue + } + if err := c.write(nl); err != nil { + consoleErrors.Add(1) + continue + } + } + + if err := c.writer.Flush(); err != nil { + consoleErrors.Add(1) + return err + } + + return nil +} + +var nl = []byte("\n") + +func (c *console) write(buf []byte) error { + written := 0 + for written < len(buf) { + n, err := c.writer.Write(buf[written:]) + if err != nil { + return err + } + written += n + } + return nil +} diff --git a/pkg/outputs/elasticsearch/config.go b/pkg/outputs/elasticsearch/config.go new file mode 100644 index 000000000..f1afae01b --- /dev/null +++ b/pkg/outputs/elasticsearch/config.go @@ -0,0 +1,101 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package elasticsearch + +import ( + "github.com/rabbitstack/fibratus/pkg/outputs" + "github.com/spf13/pflag" + "time" +) + +const ( + esEnabled = "output.elasticsearch.enabled" + esServers = "output.elasticsearch.servers" + esTimeout = "output.elasticsearch.timeout" + esFlushPeriod = "output.elasticsearch.flush-period" + esHealthcheck = "output.elasticsearch.healthcheck" + esBulkWorkers = "output.elasticsearch.bulk-workers" + esHealthcheckInterval = "output.elasticsearch.healthcheck-interval" + esHealthcheckTimeout = "output.elasticsearch.healthcheck-timeout" + esUsername = "output.elasticsearch.username" + esPassword = "output.elasticsearch.password" + esSniff = "output.elasticsearch.sniff" + esTraceLog = "output.elasticsearch.trace-log" + esIndexName = "output.elasticsearch.index-name" + esTemplateName = "output.elasticsearch.template-name" + esTemplateConfig = "output.elasticsearch.template-config" + esGzipCompression = "output.elasticsearch.gzip-compression" +) + +// Config contains the options for tweaking the output behaviour. +type Config struct { + outputs.TLSConfig + // Enabled determines whether ES output is enabled. + Enabled bool `mapstructure:"enabled"` + // Servers contains a comma separated list of Elasticsearch instances that comprise the cluster. + Servers []string `mapstructure:"servers"` + // Timeout specifies the connection timeout. + Timeout time.Duration `mapstructure:"timeout"` + // FlushPeriod specifies when to flush the bulk at the end of the given interval. + FlushPeriod time.Duration `mapstructure:"flush-period"` + // BulkWorkers represents the number of workers that commit docs to Elasticserach. + BulkWorkers int `mapstructure:"bulk-workers"` + // Healthcheck enables/disables nodes health checking. + Healthcheck bool `mapstructure:"healthcheck"` + // HealthCheckInterval specifies the interval for checking if the Elasticsearch nodes are available. + HealthCheckInterval time.Duration `mapstructure:"healthcheck-interval"` + // HealthCheckTimeout sets the timeout for periodic health checks. + HealthCheckTimeout time.Duration `mapstructure:"healthcheck-timeout"` + // Username is the user name for the basic HTTP authentication. + Username string `mapstructure:"username"` + // Password is the password for the basic HTTP authentication. + Password string `mapstructure:"password"` + // Sniff enables the discovery of all Elasticsearch nodes in the cluster. This avoids populating the list of available Elasticsearch nodes. + Sniff bool `mapstructure:"sniff"` + // TraceLog determines if the Elasticsearch trace log is enabled. Useful for troubleshooting. + TraceLog bool `mapstructure:"tracelog"` + // IndexName represents the target index for kernel events. It allows time specifiers to create indices per time frame. + IndexName string `mapstructure:"index-name"` + // TemplateName specifies the name of the index template. + TemplateName string `mapstructure:"template-name"` + // TemplateConfig contains the full JSON body of the index template. + TemplateConfig string `mapstructure:"template-config"` + // GzipCompression specifies if gzip compression is enabled. + GzipCompression bool `mapstructure:"gzip-compression"` +} + +// AddFlags registers persistent flags. +func AddFlags(flags *pflag.FlagSet) { + flags.Bool(esEnabled, false, "Determines whether ES output is enabled") + flags.StringSlice(esServers, []string{"http://127.0.0.1:9200"}, "Contains a comma separated list of Elasticsearch instances that comprise the cluster") + flags.Duration(esTimeout, time.Second*5, "Specifies the output connection timeout") + flags.Duration(esFlushPeriod, time.Second, "Specifies when to flush the bulk at the end of the given interval") + flags.Int(esBulkWorkers, 1, "Represents the number of workers that commit docs to Elasticsearch") + flags.Bool(esHealthcheck, true, "Enables/disables nodes health checking") + flags.Duration(esHealthcheckInterval, time.Second*10, "Specifies the interval for checking if the Elasticsearch nodes are available") + flags.Duration(esHealthcheckTimeout, time.Second*5, "Specifies the timeout for periodic health checks") + flags.String(esUsername, "", "Identifies the user name for the basic HTTP authentication") + flags.String(esPassword, "", "Specifies the password for the basic HTTP authentication") + flags.Bool(esSniff, false, "Enables the discovery of all Elasticsearch nodes in the cluster. This avoids populating the list of available Elasticsearch nodes") + flags.Bool(esTraceLog, false, "Determines if the Elasticsearch trace log is enabled. Useful for troubleshooting") + flags.String(esTemplateName, "fibratus", "Specifies the name of the index template") + flags.String(esIndexName, "fibratus", "Represents the target index for kernel events. It allows time specifiers to create indices per time frame") + flags.String(esTemplateConfig, "", "Contains the full JSON body of the index template") + flags.Bool(esGzipCompression, false, "Specifies if gzip compression is enabled") +} diff --git a/pkg/outputs/elasticsearch/elasticsearch.go b/pkg/outputs/elasticsearch/elasticsearch.go new file mode 100644 index 000000000..c9a9db1f2 --- /dev/null +++ b/pkg/outputs/elasticsearch/elasticsearch.go @@ -0,0 +1,201 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package elasticsearch + +import ( + "context" + "encoding/json" + "expvar" + "fmt" + "github.com/hashicorp/go-version" + "github.com/olivere/elastic/v7" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/outputs" + "github.com/rabbitstack/fibratus/pkg/util/tls" + log "github.com/sirupsen/logrus" + "net/http" +) + +// minElasticVersion is the minimal supported Elasticsearch version +var minElasticVersion, _ = version.NewVersion("5.5") + +var ( + // totalBulkedDocs contains the number of total bulked docs + totalBulkedDocs = expvar.NewInt("elasticsearch.total.bulked.docs") + // committedDocs counts the number of docs commited to Elasticsearch + committedDocs = expvar.NewInt("elasticsearch.committed.docs") + // failedDocs counts the number of docs that failed to commit to Elasticsearch + failedDocs = expvar.NewInt("elasticsearch.failed.docs") +) + +type elasticsearch struct { + client *elastic.Client + bulkProcessor *elastic.BulkProcessor + config Config + index index +} + +type logger struct{} + +func (l logger) Printf(format string, v ...interface{}) { + log.Infof(format, v...) +} + +func init() { + outputs.Register(outputs.Elasticsearch, initElastic) +} + +func initElastic(config outputs.Config) (outputs.OutputGroup, error) { + cfg, ok := config.Output.(Config) + if !ok { + return outputs.Fail(outputs.ErrInvalidConfig(outputs.Elasticsearch, config.Output)) + } + + es := &elasticsearch{config: cfg, index: index{config: cfg}} + + return outputs.Success(es), nil +} + +func (e *elasticsearch) Connect() error { + var opts []elastic.ClientOptionFunc + var client *elastic.Client + var err error + + // setup a new HTTP client with optional TLS transport + tlsConfig, err := tls.MakeConfig(e.config.TLSCert, e.config.TLSKey, e.config.TLSCA, e.config.TLSInsecureSkipVerify) + if err != nil { + return fmt.Errorf("invalid TLS config: %v", err) + } + httpClient := &http.Client{ + Timeout: e.config.Timeout, + Transport: &http.Transport{TLSClientConfig: tlsConfig}, + } + + opts = append( + opts, + elastic.SetSniff(e.config.Sniff), + elastic.SetHttpClient(httpClient), + elastic.SetURL(e.config.Servers...), + elastic.SetGzip(e.config.GzipCompression), + elastic.SetHealthcheck(e.config.Healthcheck), + elastic.SetHealthcheckTimeout(e.config.HealthCheckTimeout), + elastic.SetHealthcheckInterval(e.config.HealthCheckInterval), + ) + + if e.config.Username != "" && e.config.Password != "" { + opts = append( + opts, + elastic.SetBasicAuth(e.config.Username, e.config.Password), + ) + } + if e.config.TraceLog { + opts = append(opts, elastic.SetTraceLog(&logger{})) + } + + client, err = elastic.NewClient(opts...) + if err != nil { + return err + } + + ver, err := client.ElasticsearchVersion(e.config.Servers[0]) + if err != nil { + return fmt.Errorf("unable to fetch Elasticsearch version: %v", err) + } + + v, err := version.NewVersion(ver) + if err != nil { + return fmt.Errorf("unable to parse Elasticsearch version %s: %v", ver, err) + } + if v.LessThan(minElasticVersion) { + return fmt.Errorf("required at least Elasticsearch %s but found version %s", minElasticVersion.String(), ver) + } + + e.client = client + e.index.client = client + + bulkProcessor, err := client.BulkProcessor(). + After(func(executionId int64, requests []elastic.BulkableRequest, response *elastic.BulkResponse, err error) { + if err != nil { + log.Errorf("failed to execute bulk: %s", err) + return + } + + if response.Errors { + log.Errorf("failed to insert %d documents", len(response.Failed())) + for i, fail := range response.Failed() { + failedDocs.Add(1) + log.Errorf("failed to insert document %d: %v", i, fail.Error) + } + return + } + committedDocs.Add(int64(len(requests))) + }). + FlushInterval(e.config.FlushPeriod). + Workers(e.config.BulkWorkers). + Do(context.Background()) + if err != nil { + return fmt.Errorf("couldn't create Elasticsearch bulk processor: %v", err) + } + + err = e.index.putTemplate() + if err != nil { + return err + } + + err = bulkProcessor.Start(context.Background()) + if err != nil { + return err + } + + e.bulkProcessor = bulkProcessor + + log.Infof("established connection to Elasticsearch server(s): %v", e.config.Servers) + + return nil +} + +func (e *elasticsearch) Publish(batch *kevent.Batch) error { + for _, kevt := range batch.Events { + indexName := e.index.getName(kevt) + // create the bulk index request for each event in the batch. + // We already have a valid JSON body, so just pass the raw + // JSON message as request document + e.bulkProcessor.Add(newBulkIndexRequest(indexName, kevt)) + totalBulkedDocs.Add(1) + } + batch.Release() + + return nil +} + +func newBulkIndexRequest(indexName string, kevt *kevent.Kevent) *elastic.BulkIndexRequest { + kjson := kevt.MarshalJSON() + return elastic.NewBulkIndexRequest().Index(indexName).Doc(json.RawMessage(kjson)) +} + +func (e *elasticsearch) Close() error { + if e.bulkProcessor != nil { + // commit outstanding requests before shutdown + if err := e.bulkProcessor.Flush(); err != nil { + return err + } + return e.bulkProcessor.Close() + } + return nil +} diff --git a/pkg/outputs/elasticsearch/elasticsearch_test.go b/pkg/outputs/elasticsearch/elasticsearch_test.go new file mode 100644 index 000000000..fbe4fe10d --- /dev/null +++ b/pkg/outputs/elasticsearch/elasticsearch_test.go @@ -0,0 +1,355 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package elasticsearch + +import ( + "bytes" + "encoding/json" + "github.com/olivere/elastic/v7" + htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" + shandle "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +func TestElasticsearchConnect(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ping := elastic.PingResult{ + Name: "es", + } + ping.Version.Number = "5.5.2" + resp, err := json.Marshal(&ping) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + w.Write([]byte(resp)) + })) + defer srv.Close() + + es := &elasticsearch{config: Config{Servers: []string{srv.URL}, Healthcheck: false}} + + require.NoError(t, es.Connect()) +} + +func TestElasticsearchConnectUnsupportedVersion(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ping := elastic.PingResult{ + Name: "es", + } + ping.Version.Number = "2.4.6" + resp, err := json.Marshal(&ping) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + w.Write([]byte(resp)) + })) + defer srv.Close() + + es := &elasticsearch{config: Config{Servers: []string{srv.URL}, Healthcheck: false}} + + require.Error(t, es.Connect()) +} + +func TestElasticsearchPublish(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.URL.Path, "_bulk") { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer r.Body.Close() + // check we have the correct index name + assert.True(t, bytes.Contains(body, []byte("fibratus-2018-03"))) + // check kevent name is present + assert.True(t, bytes.Contains(body, []byte("CreateFile"))) + + // create the bulk response + response := elastic.BulkResponse{ + Took: 1, + Errors: false, + Items: []map[string]*elastic.BulkResponseItem{}, + } + resp, err := json.Marshal(&response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + w.Write(resp) + } else { + ping := elastic.PingResult{ + Name: "es", + } + ping.Version.Number = "5.5.2" + resp, err := json.Marshal(&ping) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + w.Write(resp) + } + })) + defer srv.Close() + + kevent.SerializeHandles = true + + cfg := Config{ + Servers: []string{srv.URL}, + Healthcheck: false, + FlushPeriod: time.Millisecond * 250, + IndexName: "fibratus-%Y-%d", + TemplateName: "fibratus", + } + + es := &elasticsearch{ + config: cfg, + index: index{config: cfg}, + } + + require.NoError(t, es.Connect()) + + require.NoError(t, es.Publish(getBatch())) + + time.Sleep(time.Millisecond * 450) + + assert.Equal(t, int64(3), committedDocs.Value()) + assert.Equal(t, int64(0), failedDocs.Value()) +} + +func getBatch() *kevent.Batch { + ts, _ := time.Parse(time.RFC3339, "2018-05-03T15:04:05.323Z") + + kevt := &kevent.Kevent{ + Type: ktypes.CreateFile, + Tid: 2484, + PID: 859, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: ts, + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: kevent.Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, + kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + }, + Metadata: map[string]string{"foo": "bar", "fooz": "baarz"}, + PS: &pstypes.PS{ + PID: 2436, + Ppid: 6304, + Name: "firefox.exe", + Exe: `C:\Program Files\Mozilla Firefox\firefox.exe`, + Comm: `C:\Program Files\Mozilla Firefox\firefox.exe -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, + Cwd: `C:\Program Files\Mozilla Firefox\`, + SID: "archrabbit\\SYSTEM", + Args: []string{"-contentproc", `--channel=6304.3.1055809391\1014207667`, "-childID", "1", "-isForBrowser", "-prefsHandle", "2584", "-prefMapHandle", "2580", "-prefsLen", "70", "-prefMapSize", "216993", "-parentBuildID"}, + SessionID: 4, + Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, + Threads: map[uint32]pstypes.Thread{ + 3453: {Tid: 3453, Entrypoint: kparams.Hex("0x7ffe2557ff80"), IOPrio: 2, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + 3455: {Tid: 3455, Entrypoint: kparams.Hex("0x5efe2557ff80"), IOPrio: 3, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + }, + Handles: []htypes.Handle{ + {Num: shandle.Handle(0xffffd105e9baaf70), + Name: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, + Type: "Key", + Object: 777488883434455544, + Pid: uint32(1023), + }, + { + Num: shandle.Handle(0xffffd105e9adaf70), + Name: `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, + Type: "ALPC Port", + Pid: uint32(1023), + MD: &htypes.AlpcPortInfo{ + Seqno: 1, + Context: 0x0, + Flags: 0x0, + }, + Object: 457488883434455544, + }, + { + Num: shandle.Handle(0xeaffd105e9adaf30), + Name: `C:\Users\bunny`, + Type: "File", + Pid: uint32(1023), + MD: &htypes.FileInfo{ + IsDirectory: true, + }, + Object: 357488883434455544, + }, + }, + }, + } + + kevt1 := &kevent.Kevent{ + Type: ktypes.CreateFile, + Tid: 2484, + PID: 459, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: ts, + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: kevent.Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, + kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + }, + Metadata: map[string]string{"foo": "bar", "fooz": "baarz"}, + PS: &pstypes.PS{ + PID: 2436, + Ppid: 6304, + Name: "firefox.exe", + Exe: `C:\Program Files\Mozilla Firefox\firefox.exe`, + Comm: `C:\Program Files\Mozilla Firefox\firefox.exe -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, + Cwd: `C:\Program Files\Mozilla Firefox\`, + SID: "archrabbit\\SYSTEM", + Args: []string{"-contentproc", `--channel=6304.3.1055809391\1014207667`, "-childID", "1", "-isForBrowser", "-prefsHandle", "2584", "-prefMapHandle", "2580", "-prefsLen", "70", "-prefMapSize", "216993", "-parentBuildID"}, + SessionID: 4, + Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, + Threads: map[uint32]pstypes.Thread{ + 3453: {Tid: 3453, Entrypoint: kparams.Hex("0x7ffe2557ff80"), IOPrio: 2, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + 3455: {Tid: 3455, Entrypoint: kparams.Hex("0x5efe2557ff80"), IOPrio: 3, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + }, + Handles: []htypes.Handle{ + {Num: shandle.Handle(0xffffd105e9baaf70), + Name: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, + Type: "Key", + Object: 777488883434455544, + Pid: uint32(1023), + }, + { + Num: shandle.Handle(0xffffd105e9adaf70), + Name: `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, + Type: "ALPC Port", + Pid: uint32(1023), + MD: &htypes.AlpcPortInfo{ + Seqno: 1, + Context: 0x0, + Flags: 0x0, + }, + Object: 457488883434455544, + }, + { + Num: shandle.Handle(0xeaffd105e9adaf30), + Name: `C:\Users\bunny`, + Type: "File", + Pid: uint32(1023), + MD: &htypes.FileInfo{ + IsDirectory: true, + }, + Object: 357488883434455544, + }, + }, + }, + } + + kevt2 := &kevent.Kevent{ + Type: ktypes.CreateFile, + Tid: 2484, + PID: 829, + CPU: 1, + Seq: 2, + Name: "CreateFile", + Timestamp: ts, + Category: ktypes.File, + Host: "archrabbit", + Description: "Creates or opens a new file, directory, I/O device, pipe, console", + Kparams: kevent.Kparams{ + kparams.FileObject: {Name: kparams.FileObject, Type: kparams.Uint64, Value: uint64(12456738026482168384)}, + kparams.FileName: {Name: kparams.FileName, Type: kparams.UnicodeString, Value: "\\Device\\HarddiskVolume2\\Windows\\system32\\user32.dll"}, + kparams.FileType: {Name: kparams.FileType, Type: kparams.AnsiString, Value: "file"}, + kparams.FileOperation: {Name: kparams.FileOperation, Type: kparams.AnsiString, Value: "open"}, + kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Int8, Value: int8(2)}, + kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(2)}, + }, + Metadata: map[string]string{"foo": "bar", "fooz": "baarz"}, + PS: &pstypes.PS{ + PID: 829, + Ppid: 6304, + Name: "firefox.exe", + Exe: `C:\Program Files\Mozilla Firefox\firefox.exe`, + Comm: `C:\Program Files\Mozilla Firefox\firefox.exe -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, + Cwd: `C:\Program Files\Mozilla Firefox\`, + SID: "archrabbit\\SYSTEM", + Args: []string{"-contentproc", `--channel=6304.3.1055809391\1014207667`, "-childID", "1", "-isForBrowser", "-prefsHandle", "2584", "-prefMapHandle", "2580", "-prefsLen", "70", "-prefMapSize", "216993", "-parentBuildID"}, + SessionID: 4, + Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, + Threads: map[uint32]pstypes.Thread{ + 3453: {Tid: 3453, Entrypoint: kparams.Hex("0x7ffe2557ff80"), IOPrio: 2, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + 3455: {Tid: 3455, Entrypoint: kparams.Hex("0x5efe2557ff80"), IOPrio: 3, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + }, + Handles: []htypes.Handle{ + {Num: shandle.Handle(0xffffd105e9baaf70), + Name: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, + Type: "Key", + Object: 777488883434455544, + Pid: uint32(1023), + }, + { + Num: shandle.Handle(0xffffd105e9adaf70), + Name: `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, + Type: "ALPC Port", + Pid: uint32(1023), + MD: &htypes.AlpcPortInfo{ + Seqno: 1, + Context: 0x0, + Flags: 0x0, + }, + Object: 457488883434455544, + }, + { + Num: shandle.Handle(0xeaffd105e9adaf30), + Name: `C:\Users\bunny`, + Type: "File", + Pid: uint32(1023), + MD: &htypes.FileInfo{ + IsDirectory: true, + }, + Object: 357488883434455544, + }, + }, + }, + } + + return kevent.NewBatch(kevt, kevt1, kevt2) +} diff --git a/pkg/outputs/elasticsearch/index.go b/pkg/outputs/elasticsearch/index.go new file mode 100644 index 000000000..698032c52 --- /dev/null +++ b/pkg/outputs/elasticsearch/index.go @@ -0,0 +1,95 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package elasticsearch + +import ( + "bytes" + "context" + "fmt" + "github.com/olivere/elastic/v7" + "github.com/rabbitstack/fibratus/pkg/kevent" + "html/template" + "strings" + "time" +) + +type index struct { + config Config + client *elastic.Client +} + +// putTemplate creates the index template. +func (i index) putTemplate() error { + if i.config.TemplateName == "" { + return nil + } + // get the index pattern for the template + indexPattern := i.config.IndexName + if strings.Contains(indexPattern, "%") { + indexPattern = indexPattern[0:strings.Index(indexPattern, "%")] + } + + var b bytes.Buffer + if i.config.TemplateConfig != "" { + b.WriteString(i.config.TemplateConfig) + } else { + // expand the Go template + tmpl := template.Must(template.New("template").Parse(indexTemplate)) + err := tmpl.Execute(&b, templateInfo{IndexPattern: indexPattern + "*"}) + if err != nil { + return err + } + } + + ctx := context.Background() + + exists, err := i.client.IndexTemplateExists(i.config.TemplateName).Do(ctx) + if err != nil { + return fmt.Errorf("unable to check the existence of the %q template: %v", i.config.TemplateName, err) + } + if exists { + return nil + } + // create index template + _, err = i.client.IndexPutTemplate(i.config.TemplateName).BodyJson(b.String()).Do(ctx) + if err != nil { + return fmt.Errorf("unable to create index for the %q template: %v", i.config.TemplateName, err) + } + + return nil +} + +// getName creates an index name by replacing specifiers to create time frame indices. If no time specifiers are +// used this method returns a fixed index name. +func (i index) getName(kevt *kevent.Kevent) string { + indexName := i.config.IndexName + if !strings.Contains(indexName, "%") { + return indexName + } + return i.replace(kevt.Timestamp) +} + +func (i index) replace(timestamp time.Time) string { + return strings.NewReplacer( + "%Y", timestamp.UTC().Format("2006"), + "%y", timestamp.UTC().Format("06"), + "%m", timestamp.UTC().Format("01"), + "%d", timestamp.UTC().Format("02"), + "%H", timestamp.UTC().Format("15")).Replace(i.config.IndexName) +} diff --git a/pkg/outputs/elasticsearch/index_test.go b/pkg/outputs/elasticsearch/index_test.go new file mode 100644 index 000000000..cb41f284b --- /dev/null +++ b/pkg/outputs/elasticsearch/index_test.go @@ -0,0 +1,50 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package elasticsearch + +import ( + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestProduceIndexName(t *testing.T) { + i := index{config: Config{IndexName: "fibratus-%Y-%m"}} + + ts, _ := time.Parse(time.RFC3339, "2011-05-03T15:04:05.323Z") + + indexName := i.getName(&kevent.Kevent{Timestamp: ts}) + assert.Equal(t, "fibratus-2011-05", indexName) + + i = index{config: Config{IndexName: "fibratus-%y-%d"}} + + indexName = i.getName(&kevent.Kevent{Timestamp: ts}) + assert.Equal(t, "fibratus-11-03", indexName) + + i = index{config: Config{IndexName: "fibratus-%d-%H"}} + + indexName = i.getName(&kevent.Kevent{Timestamp: ts}) + assert.Equal(t, "fibratus-03-15", indexName) + + i = index{config: Config{IndexName: "fibratus-events"}} + + indexName = i.getName(&kevent.Kevent{Timestamp: ts}) + assert.Equal(t, "fibratus-events", indexName) +} diff --git a/pkg/outputs/elasticsearch/template.go b/pkg/outputs/elasticsearch/template.go new file mode 100644 index 000000000..0d7db9f61 --- /dev/null +++ b/pkg/outputs/elasticsearch/template.go @@ -0,0 +1,74 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package elasticsearch + +type templateInfo struct { + IndexPattern string +} + +const indexTemplate = ` +{ + "index_patterns": [ "{{ .IndexPattern }}" ], + "settings": { + "index": { + "refresh_interval": "5s", + "number_of_shards": 1, + "number_of_replicas": 1 + } + }, + "mappings": { + "properties": { + "seq": { "type": "long" }, + "pid": { "type": "long" }, + "tid": { "type": "long" }, + "cpu": { "type": "short" }, + + "name": { "type": "keyword" }, + "category": { "type": "keyword" }, + "description": { "type": "text" }, + "host": { "type": "keyword" }, + + "timestamp": { "type": "date" }, + + "kparams": { + "type": "nested", + "properties": { + "dip": { "type": "ip" }, + "sip": { "type": "ip" } + } + }, + + "ps": { + "type": "nested", + "properties": { + "pid": { "type": "long" }, + "ppid": { "type": "long" }, + "name": { "type": "keyword" }, + "comm": { "type": "text" }, + "exe": { "type": "text" }, + "cwd": { "type": "text" }, + "sid": { "type": "keyword" }, + "sessionid": { "type": "short" } + } + } + + } + } +} +` diff --git a/pkg/outputs/null/config.go b/pkg/outputs/null/config.go new file mode 100644 index 000000000..51f5c6236 --- /dev/null +++ b/pkg/outputs/null/config.go @@ -0,0 +1,22 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package null + +// Config contains preferences for the null output. +type Config struct{} diff --git a/pkg/outputs/null/null.go b/pkg/outputs/null/null.go new file mode 100644 index 000000000..cc7a83eb4 --- /dev/null +++ b/pkg/outputs/null/null.go @@ -0,0 +1,46 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package null + +import ( + "expvar" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/outputs" +) + +var blackholeEventsCount = expvar.NewInt("output.null.blackhole.events") + +// null output devours kernel events the same way a black hole swallows the light +type null struct{} + +func init() { + outputs.Register(outputs.Null, initNull) +} + +func initNull(config outputs.Config) (outputs.OutputGroup, error) { + return outputs.Success(&null{}), nil +} + +func (null) Close() error { return nil } +func (null) Connect() error { return nil } +func (null) Publish(batch *kevent.Batch) error { + blackholeEventsCount.Add(batch.Len()) + batch.Release() + return nil +} diff --git a/pkg/outputs/outputs.go b/pkg/outputs/outputs.go new file mode 100644 index 000000000..a0ac4d5b9 --- /dev/null +++ b/pkg/outputs/outputs.go @@ -0,0 +1,94 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package outputs + +import ( + "fmt" + "reflect" + "strings" +) + +var ( + outputs = map[Type]Factory{} + // ErrInvalidConfig signals an invalid configuration input + ErrInvalidConfig = func(name Type, c interface{}) error { + return fmt.Errorf("invalid config for %q output. Got type %v instead of %s.Config", name, reflect.TypeOf(c), strings.ToLower(name.String())) + } +) + +type Factory func(config Config) (OutputGroup, error) + +// Type is the alias for the output type. +type Type uint8 + +const ( + // Console represents the default terminal output. + Console Type = iota + AMQP + Elasticsearch + Null +) + +// String returns the string representation of the output type. +func (t Type) String() string { + switch t { + case Console: + return "console" + case AMQP: + return "amqp" + case Elasticsearch: + return "elasticsearch" + case Null: + return "null" + default: + return "unknown" + } +} + +type OutputGroup struct { + Clients []Client +} + +func Success(clients ...Client) OutputGroup { + return OutputGroup{Clients: clients} +} + +func Fail(err error) (OutputGroup, error) { + return OutputGroup{}, err +} + +func Register(typ Type, factory Factory) { + if _, ok := outputs[typ]; ok { + panic(fmt.Sprintf("output %q is already registered", typ)) + } + outputs[typ] = factory +} + +// FindFactory locates the output factory. +func FindFactory(typ Type) Factory { + return outputs[typ] +} + +func Load(typ Type, config Config) (OutputGroup, error) { + factory := FindFactory(typ) + if factory == nil { + return OutputGroup{}, fmt.Errorf("output %q not availaible in the factory", typ) + } + return factory(config) +} diff --git a/pkg/pe/config.go b/pkg/pe/config.go new file mode 100644 index 000000000..c29c016a3 --- /dev/null +++ b/pkg/pe/config.go @@ -0,0 +1,71 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package pe + +import ( + "github.com/spf13/pflag" + "github.com/spf13/viper" + "path/filepath" + "strings" +) + +const ( + enabled = "pe.enabled" + readResources = "pe.read-resources" + readSymbols = "pe.read-symbols" + readSections = "pe.read-sections" + excludedImages = "pe.excluded-images" +) + +// Config stores the preferences that dictate the behaviour of the PE reader. +type Config struct { + Enabled bool `json:"pe.enabled" yaml:"pe.enabled"` + ReadResources bool `json:"pe.read-resources" yaml:"pe.read-resources"` + ReadSymbols bool `json:"pe.read-symbols" yaml:"pe.read-symbols"` + ReadSections bool `json:"pe.read-sections" yaml:"pe.read-sections"` + ExcludedImages []string `json:"pe.excluded-images" yaml:"pe.excluded-images"` +} + +// InitFromViper initializes PE config from Viper. +func (c *Config) InitFromViper(v *viper.Viper) { + c.Enabled = v.GetBool(enabled) + c.ReadResources = v.GetBool(readResources) + c.ReadSymbols = v.GetBool(readSymbols) + c.ReadSections = v.GetBool(readSections) + c.ExcludedImages = v.GetStringSlice(excludedImages) +} + +// AddFlags registers persistent flags. +func AddFlags(flags *pflag.FlagSet) { + flags.Bool(enabled, false, "Specifies if PE metadata is fetched from the process' image file") + flags.Bool(readResources, false, "Determines if resources are read from the PE resource directory") + flags.Bool(readSymbols, false, "Indicates if symbols are read from the PE") + flags.Bool(readSections, false, "Indicates if full section inspection is allowed") + flags.StringSlice(excludedImages, []string{}, "Contains a list of comma-separated images names that are excluded from PE parsing") +} + +// ShouldSkipProcess determines whether the specified filename name is ignored by PE reader. +func (c Config) shouldSkipImage(filename string) bool { + for _, img := range c.ExcludedImages { + if strings.ToLower(img) == strings.ToLower(filepath.Base(filename)) { + return true + } + } + return false +} diff --git a/pkg/pe/doc.go b/pkg/pe/doc.go new file mode 100644 index 000000000..9bd81f93e --- /dev/null +++ b/pkg/pe/doc.go @@ -0,0 +1,21 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package pe contains different facilities for dealing with Portable Executable specifics and digging out valuable insights +// from it. +package pe diff --git a/pkg/pe/entropy.go b/pkg/pe/entropy.go new file mode 100644 index 000000000..3c892aa7f --- /dev/null +++ b/pkg/pe/entropy.go @@ -0,0 +1,41 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package pe + +import "math" + +// entropy calculates the entropy of the PE section's data. This function relies +// on Shannon Entropy formula to calculate the entropy. High entropy scores mean +// that there is a high variety of frequency over data located in sections. +func entropy(data []byte) float64 { + entropy := 0.0 + frq := make(map[byte]int, len(data)) + + // get the frequency of each rune + for _, i := range data { + frq[i]++ + } + + for _, value := range frq { + k := float64(value) / float64(len(data)) + entropy -= k * math.Log2(k) + } + + return entropy +} diff --git a/pkg/pe/marshaller.go b/pkg/pe/marshaller.go new file mode 100644 index 000000000..01434f9bc --- /dev/null +++ b/pkg/pe/marshaller.go @@ -0,0 +1,223 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package pe + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/util/bytes" + "math" + "time" + "unsafe" +) + +// Marshal dumps the PE metadata to binary stream. +func (pe *PE) Marshal() []byte { + b := make([]byte, 0) + + // number of sections/symbols + b = append(b, bytes.WriteUint16(pe.NumberOfSections)...) + b = append(b, bytes.WriteUint32(pe.NumberOfSymbols)...) + + // image base + b = append(b, bytes.WriteUint16(uint16(len(pe.ImageBase)))...) + b = append(b, pe.ImageBase...) + + // entry point + b = append(b, bytes.WriteUint16(uint16(len(pe.EntryPoint)))...) + b = append(b, pe.EntryPoint...) + + // link time + linkTime := make([]byte, 0) + linkTime = pe.LinkTime.AppendFormat(linkTime, time.RFC3339Nano) + b = append(b, bytes.WriteUint16(uint16(len(linkTime)))...) + b = append(b, linkTime...) + + // sections + b = append(b, bytes.WriteUint16(uint16(len(pe.Sections)))...) + for _, sec := range pe.Sections { + // size + b = append(b, bytes.WriteUint32(sec.Size)...) + // entropy + b = append(b, bytes.WriteUint64(math.Float64bits(sec.Entropy))...) + // name + b = append(b, bytes.WriteUint16(uint16(len(sec.Name)))...) + b = append(b, sec.Name...) + // md5 + b = append(b, bytes.WriteUint16(uint16(len(sec.Md5)))...) + b = append(b, sec.Md5...) + } + + // symbols + b = append(b, bytes.WriteUint16(uint16(len(pe.Symbols)))...) + for _, sym := range pe.Symbols { + b = append(b, bytes.WriteUint16(uint16(len(sym)))...) + b = append(b, sym...) + } + + // imports + b = append(b, bytes.WriteUint16(uint16(len(pe.Imports)))...) + for _, imp := range pe.Imports { + b = append(b, bytes.WriteUint16(uint16(len(imp)))...) + b = append(b, imp...) + } + + // version resources + b = append(b, bytes.WriteUint16(uint16(len(pe.VersionResources)))...) + for k, v := range pe.VersionResources { + b = append(b, bytes.WriteUint16(uint16(len(k)))...) + b = append(b, k...) + b = append(b, bytes.WriteUint16(uint16(len(v)))...) + b = append(b, v...) + } + + return b +} + +// Unmarshal recovers the PE metadata from the byte stream. +func (pe *PE) Unmarshal(b []byte) error { + if len(b) < 6 { + return fmt.Errorf("expected at least 6 bytes but got %d bytes", len(b)) + } + + pe.NumberOfSections = bytes.ReadUint16(b[0:]) + pe.NumberOfSymbols = bytes.ReadUint32(b[2:]) + + // image base + l := bytes.ReadUint16(b[6:]) + buf := b[8:] + offset := uint32(l) + pe.ImageBase = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + + // entry point + l = bytes.ReadUint16(b[8+offset:]) + buf = b[10+offset:] + offset += uint32(l) + pe.EntryPoint = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + + // link time + l = bytes.ReadUint16(b[10+offset:]) + buf = b[12+offset:] + offset += uint32(l) + if len(buf) > 0 { + pe.LinkTime, _ = time.Parse(time.RFC3339Nano, string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l])) + } + + // read sections + nsections := bytes.ReadUint16(b[12+offset:]) + var soffset uint32 + + for nsec := 0; nsec < int(nsections); nsec++ { + // section size + size := bytes.ReadUint32(b[14+offset+soffset:]) + // entropy + entropy := bytes.ReadUint64(b[18+offset+soffset:]) + + // section name + l := bytes.ReadUint16(b[26+offset+soffset:]) + buf := b[28+offset+soffset:] + soffset += uint32(l) + name := string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + + // section md5 hash + l = bytes.ReadUint16(b[28+offset+soffset:]) + buf = b[30+offset+soffset:] + soffset += uint32(l) + md5 := string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + + pe.Sections = append(pe.Sections, + Sec{ + Name: name, + Size: size, + Entropy: math.Float64frombits(entropy), + Md5: md5, + }, + ) + + // increment the offset by summing the byte length of the size + entropy, and the section name length + md5 length encoded as uint16 values + soffset += 4 + 8 + 2 + 2 + } + + offset += soffset + + // read symbols + nsyms := bytes.ReadUint16(b[14+offset:]) + var syoffset uint32 + + for nsym := 0; nsym < int(nsyms); nsym++ { + l := bytes.ReadUint16(b[16+offset+syoffset:]) + buf := b[18+offset+syoffset:] + pe.Symbols = append(pe.Symbols, string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l])) + syoffset += uint32(l + 2) + } + offset += syoffset + + // read imports + nimports := bytes.ReadUint16(b[16+offset:]) + var ioffset uint32 + + for nimp := 0; nimp < int(nimports); nimp++ { + l := bytes.ReadUint16(b[18+offset+ioffset:]) + buf := b[20+offset+ioffset:] + pe.Imports = append(pe.Imports, string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l])) + ioffset += uint32(l + 2) + } + + offset += ioffset + + // read version resources + nresources := bytes.ReadUint16(b[18+offset:]) + var roffset uint32 + + for nres := 0; nres < int(nresources); nres++ { + // read key + klen := bytes.ReadUint16(b[20+offset+roffset:]) + buf := b[22+offset+roffset:] + key := string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:klen:klen]) + // read value + vlen := bytes.ReadUint16(b[22+offset+uint32(klen)+roffset:]) + buf = b[24+offset+uint32(klen)+roffset:] + if vlen == 0 { + roffset += uint32(klen) + 4 + continue + } + value := string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:vlen:vlen]) + // increment the offset by the length of the key + length value + size of uint16 * 2 + // that corresponds to byte patterns storing the lengths of the keys/values + roffset += uint32(klen) + uint32(vlen) + 4 + if key != "" { + pe.VersionResources[key] = value + } + } + + return nil +} + +// NewFromKcap restores the PE metadata from the byte stream. +func NewFromKcap(b []byte) (*PE, error) { + pe := &PE{ + Sections: make([]Sec, 0), + Symbols: make([]string, 0), + Imports: make([]string, 0), + VersionResources: make(map[string]string), + } + if err := pe.Unmarshal(b); err != nil { + return nil, err + } + return pe, nil +} diff --git a/pkg/pe/marshaller_test.go b/pkg/pe/marshaller_test.go new file mode 100644 index 000000000..c5e107c4d --- /dev/null +++ b/pkg/pe/marshaller_test.go @@ -0,0 +1,85 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package pe + +import ( + "github.com/stretchr/testify/assert" + + "github.com/stretchr/testify/require" + "testing" + "time" +) + +func TestMetadataMarshal(t *testing.T) { + + now := time.Now() + + pe := &PE{ + NumberOfSections: 7, + NumberOfSymbols: 10, + EntryPoint: "20110", + ImageBase: "140000000", + LinkTime: now, + Sections: []Sec{ + {Name: ".text", Size: 132608, Entropy: 6.368381, Md5: "db23dce3911a42e987041d98abd4f7cd"}, + {Name: ".rdata", Size: 35840, Entropy: 5.996976, Md5: "ffa5c960b421ca9887e54966588e97e8"}, + }, + Symbols: []string{"SelectObject", "GetTextFaceW", "EnumFontsW", "TextOutW", "GetProcessHeap"}, + Imports: []string{"GDI32.dll", "USER32.dll", "msvcrt.dll", "api-ms-win-core-libraryloader-l1-2-0.dl"}, + VersionResources: map[string]string{"CompanyName": "Microsoft Corporation", "FileDescription": "Notepad", "FileVersion": "10.0.18362.693"}, + } + + b := pe.Marshal() + + newPE := &PE{VersionResources: make(map[string]string)} + err := newPE.Unmarshal(b) + require.NoError(t, err) + + assert.Equal(t, uint16(7), newPE.NumberOfSections) + assert.Equal(t, uint32(10), newPE.NumberOfSymbols) + assert.Equal(t, "20110", newPE.EntryPoint) + assert.Equal(t, "140000000", newPE.ImageBase) + + assert.Equal(t, now.Day(), newPE.LinkTime.Day()) + assert.Equal(t, now.Minute(), newPE.LinkTime.Minute()) + + assert.Len(t, newPE.Sections, 2) + + textSection := newPE.Sections[0] + assert.Equal(t, ".text", textSection.Name) + assert.Equal(t, uint32(132608), textSection.Size) + assert.Equal(t, 6.368381, textSection.Entropy) + assert.Equal(t, "db23dce3911a42e987041d98abd4f7cd", textSection.Md5) + + assert.Len(t, newPE.Symbols, 5) + assert.Contains(t, newPE.Symbols, "SelectObject") + assert.Contains(t, newPE.Symbols, "TextOutW") + + assert.Len(t, newPE.Imports, 4) + assert.Contains(t, newPE.Imports, "GDI32.dll") + assert.Contains(t, newPE.Imports, "msvcrt.dll") + + assert.Len(t, newPE.VersionResources, 3) + assert.Contains(t, newPE.VersionResources, "CompanyName") + assert.Contains(t, newPE.VersionResources, "FileVersion") + + assert.Equal(t, "10.0.18362.693", newPE.VersionResources["FileVersion"]) + assert.Equal(t, "Microsoft Corporation", newPE.VersionResources["CompanyName"]) + assert.Equal(t, "Notepad", newPE.VersionResources["FileDescription"]) +} diff --git a/pkg/pe/reader.go b/pkg/pe/reader.go new file mode 100644 index 000000000..ff898d628 --- /dev/null +++ b/pkg/pe/reader.go @@ -0,0 +1,193 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package pe + +import ( + "bytes" + "debug/pe" + "expvar" + log "github.com/sirupsen/logrus" + "golang.org/x/text/encoding/unicode" + "io" + "os" + "strconv" + "strings" + "sync" + "time" +) + +var ( + peSkippedImages = expvar.NewInt("pe.skipped.images") + peReaderTimeouts = expvar.NewInt("pe.reader.timeouts") +) + +// Reader is the interface for PE (Portable Executable) format metadata parsing. The stdlib debug/pe package underpins +// the core functionality of the reader, but additionally, it provides numerous methods for reading resources, strings, +// IAT directories and other information that is not offered by the standard library package. +type Reader interface { + // Read is the main method that reads the PE metadata for the specified image file. + Read(filename string) (*PE, error) + // FindSectionByRVA gets the section containing the given address. + FindSectionByRVA(rva uint32) (*pe.Section, error) + // FindOffsetByRVA returns the file offset that maps to the given RVA. + FindOffsetByRVA(rva uint32) (int64, error) +} + +type reader struct { + f *os.File + sections []*pe.Section + oh interface{} + config Config +} + +// NewReader builds a new instance of the PE reader. +func NewReader(config Config) Reader { + return &reader{config: config} +} + +func (r *reader) Read(filename string) (*PE, error) { + if !r.config.Enabled { + return nil, nil + } + if r.config.shouldSkipImage(filename) { + peSkippedImages.Add(1) + return nil, nil + } + f, err := os.Open(filename) + if err != nil { + return nil, err + } + r.f = f + defer r.f.Close() + pefile, err := pe.NewFile(f) + if err != nil { + return nil, err + } + r.sections = pefile.Sections + r.oh = pefile.OptionalHeader + + // link time in PE header is represented as the number of seconds since January 1, 1970 + linkTime := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC).Add(time.Second * time.Duration(pefile.TimeDateStamp)) + + pex := &PE{ + NumberOfSections: pefile.NumberOfSections, + NumberOfSymbols: pefile.NumberOfSymbols, + LinkTime: linkTime, + Sections: r.readSections(pefile), + Symbols: make([]string, 0), + Imports: make([]string, 0), + } + + var resDir pe.DataDirectory + switch hdr := r.oh.(type) { + case *pe.OptionalHeader32: + resDir = hdr.DataDirectory[pe.IMAGE_DIRECTORY_ENTRY_RESOURCE] + pex.ImageBase = uintToHex(uint64(hdr.ImageBase)) + pex.EntryPoint = uintToHex(uint64(hdr.AddressOfEntryPoint)) + case *pe.OptionalHeader64: + resDir = hdr.DataDirectory[pe.IMAGE_DIRECTORY_ENTRY_RESOURCE] + pex.ImageBase = uintToHex(hdr.ImageBase) + pex.EntryPoint = uintToHex(uint64(hdr.AddressOfEntryPoint)) + } + + var wg sync.WaitGroup + + if r.config.ReadResources { + wg.Add(1) + go func(wg *sync.WaitGroup) { + defer wg.Done() + pex.VersionResources, err = r.readResources(resDir.VirtualAddress) + if err != nil { + log.Warnf("fail to read %q PE resources: %v", filename, err) + } + }(&wg) + } + + if r.config.ReadSymbols { + wg.Add(1) + go func(wg *sync.WaitGroup) { + defer wg.Done() + symbols, err := pefile.ImportedSymbols() + if err != nil { + log.Warnf("fail to read %q symbols: %v", filename, err) + return + } + // each symbol is anchored to its source library so we + // can dig out the imports from the symbol name + for _, sym := range symbols { + fields := strings.SplitN(sym, ":", 2) + if len(fields) != 2 { + continue + } + symbol, lib := fields[0], fields[1] + pex.addImport(lib) + pex.addSymbol(symbol) + } + }(&wg) + } + + // ensure this method terminates in a timely manner + done := make(chan struct{}) + + go func() { + wg.Wait() + done <- struct{}{} + }() + + select { + case <-done: + return pex, nil + case <-time.After(time.Second): + log.Warn("wait timeout reached during PE metadata parsing") + peReaderTimeouts.Add(1) + return pex, nil + } +} + +// readUTF16String reads an UTF16 string at the specified RVA. +func (r *reader) readUTF16String(rva uint32) (string, error) { + data := make([]byte, 1024) + offset, err := r.FindOffsetByRVA(rva) + if err != nil { + return "", err + } + n, err := r.f.ReadAt(data, offset) + if err != nil { + if err == io.EOF { + return "", nil + } + return "", err + } + idx := bytes.Index(data[:n], []byte{0, 0}) + if idx < 0 { + idx = n - 1 + } + decoder := unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder() + utf8, err := decoder.Bytes(data[0 : idx+1]) + if err != nil { + return "", err + } + return string(utf8), nil +} + +func dwordAlign(offset, base int64) int64 { + return ((offset + base + 3) & 0xfffffffc) - (base & 0xfffffffc) +} + +func uintToHex(v uint64) string { return strconv.FormatUint(v, 16) } diff --git a/pkg/pe/reader_test.go b/pkg/pe/reader_test.go new file mode 100644 index 000000000..ca8bcec3c --- /dev/null +++ b/pkg/pe/reader_test.go @@ -0,0 +1,55 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package pe + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "path/filepath" + "testing" +) + +func TestReader(t *testing.T) { + c := Config{ + Enabled: true, + ReadResources: true, + ReadSymbols: true, + ReadSections: true, + } + r := NewReader(c) + notepad := filepath.Join(os.Getenv("windir"), "notepad.exe") + + pe, err := r.Read(notepad) + require.NoError(t, err) + require.NotNil(t, pe) + + require.True(t, pe.NumberOfSections > 0) + require.True(t, len(pe.Symbols) > 0) + require.True(t, len(pe.Imports) > 0) + require.True(t, len(pe.Sections) > 0) + + require.NotEmpty(t, pe.EntryPoint) + require.NotEmpty(t, pe.ImageBase) + assert.Contains(t, pe.Symbols, "free") + assert.Contains(t, pe.Imports, "GDI32.dll") + + assert.Contains(t, pe.VersionResources, "CompanyName") + assert.Equal(t, "Microsoft Corporation", pe.VersionResources["CompanyName"]) +} diff --git a/pkg/pe/resource/types.go b/pkg/pe/resource/types.go new file mode 100644 index 000000000..8ae0a7846 --- /dev/null +++ b/pkg/pe/resource/types.go @@ -0,0 +1,141 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package resource + +import ( + "encoding/binary" +) + +// ID is the type for identifying resource types +type ID uint32 + +const ( + Version ID = 16 // Version defines version resources +) + +// String yields a human-readable resource type name. +func (id ID) String() string { + switch id { + case Version: + return "RT_VERSION" + default: + return "" + } +} + +// Directory represents the layout of the resource directory. +type Directory struct { + Characteristics uint32 + Timestamp uint32 + Major uint16 + Minor uint16 + NumberNamedEntries uint16 + NumberIDEntries uint16 +} + +// Size returns the size in bytes of the resource directory. +func (d Directory) Size() int { return binary.Size(d) } + +// DirectoryEntry defines the entry in the directory table. +type DirectoryEntry struct { + Name uint32 + OffsetToData uint32 +} + +// Size returns the size in bytes of the resource entry. +func (e DirectoryEntry) Size() int { return binary.Size(e) } + +// ID returns the type of the resource. +func (e DirectoryEntry) ID() ID { + if !e.IsString() { + return ID(e.Name) + } + return ID(e.Name & 0x0000FFF) +} + +// IsString determines if this resource contains string data. +func (e DirectoryEntry) IsString() bool { return ((e.Name & 0x80000000) >> 31) > 0 } + +// IsDir indicates if this resource entry is a directory instead of resource final data. +func (e DirectoryEntry) IsDir() bool { return ((e.OffsetToData & 0x80000000) >> 31) > 0 } + +// DirOffset returns the offset into the resource directory. +func (e DirectoryEntry) DirOffset() uint32 { return e.OffsetToData & 0x7FFFFFFF } + +// DataEntry stores the offset to the resource data. +type DataEntry struct { + OffsetToData uint32 + DataSize uint32 + CodePage uint32 + Reserved uint32 +} + +// Size returns the size in bytes of the resource data. +func (e DataEntry) Size() int { return binary.Size(e) } + +type VersionInfo struct { + Length uint16 + ValueLength uint16 + Type uint16 +} + +func (v VersionInfo) Size() int { return binary.Size(v) } + +type FixedFileinfo struct { + Signature uint32 + StructVer uint32 + FileVersionMS uint32 + FileVersionLS uint32 + ProductVersionMS uint32 + ProductVersionLS uint32 + FileFlagMask uint32 + FileFlags uint32 + FileOS uint32 + FileType uint32 + FileSubtype uint32 + FileDateMS uint32 + FileDateLS uint32 +} + +func (f FixedFileinfo) Size() int { return binary.Size(f) } + +type StringFileInfo struct { + Length uint16 + ValueLength uint16 + Type uint16 +} + +func (s StringFileInfo) Size() int { return binary.Size(s) } +func (s StringFileInfo) Skip() bool { return (s.Type != 0 && s.Type != 1) && s.ValueLength != 0 } + +type StringTable struct { + Length uint16 + ValueLength uint16 + Type uint16 +} + +func (s StringTable) Size() int { return binary.Size(s) } + +type String struct { + Length uint16 + ValueLength uint16 + Type uint16 +} + +func (s String) Size() int { return binary.Size(s) } diff --git a/pkg/pe/resources.go b/pkg/pe/resources.go new file mode 100644 index 000000000..e0bcf53a3 --- /dev/null +++ b/pkg/pe/resources.go @@ -0,0 +1,398 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package pe + +import ( + "encoding/binary" + "errors" + "expvar" + "fmt" + "github.com/rabbitstack/fibratus/pkg/pe/resource" + "github.com/rabbitstack/fibratus/pkg/util/multierror" + "io" + "strings" +) + +const ( + seekStart = 0 + // maxAllowedResourceEntries determines the maximum number of directory entries we are allowed to process + maxAllowedResourceEntries = 4096 +) + +var ( + maxResourceEntriesExceeded = expvar.NewInt("pe.max.resource.entries.exceeded") + failedResourceEntryReads = expvar.NewInt("pe.failed.resource.entry.reads") + + errMalformedDir = errors.New("malformed directory") + errMaxAllowedDirEntries = func(nbEntries int) error { + return fmt.Errorf("the directory contains %d entries. Max allowed entries %d", nbEntries, maxAllowedResourceEntries) + } +) + +// rdir represents the resource directory with its entries +type rdir struct { + directory resource.Directory + entries []rdirEntry +} + +type rdirEntry struct { + id resource.ID + level uint16 + entry resource.DirectoryEntry + data resource.DataEntry + dir *rdir +} + +// readResources a plenty of logic in this code is inspired by pefile tool. The native stdlib package doesn't offer +// any kind of introspection on PE resources, so this function takes care of parsing the resource directory and extracting +// the version resources from it. +func (r *reader) readResources(rva uint32) (map[string]string, error) { + var vers map[string]string + dir, err := r.readResourcesDirectory(rva, 0, 0, nil) + if err != nil { + return nil, err + } + + for _, entry := range dir.entries { + if entry.dir == nil { + continue + } + if entry.level == 0 { + switch entry.id { + case resource.Version: + // read version information resources + vers, err = r.readVersionInfo(entry.dir) + if err != nil { + continue + } + } + } + } + + return vers, nil +} + +func (r *reader) readResourcesDirectory(rva uint32, baseRVA uint32, level uint16, dirs []uint32) (*rdir, error) { + if dirs == nil { + dirs = []uint32{rva} + } + if baseRVA == 0 { + baseRVA = rva + } + sr := io.NewSectionReader(r.f, 0, 1<<63-1) + offset, err := r.FindOffsetByRVA(rva) + if err != nil { + return nil, fmt.Errorf("couldn't read resources directory: %v", err) + } + // try to read the resource directory structure that is basically + // a header of the table preceding the actual resource entries + if _, err := sr.Seek(int64(offset), seekStart); err != nil { + return nil, err + } + var dir resource.Directory + if err := binary.Read(sr, binary.LittleEndian, &dir); err != nil { + return nil, err + } + + nbEntries := int(dir.NumberIDEntries + dir.NumberNamedEntries) + dirents := make([]rdirEntry, nbEntries) + + // we have to protect us against reading a huge number of entries + if nbEntries > maxAllowedResourceEntries { + maxResourceEntriesExceeded.Add(1) + return nil, errMaxAllowedDirEntries(nbEntries) + } + // advance the RVA to the position following the directory table + // header that points the the first entry in the table of entries + rva += uint32(dir.Size()) + +loop: + for i := 0; i < nbEntries; i++ { + res, err := r.readResourceEntry(sr, rva) + if err != nil { + failedResourceEntryReads.Add(1) + continue + } + // the entry is a directory so we have to parse it recursively + if res.IsDir() { + // The following comment is from pefile.py + // + // OC Patch: + // + // One trick malware can do is to recursively reference + // the next directory. This causes hilarity to ensue when + // trying to parse everything correctly. + // If the original RVA given to this function is equal to + // the next one to parse, we assume that it's a trick. + for _, dir := range dirs { + if baseRVA+res.DirOffset() == dir { + break loop + } + } + dirs = append(dirs, baseRVA+res.DirOffset()) + dir, err := r.readResourcesDirectory(baseRVA+res.DirOffset(), baseRVA, level+1, dirs) + if err != nil { + break + } + if dir == nil { + break + } + dirents[i] = rdirEntry{ + id: res.ID(), + entry: res, + dir: dir, + level: level, + } + } else { + // if we reached the actual directory data, let's read the structure that + // contains the offset and size of the resource's data + data, err := r.readResourceData(sr, baseRVA+res.DirOffset()) + if err != nil { + break + } + dirents[i] = rdirEntry{ + id: res.ID(), + entry: res, + data: data, + level: level, + } + } + // increment the RVA to the next directory entry + rva += uint32(res.Size()) + } + + return &rdir{directory: dir, entries: dirents}, nil +} + +func (r *reader) readResourceEntry(sr *io.SectionReader, rva uint32) (resource.DirectoryEntry, error) { + offset, err := r.FindOffsetByRVA(rva) + if err != nil { + return resource.DirectoryEntry{}, err + } + if _, err := sr.Seek(offset, seekStart); err != nil { + return resource.DirectoryEntry{}, err + } + var entry resource.DirectoryEntry + if err := binary.Read(sr, binary.LittleEndian, &entry); err != nil { + return resource.DirectoryEntry{}, fmt.Errorf("invalid directory entry at RVA 0x%x: %v", rva, err) + } + return entry, nil +} + +func (r *reader) readResourceData(sr *io.SectionReader, rva uint32) (resource.DataEntry, error) { + offset, err := r.FindOffsetByRVA(rva) + if err != nil { + return resource.DataEntry{}, err + } + if _, err := sr.Seek(offset, seekStart); err != nil { + return resource.DataEntry{}, err + } + var dataEntry resource.DataEntry + if err := binary.Read(sr, binary.LittleEndian, &dataEntry); err != nil { + return resource.DataEntry{}, fmt.Errorf("invalid resource data at RVA 0x%x: %v", rva, err) + } + return dataEntry, nil +} + +func (r *reader) readVersionInfo(vsDir *rdir) (map[string]string, error) { + if vsDir == nil || len(vsDir.entries) == 0 { + return nil, errMalformedDir + } + dir := vsDir.entries[0].dir + if dir == nil { + return nil, errMalformedDir + } + + errs := make([]error, 0) + vers := make(map[string]string) + vdents := dir.entries + sr := io.NewSectionReader(r.f, 0, 1<<63-1) + + for _, ve := range vdents { + offsetToData := ve.data.OffsetToData + startOffset, err := r.FindOffsetByRVA(offsetToData) + if err != nil { + return nil, err + } + // read the version info structure and the VS_VERSION_INFO string + versionInfo, versionString, err := r.parseVersionInfo(sr, startOffset, offsetToData) + if err != nil { + errs = append(errs, err) + continue + } + // if we've able to correctly parse the VS_VERSION_INFO string, the next step + // is to process the fixed version information by getting the offset of the struct + fixedFileinfoOffset := dwordAlign(int64(versionInfo.Size()+(2*len(versionString))+1), int64(offsetToData)) + fixedFileinfo, err := r.parseFixedFileinfoStruct(sr, fixedFileinfoOffset) + if err != nil { + errs = append(errs, err) + continue + } + // now the real work begins. To reach version keys/values, we first have to parse all + // of the StringFileInfo and VarFileInfo structures until we get to string table whose + // entries store the data we're after + stringFileinfoOffset := dwordAlign(int64(fixedFileinfoOffset+int64(fixedFileinfo.Size())), int64(offsetToData)) + for { + // process StringFileInfo/VarFileInfo structures. The file info string determines whether we + // should process StringFileInfo or VarFileInfo items + stringFileinfo, fileInfoString, err := r.parseStringFileinfo( + sr, + startOffset+stringFileinfoOffset, + uint32(int64(offsetToData)+stringFileinfoOffset)+uint32(versionInfo.Size()), + ) + if err != nil { + errs = append(errs, err) + break + } + + switch { + case strings.HasPrefix(fileInfoString, "StringFileInfo"): + if stringFileinfo.Skip() { + continue + } + stringTableOffset := dwordAlign(stringFileinfoOffset+int64(stringFileinfo.Size()+2*(len(fileInfoString)+1)), int64(offsetToData)) + // now we can start processing all the StringTable entries that contain the k/v pairs + for { + stringTable, langID, err := r.parseStringTable( + sr, + int64(startOffset+stringTableOffset), + offsetToData+uint32(stringTableOffset), + ) + if err != nil { + errs = append(errs, err) + break + } + // now we can process all the entries in the string table and populate the result map + entryOffset := dwordAlign(stringTableOffset+int64(stringTable.Size()+(2*len(langID)+1)), int64(offsetToData)) + for entryOffset < stringTableOffset+int64(stringTable.Length) { + if _, err := sr.Seek(int64(startOffset+entryOffset), 0); err != nil { + break + } + var str resource.String + if err := binary.Read(sr, binary.LittleEndian, &str); err != nil { + break + } + + key, err := r.readUTF16String(offsetToData + uint32(entryOffset) + uint32(str.Size())) + if err != nil { + break + } + valueOffset := dwordAlign(int64(2*(len(key)+1))+entryOffset+int64(str.Size()), int64(offsetToData)) + value, err := r.readUTF16String(uint32(offsetToData + uint32(valueOffset))) + if err != nil { + // couldn't read the value but still index the key + vers[key] = "" + break + } + vers[key] = value + if str.Length == 0 { + entryOffset = stringTableOffset + int64(stringTable.Length) + } else { + entryOffset = dwordAlign(int64(str.Length)+entryOffset, int64(offsetToData)) + } + } + + // these checks breaks on the entries that could lead to infinite loops + newStringtableOffset := dwordAlign(int64(stringTable.Length)+stringTableOffset, int64(offsetToData)) + if newStringtableOffset == stringTableOffset { + break + } + + stringTableOffset = newStringtableOffset + if stringTableOffset >= int64(stringFileinfo.Length) { + break + } + } + case strings.HasPrefix(fileInfoString, "VarFileInfo"): + break + default: + errs = append(errs, fmt.Errorf("unknown StringFileInfo string: %s", fileInfoString)) + break + } + // increment and align the string file info offset. Use the offset to check if we've + // consumed all the StringFileInfo and VarFileinfo items so we can break the loops + stringFileinfoOffset = dwordAlign(int64(stringFileinfo.Length)+stringFileinfoOffset, int64(offsetToData)) + if stringFileinfo.Length == 0 || stringFileinfoOffset >= int64(versionInfo.Length) { + break + } + } + } + + if len(vers) == 0 && len(errs) > 0 { + return nil, multierror.Wrap(errs) + } + + return vers, nil +} + +func (r *reader) parseVersionInfo(sr *io.SectionReader, startOffset int64, rva uint32) (resource.VersionInfo, string, error) { + if _, err := sr.Seek(startOffset, seekStart); err != nil { + return resource.VersionInfo{}, "", err + } + var versionInfo resource.VersionInfo + if err := binary.Read(sr, binary.LittleEndian, &versionInfo); err != nil { + return resource.VersionInfo{}, "", err + } + versionString, err := r.readUTF16String(rva + uint32(versionInfo.Size())) + if err != nil || versionString != "VS_VERSION_INFO" { + return resource.VersionInfo{}, "", fmt.Errorf("invalid VS_VERSION_INFO block: %s", versionString) + } + return versionInfo, versionString, nil +} + +func (r *reader) parseFixedFileinfoStruct(sr *io.SectionReader, offset int64) (resource.FixedFileinfo, error) { + if _, err := sr.Seek(offset, seekStart); err != nil { + return resource.FixedFileinfo{}, err + } + var fixedFileinfo resource.FixedFileinfo + if err := binary.Read(sr, binary.LittleEndian, &fixedFileinfo); err != nil { + return resource.FixedFileinfo{}, err + } + return fixedFileinfo, nil +} + +func (r *reader) parseStringFileinfo(sr *io.SectionReader, offset int64, rva uint32) (resource.StringFileInfo, string, error) { + var stringFileinfo resource.StringFileInfo + if _, err := sr.Seek(offset, seekStart); err != nil { + return resource.StringFileInfo{}, "", err + } + if err := binary.Read(sr, binary.LittleEndian, &stringFileinfo); err != nil { + return resource.StringFileInfo{}, "", fmt.Errorf("couldn't parse StringFileInfo/VarFileInfo structure: %v", err) + } + str, err := r.readUTF16String(rva) + if err != nil { + return resource.StringFileInfo{}, "", fmt.Errorf("couldn't read StringFileInfo unicode string at RVA 0x%x: %v", rva, err) + } + return stringFileinfo, str, nil +} + +func (r *reader) parseStringTable(sr *io.SectionReader, offset int64, rva uint32) (resource.StringTable, string, error) { + if _, err := sr.Seek(offset, seekStart); err != nil { + return resource.StringTable{}, "", err + } + var stringTable resource.StringTable + if err := binary.Read(sr, binary.LittleEndian, &stringTable); err != nil { + return resource.StringTable{}, "", fmt.Errorf("couldn't parse StringTable structure: %v", err) + } + langID, err := r.readUTF16String(rva + uint32(stringTable.Size())) + if err != nil { + return resource.StringTable{}, "", fmt.Errorf("couldn't read StringTable unicode string at RVA 0x%x: %v", rva+uint32(stringTable.Size()), err) + } + return stringTable, langID, nil +} diff --git a/pkg/pe/resources_test.go b/pkg/pe/resources_test.go new file mode 100644 index 000000000..0260aef76 --- /dev/null +++ b/pkg/pe/resources_test.go @@ -0,0 +1,19 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package pe diff --git a/pkg/pe/section.go b/pkg/pe/section.go new file mode 100644 index 000000000..13a770e43 --- /dev/null +++ b/pkg/pe/section.go @@ -0,0 +1,157 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package pe + +import ( + "context" + "crypto/md5" + "debug/pe" + "encoding/hex" + "fmt" + "os" + "sync" + "time" +) + +// Sec contains the section attributes. +type Sec struct { + Name string + Size uint32 + Entropy float64 + Md5 string +} + +// String returns the stirng representation of the section. +func (s Sec) String() string { + return fmt.Sprintf("Name: %s, Size: %d, Entropy: %f, Md5: %s", s.Name, s.Size, s.Entropy, s.Md5) +} + +func (r *reader) FindOffsetByRVA(rva uint32) (int64, error) { + sec, err := r.FindSectionByRVA(rva) + if err != nil { + return 0, err + } + offset := int64(rva - r.fixSectionAlignment(sec.VirtualAddress) + r.fixFileAlignment(sec.Offset)) + return offset, nil +} + +func (r *reader) FindSectionByRVA(rva uint32) (*pe.Section, error) { + for _, s := range r.sections { + if r.containsRVA(s, rva) { + return s, nil + } + } + return nil, fmt.Errorf("couldn't find section at RVA 0x%x", rva) +} + +func (r *reader) readSections(pefile *pe.File) []Sec { + secs := pefile.Sections + sections := make([]Sec, 0, len(secs)) + var wg sync.WaitGroup + + if r.config.ReadSections { + wg.Add(len(secs)) + } + for i := 0; i < len(secs); i++ { + s := secs[i] + if s == nil { + continue + } + + sec := Sec{ + Name: s.Name, + Size: s.Size, + } + + if r.config.ReadSections { + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*250) + go func(wg *sync.WaitGroup, cancel context.CancelFunc) { + defer cancel() + data, err := s.Data() + if err != nil { + return + } + sum := md5.Sum(data) + sec.Md5 = hex.EncodeToString(sum[:]) + //sec.Entropy = entropy(data) + sections = append(sections, sec) + }(&wg, cancel) + + select { + case <-ctx.Done(): + wg.Done() + } + } else { + sections = append(sections, sec) + } + + } + + if r.config.ReadSections { + wg.Wait() + } + + return sections +} + +// containsRVA determines whether the section contains the address provided by checking the boundaries +// of the section address intervals. +func (r *reader) containsRVA(sec *pe.Section, rva uint32) bool { + va := r.fixSectionAlignment(sec.VirtualAddress) + if va <= rva && rva < va+sec.Size { + return true + } + return false +} + +// fixSecAlignment ensures the alignment of the section is greater or equal to the file alignment. +func (r *reader) fixSectionAlignment(rva uint32) uint32 { + var fa uint32 + var sa uint32 + switch hdr := r.oh.(type) { + case *pe.OptionalHeader32: + fa = hdr.FileAlignment + sa = hdr.SectionAlignment + case *pe.OptionalHeader64: + fa = hdr.FileAlignment + sa = hdr.SectionAlignment + } + if int(sa) < os.Getpagesize() { + sa = fa + } + if sa > 0 && (rva%fa) != 0 { + return sa * (rva / sa) + } + return rva +} + +// fixFileAlignment adjusts section file alignment. +func (r *reader) fixFileAlignment(rva uint32) uint32 { + var fa uint32 + switch hdr := r.oh.(type) { + case *pe.OptionalHeader32: + fa = hdr.FileAlignment + case *pe.OptionalHeader64: + fa = hdr.FileAlignment + } + if fa < 0x200 { + return rva + } + return (rva / 0x200) * 0x200 +} diff --git a/pkg/pe/section_test.go b/pkg/pe/section_test.go new file mode 100644 index 000000000..0260aef76 --- /dev/null +++ b/pkg/pe/section_test.go @@ -0,0 +1,19 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package pe diff --git a/pkg/pe/types.go b/pkg/pe/types.go new file mode 100644 index 000000000..c736b1761 --- /dev/null +++ b/pkg/pe/types.go @@ -0,0 +1,94 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package pe + +import ( + "fmt" + "time" +) + +// PE contains various headers that identifies the format and characteristics of the executable files. +type PE struct { + // NumberOfSections designates the total number of sections found withing the binary. + NumberOfSections uint16 `json:"nsections"` + // NumberOfSymbols represents the total number of symbols. + NumberOfSymbols uint32 `json:"nsymbols"` + // ImageBase designates the base address of the process' image. + ImageBase string `json:"image_base"` + // Entrypoint is the address of the entry point function. + EntryPoint string `json:"entry_point"` + // LinkTime represents the time that the image was created by the linker. + LinkTime time.Time `json:"link_time"` + // Sections contains all distinct sections and their metadata. + Sections []Sec `json:"sections"` + // Symbols contains the list of imported symbols. + Symbols []string `json:"symbols"` + // Imports contains the imported libraries. + Imports []string `json:"imports"` + // VersionResources holds the version resources + VersionResources map[string]string `json:"resources"` +} + +// String returns the string representation of the PE metadata. +func (pe PE) String() string { + return fmt.Sprintf(` + Number of sections: %d + Number of symbols: %d + Image base: %s + Entrypoint: %s + Link time: %v + Sections: %v + Symbols: %v + Imports: %v + Version resources: %v + `, + pe.NumberOfSections, + pe.NumberOfSymbols, + pe.ImageBase, + pe.EntryPoint, + pe.LinkTime, + pe.Sections, + pe.Symbols, + pe.Imports, + pe.VersionResources, + ) +} + +// Section returns the section with specified name. +func (pe *PE) Section(s string) *Sec { + for _, sec := range pe.Sections { + if sec.Name == s { + return &sec + } + } + return nil +} + +func (pe *PE) addImport(i string) { + for _, imp := range pe.Imports { + if imp == i { + return + } + } + pe.Imports = append(pe.Imports, i) +} + +func (pe *PE) addSymbol(s string) { + pe.Symbols = append(pe.Symbols, s) +} diff --git a/pkg/ps/doc.go b/pkg/ps/doc.go new file mode 100644 index 000000000..5b25018b9 --- /dev/null +++ b/pkg/ps/doc.go @@ -0,0 +1,20 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package ps contains process's state snapshotter implementation. +package ps diff --git a/pkg/ps/peb.go b/pkg/ps/peb.go new file mode 100644 index 000000000..91803525f --- /dev/null +++ b/pkg/ps/peb.go @@ -0,0 +1,148 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ps + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/rabbitstack/fibratus/pkg/syscall/process" + "strings" + "syscall" + "unicode/utf16" + "unsafe" +) + +const ( + maxEnvSize = 4096 +) + +// PEB contains various process's metadata from the Process Environment Block (PEB). PEB is an opaque data structure +// that contains information that apply across a whole process, including global context, startup parameters, data structures +// for the program image loader, the program image base address, and synchronization objects used to provide mutual exclusion +// for process-wide data structures. Although it is not encouraged to access this structure due to its unstable nature, some +// process's information like command line or environments strings are only available through Process Environment Block fields. +type PEB struct { + peb *process.PEB + handle handle.Handle + procParams *process.RTLUserProcessParameters +} + +// ReadPEB queries the process's basic information class structures and copies the PEB into +// the current process's address space. Returns the reference to the PEB of the process that is being queried. +func ReadPEB(handle handle.Handle) (*PEB, error) { + buf := make([]byte, unsafe.Sizeof(process.BasicInformation{})) + _, err := process.QueryInfo(handle, process.BasicInformationClass, buf) + if err != nil { + return nil, fmt.Errorf("couldn't query process information: %v", err) + } + info := (*process.BasicInformation)(unsafe.Pointer(&buf[0])) + // read the PEB to get the process parameters. Because the PEB structure resides + // in the address space of another process we must read the memory block in order + // to access the structure's fields. + peb, err := process.ReadMemory(handle, unsafe.Pointer(info.PEB), unsafe.Sizeof(process.PEB{})) + if err != nil { + return nil, fmt.Errorf("coulnd't read PEB: %v", err) + } + return &PEB{peb: (*process.PEB)(unsafe.Pointer(&peb[0])), handle: handle}, nil +} + +func (p PEB) GetImage() string { + params, err := p.readProcessParams() + if err != nil { + return "" + } + image, err := process.ReadMemoryUnicode(p.handle, unsafe.Pointer(params.ImagePathName.Buffer), uintptr(params.ImagePathName.Length)) + if err != nil { + return "" + } + return syscall.UTF16ToString(image) +} + +func (p PEB) GetCommandLine() string { + params, err := p.readProcessParams() + if err != nil { + return "" + } + comm, err := process.ReadMemoryUnicode(p.handle, unsafe.Pointer(params.CommandLine.Buffer), uintptr(params.CommandLine.Length)) + if err != nil { + return "" + } + return syscall.UTF16ToString(comm) +} + +func (p PEB) GetCurrentWorkingDirectory() string { + params, err := p.readProcessParams() + if err != nil { + return "" + } + cwd, err := process.ReadMemoryUnicode(p.handle, unsafe.Pointer(params.CurrentDirectory.DosPath.Buffer), uintptr(params.CurrentDirectory.DosPath.Length)) + if err != nil { + return "" + } + return syscall.UTF16ToString(cwd) +} + +func (p PEB) GetEnvs() map[string]string { + params, err := p.readProcessParams() + if err != nil { + return nil + } + // we can read the whole memory region starting from the env address + // and speculate the size of the env block, but we just use a fixed + // buffer size + s, err := process.ReadMemoryUnicode(p.handle, unsafe.Pointer(params.Environment), uintptr(maxEnvSize)) + if err != nil { + return nil + } + envs := make(map[string]string) + start, end := 0, 0 + for i, r := range s { + // each env variable key/value pair terminates with the NUL character + if r == 0 { + end = i + } + if end > start { + // the next token starts with a NUL character + // which means we have consumed all env variables + if s[start] == 0 { + break + } + env := string(utf16.Decode(s[start:end])) + if kv := strings.Split(env, "="); len(kv) == 2 { + envs[kv[0]] = kv[1] + } + start = end + 1 + } + } + return envs +} + +// readProcessParams reads the `RtlUserProcessParameters` struct +// which contains the command line and the image name of the process +func (p *PEB) readProcessParams() (*process.RTLUserProcessParameters, error) { + if p.procParams != nil { + return p.procParams, nil + } + b, err := process.ReadMemory(p.handle, unsafe.Pointer(p.peb.ProcessParameters), unsafe.Sizeof(process.RTLUserProcessParameters{})) + if err != nil { + return nil, fmt.Errorf("couldn't read process's parameters from PEB: %v", err) + } + p.procParams = (*process.RTLUserProcessParameters)(unsafe.Pointer(&b[0])) + return p.procParams, nil +} diff --git a/pkg/ps/peb_test.go b/pkg/ps/peb_test.go new file mode 100644 index 000000000..155007d2c --- /dev/null +++ b/pkg/ps/peb_test.go @@ -0,0 +1,42 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ps + +import ( + "github.com/rabbitstack/fibratus/pkg/syscall/process" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "path/filepath" + "testing" +) + +func TestPEBGetCurrentWorkingDirectory(t *testing.T) { + flags := process.QueryInformation | process.VMRead + handle, err := process.Open(flags, false, uint32(os.Getpid())) + if err != nil { + t.Fatal(err) + } + peb, err := ReadPEB(handle) + require.NoError(t, err) + + cwd := peb.GetCurrentWorkingDirectory() + require.NotEmpty(t, cwd) + assert.Equal(t, "ps", filepath.Base(cwd)) +} diff --git a/pkg/ps/snapshotter.go b/pkg/ps/snapshotter.go new file mode 100644 index 000000000..e41c4db70 --- /dev/null +++ b/pkg/ps/snapshotter.go @@ -0,0 +1,525 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ps + +import ( + "expvar" + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/handle" + htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/rabbitstack/fibratus/pkg/pe" + pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" + hndl "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/rabbitstack/fibratus/pkg/syscall/process" + t "github.com/rabbitstack/fibratus/pkg/syscall/thread" + "github.com/rabbitstack/fibratus/pkg/syscall/winerrno" + log "github.com/sirupsen/logrus" + "os" + "path/filepath" + "strconv" + "sync" + "time" +) + +var ( + // reapPeriod specifies the interval for triggering the house keeping of dead processes + reapPeriod = time.Minute * 2 + + processLookupFailureCount = expvar.NewMap("process.lookup.failure.count") + reapedProcesses = expvar.NewInt("process.reaped") + processCount = expvar.NewInt("process.count") + threadCount = expvar.NewInt("process.thread.count") + moduleCount = expvar.NewInt("process.module.count") + pebReadErrors = expvar.NewInt("process.peb.read.errors") +) + +// Snapshotter is the interface that exposes a set of methods all process snapshotters have to satisfy. It stores the state +// of all running processes in the system including its threads, dynamically referenced libraries, handles and other +// metadata. +type Snapshotter interface { + // Write appends a new process state to the snapshotter. It takes as an input the inbound kernel event to fetch + // the basic data, but also enriches the process' state with extra metadata such as process' env variables, PE + // metadata and so on. + Write(kevt *kevent.Kevent) error + // WriteFromKcap appends a new process state to the snapshotter from the captured kernel event. + WriteFromKcap(kevt *kevent.Kevent) error + // Remove deletes process's state from the snapshotter. + Remove(kevt *kevent.Kevent) error + // Find attempts to retrieve process' state for the specified process identifier. + Find(pid uint32) *pstypes.PS + // Size returns the total number of process state items. + Size() uint32 + // Close closes process snapshotter and disposes all allocated resources. + Close() error +} + +type snapshotter struct { + mu sync.RWMutex + procs map[uint32]*pstypes.PS + quit chan struct{} + config *config.Config + handleSnap handle.Snapshotter + peReader pe.Reader + capture bool +} + +// NewSnapshotter returns a new instance of the process snapshotter. +func NewSnapshotter(handleSnap handle.Snapshotter, config *config.Config) Snapshotter { + s := &snapshotter{ + procs: make(map[uint32]*pstypes.PS), + quit: make(chan struct{}), + config: config, + handleSnap: handleSnap, + peReader: pe.NewReader(config.PE), + } + + s.mu.Lock() + defer s.mu.Unlock() + + s.handleSnap.RegisterCreateCallback(s.onHandleCreated) + s.handleSnap.RegisterDestroyCallback(s.onHandleDestroyed) + + go s.reapDeadProcesses() + + return s +} + +// NewSnapshotterFromKcap restores the snapshotter state from the kcap file. +func NewSnapshotterFromKcap(handleSnap handle.Snapshotter, config *config.Config) Snapshotter { + s := &snapshotter{ + procs: make(map[uint32]*pstypes.PS), + quit: make(chan struct{}), + config: config, + handleSnap: handleSnap, + peReader: pe.NewReader(config.PE), + capture: true, + } + + s.mu.Lock() + defer s.mu.Unlock() + + s.handleSnap.RegisterCreateCallback(s.onHandleCreated) + s.handleSnap.RegisterDestroyCallback(s.onHandleDestroyed) + + return s +} + +func (s *snapshotter) WriteFromKcap(kevt *kevent.Kevent) error { + s.mu.Lock() + defer s.mu.Unlock() + switch kevt.Type { + case ktypes.CreateProcess, ktypes.EnumProcess: + ps := kevt.PS + if ps == nil { + return nil + } + if ps.PID == winerrno.InvalidPID { + return nil + } + s.procs[ps.PID] = ps + + case ktypes.CreateThread, ktypes.EnumThread: + pid, err := kevt.Kparams.GetPid() + if err != nil { + return err + } + threadCount.Add(1) + thread := pstypes.ThreadFromKevent(unwrapThreadParams(pid, kevt)) + if ps, ok := s.procs[pid]; ok { + ps.AddThread(thread) + } + + case ktypes.LoadImage, ktypes.EnumImage: + pid, err := kevt.Kparams.GetPid() + if err != nil { + return err + } + moduleCount.Add(1) + ps, ok := s.procs[pid] + if !ok { + return nil + } + ps.AddModule(pstypes.ImageFromKevent(unwrapImageParams(kevt))) + } + return nil +} + +func (s *snapshotter) Write(kevt *kevent.Kevent) error { + s.mu.Lock() + defer s.mu.Unlock() + + switch kevt.Type { + case ktypes.CreateProcess, ktypes.EnumProcess: + pid, err := kevt.Kparams.GetPid() + if err != nil { + return err + } + // discard writing the snapshot state if the pid is already present + if _, ok := s.procs[pid]; ok { + return nil + } + processCount.Add(1) + if pid == winerrno.InvalidPID { + pid = pidFromThreadID(kevt.Tid) + } + + ps := pstypes.FromKevent(unwrapParams(pid, kevt)) + // enumerate process handles + handles, err := s.handleSnap.FindHandles(pid) + if err != nil { + log.Warnf("couldn't enumerate handles for pid (%d): %v", pid, err) + } + ps.Handles = handles + + // inspect PE metadata and attach corresponding headers + s.readPE(ps) + + // try to enum process's env variables and the cwd + flags := process.QueryInformation | process.VMRead + h, err := process.Open(flags, false, pid) + if err != nil { + kevt.PS = ps + s.procs[pid] = ps + return nil + } + defer h.Close() + + peb, err := ReadPEB(h) + if err != nil { + pebReadErrors.Add(1) + s.procs[pid] = ps + return nil + } + + ps.Envs = peb.GetEnvs() + ps.Cwd = peb.GetCurrentWorkingDirectory() + kevt.PS = ps + s.procs[pid] = ps + + case ktypes.CreateThread, ktypes.EnumThread: + pid, err := kevt.Kparams.GetPid() + if err != nil { + return err + } + threadCount.Add(1) + if pid == winerrno.InvalidPID { + threadID, _ := kevt.Kparams.GetTid() + pid = pidFromThreadID(threadID) + } + // thread can be associated with the process + // as it already exists in the map + thread := pstypes.ThreadFromKevent(unwrapThreadParams(pid, kevt)) + if ps, ok := s.procs[pid]; ok { + ps.AddThread(thread) + return nil + } + + // search for missing process and attempt to get its info + ps := s.findProcess(pid, thread) + + // enumerate process handles + handles, err := s.handleSnap.FindHandles(pid) + if err != nil { + log.Warnf("couldn't enumerate handles for pid (%d): %v", pid, err) + } + ps.Handles = handles + + s.procs[pid] = ps + + case ktypes.LoadImage, ktypes.EnumImage: + pid, err := kevt.Kparams.GetPid() + if err != nil { + return err + } + moduleCount.Add(1) + ps, ok := s.procs[pid] + if !ok { + return nil + } + ps.AddModule(pstypes.ImageFromKevent(unwrapImageParams(kevt))) + } + return nil +} + +func (s *snapshotter) Close() error { + s.quit <- struct{}{} + return nil +} + +// reapDeadProcesses periodically scans the map of the snapshot's processes and removes +// any terminated processes from it. This guarantees that any leftovers are cleaned-up +// in case we miss process' terminate events. +func (s *snapshotter) reapDeadProcesses() { + tick := time.NewTicker(reapPeriod) + for { + select { + case <-tick.C: + s.mu.Lock() + ss := len(s.procs) + log.Debugf("scanning for dead processes on the snapshot of %d items", ss) + + for pid := range s.procs { + h, err := process.Open(process.QueryLimitedInformation, false, pid) + if err != nil { + continue + } + if !process.IsAlive(h) { + delete(s.procs, pid) + } + h.Close() + } + + if ss > len(s.procs) { + reaped := ss - len(s.procs) + reapedProcesses.Add(int64(reaped)) + log.Debugf("%d dead process(es) reaped", reaped) + } + s.mu.Unlock() + case <-s.quit: + tick.Stop() + } + } +} + +func pidFromThreadID(tid uint32) uint32 { + h, err := t.Open(t.QueryLimitedInformation, false, tid) + if err != nil { + return winerrno.InvalidPID + } + defer h.Close() + pid, err := process.GetPIDFromThread(h) + if err != nil { + return winerrno.InvalidPID + } + return pid +} + +func ntoskrnl() string { return filepath.Join(os.Getenv("SystemRoot"), "ntoskrnl.exe") } + +func fromPEB(pid, ppid uint32, peb *PEB, thread pstypes.Thread) *pstypes.PS { + return pstypes.NewPS( + pid, + ppid, + peb.GetImage(), + peb.GetCurrentWorkingDirectory(), + peb.GetCommandLine(), + thread, + peb.GetEnvs(), + ) +} + +func (s *snapshotter) findProcess(pid uint32, thread pstypes.Thread) *pstypes.PS { + // several system protected processes don't allow for getting their handles + // even if SeDebugPrivilege is present in the process' token, so we'll handle + // them manually + switch pid { + case 0: + return pstypes.NewPS(pid, pid, "idle", "", "idle", thread, nil) + case 4: + return pstypes.NewPS(pid, pid, "System", "", ntoskrnl(), thread, nil) + } + flags := process.QueryInformation | process.VMRead + h, err := process.Open(flags, false, pid) + if err != nil { + // the access to protected / system process can't be achieved through + // `VMRead` or `QueryInformation` flags. + // Try to acquire the process handle again but with restricted access rights, + // so we can get the process image file name + h, err = process.Open( + process.QueryLimitedInformation, + false, + pid, + ) + if err != nil { + return pstypes.NewPS(pid, pid, "", "", "", thread, nil) + } + } + defer h.Close() + + // read process's metadata from the PEB + peb, err := ReadPEB(h) + if err != nil { + pebReadErrors.Add(1) + // couldn't query process basic info or read the PEB, + // so at least try to obtain the full process's image name + image, err := process.QueryFullImageName(h) + if err != nil { + return pstypes.NewPS(pid, pid, "", "", "", thread, nil) + } + return pstypes.NewPS(pid, pid, image, "", image, thread, nil) + } + ppid := process.GetParentPID(h) + + return fromPEB(pid, ppid, peb, thread) +} + +func (s *snapshotter) readPE(ps *pstypes.PS) { + // skip imageless processes such as Idle, System or Registry + pid := ps.PID + if pid == 0 || pid == 4 || pid == 72 || pid == 128 { + return + } + pex, err := s.peReader.Read(ps.Exe) + if err != nil { + log.Warnf("fail to inspect PE metadata for process %s (%d): %v", ps.Name, ps.PID, err) + return + } + + if pex == nil { + return + } + + ps.PE = pex + s.procs[pid] = ps +} + +func (s *snapshotter) onHandleCreated(pid uint32, handle htypes.Handle) { + s.mu.RLock() + ps, ok := s.procs[pid] + s.mu.RUnlock() + if ok { + s.mu.Lock() + defer s.mu.Unlock() + ps.AddHandle(handle) + s.procs[pid] = ps + } +} + +func (s *snapshotter) onHandleDestroyed(pid uint32, num hndl.Handle) { + s.mu.RLock() + ps, ok := s.procs[pid] + s.mu.RUnlock() + if ok { + s.mu.Lock() + defer s.mu.Unlock() + ps.RemoveHandle(num) + s.procs[pid] = ps + } +} + +func (s *snapshotter) Remove(kevt *kevent.Kevent) error { + s.mu.Lock() + defer s.mu.Unlock() + pid, err := kevt.Kparams.GetPid() + if err != nil { + return err + } + if kevt.Type == ktypes.TerminateProcess { + if _, ok := s.procs[pid]; ok { + delete(s.procs, pid) + processCount.Add(-1) + return nil + } + } else if kevt.Type == ktypes.TerminateThread { + if ps, ok := s.procs[pid]; ok { + tid, err := kevt.Kparams.GetTid() + if err != nil { + return err + } + ps.RemoveThread(tid) + threadCount.Add(-1) + } + } else if kevt.Type == ktypes.UnloadImage { + pid, err := kevt.Kparams.GetPid() + if err != nil { + return err + } + if ps, ok := s.procs[pid]; ok { + name, _ := kevt.Kparams.GetString(kparams.ImageFilename) + ps.RemoveModule(name) + moduleCount.Add(-1) + } + } + return nil +} + +func (s *snapshotter) Find(pid uint32) *pstypes.PS { + s.mu.RLock() + ps, ok := s.procs[pid] + s.mu.RUnlock() + if ok { + return ps + } + if s.capture { + return nil + } + processLookupFailureCount.Add(strconv.Itoa(int(pid)), 1) + // allocate missing process's state and fill in metadata/handles + thread := pstypes.Thread{} + ps = s.findProcess(pid, thread) + + s.readPE(ps) + + // enumerate process handles + handles, err := s.handleSnap.FindHandles(pid) + if err != nil { + log.Warnf("couldn't enumerate handles for pid (%d): %v", pid, err) + } + + ps.Handles = handles + s.mu.Lock() + defer s.mu.Unlock() + s.procs[pid] = ps + + return ps +} + +func (s *snapshotter) Size() uint32 { + s.mu.RLock() + defer s.mu.RUnlock() + return uint32(len(s.procs)) +} + +func unwrapParams(pid uint32, kevt *kevent.Kevent) (uint32, uint32, string, string, string, string, uint8) { + ppid, _ := kevt.Kparams.GetPid() + name, _ := kevt.Kparams.GetString(kparams.ProcessName) + comm, _ := kevt.Kparams.GetString(kparams.Comm) + exe, _ := kevt.Kparams.GetString(kparams.Exe) + sid, _ := kevt.Kparams.GetString(kparams.UserSID) + sessionID, _ := kevt.Kparams.GetUint32(kparams.SessionID) + + return pid, ppid, name, comm, exe, sid, uint8(sessionID) +} + +func unwrapThreadParams(pid uint32, kevt *kevent.Kevent) (uint32, uint32, kparams.Hex, kparams.Hex, kparams.Hex, kparams.Hex, uint8, uint8, uint8, kparams.Hex) { + tid, _ := kevt.Kparams.GetTid() + ustackBase, _ := kevt.Kparams.GetHex(kparams.UstackBase) + ustackLimit, _ := kevt.Kparams.GetHex(kparams.UstackLimit) + kstackBase, _ := kevt.Kparams.GetHex(kparams.KstackBase) + kstackLimit, _ := kevt.Kparams.GetHex(kparams.KstackLimit) + ioPrio, _ := kevt.Kparams.GetUint8(kparams.IOPrio) + basePrio, _ := kevt.Kparams.GetUint8(kparams.BasePrio) + pagePrio, _ := kevt.Kparams.GetUint8(kparams.PagePrio) + entrypoint, _ := kevt.Kparams.GetHex(kparams.ThreadEntrypoint) + + return pid, tid, ustackBase, ustackLimit, kstackBase, kstackLimit, ioPrio, basePrio, pagePrio, entrypoint +} + +func unwrapImageParams(kevt *kevent.Kevent) (uint32, uint32, string, kparams.Hex, kparams.Hex) { + size, _ := kevt.Kparams.GetUint32(kparams.ImageSize) + checksum, _ := kevt.Kparams.GetUint32(kparams.ImageCheckSum) + name, _ := kevt.Kparams.GetString(kparams.ImageFilename) + baseAddress, _ := kevt.Kparams.GetHex(kparams.ImageBase) + defaultBaseAddress, _ := kevt.Kparams.GetHex(kparams.ImageDefaultBase) + + return size, checksum, name, baseAddress, defaultBaseAddress +} diff --git a/pkg/ps/snapshotter_mock.go b/pkg/ps/snapshotter_mock.go new file mode 100644 index 000000000..e11508a7c --- /dev/null +++ b/pkg/ps/snapshotter_mock.go @@ -0,0 +1,44 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ps + +import ( + "github.com/rabbitstack/fibratus/pkg/kevent" + pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" + "github.com/stretchr/testify/mock" +) + +type SnapshotterMock struct { + mock.Mock +} + +func (s *SnapshotterMock) Write(kevt *kevent.Kevent) error { return nil } +func (s *SnapshotterMock) Remove(kevt *kevent.Kevent) error { return nil } +func (s *SnapshotterMock) Find(pid uint32) *pstypes.PS { + args := s.Called(pid) + return args.Get(0).(*pstypes.PS) +} +func (s *SnapshotterMock) Size() uint32 { args := s.Called(); return uint32(args.Int(0)) } +func (s *SnapshotterMock) Close() error { return nil } +func (s *SnapshotterMock) GetSnapshot() []*pstypes.PS { + args := s.Called() + return args.Get(0).([]*pstypes.PS) +} + +func (s *SnapshotterMock) WriteFromKcap(kevt *kevent.Kevent) error { return nil } diff --git a/pkg/ps/snapshotter_test.go b/pkg/ps/snapshotter_test.go new file mode 100644 index 000000000..575a587ce --- /dev/null +++ b/pkg/ps/snapshotter_test.go @@ -0,0 +1,379 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ps + +import ( + "github.com/rabbitstack/fibratus/pkg/config" + "github.com/rabbitstack/fibratus/pkg/handle" + "github.com/rabbitstack/fibratus/pkg/kevent" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/kevent/ktypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "path/filepath" + "syscall" + "testing" + "time" +) + +func TestSnapshotterWrite(t *testing.T) { + hsnap := new(handle.SnapshotterMock) + psnap := NewSnapshotter(hsnap, &config.Config{}) + + pid := uint32(os.Getpid()) + kevt := &kevent.Kevent{ + Type: ktypes.CreateProcess, + Kparams: kevent.Kparams{ + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: pid}, + kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(8390)}, + kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "spotify.exe"}, + kparams.Comm: {Name: kparams.Comm, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, + kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe`}, + kparams.UserSID: {Name: kparams.UserSID, Type: kparams.UnicodeString, Value: `admin\SYSTEM`}, + }, + } + + err := psnap.Write(kevt) + require.NoError(t, err) + + ps := psnap.Find(pid) + require.NotNil(t, ps) + + assert.Equal(t, pid, ps.PID) + assert.Equal(t, uint32(8390), ps.Ppid) + assert.Equal(t, "spotify.exe", ps.Name) + assert.Equal(t, `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`, ps.Comm) + assert.Equal(t, `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe`, ps.Exe) + assert.Equal(t, `admin\SYSTEM`, ps.SID) + assert.Len(t, ps.Args, 13) + assert.Equal(t, "--type=crashpad-handler", ps.Args[0]) + assert.Equal(t, "ps", filepath.Base(ps.Cwd)) + assert.True(t, len(ps.Envs) > 0) +} + +func TestSnapshotterWriteNoPIDInParams(t *testing.T) { + hsnap := new(handle.SnapshotterMock) + psnap := NewSnapshotter(hsnap, &config.Config{}) + + kevt := &kevent.Kevent{ + Type: ktypes.CreateProcess, + Kparams: kevent.Kparams{ + kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(8390)}, + kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "spotify.exe"}, + kparams.Comm: {Name: kparams.Comm, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, + kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe`}, + kparams.UserSID: {Name: kparams.UserSID, Type: kparams.UnicodeString, Value: `admin\SYSTEM`}, + }, + } + + require.Error(t, psnap.Write(kevt)) +} + +func TestSnapshotterWriteThread(t *testing.T) { + hsnap := new(handle.SnapshotterMock) + psnap := NewSnapshotter(hsnap, &config.Config{}) + + kevt := &kevent.Kevent{ + Type: ktypes.CreateProcess, + Kparams: kevent.Kparams{ + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(6599)}, + kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(8390)}, + kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "spotify.exe"}, + kparams.Comm: {Name: kparams.Comm, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, + kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe`}, + kparams.UserSID: {Name: kparams.UserSID, Type: kparams.UnicodeString, Value: `admin\SYSTEM`}, + }, + } + require.NoError(t, psnap.Write(kevt)) + + kevt1 := &kevent.Kevent{ + Type: ktypes.CreateThread, + Kparams: kevent.Kparams{ + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(6599)}, + kparams.ThreadID: {Name: kparams.ThreadID, Type: kparams.TID, Value: uint32(3453)}, + kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Uint8, Value: uint8(13)}, + kparams.ThreadEntrypoint: {Name: kparams.ThreadEntrypoint, Type: kparams.HexInt64, Value: kparams.Hex("0x7ffe2557ff80")}, + kparams.IOPrio: {Name: kparams.IOPrio, Type: kparams.Uint8, Value: uint8(2)}, + kparams.KstackBase: {Name: kparams.KstackBase, Type: kparams.HexInt64, Value: kparams.Hex("0xffffc307810d6000")}, + kparams.KstackLimit: {Name: kparams.KstackLimit, Type: kparams.HexInt64, Value: kparams.Hex("0xffffc307810cf000")}, + kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(5)}, + kparams.UstackBase: {Name: kparams.UstackBase, Type: kparams.HexInt64, Value: kparams.Hex("0x5260000")}, + kparams.UstackLimit: {Name: kparams.UstackLimit, Type: kparams.HexInt64, Value: kparams.Hex("0x525f000")}, + }, + } + + require.NoError(t, psnap.Write(kevt1)) + + ps := psnap.Find(uint32(6599)) + require.NotNil(t, ps) + + require.Len(t, ps.Threads, 1) + + thread := ps.Threads[3453] + + assert.Equal(t, uint32(3453), thread.Tid) + assert.Equal(t, uint32(6599), thread.Pid) + assert.Equal(t, uint8(13), thread.BasePrio) + assert.Equal(t, kparams.Hex("0x7ffe2557ff80"), thread.Entrypoint) + assert.Equal(t, uint8(2), thread.IOPrio) + assert.Equal(t, uint8(5), thread.PagePrio) + assert.Equal(t, kparams.Hex("0xffffc307810d6000"), thread.KstackBase) + assert.Equal(t, kparams.Hex("0xffffc307810cf000"), thread.KstackLimit) + assert.Equal(t, kparams.Hex("0x5260000"), thread.UstackBase) + assert.Equal(t, kparams.Hex("0x525f000"), thread.UstackLimit) +} + +func TestSnapshotterWriteThreadPIDInParams(t *testing.T) { + hsnap := new(handle.SnapshotterMock) + psnap := NewSnapshotter(hsnap, &config.Config{}) + + kevt := &kevent.Kevent{ + Type: ktypes.CreateThread, + Kparams: kevent.Kparams{ + kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(8390)}, + kparams.ThreadID: {Name: kparams.ThreadID, Type: kparams.TID, Value: uint32(3453)}, + }, + } + + require.Error(t, psnap.Write(kevt)) +} + +func TestSnapshotterWritePSThreadMissingProc(t *testing.T) { + hsnap := new(handle.SnapshotterMock) + psnap := NewSnapshotter(hsnap, &config.Config{}) + + pid := uint32(os.Getpid()) + kevt := &kevent.Kevent{ + Type: ktypes.CreateThread, + Kparams: kevent.Kparams{kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: pid}}, + } + + err := psnap.Write(kevt) + require.NoError(t, err) + + ps := psnap.Find(pid) + require.NotNil(t, ps) + assert.Equal(t, pid, ps.PID) + assert.Contains(t, ps.Name, "TestSnapshotterWritePSThreadMissingProc_in_github_com_rabbitstack_fibratus_pkg_ps.exe") + assert.True(t, len(ps.Envs) > 0) +} + +func TestSnapshotterWritePSThreadMissingProcIdle(t *testing.T) { + hsnap := new(handle.SnapshotterMock) + psnap := NewSnapshotter(hsnap, &config.Config{}) + + kevt := &kevent.Kevent{ + Type: ktypes.CreateThread, + Kparams: kevent.Kparams{kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(0)}}, + } + + err := psnap.Write(kevt) + require.NoError(t, err) +} + +func TestSnapshotterWritePSThreadMissingProtectedProc(t *testing.T) { + hsnap := new(handle.SnapshotterMock) + psnap := NewSnapshotter(hsnap, &config.Config{}) + + kevt := &kevent.Kevent{ + Type: ktypes.CreateThread, + Kparams: kevent.Kparams{kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(0)}}, + } + + err := psnap.Write(kevt) + require.NoError(t, err) +} + +func init() { + reapPeriod = time.Millisecond * 45 +} + +func TestReapDeadProcesses(t *testing.T) { + hsnap := new(handle.SnapshotterMock) + psnap := NewSnapshotter(hsnap, &config.Config{}) + defer psnap.Close() + + var si syscall.StartupInfo + var pi syscall.ProcessInformation + + argv := syscall.StringToUTF16Ptr(filepath.Join(os.Getenv("windir"), "notepad.exe")) + + err := syscall.CreateProcess( + nil, + argv, + nil, + nil, + true, + 0, + nil, + nil, + &si, + &pi) + + require.NoError(t, err) + + kevt := &kevent.Kevent{ + Type: ktypes.CreateProcess, + Kparams: kevent.Kparams{ + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: pi.ProcessId}, + kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(8390)}, + kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "calc.exe"}, + kparams.Comm: {Name: kparams.Comm, Type: kparams.UnicodeString, Value: `c:\\windows\\system32\\calc.exe`}, + kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: `c:\\windows\\system32\\calc.exe`}, + kparams.UserSID: {Name: kparams.UserSID, Type: kparams.UnicodeString, Value: `admin\SYSTEM`}, + }, + } + + require.NoError(t, psnap.Write(kevt)) + + kevt1 := &kevent.Kevent{ + Type: ktypes.CreateProcess, + Kparams: kevent.Kparams{ + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(os.Getpid())}, + kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(8390)}, + kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "spotify.exe"}, + kparams.Comm: {Name: kparams.Comm, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, + kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe`}, + kparams.UserSID: {Name: kparams.UserSID, Type: kparams.UnicodeString, Value: `admin\SYSTEM`}, + }, + } + require.NoError(t, psnap.Write(kevt1)) + + require.True(t, psnap.Size() > 1) + require.NoError(t, syscall.TerminateProcess(pi.Process, uint32(257))) + time.Sleep(time.Millisecond * 100) + + require.True(t, psnap.Size() == 1) +} + +func TestRemove(t *testing.T) { + hsnap := new(handle.SnapshotterMock) + psnap := NewSnapshotter(hsnap, &config.Config{}) + + pid := uint32(os.Getpid()) + kevt := &kevent.Kevent{ + Type: ktypes.CreateProcess, + Kparams: kevent.Kparams{ + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: pid}, + kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(8390)}, + kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "spotify.exe"}, + kparams.Comm: {Name: kparams.Comm, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, + kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe`}, + kparams.UserSID: {Name: kparams.UserSID, Type: kparams.UnicodeString, Value: `admin\SYSTEM`}, + }, + } + + err := psnap.Write(kevt) + require.NoError(t, err) + + require.True(t, psnap.Size() > 0) + + err = psnap.Remove(&kevent.Kevent{ + Type: ktypes.TerminateProcess, + Kparams: kevent.Kparams{ + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: pid}, + }, + }) + require.NoError(t, err) + require.True(t, psnap.Size() == 0) +} + +func TestRemoveProcNotFound(t *testing.T) { + hsnap := new(handle.SnapshotterMock) + psnap := NewSnapshotter(hsnap, &config.Config{}) + + err := psnap.Remove(&kevent.Kevent{ + Type: ktypes.TerminateProcess, + Kparams: kevent.Kparams{ + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(4596)}, + }, + }) + require.Error(t, err) +} + +func TestRemoveNoPidInParams(t *testing.T) { + hsnap := new(handle.SnapshotterMock) + psnap := NewSnapshotter(hsnap, &config.Config{}) + + kevt := &kevent.Kevent{ + Type: ktypes.CreateProcess, + Kparams: kevent.Kparams{ + kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(8390)}, + kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "spotify.exe"}, + kparams.Comm: {Name: kparams.Comm, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, + kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe`}, + kparams.UserSID: {Name: kparams.UserSID, Type: kparams.UnicodeString, Value: `admin\SYSTEM`}, + }, + } + + err := psnap.Write(kevt) + require.Error(t, err) + require.True(t, psnap.Size() == 0) +} + +func TestRemoveThread(t *testing.T) { + hsnap := new(handle.SnapshotterMock) + psnap := NewSnapshotter(hsnap, &config.Config{}) + + kevt := &kevent.Kevent{ + Type: ktypes.CreateProcess, + Kparams: kevent.Kparams{ + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(6599)}, + kparams.ProcessParentID: {Name: kparams.ProcessParentID, Type: kparams.PID, Value: uint32(8390)}, + kparams.ProcessName: {Name: kparams.ProcessName, Type: kparams.UnicodeString, Value: "spotify.exe"}, + kparams.Comm: {Name: kparams.Comm, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe --type=crashpad-handler /prefetch:7 --max-uploads=5 --max-db-size=20 --max-db-age=5 --monitor-self-annotation=ptype=crashpad-handler "--metrics-dir=C:\Users\admin\AppData\Local\Spotify\User Data" --url=https://crashdump.spotify.com:443/ --annotation=platform=win32 --annotation=product=spotify --annotation=version=1.1.4.197 --initial-client-data=0x5a4,0x5a0,0x5a8,0x59c,0x5ac,0x6edcbf60,0x6edcbf70,0x6edcbf7c`}, + kparams.Exe: {Name: kparams.Exe, Type: kparams.UnicodeString, Value: `C:\Users\admin\AppData\Roaming\Spotify\Spotify.exe`}, + kparams.UserSID: {Name: kparams.UserSID, Type: kparams.UnicodeString, Value: `admin\SYSTEM`}, + }, + } + require.NoError(t, psnap.Write(kevt)) + + kevt1 := &kevent.Kevent{ + Type: ktypes.CreateThread, + Kparams: kevent.Kparams{ + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(6599)}, + kparams.ThreadID: {Name: kparams.ThreadID, Type: kparams.TID, Value: uint32(3453)}, + kparams.BasePrio: {Name: kparams.BasePrio, Type: kparams.Uint8, Value: uint8(13)}, + kparams.ThreadEntrypoint: {Name: kparams.ThreadEntrypoint, Type: kparams.HexInt64, Value: kparams.Hex("0x7ffe2557ff80")}, + kparams.IOPrio: {Name: kparams.IOPrio, Type: kparams.Uint8, Value: uint8(2)}, + kparams.KstackBase: {Name: kparams.KstackBase, Type: kparams.HexInt64, Value: kparams.Hex("0xffffc307810d6000")}, + kparams.KstackLimit: {Name: kparams.KstackLimit, Type: kparams.HexInt64, Value: kparams.Hex("0xffffc307810cf000")}, + kparams.PagePrio: {Name: kparams.PagePrio, Type: kparams.Uint8, Value: uint8(5)}, + kparams.UstackBase: {Name: kparams.UstackBase, Type: kparams.HexInt64, Value: kparams.Hex("0x5260000")}, + kparams.UstackLimit: {Name: kparams.UstackLimit, Type: kparams.HexInt64, Value: kparams.Hex("0x525f000")}, + }, + } + + require.NoError(t, psnap.Write(kevt1)) + + ps := psnap.Find(uint32(6599)) + require.NotNil(t, ps) + require.Len(t, ps.Threads, 1) + + err := psnap.Remove(&kevent.Kevent{ + Type: ktypes.TerminateThread, + Kparams: kevent.Kparams{ + kparams.ProcessID: {Name: kparams.ProcessID, Type: kparams.PID, Value: uint32(6599)}, + kparams.ThreadID: {Name: kparams.ThreadID, Type: kparams.TID, Value: uint32(3453)}, + }, + }) + require.NoError(t, err) + require.Len(t, ps.Threads, 0) +} diff --git a/pkg/ps/types/marshaller.go b/pkg/ps/types/marshaller.go new file mode 100644 index 000000000..91c9b7652 --- /dev/null +++ b/pkg/ps/types/marshaller.go @@ -0,0 +1,334 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package types + +import ( + "fmt" + htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/kcap/section" + kcapver "github.com/rabbitstack/fibratus/pkg/kcap/version" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/pe" + "github.com/rabbitstack/fibratus/pkg/util/bytes" + "unsafe" +) + +// Marshal produces a byte stream of the process state for writing to the capture file. +func (ps *PS) Marshal() []byte { + b := make([]byte, 0) + + // write pid and ppid + b = append(b, bytes.WriteUint32(ps.PID)...) + b = append(b, bytes.WriteUint32(ps.Ppid)...) + + // write process name + b = append(b, bytes.WriteUint16(uint16(len(ps.Name)))...) + b = append(b, ps.Name...) + // write process command line + b = append(b, bytes.WriteUint16(uint16(len(ps.Comm)))...) + b = append(b, ps.Comm...) + // write full executable path + b = append(b, bytes.WriteUint16(uint16(len(ps.Exe)))...) + b = append(b, ps.Exe...) + // write current working directory + b = append(b, bytes.WriteUint16(uint16(len(ps.Cwd)))...) + b = append(b, ps.Cwd...) + // write SID + b = append(b, bytes.WriteUint16(uint16(len(ps.SID)))...) + b = append(b, ps.SID...) + + // write args + b = append(b, bytes.WriteUint16(uint16(len(ps.Args)))...) + for _, arg := range ps.Args { + b = append(b, bytes.WriteUint16(uint16(len(arg)))...) + b = append(b, arg...) + } + + // write session ID + b = append(b, ps.SessionID) + + // write env vars block + b = append(b, bytes.WriteUint16(uint16(len(ps.Envs)))...) + for k, v := range ps.Envs { + b = append(b, bytes.WriteUint16(uint16(len(k)))...) + b = append(b, k...) + b = append(b, bytes.WriteUint16(uint16(len(v)))...) + b = append(b, v...) + } + + // write handles + sec := section.New(section.Handle, kcapver.HandleSecV1, uint32(len(ps.Handles)), 0) + b = append(b, sec[:]...) + for _, handle := range ps.Handles { + buf := handle.Marshal() + b = append(b, bytes.WriteUint16(handle.Offset())...) + b = append(b, buf...) + } + + // write the PE metadata + if ps.PE != nil { + buf := ps.PE.Marshal() + sec := section.New(section.PE, kcapver.PESecV1, 0, uint32(len(buf))) + b = append(b, sec[:]...) + b = append(b, buf...) + } else { + sec := section.New(section.PE, kcapver.PESecV1, 0, 0) + b = append(b, sec[:]...) + } + + return b +} + +// Unmarshal recovers the process' state from the capture file. +func (ps *PS) Unmarshal(b []byte) error { + if len(b) < 8 { + return fmt.Errorf("expected at least 8 bytes but got %d bytes", len(b)) + } + var offset uint32 + + // read pid/ppid + ps.PID = bytes.ReadUint32(b[0:]) + ps.Ppid = bytes.ReadUint32(b[4:]) + + // read process image name + l := bytes.ReadUint16(b[8:]) + buf := b[10:] + offset = uint32(l) + ps.Name = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + + // read process cmdline + l = bytes.ReadUint16(b[10+offset:]) + buf = b[12+offset:] + offset += uint32(l) + ps.Comm = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + + // read full image path + l = bytes.ReadUint16(b[12+offset:]) + buf = b[14+offset:] + offset += uint32(l) + ps.Exe = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + + // read current working directory + l = bytes.ReadUint16(b[14+offset:]) + buf = b[16+offset:] + offset += uint32(l) + ps.Cwd = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + + // read the SID + l = bytes.ReadUint16(b[16+offset:]) + buf = b[18+offset:] + offset += uint32(l) + if len(buf) > 0 { + ps.SID = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l]) + } + + // read args + nargs := bytes.ReadUint16(b[18+offset:]) + var aoffset uint16 + for i := 0; i < int(nargs); i++ { + l := bytes.ReadUint16(b[20+offset+uint32(aoffset):]) + buf = b[22+offset+uint32(aoffset):] + ps.Args = append(ps.Args, string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l])) + aoffset += 2 + l + } + + offset += uint32(aoffset) + // read session ID + ps.SessionID = b[20+offset] + + // read env vars + nvars := bytes.ReadUint16(b[21+offset:]) + var eoffset uint16 + for i := 0; i < int(nvars); i++ { + klen := bytes.ReadUint16(b[23+offset+uint32(eoffset):]) + buf = b[25+offset+uint32(eoffset):] + key := string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:klen:klen]) + vlen := bytes.ReadUint16(b[25+offset+uint32(eoffset)+uint32(klen):]) + buf = b[27+offset+uint32(eoffset)+uint32(klen):] + value := string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:vlen:vlen]) + ps.Envs[key] = value + eoffset += klen + vlen + 2 + 2 + } + + offset += uint32(eoffset) + + // read handles + sec := section.Read(b[23+offset:]) + offset += 10 // 10 is for the section size in bytes + var hoffset uint32 + if sec.Len() == 0 { + goto readpe + } + + for i := 0; i < int(sec.Len()); i++ { + // read handle length + l := uint32(bytes.ReadUint16(b[23+offset+hoffset:])) + + off := 25 + hoffset + offset + + handle, err := htypes.NewFromKcap(b[off : off+l]) + if err != nil { + return err + } + ps.Handles = append(ps.Handles, handle) + hoffset += l + 2 + } + +readpe: + offset += hoffset + // read PE metadata + sec = section.Read(b[23+offset:]) + if sec.Size() == 0 { + return nil + } + var err error + ps.PE, err = pe.NewFromKcap(b[33+offset:]) + if err != nil { + return err + } + + return nil +} + +// Marshal transforms the thread state to byte stream for persisting to capture files. +func (t *Thread) Marshal() []byte { + b := make([]byte, 0) + + // write thread/process ID + b = append(b, bytes.WriteUint32(t.Tid)...) + b = append(b, bytes.WriteUint32(t.Pid)...) + + // write priority fields + b = append(b, t.IOPrio) + b = append(b, t.BasePrio) + b = append(b, t.PagePrio) + + // write stack/kernel/entrypoint addresses + b = append(b, bytes.WriteUint16(uint16(len(t.UstackBase)))...) + b = append(b, t.UstackBase...) + b = append(b, bytes.WriteUint16(uint16(len(t.UstackLimit)))...) + b = append(b, t.UstackLimit...) + b = append(b, bytes.WriteUint16(uint16(len(t.KstackBase)))...) + b = append(b, t.KstackBase...) + b = append(b, bytes.WriteUint16(uint16(len(t.KstackLimit)))...) + b = append(b, t.KstackLimit...) + b = append(b, bytes.WriteUint16(uint16(len(t.Entrypoint)))...) + b = append(b, t.Entrypoint...) + + return b +} + +// Marshal produces a module byte stream state suitable for writing to capture files. +func (m *Module) Marshal() []byte { + b := make([]byte, 0) + + // write size and checksum + b = append(b, bytes.WriteUint32(m.Size)...) + b = append(b, bytes.WriteUint32(m.Checksum)...) + + // write image name + b = append(b, bytes.WriteUint16(uint16(len(m.Name)))...) + b = append(b, m.Name...) + + // write addresses + b = append(b, bytes.WriteUint16(uint16(len(m.BaseAddress)))...) + b = append(b, m.BaseAddress...) + b = append(b, bytes.WriteUint16(uint16(len(m.DefaultBaseAddress)))...) + b = append(b, m.DefaultBaseAddress...) + + return b +} + +func (t *Thread) Unmarshal(b []byte) (uint16, error) { + if len(b) < 11 { + return 0, fmt.Errorf("expected at least 11 bytes but got %d", len(b)) + } + + // read tid and pid + t.Tid = bytes.ReadUint32(b[0:]) + t.Pid = bytes.ReadUint32(b[4:]) + + // read priorities + t.IOPrio = b[8] + t.BasePrio = b[9] + t.PagePrio = b[10] + + // read user space stack base address + l := bytes.ReadUint16(b[11:]) + buf := b[13:] + offset := l + t.UstackBase = kparams.Hex(string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l])) + + // read user space stack limit + l = bytes.ReadUint16(b[13+offset:]) + buf = b[15+offset:] + offset += l + t.UstackLimit = kparams.Hex(string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l])) + + // read kernel space stack base address + l = bytes.ReadUint16(b[15+offset:]) + buf = b[17+offset:] + offset += l + t.KstackBase = kparams.Hex(string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l])) + + // read kernel space stack limit + l = bytes.ReadUint16(b[17+offset:]) + buf = b[19+offset:] + offset += l + t.KstackLimit = kparams.Hex(string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l])) + + // read entry point address + l = bytes.ReadUint16(b[19+offset:]) + buf = b[21+offset:] + offset += l + t.Entrypoint = kparams.Hex(string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:l:l])) + + return offset + 21, nil +} + +// Unmarshal reconstructs module state from the byte stream. +func (m *Module) Unmarshal(b []byte) (uint16, error) { + if len(b) < 11 { + return 0, fmt.Errorf("expected at least 11 bytes but got %d", len(b)) + } + + // read size + m.Size = bytes.ReadUint32(b[0:]) + // read checksum + m.Checksum = bytes.ReadUint32(b[4:]) + + // read DLL full path + length := bytes.ReadUint16(b[8:]) + buf := b[10:] + offset := length + m.Name = string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:length:length]) + + // read addresses + length = bytes.ReadUint16(b[10+offset:]) + buf = b[12+offset:] + offset += length + m.BaseAddress = kparams.Hex(string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:length:length])) + + length = bytes.ReadUint16(b[12+offset:]) + buf = b[14+offset:] + offset += length + m.DefaultBaseAddress = kparams.Hex(string((*[1<<30 - 1]byte)(unsafe.Pointer(&buf[0]))[:length:length])) + + return offset + 14, nil +} diff --git a/pkg/ps/types/marshaller_test.go b/pkg/ps/types/marshaller_test.go new file mode 100644 index 000000000..43141650a --- /dev/null +++ b/pkg/ps/types/marshaller_test.go @@ -0,0 +1,97 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package types + +import ( + htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestPSMarshaler(t *testing.T) { + ps := &PS{ + PID: 2436, + Ppid: 6304, + Name: "firefox.exe", + Exe: `C:\Program Files\Mozilla Firefox\firefox.exe`, + Comm: `C:\Program Files\Mozilla Firefox\firefox.exe -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, + Cwd: `C:\Program Files\Mozilla Firefox\`, + SID: "archrabbit\\SYSTEM", + Args: []string{"-contentproc", `--channel="6304.3.1055809391\1014207667`, "-childID", "1", "-isForBrowser", "-prefsHandle", "2584", "-prefMapHandle", "2580", "-prefsLen", "70", "-prefMapSize", "216993", "-parentBuildID"}, + SessionID: 4, + Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, + Handles: []htypes.Handle{ + {Num: handle.Handle(0xffffd105e9baaf70), + Name: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, + Type: "Key", + Object: 777488883434455544, + Pid: uint32(1023), + }, + { + Num: handle.Handle(0xffffd105e9adaf70), + Name: `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, + Type: "ALPC Port", + Pid: uint32(1023), + MD: &htypes.AlpcPortInfo{ + Seqno: 1, + Context: 0x0, + Flags: 0x0, + }, + Object: 457488883434455544, + }, + { + Num: handle.Handle(0xeaffd105e9adaf30), + Name: `C:\Users\bunny`, + Type: "File", + Pid: uint32(1023), + MD: &htypes.FileInfo{ + IsDirectory: true, + }, + Object: 357488883434455544, + }, + }, + } + + b := ps.Marshal() + clone, err := NewFromKcap(b) + require.NoError(t, err) + + assert.Equal(t, uint32(2436), clone.PID) + assert.Equal(t, uint32(6304), clone.Ppid) + assert.Equal(t, "firefox.exe", clone.Name) + assert.Equal(t, `C:\Program Files\Mozilla Firefox\firefox.exe`, clone.Exe) + assert.Equal(t, `C:\Program Files\Mozilla Firefox\firefox.exe -contentproc --channel="6304.3.1055809391\1014207667" -childID 1 -isForBrowser -prefsHandle 2584 -prefMapHandle 2580 -prefsLen 70 -prefMapSize 216993 -parentBuildID 20200107212822 -greomni "C:\Program Files\Mozilla Firefox\omni.ja" -appomni "C:\Program Files\Mozilla Firefox\browser\omni.ja" -appdir "C:\Program Files\Mozilla Firefox\browser" - 6304 "\\.\pipe\gecko-crash-server-pipe.6304" 2596 tab`, clone.Comm) + assert.Equal(t, `C:\Program Files\Mozilla Firefox\`, clone.Cwd) + assert.Equal(t, "archrabbit\\SYSTEM", clone.SID) + assert.Equal(t, []string{"-contentproc", `--channel="6304.3.1055809391\1014207667`, "-childID", "1", "-isForBrowser", "-prefsHandle", "2584", "-prefMapHandle", "2580", "-prefsLen", "70", "-prefMapSize", "216993", "-parentBuildID"}, clone.Args) + assert.Equal(t, uint8(4), clone.SessionID) + assert.Equal(t, map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, clone.Envs) + + require.Len(t, clone.Handles, 3) + + alpc := clone.Handles[1] + assert.Equal(t, "ALPC Port", alpc.Type) + assert.Equal(t, `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, alpc.Name) + assert.IsType(t, &htypes.AlpcPortInfo{}, alpc.MD) + + md := alpc.MD.(*htypes.AlpcPortInfo) + assert.Equal(t, uint32(1), md.Seqno) +} diff --git a/pkg/ps/types/types.go b/pkg/ps/types/types.go new file mode 100644 index 000000000..30049fc05 --- /dev/null +++ b/pkg/ps/types/types.go @@ -0,0 +1,293 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package types + +import ( + "fmt" + htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/pe" + hndl "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "path/filepath" + "strings" + "sync" +) + +// PS encapsulates process' state such as allocated resources and other metadata. +type PS struct { + mu sync.RWMutex + // PID is the identifier of this process. This value is valid from the time a process is created until it is terminated. + PID uint32 `json:"pid"` + // Ppipd represents the parent of this process. Process identifier numbers are reused, so they only identify a process + // for the lifetime of that process. It is possible that the process identified by `Ppid` is terminated, + // so `Ppid` may not refer to a running process. It is also possible that `Ppid` incorrectly refers + // to a process that reuses a process identifier. + Ppid uint32 `json:"ppid"` + // Name is the process' image name including file extension (e.g. cmd.exe) + Name string `json:"name"` + // Comm is the full process' command line (e.g. C:\Windows\system32\cmd.exe /cdir /-C /W) + Comm string `json:"comm"` + // Exe is the full name of the process' executable (e.g. C:\Windows\system32\cmd.exe) + Exe string `json:"exe"` + // Cwd designates the current working directory of the process. + Cwd string `json:"cwd"` + // SID is the security identifier under which this process is run. + SID string `json:"sid"` + // Args contains process' command line arguments (e.g. /cdir, /-C, /W) + Args []string `json:"args"` + // SessionID is the unique identifier for the current session. + SessionID uint8 `json:"session"` + // Envs contains process' environment variables indexed by env variable name. + Envs map[string]string `json:"envs"` + // Threads contains all the threads running in the address space of this process. + Threads map[uint32]Thread `json:"-"` + // Modules contains all the modules loaded by the process. + Modules []Module `json:"modules"` + // Handles represents the collection of handles allocated by the process. + Handles htypes.Handles `json:"handles"` + // PE stores the PE (Portable Executable) metadata. + PE *pe.PE `json:"pe"` +} + +// String returns a string representation of the process' state. +func (ps PS) String() string { + return fmt.Sprintf(` + Pid: %d + Ppid: %d + Name: %s + Comm: %s + Exe: %s + Cwd: %s + SID: %s + Args: %s + Session ID: %d + Envs: %s + `, + ps.PID, + ps.Ppid, + ps.Name, + ps.Comm, + ps.Exe, + ps.Cwd, + ps.SID, + ps.Args, + ps.SessionID, + ps.Envs, + ) +} + +// Thread stores several metadata about a thread that's executing in process's address space. +type Thread struct { + // Tid is the unique identifier of thread inside the process. + Tid uint32 + // Pid is the identifier of the process to which this thread pertains. + Pid uint32 + // IOPrio represents an I/O priority hint for scheduling I/O operations generated by the thread. + IOPrio uint8 + // BasePrio is the scheduler priority of the thread. + BasePrio uint8 + // PagePrio is a memory page priority hint for memory pages accessed by the thread. + PagePrio uint8 + // UstackBase is the base address of the thread's user space stack. + UstackBase kparams.Hex + // UstackLimit is the limit of the thread's user space stack. + UstackLimit kparams.Hex + // KStackBase is the base address of the thread's kernel space stack. + KstackBase kparams.Hex + // KstackLimit is the limit of the thread's kernel space stack. + KstackLimit kparams.Hex + // Entrypoint is the starting address of the function to be executed by the thread. + Entrypoint kparams.Hex +} + +// String returns the thread as a human-readable string. +func (t Thread) String() string { + return fmt.Sprintf("ID: %d IO prio: %d, Base prio: %d, Page prio: %d, Ustack base: %s, Ustack limit: %s, Kstack base: %s, Kstack limit: %s, Entrypoint: %s", t.Tid, t.IOPrio, t.BasePrio, t.PagePrio, t.UstackBase, t.UstackLimit, t.KstackBase, t.UstackLimit, t.Entrypoint) +} + +// Module represents the data for all dynamic libraries/executables that reside in the process' address space. +type Module struct { + // Size designates the size in bytes of the image file. + Size uint32 + // Checksum is the checksum of the image file. + Checksum uint32 + // Name represents the full path of this image. + Name string + // BaseAddress is the base address of process in which the image is loaded. + BaseAddress kparams.Hex + // DefaultBaseAddress is the default base address. + DefaultBaseAddress kparams.Hex +} + +// String returns the string representation of the module. +func (m Module) String() string { + return fmt.Sprintf("Name: %s, Size: %d, Checksum: %d, Base address: %s, Default base address: %s", m.Name, m.Size, m.Checksum, m.BaseAddress, m.DefaultBaseAddress) +} + +// FromKevent produces a new process state from kernel event. +func FromKevent(pid, ppid uint32, name, comm, exe, sid string, sessionID uint8) *PS { + return &PS{ + PID: pid, + Ppid: ppid, + Name: name, + Comm: comm, + Exe: exe, + Args: splitArgs(comm), + SID: sid, + SessionID: sessionID, + Threads: make(map[uint32]Thread), + Modules: make([]Module, 0), + Handles: make([]htypes.Handle, 0), + } +} + +// ThreadFromKevent builds a thread info from kernel event. +func ThreadFromKevent(pid, tid uint32, ustackBase, ustackLimit, kstackBase, kstackLimit kparams.Hex, ioPrio, basePrio, pagePrio uint8, entrypoint kparams.Hex) Thread { + return Thread{ + Pid: pid, + Tid: tid, + UstackBase: ustackBase, + UstackLimit: ustackLimit, + KstackBase: kstackBase, + KstackLimit: kstackLimit, + IOPrio: ioPrio, + BasePrio: basePrio, + PagePrio: pagePrio, + Entrypoint: entrypoint, + } +} + +// ImageFromKevent constructs a module info from the corresponding kernel event. +func ImageFromKevent(size, checksum uint32, name string, baseAddress, defaultBaseAddress kparams.Hex) Module { + return Module{ + Size: size, + Checksum: checksum, + Name: name, + BaseAddress: baseAddress, + DefaultBaseAddress: defaultBaseAddress, + } +} + +// NewPS produces a new process state from passed arguments. +func NewPS(pid, ppid uint32, exe, cwd, comm string, thread Thread, envs map[string]string) *PS { + return &PS{ + PID: pid, + Ppid: ppid, + Name: filepath.Base(exe), + Exe: exe, + Comm: comm, + Cwd: cwd, + Args: splitArgs(comm), + Threads: map[uint32]Thread{thread.Tid: thread}, + Modules: make([]Module, 0), + Handles: make([]htypes.Handle, 0), + Envs: envs, + } +} + +// NewFromKcap reconstructs the state of the process from kcap file. +func NewFromKcap(buf []byte) (*PS, error) { + ps := PS{ + Args: make([]string, 0), + Envs: make(map[string]string), + Handles: make([]htypes.Handle, 0), + Modules: make([]Module, 0), + Threads: make(map[uint32]Thread), + } + if err := ps.Unmarshal(buf); err != nil { + return nil, err + } + return &ps, nil +} + +func splitArgs(comm string) []string { + ext := strings.Index(comm, ".exe") + if ext == -1 { + return []string{} + } + ext += 5 + if ext > len(comm) { + return []string{} + } + return strings.Fields(comm[ext:]) +} + +// AddThread adds a thread to process's state descriptor. +func (ps *PS) AddThread(thread Thread) { + ps.mu.Lock() + defer ps.mu.Unlock() + ps.Threads[thread.Tid] = thread +} + +// RemoveThread eliminates a thread from the process's state. +func (ps *PS) RemoveThread(tid uint32) { + ps.mu.Lock() + defer ps.mu.Unlock() + delete(ps.Threads, tid) +} + +// RLock acquires a read mutex on the process state. +func (ps *PS) RLock() { + ps.mu.RLock() +} + +// RLock releases a read mutex on the process sate. +func (ps *PS) RUnlock() { + ps.mu.RUnlock() +} + +// AddHandle adds a new handle to this process state. +func (ps *PS) AddHandle(handle htypes.Handle) { + ps.Handles = append(ps.Handles, handle) +} + +// RemoveHandle removes a handle with specified identifier from the list of allocated handles. +func (ps *PS) RemoveHandle(num hndl.Handle) { + for i, h := range ps.Handles { + if h.Num == num { + ps.Handles = append(ps.Handles[:i], ps.Handles[i+1:]...) + break + } + } +} + +// AddModule adds a new module to this process state. +func (ps *PS) AddModule(mod Module) { + ps.Modules = append(ps.Modules, mod) +} + +// RemoveModule removes a module with specified full-path from this process state. +func (ps *PS) RemoveModule(name string) { + for i, mod := range ps.Modules { + if mod.Name == name { + ps.Modules = append(ps.Modules[:i], ps.Modules[i+1:]...) + break + } + } +} + +// FindModule finds the module by name. +func (ps *PS) FindModule(name string) *Module { + for _, mod := range ps.Modules { + if filepath.Base(mod.Name) == name { + return &mod + } + } + return nil +} diff --git a/pkg/syscall/doc.go b/pkg/syscall/doc.go new file mode 100644 index 000000000..7cb3585a2 --- /dev/null +++ b/pkg/syscall/doc.go @@ -0,0 +1,20 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package syscall contains the definitions of functions, structures and constants for interacting with the Windows API. +package syscall diff --git a/pkg/syscall/etw/etw.go b/pkg/syscall/etw/etw.go new file mode 100644 index 000000000..4b9470e94 --- /dev/null +++ b/pkg/syscall/etw/etw.go @@ -0,0 +1,159 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package etw + +import ( + "os" + "syscall" + "unsafe" + + kerrors "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/syscall/utf16" + "github.com/rabbitstack/fibratus/pkg/syscall/winerrno" +) + +var ( + advapi32 = syscall.NewLazyDLL("advapi32.dll") + + startTrace = advapi32.NewProc("StartTraceW") + controlTrace = advapi32.NewProc("ControlTraceW") + closeTrace = advapi32.NewProc("CloseTrace") + openTrace = advapi32.NewProc("OpenTraceW") + processTrace = advapi32.NewProc("ProcessTrace") + traceSetInformation = advapi32.NewProc("TraceSetInformation") + enableTrace = advapi32.NewProc("EnableTraceEx") +) + +type TraceOperation uint32 + +const ( + Query TraceOperation = 0 + Stop TraceOperation = 1 + Update TraceOperation = 2 + Flush TraceOperation = 3 +) + +// TraceHandle is an alias for trace handle type +type TraceHandle uintptr + +// IsValid determines if the session handle is valid +func (handle TraceHandle) IsValid() bool { return handle != 0 } + +// StartTrace registers and starts an event tracing session for the specified provider. The trace assumes there will be a real-time +// event consumer responsible for collecting and processing events. If the function succeeds, it returns the handle to the tracing +// session. +func StartTrace(name string, props *EventTraceProperties) (TraceHandle, error) { + var handle TraceHandle + errno, _, err := startTrace.Call( + uintptr(unsafe.Pointer(&handle)), + uintptr(unsafe.Pointer(utf16.StringToUTF16Ptr(name))), + uintptr(unsafe.Pointer(props)), + ) + switch winerrno.Errno(errno) { + case winerrno.Success: + return handle, nil + case winerrno.AccessDenied: + return TraceHandle(0), kerrors.ErrTraceAccessDenied + case winerrno.DiskFull: + return TraceHandle(0), kerrors.ErrTraceDiskFull + case winerrno.AlreadyExists: + return TraceHandle(0), kerrors.ErrTraceAlreadyRunning + case winerrno.InvalidParameter: + return TraceHandle(0), kerrors.ErrTraceInvalidParameter + case winerrno.BadLength: + return TraceHandle(0), kerrors.ErrTraceBadLength + case winerrno.NoSysResources: + return TraceHandle(0), kerrors.ErrTraceNoSysResources + default: + return TraceHandle(0), os.NewSyscallError("StartTrace", err) + } +} + +// ControlTrace performs various operation on the specified event tracing session, such as updating, flushing or stopping +// the session. +func ControlTrace(handle TraceHandle, name string, props *EventTraceProperties, operation TraceOperation) error { + errno, _, err := controlTrace.Call( + uintptr(handle), + uintptr(unsafe.Pointer(utf16.StringToUTF16Ptr(name))), + uintptr(unsafe.Pointer(props)), + uintptr(operation), + ) + switch winerrno.Errno(errno) { + case winerrno.Success: + return nil + case winerrno.WMIInstanceNotFound: + return kerrors.ErrKsessionNotRunning + default: + return os.NewSyscallError("ControlTrace", err) + } +} + +// CloseTrace closes a trace. If you call this function before ProcessTrace returns, the CloseTrace function +// returns ErrorCtxClosePending. The ErrorCtxClosePending code indicates that the CloseTrace function call +// was successful; the ProcessTrace function will stop processing events after it processes all events in its buffers. +func CloseTrace(handle TraceHandle) error { + errno, _, err := closeTrace.Call(uintptr(handle)) + if winerrno.Errno(errno) != winerrno.Success && winerrno.Errno(errno) != winerrno.CtxClosePending { + return os.NewSyscallError("CloseTrace", err) + } + return nil +} + +// OpenTrace opens a real-time trace session or log file for consuming. +func OpenTrace(ktrace EventTraceLogfile) TraceHandle { + handle, _, _ := openTrace.Call(uintptr(unsafe.Pointer(&ktrace))) + return TraceHandle(handle) +} + +// ProcessTrace function delivers events from one or more event tracing sessions to the consumer. Function sorts the events +// chronologically and delivers all events generated between StartTime and EndTime. The ProcessTrace function blocks the +// thread until it delivers all events, the BufferCallback function returns false, or you call CloseTrace. +func ProcessTrace(handle TraceHandle) error { + errno, _, err := processTrace.Call(uintptr(unsafe.Pointer(&handle)), 1, 0, 0) + switch winerrno.Errno(errno) { + case winerrno.Success: + return nil + case winerrno.WMIInstanceNotFound: + return kerrors.ErrKsessionNotRunning + case winerrno.NoAccess: + return kerrors.ErrEventCallbackException + case winerrno.Cancelled: + return kerrors.ErrTraceCancelled + default: + return os.NewSyscallError("ProcessTrace", err) + } +} + +// SetTraceInformation enables or disables event tracing session settings for the specified information class. +func SetTraceInformation(handle TraceHandle, infoClass uint8, traceFlags []EventTraceFlags) error { + errno, _, err := traceSetInformation.Call(uintptr(handle), uintptr(infoClass), uintptr(unsafe.Pointer(&traceFlags[0])), unsafe.Sizeof(traceFlags)) + if winerrno.Errno(errno) == winerrno.Success { + return nil + } + return os.NewSyscallError("TraceSetInformation", err) +} + +// EnableTrace influences the behaviour of the specified event trace provider. +func EnableTrace(guid syscall.GUID, handle TraceHandle, keyword uint32) error { + errno, _, err := enableTrace.Call(uintptr(unsafe.Pointer(&guid)), 0, uintptr(handle), 1, 0, uintptr(keyword), 0, 0, 0) + if winerrno.Errno(errno) == winerrno.Success { + return nil + } + return os.NewSyscallError("EnableTraceEx", err) +} diff --git a/pkg/syscall/etw/types.go b/pkg/syscall/etw/types.go new file mode 100644 index 000000000..ad7bbcaed --- /dev/null +++ b/pkg/syscall/etw/types.go @@ -0,0 +1,465 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package etw + +import ( + "strings" + "syscall" +) + +// EventTraceFlags is the type alias for kernel trace events +type EventTraceFlags uint32 + +// KernelTraceControlGUID is the GUID for the kernel system logger +var KernelTraceControlGUID = syscall.GUID{Data1: 0x9e814aad, Data2: 0x3204, Data3: 0x11d2, Data4: [8]byte{0x9a, 0x82, 0x00, 0x60, 0x08, 0xa8, 0x69, 0x39}} + +// KernelRundownGUID is the GUID for the kernel rundown logger +var KernelRundownGUID = syscall.GUID{Data1: 0x3b9c9951, Data2: 0x3480, Data3: 0x4220, Data4: [8]byte{0x93, 0x77, 0x9c, 0x8e, 0x51, 0x84, 0xf5, 0xcd}} + +const ( + // TraceSystemTraceEnableFlagsInfo controls system logger event flags + TraceSystemTraceEnableFlagsInfo = uint8(4) +) + +const ( + // KernelLoggerSession represents the default session name for NT kernel logger + KernelLoggerSession = "NT Kernel Logger" + KernelLoggerRundownSession = "Kernel Rundown Logger" + // WnodeTraceFlagGUID indicates that the structure contains event tracing information + WnodeTraceFlagGUID = 0x00020000 + // ProcessTraceModeRealtime denotes that there will be a real-time consumers for events forwarded from the providers + ProcessTraceModeRealtime = 0x00000100 + // ProcessTraceModeEventRecord is the mode that enables the "event record" format for kernel events + ProcessTraceModeEventRecord = 0x10000000 +) + +const ( + ALPC EventTraceFlags = 0x00100000 + Cswitch EventTraceFlags = 0x00000010 + DbgPrint EventTraceFlags = 0x00040000 + DiskFileIO EventTraceFlags = 0x00000200 + DiskIO EventTraceFlags = 0x00000100 + DiskIOInit EventTraceFlags = 0x00000400 + Dispatcher EventTraceFlags = 0x00000800 + DPC EventTraceFlags = 0x00000020 + Driver EventTraceFlags = 0x00800000 + FileIO EventTraceFlags = 0x02000000 + FileIOInit EventTraceFlags = 0x04000000 + ImageLoad EventTraceFlags = 0x00000004 + Handle EventTraceFlags = 0x80000040 + IRQ EventTraceFlags = 0x00000040 + Job EventTraceFlags = 0x00080000 + NetTCPIP EventTraceFlags = 0x00010000 + Process EventTraceFlags = 0x00000001 + Registry EventTraceFlags = 0x00020000 + Syscall EventTraceFlags = 0x00000080 + Thread EventTraceFlags = 0x00000002 +) + +// String returns the string representation of enabled event trace flags. +func (f EventTraceFlags) String() string { + flags := make([]string, 0) + if f&ALPC == ALPC { + flags = append(flags, "ALPC") + } + if f&Cswitch == Cswitch { + flags = append(flags, "Cswitch") + } + if f&DiskFileIO == DiskFileIO { + flags = append(flags, "DiskFileIO") + } + if f&DiskIO == DiskIO { + flags = append(flags, "DiskIO") + } + if f&FileIO == FileIO { + flags = append(flags, "FileIO") + } + if f&ImageLoad == ImageLoad { + flags = append(flags, "DLL") + } + if f&Handle == Handle { + flags = append(flags, "Handle") + } + if f&NetTCPIP == NetTCPIP { + flags = append(flags, "TCPIP") + } + if f&Process == Process { + flags = append(flags, "Process") + } + if f&Registry == Registry { + flags = append(flags, "Registry") + } + if f&Thread == Thread { + flags = append(flags, "Thread") + } + return strings.Join(flags, ", ") +} + +// WnodeHeader is a member of `EventTraceProperties` structure. The majority of the fields in this structure are not relevant to us. +type WnodeHeader struct { + // BufferSize is the total size of memory allocated, in bytes, for the event tracing session properties. + BufferSize uint32 + // ProviderID is reserved for internal use. + ProviderID uint32 + // HostricalContext is an union field with the following C representation: + // union { + // ULONG64 HistoricalContext; + // struct { + // ULONG Version; + // ULONG Linkage; + // }; + // }; + // On output, HistoricalContext stores the handle to the event tracing session. Version and Linkage fields are reserved for internal use. + HistoricalContext [8]byte + // KernelHandle is an union with the following C representation: + // union { + // HANDLE KernelHandle; + // LARGE_INTEGER TimeStamp; + // }; + // `KernelHandle` is reserved for internal use. `TimeStamp` designates the instant at which the information of this + // structure was updated. + KernelHandle [8]byte + // GUID that defines the session. For NT Kernel Logger session we have to set this member to `SystemTraceControlGuid`. + GUID syscall.GUID + // ClientContext represents clock resolution to use when logging the time stamp for each event. The default is Query performance counter (QPC). + ClientContext uint32 + // Flags must contain `WnodeFlagTracedGUID` to indicate that the structure contains event tracing information. + Flags uint32 +} + +// EventTraceProperties contains information about an event tracing session. Each time a new session is created, or an existing session +// is about to be modified, this structure is used to describe session properties. +type EventTraceProperties struct { + // Wnode structure requires `BufferSize`, `Flags` and `GUID` members to be initialized. + Wnode WnodeHeader + // BufferSize represents the amount of memory allocated for each event tracing session buffer, in kilobytes. + // The maximum buffer size is 1 MB. ETW uses the size of physical memory to calculate this value. + // If an application expects a relatively low event rate, the buffer size should be set to the memory page size. + // To get the page memory size, you can invoke GetSystemInfo() function. + // If the event rate is expected to be relatively high, the application should specify a larger buffer size, + // and should increase the maximum number of buffers. + // + // The buffer size affects the rate at which buffers fill and must be flushed. Although a small buffer size requires + // less memory, it increases the rate at which buffers must be flushed. + BufferSize uint32 + // MinimumBuffers specifies the minimum number of buffers allocated for the event tracing session's buffer pool. + // The minimum number of buffers that you can specify is two buffers per processor. For example, on a single processor machine, + // the minimum number of buffers is two. + MinimumBuffers uint32 + // MaximumBuffers is the maximum number of buffers allocated for the event tracing session's buffer pool. Typically, this value is + // the minimum number of buffers plus twenty. ETW uses the buffer size and the size of physical memory to calculate this value. + MaximumBuffers uint32 + // MaximumFileSize is the maximum size of the file used to log events, in megabytes. + MaximumFileSize uint32 + // LogFileMode determines the logging modes for the event tracing session. You use this member to specify that you want events written to a + // log file, a real-time consumer, or both. In real-time logging mode, if no consumers are available, events will be written + // to disk, and when a consumers begins processing real-time events, the events in the playback file are consumed first. + LogFileMode uint32 + // FlushTimer specifies how often, in seconds, the trace buffers are forcibly flushed. The minimum flush time is 1 second. + // This forced flush is in addition to the automatic flush that occurs whenever a buffer is full and when the trace session + // stops. If zero, ETW flushes buffers as soon as they become full. If nonzero, ETW flushes all buffers that contain events + // based on the timer value. Typically, you want to flush buffers only when they become full. Forcing the buffers to flush + // (either by setting this member to a nonzero value or by calling `FlushTrace`) can increase the file size of the log file + // with unfilled buffer space. + // + // If the consumer is consuming events in real time, you may want to set this member to a nonzero value if the event rate is + // low to force events to be delivered before the buffer is full. + // For the case of a realtime logger, a value of zero (the default value) means that the flush time will be set to 1 second. + // A realtime logger is when LogFileMode is set to `EventTraceRealTimeMode`. + FlushTimer uint32 + // EnableFlags specifies which kernel events are delievered to the consumer when NT Kernel logger session is started. + // For example, registry events, process, disk IO and so on. + EnableFlags EventTraceFlags + // AgeLimit is not used. + AgeLimit int32 + // NumberOfBuffers indicates the number of buffers allocated for the event tracing session's buffer pool. + NumberOfBuffers uint32 + // FreeBuffers indicates the number of buffers that are allocated but unused in the event tracing session's buffer pool. + FreeBuffers uint32 + // EventsLost counts the number of events that were not recorded. + EventsLost uint32 + // BuffersWritten counts the number of buffers written. + BuffersWritten uint32 + // LogBuffersLost determines the number of buffers that could not be written to the log file. + LogBuffersLost uint32 + // RealTimeBuffersLost represents the number of buffers that could not be delivered in real-time to the consumer. + RealTimeBuffersLost uint32 + // LoggerThreadID is the thread identifier for the event tracing session. + LoggerThreadID uintptr + // LogFileNameOffset is the offset from the start of the structure's allocated memory to beginning of the null-terminated + // string that contains the log file name. + LogFileNameOffset uint32 + // LoggerNameOffset is the offset from the start of the structure's allocated memory to beginning of the null-terminated + // string that contains the session name. The session name is limited to 1024 characters. The session name is case-insensitive + // and must be unique. + LoggerNameOffset uint32 +} + +// EventTraceHeader contains standard event tracing information common to all events. +type EventTraceHeader struct { + // Size represents the total number of bytes of the event. It includes the size of the header structure, + // plus the size of any event-specific data appended to the header. + Size uint16 + // FieldTypeFlags is an union field represented as follow: + // union { + // USHORT FieldTypeFlags; + // struct { + // UCHAR HeaderType; + // UCHAR MarkerFlags; + // }; + // }; + // All memebers of this union are reserved for internal use. + FieldTypeFlags [2]byte + // Versions in an union field with the following declaration: + // union { + // ULONG Version; + // struct { + // UCHAR Type; + // UCHAR Level; + // USHORT Version; + // } Class; + // }; + // `Type` field indicates the general purpose type of this event (e.g. data collection end/start, checkpoint, etc.) + // `Level` designates the severity of the generated event and the `Version` tells the consumer which MOF class to use to + // decipher the event data. + Version [4]byte + // ThreadID identifes the thread that generated this event. + ThreadID uint32 + // ProcessID identifes the process that generated this event. + ProcessID uint32 + // Timestamp contains the time that the event occurred. + Timestamp uint64 + // GUID is an union: + // union { + // GUID Guid; + // ULONGLONG GuidPtr; + // }; + // `Guid` identifies a category of events. `GuidPtr` is the pointer to an event trace class GUID. + GUID [16]byte + // ProcessorTime is another union type: + // union { + // struct { + // ULONG ClientContext; + // ULONG Flags; + // }; + // struct { + // ULONG KernelTime; + // ULONG UserTime; + // }; + // ULONG64 ProcessorTime; + // }; + // `ClientContext` is reserved, while `Flags` must be set to `WnodeFlagTracedGuid`. The rest of the members + // specify elapsed execution time for kernel and user mode instructions respectively. + ProcessorTime [8]byte +} + +// EventTrace stores event information that is delivered to an event trace consumer. +type EventTrace struct { + // Header contains standard event tracing metadata. + Header EventTraceHeader + // InstanceID represents the instance identifier. + InstanceID uint32 + // ParentInstanceID represents instance identifer for a parent event. + ParentInstanceID uint32 + // ParentGUID is the class GUID of the parent event. + ParentGUID syscall.GUID + // MofData is the pointer to the beginning of the event-specific data for this event. + MofData uintptr + // MofLength represents the number of bytes to which `MofData` points. + MofLength uint32 + // Context is an union type: + // union { + // ULONG ClientContext; + // ETW_BUFFER_CONTEXT BufferContext; + // }; + // `ClientContext` field is reserved. `BufferContext` Provides information about the event such as the session identifier + // and processor number of the CPU on which the provider process ran. + Context [2]byte +} + +// TraceLogfileHeader contains information about an event tracing session and its events. +type TraceLogfileHeader struct { + // BufferSize is the size of the event tracing session's buffers in bytes. + BufferSize uint32 + // Version is the union type that represents version number of the operating system. + Version [4]byte + // ProviderVersion is the build number of the operating system. + ProviderVersion uint32 + // NumberOfProcessors indicates the number of processors on the system. + NumberOfProcessors uint32 + // EndTime is the time at which the event tracing session stopped. This value is 0 for real time event consumers. + EndTime uint64 + // TimerResolution is the resolution of the hardware timer, in units of 100 nanoseconds. + TimerResolution uint32 + // MaximumFileSize is the size of the log file, in megabytes. + MaximumFileSize uint32 + // LogfileMode represents the current logging mode for the event tracing session. + LogfileMode uint32 + // BuffersWritten is the total number of buffers written by the event tracing session. + BuffersWritten uint32 + // GUID is a an union type with the two first field reserved for internal usage. Other fields indicate + // the number of events lost and the CPU speed in Mhz. + GUID [16]byte + // LoggerName is a reserved field. + LoggerName *uint16 + // LogfileName is a reserved field. + LogfileName *uint16 + // TimeZone contains time-zone information for `BootTime`, `EndTime` and `StartTime` fields. + TimeZone syscall.Timezoneinformation + // BootTime is the time at which the system was started, in 100-nanosecond intervals since midnight, January 1, 1601. + BootTime uint64 + // PerfFreq is the frequency of the high-resolution performance counter, if one exists. + PerfFreq uint64 + // StartTime is the time at which the event tracing session started, in 100-nanosecond intervals since midnight, January 1, 1601. + StartTime uint64 + // ReservedFlags specifies the clock type. + ReservedFlags uint32 + // BuffersLost is the total number of buffers lost during the event tracing session. + BuffersLost uint32 +} + +// EventTraceLogfile specifies how the consumer wants to read events (from a log file or in real-time) and the callbacks +// that will receive the events.When ETW flushes a buffer, this structure contains information about the event tracing +// session and the buffer that ETW flushed. +type EventTraceLogfile struct { + // LogFileName is the name of the log file used by the event tracing session. + LogFileName *uint16 + // LoggerName is the name of the event tracing session. Only applicable when consuming events in real time. + LoggerName *uint16 + // CurrentTime on output, the current time, in 100-nanosecond intervals since midnight, January 1, 1601. + CurrentTime int64 + // BuffersRead represents the number of buffers processed. + BuffersRead uint32 + // LogFileMode is union type the dictates the processing mode for events. + LogFileMode [4]byte + // CurrentEvents contains the last event processed. + CurrentEvent EventTrace + // LogfileHeader represents global information about the tracing session. + LogfileHeader TraceLogfileHeader + // BufferCallback is a pointer to the function that receives buffer-related statistics for each buffer ETW flushes. + // ETW calls this callback after it delivers all the events in the buffer. + BufferCallback uintptr + // BufferSize contains the size of each buffer, in bytes. + BufferSize uint32 + // Filled contains the number of bytes in the buffer that contain valid information. + Filled uint32 + // EventsLost is an unused field. + EventsLost uint32 + // EventCallback is the union field that contains pointers to callback functions that ETW calls for each buffer. + EventCallback [8]byte + // IsKernelTrace specifies whether the event tracing session is the NT kernel logger. + IsKernelTrace uint32 + // Context is data that a consumer can specify when calling `OpenTrace` function. + Context uintptr +} + +// EventDescriptor contains metadata that defines the event. +type EventDescriptor struct { + // ID represents event identifier. + ID uint16 + // Version indicates a revision to the event definition. + Version uint8 + // Channel is the audience for the event (e.g. administrator or developer). + Channel uint8 + // Level is the severity or level of detail included in the event. + Level uint8 + // Opcode is step in a sequence of operations being performed within the `Task` field. For MOF-defined events, + // the `Opcode` member contains the event type value. + Opcode uint8 + // Task represents a larger unit of work within an application or component. + Task uint16 + // Keyword A bitmask that specifies a logical group of related events. Each bit corresponds to one group. An event may belong to one or more groups. + // The keyword can contain one or more provider-defined keywords, standard keywords, or both. + Keyword uint64 +} + +// EventHeader defines information about the event. +type EventHeader struct { + // Size represents the size of the event, in bytes. + Size uint16 + // HeaderType is reserved. + HeaderType uint16 + // Flags provides information about the event such as the type of session it was logged to and if + // the event contains extended data. + Flags uint16 + // EventProperty indicates the source to use for parsing the event data. + EventProperty uint16 + // ThreadID identifies the thread that generated the event. + ThreadID uint32 + // ProcessID identifies the process that generated the event. + ProcessID uint32 + // Timestamps contains the time that the event occurred. + Timestamp uint64 + // ProviderID is the GUID that uniquely identifies the provider that logged the event. + ProviderID syscall.GUID + // EventDescriptor defines the information about the event such as the event identifier and severity level. + EventDescriptor EventDescriptor + // ProcessorTime is the union type that defines elapsed execution time for kernel-mode and user-mode instructions + // in CPU units. + ProcessorTime [8]byte + // ActivityID is the identifier that relates two events. + ActivityID syscall.GUID +} + +// ETWBufferContexts provides context information about the event. +type ETWBufferContext struct { + // ProcessorIndex is an union type that contains among other fields the number of the CPU on which + // the provider process was running. + ProcessorIndex [2]byte + // LoggerID identifies of the session that logged the event. + LoggerID uint16 +} + +// Linkage is the inner struct for EventHeaderExtendedDataItem. +type Linkage struct { + Linkage uint16 + Resreved2 uint16 +} + +// EventHeaderExtendedDataItem defines the extended data that ETW collects as part of the event data. +type EventHeaderExtendedDataItem struct { + // Reserverd1 is a reserved field. + Reserved1 uint16 + // ExtType defines the type of extended data. + ExtType uint16 + Linkage + // DataSize is the size in bytes, of the extended data + DataSize uint16 + // DataPtr is the pointer to extended data. + DataPtr uint64 +} + +// EventRecord defines the layout of an event that ETW delivers. +type EventRecord struct { + // Header represents information about the event such as the time stamp for when it was written. + Header EventHeader + // BufferContext defines information such as the session that logged the event. + BufferContext ETWBufferContext + // ExtendedDataCount is the number of extended data structures in the `ExtendedData` field. + ExtendedDataCount uint16 + // UserDataLength represents the size, in bytes, of the data in the `UserData` field. + UserDataLength uint16 + // ExtendedData designates extended data items that ETW collects. The extended data includes some items, such as the security + // identifier (SID) of the user that logged the event. + ExtendedData *EventHeaderExtendedDataItem + // UserData represents event specific data that's parsed via TDH API. + UserData uintptr + // UserContext is a pointer to custom user data passed in `EventTraceLogfile` structure. + UserContext uintptr +} diff --git a/pkg/syscall/file/file.go b/pkg/syscall/file/file.go new file mode 100644 index 000000000..6de55705b --- /dev/null +++ b/pkg/syscall/file/file.go @@ -0,0 +1,124 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package file + +import ( + "github.com/rabbitstack/fibratus/pkg/syscall/utf16" + "github.com/rabbitstack/fibratus/pkg/syscall/winerrno" + "os" + "syscall" + "unsafe" +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32") + shlwapi = syscall.NewLazyDLL("shlwapi") + nt = syscall.NewLazyDLL("ntdll") + + getLogicalDrives = kernel32.NewProc("GetLogicalDrives") + queryDosDevice = kernel32.NewProc("QueryDosDeviceW") + pathIsDirectory = shlwapi.NewProc("PathIsDirectoryW") + + ntQueryVolumeInformationFile = nt.NewProc("NtQueryVolumeInformationFile") + + drives = []string{ + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z"} +) + +const ( + fsDeviceInformation = 4 +) + +// GetLogicalDrives returns available device drives in the system. +func GetLogicalDrives() []string { + r, _, _ := getLogicalDrives.Call() + bitmask := uint32(r) + devs := make([]string, 0) + for _, drive := range drives { + if bitmask&1 == 1 { + devs = append(devs, drive+":") + } + bitmask >>= 1 + } + return devs +} + +// QueryVolumeInfo obtains device information for the specified file handle. +func QueryVolumeInfo(fd uintptr) (*DevInfo, error) { + var ( + iosb ioStatusBlock + di DevInfo + ) + _, _, err := ntQueryVolumeInformationFile.Call( + fd, + uintptr(unsafe.Pointer(&iosb)), + uintptr(unsafe.Pointer(&di)), + uintptr(unsafe.Sizeof(di)), + uintptr(fsDeviceInformation), + ) + if err != nil && err != syscall.Errno(0) { + return nil, os.NewSyscallError("NtQueryVolumeInformationFile", err) + } + return &di, nil +} + +// QueryDosDevice translates the DOS device name to hard disk drive letter. +func QueryDosDevice(drive string) (string, error) { + dev := make([]uint16, syscall.MAX_PATH) + errno, _, err := queryDosDevice.Call( + uintptr(unsafe.Pointer(utf16.StringToUTF16Ptr(drive))), + uintptr(unsafe.Pointer(&dev[0])), + uintptr(syscall.MAX_PATH), + ) + if winerrno.Errno(errno) == winerrno.Success { + return "", os.NewSyscallError("QueryDosDevice", err) + } + return syscall.UTF16ToString(dev), nil +} + +// IsPathDirectory indicates if path is a valid directory. +func IsPathDirectory(path string) bool { + isDir, _, _ := pathIsDirectory.Call(uintptr(unsafe.Pointer(utf16.StringToUTF16Ptr(path)))) + return isDir > 0 +} diff --git a/pkg/syscall/file/types.go b/pkg/syscall/file/types.go new file mode 100644 index 000000000..bcb0f52fb --- /dev/null +++ b/pkg/syscall/file/types.go @@ -0,0 +1,54 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package file + +import "syscall" + +// AttributeData contains meta information about a file. +type AttributeData struct { + // FileAttributes represents the file attributes. + FileAttributes uint32 + // CreationTime specifies when a file or directory is created. If the underlying file system does not support creation time, this member is zero. + CreationTime syscall.Filetime + // LastAccessTime for a file, the structure specifies the last time that a file is read from or written to. + // For a directory, the structure specifies when the directory is created. For both files and directories, + // the specified date is correct, but the time of day is always set to midnight. If the underlying file + // system does not support the last access time, this member is zero (0). + LastAccessTime syscall.Filetime + // LastWriteTime for a file, the structure specifies the last time that a file is written to. For a directory, + // the structure specifies when the directory is created. If the underlying file system does not support the last write time, + // this member is zero (0). + LastWriteTime syscall.Filetime + // FileSizeHigh high-order part of the file size. + FileSizeHigh uint32 + // FileSizeLow low-order part of the file size. + FileSizeLow uint32 +} + +// DevInfo provides file system device information about the type of device object associated with a file object. +type DevInfo struct { + // Type designates the type of underlying device. + Type uint32 + // Characteristics represents device characteristics. + Characteristcs uint32 +} + +type ioStatusBlock struct { + status, information uintptr +} diff --git a/pkg/syscall/handle/handle.go b/pkg/syscall/handle/handle.go new file mode 100644 index 000000000..cba0c5ef8 --- /dev/null +++ b/pkg/syscall/handle/handle.go @@ -0,0 +1,83 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package handle + +import ( + "github.com/rabbitstack/fibratus/pkg/syscall/winerrno" + "os" + "syscall" + "unsafe" +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + + closeHandle = kernel32.NewProc("CloseHandle") + duplicateHandle = kernel32.NewProc("DuplicateHandle") +) + +// Handle represents the handle type. +type Handle uintptr + +// DuplicateAccess is the enum for handle duplicate access flags. +type DuplicateAccess uint32 + +const ( + // ThreadQueryAccess determines that handle duplication requires the ability to query thread info. + ThreadQueryAccess DuplicateAccess = 0x0040 + // ProcessQueryAccess determines that handle duplication requires the ability to query process info. + ProcessQueryAccess DuplicateAccess = 0x1000 + // ReadControlAccess specifies the ability to query the security descriptor. + ReadControlAccess DuplicateAccess = 0x00020000 + // SemaQueryAccess is the duplicate access type required for synchronization objects such as mutants. + SemaQueryAccess DuplicateAccess = 0x0001 + // AllAccess doesn't specify the access mask. + AllAccess DuplicateAccess = 0 +) + +// IsValid determines if handle instance if valid. +func (handle Handle) IsValid() bool { + return handle != ^Handle(0) +} + +// Close disposes the underlying handle object. +func (handle Handle) Close() { + if handle == 0 { + return + } + _, _, _ = closeHandle.Call(uintptr(handle)) +} + +// Duplicate duplicates an object handle in the caller address's space. +func (handle Handle) Duplicate(src, dest Handle, access DuplicateAccess) (Handle, error) { + var destHandle Handle + errno, _, err := duplicateHandle.Call( + uintptr(src), + uintptr(handle), + uintptr(dest), + uintptr(unsafe.Pointer(&destHandle)), + uintptr(access), + 0, + 0, + ) + if winerrno.Errno(errno) != winerrno.Success { + return destHandle, nil + } + return Handle(0), os.NewSyscallError("DuplicateHandle", err) +} diff --git a/pkg/syscall/object/alpc.go b/pkg/syscall/object/alpc.go new file mode 100644 index 000000000..136346ec0 --- /dev/null +++ b/pkg/syscall/object/alpc.go @@ -0,0 +1,50 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package object + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "unsafe" +) + +// AlpcInformationClass defines the type for the ALPC information class values. +type AlpcInformationClass uint8 + +const ( + // AlpcBasicPortInfo obtains basic ALPC port information + AlpcBasicPortInfo AlpcInformationClass = iota +) + +var ntAlpcQueryInformation = nt.NewProc("NtAlpcQueryInformation") + +// GetAlpcInformation gets specified information for the ALPC handle. +func GetAlpcInformation(handle handle.Handle, klass AlpcInformationClass, buf []byte) error { + status, _, _ := ntAlpcQueryInformation.Call( + uintptr(handle), + uintptr(klass), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(len(buf)), + 0, + ) + if status != 0 { + return fmt.Errorf("NtAlpcQueryInformation failed with status code 0x%X", status) + } + return nil +} diff --git a/pkg/syscall/object/event.go b/pkg/syscall/object/event.go new file mode 100644 index 000000000..87fafa8e4 --- /dev/null +++ b/pkg/syscall/object/event.go @@ -0,0 +1,73 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package object + +import ( + "os" + "syscall" +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32") + createEvent = kernel32.NewProc("CreateEventA") + setEvent = kernel32.NewProc("SetEvent") + resetEvent = kernel32.NewProc("ResetEvent") +) + +type Event uintptr + +func NewEvent(manualReset, isSignaled bool) (Event, error) { + var reset uint8 + var signaled uint8 + if manualReset { + reset = 1 + } + if isSignaled { + signaled = 1 + } + handle, _, err := createEvent.Call(0, uintptr(reset), uintptr(signaled), 0) + if handle == 0 { + return Event(0), os.NewSyscallError("CreateEventA", err) + } + return Event(handle), nil +} + +func NewNamedEvent() { + +} + +func (e Event) Set() error { + errno, _, err := setEvent.Call(uintptr(e)) + if errno == 0 { + return os.NewSyscallError("SetEvent", err) + } + return nil +} + +func (e Event) Reset() error { + errno, _, err := resetEvent.Call(uintptr(e)) + if errno == 0 { + return os.NewSyscallError("ResetEvent", err) + } + return nil +} + +func (e Event) Close() error { + return syscall.Close(syscall.Handle(e)) +} diff --git a/pkg/syscall/object/mutant.go b/pkg/syscall/object/mutant.go new file mode 100644 index 000000000..2410d4bdf --- /dev/null +++ b/pkg/syscall/object/mutant.go @@ -0,0 +1,48 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package object + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "unsafe" +) + +var ntQueryMutant = nt.NewProc("NtQueryMutant") + +type MutantInformationClass uint8 + +const ( + MutantBasicInfo MutantInformationClass = iota +) + +// QueryMutant gets mutant detalied information according to the information class. +func QueryMutant(handle handle.Handle, klass MutantInformationClass, buf []byte) error { + status, _, _ := ntQueryMutant.Call( + uintptr(handle), + uintptr(klass), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(len(buf)), + 0, + ) + if status != 0 { + return fmt.Errorf("NtQueryMutant failed with status code 0x%X", status) + } + return nil +} diff --git a/pkg/syscall/object/object.go b/pkg/syscall/object/object.go new file mode 100644 index 000000000..e3d14e105 --- /dev/null +++ b/pkg/syscall/object/object.go @@ -0,0 +1,63 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package object + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/rabbitstack/fibratus/pkg/syscall/winerrno" + "syscall" + "unsafe" +) + +var ( + nt = syscall.NewLazyDLL("ntdll") + + ntQueryObject = nt.NewProc("NtQueryObject") +) + +type InformationClass uint8 + +const ( + NameInformationClass InformationClass = 1 + TypeInformationClass InformationClass = 2 + TypesInformationClass InformationClass = 3 + SystemHandleInformationClass = 16 + SystemExtendedHandleInformation = 64 +) + +// Query retrieves specified information for the handle reference. +func Query(handle handle.Handle, klass InformationClass, buf []byte) (uint32, error) { + size := uint32(len(buf)) + status, _, _ := ntQueryObject.Call( + uintptr(handle), + uintptr(klass), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(size), + uintptr(unsafe.Pointer(&size)), + ) + if status != 0 { + if status == winerrno.StatusInfoLengthMismatch || status == winerrno.StatusBufferTooSmall { + return size, errors.ErrNeedsReallocateBuffer + } + return size, fmt.Errorf("NtQueryObject failed with status code 0x%X", status) + } + return size, nil +} diff --git a/pkg/syscall/object/types.go b/pkg/syscall/object/types.go new file mode 100644 index 000000000..4dbdd2290 --- /dev/null +++ b/pkg/syscall/object/types.go @@ -0,0 +1,100 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package object + +import ( + "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/rabbitstack/fibratus/pkg/syscall/utf16" +) + +type genericMapping struct { + GenericRead uint32 + GenericWrite uint32 + GenericExecute uint32 + GenericAll uint32 +} + +// TypeInformation contains object type data. +type TypeInformation struct { + TypeName utf16.UnicodeString + TotalNumberOfObjects uint32 + TotalNumberOfHandles uint32 + TotalPagedPoolUsage uint32 + TotalNonPagedPoolUsage uint32 + TotalNamePoolUsage uint32 + TotalHandleTableUsage uint32 + HighWaterNumberOfObjects uint32 + HighWaterNumberOfHandles uint32 + HighWaterPagedPoolUsage uint32 + HighWaterNonPagedPoolUsage uint32 + HighWaterNamePoolUsage uint32 + HighWaterHandleTableUsage uint32 + InvalidAttributes uint32 + GenericMapping genericMapping + ValidAccessMask uint32 + SecurityRequired bool + MaintainHandleCount bool + TypeIndex uint8 + ReservedByte int8 + PoolType uint32 + DefaultPagedPoolCharge uint32 + DefaultNonPagedPoolCharge uint32 +} + +// TypesInformation stores the number of resolved object type names. +type TypesInformation struct { + NumberOfTypes uint32 +} + +type NameInformation struct { + ObjectName utf16.UnicodeString +} + +type ProcessHandleTableEntryInfo struct { + Handle handle.Handle + HandleCount uintptr + PointerCount uintptr + GrantedAccess uint32 + ObjectTypeIndex uint32 + HandleAttributes uint32 + reserved uint32 +} + +type ProcessHandleSnapshotInformation struct { + NumberOfHandles uintptr + reserved uintptr + Handles [1]ProcessHandleTableEntryInfo +} + +type SystemHandleTableEntryInfoEx struct { + Object uint64 + ProcessID uintptr + Handle handle.Handle + GrantedAccess uint32 + CreatorBackTraceIndex uint8 + ObjectTypeIndex uint8 + HandleAttributes uint32 + reserved uint32 +} + +type SystemHandleInformationEx struct { + NumberOfHandles uintptr + reserved uintptr + Handles [1]SystemHandleTableEntryInfoEx +} diff --git a/pkg/syscall/process/process.go b/pkg/syscall/process/process.go new file mode 100644 index 000000000..b299e91ec --- /dev/null +++ b/pkg/syscall/process/process.go @@ -0,0 +1,174 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package process + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/rabbitstack/fibratus/pkg/syscall/winerrno" + "os" + "syscall" + "time" + "unsafe" +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + native = syscall.NewLazyDLL("ntdll.dll") + + openProcess = kernel32.NewProc("OpenProcess") + queryFullProcessImageName = kernel32.NewProc("QueryFullProcessImageNameW") + readProcessMemory = kernel32.NewProc("ReadProcessMemory") + ntQueryInformationProcess = native.NewProc("NtQueryInformationProcess") + getProcessTimes = kernel32.NewProc("GetProcessTimes") + getProcessIdOfThread = kernel32.NewProc("GetProcessIdOfThread") + getExitCodeProcess = kernel32.NewProc("GetExitCodeProcess") +) + +// DesiredAccess defines the type alias for process's access modifiers +type DesiredAccess uint32 + +const ( + procStatusStillActive = 259 + // QueryInformation is required to retrieve certain information about a process, such as its token, exit code, and priority class + QueryInformation DesiredAccess = 0x0400 + // QueryLimitedInformation is required to get certain information about process, such as process's image name + QueryLimitedInformation DesiredAccess = 0x1000 + // VMRead is required to read memory in a process + VMRead DesiredAccess = 0x0010 + // DupHandle lets duplicate handles of the target process + DupHandle DesiredAccess = 0x0040 +) + +// InfoClassFlags defines the type for process's info class +type InfoClassFlags uint8 + +const ( + // BasicInformationClass returns basic process's information + BasicInformationClass InfoClassFlags = 0 + HandleInformationClass InfoClassFlags = 51 +) + +// Open acquires an handle from the running process. +func Open(access DesiredAccess, inheritHandle bool, processID uint32) (handle.Handle, error) { + var inherit uint8 + if inheritHandle { + inherit = 1 + } else { + inherit = 0 + } + h, _, err := openProcess.Call(uintptr(access), uintptr(inherit), uintptr(processID)) + if h == 0 { + return handle.Handle(0), os.NewSyscallError("OpenProcess", err) + } + return handle.Handle(h), nil +} + +// QueryFullImageName retrieves the full name of the executable image for the specified process. +func QueryFullImageName(handle handle.Handle) (string, error) { + var size uint32 = syscall.MAX_PATH + name := make([]uint16, size) + + errno, _, err := queryFullProcessImageName.Call(uintptr(handle), uintptr(0), uintptr(unsafe.Pointer(&name[0])), uintptr(unsafe.Pointer(&size))) + if winerrno.Errno(errno) != winerrno.Success { + return syscall.UTF16ToString(name), nil + } + return "", os.NewSyscallError("QueryFullProcessImageName", err) +} + +// QueryInfo retrieves a variety of process's information depending on the info class passed to this function. +func QueryInfo(handle handle.Handle, infoClass InfoClassFlags, buf []byte) (uint32, error) { + var size uint32 + if ntQueryInformationProcess != nil { + errno, _, _ := ntQueryInformationProcess.Call(uintptr(handle), uintptr(infoClass), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), uintptr(unsafe.Pointer(&size))) + if winerrno.Errno(errno) != winerrno.Success { + if errno == winerrno.StatusInfoLengthMismatch || errno == winerrno.StatusBufferTooSmall { + return size, errors.ErrNeedsReallocateBuffer + } + return size, fmt.Errorf("NtQueryInformationProcess failed with status code 0x%X", errno) + } + return size, nil + } + return size, nil +} + +// ReadMemory reads data from an area of memory in a specified process. The entire area to be read must be accessible or the operation fails. +func ReadMemory(handle handle.Handle, addr unsafe.Pointer, size uintptr) ([]byte, error) { + buf := make([]byte, size) + errno, _, err := readProcessMemory.Call(uintptr(handle), uintptr(addr), uintptr(unsafe.Pointer(&buf[0])), size, uintptr(0)) + if winerrno.Errno(errno) != winerrno.Success { + return buf, nil + } + return nil, os.NewSyscallError("ReadProcessMemory", err) +} + +func ReadMemoryUnicode(handle handle.Handle, addr unsafe.Pointer, size uintptr) ([]uint16, error) { + buf := make([]uint16, size) + errno, _, err := readProcessMemory.Call(uintptr(handle), uintptr(addr), uintptr(unsafe.Pointer(&buf[0])), size, uintptr(0)) + if winerrno.Errno(errno) != winerrno.Success { + return buf, nil + } + return nil, os.NewSyscallError("ReadProcessMemory", err) +} + +// GetParentPID returns the identifier of the parent process from the process's basic information structure. +func GetParentPID(handle handle.Handle) uint32 { + buf := make([]byte, unsafe.Sizeof(BasicInformation{})) + _, err := QueryInfo(handle, BasicInformationClass, buf) + if err != nil { + return uint32(0) + } + info := (*BasicInformation)(unsafe.Pointer(&buf[0])) + return uint32(info.InheritedFromUniqueProcessID) +} + +// GetStartTime returns process's timing statistics. +func GetStartTime(handle handle.Handle) (time.Time, error) { + var ( + ct syscall.Filetime + xt syscall.Filetime + kt syscall.Filetime + ut syscall.Filetime + ) + errno, _, err := getProcessTimes.Call(uintptr(handle), uintptr(unsafe.Pointer(&ct)), uintptr(unsafe.Pointer(&xt)), uintptr(unsafe.Pointer(&kt)), uintptr(unsafe.Pointer(&ut))) + if winerrno.Errno(errno) != winerrno.Success { + return time.Unix(0, ct.Nanoseconds()), nil + } + return time.Now(), os.NewSyscallError("GetProcessTime", err) +} + +// GetPIDFromThread returns the pid to which the specified thread belongs. +func GetPIDFromThread(handle handle.Handle) (uint32, error) { + pid, _, err := getProcessIdOfThread.Call(uintptr(handle)) + if pid == 0 { + return uint32(0), os.NewSyscallError("GetProcessIdOfThread", err) + } + return uint32(pid), nil +} + +// IsAlive checks if the process identified by the specified handle is still in running state. +func IsAlive(handle handle.Handle) bool { + var exitCode uint32 + errno, _, _ := getExitCodeProcess.Call(uintptr(handle), uintptr(unsafe.Pointer(&exitCode))) + if winerrno.Errno(errno) == winerrno.Success { + return false + } + return exitCode == procStatusStillActive +} diff --git a/pkg/syscall/process/types.go b/pkg/syscall/process/types.go new file mode 100644 index 000000000..ece61db26 --- /dev/null +++ b/pkg/syscall/process/types.go @@ -0,0 +1,76 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package process + +import "github.com/rabbitstack/fibratus/pkg/syscall/utf16" + +type PEB struct { + Reserved1 [2]byte + BeingDebugged byte + Reserved2 [21]byte + LDR *LDRData + ProcessParameters *RTLUserProcessParameters + Reserved3 [520]byte + PostProcessInitRoutine uintptr + Reserved4 [136]byte + SessionID uint32 +} + +type BasicInformation struct { + Reserved1 uintptr + PEB *PEB + Reserved2 [2]uintptr + UniqueProcessID uintptr + InheritedFromUniqueProcessID uintptr +} + +type String struct { + Length uint8 + MaximumLength uint8 +} + +type RTLUserProcessParameters struct { + Reserved1 [16]byte + consoleHandle uintptr + consoleFlags uint32 + stdin uintptr + stdout uintptr + stderr uintptr + CurrentDirectory CurDir + dllPath utf16.UnicodeString + ImagePathName utf16.UnicodeString + CommandLine utf16.UnicodeString + Environment uintptr +} + +type CurDir struct { + DosPath utf16.UnicodeString + Handle uintptr +} + +type LDRData struct { + Reserved1 [8]byte + Reserved2 [3]uintptr + ModuleList ListEntry +} + +type ListEntry struct { + Flink *ListEntry + Blink *ListEntry +} diff --git a/pkg/syscall/registry/key.go b/pkg/syscall/registry/key.go new file mode 100644 index 000000000..8ebc3d387 --- /dev/null +++ b/pkg/syscall/registry/key.go @@ -0,0 +1,52 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package registry + +// Key is the type alias for the registry root keys +type Key uint32 + +const ( + InvalidKey Key = 0 +) + +const ( + ClassesRoot Key = 0x80000000 + iota + CurrentUser + LocalMachine + Users + Hive +) + +// String returns a human-readable root key name. +func (k Key) String() string { + switch k { + case ClassesRoot: + return "HKEY_CLASSES_ROOT" + case CurrentUser: + return "HKEY_CURRENT_USER" + case LocalMachine: + return "HKEY_LOCAL_MACHINE" + case Users: + return "HKEY_USERS" + case Hive: + return "" + default: + return "Unknown" + } +} diff --git a/pkg/syscall/security/privileges.go b/pkg/syscall/security/privileges.go new file mode 100644 index 000000000..4e50092ea --- /dev/null +++ b/pkg/syscall/security/privileges.go @@ -0,0 +1,158 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package security + +import ( + "bytes" + "encoding/binary" + "github.com/pkg/errors" + "sync" + "syscall" + "unsafe" +) + +var ( + procLookupPrivilegeValueW = advapi32.NewProc("LookupPrivilegeValueW") + procAdjustTokenPrivileges = advapi32.NewProc("AdjustTokenPrivileges") +) + +// Cache of privilege names to LUIDs. +var ( + privNames = make(map[string]int64) + privNameMutex sync.Mutex +) + +const ( + // SeDebugPrivilege is the name of the privilege used to debug programs. + SeDebugPrivilege = "SeDebugPrivilege" +) + +// Errors returned by AdjustTokenPrivileges. +const ( + ERROR_NOT_ALL_ASSIGNED syscall.Errno = 1300 +) + +// Attribute bits for privileges. +const ( + _SE_PRIVILEGE_ENABLED_BY_DEFAULT uint32 = 0x00000001 + _SE_PRIVILEGE_ENABLED uint32 = 0x00000002 + _SE_PRIVILEGE_REMOVED uint32 = 0x00000004 + _SE_PRIVILEGE_USED_FOR_ACCESS uint32 = 0x80000000 +) + +func _LookupPrivilegeValue(systemName string, name string, luid *int64) (err error) { + var _p0 *uint16 + _p0, err = syscall.UTF16PtrFromString(systemName) + if err != nil { + return + } + var _p1 *uint16 + _p1, err = syscall.UTF16PtrFromString(name) + if err != nil { + return + } + return __LookupPrivilegeValue(_p0, _p1, luid) +} + +func __LookupPrivilegeValue(systemName *uint16, name *uint16, luid *int64) (err error) { + r1, _, e1 := syscall.Syscall(procLookupPrivilegeValueW.Addr(), 3, uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(luid))) + if r1 == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func _AdjustTokenPrivileges(token syscall.Token, releaseAll bool, input *byte, outputSize uint32, output *byte, requiredSize *uint32) (success bool, err error) { + var _p0 uint32 + if releaseAll { + _p0 = 1 + } else { + _p0 = 0 + } + r0, _, e1 := syscall.Syscall6(procAdjustTokenPrivileges.Addr(), 6, uintptr(token), uintptr(_p0), uintptr(unsafe.Pointer(input)), uintptr(outputSize), uintptr(unsafe.Pointer(output)), uintptr(unsafe.Pointer(requiredSize))) + success = r0 != 0 + if true { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +// mapPrivileges maps privilege names to LUID values. +func mapPrivileges(names []string) ([]int64, error) { + var privileges []int64 + privNameMutex.Lock() + defer privNameMutex.Unlock() + for _, name := range names { + p, ok := privNames[name] + if !ok { + err := _LookupPrivilegeValue("", name, &p) + if err != nil { + return nil, errors.Wrapf(err, "LookupPrivilegeValue failed on '%v'", name) + } + privNames[name] = p + } + privileges = append(privileges, p) + } + return privileges, nil +} + +// EnableTokenPrivileges enables the specified privileges in the given +// Token. The token must have TOKEN_ADJUST_PRIVILEGES access. If the token +// does not already contain the privilege it cannot be enabled. +func EnableTokenPrivileges(token syscall.Token, privileges ...string) error { + privValues, err := mapPrivileges(privileges) + if err != nil { + return err + } + + var b bytes.Buffer + binary.Write(&b, binary.LittleEndian, uint32(len(privValues))) + for _, p := range privValues { + binary.Write(&b, binary.LittleEndian, p) + binary.Write(&b, binary.LittleEndian, uint32(_SE_PRIVILEGE_ENABLED)) + } + + success, err := _AdjustTokenPrivileges(token, false, &b.Bytes()[0], uint32(b.Len()), nil, nil) + if !success { + return err + } + if err == ERROR_NOT_ALL_ASSIGNED { + return errors.Wrap(err, "error not all privileges were assigned") + } + + return nil +} + +// SetDebugPrivilege sets the debug privilege in the current running process. +func SetDebugPrivilege() { + h, err := syscall.GetCurrentProcess() + if err == nil { + var token syscall.Token + _ = syscall.OpenProcessToken(syscall.Handle(h), syscall.TOKEN_ADJUST_PRIVILEGES|syscall.TOKEN_QUERY, &token) + _ = EnableTokenPrivileges(token, SeDebugPrivilege) + } +} diff --git a/pkg/syscall/security/sid.go b/pkg/syscall/security/sid.go new file mode 100644 index 000000000..d423abf46 --- /dev/null +++ b/pkg/syscall/security/sid.go @@ -0,0 +1,131 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package security + +import ( + "errors" + "github.com/rabbitstack/fibratus/pkg/syscall/winerrno" + "os" + "syscall" + "unsafe" +) + +var ( + advapi32 = syscall.NewLazyDLL("advapi32.dll") + netapi32 = syscall.NewLazyDLL("netapi32.dll") + + lookupAccountSid = advapi32.NewProc("LookupAccountSidW") + netUserEnum = netapi32.NewProc("NetUserEnum") + netBufferFree = netapi32.NewProc("NetApiBufferFree") +) + +const ( + userMaxPreferredLength = 0xFFFFFFFF +) + +// LookupAccount returns the account and domain name from a security identifier. +func LookupAccount(buffer []byte, wbemSID bool) (string, string) { + n := uint32(50) + dn := uint32(50) + sid := uintptr(unsafe.Pointer(&buffer[0])) + + if wbemSID { + // a WBEM SID is actually a TOKEN_USER structure followed + // by the SID, so we have to double the pointer size + sid += uintptr(8 * 2) + } + var accType uint32 + for { + b := make([]uint16, n) + db := make([]uint16, dn) + errno, _, _ := lookupAccountSid.Call( + 0, + sid, + uintptr(unsafe.Pointer(&b[0])), + uintptr(unsafe.Pointer(&n)), + uintptr(unsafe.Pointer(&db[0])), + uintptr(unsafe.Pointer(&dn)), + uintptr(unsafe.Pointer(&accType)), + 0, + 0) + + if winerrno.Errno(errno) != winerrno.Success { + return syscall.UTF16ToString(b), syscall.UTF16ToString(db) + } + if winerrno.Errno(errno) != winerrno.InsufficientBuffer { + return "", "" + } + if n <= uint32(len(b)) { + return "", "" + } + } +} + +type userInfo struct { + name *uint16 +} + +// LookupAllSids returns SIDs for each user account in the system. +func LookupAllSids() ([]string, error) { + var ( + buf uintptr + handle uintptr + read uint32 + total uint32 + ) + + errno, _, err := netUserEnum.Call( + uintptr(0), + uintptr(uint32(0)), + uintptr(0), + uintptr(unsafe.Pointer(&buf)), + uintptr(uint32(userMaxPreferredLength)), + uintptr(unsafe.Pointer(&read)), + uintptr(unsafe.Pointer(&total)), + uintptr(unsafe.Pointer(&handle)), + ) + if winerrno.Errno(errno) != winerrno.Success { + return nil, os.NewSyscallError("NetUserEnum", err) + } + + if buf == uintptr(0) { + return nil, os.NewSyscallError("NetUserEnum", errors.New("null buffer pointer")) + } + sids := make([]string, 0) + entry := buf + for i := uint32(0); i < read; i++ { + info := (*userInfo)(unsafe.Pointer(entry)) + if info == nil { + continue + } + username := syscall.UTF16ToString((*[4096]uint16)(unsafe.Pointer(info.name))[:]) + sid, _, _, err := syscall.LookupSID("", username) + if err != nil { + continue + } + s, err := sid.String() + if err != nil { + continue + } + sids = append(sids, s) + entry = uintptr(unsafe.Pointer(entry + unsafe.Sizeof(userInfo{}))) + } + netBufferFree.Call(buf) + return sids, nil +} diff --git a/pkg/syscall/sys/sys.go b/pkg/syscall/sys/sys.go new file mode 100644 index 000000000..f289bf9a7 --- /dev/null +++ b/pkg/syscall/sys/sys.go @@ -0,0 +1,57 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package sys + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/syscall/object" + "github.com/rabbitstack/fibratus/pkg/syscall/winerrno" + "syscall" + "unsafe" +) + +var ( + native = syscall.NewLazyDLL("ntdll") + + ntQuerySystemInformation = native.NewProc("NtQuerySystemInformation") + rtlNtStatusToDosError = native.NewProc("RtlNtStatusToDosError") +) + +// QuerySystemInformation retrieves system low-level information. +func QuerySystemInformation(class object.InformationClass, buf []byte) (uint32, error) { + size := uint32(0) + status, _, _ := ntQuerySystemInformation.Call(uintptr(class), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(len(buf)), + uintptr(unsafe.Pointer(&size))) + if status != 0 { + if status == winerrno.StatusInfoLengthMismatch || status == winerrno.StatusBufferTooSmall { + return size, errors.ErrNeedsReallocateBuffer + } + return size, fmt.Errorf("NtQuerySystemInformation failed with status code 0x%X", status) + } + return size, nil +} + +// CodeFromNtStatus converts the specified NTSTATUS code to its equivalent system error code. +func CodeFromNtStatus(status uint32) uint32 { + code, _, _ := rtlNtStatusToDosError.Call(uintptr(status)) + return uint32(code) +} diff --git a/pkg/syscall/tdh/tdh.go b/pkg/syscall/tdh/tdh.go new file mode 100644 index 000000000..9e9a36ef6 --- /dev/null +++ b/pkg/syscall/tdh/tdh.go @@ -0,0 +1,90 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package tdh + +import ( + kerrors "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/syscall/etw" + "github.com/rabbitstack/fibratus/pkg/syscall/winerrno" + "os" + "syscall" + "unsafe" +) + +var ( + tdh = syscall.NewLazyDLL("tdh.dll") + + tdhGetEventInformation = tdh.NewProc("TdhGetEventInformation") + tdhGetPropertySize = tdh.NewProc("TdhGetPropertySize") + tdhGetProperty = tdh.NewProc("TdhGetProperty") +) + +// GetEventInformation retrieves metadata about an event. It receives a buffer that to allocate +// `TraceEventInfo` structure. +func GetEventInformation(evt *etw.EventRecord, buffer []byte, size uint32) error { + errno, _, err := tdhGetEventInformation.Call( + uintptr(unsafe.Pointer(evt)), + 0, + 0, + uintptr(unsafe.Pointer(&buffer[0])), + uintptr(unsafe.Pointer(&size)), + ) + switch winerrno.Errno(errno) { + case winerrno.Success: + return nil + case winerrno.InsufficientBuffer: + return kerrors.ErrInsufficentBuffer + case winerrno.NotFound: + return kerrors.ErrEventSchemaNotFound + default: + return os.NewSyscallError("TdhGetEventInformation", err) + } +} + +func GetPropertySize(evt *etw.EventRecord, descriptor *PropertyDataDescriptor) (uint32, error) { + var size uint32 + errno, _, err := tdhGetPropertySize.Call( + uintptr(unsafe.Pointer(evt)), + 0, + 0, + 1, + uintptr(unsafe.Pointer(descriptor)), + uintptr(unsafe.Pointer(&size)), + ) + if winerrno.Errno(errno) != winerrno.Success { + return uint32(0), os.NewSyscallError("TdhGetPropertySize", err) + } + return size, nil +} + +func GetProperty(evt *etw.EventRecord, descriptor *PropertyDataDescriptor, size uint32, buffer []byte) error { + errno, _, err := tdhGetProperty.Call( + uintptr(unsafe.Pointer(evt)), + 0, + 0, + 1, + uintptr(unsafe.Pointer(descriptor)), + uintptr(size), + uintptr(unsafe.Pointer(&buffer[0])), + ) + if winerrno.Errno(errno) != winerrno.Success { + return os.NewSyscallError("TdhGetProperty", err) + } + return nil +} diff --git a/pkg/syscall/tdh/types.go b/pkg/syscall/tdh/types.go new file mode 100644 index 000000000..8314af476 --- /dev/null +++ b/pkg/syscall/tdh/types.go @@ -0,0 +1,139 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package tdh + +import ( + "github.com/rabbitstack/fibratus/pkg/syscall/etw" + sc "syscall" + "unsafe" +) + +const ( + IntypeNull = iota + IntypeUnicodeString + IntypeAnsiString + IntypeInt8 + IntypeUint8 + IntypeInt16 + IntypeUint16 + IntypeInt32 + IntypeUint32 + IntypeInt64 + IntypeUint64 + IntypeFloat + IntypeDouble + IntypeBoolean + IntypeBinary + IntypeGUID + IntypePointer + IntypeFiletime + IntypeSystime + IntypeSID + IntypeHexInt32 + IntypeHexInt64 + IntypeCountedString = 300 + IntypeCountedAnsiString = 301 + IntypeReversedCountedString = 302 + IntypeReversedCountedAnsiString = 303 + IntypeNoNullTerminatedString = 304 + IntypeNoNulTerminatedAnsiString = 305 + IntypeUnicodeChar = 306 + IntypeAnsiChar = 307 + IntypeSizet = 308 + IntypeHexdump = 309 + IntypeWbemSID = 310 +) + +const ( + OutypeNull = iota + OutypeString + OutypeDatetime + OutypeByte + OutypeUnsignedByte + OutypeShort + OutypeUnsignedShort + OutypeInt + OutypeUnsignedInt + OutypeLong + OutypeUnsignedLong + OutypeFloat + OutypeDouble + OutypeBoolean + OutypeGUID + OutypeHexBinary + OutypeHexInt8 + OutypeHexInt16 + OutypeHexInt32 + OutypeHexInt64 + OutypePID + OutypeTID + OutypePort + OutypeIPv4 + OutypeIPv6 + OutypeSocketAddress + OutypeCIMDatetime + OutypeETWTime + OutypeXML + OUTYTPEErrorCode + OutypeReducedString = 300 +) + +type NonStructType struct { + InType uint16 + OutType uint16 + MapNameOffset uint32 +} + +type EventPropertyInfo struct { + Flags int32 + NameOffset uint32 + Types [8]byte + Count [2]byte + Length [2]byte + Reserved [4]byte +} + +type TraceEventInfo struct { + ProviderGUID sc.GUID + EventGUID sc.GUID + EventDescriptor etw.EventDescriptor + DecodingSource int32 + ProviderNameOffset uint32 + LevelNameOffset uint32 + ChannelNameOffset uint32 + KeywordsNameOffset uint32 + TaskNameOffset uint32 + OpcodeNameOffset uint32 + EventMessageOffset uint32 + ProviderMessageOffset uint32 + BinaryXMLOffset uint32 + BinaryXMLSize uint32 + EventNameOffset [4]byte + EventAttributeOffset [4]byte + PropertyCount uint32 + TopLevelPropertyCount uint32 + Flags [4]byte + EventPropertyInfoArray [1]EventPropertyInfo +} + +type PropertyDataDescriptor struct { + PropertyName unsafe.Pointer + ArrayIndex uint32 + Reserved uint32 +} diff --git a/pkg/syscall/thread/thread.go b/pkg/syscall/thread/thread.go new file mode 100644 index 000000000..a96743d7b --- /dev/null +++ b/pkg/syscall/thread/thread.go @@ -0,0 +1,79 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package thread + +import ( + "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/rabbitstack/fibratus/pkg/syscall/utf16" + "os" + "syscall" + "unsafe" +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + + openThread = kernel32.NewProc("OpenThread") + createThread = kernel32.NewProc("CreateThread") + terminateThread = kernel32.NewProc("TerminateThread") +) + +// DesiredAccess defines the type alias for thread's access modifiers +type DesiredAccess uint32 + +const ( + // QueryLimitedInformation is required to get certain information from the thread objects (e.g. PID to which pertains some thread) + QueryLimitedInformation DesiredAccess = 0x0800 +) + +type threadNameInfo struct { + ThreadName utf16.UnicodeString +} + +// Open opens an existing thread object. +func Open(access DesiredAccess, inheritHandle bool, threadID uint32) (handle.Handle, error) { + var inherit uint8 + if inheritHandle { + inherit = 1 + } else { + inherit = 0 + } + h, _, err := openThread.Call(uintptr(access), uintptr(inherit), uintptr(threadID)) + if h == 0 { + return handle.Handle(0), os.NewSyscallError("OpenThread", err) + } + return handle.Handle(h), nil +} + +func Create(ctx unsafe.Pointer, cb uintptr) (handle.Handle, uint32, error) { + var threadID uint32 + h, _, err := createThread.Call(0, 0, cb, uintptr(ctx), 0, uintptr(unsafe.Pointer(&threadID))) + if h == 0 { + return handle.Handle(0), threadID, os.NewSyscallError("CreateThread", err) + } + return handle.Handle(h), threadID, nil +} + +func Terminate(handle handle.Handle, exitCode uint32) error { + errno, _, err := terminateThread.Call(uintptr(handle), uintptr(exitCode)) + if errno == 0 { + return os.NewSyscallError("TerminateThread", err) + } + return nil +} diff --git a/pkg/syscall/utf16/string.go b/pkg/syscall/utf16/string.go new file mode 100644 index 000000000..1e90d8c44 --- /dev/null +++ b/pkg/syscall/utf16/string.go @@ -0,0 +1,73 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utf16 + +import ( + "reflect" + "syscall" + "unicode/utf16" + "unsafe" +) + +// UnicodeString stores the size and the memory buffer of the unicode string. +type UnicodeString struct { + Length uint16 + MaxLength uint16 + Buffer *uint16 +} + +// String returns the native string from the Unicode stream. +func (u UnicodeString) String() string { + if u.Length == 0 { + return "" + } + var s []uint16 + hdr := (*reflect.SliceHeader)(unsafe.Pointer(&s)) + hdr.Data = uintptr(unsafe.Pointer(u.Buffer)) + hdr.Len = int(u.Length / 2) + hdr.Cap = int(u.MaxLength / 2) + return string(utf16.Decode(s)) +} + +// StringToUTF16Ptr returns the pointer to UTF-8 encoded string. It will silently return +// an invalid pointer if `s` argument contains a NUL byte at any location. +func StringToUTF16Ptr(s string) *uint16 { + var p *uint16 + p, _ = syscall.UTF16PtrFromString(s) + return p +} + +// UTF16PtrToString is like UTF16ToString, but takes *uint16 +// as a parameter instead of []uint16. +func UTF16PtrToString(p unsafe.Pointer) string { + if p == nil { + return "" + } + var s []uint16 + hdr := (*reflect.SliceHeader)(unsafe.Pointer(&s)) + hdr.Data = uintptr(p) + hdr.Cap = 1 + hdr.Len = 1 + for s[len(s)-1] != 0 { + hdr.Cap++ + hdr.Len++ + } + // Remove trailing NUL and decode into a Go string.. + return string(utf16.Decode(s[:len(s)-1])) +} diff --git a/pkg/syscall/ver/ver.go b/pkg/syscall/ver/ver.go new file mode 100644 index 000000000..293c19d33 --- /dev/null +++ b/pkg/syscall/ver/ver.go @@ -0,0 +1,24 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ver + +// IsWin8OrGreater indicates if the current OS version matches, or is greater than, the Windows 8 version. +func IsWin8OrGreater() bool { + return true +} diff --git a/pkg/syscall/winerrno/errors.go b/pkg/syscall/winerrno/errors.go new file mode 100644 index 000000000..8f5f60b39 --- /dev/null +++ b/pkg/syscall/winerrno/errors.go @@ -0,0 +1,53 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package winerrno + +// Errno is the type alias for error codes returned by API functions +type Errno uintptr + +const ( + // InvalidProcessTraceHandle designates an invalid trace handle reference + InvalidProcessTraceHandle uint64 = 0xffffffffffffffff + // InvalidPID indicates invalid process identifier value + InvalidPID uint32 = 0xffffffff + StatusInfoLengthMismatch = 0xC0000004 + StatusBufferTooSmall = 0xC0000023 + // StatusBufferOverflow indicates that the data was too small to fit in the buffer + StatusBufferOverflow = 0x80000005 + // Success determines successful return code + Success Errno = 0x0 + InvalidParameter Errno = 0x57 + AlreadyExists Errno = 0xb7 + DiskFull Errno = 0x70 + AccessDenied Errno = 0x5 + NoSysResources Errno = 0x5aa + BadLength Errno = 0x18 + WMIInstanceNotFound Errno = 0x1069 + Cancelled Errno = 0x4c7 + NoAccess Errno = 0x3e6 + InsufficientBuffer Errno = 0x7a + NotFound Errno = 0x490 + // CtxClosePending indicates that function will stop after it has processed all real-time events in + // its buffers (it will not receive any new events) + CtxClosePending Errno = 0x1B5F +) + +func (e Errno) IsNotFound() bool { + return e == NotFound +} diff --git a/pkg/util/bytes/bytes.go b/pkg/util/bytes/bytes.go new file mode 100644 index 000000000..5674bd949 --- /dev/null +++ b/pkg/util/bytes/bytes.go @@ -0,0 +1,80 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package bytes + +import ( + "encoding/binary" + "unsafe" +) + +// NativeEndian represents the endianness of the current machine +var NativeEndian binary.ByteOrder + +func init() { + InitNativeEndian(nil) +} + +// InitNativeEndian figures out the endianness of the current machine (https://stackoverflow.com/questions/51332658/any-better-way-to-check-endianness-in-go) +func InitNativeEndian(b []byte) { + buf := [8]byte{} + if b != nil && len(b) == 8 { + copy(buf[:], b[:8]) + } else { + *(*uint64)(unsafe.Pointer(&buf[0])) = uint64(0x6669627261747573) + } + + switch buf { + case [8]byte{0x73, 0x75, 0x74, 0x61, 0x72, 0x62, 0x69, 0x66}: + NativeEndian = binary.LittleEndian + case [8]byte{0x66, 0x69, 0x62, 0x72, 0x61, 0x74, 0x75, 0x73}: + NativeEndian = binary.BigEndian + default: + panic("could not determine native endianness") + } +} + +func ReadUint16(b []byte) uint16 { + return NativeEndian.Uint16(b) +} + +func ReadUint32(b []byte) uint32 { + return NativeEndian.Uint32(b) +} + +func ReadUint64(b []byte) uint64 { + return NativeEndian.Uint64(b) +} + +func WriteUint16(v uint16) (b []byte) { + b = make([]byte, 2) + NativeEndian.PutUint16(b, v) + return +} + +func WriteUint32(v uint32) (b []byte) { + b = make([]byte, 4) + NativeEndian.PutUint32(b, v) + return +} + +func WriteUint64(v uint64) (b []byte) { + b = make([]byte, 8) + NativeEndian.PutUint64(b, v) + return +} diff --git a/pkg/util/fasttemplate/doc.go b/pkg/util/fasttemplate/doc.go new file mode 100644 index 000000000..e9ffd8131 --- /dev/null +++ b/pkg/util/fasttemplate/doc.go @@ -0,0 +1,25 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Package fasttemplate implements simple and fast template library. Credits goes to: https://github.com/valyala/fasttemplate +// +// Fasttemplate is faster than text/template, strings.Replace +// and strings.Replacer. +// +// Fasttemplate ideally fits for fast and simple placeholders' substitutions. +package fasttemplate diff --git a/pkg/util/fasttemplate/template.go b/pkg/util/fasttemplate/template.go new file mode 100644 index 000000000..f02024db5 --- /dev/null +++ b/pkg/util/fasttemplate/template.go @@ -0,0 +1,210 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 Aliaksandr Valialkin + * Modifications Copyright (c) 2019-2020 by Nedim Sabic + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + */ + +package fasttemplate + +import ( + "bytes" + "errors" + "fmt" + "github.com/valyala/bytebufferpool" + "io" +) + +// Template implements simple template engine, which can be used for fast +// tags' (aka placeholders) substitution. +type Template struct { + template string + startTag string + endTag string + + texts [][]byte + tags []string + byteBufferPool bytebufferpool.Pool +} + +// NewTemplate parses the given template using the given startTag and endTag +// as tag start and tag end. +// +// The returned template can be executed by concurrently running goroutines +// using Execute* methods. +func NewTemplate(template, startTag, endTag string) (*Template, error) { + var t Template + err := t.Reset(template, startTag, endTag) + if err != nil { + return nil, err + } + return &t, nil +} + +// TagFunc can be used as a substitution value in the map passed to Execute*. +// Execute* functions pass tag (placeholder) name in 'tag' argument. +// +// TagFunc must be safe to call from concurrently running goroutines. +// +// TagFunc must write contents to w and return the number of bytes written. +type TagFunc func(w io.Writer, tag string) (int, error) + +// Reset resets the template t to new one defined by +// template, startTag and endTag. +// +// Reset allows Template object re-use. +// +// Reset may be called only if no other goroutines call t methods at the moment. +func (t *Template) Reset(template, startTag, endTag string) error { + // Keep these vars in t, so GC won't collect them and won't break + // vars derived via unsafe* + t.template = template + t.startTag = startTag + t.endTag = endTag + t.texts = t.texts[:0] + t.tags = t.tags[:0] + + if len(startTag) == 0 { + return errors.New("start tag cannot be empty") + } + if len(endTag) == 0 { + return errors.New("end tag cannot be empty") + } + + s := stringToBytes(template) + a := stringToBytes(startTag) + b := stringToBytes(endTag) + + tagsCount := bytes.Count(s, a) + if tagsCount == 0 { + return nil + } + + if tagsCount+1 > cap(t.texts) { + t.texts = make([][]byte, 0, tagsCount+1) + } + if tagsCount > cap(t.tags) { + t.tags = make([]string, 0, tagsCount) + } + + for { + n := bytes.Index(s, a) + if n < 0 { + t.texts = append(t.texts, s) + break + } + t.texts = append(t.texts, s[:n]) + + s = s[n+len(a):] + n = bytes.Index(s, b) + if n < 0 { + return fmt.Errorf("cannot find end tag=%q in the template=%q starting from %q", endTag, template, s) + } + + t.tags = append(t.tags, bytesToString(s[:n])) + s = s[n+len(b):] + } + + return nil +} + +// ExecuteFunc calls f on each template tag (placeholder) occurrence. +// +// Returns the number of bytes written to w. +// +// This function is optimized for frozen templates. +// Use ExecuteFunc for constantly changing templates. +func (t *Template) ExecuteFunc(w io.Writer, f TagFunc) (int64, error) { + var nn int64 + + n := len(t.texts) - 1 + if n == -1 { + ni, err := w.Write(stringToBytes(t.template)) + return int64(ni), err + } + + for i := 0; i < n; i++ { + ni, err := w.Write(t.texts[i]) + nn += int64(ni) + if err != nil { + return nn, err + } + + ni, err = f(w, t.tags[i]) + nn += int64(ni) + if err != nil { + return nn, err + } + } + ni, err := w.Write(t.texts[n]) + nn += int64(ni) + return nn, err +} + +// Execute substitutes template tags (placeholders) with the corresponding +// values from the map m and writes the result to the given writer w. +// +// Substitution map m may contain values with the following types: +// * []byte - the fastest value type +// * string - convenient value type +// * TagFunc - flexible value type +// +// Returns the number of bytes written to w. +func (t *Template) Execute(w io.Writer, m map[string]interface{}) (int64, error) { + return t.ExecuteFunc(w, func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) }) +} + +// ExecuteFuncString calls f on each template tag (placeholder) occurrence +// and substitutes it with the data written to TagFunc's w. +// +// Returns the resulting string. +// +// This function is optimized for frozen templates. +// Use ExecuteFuncString for constantly changing templates. +func (t *Template) ExecuteFuncString(f TagFunc) []byte { + bb := t.byteBufferPool.Get() + if _, err := t.ExecuteFunc(bb, f); err != nil { + return []byte{} + } + s := bb.Bytes() + bb.Reset() + t.byteBufferPool.Put(bb) + return s +} + +// ExecuteString substitutes template tags (placeholders) with the corresponding +// values from the map m and returns the result. +// +// Substitution map m may contain values with the following types: +// * []byte - the fastest value type +// * string - convenient value type +// * TagFunc - flexible value type +// +// This function is optimized for frozen templates. +// Use ExecuteString for constantly changing templates. +func (t *Template) ExecuteString(m map[string]interface{}) []byte { + return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) }) +} + +func stdTagFunc(w io.Writer, tag string, m map[string]interface{}) (int, error) { + v := m[tag] + if v == nil { + return 0, nil + } + switch value := v.(type) { + case []byte: + return w.Write(value) + case string: + return w.Write([]byte(value)) + case TagFunc: + return value(w, tag) + default: + return 0, fmt.Errorf("tag=%q contains unexpected value type=%#v. Expected []byte, string or TagFunc", tag, v) + } +} diff --git a/pkg/util/fasttemplate/unsafe.go b/pkg/util/fasttemplate/unsafe.go new file mode 100644 index 000000000..6552e08ca --- /dev/null +++ b/pkg/util/fasttemplate/unsafe.go @@ -0,0 +1,33 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 Aliaksandr Valialkin + * Modifications Copyright (c) 2019-2020 by Nedim Sabic + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + */ + +package fasttemplate + +import ( + "reflect" + "unsafe" +) + +func bytesToString(b []byte) string { + return *(*string)(unsafe.Pointer(&b)) +} + +func stringToBytes(s string) []byte { + sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) + bh := reflect.SliceHeader{ + Data: sh.Data, + Len: sh.Len, + Cap: sh.Len, + } + return *(*[]byte)(unsafe.Pointer(&bh)) +} diff --git a/pkg/util/filetime/filetime.go b/pkg/util/filetime/filetime.go new file mode 100644 index 000000000..19636186e --- /dev/null +++ b/pkg/util/filetime/filetime.go @@ -0,0 +1,30 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package filetime + +import ( + "syscall" + "time" +) + +// ToEpoch converts file timestamp to Unix time. +func ToEpoch(ts uint64) time.Time { + ft := &syscall.Filetime{HighDateTime: uint32(ts >> 32), LowDateTime: uint32(ts)} + return time.Unix(0, ft.Nanoseconds()) +} diff --git a/pkg/util/hostname/hostname.go b/pkg/util/hostname/hostname.go new file mode 100644 index 000000000..f788b85e6 --- /dev/null +++ b/pkg/util/hostname/hostname.go @@ -0,0 +1,109 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package hostname + +import "C" +import ( + "expvar" + "net" + "os" + "syscall" + "unsafe" +) + +// hostname is the current host name or FQDN +var hostname string + +// hostnameErrors exposes host/fqdn resolution errors +var hostnameErrors = expvar.NewMap("hostname.errors") + +const computerNamePhysicalDnsFullyQualified = 7 + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + getComputerName = kernel32.NewProc("GetComputerNameExW") +) + +// Get returns the host name or the FQDN of the machine. +func Get() string { + if hostname != "" { + return hostname + } + var err error + hostname, err = os.Hostname() + if err != nil { + hostnameErrors.Add(err.Error(), 1) + } + + // get the Fully Qualified Domain Name (FQDN) of this machine + maxComputerLength := 1024 + buf := make([]uint16, maxComputerLength) + errno, _, err := getComputerName.Call( + uintptr(computerNamePhysicalDnsFullyQualified), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&maxComputerLength))) + if errno == 0 { + // we couldn't get the hostname neither the FQDN + // so we try to fetch the local IP and use as hostname + if hostname == "" { + ip := localIP() + if ip != "" { + hostname = ip + } else { + hostname = "unknown" + } + } + hostnameErrors.Add(err.Error(), 1) + return hostname + } + + fqdn := syscall.UTF16ToString(buf) + if fqdn != "" { + hostname = fqdn + } + + return hostname +} + +// localIP returns the first non-loopback interface IP address. +func localIP() string { + ifaces, err := net.Interfaces() + if err != nil { + return "" + } + for _, i := range ifaces { + addrs, err := i.Addrs() + if err != nil { + continue + } + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + if !ip.IsLoopback() { + return ip.String() + } + } + } + return "" +} diff --git a/pkg/util/ip/ip.go b/pkg/util/ip/ip.go new file mode 100644 index 000000000..613ca994d --- /dev/null +++ b/pkg/util/ip/ip.go @@ -0,0 +1,46 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ip + +import ( + "net" + "syscall" + "unsafe" +) + +var ( + nt = syscall.NewLazyDLL("ntdll.dll") + + // rtlIpv6AddressToString is the procedure for `RtlIpv6AddressToStringW` API call. + rtlIpv6AddressToString = nt.NewProc("RtlIpv6AddressToStringW") +) + +// ToIPv4 accepts an integer IP address in network byte order and returns an IP-typed address. +func ToIPv4(ip uint32) net.IP { + return net.IPv4(byte(ip), byte(ip>>8), byte(ip>>16), byte(ip>>24)) +} + +// ToIPv6 converts the buffer with IPv6 address in network byte order to an IP-typed address. +func ToIPv6(buffer []byte) net.IP { + ipv6 := make([]uint16, 46) + if rtlIpv6AddressToString != nil { + _, _, _ = rtlIpv6AddressToString.Call(uintptr(unsafe.Pointer(&buffer[0])), uintptr(unsafe.Pointer(&ipv6[0]))) + } + return net.ParseIP(syscall.UTF16ToString(ipv6)) +} diff --git a/pkg/util/ip/ip_test.go b/pkg/util/ip/ip_test.go new file mode 100644 index 000000000..2bcb18835 --- /dev/null +++ b/pkg/util/ip/ip_test.go @@ -0,0 +1,34 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ip + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestToIPv4(t *testing.T) { + ip := ToIPv4(16777343) + assert.Equal(t, "127.0.0.1", ip.String()) +} + +func TestToIPv6(t *testing.T) { + ip := ToIPv6([]byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}) + assert.Equal(t, "::1", ip.String()) +} diff --git a/pkg/util/log/_fixtures/.gitkeep b/pkg/util/log/_fixtures/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/util/log/_fixtures/fibratus.log b/pkg/util/log/_fixtures/fibratus.log new file mode 100644 index 000000000..1f45c1934 --- /dev/null +++ b/pkg/util/log/_fixtures/fibratus.log @@ -0,0 +1 @@ +time="2020-11-07T21:58:53+01:00" level=info msg="fibratus initialized" source="log/logger_test.go:28" diff --git a/pkg/util/log/config.go b/pkg/util/log/config.go new file mode 100644 index 000000000..64755cf49 --- /dev/null +++ b/pkg/util/log/config.go @@ -0,0 +1,76 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package log + +import ( + "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +const ( + logLevel = "logging.level" + logMaxAge = "logging.max-age" + logMaxBackups = "logging.max-backups" + logMaxSize = "logging.max-size" + logFormatter = "logging.formatter" + logPath = "logging.path" + logStdout = "logging.log-stdout" +) + +// Config contains a set of setting that control the behaviour of the logging system. +type Config struct { + // Level specifies the minimum allowed log level. + Level string `json:"logging.level" yaml:"logging.level"` + // MaxAge is the maximum number of days to retain old log files based on the + // timestamp encoded in their filename. + MaxAge int `json:"logging.max-age" yaml:"logging.max-age"` + // MaxBackups is the maximum number of old log files to retain. + MaxBackups int `json:"logging.max-backups" yaml:"logging.max-backups"` + // MaxSize is the maximum size in megabytes of the log file before it gets rotated. + MaxSize int `json:"logging.max-size" yaml:"logging.max-size"` + // Formatter represents the log formatter (json | text ). + Formatter string `json:"logging.formatter" yaml:"logging.formatter"` + // Path represents the alternative paths for storing the logs. + Path string `json:"logging.path" yaml:"logging.path"` + // LogStdout indicates whether log lines are written to standard output in addition to writing them + // to log files. + LogStdout bool `json:"logging.log-stdout" yaml:"logging.log-stdout"` +} + +// InitFromViper initializes logging configuration from Viper. +func (c *Config) InitFromViper(v *viper.Viper) { + c.Level = v.GetString(logLevel) + c.MaxAge = v.GetInt(logMaxAge) + c.MaxBackups = v.GetInt(logMaxBackups) + c.MaxSize = v.GetInt(logMaxSize) + c.Formatter = v.GetString(logFormatter) + c.Path = v.GetString(logPath) + c.LogStdout = v.GetBool(logStdout) +} + +// AddFlags registers persistent logging flags. +func (c *Config) AddFlags(flags *pflag.FlagSet) { + flags.String(logLevel, "info", "Specifies the minimum allowed log level") + flags.Int(logMaxAge, 0, "Sets he maximum number of days to retain old log files based on the timestamp encoded in their filename. By default no old log files will be removed") + flags.Int(logMaxBackups, 15, "Specifies the maximum number of old log files to retain") + flags.Int(logMaxSize, 100, "Specifies the maximum size in megabytes of the log file before it gets rotated") + flags.String(logFormatter, "json", "Represents the log formatter (json|text )") + flags.String(logPath, "", "Specifies the alternative paths for storing the logs") + flags.Bool(logStdout, false, "Indicates whether log lines are written to standard output in addition to writing them to log files") +} diff --git a/pkg/util/log/logger.go b/pkg/util/log/logger.go new file mode 100644 index 000000000..ef2b170aa --- /dev/null +++ b/pkg/util/log/logger.go @@ -0,0 +1,112 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package log + +import ( + "errors" + "expvar" + "fmt" + "github.com/rabbitstack/fibratus/pkg/util/log/rotate" + fs "github.com/rifflock/lfshook" + "github.com/sirupsen/logrus" + "io/ioutil" + "os" + "path/filepath" +) + +var ( + // errEmptyLogsPath contains logger setup errors + loggerErrors = expvar.NewMap("logger.errors") +) + +// InitFromConfig initializes a Logrus instance from config options. +func InitFromConfig(c Config) error { + exe, err := os.Executable() + var path string + if err != nil { + path = filepath.Join(os.Getenv("PROGRAMFILES"), "fibratus", "logs") + } else { + path = filepath.Join(filepath.Dir(exe), "..", "logs") + } + if c.Path != "" { + path = c.Path + } + if path == "" { + return errors.New("got an empty logs directory path. Please make sure Fibratus is installed properly") + } + _, err = os.Stat(path) + if err != nil { + // let's create the logs directory since it doesn't exist, even though + // this should rarely happen because Fibratus installer already creates + // the logs directory + if err := os.MkdirAll(path, os.ModePerm); err != nil { + return fmt.Errorf("unable to create the %s logs directory: %v", path, err) + } + } + + file := filepath.Join(path, "fibratus.log") + + // setup log formatter + var formatter logrus.Formatter + switch c.Formatter { + case "json": + formatter = &logrus.JSONFormatter{} + case "text": + formatter = &logrus.TextFormatter{} + default: + formatter = &logrus.JSONFormatter{} + } + logrus.SetFormatter(formatter) + + level, err := logrus.ParseLevel(c.Level) + if err != nil { + return err + } + logrus.SetLevel(level) + + // disable writing to stdout + if !c.LogStdout { + logrus.SetOutput(ioutil.Discard) + } + + // initialize log rotate hook + rhook, err := rotate.NewHook(rotate.Config{ + MaxAge: c.MaxAge, + MaxBackups: c.MaxBackups, + MaxSize: c.MaxSize, + Level: level, + Formatter: formatter, + Filename: file, + }) + + if err != nil { + loggerErrors.Add(err.Error(), 1) + // failed to initialize log rotate, so we fallback on simple log hook + var pathMap fs.PathMap = make(map[logrus.Level]string, 0) + for _, lvl := range logrus.AllLevels { + pathMap[lvl] = file + } + logrus.AddHook(fs.NewHook(pathMap, formatter)) + logrus.Warnf("unable to initialize rotate file hook: %v", err) + return nil + } + logrus.AddHook(rhook) + + return nil +} diff --git a/pkg/util/log/logger_test.go b/pkg/util/log/logger_test.go new file mode 100644 index 000000000..a87a97e19 --- /dev/null +++ b/pkg/util/log/logger_test.go @@ -0,0 +1,38 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package log + +import ( + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + "os" + "testing" +) + +func TestInitFromConfig(t *testing.T) { + require.Error(t, InitFromConfig(Config{})) + require.NoError(t, InitFromConfig(Config{Path: "_fixtures", Level: "info", Formatter: "text"})) + + os.Remove("_fixtures\\fibratus.log") + + logrus.Info("fibratus initialized") + + _, err := os.Stat("_fixtures\\fibratus.log") + require.NoError(t, err) +} diff --git a/pkg/util/log/rotate/rotate.go b/pkg/util/log/rotate/rotate.go new file mode 100644 index 000000000..19590a0a0 --- /dev/null +++ b/pkg/util/log/rotate/rotate.go @@ -0,0 +1,137 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package rotate + +import ( + "fmt" + "github.com/sirupsen/logrus" + "gopkg.in/natefinch/lumberjack.v2" + "io" + "runtime" + "strings" +) + +// Config is the configuration for the rotate file hook. +type Config struct { + Filename string + MaxSize int + MaxBackups int + MaxAge int + Level logrus.Level + Formatter logrus.Formatter +} + +// RotateFile represents the rotate file hook. +type RotateFile struct { + config Config + w io.Writer + depth int + skip int + formatter func(file, function string, line int) string + skipPrefixes []string +} + +// NewHook builds a new rotate file hook. +func NewHook(config Config) (logrus.Hook, error) { + hook := RotateFile{ + config: config, + depth: 20, + skip: 5, + skipPrefixes: []string{"logrus/", "logrus@"}, + formatter: func(file, function string, line int) string { + return fmt.Sprintf("%s:%d", file, line) + }, + } + hook.w = &lumberjack.Logger{ + Filename: config.Filename, + MaxSize: config.MaxSize, + MaxBackups: config.MaxBackups, + MaxAge: config.MaxAge, + } + return &hook, nil +} + +// Levels determines log levels that for which the logs are written. +func (hook *RotateFile) Levels() []logrus.Level { + return logrus.AllLevels[:hook.config.Level+1] +} + +// Fire is called by logrus when it is about to write the log entry. +func (hook *RotateFile) Fire(entry *logrus.Entry) (err error) { + modified := entry.WithField("source", hook.formatter(hook.findCaller())) + modified.Level = entry.Level + modified.Message = entry.Message + b, err := hook.config.Formatter.Format(modified) + if err != nil { + return err + } + _, err = hook.w.Write(b) + return err +} + +func (hook *RotateFile) findCaller() (string, string, int) { + var ( + pc uintptr + file string + function string + line int + ) + for i := 0; i < hook.depth; i++ { + pc, file, line = getCaller(hook.skip + i) + if !hook.skipFile(file) { + break + } + } + if pc != 0 { + frames := runtime.CallersFrames([]uintptr{pc}) + frame, _ := frames.Next() + function = frame.Function + } + + return file, function, line +} + +func (hook *RotateFile) skipFile(file string) bool { + for i := range hook.skipPrefixes { + if strings.HasPrefix(file, hook.skipPrefixes[i]) { + return true + } + } + return false +} + +func getCaller(skip int) (uintptr, string, int) { + pc, file, line, ok := runtime.Caller(skip) + if !ok { + return 0, "", 0 + } + + n := 0 + for i := len(file) - 1; i > 0; i-- { + if file[i] == '/' { + n++ + if n >= 2 { + file = file[i+1:] + break + } + } + } + + return pc, file, line +} diff --git a/pkg/util/multierror/multierror.go b/pkg/util/multierror/multierror.go new file mode 100644 index 000000000..04d0d6464 --- /dev/null +++ b/pkg/util/multierror/multierror.go @@ -0,0 +1,55 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package multierror + +import ( + "fmt" + "strings" +) + +// Wrap takes a slice of errors and returns a single error that encapsulates +// those underlying errors. If the slice is nil or empty it returns nil. +// If the slice only contains a single element, that error is returned directly. +// When more than one error is wrapped, the Error() string is a concatenation +// of the Error() values of all underlying errors. +func Wrap(errs []error) error { + return multiError(errs).flatten() +} + +// multiError bundles several errors together into a single error. +type multiError []error + +// flatten returns either: nil, the only error, or the multiError instance itself +// if there are 0, 1, or more errors in the slice respectively. +func (errors multiError) flatten() error { + switch len(errors) { + case 0: + return nil + case 1: + return errors[0] + default: + return errors + } +} + +// Error returns a string like "[e1, e2, ...]" where each eN is the Error() of +// each error in the slice. +func (errors multiError) Error() string { + parts := make([]string, len(errors)) + for i, err := range errors { + parts[i] = err.Error() + } + return fmt.Sprintf("[%s]", strings.Join(parts, ", ")) +} diff --git a/pkg/util/ports/iana_ports.go b/pkg/util/ports/iana_ports.go new file mode 100644 index 000000000..ab657f33e --- /dev/null +++ b/pkg/util/ports/iana_ports.go @@ -0,0 +1,11352 @@ +// Copyright 2012 Google, Inc. All rights reserved. +// Borrowed from gopacket. https://github.com/google/gopacket/blob/master/layers/iana_ports.go + +package ports + +// Created by gen.go, don't edit manually +// Generated at 2017-10-23 09:57:28.214859163 -0600 MDT m=+1.011679290 +// Fetched from "http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xml" + +// TCPPortNames contains the port names for all TCP ports. +var TCPPortNames = tcpPortNames + +// UDPPortNames contains the port names for all UDP ports. +var UDPPortNames = udpPortNames + +// SCTPPortNames contains the port names for all SCTP ports. +var SCTPPortNames = sctpPortNames + +var tcpPortNames = map[uint16]string{ + 1: "tcpmux", + 2: "compressnet", + 3: "compressnet", + 5: "rje", + 7: "echo", + 9: "discard", + 11: "systat", + 13: "daytime", + 17: "qotd", + 18: "msp", + 19: "chargen", + 20: "ftp-data", + 21: "ftp", + 22: "ssh", + 23: "telnet", + 25: "smtp", + 27: "nsw-fe", + 29: "msg-icp", + 31: "msg-auth", + 33: "dsp", + 37: "time", + 38: "rap", + 39: "rlp", + 41: "graphics", + 42: "name", + 43: "nicname", + 44: "mpm-flags", + 45: "mpm", + 46: "mpm-snd", + 48: "auditd", + 49: "tacacs", + 50: "re-mail-ck", + 52: "xns-time", + 53: "domain", + 54: "xns-ch", + 55: "isi-gl", + 56: "xns-auth", + 58: "xns-mail", + 62: "acas", + 63: "whoispp", + 64: "covia", + 65: "tacacs-ds", + 66: "sql-net", + 67: "bootps", + 68: "bootpc", + 69: "tftp", + 70: "gopher", + 71: "netrjs-1", + 72: "netrjs-2", + 73: "netrjs-3", + 74: "netrjs-4", + 76: "deos", + 78: "vettcp", + 79: "finger", + 80: "http", + 82: "xfer", + 83: "mit-ml-dev", + 84: "ctf", + 85: "mit-ml-dev", + 86: "mfcobol", + 88: "kerberos", + 89: "su-mit-tg", + 90: "dnsix", + 91: "mit-dov", + 92: "npp", + 93: "dcp", + 94: "objcall", + 95: "supdup", + 96: "dixie", + 97: "swift-rvf", + 98: "tacnews", + 99: "metagram", + 101: "hostname", + 102: "iso-tsap", + 103: "gppitnp", + 104: "acr-nema", + 105: "cso", + 106: "3com-tsmux", + 107: "rtelnet", + 108: "snagas", + 109: "pop2", + 110: "pop3", + 111: "sunrpc", + 112: "mcidas", + 113: "ident", + 115: "sftp", + 116: "ansanotify", + 117: "uucp-path", + 118: "sqlserv", + 119: "nntp", + 120: "cfdptkt", + 121: "erpc", + 122: "smakynet", + 123: "ntp", + 124: "ansatrader", + 125: "locus-map", + 126: "nxedit", + 127: "locus-con", + 128: "gss-xlicen", + 129: "pwdgen", + 130: "cisco-fna", + 131: "cisco-tna", + 132: "cisco-sys", + 133: "statsrv", + 134: "ingres-net", + 135: "epmap", + 136: "profile", + 137: "netbios-ns", + 138: "netbios-dgm", + 139: "netbios-ssn", + 140: "emfis-data", + 141: "emfis-cntl", + 142: "bl-idm", + 143: "imap", + 144: "uma", + 145: "uaac", + 146: "iso-tp0", + 147: "iso-ip", + 148: "jargon", + 149: "aed-512", + 150: "sql-net", + 151: "hems", + 152: "bftp", + 153: "sgmp", + 154: "netsc-prod", + 155: "netsc-dev", + 156: "sqlsrv", + 157: "knet-cmp", + 158: "pcmail-srv", + 159: "nss-routing", + 160: "sgmp-traps", + 161: "snmp", + 162: "snmptrap", + 163: "cmip-man", + 164: "cmip-agent", + 165: "xns-courier", + 166: "s-net", + 167: "namp", + 168: "rsvd", + 169: "send", + 170: "print-srv", + 171: "multiplex", + 172: "cl-1", + 173: "xyplex-mux", + 174: "mailq", + 175: "vmnet", + 176: "genrad-mux", + 177: "xdmcp", + 178: "nextstep", + 179: "bgp", + 180: "ris", + 181: "unify", + 182: "audit", + 183: "ocbinder", + 184: "ocserver", + 185: "remote-kis", + 186: "kis", + 187: "aci", + 188: "mumps", + 189: "qft", + 190: "gacp", + 191: "prospero", + 192: "osu-nms", + 193: "srmp", + 194: "irc", + 195: "dn6-nlm-aud", + 196: "dn6-smm-red", + 197: "dls", + 198: "dls-mon", + 199: "smux", + 200: "src", + 201: "at-rtmp", + 202: "at-nbp", + 203: "at-3", + 204: "at-echo", + 205: "at-5", + 206: "at-zis", + 207: "at-7", + 208: "at-8", + 209: "qmtp", + 210: "z39-50", + 211: "914c-g", + 212: "anet", + 213: "ipx", + 214: "vmpwscs", + 215: "softpc", + 216: "CAIlic", + 217: "dbase", + 218: "mpp", + 219: "uarps", + 220: "imap3", + 221: "fln-spx", + 222: "rsh-spx", + 223: "cdc", + 224: "masqdialer", + 242: "direct", + 243: "sur-meas", + 244: "inbusiness", + 245: "link", + 246: "dsp3270", + 247: "subntbcst-tftp", + 248: "bhfhs", + 256: "rap", + 257: "set", + 259: "esro-gen", + 260: "openport", + 261: "nsiiops", + 262: "arcisdms", + 263: "hdap", + 264: "bgmp", + 265: "x-bone-ctl", + 266: "sst", + 267: "td-service", + 268: "td-replica", + 269: "manet", + 271: "pt-tls", + 280: "http-mgmt", + 281: "personal-link", + 282: "cableport-ax", + 283: "rescap", + 284: "corerjd", + 286: "fxp", + 287: "k-block", + 308: "novastorbakcup", + 309: "entrusttime", + 310: "bhmds", + 311: "asip-webadmin", + 312: "vslmp", + 313: "magenta-logic", + 314: "opalis-robot", + 315: "dpsi", + 316: "decauth", + 317: "zannet", + 318: "pkix-timestamp", + 319: "ptp-event", + 320: "ptp-general", + 321: "pip", + 322: "rtsps", + 323: "rpki-rtr", + 324: "rpki-rtr-tls", + 333: "texar", + 344: "pdap", + 345: "pawserv", + 346: "zserv", + 347: "fatserv", + 348: "csi-sgwp", + 349: "mftp", + 350: "matip-type-a", + 351: "matip-type-b", + 352: "dtag-ste-sb", + 353: "ndsauth", + 354: "bh611", + 355: "datex-asn", + 356: "cloanto-net-1", + 357: "bhevent", + 358: "shrinkwrap", + 359: "nsrmp", + 360: "scoi2odialog", + 361: "semantix", + 362: "srssend", + 363: "rsvp-tunnel", + 364: "aurora-cmgr", + 365: "dtk", + 366: "odmr", + 367: "mortgageware", + 368: "qbikgdp", + 369: "rpc2portmap", + 370: "codaauth2", + 371: "clearcase", + 372: "ulistproc", + 373: "legent-1", + 374: "legent-2", + 375: "hassle", + 376: "nip", + 377: "tnETOS", + 378: "dsETOS", + 379: "is99c", + 380: "is99s", + 381: "hp-collector", + 382: "hp-managed-node", + 383: "hp-alarm-mgr", + 384: "arns", + 385: "ibm-app", + 386: "asa", + 387: "aurp", + 388: "unidata-ldm", + 389: "ldap", + 390: "uis", + 391: "synotics-relay", + 392: "synotics-broker", + 393: "meta5", + 394: "embl-ndt", + 395: "netcp", + 396: "netware-ip", + 397: "mptn", + 398: "kryptolan", + 399: "iso-tsap-c2", + 400: "osb-sd", + 401: "ups", + 402: "genie", + 403: "decap", + 404: "nced", + 405: "ncld", + 406: "imsp", + 407: "timbuktu", + 408: "prm-sm", + 409: "prm-nm", + 410: "decladebug", + 411: "rmt", + 412: "synoptics-trap", + 413: "smsp", + 414: "infoseek", + 415: "bnet", + 416: "silverplatter", + 417: "onmux", + 418: "hyper-g", + 419: "ariel1", + 420: "smpte", + 421: "ariel2", + 422: "ariel3", + 423: "opc-job-start", + 424: "opc-job-track", + 425: "icad-el", + 426: "smartsdp", + 427: "svrloc", + 428: "ocs-cmu", + 429: "ocs-amu", + 430: "utmpsd", + 431: "utmpcd", + 432: "iasd", + 433: "nnsp", + 434: "mobileip-agent", + 435: "mobilip-mn", + 436: "dna-cml", + 437: "comscm", + 438: "dsfgw", + 439: "dasp", + 440: "sgcp", + 441: "decvms-sysmgt", + 442: "cvc-hostd", + 443: "https", + 444: "snpp", + 445: "microsoft-ds", + 446: "ddm-rdb", + 447: "ddm-dfm", + 448: "ddm-ssl", + 449: "as-servermap", + 450: "tserver", + 451: "sfs-smp-net", + 452: "sfs-config", + 453: "creativeserver", + 454: "contentserver", + 455: "creativepartnr", + 456: "macon-tcp", + 457: "scohelp", + 458: "appleqtc", + 459: "ampr-rcmd", + 460: "skronk", + 461: "datasurfsrv", + 462: "datasurfsrvsec", + 463: "alpes", + 464: "kpasswd", + 465: "urd", + 466: "digital-vrc", + 467: "mylex-mapd", + 468: "photuris", + 469: "rcp", + 470: "scx-proxy", + 471: "mondex", + 472: "ljk-login", + 473: "hybrid-pop", + 474: "tn-tl-w1", + 475: "tcpnethaspsrv", + 476: "tn-tl-fd1", + 477: "ss7ns", + 478: "spsc", + 479: "iafserver", + 480: "iafdbase", + 481: "ph", + 482: "bgs-nsi", + 483: "ulpnet", + 484: "integra-sme", + 485: "powerburst", + 486: "avian", + 487: "saft", + 488: "gss-http", + 489: "nest-protocol", + 490: "micom-pfs", + 491: "go-login", + 492: "ticf-1", + 493: "ticf-2", + 494: "pov-ray", + 495: "intecourier", + 496: "pim-rp-disc", + 497: "retrospect", + 498: "siam", + 499: "iso-ill", + 500: "isakmp", + 501: "stmf", + 502: "mbap", + 503: "intrinsa", + 504: "citadel", + 505: "mailbox-lm", + 506: "ohimsrv", + 507: "crs", + 508: "xvttp", + 509: "snare", + 510: "fcp", + 511: "passgo", + 512: "exec", + 513: "login", + 514: "shell", + 515: "printer", + 516: "videotex", + 517: "talk", + 518: "ntalk", + 519: "utime", + 520: "efs", + 521: "ripng", + 522: "ulp", + 523: "ibm-db2", + 524: "ncp", + 525: "timed", + 526: "tempo", + 527: "stx", + 528: "custix", + 529: "irc-serv", + 530: "courier", + 531: "conference", + 532: "netnews", + 533: "netwall", + 534: "windream", + 535: "iiop", + 536: "opalis-rdv", + 537: "nmsp", + 538: "gdomap", + 539: "apertus-ldp", + 540: "uucp", + 541: "uucp-rlogin", + 542: "commerce", + 543: "klogin", + 544: "kshell", + 545: "appleqtcsrvr", + 546: "dhcpv6-client", + 547: "dhcpv6-server", + 548: "afpovertcp", + 549: "idfp", + 550: "new-rwho", + 551: "cybercash", + 552: "devshr-nts", + 553: "pirp", + 554: "rtsp", + 555: "dsf", + 556: "remotefs", + 557: "openvms-sysipc", + 558: "sdnskmp", + 559: "teedtap", + 560: "rmonitor", + 561: "monitor", + 562: "chshell", + 563: "nntps", + 564: "9pfs", + 565: "whoami", + 566: "streettalk", + 567: "banyan-rpc", + 568: "ms-shuttle", + 569: "ms-rome", + 570: "meter", + 571: "meter", + 572: "sonar", + 573: "banyan-vip", + 574: "ftp-agent", + 575: "vemmi", + 576: "ipcd", + 577: "vnas", + 578: "ipdd", + 579: "decbsrv", + 580: "sntp-heartbeat", + 581: "bdp", + 582: "scc-security", + 583: "philips-vc", + 584: "keyserver", + 586: "password-chg", + 587: "submission", + 588: "cal", + 589: "eyelink", + 590: "tns-cml", + 591: "http-alt", + 592: "eudora-set", + 593: "http-rpc-epmap", + 594: "tpip", + 595: "cab-protocol", + 596: "smsd", + 597: "ptcnameservice", + 598: "sco-websrvrmg3", + 599: "acp", + 600: "ipcserver", + 601: "syslog-conn", + 602: "xmlrpc-beep", + 603: "idxp", + 604: "tunnel", + 605: "soap-beep", + 606: "urm", + 607: "nqs", + 608: "sift-uft", + 609: "npmp-trap", + 610: "npmp-local", + 611: "npmp-gui", + 612: "hmmp-ind", + 613: "hmmp-op", + 614: "sshell", + 615: "sco-inetmgr", + 616: "sco-sysmgr", + 617: "sco-dtmgr", + 618: "dei-icda", + 619: "compaq-evm", + 620: "sco-websrvrmgr", + 621: "escp-ip", + 622: "collaborator", + 623: "oob-ws-http", + 624: "cryptoadmin", + 625: "dec-dlm", + 626: "asia", + 627: "passgo-tivoli", + 628: "qmqp", + 629: "3com-amp3", + 630: "rda", + 631: "ipp", + 632: "bmpp", + 633: "servstat", + 634: "ginad", + 635: "rlzdbase", + 636: "ldaps", + 637: "lanserver", + 638: "mcns-sec", + 639: "msdp", + 640: "entrust-sps", + 641: "repcmd", + 642: "esro-emsdp", + 643: "sanity", + 644: "dwr", + 645: "pssc", + 646: "ldp", + 647: "dhcp-failover", + 648: "rrp", + 649: "cadview-3d", + 650: "obex", + 651: "ieee-mms", + 652: "hello-port", + 653: "repscmd", + 654: "aodv", + 655: "tinc", + 656: "spmp", + 657: "rmc", + 658: "tenfold", + 660: "mac-srvr-admin", + 661: "hap", + 662: "pftp", + 663: "purenoise", + 664: "oob-ws-https", + 665: "sun-dr", + 666: "mdqs", + 667: "disclose", + 668: "mecomm", + 669: "meregister", + 670: "vacdsm-sws", + 671: "vacdsm-app", + 672: "vpps-qua", + 673: "cimplex", + 674: "acap", + 675: "dctp", + 676: "vpps-via", + 677: "vpp", + 678: "ggf-ncp", + 679: "mrm", + 680: "entrust-aaas", + 681: "entrust-aams", + 682: "xfr", + 683: "corba-iiop", + 684: "corba-iiop-ssl", + 685: "mdc-portmapper", + 686: "hcp-wismar", + 687: "asipregistry", + 688: "realm-rusd", + 689: "nmap", + 690: "vatp", + 691: "msexch-routing", + 692: "hyperwave-isp", + 693: "connendp", + 694: "ha-cluster", + 695: "ieee-mms-ssl", + 696: "rushd", + 697: "uuidgen", + 698: "olsr", + 699: "accessnetwork", + 700: "epp", + 701: "lmp", + 702: "iris-beep", + 704: "elcsd", + 705: "agentx", + 706: "silc", + 707: "borland-dsj", + 709: "entrust-kmsh", + 710: "entrust-ash", + 711: "cisco-tdp", + 712: "tbrpf", + 713: "iris-xpc", + 714: "iris-xpcs", + 715: "iris-lwz", + 729: "netviewdm1", + 730: "netviewdm2", + 731: "netviewdm3", + 741: "netgw", + 742: "netrcs", + 744: "flexlm", + 747: "fujitsu-dev", + 748: "ris-cm", + 749: "kerberos-adm", + 750: "rfile", + 751: "pump", + 752: "qrh", + 753: "rrh", + 754: "tell", + 758: "nlogin", + 759: "con", + 760: "ns", + 761: "rxe", + 762: "quotad", + 763: "cycleserv", + 764: "omserv", + 765: "webster", + 767: "phonebook", + 769: "vid", + 770: "cadlock", + 771: "rtip", + 772: "cycleserv2", + 773: "submit", + 774: "rpasswd", + 775: "entomb", + 776: "wpages", + 777: "multiling-http", + 780: "wpgs", + 800: "mdbs-daemon", + 801: "device", + 802: "mbap-s", + 810: "fcp-udp", + 828: "itm-mcell-s", + 829: "pkix-3-ca-ra", + 830: "netconf-ssh", + 831: "netconf-beep", + 832: "netconfsoaphttp", + 833: "netconfsoapbeep", + 847: "dhcp-failover2", + 848: "gdoi", + 853: "domain-s", + 854: "dlep", + 860: "iscsi", + 861: "owamp-control", + 862: "twamp-control", + 873: "rsync", + 886: "iclcnet-locate", + 887: "iclcnet-svinfo", + 888: "accessbuilder", + 900: "omginitialrefs", + 901: "smpnameres", + 902: "ideafarm-door", + 903: "ideafarm-panic", + 910: "kink", + 911: "xact-backup", + 912: "apex-mesh", + 913: "apex-edge", + 953: "rndc", + 989: "ftps-data", + 990: "ftps", + 991: "nas", + 992: "telnets", + 993: "imaps", + 995: "pop3s", + 996: "vsinet", + 997: "maitrd", + 998: "busboy", + 999: "garcon", + 1000: "cadlock2", + 1001: "webpush", + 1010: "surf", + 1021: "exp1", + 1022: "exp2", + 1025: "blackjack", + 1026: "cap", + 1029: "solid-mux", + 1033: "netinfo-local", + 1034: "activesync", + 1035: "mxxrlogin", + 1036: "nsstp", + 1037: "ams", + 1038: "mtqp", + 1039: "sbl", + 1040: "netarx", + 1041: "danf-ak2", + 1042: "afrog", + 1043: "boinc-client", + 1044: "dcutility", + 1045: "fpitp", + 1046: "wfremotertm", + 1047: "neod1", + 1048: "neod2", + 1049: "td-postman", + 1050: "cma", + 1051: "optima-vnet", + 1052: "ddt", + 1053: "remote-as", + 1054: "brvread", + 1055: "ansyslmd", + 1056: "vfo", + 1057: "startron", + 1058: "nim", + 1059: "nimreg", + 1060: "polestar", + 1061: "kiosk", + 1062: "veracity", + 1063: "kyoceranetdev", + 1064: "jstel", + 1065: "syscomlan", + 1066: "fpo-fns", + 1067: "instl-boots", + 1068: "instl-bootc", + 1069: "cognex-insight", + 1070: "gmrupdateserv", + 1071: "bsquare-voip", + 1072: "cardax", + 1073: "bridgecontrol", + 1074: "warmspotMgmt", + 1075: "rdrmshc", + 1076: "dab-sti-c", + 1077: "imgames", + 1078: "avocent-proxy", + 1079: "asprovatalk", + 1080: "socks", + 1081: "pvuniwien", + 1082: "amt-esd-prot", + 1083: "ansoft-lm-1", + 1084: "ansoft-lm-2", + 1085: "webobjects", + 1086: "cplscrambler-lg", + 1087: "cplscrambler-in", + 1088: "cplscrambler-al", + 1089: "ff-annunc", + 1090: "ff-fms", + 1091: "ff-sm", + 1092: "obrpd", + 1093: "proofd", + 1094: "rootd", + 1095: "nicelink", + 1096: "cnrprotocol", + 1097: "sunclustermgr", + 1098: "rmiactivation", + 1099: "rmiregistry", + 1100: "mctp", + 1101: "pt2-discover", + 1102: "adobeserver-1", + 1103: "adobeserver-2", + 1104: "xrl", + 1105: "ftranhc", + 1106: "isoipsigport-1", + 1107: "isoipsigport-2", + 1108: "ratio-adp", + 1110: "webadmstart", + 1111: "lmsocialserver", + 1112: "icp", + 1113: "ltp-deepspace", + 1114: "mini-sql", + 1115: "ardus-trns", + 1116: "ardus-cntl", + 1117: "ardus-mtrns", + 1118: "sacred", + 1119: "bnetgame", + 1120: "bnetfile", + 1121: "rmpp", + 1122: "availant-mgr", + 1123: "murray", + 1124: "hpvmmcontrol", + 1125: "hpvmmagent", + 1126: "hpvmmdata", + 1127: "kwdb-commn", + 1128: "saphostctrl", + 1129: "saphostctrls", + 1130: "casp", + 1131: "caspssl", + 1132: "kvm-via-ip", + 1133: "dfn", + 1134: "aplx", + 1135: "omnivision", + 1136: "hhb-gateway", + 1137: "trim", + 1138: "encrypted-admin", + 1139: "evm", + 1140: "autonoc", + 1141: "mxomss", + 1142: "edtools", + 1143: "imyx", + 1144: "fuscript", + 1145: "x9-icue", + 1146: "audit-transfer", + 1147: "capioverlan", + 1148: "elfiq-repl", + 1149: "bvtsonar", + 1150: "blaze", + 1151: "unizensus", + 1152: "winpoplanmess", + 1153: "c1222-acse", + 1154: "resacommunity", + 1155: "nfa", + 1156: "iascontrol-oms", + 1157: "iascontrol", + 1158: "dbcontrol-oms", + 1159: "oracle-oms", + 1160: "olsv", + 1161: "health-polling", + 1162: "health-trap", + 1163: "sddp", + 1164: "qsm-proxy", + 1165: "qsm-gui", + 1166: "qsm-remote", + 1167: "cisco-ipsla", + 1168: "vchat", + 1169: "tripwire", + 1170: "atc-lm", + 1171: "atc-appserver", + 1172: "dnap", + 1173: "d-cinema-rrp", + 1174: "fnet-remote-ui", + 1175: "dossier", + 1176: "indigo-server", + 1177: "dkmessenger", + 1178: "sgi-storman", + 1179: "b2n", + 1180: "mc-client", + 1181: "3comnetman", + 1182: "accelenet", + 1183: "llsurfup-http", + 1184: "llsurfup-https", + 1185: "catchpole", + 1186: "mysql-cluster", + 1187: "alias", + 1188: "hp-webadmin", + 1189: "unet", + 1190: "commlinx-avl", + 1191: "gpfs", + 1192: "caids-sensor", + 1193: "fiveacross", + 1194: "openvpn", + 1195: "rsf-1", + 1196: "netmagic", + 1197: "carrius-rshell", + 1198: "cajo-discovery", + 1199: "dmidi", + 1200: "scol", + 1201: "nucleus-sand", + 1202: "caiccipc", + 1203: "ssslic-mgr", + 1204: "ssslog-mgr", + 1205: "accord-mgc", + 1206: "anthony-data", + 1207: "metasage", + 1208: "seagull-ais", + 1209: "ipcd3", + 1210: "eoss", + 1211: "groove-dpp", + 1212: "lupa", + 1213: "mpc-lifenet", + 1214: "kazaa", + 1215: "scanstat-1", + 1216: "etebac5", + 1217: "hpss-ndapi", + 1218: "aeroflight-ads", + 1219: "aeroflight-ret", + 1220: "qt-serveradmin", + 1221: "sweetware-apps", + 1222: "nerv", + 1223: "tgp", + 1224: "vpnz", + 1225: "slinkysearch", + 1226: "stgxfws", + 1227: "dns2go", + 1228: "florence", + 1229: "zented", + 1230: "periscope", + 1231: "menandmice-lpm", + 1232: "first-defense", + 1233: "univ-appserver", + 1234: "search-agent", + 1235: "mosaicsyssvc1", + 1236: "bvcontrol", + 1237: "tsdos390", + 1238: "hacl-qs", + 1239: "nmsd", + 1240: "instantia", + 1241: "nessus", + 1242: "nmasoverip", + 1243: "serialgateway", + 1244: "isbconference1", + 1245: "isbconference2", + 1246: "payrouter", + 1247: "visionpyramid", + 1248: "hermes", + 1249: "mesavistaco", + 1250: "swldy-sias", + 1251: "servergraph", + 1252: "bspne-pcc", + 1253: "q55-pcc", + 1254: "de-noc", + 1255: "de-cache-query", + 1256: "de-server", + 1257: "shockwave2", + 1258: "opennl", + 1259: "opennl-voice", + 1260: "ibm-ssd", + 1261: "mpshrsv", + 1262: "qnts-orb", + 1263: "dka", + 1264: "prat", + 1265: "dssiapi", + 1266: "dellpwrappks", + 1267: "epc", + 1268: "propel-msgsys", + 1269: "watilapp", + 1270: "opsmgr", + 1271: "excw", + 1272: "cspmlockmgr", + 1273: "emc-gateway", + 1274: "t1distproc", + 1275: "ivcollector", + 1277: "miva-mqs", + 1278: "dellwebadmin-1", + 1279: "dellwebadmin-2", + 1280: "pictrography", + 1281: "healthd", + 1282: "emperion", + 1283: "productinfo", + 1284: "iee-qfx", + 1285: "neoiface", + 1286: "netuitive", + 1287: "routematch", + 1288: "navbuddy", + 1289: "jwalkserver", + 1290: "winjaserver", + 1291: "seagulllms", + 1292: "dsdn", + 1293: "pkt-krb-ipsec", + 1294: "cmmdriver", + 1295: "ehtp", + 1296: "dproxy", + 1297: "sdproxy", + 1298: "lpcp", + 1299: "hp-sci", + 1300: "h323hostcallsc", + 1301: "ci3-software-1", + 1302: "ci3-software-2", + 1303: "sftsrv", + 1304: "boomerang", + 1305: "pe-mike", + 1306: "re-conn-proto", + 1307: "pacmand", + 1308: "odsi", + 1309: "jtag-server", + 1310: "husky", + 1311: "rxmon", + 1312: "sti-envision", + 1313: "bmc-patroldb", + 1314: "pdps", + 1315: "els", + 1316: "exbit-escp", + 1317: "vrts-ipcserver", + 1318: "krb5gatekeeper", + 1319: "amx-icsp", + 1320: "amx-axbnet", + 1321: "pip", + 1322: "novation", + 1323: "brcd", + 1324: "delta-mcp", + 1325: "dx-instrument", + 1326: "wimsic", + 1327: "ultrex", + 1328: "ewall", + 1329: "netdb-export", + 1330: "streetperfect", + 1331: "intersan", + 1332: "pcia-rxp-b", + 1333: "passwrd-policy", + 1334: "writesrv", + 1335: "digital-notary", + 1336: "ischat", + 1337: "menandmice-dns", + 1338: "wmc-log-svc", + 1339: "kjtsiteserver", + 1340: "naap", + 1341: "qubes", + 1342: "esbroker", + 1343: "re101", + 1344: "icap", + 1345: "vpjp", + 1346: "alta-ana-lm", + 1347: "bbn-mmc", + 1348: "bbn-mmx", + 1349: "sbook", + 1350: "editbench", + 1351: "equationbuilder", + 1352: "lotusnote", + 1353: "relief", + 1354: "XSIP-network", + 1355: "intuitive-edge", + 1356: "cuillamartin", + 1357: "pegboard", + 1358: "connlcli", + 1359: "ftsrv", + 1360: "mimer", + 1361: "linx", + 1362: "timeflies", + 1363: "ndm-requester", + 1364: "ndm-server", + 1365: "adapt-sna", + 1366: "netware-csp", + 1367: "dcs", + 1368: "screencast", + 1369: "gv-us", + 1370: "us-gv", + 1371: "fc-cli", + 1372: "fc-ser", + 1373: "chromagrafx", + 1374: "molly", + 1375: "bytex", + 1376: "ibm-pps", + 1377: "cichlid", + 1378: "elan", + 1379: "dbreporter", + 1380: "telesis-licman", + 1381: "apple-licman", + 1382: "udt-os", + 1383: "gwha", + 1384: "os-licman", + 1385: "atex-elmd", + 1386: "checksum", + 1387: "cadsi-lm", + 1388: "objective-dbc", + 1389: "iclpv-dm", + 1390: "iclpv-sc", + 1391: "iclpv-sas", + 1392: "iclpv-pm", + 1393: "iclpv-nls", + 1394: "iclpv-nlc", + 1395: "iclpv-wsm", + 1396: "dvl-activemail", + 1397: "audio-activmail", + 1398: "video-activmail", + 1399: "cadkey-licman", + 1400: "cadkey-tablet", + 1401: "goldleaf-licman", + 1402: "prm-sm-np", + 1403: "prm-nm-np", + 1404: "igi-lm", + 1405: "ibm-res", + 1406: "netlabs-lm", + 1407: "tibet-server", + 1408: "sophia-lm", + 1409: "here-lm", + 1410: "hiq", + 1411: "af", + 1412: "innosys", + 1413: "innosys-acl", + 1414: "ibm-mqseries", + 1415: "dbstar", + 1416: "novell-lu6-2", + 1417: "timbuktu-srv1", + 1418: "timbuktu-srv2", + 1419: "timbuktu-srv3", + 1420: "timbuktu-srv4", + 1421: "gandalf-lm", + 1422: "autodesk-lm", + 1423: "essbase", + 1424: "hybrid", + 1425: "zion-lm", + 1426: "sais", + 1427: "mloadd", + 1428: "informatik-lm", + 1429: "nms", + 1430: "tpdu", + 1431: "rgtp", + 1432: "blueberry-lm", + 1433: "ms-sql-s", + 1434: "ms-sql-m", + 1435: "ibm-cics", + 1436: "saism", + 1437: "tabula", + 1438: "eicon-server", + 1439: "eicon-x25", + 1440: "eicon-slp", + 1441: "cadis-1", + 1442: "cadis-2", + 1443: "ies-lm", + 1444: "marcam-lm", + 1445: "proxima-lm", + 1446: "ora-lm", + 1447: "apri-lm", + 1448: "oc-lm", + 1449: "peport", + 1450: "dwf", + 1451: "infoman", + 1452: "gtegsc-lm", + 1453: "genie-lm", + 1454: "interhdl-elmd", + 1455: "esl-lm", + 1456: "dca", + 1457: "valisys-lm", + 1458: "nrcabq-lm", + 1459: "proshare1", + 1460: "proshare2", + 1461: "ibm-wrless-lan", + 1462: "world-lm", + 1463: "nucleus", + 1464: "msl-lmd", + 1465: "pipes", + 1466: "oceansoft-lm", + 1467: "csdmbase", + 1468: "csdm", + 1469: "aal-lm", + 1470: "uaiact", + 1471: "csdmbase", + 1472: "csdm", + 1473: "openmath", + 1474: "telefinder", + 1475: "taligent-lm", + 1476: "clvm-cfg", + 1477: "ms-sna-server", + 1478: "ms-sna-base", + 1479: "dberegister", + 1480: "pacerforum", + 1481: "airs", + 1482: "miteksys-lm", + 1483: "afs", + 1484: "confluent", + 1485: "lansource", + 1486: "nms-topo-serv", + 1487: "localinfosrvr", + 1488: "docstor", + 1489: "dmdocbroker", + 1490: "insitu-conf", + 1492: "stone-design-1", + 1493: "netmap-lm", + 1494: "ica", + 1495: "cvc", + 1496: "liberty-lm", + 1497: "rfx-lm", + 1498: "sybase-sqlany", + 1499: "fhc", + 1500: "vlsi-lm", + 1501: "saiscm", + 1502: "shivadiscovery", + 1503: "imtc-mcs", + 1504: "evb-elm", + 1505: "funkproxy", + 1506: "utcd", + 1507: "symplex", + 1508: "diagmond", + 1509: "robcad-lm", + 1510: "mvx-lm", + 1511: "3l-l1", + 1512: "wins", + 1513: "fujitsu-dtc", + 1514: "fujitsu-dtcns", + 1515: "ifor-protocol", + 1516: "vpad", + 1517: "vpac", + 1518: "vpvd", + 1519: "vpvc", + 1520: "atm-zip-office", + 1521: "ncube-lm", + 1522: "ricardo-lm", + 1523: "cichild-lm", + 1524: "ingreslock", + 1525: "orasrv", + 1526: "pdap-np", + 1527: "tlisrv", + 1529: "coauthor", + 1530: "rap-service", + 1531: "rap-listen", + 1532: "miroconnect", + 1533: "virtual-places", + 1534: "micromuse-lm", + 1535: "ampr-info", + 1536: "ampr-inter", + 1537: "sdsc-lm", + 1538: "3ds-lm", + 1539: "intellistor-lm", + 1540: "rds", + 1541: "rds2", + 1542: "gridgen-elmd", + 1543: "simba-cs", + 1544: "aspeclmd", + 1545: "vistium-share", + 1546: "abbaccuray", + 1547: "laplink", + 1548: "axon-lm", + 1549: "shivahose", + 1550: "3m-image-lm", + 1551: "hecmtl-db", + 1552: "pciarray", + 1553: "sna-cs", + 1554: "caci-lm", + 1555: "livelan", + 1556: "veritas-pbx", + 1557: "arbortext-lm", + 1558: "xingmpeg", + 1559: "web2host", + 1560: "asci-val", + 1561: "facilityview", + 1562: "pconnectmgr", + 1563: "cadabra-lm", + 1564: "pay-per-view", + 1565: "winddlb", + 1566: "corelvideo", + 1567: "jlicelmd", + 1568: "tsspmap", + 1569: "ets", + 1570: "orbixd", + 1571: "rdb-dbs-disp", + 1572: "chip-lm", + 1573: "itscomm-ns", + 1574: "mvel-lm", + 1575: "oraclenames", + 1576: "moldflow-lm", + 1577: "hypercube-lm", + 1578: "jacobus-lm", + 1579: "ioc-sea-lm", + 1580: "tn-tl-r1", + 1581: "mil-2045-47001", + 1582: "msims", + 1583: "simbaexpress", + 1584: "tn-tl-fd2", + 1585: "intv", + 1586: "ibm-abtact", + 1587: "pra-elmd", + 1588: "triquest-lm", + 1589: "vqp", + 1590: "gemini-lm", + 1591: "ncpm-pm", + 1592: "commonspace", + 1593: "mainsoft-lm", + 1594: "sixtrak", + 1595: "radio", + 1596: "radio-sm", + 1597: "orbplus-iiop", + 1598: "picknfs", + 1599: "simbaservices", + 1600: "issd", + 1601: "aas", + 1602: "inspect", + 1603: "picodbc", + 1604: "icabrowser", + 1605: "slp", + 1606: "slm-api", + 1607: "stt", + 1608: "smart-lm", + 1609: "isysg-lm", + 1610: "taurus-wh", + 1611: "ill", + 1612: "netbill-trans", + 1613: "netbill-keyrep", + 1614: "netbill-cred", + 1615: "netbill-auth", + 1616: "netbill-prod", + 1617: "nimrod-agent", + 1618: "skytelnet", + 1619: "xs-openstorage", + 1620: "faxportwinport", + 1621: "softdataphone", + 1622: "ontime", + 1623: "jaleosnd", + 1624: "udp-sr-port", + 1625: "svs-omagent", + 1626: "shockwave", + 1627: "t128-gateway", + 1628: "lontalk-norm", + 1629: "lontalk-urgnt", + 1630: "oraclenet8cman", + 1631: "visitview", + 1632: "pammratc", + 1633: "pammrpc", + 1634: "loaprobe", + 1635: "edb-server1", + 1636: "isdc", + 1637: "islc", + 1638: "ismc", + 1639: "cert-initiator", + 1640: "cert-responder", + 1641: "invision", + 1642: "isis-am", + 1643: "isis-ambc", + 1644: "saiseh", + 1645: "sightline", + 1646: "sa-msg-port", + 1647: "rsap", + 1648: "concurrent-lm", + 1649: "kermit", + 1650: "nkd", + 1651: "shiva-confsrvr", + 1652: "xnmp", + 1653: "alphatech-lm", + 1654: "stargatealerts", + 1655: "dec-mbadmin", + 1656: "dec-mbadmin-h", + 1657: "fujitsu-mmpdc", + 1658: "sixnetudr", + 1659: "sg-lm", + 1660: "skip-mc-gikreq", + 1661: "netview-aix-1", + 1662: "netview-aix-2", + 1663: "netview-aix-3", + 1664: "netview-aix-4", + 1665: "netview-aix-5", + 1666: "netview-aix-6", + 1667: "netview-aix-7", + 1668: "netview-aix-8", + 1669: "netview-aix-9", + 1670: "netview-aix-10", + 1671: "netview-aix-11", + 1672: "netview-aix-12", + 1673: "proshare-mc-1", + 1674: "proshare-mc-2", + 1675: "pdp", + 1676: "netcomm1", + 1677: "groupwise", + 1678: "prolink", + 1679: "darcorp-lm", + 1680: "microcom-sbp", + 1681: "sd-elmd", + 1682: "lanyon-lantern", + 1683: "ncpm-hip", + 1684: "snaresecure", + 1685: "n2nremote", + 1686: "cvmon", + 1687: "nsjtp-ctrl", + 1688: "nsjtp-data", + 1689: "firefox", + 1690: "ng-umds", + 1691: "empire-empuma", + 1692: "sstsys-lm", + 1693: "rrirtr", + 1694: "rrimwm", + 1695: "rrilwm", + 1696: "rrifmm", + 1697: "rrisat", + 1698: "rsvp-encap-1", + 1699: "rsvp-encap-2", + 1700: "mps-raft", + 1701: "l2f", + 1702: "deskshare", + 1703: "hb-engine", + 1704: "bcs-broker", + 1705: "slingshot", + 1706: "jetform", + 1707: "vdmplay", + 1708: "gat-lmd", + 1709: "centra", + 1710: "impera", + 1711: "pptconference", + 1712: "registrar", + 1713: "conferencetalk", + 1714: "sesi-lm", + 1715: "houdini-lm", + 1716: "xmsg", + 1717: "fj-hdnet", + 1718: "h323gatedisc", + 1719: "h323gatestat", + 1720: "h323hostcall", + 1721: "caicci", + 1722: "hks-lm", + 1723: "pptp", + 1724: "csbphonemaster", + 1725: "iden-ralp", + 1726: "iberiagames", + 1727: "winddx", + 1728: "telindus", + 1729: "citynl", + 1730: "roketz", + 1731: "msiccp", + 1732: "proxim", + 1733: "siipat", + 1734: "cambertx-lm", + 1735: "privatechat", + 1736: "street-stream", + 1737: "ultimad", + 1738: "gamegen1", + 1739: "webaccess", + 1740: "encore", + 1741: "cisco-net-mgmt", + 1742: "3Com-nsd", + 1743: "cinegrfx-lm", + 1744: "ncpm-ft", + 1745: "remote-winsock", + 1746: "ftrapid-1", + 1747: "ftrapid-2", + 1748: "oracle-em1", + 1749: "aspen-services", + 1750: "sslp", + 1751: "swiftnet", + 1752: "lofr-lm", + 1753: "predatar-comms", + 1754: "oracle-em2", + 1755: "ms-streaming", + 1756: "capfast-lmd", + 1757: "cnhrp", + 1758: "tftp-mcast", + 1759: "spss-lm", + 1760: "www-ldap-gw", + 1761: "cft-0", + 1762: "cft-1", + 1763: "cft-2", + 1764: "cft-3", + 1765: "cft-4", + 1766: "cft-5", + 1767: "cft-6", + 1768: "cft-7", + 1769: "bmc-net-adm", + 1770: "bmc-net-svc", + 1771: "vaultbase", + 1772: "essweb-gw", + 1773: "kmscontrol", + 1774: "global-dtserv", + 1775: "vdab", + 1776: "femis", + 1777: "powerguardian", + 1778: "prodigy-intrnet", + 1779: "pharmasoft", + 1780: "dpkeyserv", + 1781: "answersoft-lm", + 1782: "hp-hcip", + 1784: "finle-lm", + 1785: "windlm", + 1786: "funk-logger", + 1787: "funk-license", + 1788: "psmond", + 1789: "hello", + 1790: "nmsp", + 1791: "ea1", + 1792: "ibm-dt-2", + 1793: "rsc-robot", + 1794: "cera-bcm", + 1795: "dpi-proxy", + 1796: "vocaltec-admin", + 1797: "uma", + 1798: "etp", + 1799: "netrisk", + 1800: "ansys-lm", + 1801: "msmq", + 1802: "concomp1", + 1803: "hp-hcip-gwy", + 1804: "enl", + 1805: "enl-name", + 1806: "musiconline", + 1807: "fhsp", + 1808: "oracle-vp2", + 1809: "oracle-vp1", + 1810: "jerand-lm", + 1811: "scientia-sdb", + 1812: "radius", + 1813: "radius-acct", + 1814: "tdp-suite", + 1815: "mmpft", + 1816: "harp", + 1817: "rkb-oscs", + 1818: "etftp", + 1819: "plato-lm", + 1820: "mcagent", + 1821: "donnyworld", + 1822: "es-elmd", + 1823: "unisys-lm", + 1824: "metrics-pas", + 1825: "direcpc-video", + 1826: "ardt", + 1827: "asi", + 1828: "itm-mcell-u", + 1829: "optika-emedia", + 1830: "net8-cman", + 1831: "myrtle", + 1832: "tht-treasure", + 1833: "udpradio", + 1834: "ardusuni", + 1835: "ardusmul", + 1836: "ste-smsc", + 1837: "csoft1", + 1838: "talnet", + 1839: "netopia-vo1", + 1840: "netopia-vo2", + 1841: "netopia-vo3", + 1842: "netopia-vo4", + 1843: "netopia-vo5", + 1844: "direcpc-dll", + 1845: "altalink", + 1846: "tunstall-pnc", + 1847: "slp-notify", + 1848: "fjdocdist", + 1849: "alpha-sms", + 1850: "gsi", + 1851: "ctcd", + 1852: "virtual-time", + 1853: "vids-avtp", + 1854: "buddy-draw", + 1855: "fiorano-rtrsvc", + 1856: "fiorano-msgsvc", + 1857: "datacaptor", + 1858: "privateark", + 1859: "gammafetchsvr", + 1860: "sunscalar-svc", + 1861: "lecroy-vicp", + 1862: "mysql-cm-agent", + 1863: "msnp", + 1864: "paradym-31port", + 1865: "entp", + 1866: "swrmi", + 1867: "udrive", + 1868: "viziblebrowser", + 1869: "transact", + 1870: "sunscalar-dns", + 1871: "canocentral0", + 1872: "canocentral1", + 1873: "fjmpjps", + 1874: "fjswapsnp", + 1875: "westell-stats", + 1876: "ewcappsrv", + 1877: "hp-webqosdb", + 1878: "drmsmc", + 1879: "nettgain-nms", + 1880: "vsat-control", + 1881: "ibm-mqseries2", + 1882: "ecsqdmn", + 1883: "mqtt", + 1884: "idmaps", + 1885: "vrtstrapserver", + 1886: "leoip", + 1887: "filex-lport", + 1888: "ncconfig", + 1889: "unify-adapter", + 1890: "wilkenlistener", + 1891: "childkey-notif", + 1892: "childkey-ctrl", + 1893: "elad", + 1894: "o2server-port", + 1896: "b-novative-ls", + 1897: "metaagent", + 1898: "cymtec-port", + 1899: "mc2studios", + 1900: "ssdp", + 1901: "fjicl-tep-a", + 1902: "fjicl-tep-b", + 1903: "linkname", + 1904: "fjicl-tep-c", + 1905: "sugp", + 1906: "tpmd", + 1907: "intrastar", + 1908: "dawn", + 1909: "global-wlink", + 1910: "ultrabac", + 1911: "mtp", + 1912: "rhp-iibp", + 1913: "armadp", + 1914: "elm-momentum", + 1915: "facelink", + 1916: "persona", + 1917: "noagent", + 1918: "can-nds", + 1919: "can-dch", + 1920: "can-ferret", + 1921: "noadmin", + 1922: "tapestry", + 1923: "spice", + 1924: "xiip", + 1925: "discovery-port", + 1926: "egs", + 1927: "videte-cipc", + 1928: "emsd-port", + 1929: "bandwiz-system", + 1930: "driveappserver", + 1931: "amdsched", + 1932: "ctt-broker", + 1933: "xmapi", + 1934: "xaapi", + 1935: "macromedia-fcs", + 1936: "jetcmeserver", + 1937: "jwserver", + 1938: "jwclient", + 1939: "jvserver", + 1940: "jvclient", + 1941: "dic-aida", + 1942: "res", + 1943: "beeyond-media", + 1944: "close-combat", + 1945: "dialogic-elmd", + 1946: "tekpls", + 1947: "sentinelsrm", + 1948: "eye2eye", + 1949: "ismaeasdaqlive", + 1950: "ismaeasdaqtest", + 1951: "bcs-lmserver", + 1952: "mpnjsc", + 1953: "rapidbase", + 1954: "abr-api", + 1955: "abr-secure", + 1956: "vrtl-vmf-ds", + 1957: "unix-status", + 1958: "dxadmind", + 1959: "simp-all", + 1960: "nasmanager", + 1961: "bts-appserver", + 1962: "biap-mp", + 1963: "webmachine", + 1964: "solid-e-engine", + 1965: "tivoli-npm", + 1966: "slush", + 1967: "sns-quote", + 1968: "lipsinc", + 1969: "lipsinc1", + 1970: "netop-rc", + 1971: "netop-school", + 1972: "intersys-cache", + 1973: "dlsrap", + 1974: "drp", + 1975: "tcoflashagent", + 1976: "tcoregagent", + 1977: "tcoaddressbook", + 1978: "unisql", + 1979: "unisql-java", + 1980: "pearldoc-xact", + 1981: "p2pq", + 1982: "estamp", + 1983: "lhtp", + 1984: "bb", + 1985: "hsrp", + 1986: "licensedaemon", + 1987: "tr-rsrb-p1", + 1988: "tr-rsrb-p2", + 1989: "tr-rsrb-p3", + 1990: "stun-p1", + 1991: "stun-p2", + 1992: "stun-p3", + 1993: "snmp-tcp-port", + 1994: "stun-port", + 1995: "perf-port", + 1996: "tr-rsrb-port", + 1997: "gdp-port", + 1998: "x25-svc-port", + 1999: "tcp-id-port", + 2000: "cisco-sccp", + 2001: "dc", + 2002: "globe", + 2003: "brutus", + 2004: "mailbox", + 2005: "berknet", + 2006: "invokator", + 2007: "dectalk", + 2008: "conf", + 2009: "news", + 2010: "search", + 2011: "raid-cc", + 2012: "ttyinfo", + 2013: "raid-am", + 2014: "troff", + 2015: "cypress", + 2016: "bootserver", + 2017: "cypress-stat", + 2018: "terminaldb", + 2019: "whosockami", + 2020: "xinupageserver", + 2021: "servexec", + 2022: "down", + 2023: "xinuexpansion3", + 2024: "xinuexpansion4", + 2025: "ellpack", + 2026: "scrabble", + 2027: "shadowserver", + 2028: "submitserver", + 2029: "hsrpv6", + 2030: "device2", + 2031: "mobrien-chat", + 2032: "blackboard", + 2033: "glogger", + 2034: "scoremgr", + 2035: "imsldoc", + 2036: "e-dpnet", + 2037: "applus", + 2038: "objectmanager", + 2039: "prizma", + 2040: "lam", + 2041: "interbase", + 2042: "isis", + 2043: "isis-bcast", + 2044: "rimsl", + 2045: "cdfunc", + 2046: "sdfunc", + 2047: "dls", + 2048: "dls-monitor", + 2049: "shilp", + 2050: "av-emb-config", + 2051: "epnsdp", + 2052: "clearvisn", + 2053: "lot105-ds-upd", + 2054: "weblogin", + 2055: "iop", + 2056: "omnisky", + 2057: "rich-cp", + 2058: "newwavesearch", + 2059: "bmc-messaging", + 2060: "teleniumdaemon", + 2061: "netmount", + 2062: "icg-swp", + 2063: "icg-bridge", + 2064: "icg-iprelay", + 2065: "dlsrpn", + 2066: "aura", + 2067: "dlswpn", + 2068: "avauthsrvprtcl", + 2069: "event-port", + 2070: "ah-esp-encap", + 2071: "acp-port", + 2072: "msync", + 2073: "gxs-data-port", + 2074: "vrtl-vmf-sa", + 2075: "newlixengine", + 2076: "newlixconfig", + 2077: "tsrmagt", + 2078: "tpcsrvr", + 2079: "idware-router", + 2080: "autodesk-nlm", + 2081: "kme-trap-port", + 2082: "infowave", + 2083: "radsec", + 2084: "sunclustergeo", + 2085: "ada-cip", + 2086: "gnunet", + 2087: "eli", + 2088: "ip-blf", + 2089: "sep", + 2090: "lrp", + 2091: "prp", + 2092: "descent3", + 2093: "nbx-cc", + 2094: "nbx-au", + 2095: "nbx-ser", + 2096: "nbx-dir", + 2097: "jetformpreview", + 2098: "dialog-port", + 2099: "h2250-annex-g", + 2100: "amiganetfs", + 2101: "rtcm-sc104", + 2102: "zephyr-srv", + 2103: "zephyr-clt", + 2104: "zephyr-hm", + 2105: "minipay", + 2106: "mzap", + 2107: "bintec-admin", + 2108: "comcam", + 2109: "ergolight", + 2110: "umsp", + 2111: "dsatp", + 2112: "idonix-metanet", + 2113: "hsl-storm", + 2114: "newheights", + 2115: "kdm", + 2116: "ccowcmr", + 2117: "mentaclient", + 2118: "mentaserver", + 2119: "gsigatekeeper", + 2120: "qencp", + 2121: "scientia-ssdb", + 2122: "caupc-remote", + 2123: "gtp-control", + 2124: "elatelink", + 2125: "lockstep", + 2126: "pktcable-cops", + 2127: "index-pc-wb", + 2128: "net-steward", + 2129: "cs-live", + 2130: "xds", + 2131: "avantageb2b", + 2132: "solera-epmap", + 2133: "zymed-zpp", + 2134: "avenue", + 2135: "gris", + 2136: "appworxsrv", + 2137: "connect", + 2138: "unbind-cluster", + 2139: "ias-auth", + 2140: "ias-reg", + 2141: "ias-admind", + 2142: "tdmoip", + 2143: "lv-jc", + 2144: "lv-ffx", + 2145: "lv-pici", + 2146: "lv-not", + 2147: "lv-auth", + 2148: "veritas-ucl", + 2149: "acptsys", + 2150: "dynamic3d", + 2151: "docent", + 2152: "gtp-user", + 2153: "ctlptc", + 2154: "stdptc", + 2155: "brdptc", + 2156: "trp", + 2157: "xnds", + 2158: "touchnetplus", + 2159: "gdbremote", + 2160: "apc-2160", + 2161: "apc-2161", + 2162: "navisphere", + 2163: "navisphere-sec", + 2164: "ddns-v3", + 2165: "x-bone-api", + 2166: "iwserver", + 2167: "raw-serial", + 2168: "easy-soft-mux", + 2169: "brain", + 2170: "eyetv", + 2171: "msfw-storage", + 2172: "msfw-s-storage", + 2173: "msfw-replica", + 2174: "msfw-array", + 2175: "airsync", + 2176: "rapi", + 2177: "qwave", + 2178: "bitspeer", + 2179: "vmrdp", + 2180: "mc-gt-srv", + 2181: "eforward", + 2182: "cgn-stat", + 2183: "cgn-config", + 2184: "nvd", + 2185: "onbase-dds", + 2186: "gtaua", + 2187: "ssmc", + 2188: "radware-rpm", + 2189: "radware-rpm-s", + 2190: "tivoconnect", + 2191: "tvbus", + 2192: "asdis", + 2193: "drwcs", + 2197: "mnp-exchange", + 2198: "onehome-remote", + 2199: "onehome-help", + 2200: "ici", + 2201: "ats", + 2202: "imtc-map", + 2203: "b2-runtime", + 2204: "b2-license", + 2205: "jps", + 2206: "hpocbus", + 2207: "hpssd", + 2208: "hpiod", + 2209: "rimf-ps", + 2210: "noaaport", + 2211: "emwin", + 2212: "leecoposserver", + 2213: "kali", + 2214: "rpi", + 2215: "ipcore", + 2216: "vtu-comms", + 2217: "gotodevice", + 2218: "bounzza", + 2219: "netiq-ncap", + 2220: "netiq", + 2221: "ethernet-ip-s", + 2222: "EtherNet-IP-1", + 2223: "rockwell-csp2", + 2224: "efi-mg", + 2225: "rcip-itu", + 2226: "di-drm", + 2227: "di-msg", + 2228: "ehome-ms", + 2229: "datalens", + 2230: "queueadm", + 2231: "wimaxasncp", + 2232: "ivs-video", + 2233: "infocrypt", + 2234: "directplay", + 2235: "sercomm-wlink", + 2236: "nani", + 2237: "optech-port1-lm", + 2238: "aviva-sna", + 2239: "imagequery", + 2240: "recipe", + 2241: "ivsd", + 2242: "foliocorp", + 2243: "magicom", + 2244: "nmsserver", + 2245: "hao", + 2246: "pc-mta-addrmap", + 2247: "antidotemgrsvr", + 2248: "ums", + 2249: "rfmp", + 2250: "remote-collab", + 2251: "dif-port", + 2252: "njenet-ssl", + 2253: "dtv-chan-req", + 2254: "seispoc", + 2255: "vrtp", + 2256: "pcc-mfp", + 2257: "simple-tx-rx", + 2258: "rcts", + 2260: "apc-2260", + 2261: "comotionmaster", + 2262: "comotionback", + 2263: "ecwcfg", + 2264: "apx500api-1", + 2265: "apx500api-2", + 2266: "mfserver", + 2267: "ontobroker", + 2268: "amt", + 2269: "mikey", + 2270: "starschool", + 2271: "mmcals", + 2272: "mmcal", + 2273: "mysql-im", + 2274: "pcttunnell", + 2275: "ibridge-data", + 2276: "ibridge-mgmt", + 2277: "bluectrlproxy", + 2278: "s3db", + 2279: "xmquery", + 2280: "lnvpoller", + 2281: "lnvconsole", + 2282: "lnvalarm", + 2283: "lnvstatus", + 2284: "lnvmaps", + 2285: "lnvmailmon", + 2286: "nas-metering", + 2287: "dna", + 2288: "netml", + 2289: "dict-lookup", + 2290: "sonus-logging", + 2291: "eapsp", + 2292: "mib-streaming", + 2293: "npdbgmngr", + 2294: "konshus-lm", + 2295: "advant-lm", + 2296: "theta-lm", + 2297: "d2k-datamover1", + 2298: "d2k-datamover2", + 2299: "pc-telecommute", + 2300: "cvmmon", + 2301: "cpq-wbem", + 2302: "binderysupport", + 2303: "proxy-gateway", + 2304: "attachmate-uts", + 2305: "mt-scaleserver", + 2306: "tappi-boxnet", + 2307: "pehelp", + 2308: "sdhelp", + 2309: "sdserver", + 2310: "sdclient", + 2311: "messageservice", + 2312: "wanscaler", + 2313: "iapp", + 2314: "cr-websystems", + 2315: "precise-sft", + 2316: "sent-lm", + 2317: "attachmate-g32", + 2318: "cadencecontrol", + 2319: "infolibria", + 2320: "siebel-ns", + 2321: "rdlap", + 2322: "ofsd", + 2323: "3d-nfsd", + 2324: "cosmocall", + 2325: "ansysli", + 2326: "idcp", + 2327: "xingcsm", + 2328: "netrix-sftm", + 2329: "nvd", + 2330: "tscchat", + 2331: "agentview", + 2332: "rcc-host", + 2333: "snapp", + 2334: "ace-client", + 2335: "ace-proxy", + 2336: "appleugcontrol", + 2337: "ideesrv", + 2338: "norton-lambert", + 2339: "3com-webview", + 2340: "wrs-registry", + 2341: "xiostatus", + 2342: "manage-exec", + 2343: "nati-logos", + 2344: "fcmsys", + 2345: "dbm", + 2346: "redstorm-join", + 2347: "redstorm-find", + 2348: "redstorm-info", + 2349: "redstorm-diag", + 2350: "psbserver", + 2351: "psrserver", + 2352: "pslserver", + 2353: "pspserver", + 2354: "psprserver", + 2355: "psdbserver", + 2356: "gxtelmd", + 2357: "unihub-server", + 2358: "futrix", + 2359: "flukeserver", + 2360: "nexstorindltd", + 2361: "tl1", + 2362: "digiman", + 2363: "mediacntrlnfsd", + 2364: "oi-2000", + 2365: "dbref", + 2366: "qip-login", + 2367: "service-ctrl", + 2368: "opentable", + 2370: "l3-hbmon", + 2371: "hp-rda", + 2372: "lanmessenger", + 2373: "remographlm", + 2374: "hydra", + 2375: "docker", + 2376: "docker-s", + 2377: "swarm", + 2379: "etcd-client", + 2380: "etcd-server", + 2381: "compaq-https", + 2382: "ms-olap3", + 2383: "ms-olap4", + 2384: "sd-request", + 2385: "sd-data", + 2386: "virtualtape", + 2387: "vsamredirector", + 2388: "mynahautostart", + 2389: "ovsessionmgr", + 2390: "rsmtp", + 2391: "3com-net-mgmt", + 2392: "tacticalauth", + 2393: "ms-olap1", + 2394: "ms-olap2", + 2395: "lan900-remote", + 2396: "wusage", + 2397: "ncl", + 2398: "orbiter", + 2399: "fmpro-fdal", + 2400: "opequus-server", + 2401: "cvspserver", + 2402: "taskmaster2000", + 2403: "taskmaster2000", + 2404: "iec-104", + 2405: "trc-netpoll", + 2406: "jediserver", + 2407: "orion", + 2408: "railgun-webaccl", + 2409: "sns-protocol", + 2410: "vrts-registry", + 2411: "netwave-ap-mgmt", + 2412: "cdn", + 2413: "orion-rmi-reg", + 2414: "beeyond", + 2415: "codima-rtp", + 2416: "rmtserver", + 2417: "composit-server", + 2418: "cas", + 2419: "attachmate-s2s", + 2420: "dslremote-mgmt", + 2421: "g-talk", + 2422: "crmsbits", + 2423: "rnrp", + 2424: "kofax-svr", + 2425: "fjitsuappmgr", + 2426: "vcmp", + 2427: "mgcp-gateway", + 2428: "ott", + 2429: "ft-role", + 2430: "venus", + 2431: "venus-se", + 2432: "codasrv", + 2433: "codasrv-se", + 2434: "pxc-epmap", + 2435: "optilogic", + 2436: "topx", + 2437: "unicontrol", + 2438: "msp", + 2439: "sybasedbsynch", + 2440: "spearway", + 2441: "pvsw-inet", + 2442: "netangel", + 2443: "powerclientcsf", + 2444: "btpp2sectrans", + 2445: "dtn1", + 2446: "bues-service", + 2447: "ovwdb", + 2448: "hpppssvr", + 2449: "ratl", + 2450: "netadmin", + 2451: "netchat", + 2452: "snifferclient", + 2453: "madge-ltd", + 2454: "indx-dds", + 2455: "wago-io-system", + 2456: "altav-remmgt", + 2457: "rapido-ip", + 2458: "griffin", + 2459: "community", + 2460: "ms-theater", + 2461: "qadmifoper", + 2462: "qadmifevent", + 2463: "lsi-raid-mgmt", + 2464: "direcpc-si", + 2465: "lbm", + 2466: "lbf", + 2467: "high-criteria", + 2468: "qip-msgd", + 2469: "mti-tcs-comm", + 2470: "taskman-port", + 2471: "seaodbc", + 2472: "c3", + 2473: "aker-cdp", + 2474: "vitalanalysis", + 2475: "ace-server", + 2476: "ace-svr-prop", + 2477: "ssm-cvs", + 2478: "ssm-cssps", + 2479: "ssm-els", + 2480: "powerexchange", + 2481: "giop", + 2482: "giop-ssl", + 2483: "ttc", + 2484: "ttc-ssl", + 2485: "netobjects1", + 2486: "netobjects2", + 2487: "pns", + 2488: "moy-corp", + 2489: "tsilb", + 2490: "qip-qdhcp", + 2491: "conclave-cpp", + 2492: "groove", + 2493: "talarian-mqs", + 2494: "bmc-ar", + 2495: "fast-rem-serv", + 2496: "dirgis", + 2497: "quaddb", + 2498: "odn-castraq", + 2499: "unicontrol", + 2500: "rtsserv", + 2501: "rtsclient", + 2502: "kentrox-prot", + 2503: "nms-dpnss", + 2504: "wlbs", + 2505: "ppcontrol", + 2506: "jbroker", + 2507: "spock", + 2508: "jdatastore", + 2509: "fjmpss", + 2510: "fjappmgrbulk", + 2511: "metastorm", + 2512: "citrixima", + 2513: "citrixadmin", + 2514: "facsys-ntp", + 2515: "facsys-router", + 2516: "maincontrol", + 2517: "call-sig-trans", + 2518: "willy", + 2519: "globmsgsvc", + 2520: "pvsw", + 2521: "adaptecmgr", + 2522: "windb", + 2523: "qke-llc-v3", + 2524: "optiwave-lm", + 2525: "ms-v-worlds", + 2526: "ema-sent-lm", + 2527: "iqserver", + 2528: "ncr-ccl", + 2529: "utsftp", + 2530: "vrcommerce", + 2531: "ito-e-gui", + 2532: "ovtopmd", + 2533: "snifferserver", + 2534: "combox-web-acc", + 2535: "madcap", + 2536: "btpp2audctr1", + 2537: "upgrade", + 2538: "vnwk-prapi", + 2539: "vsiadmin", + 2540: "lonworks", + 2541: "lonworks2", + 2542: "udrawgraph", + 2543: "reftek", + 2544: "novell-zen", + 2545: "sis-emt", + 2546: "vytalvaultbrtp", + 2547: "vytalvaultvsmp", + 2548: "vytalvaultpipe", + 2549: "ipass", + 2550: "ads", + 2551: "isg-uda-server", + 2552: "call-logging", + 2553: "efidiningport", + 2554: "vcnet-link-v10", + 2555: "compaq-wcp", + 2556: "nicetec-nmsvc", + 2557: "nicetec-mgmt", + 2558: "pclemultimedia", + 2559: "lstp", + 2560: "labrat", + 2561: "mosaixcc", + 2562: "delibo", + 2563: "cti-redwood", + 2564: "hp-3000-telnet", + 2565: "coord-svr", + 2566: "pcs-pcw", + 2567: "clp", + 2568: "spamtrap", + 2569: "sonuscallsig", + 2570: "hs-port", + 2571: "cecsvc", + 2572: "ibp", + 2573: "trustestablish", + 2574: "blockade-bpsp", + 2575: "hl7", + 2576: "tclprodebugger", + 2577: "scipticslsrvr", + 2578: "rvs-isdn-dcp", + 2579: "mpfoncl", + 2580: "tributary", + 2581: "argis-te", + 2582: "argis-ds", + 2583: "mon", + 2584: "cyaserv", + 2585: "netx-server", + 2586: "netx-agent", + 2587: "masc", + 2588: "privilege", + 2589: "quartus-tcl", + 2590: "idotdist", + 2591: "maytagshuffle", + 2592: "netrek", + 2593: "mns-mail", + 2594: "dts", + 2595: "worldfusion1", + 2596: "worldfusion2", + 2597: "homesteadglory", + 2598: "citriximaclient", + 2599: "snapd", + 2600: "hpstgmgr", + 2601: "discp-client", + 2602: "discp-server", + 2603: "servicemeter", + 2604: "nsc-ccs", + 2605: "nsc-posa", + 2606: "netmon", + 2607: "connection", + 2608: "wag-service", + 2609: "system-monitor", + 2610: "versa-tek", + 2611: "lionhead", + 2612: "qpasa-agent", + 2613: "smntubootstrap", + 2614: "neveroffline", + 2615: "firepower", + 2616: "appswitch-emp", + 2617: "cmadmin", + 2618: "priority-e-com", + 2619: "bruce", + 2620: "lpsrecommender", + 2621: "miles-apart", + 2622: "metricadbc", + 2623: "lmdp", + 2624: "aria", + 2625: "blwnkl-port", + 2626: "gbjd816", + 2627: "moshebeeri", + 2628: "dict", + 2629: "sitaraserver", + 2630: "sitaramgmt", + 2631: "sitaradir", + 2632: "irdg-post", + 2633: "interintelli", + 2634: "pk-electronics", + 2635: "backburner", + 2636: "solve", + 2637: "imdocsvc", + 2638: "sybaseanywhere", + 2639: "aminet", + 2640: "ami-control", + 2641: "hdl-srv", + 2642: "tragic", + 2643: "gte-samp", + 2644: "travsoft-ipx-t", + 2645: "novell-ipx-cmd", + 2646: "and-lm", + 2647: "syncserver", + 2648: "upsnotifyprot", + 2649: "vpsipport", + 2650: "eristwoguns", + 2651: "ebinsite", + 2652: "interpathpanel", + 2653: "sonus", + 2654: "corel-vncadmin", + 2655: "unglue", + 2656: "kana", + 2657: "sns-dispatcher", + 2658: "sns-admin", + 2659: "sns-query", + 2660: "gcmonitor", + 2661: "olhost", + 2662: "bintec-capi", + 2663: "bintec-tapi", + 2664: "patrol-mq-gm", + 2665: "patrol-mq-nm", + 2666: "extensis", + 2667: "alarm-clock-s", + 2668: "alarm-clock-c", + 2669: "toad", + 2670: "tve-announce", + 2671: "newlixreg", + 2672: "nhserver", + 2673: "firstcall42", + 2674: "ewnn", + 2675: "ttc-etap", + 2676: "simslink", + 2677: "gadgetgate1way", + 2678: "gadgetgate2way", + 2679: "syncserverssl", + 2680: "pxc-sapxom", + 2681: "mpnjsomb", + 2683: "ncdloadbalance", + 2684: "mpnjsosv", + 2685: "mpnjsocl", + 2686: "mpnjsomg", + 2687: "pq-lic-mgmt", + 2688: "md-cg-http", + 2689: "fastlynx", + 2690: "hp-nnm-data", + 2691: "itinternet", + 2692: "admins-lms", + 2694: "pwrsevent", + 2695: "vspread", + 2696: "unifyadmin", + 2697: "oce-snmp-trap", + 2698: "mck-ivpip", + 2699: "csoft-plusclnt", + 2700: "tqdata", + 2701: "sms-rcinfo", + 2702: "sms-xfer", + 2703: "sms-chat", + 2704: "sms-remctrl", + 2705: "sds-admin", + 2706: "ncdmirroring", + 2707: "emcsymapiport", + 2708: "banyan-net", + 2709: "supermon", + 2710: "sso-service", + 2711: "sso-control", + 2712: "aocp", + 2713: "raventbs", + 2714: "raventdm", + 2715: "hpstgmgr2", + 2716: "inova-ip-disco", + 2717: "pn-requester", + 2718: "pn-requester2", + 2719: "scan-change", + 2720: "wkars", + 2721: "smart-diagnose", + 2722: "proactivesrvr", + 2723: "watchdog-nt", + 2724: "qotps", + 2725: "msolap-ptp2", + 2726: "tams", + 2727: "mgcp-callagent", + 2728: "sqdr", + 2729: "tcim-control", + 2730: "nec-raidplus", + 2731: "fyre-messanger", + 2732: "g5m", + 2733: "signet-ctf", + 2734: "ccs-software", + 2735: "netiq-mc", + 2736: "radwiz-nms-srv", + 2737: "srp-feedback", + 2738: "ndl-tcp-ois-gw", + 2739: "tn-timing", + 2740: "alarm", + 2741: "tsb", + 2742: "tsb2", + 2743: "murx", + 2744: "honyaku", + 2745: "urbisnet", + 2746: "cpudpencap", + 2747: "fjippol-swrly", + 2748: "fjippol-polsvr", + 2749: "fjippol-cnsl", + 2750: "fjippol-port1", + 2751: "fjippol-port2", + 2752: "rsisysaccess", + 2753: "de-spot", + 2754: "apollo-cc", + 2755: "expresspay", + 2756: "simplement-tie", + 2757: "cnrp", + 2758: "apollo-status", + 2759: "apollo-gms", + 2760: "sabams", + 2761: "dicom-iscl", + 2762: "dicom-tls", + 2763: "desktop-dna", + 2764: "data-insurance", + 2765: "qip-audup", + 2766: "compaq-scp", + 2767: "uadtc", + 2768: "uacs", + 2769: "exce", + 2770: "veronica", + 2771: "vergencecm", + 2772: "auris", + 2773: "rbakcup1", + 2774: "rbakcup2", + 2775: "smpp", + 2776: "ridgeway1", + 2777: "ridgeway2", + 2778: "gwen-sonya", + 2779: "lbc-sync", + 2780: "lbc-control", + 2781: "whosells", + 2782: "everydayrc", + 2783: "aises", + 2784: "www-dev", + 2785: "aic-np", + 2786: "aic-oncrpc", + 2787: "piccolo", + 2788: "fryeserv", + 2789: "media-agent", + 2790: "plgproxy", + 2791: "mtport-regist", + 2792: "f5-globalsite", + 2793: "initlsmsad", + 2795: "livestats", + 2796: "ac-tech", + 2797: "esp-encap", + 2798: "tmesis-upshot", + 2799: "icon-discover", + 2800: "acc-raid", + 2801: "igcp", + 2802: "veritas-tcp1", + 2803: "btprjctrl", + 2804: "dvr-esm", + 2805: "wta-wsp-s", + 2806: "cspuni", + 2807: "cspmulti", + 2808: "j-lan-p", + 2809: "corbaloc", + 2810: "netsteward", + 2811: "gsiftp", + 2812: "atmtcp", + 2813: "llm-pass", + 2814: "llm-csv", + 2815: "lbc-measure", + 2816: "lbc-watchdog", + 2817: "nmsigport", + 2818: "rmlnk", + 2819: "fc-faultnotify", + 2820: "univision", + 2821: "vrts-at-port", + 2822: "ka0wuc", + 2823: "cqg-netlan", + 2824: "cqg-netlan-1", + 2826: "slc-systemlog", + 2827: "slc-ctrlrloops", + 2828: "itm-lm", + 2829: "silkp1", + 2830: "silkp2", + 2831: "silkp3", + 2832: "silkp4", + 2833: "glishd", + 2834: "evtp", + 2835: "evtp-data", + 2836: "catalyst", + 2837: "repliweb", + 2838: "starbot", + 2839: "nmsigport", + 2840: "l3-exprt", + 2841: "l3-ranger", + 2842: "l3-hawk", + 2843: "pdnet", + 2844: "bpcp-poll", + 2845: "bpcp-trap", + 2846: "aimpp-hello", + 2847: "aimpp-port-req", + 2848: "amt-blc-port", + 2849: "fxp", + 2850: "metaconsole", + 2851: "webemshttp", + 2852: "bears-01", + 2853: "ispipes", + 2854: "infomover", + 2855: "msrp", + 2856: "cesdinv", + 2857: "simctlp", + 2858: "ecnp", + 2859: "activememory", + 2860: "dialpad-voice1", + 2861: "dialpad-voice2", + 2862: "ttg-protocol", + 2863: "sonardata", + 2864: "astromed-main", + 2865: "pit-vpn", + 2866: "iwlistener", + 2867: "esps-portal", + 2868: "npep-messaging", + 2869: "icslap", + 2870: "daishi", + 2871: "msi-selectplay", + 2872: "radix", + 2874: "dxmessagebase1", + 2875: "dxmessagebase2", + 2876: "sps-tunnel", + 2877: "bluelance", + 2878: "aap", + 2879: "ucentric-ds", + 2880: "synapse", + 2881: "ndsp", + 2882: "ndtp", + 2883: "ndnp", + 2884: "flashmsg", + 2885: "topflow", + 2886: "responselogic", + 2887: "aironetddp", + 2888: "spcsdlobby", + 2889: "rsom", + 2890: "cspclmulti", + 2891: "cinegrfx-elmd", + 2892: "snifferdata", + 2893: "vseconnector", + 2894: "abacus-remote", + 2895: "natuslink", + 2896: "ecovisiong6-1", + 2897: "citrix-rtmp", + 2898: "appliance-cfg", + 2899: "powergemplus", + 2900: "quicksuite", + 2901: "allstorcns", + 2902: "netaspi", + 2903: "suitcase", + 2904: "m2ua", + 2905: "m3ua", + 2906: "caller9", + 2907: "webmethods-b2b", + 2908: "mao", + 2909: "funk-dialout", + 2910: "tdaccess", + 2911: "blockade", + 2912: "epicon", + 2913: "boosterware", + 2914: "gamelobby", + 2915: "tksocket", + 2916: "elvin-server", + 2917: "elvin-client", + 2918: "kastenchasepad", + 2919: "roboer", + 2920: "roboeda", + 2921: "cesdcdman", + 2922: "cesdcdtrn", + 2923: "wta-wsp-wtp-s", + 2924: "precise-vip", + 2926: "mobile-file-dl", + 2927: "unimobilectrl", + 2928: "redstone-cpss", + 2929: "amx-webadmin", + 2930: "amx-weblinx", + 2931: "circle-x", + 2932: "incp", + 2933: "4-tieropmgw", + 2934: "4-tieropmcli", + 2935: "qtp", + 2936: "otpatch", + 2937: "pnaconsult-lm", + 2938: "sm-pas-1", + 2939: "sm-pas-2", + 2940: "sm-pas-3", + 2941: "sm-pas-4", + 2942: "sm-pas-5", + 2943: "ttnrepository", + 2944: "megaco-h248", + 2945: "h248-binary", + 2946: "fjsvmpor", + 2947: "gpsd", + 2948: "wap-push", + 2949: "wap-pushsecure", + 2950: "esip", + 2951: "ottp", + 2952: "mpfwsas", + 2953: "ovalarmsrv", + 2954: "ovalarmsrv-cmd", + 2955: "csnotify", + 2956: "ovrimosdbman", + 2957: "jmact5", + 2958: "jmact6", + 2959: "rmopagt", + 2960: "dfoxserver", + 2961: "boldsoft-lm", + 2962: "iph-policy-cli", + 2963: "iph-policy-adm", + 2964: "bullant-srap", + 2965: "bullant-rap", + 2966: "idp-infotrieve", + 2967: "ssc-agent", + 2968: "enpp", + 2969: "essp", + 2970: "index-net", + 2971: "netclip", + 2972: "pmsm-webrctl", + 2973: "svnetworks", + 2974: "signal", + 2975: "fjmpcm", + 2976: "cns-srv-port", + 2977: "ttc-etap-ns", + 2978: "ttc-etap-ds", + 2979: "h263-video", + 2980: "wimd", + 2981: "mylxamport", + 2982: "iwb-whiteboard", + 2983: "netplan", + 2984: "hpidsadmin", + 2985: "hpidsagent", + 2986: "stonefalls", + 2987: "identify", + 2988: "hippad", + 2989: "zarkov", + 2990: "boscap", + 2991: "wkstn-mon", + 2992: "avenyo", + 2993: "veritas-vis1", + 2994: "veritas-vis2", + 2995: "idrs", + 2996: "vsixml", + 2997: "rebol", + 2998: "realsecure", + 2999: "remoteware-un", + 3000: "hbci", + 3001: "origo-native", + 3002: "exlm-agent", + 3003: "cgms", + 3004: "csoftragent", + 3005: "geniuslm", + 3006: "ii-admin", + 3007: "lotusmtap", + 3008: "midnight-tech", + 3009: "pxc-ntfy", + 3010: "gw", + 3011: "trusted-web", + 3012: "twsdss", + 3013: "gilatskysurfer", + 3014: "broker-service", + 3015: "nati-dstp", + 3016: "notify-srvr", + 3017: "event-listener", + 3018: "srvc-registry", + 3019: "resource-mgr", + 3020: "cifs", + 3021: "agriserver", + 3022: "csregagent", + 3023: "magicnotes", + 3024: "nds-sso", + 3025: "arepa-raft", + 3026: "agri-gateway", + 3027: "LiebDevMgmt-C", + 3028: "LiebDevMgmt-DM", + 3029: "LiebDevMgmt-A", + 3030: "arepa-cas", + 3031: "eppc", + 3032: "redwood-chat", + 3033: "pdb", + 3034: "osmosis-aeea", + 3035: "fjsv-gssagt", + 3036: "hagel-dump", + 3037: "hp-san-mgmt", + 3038: "santak-ups", + 3039: "cogitate", + 3040: "tomato-springs", + 3041: "di-traceware", + 3042: "journee", + 3043: "brp", + 3044: "epp", + 3045: "responsenet", + 3046: "di-ase", + 3047: "hlserver", + 3048: "pctrader", + 3049: "nsws", + 3050: "gds-db", + 3051: "galaxy-server", + 3052: "apc-3052", + 3053: "dsom-server", + 3054: "amt-cnf-prot", + 3055: "policyserver", + 3056: "cdl-server", + 3057: "goahead-fldup", + 3058: "videobeans", + 3059: "qsoft", + 3060: "interserver", + 3061: "cautcpd", + 3062: "ncacn-ip-tcp", + 3063: "ncadg-ip-udp", + 3064: "rprt", + 3065: "slinterbase", + 3066: "netattachsdmp", + 3067: "fjhpjp", + 3068: "ls3bcast", + 3069: "ls3", + 3070: "mgxswitch", + 3071: "xplat-replicate", + 3072: "csd-monitor", + 3073: "vcrp", + 3074: "xbox", + 3075: "orbix-locator", + 3076: "orbix-config", + 3077: "orbix-loc-ssl", + 3078: "orbix-cfg-ssl", + 3079: "lv-frontpanel", + 3080: "stm-pproc", + 3081: "tl1-lv", + 3082: "tl1-raw", + 3083: "tl1-telnet", + 3084: "itm-mccs", + 3085: "pcihreq", + 3086: "jdl-dbkitchen", + 3087: "asoki-sma", + 3088: "xdtp", + 3089: "ptk-alink", + 3090: "stss", + 3091: "1ci-smcs", + 3093: "rapidmq-center", + 3094: "rapidmq-reg", + 3095: "panasas", + 3096: "ndl-aps", + 3098: "umm-port", + 3099: "chmd", + 3100: "opcon-xps", + 3101: "hp-pxpib", + 3102: "slslavemon", + 3103: "autocuesmi", + 3104: "autocuelog", + 3105: "cardbox", + 3106: "cardbox-http", + 3107: "business", + 3108: "geolocate", + 3109: "personnel", + 3110: "sim-control", + 3111: "wsynch", + 3112: "ksysguard", + 3113: "cs-auth-svr", + 3114: "ccmad", + 3115: "mctet-master", + 3116: "mctet-gateway", + 3117: "mctet-jserv", + 3118: "pkagent", + 3119: "d2000kernel", + 3120: "d2000webserver", + 3121: "pcmk-remote", + 3122: "vtr-emulator", + 3123: "edix", + 3124: "beacon-port", + 3125: "a13-an", + 3127: "ctx-bridge", + 3128: "ndl-aas", + 3129: "netport-id", + 3130: "icpv2", + 3131: "netbookmark", + 3132: "ms-rule-engine", + 3133: "prism-deploy", + 3134: "ecp", + 3135: "peerbook-port", + 3136: "grubd", + 3137: "rtnt-1", + 3138: "rtnt-2", + 3139: "incognitorv", + 3140: "ariliamulti", + 3141: "vmodem", + 3142: "rdc-wh-eos", + 3143: "seaview", + 3144: "tarantella", + 3145: "csi-lfap", + 3146: "bears-02", + 3147: "rfio", + 3148: "nm-game-admin", + 3149: "nm-game-server", + 3150: "nm-asses-admin", + 3151: "nm-assessor", + 3152: "feitianrockey", + 3153: "s8-client-port", + 3154: "ccmrmi", + 3155: "jpegmpeg", + 3156: "indura", + 3157: "e3consultants", + 3158: "stvp", + 3159: "navegaweb-port", + 3160: "tip-app-server", + 3161: "doc1lm", + 3162: "sflm", + 3163: "res-sap", + 3164: "imprs", + 3165: "newgenpay", + 3166: "sossecollector", + 3167: "nowcontact", + 3168: "poweronnud", + 3169: "serverview-as", + 3170: "serverview-asn", + 3171: "serverview-gf", + 3172: "serverview-rm", + 3173: "serverview-icc", + 3174: "armi-server", + 3175: "t1-e1-over-ip", + 3176: "ars-master", + 3177: "phonex-port", + 3178: "radclientport", + 3179: "h2gf-w-2m", + 3180: "mc-brk-srv", + 3181: "bmcpatrolagent", + 3182: "bmcpatrolrnvu", + 3183: "cops-tls", + 3184: "apogeex-port", + 3185: "smpppd", + 3186: "iiw-port", + 3187: "odi-port", + 3188: "brcm-comm-port", + 3189: "pcle-infex", + 3190: "csvr-proxy", + 3191: "csvr-sslproxy", + 3192: "firemonrcc", + 3193: "spandataport", + 3194: "magbind", + 3195: "ncu-1", + 3196: "ncu-2", + 3197: "embrace-dp-s", + 3198: "embrace-dp-c", + 3199: "dmod-workspace", + 3200: "tick-port", + 3201: "cpq-tasksmart", + 3202: "intraintra", + 3203: "netwatcher-mon", + 3204: "netwatcher-db", + 3205: "isns", + 3206: "ironmail", + 3207: "vx-auth-port", + 3208: "pfu-prcallback", + 3209: "netwkpathengine", + 3210: "flamenco-proxy", + 3211: "avsecuremgmt", + 3212: "surveyinst", + 3213: "neon24x7", + 3214: "jmq-daemon-1", + 3215: "jmq-daemon-2", + 3216: "ferrari-foam", + 3217: "unite", + 3218: "smartpackets", + 3219: "wms-messenger", + 3220: "xnm-ssl", + 3221: "xnm-clear-text", + 3222: "glbp", + 3223: "digivote", + 3224: "aes-discovery", + 3225: "fcip-port", + 3226: "isi-irp", + 3227: "dwnmshttp", + 3228: "dwmsgserver", + 3229: "global-cd-port", + 3230: "sftdst-port", + 3231: "vidigo", + 3232: "mdtp", + 3233: "whisker", + 3234: "alchemy", + 3235: "mdap-port", + 3236: "apparenet-ts", + 3237: "apparenet-tps", + 3238: "apparenet-as", + 3239: "apparenet-ui", + 3240: "triomotion", + 3241: "sysorb", + 3242: "sdp-id-port", + 3243: "timelot", + 3244: "onesaf", + 3245: "vieo-fe", + 3246: "dvt-system", + 3247: "dvt-data", + 3248: "procos-lm", + 3249: "ssp", + 3250: "hicp", + 3251: "sysscanner", + 3252: "dhe", + 3253: "pda-data", + 3254: "pda-sys", + 3255: "semaphore", + 3256: "cpqrpm-agent", + 3257: "cpqrpm-server", + 3258: "ivecon-port", + 3259: "epncdp2", + 3260: "iscsi-target", + 3261: "winshadow", + 3262: "necp", + 3263: "ecolor-imager", + 3264: "ccmail", + 3265: "altav-tunnel", + 3266: "ns-cfg-server", + 3267: "ibm-dial-out", + 3268: "msft-gc", + 3269: "msft-gc-ssl", + 3270: "verismart", + 3271: "csoft-prev", + 3272: "user-manager", + 3273: "sxmp", + 3274: "ordinox-server", + 3275: "samd", + 3276: "maxim-asics", + 3277: "awg-proxy", + 3278: "lkcmserver", + 3279: "admind", + 3280: "vs-server", + 3281: "sysopt", + 3282: "datusorb", + 3283: "Apple Remote Desktop (Net Assistant)", + 3284: "4talk", + 3285: "plato", + 3286: "e-net", + 3287: "directvdata", + 3288: "cops", + 3289: "enpc", + 3290: "caps-lm", + 3291: "sah-lm", + 3292: "cart-o-rama", + 3293: "fg-fps", + 3294: "fg-gip", + 3295: "dyniplookup", + 3296: "rib-slm", + 3297: "cytel-lm", + 3298: "deskview", + 3299: "pdrncs", + 3300: "ceph", + 3302: "mcs-fastmail", + 3303: "opsession-clnt", + 3304: "opsession-srvr", + 3305: "odette-ftp", + 3306: "mysql", + 3307: "opsession-prxy", + 3308: "tns-server", + 3309: "tns-adv", + 3310: "dyna-access", + 3311: "mcns-tel-ret", + 3312: "appman-server", + 3313: "uorb", + 3314: "uohost", + 3315: "cdid", + 3316: "aicc-cmi", + 3317: "vsaiport", + 3318: "ssrip", + 3319: "sdt-lmd", + 3320: "officelink2000", + 3321: "vnsstr", + 3326: "sftu", + 3327: "bbars", + 3328: "egptlm", + 3329: "hp-device-disc", + 3330: "mcs-calypsoicf", + 3331: "mcs-messaging", + 3332: "mcs-mailsvr", + 3333: "dec-notes", + 3334: "directv-web", + 3335: "directv-soft", + 3336: "directv-tick", + 3337: "directv-catlg", + 3338: "anet-b", + 3339: "anet-l", + 3340: "anet-m", + 3341: "anet-h", + 3342: "webtie", + 3343: "ms-cluster-net", + 3344: "bnt-manager", + 3345: "influence", + 3346: "trnsprntproxy", + 3347: "phoenix-rpc", + 3348: "pangolin-laser", + 3349: "chevinservices", + 3350: "findviatv", + 3351: "btrieve", + 3352: "ssql", + 3353: "fatpipe", + 3354: "suitjd", + 3355: "ordinox-dbase", + 3356: "upnotifyps", + 3357: "adtech-test", + 3358: "mpsysrmsvr", + 3359: "wg-netforce", + 3360: "kv-server", + 3361: "kv-agent", + 3362: "dj-ilm", + 3363: "nati-vi-server", + 3364: "creativeserver", + 3365: "contentserver", + 3366: "creativepartnr", + 3372: "tip2", + 3373: "lavenir-lm", + 3374: "cluster-disc", + 3375: "vsnm-agent", + 3376: "cdbroker", + 3377: "cogsys-lm", + 3378: "wsicopy", + 3379: "socorfs", + 3380: "sns-channels", + 3381: "geneous", + 3382: "fujitsu-neat", + 3383: "esp-lm", + 3384: "hp-clic", + 3385: "qnxnetman", + 3386: "gprs-data", + 3387: "backroomnet", + 3388: "cbserver", + 3389: "ms-wbt-server", + 3390: "dsc", + 3391: "savant", + 3392: "efi-lm", + 3393: "d2k-tapestry1", + 3394: "d2k-tapestry2", + 3395: "dyna-lm", + 3396: "printer-agent", + 3397: "cloanto-lm", + 3398: "mercantile", + 3399: "csms", + 3400: "csms2", + 3401: "filecast", + 3402: "fxaengine-net", + 3405: "nokia-ann-ch1", + 3406: "nokia-ann-ch2", + 3407: "ldap-admin", + 3408: "BESApi", + 3409: "networklens", + 3410: "networklenss", + 3411: "biolink-auth", + 3412: "xmlblaster", + 3413: "svnet", + 3414: "wip-port", + 3415: "bcinameservice", + 3416: "commandport", + 3417: "csvr", + 3418: "rnmap", + 3419: "softaudit", + 3420: "ifcp-port", + 3421: "bmap", + 3422: "rusb-sys-port", + 3423: "xtrm", + 3424: "xtrms", + 3425: "agps-port", + 3426: "arkivio", + 3427: "websphere-snmp", + 3428: "twcss", + 3429: "gcsp", + 3430: "ssdispatch", + 3431: "ndl-als", + 3432: "osdcp", + 3433: "opnet-smp", + 3434: "opencm", + 3435: "pacom", + 3436: "gc-config", + 3437: "autocueds", + 3438: "spiral-admin", + 3439: "hri-port", + 3440: "ans-console", + 3441: "connect-client", + 3442: "connect-server", + 3443: "ov-nnm-websrv", + 3444: "denali-server", + 3445: "monp", + 3446: "3comfaxrpc", + 3447: "directnet", + 3448: "dnc-port", + 3449: "hotu-chat", + 3450: "castorproxy", + 3451: "asam", + 3452: "sabp-signal", + 3453: "pscupd", + 3454: "mira", + 3455: "prsvp", + 3456: "vat", + 3457: "vat-control", + 3458: "d3winosfi", + 3459: "integral", + 3460: "edm-manager", + 3461: "edm-stager", + 3462: "edm-std-notify", + 3463: "edm-adm-notify", + 3464: "edm-mgr-sync", + 3465: "edm-mgr-cntrl", + 3466: "workflow", + 3467: "rcst", + 3468: "ttcmremotectrl", + 3469: "pluribus", + 3470: "jt400", + 3471: "jt400-ssl", + 3472: "jaugsremotec-1", + 3473: "jaugsremotec-2", + 3474: "ttntspauto", + 3475: "genisar-port", + 3476: "nppmp", + 3477: "ecomm", + 3478: "stun", + 3479: "twrpc", + 3480: "plethora", + 3481: "cleanerliverc", + 3482: "vulture", + 3483: "slim-devices", + 3484: "gbs-stp", + 3485: "celatalk", + 3486: "ifsf-hb-port", + 3487: "ltctcp", + 3488: "fs-rh-srv", + 3489: "dtp-dia", + 3490: "colubris", + 3491: "swr-port", + 3492: "tvdumtray-port", + 3493: "nut", + 3494: "ibm3494", + 3495: "seclayer-tcp", + 3496: "seclayer-tls", + 3497: "ipether232port", + 3498: "dashpas-port", + 3499: "sccip-media", + 3500: "rtmp-port", + 3501: "isoft-p2p", + 3502: "avinstalldisc", + 3503: "lsp-ping", + 3504: "ironstorm", + 3505: "ccmcomm", + 3506: "apc-3506", + 3507: "nesh-broker", + 3508: "interactionweb", + 3509: "vt-ssl", + 3510: "xss-port", + 3511: "webmail-2", + 3512: "aztec", + 3513: "arcpd", + 3514: "must-p2p", + 3515: "must-backplane", + 3516: "smartcard-port", + 3517: "802-11-iapp", + 3518: "artifact-msg", + 3519: "nvmsgd", + 3520: "galileolog", + 3521: "mc3ss", + 3522: "nssocketport", + 3523: "odeumservlink", + 3524: "ecmport", + 3525: "eisport", + 3526: "starquiz-port", + 3527: "beserver-msg-q", + 3528: "jboss-iiop", + 3529: "jboss-iiop-ssl", + 3530: "gf", + 3531: "joltid", + 3532: "raven-rmp", + 3533: "raven-rdp", + 3534: "urld-port", + 3535: "ms-la", + 3536: "snac", + 3537: "ni-visa-remote", + 3538: "ibm-diradm", + 3539: "ibm-diradm-ssl", + 3540: "pnrp-port", + 3541: "voispeed-port", + 3542: "hacl-monitor", + 3543: "qftest-lookup", + 3544: "teredo", + 3545: "camac", + 3547: "symantec-sim", + 3548: "interworld", + 3549: "tellumat-nms", + 3550: "ssmpp", + 3551: "apcupsd", + 3552: "taserver", + 3553: "rbr-discovery", + 3554: "questnotify", + 3555: "razor", + 3556: "sky-transport", + 3557: "personalos-001", + 3558: "mcp-port", + 3559: "cctv-port", + 3560: "iniserve-port", + 3561: "bmc-onekey", + 3562: "sdbproxy", + 3563: "watcomdebug", + 3564: "esimport", + 3565: "m2pa", + 3566: "quest-data-hub", + 3567: "dof-eps", + 3568: "dof-tunnel-sec", + 3569: "mbg-ctrl", + 3570: "mccwebsvr-port", + 3571: "megardsvr-port", + 3572: "megaregsvrport", + 3573: "tag-ups-1", + 3574: "dmaf-server", + 3575: "ccm-port", + 3576: "cmc-port", + 3577: "config-port", + 3578: "data-port", + 3579: "ttat3lb", + 3580: "nati-svrloc", + 3581: "kfxaclicensing", + 3582: "press", + 3583: "canex-watch", + 3584: "u-dbap", + 3585: "emprise-lls", + 3586: "emprise-lsc", + 3587: "p2pgroup", + 3588: "sentinel", + 3589: "isomair", + 3590: "wv-csp-sms", + 3591: "gtrack-server", + 3592: "gtrack-ne", + 3593: "bpmd", + 3594: "mediaspace", + 3595: "shareapp", + 3596: "iw-mmogame", + 3597: "a14", + 3598: "a15", + 3599: "quasar-server", + 3600: "trap-daemon", + 3601: "visinet-gui", + 3602: "infiniswitchcl", + 3603: "int-rcv-cntrl", + 3604: "bmc-jmx-port", + 3605: "comcam-io", + 3606: "splitlock", + 3607: "precise-i3", + 3608: "trendchip-dcp", + 3609: "cpdi-pidas-cm", + 3610: "echonet", + 3611: "six-degrees", + 3612: "hp-dataprotect", + 3613: "alaris-disc", + 3614: "sigma-port", + 3615: "start-network", + 3616: "cd3o-protocol", + 3617: "sharp-server", + 3618: "aairnet-1", + 3619: "aairnet-2", + 3620: "ep-pcp", + 3621: "ep-nsp", + 3622: "ff-lr-port", + 3623: "haipe-discover", + 3624: "dist-upgrade", + 3625: "volley", + 3626: "bvcdaemon-port", + 3627: "jamserverport", + 3628: "ept-machine", + 3629: "escvpnet", + 3630: "cs-remote-db", + 3631: "cs-services", + 3632: "distcc", + 3633: "wacp", + 3634: "hlibmgr", + 3635: "sdo", + 3636: "servistaitsm", + 3637: "scservp", + 3638: "ehp-backup", + 3639: "xap-ha", + 3640: "netplay-port1", + 3641: "netplay-port2", + 3642: "juxml-port", + 3643: "audiojuggler", + 3644: "ssowatch", + 3645: "cyc", + 3646: "xss-srv-port", + 3647: "splitlock-gw", + 3648: "fjcp", + 3649: "nmmp", + 3650: "prismiq-plugin", + 3651: "xrpc-registry", + 3652: "vxcrnbuport", + 3653: "tsp", + 3654: "vaprtm", + 3655: "abatemgr", + 3656: "abatjss", + 3657: "immedianet-bcn", + 3658: "ps-ams", + 3659: "apple-sasl", + 3660: "can-nds-ssl", + 3661: "can-ferret-ssl", + 3662: "pserver", + 3663: "dtp", + 3664: "ups-engine", + 3665: "ent-engine", + 3666: "eserver-pap", + 3667: "infoexch", + 3668: "dell-rm-port", + 3669: "casanswmgmt", + 3670: "smile", + 3671: "efcp", + 3672: "lispworks-orb", + 3673: "mediavault-gui", + 3674: "wininstall-ipc", + 3675: "calltrax", + 3676: "va-pacbase", + 3677: "roverlog", + 3678: "ipr-dglt", + 3679: "Escale (Newton Dock)", + 3680: "npds-tracker", + 3681: "bts-x73", + 3682: "cas-mapi", + 3683: "bmc-ea", + 3684: "faxstfx-port", + 3685: "dsx-agent", + 3686: "tnmpv2", + 3687: "simple-push", + 3688: "simple-push-s", + 3689: "daap", + 3690: "svn", + 3691: "magaya-network", + 3692: "intelsync", + 3693: "easl", + 3695: "bmc-data-coll", + 3696: "telnetcpcd", + 3697: "nw-license", + 3698: "sagectlpanel", + 3699: "kpn-icw", + 3700: "lrs-paging", + 3701: "netcelera", + 3702: "ws-discovery", + 3703: "adobeserver-3", + 3704: "adobeserver-4", + 3705: "adobeserver-5", + 3706: "rt-event", + 3707: "rt-event-s", + 3708: "sun-as-iiops", + 3709: "ca-idms", + 3710: "portgate-auth", + 3711: "edb-server2", + 3712: "sentinel-ent", + 3713: "tftps", + 3714: "delos-dms", + 3715: "anoto-rendezv", + 3716: "wv-csp-sms-cir", + 3717: "wv-csp-udp-cir", + 3718: "opus-services", + 3719: "itelserverport", + 3720: "ufastro-instr", + 3721: "xsync", + 3722: "xserveraid", + 3723: "sychrond", + 3724: "blizwow", + 3725: "na-er-tip", + 3726: "array-manager", + 3727: "e-mdu", + 3728: "e-woa", + 3729: "fksp-audit", + 3730: "client-ctrl", + 3731: "smap", + 3732: "m-wnn", + 3733: "multip-msg", + 3734: "synel-data", + 3735: "pwdis", + 3736: "rs-rmi", + 3737: "xpanel", + 3738: "versatalk", + 3739: "launchbird-lm", + 3740: "heartbeat", + 3741: "wysdma", + 3742: "cst-port", + 3743: "ipcs-command", + 3744: "sasg", + 3745: "gw-call-port", + 3746: "linktest", + 3747: "linktest-s", + 3748: "webdata", + 3749: "cimtrak", + 3750: "cbos-ip-port", + 3751: "gprs-cube", + 3752: "vipremoteagent", + 3753: "nattyserver", + 3754: "timestenbroker", + 3755: "sas-remote-hlp", + 3756: "canon-capt", + 3757: "grf-port", + 3758: "apw-registry", + 3759: "exapt-lmgr", + 3760: "adtempusclient", + 3761: "gsakmp", + 3762: "gbs-smp", + 3763: "xo-wave", + 3764: "mni-prot-rout", + 3765: "rtraceroute", + 3766: "sitewatch-s", + 3767: "listmgr-port", + 3768: "rblcheckd", + 3769: "haipe-otnk", + 3770: "cindycollab", + 3771: "paging-port", + 3772: "ctp", + 3773: "ctdhercules", + 3774: "zicom", + 3775: "ispmmgr", + 3776: "dvcprov-port", + 3777: "jibe-eb", + 3778: "c-h-it-port", + 3779: "cognima", + 3780: "nnp", + 3781: "abcvoice-port", + 3782: "iso-tp0s", + 3783: "bim-pem", + 3784: "bfd-control", + 3785: "bfd-echo", + 3786: "upstriggervsw", + 3787: "fintrx", + 3788: "isrp-port", + 3789: "remotedeploy", + 3790: "quickbooksrds", + 3791: "tvnetworkvideo", + 3792: "sitewatch", + 3793: "dcsoftware", + 3794: "jaus", + 3795: "myblast", + 3796: "spw-dialer", + 3797: "idps", + 3798: "minilock", + 3799: "radius-dynauth", + 3800: "pwgpsi", + 3801: "ibm-mgr", + 3802: "vhd", + 3803: "soniqsync", + 3804: "iqnet-port", + 3805: "tcpdataserver", + 3806: "wsmlb", + 3807: "spugna", + 3808: "sun-as-iiops-ca", + 3809: "apocd", + 3810: "wlanauth", + 3811: "amp", + 3812: "neto-wol-server", + 3813: "rap-ip", + 3814: "neto-dcs", + 3815: "lansurveyorxml", + 3816: "sunlps-http", + 3817: "tapeware", + 3818: "crinis-hb", + 3819: "epl-slp", + 3820: "scp", + 3821: "pmcp", + 3822: "acp-discovery", + 3823: "acp-conduit", + 3824: "acp-policy", + 3825: "ffserver", + 3826: "warmux", + 3827: "netmpi", + 3828: "neteh", + 3829: "neteh-ext", + 3830: "cernsysmgmtagt", + 3831: "dvapps", + 3832: "xxnetserver", + 3833: "aipn-auth", + 3834: "spectardata", + 3835: "spectardb", + 3836: "markem-dcp", + 3837: "mkm-discovery", + 3838: "sos", + 3839: "amx-rms", + 3840: "flirtmitmir", + 3841: "shiprush-db-svr", + 3842: "nhci", + 3843: "quest-agent", + 3844: "rnm", + 3845: "v-one-spp", + 3846: "an-pcp", + 3847: "msfw-control", + 3848: "item", + 3849: "spw-dnspreload", + 3850: "qtms-bootstrap", + 3851: "spectraport", + 3852: "sse-app-config", + 3853: "sscan", + 3854: "stryker-com", + 3855: "opentrac", + 3856: "informer", + 3857: "trap-port", + 3858: "trap-port-mom", + 3859: "nav-port", + 3860: "sasp", + 3861: "winshadow-hd", + 3862: "giga-pocket", + 3863: "asap-tcp", + 3864: "asap-tcp-tls", + 3865: "xpl", + 3866: "dzdaemon", + 3867: "dzoglserver", + 3868: "diameter", + 3869: "ovsam-mgmt", + 3870: "ovsam-d-agent", + 3871: "avocent-adsap", + 3872: "oem-agent", + 3873: "fagordnc", + 3874: "sixxsconfig", + 3875: "pnbscada", + 3876: "dl-agent", + 3877: "xmpcr-interface", + 3878: "fotogcad", + 3879: "appss-lm", + 3880: "igrs", + 3881: "idac", + 3882: "msdts1", + 3883: "vrpn", + 3884: "softrack-meter", + 3885: "topflow-ssl", + 3886: "nei-management", + 3887: "ciphire-data", + 3888: "ciphire-serv", + 3889: "dandv-tester", + 3890: "ndsconnect", + 3891: "rtc-pm-port", + 3892: "pcc-image-port", + 3893: "cgi-starapi", + 3894: "syam-agent", + 3895: "syam-smc", + 3896: "sdo-tls", + 3897: "sdo-ssh", + 3898: "senip", + 3899: "itv-control", + 3900: "udt-os", + 3901: "nimsh", + 3902: "nimaux", + 3903: "charsetmgr", + 3904: "omnilink-port", + 3905: "mupdate", + 3906: "topovista-data", + 3907: "imoguia-port", + 3908: "hppronetman", + 3909: "surfcontrolcpa", + 3910: "prnrequest", + 3911: "prnstatus", + 3912: "gbmt-stars", + 3913: "listcrt-port", + 3914: "listcrt-port-2", + 3915: "agcat", + 3916: "wysdmc", + 3917: "aftmux", + 3918: "pktcablemmcops", + 3919: "hyperip", + 3920: "exasoftport1", + 3921: "herodotus-net", + 3922: "sor-update", + 3923: "symb-sb-port", + 3924: "mpl-gprs-port", + 3925: "zmp", + 3926: "winport", + 3927: "natdataservice", + 3928: "netboot-pxe", + 3929: "smauth-port", + 3930: "syam-webserver", + 3931: "msr-plugin-port", + 3932: "dyn-site", + 3933: "plbserve-port", + 3934: "sunfm-port", + 3935: "sdp-portmapper", + 3936: "mailprox", + 3937: "dvbservdsc", + 3938: "dbcontrol-agent", + 3939: "aamp", + 3940: "xecp-node", + 3941: "homeportal-web", + 3942: "srdp", + 3943: "tig", + 3944: "sops", + 3945: "emcads", + 3946: "backupedge", + 3947: "ccp", + 3948: "apdap", + 3949: "drip", + 3950: "namemunge", + 3951: "pwgippfax", + 3952: "i3-sessionmgr", + 3953: "xmlink-connect", + 3954: "adrep", + 3955: "p2pcommunity", + 3956: "gvcp", + 3957: "mqe-broker", + 3958: "mqe-agent", + 3959: "treehopper", + 3960: "bess", + 3961: "proaxess", + 3962: "sbi-agent", + 3963: "thrp", + 3964: "sasggprs", + 3965: "ati-ip-to-ncpe", + 3966: "bflckmgr", + 3967: "ppsms", + 3968: "ianywhere-dbns", + 3969: "landmarks", + 3970: "lanrevagent", + 3971: "lanrevserver", + 3972: "iconp", + 3973: "progistics", + 3974: "citysearch", + 3975: "airshot", + 3976: "opswagent", + 3977: "opswmanager", + 3978: "secure-cfg-svr", + 3979: "smwan", + 3980: "acms", + 3981: "starfish", + 3982: "eis", + 3983: "eisp", + 3984: "mapper-nodemgr", + 3985: "mapper-mapethd", + 3986: "mapper-ws-ethd", + 3987: "centerline", + 3988: "dcs-config", + 3989: "bv-queryengine", + 3990: "bv-is", + 3991: "bv-smcsrv", + 3992: "bv-ds", + 3993: "bv-agent", + 3995: "iss-mgmt-ssl", + 3996: "abcsoftware", + 3997: "agentsease-db", + 3998: "dnx", + 3999: "nvcnet", + 4000: "terabase", + 4001: "newoak", + 4002: "pxc-spvr-ft", + 4003: "pxc-splr-ft", + 4004: "pxc-roid", + 4005: "pxc-pin", + 4006: "pxc-spvr", + 4007: "pxc-splr", + 4008: "netcheque", + 4009: "chimera-hwm", + 4010: "samsung-unidex", + 4011: "altserviceboot", + 4012: "pda-gate", + 4013: "acl-manager", + 4014: "taiclock", + 4015: "talarian-mcast1", + 4016: "talarian-mcast2", + 4017: "talarian-mcast3", + 4018: "talarian-mcast4", + 4019: "talarian-mcast5", + 4020: "trap", + 4021: "nexus-portal", + 4022: "dnox", + 4023: "esnm-zoning", + 4024: "tnp1-port", + 4025: "partimage", + 4026: "as-debug", + 4027: "bxp", + 4028: "dtserver-port", + 4029: "ip-qsig", + 4030: "jdmn-port", + 4031: "suucp", + 4032: "vrts-auth-port", + 4033: "sanavigator", + 4034: "ubxd", + 4035: "wap-push-http", + 4036: "wap-push-https", + 4037: "ravehd", + 4038: "fazzt-ptp", + 4039: "fazzt-admin", + 4040: "yo-main", + 4041: "houston", + 4042: "ldxp", + 4043: "nirp", + 4044: "ltp", + 4045: "npp", + 4046: "acp-proto", + 4047: "ctp-state", + 4049: "wafs", + 4050: "cisco-wafs", + 4051: "cppdp", + 4052: "interact", + 4053: "ccu-comm-1", + 4054: "ccu-comm-2", + 4055: "ccu-comm-3", + 4056: "lms", + 4057: "wfm", + 4058: "kingfisher", + 4059: "dlms-cosem", + 4060: "dsmeter-iatc", + 4061: "ice-location", + 4062: "ice-slocation", + 4063: "ice-router", + 4064: "ice-srouter", + 4065: "avanti-cdp", + 4066: "pmas", + 4067: "idp", + 4068: "ipfltbcst", + 4069: "minger", + 4070: "tripe", + 4071: "aibkup", + 4072: "zieto-sock", + 4073: "iRAPP", + 4074: "cequint-cityid", + 4075: "perimlan", + 4076: "seraph", + 4078: "cssp", + 4079: "santools", + 4080: "lorica-in", + 4081: "lorica-in-sec", + 4082: "lorica-out", + 4083: "lorica-out-sec", + 4085: "ezmessagesrv", + 4087: "applusservice", + 4088: "npsp", + 4089: "opencore", + 4090: "omasgport", + 4091: "ewinstaller", + 4092: "ewdgs", + 4093: "pvxpluscs", + 4094: "sysrqd", + 4095: "xtgui", + 4096: "bre", + 4097: "patrolview", + 4098: "drmsfsd", + 4099: "dpcp", + 4100: "igo-incognito", + 4101: "brlp-0", + 4102: "brlp-1", + 4103: "brlp-2", + 4104: "brlp-3", + 4105: "shofar", + 4106: "synchronite", + 4107: "j-ac", + 4108: "accel", + 4109: "izm", + 4110: "g2tag", + 4111: "xgrid", + 4112: "apple-vpns-rp", + 4113: "aipn-reg", + 4114: "jomamqmonitor", + 4115: "cds", + 4116: "smartcard-tls", + 4117: "hillrserv", + 4118: "netscript", + 4119: "assuria-slm", + 4120: "minirem", + 4121: "e-builder", + 4122: "fprams", + 4123: "z-wave", + 4124: "tigv2", + 4125: "opsview-envoy", + 4126: "ddrepl", + 4127: "unikeypro", + 4128: "nufw", + 4129: "nuauth", + 4130: "fronet", + 4131: "stars", + 4132: "nuts-dem", + 4133: "nuts-bootp", + 4134: "nifty-hmi", + 4135: "cl-db-attach", + 4136: "cl-db-request", + 4137: "cl-db-remote", + 4138: "nettest", + 4139: "thrtx", + 4140: "cedros-fds", + 4141: "oirtgsvc", + 4142: "oidocsvc", + 4143: "oidsr", + 4145: "vvr-control", + 4146: "tgcconnect", + 4147: "vrxpservman", + 4148: "hhb-handheld", + 4149: "agslb", + 4150: "PowerAlert-nsa", + 4151: "menandmice-noh", + 4152: "idig-mux", + 4153: "mbl-battd", + 4154: "atlinks", + 4155: "bzr", + 4156: "stat-results", + 4157: "stat-scanner", + 4158: "stat-cc", + 4159: "nss", + 4160: "jini-discovery", + 4161: "omscontact", + 4162: "omstopology", + 4163: "silverpeakpeer", + 4164: "silverpeakcomm", + 4165: "altcp", + 4166: "joost", + 4167: "ddgn", + 4168: "pslicser", + 4169: "iadt", + 4170: "d-cinema-csp", + 4171: "ml-svnet", + 4172: "pcoip", + 4174: "smcluster", + 4175: "bccp", + 4176: "tl-ipcproxy", + 4177: "wello", + 4178: "storman", + 4179: "MaxumSP", + 4180: "httpx", + 4181: "macbak", + 4182: "pcptcpservice", + 4183: "cyborgnet", + 4184: "universe-suite", + 4185: "wcpp", + 4186: "boxbackupstore", + 4187: "csc-proxy", + 4188: "vatata", + 4189: "pcep", + 4190: "sieve", + 4192: "azeti", + 4193: "pvxplusio", + 4197: "hctl", + 4199: "eims-admin", + 4300: "corelccam", + 4301: "d-data", + 4302: "d-data-control", + 4303: "srcp", + 4304: "owserver", + 4305: "batman", + 4306: "pinghgl", + 4307: "trueconf", + 4308: "compx-lockview", + 4309: "dserver", + 4310: "mirrtex", + 4311: "p6ssmc", + 4312: "pscl-mgt", + 4313: "perrla", + 4314: "choiceview-agt", + 4316: "choiceview-clt", + 4320: "fdt-rcatp", + 4321: "rwhois", + 4322: "trim-event", + 4323: "trim-ice", + 4325: "geognosisman", + 4326: "geognosis", + 4327: "jaxer-web", + 4328: "jaxer-manager", + 4329: "publiqare-sync", + 4330: "dey-sapi", + 4331: "ktickets-rest", + 4333: "ahsp", + 4334: "netconf-ch-ssh", + 4335: "netconf-ch-tls", + 4336: "restconf-ch-tls", + 4340: "gaia", + 4341: "lisp-data", + 4342: "lisp-cons", + 4343: "unicall", + 4344: "vinainstall", + 4345: "m4-network-as", + 4346: "elanlm", + 4347: "lansurveyor", + 4348: "itose", + 4349: "fsportmap", + 4350: "net-device", + 4351: "plcy-net-svcs", + 4352: "pjlink", + 4353: "f5-iquery", + 4354: "qsnet-trans", + 4355: "qsnet-workst", + 4356: "qsnet-assist", + 4357: "qsnet-cond", + 4358: "qsnet-nucl", + 4359: "omabcastltkm", + 4360: "matrix-vnet", + 4368: "wxbrief", + 4369: "epmd", + 4370: "elpro-tunnel", + 4371: "l2c-control", + 4372: "l2c-data", + 4373: "remctl", + 4374: "psi-ptt", + 4375: "tolteces", + 4376: "bip", + 4377: "cp-spxsvr", + 4378: "cp-spxdpy", + 4379: "ctdb", + 4389: "xandros-cms", + 4390: "wiegand", + 4391: "apwi-imserver", + 4392: "apwi-rxserver", + 4393: "apwi-rxspooler", + 4395: "omnivisionesx", + 4396: "fly", + 4400: "ds-srv", + 4401: "ds-srvr", + 4402: "ds-clnt", + 4403: "ds-user", + 4404: "ds-admin", + 4405: "ds-mail", + 4406: "ds-slp", + 4407: "nacagent", + 4408: "slscc", + 4409: "netcabinet-com", + 4410: "itwo-server", + 4411: "found", + 4413: "avi-nms", + 4414: "updog", + 4415: "brcd-vr-req", + 4416: "pjj-player", + 4417: "workflowdir", + 4419: "cbp", + 4420: "nvm-express", + 4421: "scaleft", + 4422: "tsepisp", + 4423: "thingkit", + 4425: "netrockey6", + 4426: "beacon-port-2", + 4427: "drizzle", + 4428: "omviserver", + 4429: "omviagent", + 4430: "rsqlserver", + 4431: "wspipe", + 4432: "l-acoustics", + 4433: "vop", + 4442: "saris", + 4443: "pharos", + 4444: "krb524", + 4445: "upnotifyp", + 4446: "n1-fwp", + 4447: "n1-rmgmt", + 4448: "asc-slmd", + 4449: "privatewire", + 4450: "camp", + 4451: "ctisystemmsg", + 4452: "ctiprogramload", + 4453: "nssalertmgr", + 4454: "nssagentmgr", + 4455: "prchat-user", + 4456: "prchat-server", + 4457: "prRegister", + 4458: "mcp", + 4484: "hpssmgmt", + 4485: "assyst-dr", + 4486: "icms", + 4487: "prex-tcp", + 4488: "awacs-ice", + 4500: "ipsec-nat-t", + 4535: "ehs", + 4536: "ehs-ssl", + 4537: "wssauthsvc", + 4538: "swx-gate", + 4545: "worldscores", + 4546: "sf-lm", + 4547: "lanner-lm", + 4548: "synchromesh", + 4549: "aegate", + 4550: "gds-adppiw-db", + 4551: "ieee-mih", + 4552: "menandmice-mon", + 4553: "icshostsvc", + 4554: "msfrs", + 4555: "rsip", + 4556: "dtn-bundle", + 4559: "hylafax", + 4563: "amahi-anywhere", + 4566: "kwtc", + 4567: "tram", + 4568: "bmc-reporting", + 4569: "iax", + 4570: "deploymentmap", + 4573: "cardifftec-back", + 4590: "rid", + 4591: "l3t-at-an", + 4593: "ipt-anri-anri", + 4594: "ias-session", + 4595: "ias-paging", + 4596: "ias-neighbor", + 4597: "a21-an-1xbs", + 4598: "a16-an-an", + 4599: "a17-an-an", + 4600: "piranha1", + 4601: "piranha2", + 4602: "mtsserver", + 4603: "menandmice-upg", + 4604: "irp", + 4605: "sixchat", + 4658: "playsta2-app", + 4659: "playsta2-lob", + 4660: "smaclmgr", + 4661: "kar2ouche", + 4662: "oms", + 4663: "noteit", + 4664: "ems", + 4665: "contclientms", + 4666: "eportcomm", + 4667: "mmacomm", + 4668: "mmaeds", + 4669: "eportcommdata", + 4670: "light", + 4671: "acter", + 4672: "rfa", + 4673: "cxws", + 4674: "appiq-mgmt", + 4675: "dhct-status", + 4676: "dhct-alerts", + 4677: "bcs", + 4678: "traversal", + 4679: "mgesupervision", + 4680: "mgemanagement", + 4681: "parliant", + 4682: "finisar", + 4683: "spike", + 4684: "rfid-rp1", + 4685: "autopac", + 4686: "msp-os", + 4687: "nst", + 4688: "mobile-p2p", + 4689: "altovacentral", + 4690: "prelude", + 4691: "mtn", + 4692: "conspiracy", + 4700: "netxms-agent", + 4701: "netxms-mgmt", + 4702: "netxms-sync", + 4703: "npqes-test", + 4704: "assuria-ins", + 4711: "trinity-dist", + 4725: "truckstar", + 4727: "fcis", + 4728: "capmux", + 4730: "gearman", + 4731: "remcap", + 4733: "resorcs", + 4737: "ipdr-sp", + 4738: "solera-lpn", + 4739: "ipfix", + 4740: "ipfixs", + 4741: "lumimgrd", + 4742: "sicct", + 4743: "openhpid", + 4744: "ifsp", + 4745: "fmp", + 4749: "profilemac", + 4750: "ssad", + 4751: "spocp", + 4752: "snap", + 4753: "simon", + 4756: "RDCenter", + 4774: "converge", + 4784: "bfd-multi-ctl", + 4786: "smart-install", + 4787: "sia-ctrl-plane", + 4788: "xmcp", + 4800: "iims", + 4801: "iwec", + 4802: "ilss", + 4803: "notateit", + 4827: "htcp", + 4837: "varadero-0", + 4838: "varadero-1", + 4839: "varadero-2", + 4840: "opcua-tcp", + 4841: "quosa", + 4842: "gw-asv", + 4843: "opcua-tls", + 4844: "gw-log", + 4845: "wcr-remlib", + 4846: "contamac-icm", + 4847: "wfc", + 4848: "appserv-http", + 4849: "appserv-https", + 4850: "sun-as-nodeagt", + 4851: "derby-repli", + 4867: "unify-debug", + 4868: "phrelay", + 4869: "phrelaydbg", + 4870: "cc-tracking", + 4871: "wired", + 4876: "tritium-can", + 4877: "lmcs", + 4879: "wsdl-event", + 4880: "hislip", + 4883: "wmlserver", + 4884: "hivestor", + 4885: "abbs", + 4894: "lyskom", + 4899: "radmin-port", + 4900: "hfcs", + 4901: "flr-agent", + 4902: "magiccontrol", + 4912: "lutap", + 4913: "lutcp", + 4914: "bones", + 4915: "frcs", + 4940: "eq-office-4940", + 4941: "eq-office-4941", + 4942: "eq-office-4942", + 4949: "munin", + 4950: "sybasesrvmon", + 4951: "pwgwims", + 4952: "sagxtsds", + 4953: "dbsyncarbiter", + 4969: "ccss-qmm", + 4970: "ccss-qsm", + 4971: "burp", + 4984: "webyast", + 4985: "gerhcs", + 4986: "mrip", + 4987: "smar-se-port1", + 4988: "smar-se-port2", + 4989: "parallel", + 4990: "busycal", + 4991: "vrt", + 4999: "hfcs-manager", + 5000: "commplex-main", + 5001: "commplex-link", + 5002: "rfe", + 5003: "fmpro-internal", + 5004: "avt-profile-1", + 5005: "avt-profile-2", + 5006: "wsm-server", + 5007: "wsm-server-ssl", + 5008: "synapsis-edge", + 5009: "winfs", + 5010: "telelpathstart", + 5011: "telelpathattack", + 5012: "nsp", + 5013: "fmpro-v6", + 5015: "fmwp", + 5020: "zenginkyo-1", + 5021: "zenginkyo-2", + 5022: "mice", + 5023: "htuilsrv", + 5024: "scpi-telnet", + 5025: "scpi-raw", + 5026: "strexec-d", + 5027: "strexec-s", + 5028: "qvr", + 5029: "infobright", + 5030: "surfpass", + 5032: "signacert-agent", + 5033: "jtnetd-server", + 5034: "jtnetd-status", + 5042: "asnaacceler8db", + 5043: "swxadmin", + 5044: "lxi-evntsvc", + 5045: "osp", + 5048: "texai", + 5049: "ivocalize", + 5050: "mmcc", + 5051: "ita-agent", + 5052: "ita-manager", + 5053: "rlm", + 5054: "rlm-admin", + 5055: "unot", + 5056: "intecom-ps1", + 5057: "intecom-ps2", + 5059: "sds", + 5060: "sip", + 5061: "sips", + 5062: "na-localise", + 5063: "csrpc", + 5064: "ca-1", + 5065: "ca-2", + 5066: "stanag-5066", + 5067: "authentx", + 5068: "bitforestsrv", + 5069: "i-net-2000-npr", + 5070: "vtsas", + 5071: "powerschool", + 5072: "ayiya", + 5073: "tag-pm", + 5074: "alesquery", + 5075: "pvaccess", + 5080: "onscreen", + 5081: "sdl-ets", + 5082: "qcp", + 5083: "qfp", + 5084: "llrp", + 5085: "encrypted-llrp", + 5086: "aprigo-cs", + 5087: "biotic", + 5093: "sentinel-lm", + 5094: "hart-ip", + 5099: "sentlm-srv2srv", + 5100: "socalia", + 5101: "talarian-tcp", + 5102: "oms-nonsecure", + 5103: "actifio-c2c", + 5106: "actifioudsagent", + 5107: "actifioreplic", + 5111: "taep-as-svc", + 5112: "pm-cmdsvr", + 5114: "ev-services", + 5115: "autobuild", + 5117: "gradecam", + 5120: "barracuda-bbs", + 5133: "nbt-pc", + 5134: "ppactivation", + 5135: "erp-scale", + 5137: "ctsd", + 5145: "rmonitor-secure", + 5146: "social-alarm", + 5150: "atmp", + 5151: "esri-sde", + 5152: "sde-discovery", + 5153: "toruxserver", + 5154: "bzflag", + 5155: "asctrl-agent", + 5156: "rugameonline", + 5157: "mediat", + 5161: "snmpssh", + 5162: "snmpssh-trap", + 5163: "sbackup", + 5164: "vpa", + 5165: "ife-icorp", + 5166: "winpcs", + 5167: "scte104", + 5168: "scte30", + 5172: "pcoip-mgmt", + 5190: "aol", + 5191: "aol-1", + 5192: "aol-2", + 5193: "aol-3", + 5194: "cpscomm", + 5195: "ampl-lic", + 5196: "ampl-tableproxy", + 5197: "tunstall-lwp", + 5200: "targus-getdata", + 5201: "targus-getdata1", + 5202: "targus-getdata2", + 5203: "targus-getdata3", + 5209: "nomad", + 5215: "noteza", + 5221: "3exmp", + 5222: "xmpp-client", + 5223: "hpvirtgrp", + 5224: "hpvirtctrl", + 5225: "hp-server", + 5226: "hp-status", + 5227: "perfd", + 5228: "hpvroom", + 5229: "jaxflow", + 5230: "jaxflow-data", + 5231: "crusecontrol", + 5232: "csedaemon", + 5233: "enfs", + 5234: "eenet", + 5235: "galaxy-network", + 5236: "padl2sim", + 5237: "mnet-discovery", + 5245: "downtools", + 5248: "caacws", + 5249: "caaclang2", + 5250: "soagateway", + 5251: "caevms", + 5252: "movaz-ssc", + 5253: "kpdp", + 5254: "logcabin", + 5264: "3com-njack-1", + 5265: "3com-njack-2", + 5269: "xmpp-server", + 5270: "cartographerxmp", + 5271: "cuelink", + 5272: "pk", + 5280: "xmpp-bosh", + 5281: "undo-lm", + 5282: "transmit-port", + 5298: "presence", + 5299: "nlg-data", + 5300: "hacl-hb", + 5301: "hacl-gs", + 5302: "hacl-cfg", + 5303: "hacl-probe", + 5304: "hacl-local", + 5305: "hacl-test", + 5306: "sun-mc-grp", + 5307: "sco-aip", + 5308: "cfengine", + 5309: "jprinter", + 5310: "outlaws", + 5312: "permabit-cs", + 5313: "rrdp", + 5314: "opalis-rbt-ipc", + 5315: "hacl-poll", + 5316: "hpbladems", + 5317: "hpdevms", + 5318: "pkix-cmc", + 5320: "bsfserver-zn", + 5321: "bsfsvr-zn-ssl", + 5343: "kfserver", + 5344: "xkotodrcp", + 5349: "stuns", + 5352: "dns-llq", + 5353: "mdns", + 5354: "mdnsresponder", + 5355: "llmnr", + 5356: "ms-smlbiz", + 5357: "wsdapi", + 5358: "wsdapi-s", + 5359: "ms-alerter", + 5360: "ms-sideshow", + 5361: "ms-s-sideshow", + 5362: "serverwsd2", + 5363: "net-projection", + 5397: "stresstester", + 5398: "elektron-admin", + 5399: "securitychase", + 5400: "excerpt", + 5401: "excerpts", + 5402: "mftp", + 5403: "hpoms-ci-lstn", + 5404: "hpoms-dps-lstn", + 5405: "netsupport", + 5406: "systemics-sox", + 5407: "foresyte-clear", + 5408: "foresyte-sec", + 5409: "salient-dtasrv", + 5410: "salient-usrmgr", + 5411: "actnet", + 5412: "continuus", + 5413: "wwiotalk", + 5414: "statusd", + 5415: "ns-server", + 5416: "sns-gateway", + 5417: "sns-agent", + 5418: "mcntp", + 5419: "dj-ice", + 5420: "cylink-c", + 5421: "netsupport2", + 5422: "salient-mux", + 5423: "virtualuser", + 5424: "beyond-remote", + 5425: "br-channel", + 5426: "devbasic", + 5427: "sco-peer-tta", + 5428: "telaconsole", + 5429: "base", + 5430: "radec-corp", + 5431: "park-agent", + 5432: "postgresql", + 5433: "pyrrho", + 5434: "sgi-arrayd", + 5435: "sceanics", + 5443: "spss", + 5445: "smbdirect", + 5450: "tiepie", + 5453: "surebox", + 5454: "apc-5454", + 5455: "apc-5455", + 5456: "apc-5456", + 5461: "silkmeter", + 5462: "ttl-publisher", + 5463: "ttlpriceproxy", + 5464: "quailnet", + 5465: "netops-broker", + 5470: "apsolab-col", + 5471: "apsolab-cols", + 5472: "apsolab-tag", + 5473: "apsolab-tags", + 5475: "apsolab-data", + 5500: "fcp-addr-srvr1", + 5501: "fcp-addr-srvr2", + 5502: "fcp-srvr-inst1", + 5503: "fcp-srvr-inst2", + 5504: "fcp-cics-gw1", + 5505: "checkoutdb", + 5506: "amc", + 5507: "psl-management", + 5550: "cbus", + 5553: "sgi-eventmond", + 5554: "sgi-esphttp", + 5555: "personal-agent", + 5556: "freeciv", + 5557: "farenet", + 5565: "hpe-dp-bura", + 5566: "westec-connect", + 5567: "dof-dps-mc-sec", + 5568: "sdt", + 5569: "rdmnet-ctrl", + 5573: "sdmmp", + 5574: "lsi-bobcat", + 5575: "ora-oap", + 5579: "fdtracks", + 5580: "tmosms0", + 5581: "tmosms1", + 5582: "fac-restore", + 5583: "tmo-icon-sync", + 5584: "bis-web", + 5585: "bis-sync", + 5586: "att-mt-sms", + 5597: "ininmessaging", + 5598: "mctfeed", + 5599: "esinstall", + 5600: "esmmanager", + 5601: "esmagent", + 5602: "a1-msc", + 5603: "a1-bs", + 5604: "a3-sdunode", + 5605: "a4-sdunode", + 5618: "efr", + 5627: "ninaf", + 5628: "htrust", + 5629: "symantec-sfdb", + 5630: "precise-comm", + 5631: "pcanywheredata", + 5632: "pcanywherestat", + 5633: "beorl", + 5634: "xprtld", + 5635: "sfmsso", + 5636: "sfm-db-server", + 5637: "cssc", + 5638: "flcrs", + 5639: "ics", + 5646: "vfmobile", + 5666: "nrpe", + 5670: "filemq", + 5671: "amqps", + 5672: "amqp", + 5673: "jms", + 5674: "hyperscsi-port", + 5675: "v5ua", + 5676: "raadmin", + 5677: "questdb2-lnchr", + 5678: "rrac", + 5679: "dccm", + 5680: "auriga-router", + 5681: "ncxcp", + 5688: "ggz", + 5689: "qmvideo", + 5693: "rbsystem", + 5696: "kmip", + 5700: "supportassist", + 5705: "storageos", + 5713: "proshareaudio", + 5714: "prosharevideo", + 5715: "prosharedata", + 5716: "prosharerequest", + 5717: "prosharenotify", + 5718: "dpm", + 5719: "dpm-agent", + 5720: "ms-licensing", + 5721: "dtpt", + 5722: "msdfsr", + 5723: "omhs", + 5724: "omsdk", + 5725: "ms-ilm", + 5726: "ms-ilm-sts", + 5727: "asgenf", + 5728: "io-dist-data", + 5729: "openmail", + 5730: "unieng", + 5741: "ida-discover1", + 5742: "ida-discover2", + 5743: "watchdoc-pod", + 5744: "watchdoc", + 5745: "fcopy-server", + 5746: "fcopys-server", + 5747: "tunatic", + 5748: "tunalyzer", + 5750: "rscd", + 5755: "openmailg", + 5757: "x500ms", + 5766: "openmailns", + 5767: "s-openmail", + 5768: "openmailpxy", + 5769: "spramsca", + 5770: "spramsd", + 5771: "netagent", + 5777: "dali-port", + 5780: "vts-rpc", + 5781: "3par-evts", + 5782: "3par-mgmt", + 5783: "3par-mgmt-ssl", + 5785: "3par-rcopy", + 5793: "xtreamx", + 5813: "icmpd", + 5814: "spt-automation", + 5841: "shiprush-d-ch", + 5842: "reversion", + 5859: "wherehoo", + 5863: "ppsuitemsg", + 5868: "diameters", + 5883: "jute", + 5900: "rfb", + 5910: "cm", + 5911: "cpdlc", + 5912: "fis", + 5913: "ads-c", + 5963: "indy", + 5968: "mppolicy-v5", + 5969: "mppolicy-mgr", + 5984: "couchdb", + 5985: "wsman", + 5986: "wsmans", + 5987: "wbem-rmi", + 5988: "wbem-http", + 5989: "wbem-https", + 5990: "wbem-exp-https", + 5991: "nuxsl", + 5992: "consul-insight", + 5993: "cim-rs", + 5999: "cvsup", + 6064: "ndl-ahp-svc", + 6065: "winpharaoh", + 6066: "ewctsp", + 6068: "gsmp-ancp", + 6069: "trip", + 6070: "messageasap", + 6071: "ssdtp", + 6072: "diagnose-proc", + 6073: "directplay8", + 6074: "max", + 6075: "dpm-acm", + 6076: "msft-dpm-cert", + 6077: "iconstructsrv", + 6084: "reload-config", + 6085: "konspire2b", + 6086: "pdtp", + 6087: "ldss", + 6088: "doglms", + 6099: "raxa-mgmt", + 6100: "synchronet-db", + 6101: "synchronet-rtc", + 6102: "synchronet-upd", + 6103: "rets", + 6104: "dbdb", + 6105: "primaserver", + 6106: "mpsserver", + 6107: "etc-control", + 6108: "sercomm-scadmin", + 6109: "globecast-id", + 6110: "softcm", + 6111: "spc", + 6112: "dtspcd", + 6113: "dayliteserver", + 6114: "wrspice", + 6115: "xic", + 6116: "xtlserv", + 6117: "daylitetouch", + 6121: "spdy", + 6122: "bex-webadmin", + 6123: "backup-express", + 6124: "pnbs", + 6130: "damewaremobgtwy", + 6133: "nbt-wol", + 6140: "pulsonixnls", + 6141: "meta-corp", + 6142: "aspentec-lm", + 6143: "watershed-lm", + 6144: "statsci1-lm", + 6145: "statsci2-lm", + 6146: "lonewolf-lm", + 6147: "montage-lm", + 6148: "ricardo-lm", + 6149: "tal-pod", + 6159: "efb-aci", + 6160: "ecmp", + 6161: "patrol-ism", + 6162: "patrol-coll", + 6163: "pscribe", + 6200: "lm-x", + 6209: "qmtps", + 6222: "radmind", + 6241: "jeol-nsdtp-1", + 6242: "jeol-nsdtp-2", + 6243: "jeol-nsdtp-3", + 6244: "jeol-nsdtp-4", + 6251: "tl1-raw-ssl", + 6252: "tl1-ssh", + 6253: "crip", + 6267: "gld", + 6268: "grid", + 6269: "grid-alt", + 6300: "bmc-grx", + 6301: "bmc-ctd-ldap", + 6306: "ufmp", + 6315: "scup", + 6316: "abb-escp", + 6317: "nav-data-cmd", + 6320: "repsvc", + 6321: "emp-server1", + 6322: "emp-server2", + 6324: "hrd-ncs", + 6325: "dt-mgmtsvc", + 6326: "dt-vra", + 6343: "sflow", + 6344: "streletz", + 6346: "gnutella-svc", + 6347: "gnutella-rtr", + 6350: "adap", + 6355: "pmcs", + 6360: "metaedit-mu", + 6370: "metaedit-se", + 6379: "redis", + 6382: "metatude-mds", + 6389: "clariion-evr01", + 6390: "metaedit-ws", + 6417: "faxcomservice", + 6418: "syserverremote", + 6419: "svdrp", + 6420: "nim-vdrshell", + 6421: "nim-wan", + 6432: "pgbouncer", + 6442: "tarp", + 6443: "sun-sr-https", + 6444: "sge-qmaster", + 6445: "sge-execd", + 6446: "mysql-proxy", + 6455: "skip-cert-recv", + 6456: "skip-cert-send", + 6464: "ieee11073-20701", + 6471: "lvision-lm", + 6480: "sun-sr-http", + 6481: "servicetags", + 6482: "ldoms-mgmt", + 6483: "SunVTS-RMI", + 6484: "sun-sr-jms", + 6485: "sun-sr-iiop", + 6486: "sun-sr-iiops", + 6487: "sun-sr-iiop-aut", + 6488: "sun-sr-jmx", + 6489: "sun-sr-admin", + 6500: "boks", + 6501: "boks-servc", + 6502: "boks-servm", + 6503: "boks-clntd", + 6505: "badm-priv", + 6506: "badm-pub", + 6507: "bdir-priv", + 6508: "bdir-pub", + 6509: "mgcs-mfp-port", + 6510: "mcer-port", + 6513: "netconf-tls", + 6514: "syslog-tls", + 6515: "elipse-rec", + 6543: "lds-distrib", + 6544: "lds-dump", + 6547: "apc-6547", + 6548: "apc-6548", + 6549: "apc-6549", + 6550: "fg-sysupdate", + 6551: "sum", + 6558: "xdsxdm", + 6566: "sane-port", + 6568: "canit-store", + 6579: "affiliate", + 6580: "parsec-master", + 6581: "parsec-peer", + 6582: "parsec-game", + 6583: "joaJewelSuite", + 6600: "mshvlm", + 6601: "mstmg-sstp", + 6602: "wsscomfrmwk", + 6619: "odette-ftps", + 6620: "kftp-data", + 6621: "kftp", + 6622: "mcftp", + 6623: "ktelnet", + 6624: "datascaler-db", + 6625: "datascaler-ctl", + 6626: "wago-service", + 6627: "nexgen", + 6628: "afesc-mc", + 6629: "nexgen-aux", + 6632: "mxodbc-connect", + 6640: "ovsdb", + 6653: "openflow", + 6655: "pcs-sf-ui-man", + 6656: "emgmsg", + 6670: "vocaltec-gold", + 6671: "p4p-portal", + 6672: "vision-server", + 6673: "vision-elmd", + 6678: "vfbp", + 6679: "osaut", + 6687: "clever-ctrace", + 6688: "clever-tcpip", + 6689: "tsa", + 6690: "cleverdetect", + 6697: "ircs-u", + 6701: "kti-icad-srvr", + 6702: "e-design-net", + 6703: "e-design-web", + 6714: "ibprotocol", + 6715: "fibotrader-com", + 6716: "princity-agent", + 6767: "bmc-perf-agent", + 6768: "bmc-perf-mgrd", + 6769: "adi-gxp-srvprt", + 6770: "plysrv-http", + 6771: "plysrv-https", + 6777: "ntz-tracker", + 6778: "ntz-p2p-storage", + 6785: "dgpf-exchg", + 6786: "smc-jmx", + 6787: "smc-admin", + 6788: "smc-http", + 6789: "radg", + 6790: "hnmp", + 6791: "hnm", + 6801: "acnet", + 6817: "pentbox-sim", + 6831: "ambit-lm", + 6841: "netmo-default", + 6842: "netmo-http", + 6850: "iccrushmore", + 6868: "acctopus-cc", + 6888: "muse", + 6900: "rtimeviewer", + 6901: "jetstream", + 6935: "ethoscan", + 6936: "xsmsvc", + 6946: "bioserver", + 6951: "otlp", + 6961: "jmact3", + 6962: "jmevt2", + 6963: "swismgr1", + 6964: "swismgr2", + 6965: "swistrap", + 6966: "swispol", + 6969: "acmsoda", + 6970: "conductor", + 6997: "MobilitySrv", + 6998: "iatp-highpri", + 6999: "iatp-normalpri", + 7000: "afs3-fileserver", + 7001: "afs3-callback", + 7002: "afs3-prserver", + 7003: "afs3-vlserver", + 7004: "afs3-kaserver", + 7005: "afs3-volser", + 7006: "afs3-errors", + 7007: "afs3-bos", + 7008: "afs3-update", + 7009: "afs3-rmtsys", + 7010: "ups-onlinet", + 7011: "talon-disc", + 7012: "talon-engine", + 7013: "microtalon-dis", + 7014: "microtalon-com", + 7015: "talon-webserver", + 7016: "spg", + 7017: "grasp", + 7018: "fisa-svc", + 7019: "doceri-ctl", + 7020: "dpserve", + 7021: "dpserveadmin", + 7022: "ctdp", + 7023: "ct2nmcs", + 7024: "vmsvc", + 7025: "vmsvc-2", + 7030: "op-probe", + 7031: "iposplanet", + 7070: "arcp", + 7071: "iwg1", + 7073: "martalk", + 7080: "empowerid", + 7099: "lazy-ptop", + 7100: "font-service", + 7101: "elcn", + 7117: "rothaga", + 7121: "virprot-lm", + 7128: "scenidm", + 7129: "scenccs", + 7161: "cabsm-comm", + 7162: "caistoragemgr", + 7163: "cacsambroker", + 7164: "fsr", + 7165: "doc-server", + 7166: "aruba-server", + 7167: "casrmagent", + 7168: "cnckadserver", + 7169: "ccag-pib", + 7170: "nsrp", + 7171: "drm-production", + 7172: "metalbend", + 7173: "zsecure", + 7174: "clutild", + 7200: "fodms", + 7201: "dlip", + 7202: "pon-ictp", + 7215: "PS-Server", + 7216: "PS-Capture-Pro", + 7227: "ramp", + 7228: "citrixupp", + 7229: "citrixuppg", + 7236: "display", + 7237: "pads", + 7244: "frc-hicp", + 7262: "cnap", + 7272: "watchme-7272", + 7273: "oma-rlp", + 7274: "oma-rlp-s", + 7275: "oma-ulp", + 7276: "oma-ilp", + 7277: "oma-ilp-s", + 7278: "oma-dcdocbs", + 7279: "ctxlic", + 7280: "itactionserver1", + 7281: "itactionserver2", + 7282: "mzca-action", + 7283: "genstat", + 7365: "lcm-server", + 7391: "mindfilesys", + 7392: "mrssrendezvous", + 7393: "nfoldman", + 7394: "fse", + 7395: "winqedit", + 7397: "hexarc", + 7400: "rtps-discovery", + 7401: "rtps-dd-ut", + 7402: "rtps-dd-mt", + 7410: "ionixnetmon", + 7411: "daqstream", + 7421: "mtportmon", + 7426: "pmdmgr", + 7427: "oveadmgr", + 7428: "ovladmgr", + 7429: "opi-sock", + 7430: "xmpv7", + 7431: "pmd", + 7437: "faximum", + 7443: "oracleas-https", + 7471: "sttunnel", + 7473: "rise", + 7474: "neo4j", + 7478: "openit", + 7491: "telops-lmd", + 7500: "silhouette", + 7501: "ovbus", + 7508: "adcp", + 7509: "acplt", + 7510: "ovhpas", + 7511: "pafec-lm", + 7542: "saratoga", + 7543: "atul", + 7544: "nta-ds", + 7545: "nta-us", + 7546: "cfs", + 7547: "cwmp", + 7548: "tidp", + 7549: "nls-tl", + 7551: "controlone-con", + 7560: "sncp", + 7563: "cfw", + 7566: "vsi-omega", + 7569: "dell-eql-asm", + 7570: "aries-kfinder", + 7574: "coherence", + 7588: "sun-lm", + 7606: "mipi-debug", + 7624: "indi", + 7626: "simco", + 7627: "soap-http", + 7628: "zen-pawn", + 7629: "xdas", + 7630: "hawk", + 7631: "tesla-sys-msg", + 7633: "pmdfmgt", + 7648: "cuseeme", + 7672: "imqstomp", + 7673: "imqstomps", + 7674: "imqtunnels", + 7675: "imqtunnel", + 7676: "imqbrokerd", + 7677: "sun-user-https", + 7680: "pando-pub", + 7683: "dmt", + 7687: "bolt", + 7689: "collaber", + 7697: "klio", + 7700: "em7-secom", + 7707: "sync-em7", + 7708: "scinet", + 7720: "medimageportal", + 7724: "nsdeepfreezectl", + 7725: "nitrogen", + 7726: "freezexservice", + 7727: "trident-data", + 7728: "osvr", + 7734: "smip", + 7738: "aiagent", + 7741: "scriptview", + 7742: "msss", + 7743: "sstp-1", + 7744: "raqmon-pdu", + 7747: "prgp", + 7775: "inetfs", + 7777: "cbt", + 7778: "interwise", + 7779: "vstat", + 7781: "accu-lmgr", + 7786: "minivend", + 7787: "popup-reminders", + 7789: "office-tools", + 7794: "q3ade", + 7797: "pnet-conn", + 7798: "pnet-enc", + 7799: "altbsdp", + 7800: "asr", + 7801: "ssp-client", + 7810: "rbt-wanopt", + 7845: "apc-7845", + 7846: "apc-7846", + 7847: "csoauth", + 7869: "mobileanalyzer", + 7870: "rbt-smc", + 7871: "mdm", + 7878: "owms", + 7880: "pss", + 7887: "ubroker", + 7900: "mevent", + 7901: "tnos-sp", + 7902: "tnos-dp", + 7903: "tnos-dps", + 7913: "qo-secure", + 7932: "t2-drm", + 7933: "t2-brm", + 7962: "generalsync", + 7967: "supercell", + 7979: "micromuse-ncps", + 7980: "quest-vista", + 7981: "sossd-collect", + 7982: "sossd-agent", + 7997: "pushns", + 7999: "irdmi2", + 8000: "irdmi", + 8001: "vcom-tunnel", + 8002: "teradataordbms", + 8003: "mcreport", + 8005: "mxi", + 8006: "wpl-analytics", + 8007: "warppipe", + 8008: "http-alt", + 8019: "qbdb", + 8020: "intu-ec-svcdisc", + 8021: "intu-ec-client", + 8022: "oa-system", + 8025: "ca-audit-da", + 8026: "ca-audit-ds", + 8032: "pro-ed", + 8033: "mindprint", + 8034: "vantronix-mgmt", + 8040: "ampify", + 8041: "enguity-xccetp", + 8042: "fs-agent", + 8043: "fs-server", + 8044: "fs-mgmt", + 8051: "rocrail", + 8052: "senomix01", + 8053: "senomix02", + 8054: "senomix03", + 8055: "senomix04", + 8056: "senomix05", + 8057: "senomix06", + 8058: "senomix07", + 8059: "senomix08", + 8066: "toad-bi-appsrvr", + 8067: "infi-async", + 8070: "ucs-isc", + 8074: "gadugadu", + 8077: "mles", + 8080: "http-alt", + 8081: "sunproxyadmin", + 8082: "us-cli", + 8083: "us-srv", + 8086: "d-s-n", + 8087: "simplifymedia", + 8088: "radan-http", + 8090: "opsmessaging", + 8091: "jamlink", + 8097: "sac", + 8100: "xprint-server", + 8101: "ldoms-migr", + 8102: "kz-migr", + 8115: "mtl8000-matrix", + 8116: "cp-cluster", + 8117: "purityrpc", + 8118: "privoxy", + 8121: "apollo-data", + 8122: "apollo-admin", + 8128: "paycash-online", + 8129: "paycash-wbp", + 8130: "indigo-vrmi", + 8131: "indigo-vbcp", + 8132: "dbabble", + 8140: "puppet", + 8148: "isdd", + 8153: "quantastor", + 8160: "patrol", + 8161: "patrol-snmp", + 8162: "lpar2rrd", + 8181: "intermapper", + 8182: "vmware-fdm", + 8183: "proremote", + 8184: "itach", + 8190: "gcp-rphy", + 8191: "limnerpressure", + 8192: "spytechphone", + 8194: "blp1", + 8195: "blp2", + 8199: "vvr-data", + 8200: "trivnet1", + 8201: "trivnet2", + 8204: "lm-perfworks", + 8205: "lm-instmgr", + 8206: "lm-dta", + 8207: "lm-sserver", + 8208: "lm-webwatcher", + 8230: "rexecj", + 8243: "synapse-nhttps", + 8270: "robot-remote", + 8276: "pando-sec", + 8280: "synapse-nhttp", + 8282: "libelle", + 8292: "blp3", + 8293: "hiperscan-id", + 8294: "blp4", + 8300: "tmi", + 8301: "amberon", + 8313: "hub-open-net", + 8320: "tnp-discover", + 8321: "tnp", + 8322: "garmin-marine", + 8351: "server-find", + 8376: "cruise-enum", + 8377: "cruise-swroute", + 8378: "cruise-config", + 8379: "cruise-diags", + 8380: "cruise-update", + 8383: "m2mservices", + 8400: "cvd", + 8401: "sabarsd", + 8402: "abarsd", + 8403: "admind", + 8404: "svcloud", + 8405: "svbackup", + 8415: "dlpx-sp", + 8416: "espeech", + 8417: "espeech-rtp", + 8423: "aritts", + 8442: "cybro-a-bus", + 8443: "pcsync-https", + 8444: "pcsync-http", + 8445: "copy", + 8450: "npmp", + 8457: "nexentamv", + 8470: "cisco-avp", + 8471: "pim-port", + 8472: "otv", + 8473: "vp2p", + 8474: "noteshare", + 8500: "fmtp", + 8501: "cmtp-mgt", + 8502: "ftnmtp", + 8554: "rtsp-alt", + 8555: "d-fence", + 8567: "dof-tunnel", + 8600: "asterix", + 8610: "canon-mfnp", + 8611: "canon-bjnp1", + 8612: "canon-bjnp2", + 8613: "canon-bjnp3", + 8614: "canon-bjnp4", + 8615: "imink", + 8665: "monetra", + 8666: "monetra-admin", + 8675: "msi-cps-rm", + 8686: "sun-as-jmxrmi", + 8688: "openremote-ctrl", + 8699: "vnyx", + 8711: "nvc", + 8733: "ibus", + 8750: "dey-keyneg", + 8763: "mc-appserver", + 8764: "openqueue", + 8765: "ultraseek-http", + 8766: "amcs", + 8770: "dpap", + 8778: "uec", + 8786: "msgclnt", + 8787: "msgsrvr", + 8793: "acd-pm", + 8800: "sunwebadmin", + 8804: "truecm", + 8873: "dxspider", + 8880: "cddbp-alt", + 8881: "galaxy4d", + 8883: "secure-mqtt", + 8888: "ddi-tcp-1", + 8889: "ddi-tcp-2", + 8890: "ddi-tcp-3", + 8891: "ddi-tcp-4", + 8892: "ddi-tcp-5", + 8893: "ddi-tcp-6", + 8894: "ddi-tcp-7", + 8899: "ospf-lite", + 8900: "jmb-cds1", + 8901: "jmb-cds2", + 8910: "manyone-http", + 8911: "manyone-xml", + 8912: "wcbackup", + 8913: "dragonfly", + 8937: "twds", + 8953: "ub-dns-control", + 8954: "cumulus-admin", + 8980: "nod-provider", + 8989: "sunwebadmins", + 8990: "http-wmap", + 8991: "https-wmap", + 8997: "oracle-ms-ens", + 8998: "canto-roboflow", + 8999: "bctp", + 9000: "cslistener", + 9001: "etlservicemgr", + 9002: "dynamid", + 9005: "golem", + 9008: "ogs-server", + 9009: "pichat", + 9010: "sdr", + 9020: "tambora", + 9021: "panagolin-ident", + 9022: "paragent", + 9023: "swa-1", + 9024: "swa-2", + 9025: "swa-3", + 9026: "swa-4", + 9050: "versiera", + 9051: "fio-cmgmt", + 9060: "CardWeb-IO", + 9080: "glrpc", + 9083: "emc-pp-mgmtsvc", + 9084: "aurora", + 9085: "ibm-rsyscon", + 9086: "net2display", + 9087: "classic", + 9088: "sqlexec", + 9089: "sqlexec-ssl", + 9090: "websm", + 9091: "xmltec-xmlmail", + 9092: "XmlIpcRegSvc", + 9093: "copycat", + 9100: "hp-pdl-datastr", + 9101: "bacula-dir", + 9102: "bacula-fd", + 9103: "bacula-sd", + 9104: "peerwire", + 9105: "xadmin", + 9106: "astergate", + 9107: "astergatefax", + 9119: "mxit", + 9122: "grcmp", + 9123: "grcp", + 9131: "dddp", + 9160: "apani1", + 9161: "apani2", + 9162: "apani3", + 9163: "apani4", + 9164: "apani5", + 9191: "sun-as-jpda", + 9200: "wap-wsp", + 9201: "wap-wsp-wtp", + 9202: "wap-wsp-s", + 9203: "wap-wsp-wtp-s", + 9204: "wap-vcard", + 9205: "wap-vcal", + 9206: "wap-vcard-s", + 9207: "wap-vcal-s", + 9208: "rjcdb-vcards", + 9209: "almobile-system", + 9210: "oma-mlp", + 9211: "oma-mlp-s", + 9212: "serverviewdbms", + 9213: "serverstart", + 9214: "ipdcesgbs", + 9215: "insis", + 9216: "acme", + 9217: "fsc-port", + 9222: "teamcoherence", + 9255: "mon", + 9278: "pegasus", + 9279: "pegasus-ctl", + 9280: "pgps", + 9281: "swtp-port1", + 9282: "swtp-port2", + 9283: "callwaveiam", + 9284: "visd", + 9285: "n2h2server", + 9287: "cumulus", + 9292: "armtechdaemon", + 9293: "storview", + 9294: "armcenterhttp", + 9295: "armcenterhttps", + 9300: "vrace", + 9306: "sphinxql", + 9312: "sphinxapi", + 9318: "secure-ts", + 9321: "guibase", + 9343: "mpidcmgr", + 9344: "mphlpdmc", + 9345: "rancher", + 9346: "ctechlicensing", + 9374: "fjdmimgr", + 9380: "boxp", + 9387: "d2dconfig", + 9388: "d2ddatatrans", + 9389: "adws", + 9390: "otp", + 9396: "fjinvmgr", + 9397: "mpidcagt", + 9400: "sec-t4net-srv", + 9401: "sec-t4net-clt", + 9402: "sec-pc2fax-srv", + 9418: "git", + 9443: "tungsten-https", + 9444: "wso2esb-console", + 9445: "mindarray-ca", + 9450: "sntlkeyssrvr", + 9500: "ismserver", + 9535: "mngsuite", + 9536: "laes-bf", + 9555: "trispen-sra", + 9592: "ldgateway", + 9593: "cba8", + 9594: "msgsys", + 9595: "pds", + 9596: "mercury-disc", + 9597: "pd-admin", + 9598: "vscp", + 9599: "robix", + 9600: "micromuse-ncpw", + 9612: "streamcomm-ds", + 9614: "iadt-tls", + 9616: "erunbook-agent", + 9617: "erunbook-server", + 9618: "condor", + 9628: "odbcpathway", + 9629: "uniport", + 9630: "peoctlr", + 9631: "peocoll", + 9640: "pqsflows", + 9666: "zoomcp", + 9667: "xmms2", + 9668: "tec5-sdctp", + 9694: "client-wakeup", + 9695: "ccnx", + 9700: "board-roar", + 9747: "l5nas-parchan", + 9750: "board-voip", + 9753: "rasadv", + 9762: "tungsten-http", + 9800: "davsrc", + 9801: "sstp-2", + 9802: "davsrcs", + 9875: "sapv1", + 9876: "sd", + 9888: "cyborg-systems", + 9889: "gt-proxy", + 9898: "monkeycom", + 9900: "iua", + 9909: "domaintime", + 9911: "sype-transport", + 9925: "xybrid-cloud", + 9950: "apc-9950", + 9951: "apc-9951", + 9952: "apc-9952", + 9953: "acis", + 9954: "hinp", + 9955: "alljoyn-stm", + 9966: "odnsp", + 9978: "xybrid-rt", + 9979: "visweather", + 9981: "pumpkindb", + 9987: "dsm-scm-target", + 9988: "nsesrvr", + 9990: "osm-appsrvr", + 9991: "osm-oev", + 9992: "palace-1", + 9993: "palace-2", + 9994: "palace-3", + 9995: "palace-4", + 9996: "palace-5", + 9997: "palace-6", + 9998: "distinct32", + 9999: "distinct", + 10000: "ndmp", + 10001: "scp-config", + 10002: "documentum", + 10003: "documentum-s", + 10004: "emcrmirccd", + 10005: "emcrmird", + 10006: "netapp-sync", + 10007: "mvs-capacity", + 10008: "octopus", + 10009: "swdtp-sv", + 10010: "rxapi", + 10020: "abb-hw", + 10050: "zabbix-agent", + 10051: "zabbix-trapper", + 10055: "qptlmd", + 10080: "amanda", + 10081: "famdc", + 10100: "itap-ddtp", + 10101: "ezmeeting-2", + 10102: "ezproxy-2", + 10103: "ezrelay", + 10104: "swdtp", + 10107: "bctp-server", + 10110: "nmea-0183", + 10113: "netiq-endpoint", + 10114: "netiq-qcheck", + 10115: "netiq-endpt", + 10116: "netiq-voipa", + 10117: "iqrm", + 10125: "cimple", + 10128: "bmc-perf-sd", + 10129: "bmc-gms", + 10160: "qb-db-server", + 10161: "snmptls", + 10162: "snmptls-trap", + 10200: "trisoap", + 10201: "rsms", + 10252: "apollo-relay", + 10260: "axis-wimp-port", + 10261: "tile-ml", + 10288: "blocks", + 10321: "cosir", + 10540: "MOS-lower", + 10541: "MOS-upper", + 10542: "MOS-aux", + 10543: "MOS-soap", + 10544: "MOS-soap-opt", + 10548: "serverdocs", + 10631: "printopia", + 10800: "gap", + 10805: "lpdg", + 10809: "nbd", + 10860: "helix", + 10880: "bveapi", + 10933: "octopustentacle", + 10990: "rmiaux", + 11000: "irisa", + 11001: "metasys", + 11095: "weave", + 11103: "origo-sync", + 11104: "netapp-icmgmt", + 11105: "netapp-icdata", + 11106: "sgi-lk", + 11109: "sgi-dmfmgr", + 11110: "sgi-soap", + 11111: "vce", + 11112: "dicom", + 11161: "suncacao-snmp", + 11162: "suncacao-jmxmp", + 11163: "suncacao-rmi", + 11164: "suncacao-csa", + 11165: "suncacao-websvc", + 11172: "oemcacao-jmxmp", + 11173: "t5-straton", + 11174: "oemcacao-rmi", + 11175: "oemcacao-websvc", + 11201: "smsqp", + 11202: "dcsl-backup", + 11208: "wifree", + 11211: "memcache", + 11319: "imip", + 11320: "imip-channels", + 11321: "arena-server", + 11367: "atm-uhas", + 11371: "hkp", + 11489: "asgcypresstcps", + 11600: "tempest-port", + 11623: "emc-xsw-dconfig", + 11720: "h323callsigalt", + 11723: "emc-xsw-dcache", + 11751: "intrepid-ssl", + 11796: "lanschool", + 11876: "xoraya", + 11967: "sysinfo-sp", + 12000: "entextxid", + 12001: "entextnetwk", + 12002: "entexthigh", + 12003: "entextmed", + 12004: "entextlow", + 12005: "dbisamserver1", + 12006: "dbisamserver2", + 12007: "accuracer", + 12008: "accuracer-dbms", + 12010: "edbsrvr", + 12012: "vipera", + 12013: "vipera-ssl", + 12109: "rets-ssl", + 12121: "nupaper-ss", + 12168: "cawas", + 12172: "hivep", + 12300: "linogridengine", + 12302: "rads", + 12321: "warehouse-sss", + 12322: "warehouse", + 12345: "italk", + 12753: "tsaf", + 12865: "netperf", + 13160: "i-zipqd", + 13216: "bcslogc", + 13217: "rs-pias", + 13218: "emc-vcas-tcp", + 13223: "powwow-client", + 13224: "powwow-server", + 13400: "doip-data", + 13720: "bprd", + 13721: "bpdbm", + 13722: "bpjava-msvc", + 13724: "vnetd", + 13782: "bpcd", + 13783: "vopied", + 13785: "nbdb", + 13786: "nomdb", + 13818: "dsmcc-config", + 13819: "dsmcc-session", + 13820: "dsmcc-passthru", + 13821: "dsmcc-download", + 13822: "dsmcc-ccp", + 13823: "bmdss", + 13894: "ucontrol", + 13929: "dta-systems", + 13930: "medevolve", + 14000: "scotty-ft", + 14001: "sua", + 14033: "sage-best-com1", + 14034: "sage-best-com2", + 14141: "vcs-app", + 14142: "icpp", + 14143: "icpps", + 14145: "gcm-app", + 14149: "vrts-tdd", + 14150: "vcscmd", + 14154: "vad", + 14250: "cps", + 14414: "ca-web-update", + 14500: "xpra", + 14936: "hde-lcesrvr-1", + 14937: "hde-lcesrvr-2", + 15000: "hydap", + 15002: "onep-tls", + 15345: "xpilot", + 15363: "3link", + 15555: "cisco-snat", + 15660: "bex-xr", + 15740: "ptp", + 15999: "programmar", + 16000: "fmsas", + 16001: "fmsascon", + 16002: "gsms", + 16020: "jwpc", + 16021: "jwpc-bin", + 16161: "sun-sea-port", + 16162: "solaris-audit", + 16309: "etb4j", + 16310: "pduncs", + 16311: "pdefmns", + 16360: "netserialext1", + 16361: "netserialext2", + 16367: "netserialext3", + 16368: "netserialext4", + 16384: "connected", + 16385: "rdgs", + 16619: "xoms", + 16665: "axon-tunnel", + 16789: "cadsisvr", + 16900: "newbay-snc-mc", + 16950: "sgcip", + 16991: "intel-rci-mp", + 16992: "amt-soap-http", + 16993: "amt-soap-https", + 16994: "amt-redir-tcp", + 16995: "amt-redir-tls", + 17007: "isode-dua", + 17184: "vestasdlp", + 17185: "soundsvirtual", + 17219: "chipper", + 17220: "avtp", + 17221: "avdecc", + 17223: "isa100-gci", + 17225: "trdp-md", + 17234: "integrius-stp", + 17235: "ssh-mgmt", + 17500: "db-lsp", + 17555: "ailith", + 17729: "ea", + 17754: "zep", + 17755: "zigbee-ip", + 17756: "zigbee-ips", + 17777: "sw-orion", + 18000: "biimenu", + 18104: "radpdf", + 18136: "racf", + 18181: "opsec-cvp", + 18182: "opsec-ufp", + 18183: "opsec-sam", + 18184: "opsec-lea", + 18185: "opsec-omi", + 18186: "ohsc", + 18187: "opsec-ela", + 18241: "checkpoint-rtm", + 18242: "iclid", + 18243: "clusterxl", + 18262: "gv-pf", + 18463: "ac-cluster", + 18634: "rds-ib", + 18635: "rds-ip", + 18668: "vdmmesh", + 18769: "ique", + 18881: "infotos", + 18888: "apc-necmp", + 19000: "igrid", + 19007: "scintilla", + 19020: "j-link", + 19191: "opsec-uaa", + 19194: "ua-secureagent", + 19220: "cora", + 19283: "keysrvr", + 19315: "keyshadow", + 19398: "mtrgtrans", + 19410: "hp-sco", + 19411: "hp-sca", + 19412: "hp-sessmon", + 19539: "fxuptp", + 19540: "sxuptp", + 19541: "jcp", + 19998: "iec-104-sec", + 19999: "dnp-sec", + 20000: "dnp", + 20001: "microsan", + 20002: "commtact-http", + 20003: "commtact-https", + 20005: "openwebnet", + 20013: "ss-idi", + 20014: "opendeploy", + 20034: "nburn-id", + 20046: "tmophl7mts", + 20048: "mountd", + 20049: "nfsrdma", + 20057: "avesterra", + 20167: "tolfab", + 20202: "ipdtp-port", + 20222: "ipulse-ics", + 20480: "emwavemsg", + 20670: "track", + 20999: "athand-mmp", + 21000: "irtrans", + 21010: "notezilla-lan", + 21221: "aigairserver", + 21553: "rdm-tfs", + 21554: "dfserver", + 21590: "vofr-gateway", + 21800: "tvpm", + 21845: "webphone", + 21846: "netspeak-is", + 21847: "netspeak-cs", + 21848: "netspeak-acd", + 21849: "netspeak-cps", + 22000: "snapenetio", + 22001: "optocontrol", + 22002: "optohost002", + 22003: "optohost003", + 22004: "optohost004", + 22005: "optohost004", + 22125: "dcap", + 22128: "gsidcap", + 22222: "easyengine", + 22273: "wnn6", + 22305: "cis", + 22335: "shrewd-control", + 22343: "cis-secure", + 22347: "wibukey", + 22350: "codemeter", + 22351: "codemeter-cmwan", + 22537: "caldsoft-backup", + 22555: "vocaltec-wconf", + 22763: "talikaserver", + 22800: "aws-brf", + 22951: "brf-gw", + 23000: "inovaport1", + 23001: "inovaport2", + 23002: "inovaport3", + 23003: "inovaport4", + 23004: "inovaport5", + 23005: "inovaport6", + 23053: "gntp", + 23294: "5afe-dir", + 23333: "elxmgmt", + 23400: "novar-dbase", + 23401: "novar-alarm", + 23402: "novar-global", + 23456: "aequus", + 23457: "aequus-alt", + 23546: "areaguard-neo", + 24000: "med-ltp", + 24001: "med-fsp-rx", + 24002: "med-fsp-tx", + 24003: "med-supp", + 24004: "med-ovw", + 24005: "med-ci", + 24006: "med-net-svc", + 24242: "filesphere", + 24249: "vista-4gl", + 24321: "ild", + 24386: "intel-rci", + 24465: "tonidods", + 24554: "binkp", + 24577: "bilobit", + 24666: "sdtvwcam", + 24676: "canditv", + 24677: "flashfiler", + 24678: "proactivate", + 24680: "tcc-http", + 24754: "cslg", + 24922: "find", + 25000: "icl-twobase1", + 25001: "icl-twobase2", + 25002: "icl-twobase3", + 25003: "icl-twobase4", + 25004: "icl-twobase5", + 25005: "icl-twobase6", + 25006: "icl-twobase7", + 25007: "icl-twobase8", + 25008: "icl-twobase9", + 25009: "icl-twobase10", + 25576: "sauterdongle", + 25604: "idtp", + 25793: "vocaltec-hos", + 25900: "tasp-net", + 25901: "niobserver", + 25902: "nilinkanalyst", + 25903: "niprobe", + 26000: "quake", + 26133: "scscp", + 26208: "wnn6-ds", + 26257: "cockroach", + 26260: "ezproxy", + 26261: "ezmeeting", + 26262: "k3software-svr", + 26263: "k3software-cli", + 26486: "exoline-tcp", + 26487: "exoconfig", + 26489: "exonet", + 27345: "imagepump", + 27442: "jesmsjc", + 27504: "kopek-httphead", + 27782: "ars-vista", + 27876: "astrolink", + 27999: "tw-auth-key", + 28000: "nxlmd", + 28001: "pqsp", + 28200: "voxelstorm", + 28240: "siemensgsm", + 28589: "bosswave", + 29167: "otmp", + 29999: "bingbang", + 30000: "ndmps", + 30001: "pago-services1", + 30002: "pago-services2", + 30003: "amicon-fpsu-ra", + 30100: "rwp", + 30260: "kingdomsonline", + 30400: "gs-realtime", + 30999: "ovobs", + 31016: "ka-sddp", + 31020: "autotrac-acp", + 31400: "pace-licensed", + 31416: "xqosd", + 31457: "tetrinet", + 31620: "lm-mon", + 31685: "dsx-monitor", + 31765: "gamesmith-port", + 31948: "iceedcp-tx", + 31949: "iceedcp-rx", + 32034: "iracinghelper", + 32249: "t1distproc60", + 32400: "plex", + 32483: "apm-link", + 32635: "sec-ntb-clnt", + 32636: "DMExpress", + 32767: "filenet-powsrm", + 32768: "filenet-tms", + 32769: "filenet-rpc", + 32770: "filenet-nch", + 32771: "filenet-rmi", + 32772: "filenet-pa", + 32773: "filenet-cm", + 32774: "filenet-re", + 32775: "filenet-pch", + 32776: "filenet-peior", + 32777: "filenet-obrok", + 32801: "mlsn", + 32811: "retp", + 32896: "idmgratm", + 33060: "mysqlx", + 33123: "aurora-balaena", + 33331: "diamondport", + 33333: "dgi-serv", + 33334: "speedtrace", + 33434: "traceroute", + 33656: "snip-slave", + 34249: "turbonote-2", + 34378: "p-net-local", + 34379: "p-net-remote", + 34567: "dhanalakshmi", + 34962: "profinet-rt", + 34963: "profinet-rtm", + 34964: "profinet-cm", + 34980: "ethercat", + 35000: "heathview", + 35001: "rt-viewer", + 35002: "rt-sound", + 35003: "rt-devicemapper", + 35004: "rt-classmanager", + 35005: "rt-labtracker", + 35006: "rt-helper", + 35100: "axio-disc", + 35354: "kitim", + 35355: "altova-lm", + 35356: "guttersnex", + 35357: "openstack-id", + 36001: "allpeers", + 36524: "febooti-aw", + 36602: "observium-agent", + 36700: "mapx", + 36865: "kastenxpipe", + 37475: "neckar", + 37483: "gdrive-sync", + 37601: "eftp", + 37654: "unisys-eportal", + 38000: "ivs-database", + 38001: "ivs-insertion", + 38002: "cresco-control", + 38201: "galaxy7-data", + 38202: "fairview", + 38203: "agpolicy", + 38800: "sruth", + 38865: "secrmmsafecopya", + 39681: "turbonote-1", + 40000: "safetynetp", + 40404: "sptx", + 40841: "cscp", + 40842: "csccredir", + 40843: "csccfirewall", + 41111: "fs-qos", + 41121: "tentacle", + 41230: "z-wave-s", + 41794: "crestron-cip", + 41795: "crestron-ctp", + 41796: "crestron-cips", + 41797: "crestron-ctps", + 42508: "candp", + 42509: "candrp", + 42510: "caerpc", + 43000: "recvr-rc", + 43188: "reachout", + 43189: "ndm-agent-port", + 43190: "ip-provision", + 43191: "noit-transport", + 43210: "shaperai", + 43439: "eq3-update", + 43440: "ew-mgmt", + 43441: "ciscocsdb", + 44123: "z-wave-tunnel", + 44321: "pmcd", + 44322: "pmcdproxy", + 44323: "pmwebapi", + 44444: "cognex-dataman", + 44553: "rbr-debug", + 44818: "EtherNet-IP-2", + 44900: "m3da", + 45000: "asmp", + 45001: "asmps", + 45002: "rs-status", + 45045: "synctest", + 45054: "invision-ag", + 45514: "cloudcheck", + 45678: "eba", + 45824: "dai-shell", + 45825: "qdb2service", + 45966: "ssr-servermgr", + 46336: "inedo", + 46998: "spremotetablet", + 46999: "mediabox", + 47000: "mbus", + 47001: "winrm", + 47557: "dbbrowse", + 47624: "directplaysrvr", + 47806: "ap", + 47808: "bacnet", + 48000: "nimcontroller", + 48001: "nimspooler", + 48002: "nimhub", + 48003: "nimgtw", + 48004: "nimbusdb", + 48005: "nimbusdbctrl", + 48049: "3gpp-cbsp", + 48050: "weandsf", + 48128: "isnetserv", + 48129: "blp5", + 48556: "com-bardac-dw", + 48619: "iqobject", + 48653: "robotraconteur", + 49000: "matahari", + 49001: "nusrp", +} +var udpPortNames = map[uint16]string{ + 1: "tcpmux", + 2: "compressnet", + 3: "compressnet", + 5: "rje", + 7: "echo", + 9: "discard", + 11: "systat", + 13: "daytime", + 17: "qotd", + 18: "msp", + 19: "chargen", + 20: "ftp-data", + 21: "ftp", + 22: "ssh", + 23: "telnet", + 25: "smtp", + 27: "nsw-fe", + 29: "msg-icp", + 31: "msg-auth", + 33: "dsp", + 37: "time", + 38: "rap", + 39: "rlp", + 41: "graphics", + 42: "name", + 43: "nicname", + 44: "mpm-flags", + 45: "mpm", + 46: "mpm-snd", + 48: "auditd", + 49: "tacacs", + 50: "re-mail-ck", + 52: "xns-time", + 53: "domain", + 54: "xns-ch", + 55: "isi-gl", + 56: "xns-auth", + 58: "xns-mail", + 62: "acas", + 63: "whoispp", + 64: "covia", + 65: "tacacs-ds", + 66: "sql-net", + 67: "bootps", + 68: "bootpc", + 69: "tftp", + 70: "gopher", + 71: "netrjs-1", + 72: "netrjs-2", + 73: "netrjs-3", + 74: "netrjs-4", + 76: "deos", + 78: "vettcp", + 79: "finger", + 80: "http", + 82: "xfer", + 83: "mit-ml-dev", + 84: "ctf", + 85: "mit-ml-dev", + 86: "mfcobol", + 88: "kerberos", + 89: "su-mit-tg", + 90: "dnsix", + 91: "mit-dov", + 92: "npp", + 93: "dcp", + 94: "objcall", + 95: "supdup", + 96: "dixie", + 97: "swift-rvf", + 98: "tacnews", + 99: "metagram", + 101: "hostname", + 102: "iso-tsap", + 103: "gppitnp", + 104: "acr-nema", + 105: "cso", + 106: "3com-tsmux", + 107: "rtelnet", + 108: "snagas", + 109: "pop2", + 110: "pop3", + 111: "sunrpc", + 112: "mcidas", + 113: "auth", + 115: "sftp", + 116: "ansanotify", + 117: "uucp-path", + 118: "sqlserv", + 119: "nntp", + 120: "cfdptkt", + 121: "erpc", + 122: "smakynet", + 123: "ntp", + 124: "ansatrader", + 125: "locus-map", + 126: "nxedit", + 127: "locus-con", + 128: "gss-xlicen", + 129: "pwdgen", + 130: "cisco-fna", + 131: "cisco-tna", + 132: "cisco-sys", + 133: "statsrv", + 134: "ingres-net", + 135: "epmap", + 136: "profile", + 137: "netbios-ns", + 138: "netbios-dgm", + 139: "netbios-ssn", + 140: "emfis-data", + 141: "emfis-cntl", + 142: "bl-idm", + 143: "imap", + 144: "uma", + 145: "uaac", + 146: "iso-tp0", + 147: "iso-ip", + 148: "jargon", + 149: "aed-512", + 150: "sql-net", + 151: "hems", + 152: "bftp", + 153: "sgmp", + 154: "netsc-prod", + 155: "netsc-dev", + 156: "sqlsrv", + 157: "knet-cmp", + 158: "pcmail-srv", + 159: "nss-routing", + 160: "sgmp-traps", + 161: "snmp", + 162: "snmptrap", + 163: "cmip-man", + 164: "cmip-agent", + 165: "xns-courier", + 166: "s-net", + 167: "namp", + 168: "rsvd", + 169: "send", + 170: "print-srv", + 171: "multiplex", + 172: "cl-1", + 173: "xyplex-mux", + 174: "mailq", + 175: "vmnet", + 176: "genrad-mux", + 177: "xdmcp", + 178: "nextstep", + 179: "bgp", + 180: "ris", + 181: "unify", + 182: "audit", + 183: "ocbinder", + 184: "ocserver", + 185: "remote-kis", + 186: "kis", + 187: "aci", + 188: "mumps", + 189: "qft", + 190: "gacp", + 191: "prospero", + 192: "osu-nms", + 193: "srmp", + 194: "irc", + 195: "dn6-nlm-aud", + 196: "dn6-smm-red", + 197: "dls", + 198: "dls-mon", + 199: "smux", + 200: "src", + 201: "at-rtmp", + 202: "at-nbp", + 203: "at-3", + 204: "at-echo", + 205: "at-5", + 206: "at-zis", + 207: "at-7", + 208: "at-8", + 209: "qmtp", + 210: "z39-50", + 211: "914c-g", + 212: "anet", + 213: "ipx", + 214: "vmpwscs", + 215: "softpc", + 216: "CAIlic", + 217: "dbase", + 218: "mpp", + 219: "uarps", + 220: "imap3", + 221: "fln-spx", + 222: "rsh-spx", + 223: "cdc", + 224: "masqdialer", + 242: "direct", + 243: "sur-meas", + 244: "inbusiness", + 245: "link", + 246: "dsp3270", + 247: "subntbcst-tftp", + 248: "bhfhs", + 256: "rap", + 257: "set", + 259: "esro-gen", + 260: "openport", + 261: "nsiiops", + 262: "arcisdms", + 263: "hdap", + 264: "bgmp", + 265: "x-bone-ctl", + 266: "sst", + 267: "td-service", + 268: "td-replica", + 269: "manet", + 270: "gist", + 280: "http-mgmt", + 281: "personal-link", + 282: "cableport-ax", + 283: "rescap", + 284: "corerjd", + 286: "fxp", + 287: "k-block", + 308: "novastorbakcup", + 309: "entrusttime", + 310: "bhmds", + 311: "asip-webadmin", + 312: "vslmp", + 313: "magenta-logic", + 314: "opalis-robot", + 315: "dpsi", + 316: "decauth", + 317: "zannet", + 318: "pkix-timestamp", + 319: "ptp-event", + 320: "ptp-general", + 321: "pip", + 322: "rtsps", + 333: "texar", + 344: "pdap", + 345: "pawserv", + 346: "zserv", + 347: "fatserv", + 348: "csi-sgwp", + 349: "mftp", + 350: "matip-type-a", + 351: "matip-type-b", + 352: "dtag-ste-sb", + 353: "ndsauth", + 354: "bh611", + 355: "datex-asn", + 356: "cloanto-net-1", + 357: "bhevent", + 358: "shrinkwrap", + 359: "nsrmp", + 360: "scoi2odialog", + 361: "semantix", + 362: "srssend", + 363: "rsvp-tunnel", + 364: "aurora-cmgr", + 365: "dtk", + 366: "odmr", + 367: "mortgageware", + 368: "qbikgdp", + 369: "rpc2portmap", + 370: "codaauth2", + 371: "clearcase", + 372: "ulistproc", + 373: "legent-1", + 374: "legent-2", + 375: "hassle", + 376: "nip", + 377: "tnETOS", + 378: "dsETOS", + 379: "is99c", + 380: "is99s", + 381: "hp-collector", + 382: "hp-managed-node", + 383: "hp-alarm-mgr", + 384: "arns", + 385: "ibm-app", + 386: "asa", + 387: "aurp", + 388: "unidata-ldm", + 389: "ldap", + 390: "uis", + 391: "synotics-relay", + 392: "synotics-broker", + 393: "meta5", + 394: "embl-ndt", + 395: "netcp", + 396: "netware-ip", + 397: "mptn", + 398: "kryptolan", + 399: "iso-tsap-c2", + 400: "osb-sd", + 401: "ups", + 402: "genie", + 403: "decap", + 404: "nced", + 405: "ncld", + 406: "imsp", + 407: "timbuktu", + 408: "prm-sm", + 409: "prm-nm", + 410: "decladebug", + 411: "rmt", + 412: "synoptics-trap", + 413: "smsp", + 414: "infoseek", + 415: "bnet", + 416: "silverplatter", + 417: "onmux", + 418: "hyper-g", + 419: "ariel1", + 420: "smpte", + 421: "ariel2", + 422: "ariel3", + 423: "opc-job-start", + 424: "opc-job-track", + 425: "icad-el", + 426: "smartsdp", + 427: "svrloc", + 428: "ocs-cmu", + 429: "ocs-amu", + 430: "utmpsd", + 431: "utmpcd", + 432: "iasd", + 433: "nnsp", + 434: "mobileip-agent", + 435: "mobilip-mn", + 436: "dna-cml", + 437: "comscm", + 438: "dsfgw", + 439: "dasp", + 440: "sgcp", + 441: "decvms-sysmgt", + 442: "cvc-hostd", + 443: "https", + 444: "snpp", + 445: "microsoft-ds", + 446: "ddm-rdb", + 447: "ddm-dfm", + 448: "ddm-ssl", + 449: "as-servermap", + 450: "tserver", + 451: "sfs-smp-net", + 452: "sfs-config", + 453: "creativeserver", + 454: "contentserver", + 455: "creativepartnr", + 456: "macon-udp", + 457: "scohelp", + 458: "appleqtc", + 459: "ampr-rcmd", + 460: "skronk", + 461: "datasurfsrv", + 462: "datasurfsrvsec", + 463: "alpes", + 464: "kpasswd", + 465: "igmpv3lite", + 466: "digital-vrc", + 467: "mylex-mapd", + 468: "photuris", + 469: "rcp", + 470: "scx-proxy", + 471: "mondex", + 472: "ljk-login", + 473: "hybrid-pop", + 474: "tn-tl-w2", + 475: "tcpnethaspsrv", + 476: "tn-tl-fd1", + 477: "ss7ns", + 478: "spsc", + 479: "iafserver", + 480: "iafdbase", + 481: "ph", + 482: "bgs-nsi", + 483: "ulpnet", + 484: "integra-sme", + 485: "powerburst", + 486: "avian", + 487: "saft", + 488: "gss-http", + 489: "nest-protocol", + 490: "micom-pfs", + 491: "go-login", + 492: "ticf-1", + 493: "ticf-2", + 494: "pov-ray", + 495: "intecourier", + 496: "pim-rp-disc", + 497: "retrospect", + 498: "siam", + 499: "iso-ill", + 500: "isakmp", + 501: "stmf", + 502: "mbap", + 503: "intrinsa", + 504: "citadel", + 505: "mailbox-lm", + 506: "ohimsrv", + 507: "crs", + 508: "xvttp", + 509: "snare", + 510: "fcp", + 511: "passgo", + 512: "comsat", + 513: "who", + 514: "syslog", + 515: "printer", + 516: "videotex", + 517: "talk", + 518: "ntalk", + 519: "utime", + 520: "router", + 521: "ripng", + 522: "ulp", + 523: "ibm-db2", + 524: "ncp", + 525: "timed", + 526: "tempo", + 527: "stx", + 528: "custix", + 529: "irc-serv", + 530: "courier", + 531: "conference", + 532: "netnews", + 533: "netwall", + 534: "windream", + 535: "iiop", + 536: "opalis-rdv", + 537: "nmsp", + 538: "gdomap", + 539: "apertus-ldp", + 540: "uucp", + 541: "uucp-rlogin", + 542: "commerce", + 543: "klogin", + 544: "kshell", + 545: "appleqtcsrvr", + 546: "dhcpv6-client", + 547: "dhcpv6-server", + 548: "afpovertcp", + 549: "idfp", + 550: "new-rwho", + 551: "cybercash", + 552: "devshr-nts", + 553: "pirp", + 554: "rtsp", + 555: "dsf", + 556: "remotefs", + 557: "openvms-sysipc", + 558: "sdnskmp", + 559: "teedtap", + 560: "rmonitor", + 561: "monitor", + 562: "chshell", + 563: "nntps", + 564: "9pfs", + 565: "whoami", + 566: "streettalk", + 567: "banyan-rpc", + 568: "ms-shuttle", + 569: "ms-rome", + 570: "meter", + 571: "meter", + 572: "sonar", + 573: "banyan-vip", + 574: "ftp-agent", + 575: "vemmi", + 576: "ipcd", + 577: "vnas", + 578: "ipdd", + 579: "decbsrv", + 580: "sntp-heartbeat", + 581: "bdp", + 582: "scc-security", + 583: "philips-vc", + 584: "keyserver", + 586: "password-chg", + 587: "submission", + 588: "cal", + 589: "eyelink", + 590: "tns-cml", + 591: "http-alt", + 592: "eudora-set", + 593: "http-rpc-epmap", + 594: "tpip", + 595: "cab-protocol", + 596: "smsd", + 597: "ptcnameservice", + 598: "sco-websrvrmg3", + 599: "acp", + 600: "ipcserver", + 601: "syslog-conn", + 602: "xmlrpc-beep", + 603: "idxp", + 604: "tunnel", + 605: "soap-beep", + 606: "urm", + 607: "nqs", + 608: "sift-uft", + 609: "npmp-trap", + 610: "npmp-local", + 611: "npmp-gui", + 612: "hmmp-ind", + 613: "hmmp-op", + 614: "sshell", + 615: "sco-inetmgr", + 616: "sco-sysmgr", + 617: "sco-dtmgr", + 618: "dei-icda", + 619: "compaq-evm", + 620: "sco-websrvrmgr", + 621: "escp-ip", + 622: "collaborator", + 623: "asf-rmcp", + 624: "cryptoadmin", + 625: "dec-dlm", + 626: "asia", + 627: "passgo-tivoli", + 628: "qmqp", + 629: "3com-amp3", + 630: "rda", + 631: "ipp", + 632: "bmpp", + 633: "servstat", + 634: "ginad", + 635: "rlzdbase", + 636: "ldaps", + 637: "lanserver", + 638: "mcns-sec", + 639: "msdp", + 640: "entrust-sps", + 641: "repcmd", + 642: "esro-emsdp", + 643: "sanity", + 644: "dwr", + 645: "pssc", + 646: "ldp", + 647: "dhcp-failover", + 648: "rrp", + 649: "cadview-3d", + 650: "obex", + 651: "ieee-mms", + 652: "hello-port", + 653: "repscmd", + 654: "aodv", + 655: "tinc", + 656: "spmp", + 657: "rmc", + 658: "tenfold", + 660: "mac-srvr-admin", + 661: "hap", + 662: "pftp", + 663: "purenoise", + 664: "asf-secure-rmcp", + 665: "sun-dr", + 666: "mdqs", + 667: "disclose", + 668: "mecomm", + 669: "meregister", + 670: "vacdsm-sws", + 671: "vacdsm-app", + 672: "vpps-qua", + 673: "cimplex", + 674: "acap", + 675: "dctp", + 676: "vpps-via", + 677: "vpp", + 678: "ggf-ncp", + 679: "mrm", + 680: "entrust-aaas", + 681: "entrust-aams", + 682: "xfr", + 683: "corba-iiop", + 684: "corba-iiop-ssl", + 685: "mdc-portmapper", + 686: "hcp-wismar", + 687: "asipregistry", + 688: "realm-rusd", + 689: "nmap", + 690: "vatp", + 691: "msexch-routing", + 692: "hyperwave-isp", + 693: "connendp", + 694: "ha-cluster", + 695: "ieee-mms-ssl", + 696: "rushd", + 697: "uuidgen", + 698: "olsr", + 699: "accessnetwork", + 700: "epp", + 701: "lmp", + 702: "iris-beep", + 704: "elcsd", + 705: "agentx", + 706: "silc", + 707: "borland-dsj", + 709: "entrust-kmsh", + 710: "entrust-ash", + 711: "cisco-tdp", + 712: "tbrpf", + 713: "iris-xpc", + 714: "iris-xpcs", + 715: "iris-lwz", + 716: "pana", + 729: "netviewdm1", + 730: "netviewdm2", + 731: "netviewdm3", + 741: "netgw", + 742: "netrcs", + 744: "flexlm", + 747: "fujitsu-dev", + 748: "ris-cm", + 749: "kerberos-adm", + 750: "loadav", + 751: "pump", + 752: "qrh", + 753: "rrh", + 754: "tell", + 758: "nlogin", + 759: "con", + 760: "ns", + 761: "rxe", + 762: "quotad", + 763: "cycleserv", + 764: "omserv", + 765: "webster", + 767: "phonebook", + 769: "vid", + 770: "cadlock", + 771: "rtip", + 772: "cycleserv2", + 773: "notify", + 774: "acmaint-dbd", + 775: "acmaint-transd", + 776: "wpages", + 777: "multiling-http", + 780: "wpgs", + 800: "mdbs-daemon", + 801: "device", + 802: "mbap-s", + 810: "fcp-udp", + 828: "itm-mcell-s", + 829: "pkix-3-ca-ra", + 830: "netconf-ssh", + 831: "netconf-beep", + 832: "netconfsoaphttp", + 833: "netconfsoapbeep", + 847: "dhcp-failover2", + 848: "gdoi", + 853: "domain-s", + 854: "dlep", + 860: "iscsi", + 861: "owamp-control", + 862: "twamp-control", + 873: "rsync", + 886: "iclcnet-locate", + 887: "iclcnet-svinfo", + 888: "accessbuilder", + 900: "omginitialrefs", + 901: "smpnameres", + 902: "ideafarm-door", + 903: "ideafarm-panic", + 910: "kink", + 911: "xact-backup", + 912: "apex-mesh", + 913: "apex-edge", + 989: "ftps-data", + 990: "ftps", + 991: "nas", + 992: "telnets", + 993: "imaps", + 995: "pop3s", + 996: "vsinet", + 997: "maitrd", + 998: "puparp", + 999: "applix", + 1000: "cadlock2", + 1010: "surf", + 1021: "exp1", + 1022: "exp2", + 1025: "blackjack", + 1026: "cap", + 1027: "6a44", + 1029: "solid-mux", + 1033: "netinfo-local", + 1034: "activesync", + 1035: "mxxrlogin", + 1036: "nsstp", + 1037: "ams", + 1038: "mtqp", + 1039: "sbl", + 1040: "netarx", + 1041: "danf-ak2", + 1042: "afrog", + 1043: "boinc-client", + 1044: "dcutility", + 1045: "fpitp", + 1046: "wfremotertm", + 1047: "neod1", + 1048: "neod2", + 1049: "td-postman", + 1050: "cma", + 1051: "optima-vnet", + 1052: "ddt", + 1053: "remote-as", + 1054: "brvread", + 1055: "ansyslmd", + 1056: "vfo", + 1057: "startron", + 1058: "nim", + 1059: "nimreg", + 1060: "polestar", + 1061: "kiosk", + 1062: "veracity", + 1063: "kyoceranetdev", + 1064: "jstel", + 1065: "syscomlan", + 1066: "fpo-fns", + 1067: "instl-boots", + 1068: "instl-bootc", + 1069: "cognex-insight", + 1070: "gmrupdateserv", + 1071: "bsquare-voip", + 1072: "cardax", + 1073: "bridgecontrol", + 1074: "warmspotMgmt", + 1075: "rdrmshc", + 1076: "dab-sti-c", + 1077: "imgames", + 1078: "avocent-proxy", + 1079: "asprovatalk", + 1080: "socks", + 1081: "pvuniwien", + 1082: "amt-esd-prot", + 1083: "ansoft-lm-1", + 1084: "ansoft-lm-2", + 1085: "webobjects", + 1086: "cplscrambler-lg", + 1087: "cplscrambler-in", + 1088: "cplscrambler-al", + 1089: "ff-annunc", + 1090: "ff-fms", + 1091: "ff-sm", + 1092: "obrpd", + 1093: "proofd", + 1094: "rootd", + 1095: "nicelink", + 1096: "cnrprotocol", + 1097: "sunclustermgr", + 1098: "rmiactivation", + 1099: "rmiregistry", + 1100: "mctp", + 1101: "pt2-discover", + 1102: "adobeserver-1", + 1103: "adobeserver-2", + 1104: "xrl", + 1105: "ftranhc", + 1106: "isoipsigport-1", + 1107: "isoipsigport-2", + 1108: "ratio-adp", + 1110: "nfsd-keepalive", + 1111: "lmsocialserver", + 1112: "icp", + 1113: "ltp-deepspace", + 1114: "mini-sql", + 1115: "ardus-trns", + 1116: "ardus-cntl", + 1117: "ardus-mtrns", + 1118: "sacred", + 1119: "bnetgame", + 1120: "bnetfile", + 1121: "rmpp", + 1122: "availant-mgr", + 1123: "murray", + 1124: "hpvmmcontrol", + 1125: "hpvmmagent", + 1126: "hpvmmdata", + 1127: "kwdb-commn", + 1128: "saphostctrl", + 1129: "saphostctrls", + 1130: "casp", + 1131: "caspssl", + 1132: "kvm-via-ip", + 1133: "dfn", + 1134: "aplx", + 1135: "omnivision", + 1136: "hhb-gateway", + 1137: "trim", + 1138: "encrypted-admin", + 1139: "evm", + 1140: "autonoc", + 1141: "mxomss", + 1142: "edtools", + 1143: "imyx", + 1144: "fuscript", + 1145: "x9-icue", + 1146: "audit-transfer", + 1147: "capioverlan", + 1148: "elfiq-repl", + 1149: "bvtsonar", + 1150: "blaze", + 1151: "unizensus", + 1152: "winpoplanmess", + 1153: "c1222-acse", + 1154: "resacommunity", + 1155: "nfa", + 1156: "iascontrol-oms", + 1157: "iascontrol", + 1158: "dbcontrol-oms", + 1159: "oracle-oms", + 1160: "olsv", + 1161: "health-polling", + 1162: "health-trap", + 1163: "sddp", + 1164: "qsm-proxy", + 1165: "qsm-gui", + 1166: "qsm-remote", + 1167: "cisco-ipsla", + 1168: "vchat", + 1169: "tripwire", + 1170: "atc-lm", + 1171: "atc-appserver", + 1172: "dnap", + 1173: "d-cinema-rrp", + 1174: "fnet-remote-ui", + 1175: "dossier", + 1176: "indigo-server", + 1177: "dkmessenger", + 1178: "sgi-storman", + 1179: "b2n", + 1180: "mc-client", + 1181: "3comnetman", + 1182: "accelenet-data", + 1183: "llsurfup-http", + 1184: "llsurfup-https", + 1185: "catchpole", + 1186: "mysql-cluster", + 1187: "alias", + 1188: "hp-webadmin", + 1189: "unet", + 1190: "commlinx-avl", + 1191: "gpfs", + 1192: "caids-sensor", + 1193: "fiveacross", + 1194: "openvpn", + 1195: "rsf-1", + 1196: "netmagic", + 1197: "carrius-rshell", + 1198: "cajo-discovery", + 1199: "dmidi", + 1200: "scol", + 1201: "nucleus-sand", + 1202: "caiccipc", + 1203: "ssslic-mgr", + 1204: "ssslog-mgr", + 1205: "accord-mgc", + 1206: "anthony-data", + 1207: "metasage", + 1208: "seagull-ais", + 1209: "ipcd3", + 1210: "eoss", + 1211: "groove-dpp", + 1212: "lupa", + 1213: "mpc-lifenet", + 1214: "kazaa", + 1215: "scanstat-1", + 1216: "etebac5", + 1217: "hpss-ndapi", + 1218: "aeroflight-ads", + 1219: "aeroflight-ret", + 1220: "qt-serveradmin", + 1221: "sweetware-apps", + 1222: "nerv", + 1223: "tgp", + 1224: "vpnz", + 1225: "slinkysearch", + 1226: "stgxfws", + 1227: "dns2go", + 1228: "florence", + 1229: "zented", + 1230: "periscope", + 1231: "menandmice-lpm", + 1232: "first-defense", + 1233: "univ-appserver", + 1234: "search-agent", + 1235: "mosaicsyssvc1", + 1236: "bvcontrol", + 1237: "tsdos390", + 1238: "hacl-qs", + 1239: "nmsd", + 1240: "instantia", + 1241: "nessus", + 1242: "nmasoverip", + 1243: "serialgateway", + 1244: "isbconference1", + 1245: "isbconference2", + 1246: "payrouter", + 1247: "visionpyramid", + 1248: "hermes", + 1249: "mesavistaco", + 1250: "swldy-sias", + 1251: "servergraph", + 1252: "bspne-pcc", + 1253: "q55-pcc", + 1254: "de-noc", + 1255: "de-cache-query", + 1256: "de-server", + 1257: "shockwave2", + 1258: "opennl", + 1259: "opennl-voice", + 1260: "ibm-ssd", + 1261: "mpshrsv", + 1262: "qnts-orb", + 1263: "dka", + 1264: "prat", + 1265: "dssiapi", + 1266: "dellpwrappks", + 1267: "epc", + 1268: "propel-msgsys", + 1269: "watilapp", + 1270: "opsmgr", + 1271: "excw", + 1272: "cspmlockmgr", + 1273: "emc-gateway", + 1274: "t1distproc", + 1275: "ivcollector", + 1277: "miva-mqs", + 1278: "dellwebadmin-1", + 1279: "dellwebadmin-2", + 1280: "pictrography", + 1281: "healthd", + 1282: "emperion", + 1283: "productinfo", + 1284: "iee-qfx", + 1285: "neoiface", + 1286: "netuitive", + 1287: "routematch", + 1288: "navbuddy", + 1289: "jwalkserver", + 1290: "winjaserver", + 1291: "seagulllms", + 1292: "dsdn", + 1293: "pkt-krb-ipsec", + 1294: "cmmdriver", + 1295: "ehtp", + 1296: "dproxy", + 1297: "sdproxy", + 1298: "lpcp", + 1299: "hp-sci", + 1300: "h323hostcallsc", + 1301: "ci3-software-1", + 1302: "ci3-software-2", + 1303: "sftsrv", + 1304: "boomerang", + 1305: "pe-mike", + 1306: "re-conn-proto", + 1307: "pacmand", + 1308: "odsi", + 1309: "jtag-server", + 1310: "husky", + 1311: "rxmon", + 1312: "sti-envision", + 1313: "bmc-patroldb", + 1314: "pdps", + 1315: "els", + 1316: "exbit-escp", + 1317: "vrts-ipcserver", + 1318: "krb5gatekeeper", + 1319: "amx-icsp", + 1320: "amx-axbnet", + 1321: "pip", + 1322: "novation", + 1323: "brcd", + 1324: "delta-mcp", + 1325: "dx-instrument", + 1326: "wimsic", + 1327: "ultrex", + 1328: "ewall", + 1329: "netdb-export", + 1330: "streetperfect", + 1331: "intersan", + 1332: "pcia-rxp-b", + 1333: "passwrd-policy", + 1334: "writesrv", + 1335: "digital-notary", + 1336: "ischat", + 1337: "menandmice-dns", + 1338: "wmc-log-svc", + 1339: "kjtsiteserver", + 1340: "naap", + 1341: "qubes", + 1342: "esbroker", + 1343: "re101", + 1344: "icap", + 1345: "vpjp", + 1346: "alta-ana-lm", + 1347: "bbn-mmc", + 1348: "bbn-mmx", + 1349: "sbook", + 1350: "editbench", + 1351: "equationbuilder", + 1352: "lotusnote", + 1353: "relief", + 1354: "XSIP-network", + 1355: "intuitive-edge", + 1356: "cuillamartin", + 1357: "pegboard", + 1358: "connlcli", + 1359: "ftsrv", + 1360: "mimer", + 1361: "linx", + 1362: "timeflies", + 1363: "ndm-requester", + 1364: "ndm-server", + 1365: "adapt-sna", + 1366: "netware-csp", + 1367: "dcs", + 1368: "screencast", + 1369: "gv-us", + 1370: "us-gv", + 1371: "fc-cli", + 1372: "fc-ser", + 1373: "chromagrafx", + 1374: "molly", + 1375: "bytex", + 1376: "ibm-pps", + 1377: "cichlid", + 1378: "elan", + 1379: "dbreporter", + 1380: "telesis-licman", + 1381: "apple-licman", + 1382: "udt-os", + 1383: "gwha", + 1384: "os-licman", + 1385: "atex-elmd", + 1386: "checksum", + 1387: "cadsi-lm", + 1388: "objective-dbc", + 1389: "iclpv-dm", + 1390: "iclpv-sc", + 1391: "iclpv-sas", + 1392: "iclpv-pm", + 1393: "iclpv-nls", + 1394: "iclpv-nlc", + 1395: "iclpv-wsm", + 1396: "dvl-activemail", + 1397: "audio-activmail", + 1398: "video-activmail", + 1399: "cadkey-licman", + 1400: "cadkey-tablet", + 1401: "goldleaf-licman", + 1402: "prm-sm-np", + 1403: "prm-nm-np", + 1404: "igi-lm", + 1405: "ibm-res", + 1406: "netlabs-lm", + 1408: "sophia-lm", + 1409: "here-lm", + 1410: "hiq", + 1411: "af", + 1412: "innosys", + 1413: "innosys-acl", + 1414: "ibm-mqseries", + 1415: "dbstar", + 1416: "novell-lu6-2", + 1417: "timbuktu-srv1", + 1418: "timbuktu-srv2", + 1419: "timbuktu-srv3", + 1420: "timbuktu-srv4", + 1421: "gandalf-lm", + 1422: "autodesk-lm", + 1423: "essbase", + 1424: "hybrid", + 1425: "zion-lm", + 1426: "sais", + 1427: "mloadd", + 1428: "informatik-lm", + 1429: "nms", + 1430: "tpdu", + 1431: "rgtp", + 1432: "blueberry-lm", + 1433: "ms-sql-s", + 1434: "ms-sql-m", + 1435: "ibm-cics", + 1436: "saism", + 1437: "tabula", + 1438: "eicon-server", + 1439: "eicon-x25", + 1440: "eicon-slp", + 1441: "cadis-1", + 1442: "cadis-2", + 1443: "ies-lm", + 1444: "marcam-lm", + 1445: "proxima-lm", + 1446: "ora-lm", + 1447: "apri-lm", + 1448: "oc-lm", + 1449: "peport", + 1450: "dwf", + 1451: "infoman", + 1452: "gtegsc-lm", + 1453: "genie-lm", + 1454: "interhdl-elmd", + 1455: "esl-lm", + 1456: "dca", + 1457: "valisys-lm", + 1458: "nrcabq-lm", + 1459: "proshare1", + 1460: "proshare2", + 1461: "ibm-wrless-lan", + 1462: "world-lm", + 1463: "nucleus", + 1464: "msl-lmd", + 1465: "pipes", + 1466: "oceansoft-lm", + 1467: "csdmbase", + 1468: "csdm", + 1469: "aal-lm", + 1470: "uaiact", + 1471: "csdmbase", + 1472: "csdm", + 1473: "openmath", + 1474: "telefinder", + 1475: "taligent-lm", + 1476: "clvm-cfg", + 1477: "ms-sna-server", + 1478: "ms-sna-base", + 1479: "dberegister", + 1480: "pacerforum", + 1481: "airs", + 1482: "miteksys-lm", + 1483: "afs", + 1484: "confluent", + 1485: "lansource", + 1486: "nms-topo-serv", + 1487: "localinfosrvr", + 1488: "docstor", + 1489: "dmdocbroker", + 1490: "insitu-conf", + 1492: "stone-design-1", + 1493: "netmap-lm", + 1494: "ica", + 1495: "cvc", + 1496: "liberty-lm", + 1497: "rfx-lm", + 1498: "sybase-sqlany", + 1499: "fhc", + 1500: "vlsi-lm", + 1501: "saiscm", + 1502: "shivadiscovery", + 1503: "imtc-mcs", + 1504: "evb-elm", + 1505: "funkproxy", + 1506: "utcd", + 1507: "symplex", + 1508: "diagmond", + 1509: "robcad-lm", + 1510: "mvx-lm", + 1511: "3l-l1", + 1512: "wins", + 1513: "fujitsu-dtc", + 1514: "fujitsu-dtcns", + 1515: "ifor-protocol", + 1516: "vpad", + 1517: "vpac", + 1518: "vpvd", + 1519: "vpvc", + 1520: "atm-zip-office", + 1521: "ncube-lm", + 1522: "ricardo-lm", + 1523: "cichild-lm", + 1524: "ingreslock", + 1525: "orasrv", + 1526: "pdap-np", + 1527: "tlisrv", + 1528: "ngr-t", + 1529: "coauthor", + 1530: "rap-service", + 1531: "rap-listen", + 1532: "miroconnect", + 1533: "virtual-places", + 1534: "micromuse-lm", + 1535: "ampr-info", + 1536: "ampr-inter", + 1537: "sdsc-lm", + 1538: "3ds-lm", + 1539: "intellistor-lm", + 1540: "rds", + 1541: "rds2", + 1542: "gridgen-elmd", + 1543: "simba-cs", + 1544: "aspeclmd", + 1545: "vistium-share", + 1546: "abbaccuray", + 1547: "laplink", + 1548: "axon-lm", + 1549: "shivasound", + 1550: "3m-image-lm", + 1551: "hecmtl-db", + 1552: "pciarray", + 1553: "sna-cs", + 1554: "caci-lm", + 1555: "livelan", + 1556: "veritas-pbx", + 1557: "arbortext-lm", + 1558: "xingmpeg", + 1559: "web2host", + 1560: "asci-val", + 1561: "facilityview", + 1562: "pconnectmgr", + 1563: "cadabra-lm", + 1564: "pay-per-view", + 1565: "winddlb", + 1566: "corelvideo", + 1567: "jlicelmd", + 1568: "tsspmap", + 1569: "ets", + 1570: "orbixd", + 1571: "rdb-dbs-disp", + 1572: "chip-lm", + 1573: "itscomm-ns", + 1574: "mvel-lm", + 1575: "oraclenames", + 1576: "moldflow-lm", + 1577: "hypercube-lm", + 1578: "jacobus-lm", + 1579: "ioc-sea-lm", + 1580: "tn-tl-r2", + 1581: "mil-2045-47001", + 1582: "msims", + 1583: "simbaexpress", + 1584: "tn-tl-fd2", + 1585: "intv", + 1586: "ibm-abtact", + 1587: "pra-elmd", + 1588: "triquest-lm", + 1589: "vqp", + 1590: "gemini-lm", + 1591: "ncpm-pm", + 1592: "commonspace", + 1593: "mainsoft-lm", + 1594: "sixtrak", + 1595: "radio", + 1596: "radio-bc", + 1597: "orbplus-iiop", + 1598: "picknfs", + 1599: "simbaservices", + 1600: "issd", + 1601: "aas", + 1602: "inspect", + 1603: "picodbc", + 1604: "icabrowser", + 1605: "slp", + 1606: "slm-api", + 1607: "stt", + 1608: "smart-lm", + 1609: "isysg-lm", + 1610: "taurus-wh", + 1611: "ill", + 1612: "netbill-trans", + 1613: "netbill-keyrep", + 1614: "netbill-cred", + 1615: "netbill-auth", + 1616: "netbill-prod", + 1617: "nimrod-agent", + 1618: "skytelnet", + 1619: "xs-openstorage", + 1620: "faxportwinport", + 1621: "softdataphone", + 1622: "ontime", + 1623: "jaleosnd", + 1624: "udp-sr-port", + 1625: "svs-omagent", + 1626: "shockwave", + 1627: "t128-gateway", + 1628: "lontalk-norm", + 1629: "lontalk-urgnt", + 1630: "oraclenet8cman", + 1631: "visitview", + 1632: "pammratc", + 1633: "pammrpc", + 1634: "loaprobe", + 1635: "edb-server1", + 1636: "isdc", + 1637: "islc", + 1638: "ismc", + 1639: "cert-initiator", + 1640: "cert-responder", + 1641: "invision", + 1642: "isis-am", + 1643: "isis-ambc", + 1644: "saiseh", + 1645: "sightline", + 1646: "sa-msg-port", + 1647: "rsap", + 1648: "concurrent-lm", + 1649: "kermit", + 1650: "nkd", + 1651: "shiva-confsrvr", + 1652: "xnmp", + 1653: "alphatech-lm", + 1654: "stargatealerts", + 1655: "dec-mbadmin", + 1656: "dec-mbadmin-h", + 1657: "fujitsu-mmpdc", + 1658: "sixnetudr", + 1659: "sg-lm", + 1660: "skip-mc-gikreq", + 1661: "netview-aix-1", + 1662: "netview-aix-2", + 1663: "netview-aix-3", + 1664: "netview-aix-4", + 1665: "netview-aix-5", + 1666: "netview-aix-6", + 1667: "netview-aix-7", + 1668: "netview-aix-8", + 1669: "netview-aix-9", + 1670: "netview-aix-10", + 1671: "netview-aix-11", + 1672: "netview-aix-12", + 1673: "proshare-mc-1", + 1674: "proshare-mc-2", + 1675: "pdp", + 1676: "netcomm2", + 1677: "groupwise", + 1678: "prolink", + 1679: "darcorp-lm", + 1680: "microcom-sbp", + 1681: "sd-elmd", + 1682: "lanyon-lantern", + 1683: "ncpm-hip", + 1684: "snaresecure", + 1685: "n2nremote", + 1686: "cvmon", + 1687: "nsjtp-ctrl", + 1688: "nsjtp-data", + 1689: "firefox", + 1690: "ng-umds", + 1691: "empire-empuma", + 1692: "sstsys-lm", + 1693: "rrirtr", + 1694: "rrimwm", + 1695: "rrilwm", + 1696: "rrifmm", + 1697: "rrisat", + 1698: "rsvp-encap-1", + 1699: "rsvp-encap-2", + 1700: "mps-raft", + 1701: "l2f", + 1702: "deskshare", + 1703: "hb-engine", + 1704: "bcs-broker", + 1705: "slingshot", + 1706: "jetform", + 1707: "vdmplay", + 1708: "gat-lmd", + 1709: "centra", + 1710: "impera", + 1711: "pptconference", + 1712: "registrar", + 1713: "conferencetalk", + 1714: "sesi-lm", + 1715: "houdini-lm", + 1716: "xmsg", + 1717: "fj-hdnet", + 1718: "h323gatedisc", + 1719: "h323gatestat", + 1720: "h323hostcall", + 1721: "caicci", + 1722: "hks-lm", + 1723: "pptp", + 1724: "csbphonemaster", + 1725: "iden-ralp", + 1726: "iberiagames", + 1727: "winddx", + 1728: "telindus", + 1729: "citynl", + 1730: "roketz", + 1731: "msiccp", + 1732: "proxim", + 1733: "siipat", + 1734: "cambertx-lm", + 1735: "privatechat", + 1736: "street-stream", + 1737: "ultimad", + 1738: "gamegen1", + 1739: "webaccess", + 1740: "encore", + 1741: "cisco-net-mgmt", + 1742: "3Com-nsd", + 1743: "cinegrfx-lm", + 1744: "ncpm-ft", + 1745: "remote-winsock", + 1746: "ftrapid-1", + 1747: "ftrapid-2", + 1748: "oracle-em1", + 1749: "aspen-services", + 1750: "sslp", + 1751: "swiftnet", + 1752: "lofr-lm", + 1754: "oracle-em2", + 1755: "ms-streaming", + 1756: "capfast-lmd", + 1757: "cnhrp", + 1758: "tftp-mcast", + 1759: "spss-lm", + 1760: "www-ldap-gw", + 1761: "cft-0", + 1762: "cft-1", + 1763: "cft-2", + 1764: "cft-3", + 1765: "cft-4", + 1766: "cft-5", + 1767: "cft-6", + 1768: "cft-7", + 1769: "bmc-net-adm", + 1770: "bmc-net-svc", + 1771: "vaultbase", + 1772: "essweb-gw", + 1773: "kmscontrol", + 1774: "global-dtserv", + 1776: "femis", + 1777: "powerguardian", + 1778: "prodigy-intrnet", + 1779: "pharmasoft", + 1780: "dpkeyserv", + 1781: "answersoft-lm", + 1782: "hp-hcip", + 1784: "finle-lm", + 1785: "windlm", + 1786: "funk-logger", + 1787: "funk-license", + 1788: "psmond", + 1789: "hello", + 1790: "nmsp", + 1791: "ea1", + 1792: "ibm-dt-2", + 1793: "rsc-robot", + 1794: "cera-bcm", + 1795: "dpi-proxy", + 1796: "vocaltec-admin", + 1797: "uma", + 1798: "etp", + 1799: "netrisk", + 1800: "ansys-lm", + 1801: "msmq", + 1802: "concomp1", + 1803: "hp-hcip-gwy", + 1804: "enl", + 1805: "enl-name", + 1806: "musiconline", + 1807: "fhsp", + 1808: "oracle-vp2", + 1809: "oracle-vp1", + 1810: "jerand-lm", + 1811: "scientia-sdb", + 1812: "radius", + 1813: "radius-acct", + 1814: "tdp-suite", + 1815: "mmpft", + 1816: "harp", + 1817: "rkb-oscs", + 1818: "etftp", + 1819: "plato-lm", + 1820: "mcagent", + 1821: "donnyworld", + 1822: "es-elmd", + 1823: "unisys-lm", + 1824: "metrics-pas", + 1825: "direcpc-video", + 1826: "ardt", + 1827: "asi", + 1828: "itm-mcell-u", + 1829: "optika-emedia", + 1830: "net8-cman", + 1831: "myrtle", + 1832: "tht-treasure", + 1833: "udpradio", + 1834: "ardusuni", + 1835: "ardusmul", + 1836: "ste-smsc", + 1837: "csoft1", + 1838: "talnet", + 1839: "netopia-vo1", + 1840: "netopia-vo2", + 1841: "netopia-vo3", + 1842: "netopia-vo4", + 1843: "netopia-vo5", + 1844: "direcpc-dll", + 1845: "altalink", + 1846: "tunstall-pnc", + 1847: "slp-notify", + 1848: "fjdocdist", + 1849: "alpha-sms", + 1850: "gsi", + 1851: "ctcd", + 1852: "virtual-time", + 1853: "vids-avtp", + 1854: "buddy-draw", + 1855: "fiorano-rtrsvc", + 1856: "fiorano-msgsvc", + 1857: "datacaptor", + 1858: "privateark", + 1859: "gammafetchsvr", + 1860: "sunscalar-svc", + 1861: "lecroy-vicp", + 1862: "mysql-cm-agent", + 1863: "msnp", + 1864: "paradym-31port", + 1865: "entp", + 1866: "swrmi", + 1867: "udrive", + 1868: "viziblebrowser", + 1869: "transact", + 1870: "sunscalar-dns", + 1871: "canocentral0", + 1872: "canocentral1", + 1873: "fjmpjps", + 1874: "fjswapsnp", + 1875: "westell-stats", + 1876: "ewcappsrv", + 1877: "hp-webqosdb", + 1878: "drmsmc", + 1879: "nettgain-nms", + 1880: "vsat-control", + 1881: "ibm-mqseries2", + 1882: "ecsqdmn", + 1883: "mqtt", + 1884: "idmaps", + 1885: "vrtstrapserver", + 1886: "leoip", + 1887: "filex-lport", + 1888: "ncconfig", + 1889: "unify-adapter", + 1890: "wilkenlistener", + 1891: "childkey-notif", + 1892: "childkey-ctrl", + 1893: "elad", + 1894: "o2server-port", + 1896: "b-novative-ls", + 1897: "metaagent", + 1898: "cymtec-port", + 1899: "mc2studios", + 1900: "ssdp", + 1901: "fjicl-tep-a", + 1902: "fjicl-tep-b", + 1903: "linkname", + 1904: "fjicl-tep-c", + 1905: "sugp", + 1906: "tpmd", + 1907: "intrastar", + 1908: "dawn", + 1909: "global-wlink", + 1910: "ultrabac", + 1911: "mtp", + 1912: "rhp-iibp", + 1913: "armadp", + 1914: "elm-momentum", + 1915: "facelink", + 1916: "persona", + 1917: "noagent", + 1918: "can-nds", + 1919: "can-dch", + 1920: "can-ferret", + 1921: "noadmin", + 1922: "tapestry", + 1923: "spice", + 1924: "xiip", + 1925: "discovery-port", + 1926: "egs", + 1927: "videte-cipc", + 1928: "emsd-port", + 1929: "bandwiz-system", + 1930: "driveappserver", + 1931: "amdsched", + 1932: "ctt-broker", + 1933: "xmapi", + 1934: "xaapi", + 1935: "macromedia-fcs", + 1936: "jetcmeserver", + 1937: "jwserver", + 1938: "jwclient", + 1939: "jvserver", + 1940: "jvclient", + 1941: "dic-aida", + 1942: "res", + 1943: "beeyond-media", + 1944: "close-combat", + 1945: "dialogic-elmd", + 1946: "tekpls", + 1947: "sentinelsrm", + 1948: "eye2eye", + 1949: "ismaeasdaqlive", + 1950: "ismaeasdaqtest", + 1951: "bcs-lmserver", + 1952: "mpnjsc", + 1953: "rapidbase", + 1954: "abr-api", + 1955: "abr-secure", + 1956: "vrtl-vmf-ds", + 1957: "unix-status", + 1958: "dxadmind", + 1959: "simp-all", + 1960: "nasmanager", + 1961: "bts-appserver", + 1962: "biap-mp", + 1963: "webmachine", + 1964: "solid-e-engine", + 1965: "tivoli-npm", + 1966: "slush", + 1967: "sns-quote", + 1968: "lipsinc", + 1969: "lipsinc1", + 1970: "netop-rc", + 1971: "netop-school", + 1972: "intersys-cache", + 1973: "dlsrap", + 1974: "drp", + 1975: "tcoflashagent", + 1976: "tcoregagent", + 1977: "tcoaddressbook", + 1978: "unisql", + 1979: "unisql-java", + 1980: "pearldoc-xact", + 1981: "p2pq", + 1982: "estamp", + 1983: "lhtp", + 1984: "bb", + 1985: "hsrp", + 1986: "licensedaemon", + 1987: "tr-rsrb-p1", + 1988: "tr-rsrb-p2", + 1989: "tr-rsrb-p3", + 1990: "stun-p1", + 1991: "stun-p2", + 1992: "stun-p3", + 1993: "snmp-tcp-port", + 1994: "stun-port", + 1995: "perf-port", + 1996: "tr-rsrb-port", + 1997: "gdp-port", + 1998: "x25-svc-port", + 1999: "tcp-id-port", + 2000: "cisco-sccp", + 2001: "wizard", + 2002: "globe", + 2003: "brutus", + 2004: "emce", + 2005: "oracle", + 2006: "raid-cd", + 2007: "raid-am", + 2008: "terminaldb", + 2009: "whosockami", + 2010: "pipe-server", + 2011: "servserv", + 2012: "raid-ac", + 2013: "raid-cd", + 2014: "raid-sf", + 2015: "raid-cs", + 2016: "bootserver", + 2017: "bootclient", + 2018: "rellpack", + 2019: "about", + 2020: "xinupageserver", + 2021: "xinuexpansion1", + 2022: "xinuexpansion2", + 2023: "xinuexpansion3", + 2024: "xinuexpansion4", + 2025: "xribs", + 2026: "scrabble", + 2027: "shadowserver", + 2028: "submitserver", + 2029: "hsrpv6", + 2030: "device2", + 2031: "mobrien-chat", + 2032: "blackboard", + 2033: "glogger", + 2034: "scoremgr", + 2035: "imsldoc", + 2036: "e-dpnet", + 2037: "applus", + 2038: "objectmanager", + 2039: "prizma", + 2040: "lam", + 2041: "interbase", + 2042: "isis", + 2043: "isis-bcast", + 2044: "rimsl", + 2045: "cdfunc", + 2046: "sdfunc", + 2047: "dls", + 2048: "dls-monitor", + 2049: "shilp", + 2050: "av-emb-config", + 2051: "epnsdp", + 2052: "clearvisn", + 2053: "lot105-ds-upd", + 2054: "weblogin", + 2055: "iop", + 2056: "omnisky", + 2057: "rich-cp", + 2058: "newwavesearch", + 2059: "bmc-messaging", + 2060: "teleniumdaemon", + 2061: "netmount", + 2062: "icg-swp", + 2063: "icg-bridge", + 2064: "icg-iprelay", + 2065: "dlsrpn", + 2066: "aura", + 2067: "dlswpn", + 2068: "avauthsrvprtcl", + 2069: "event-port", + 2070: "ah-esp-encap", + 2071: "acp-port", + 2072: "msync", + 2073: "gxs-data-port", + 2074: "vrtl-vmf-sa", + 2075: "newlixengine", + 2076: "newlixconfig", + 2077: "tsrmagt", + 2078: "tpcsrvr", + 2079: "idware-router", + 2080: "autodesk-nlm", + 2081: "kme-trap-port", + 2082: "infowave", + 2083: "radsec", + 2084: "sunclustergeo", + 2085: "ada-cip", + 2086: "gnunet", + 2087: "eli", + 2088: "ip-blf", + 2089: "sep", + 2090: "lrp", + 2091: "prp", + 2092: "descent3", + 2093: "nbx-cc", + 2094: "nbx-au", + 2095: "nbx-ser", + 2096: "nbx-dir", + 2097: "jetformpreview", + 2098: "dialog-port", + 2099: "h2250-annex-g", + 2100: "amiganetfs", + 2101: "rtcm-sc104", + 2102: "zephyr-srv", + 2103: "zephyr-clt", + 2104: "zephyr-hm", + 2105: "minipay", + 2106: "mzap", + 2107: "bintec-admin", + 2108: "comcam", + 2109: "ergolight", + 2110: "umsp", + 2111: "dsatp", + 2112: "idonix-metanet", + 2113: "hsl-storm", + 2114: "newheights", + 2115: "kdm", + 2116: "ccowcmr", + 2117: "mentaclient", + 2118: "mentaserver", + 2119: "gsigatekeeper", + 2120: "qencp", + 2121: "scientia-ssdb", + 2122: "caupc-remote", + 2123: "gtp-control", + 2124: "elatelink", + 2125: "lockstep", + 2126: "pktcable-cops", + 2127: "index-pc-wb", + 2128: "net-steward", + 2129: "cs-live", + 2130: "xds", + 2131: "avantageb2b", + 2132: "solera-epmap", + 2133: "zymed-zpp", + 2134: "avenue", + 2135: "gris", + 2136: "appworxsrv", + 2137: "connect", + 2138: "unbind-cluster", + 2139: "ias-auth", + 2140: "ias-reg", + 2141: "ias-admind", + 2142: "tdmoip", + 2143: "lv-jc", + 2144: "lv-ffx", + 2145: "lv-pici", + 2146: "lv-not", + 2147: "lv-auth", + 2148: "veritas-ucl", + 2149: "acptsys", + 2150: "dynamic3d", + 2151: "docent", + 2152: "gtp-user", + 2153: "ctlptc", + 2154: "stdptc", + 2155: "brdptc", + 2156: "trp", + 2157: "xnds", + 2158: "touchnetplus", + 2159: "gdbremote", + 2160: "apc-2160", + 2161: "apc-2161", + 2162: "navisphere", + 2163: "navisphere-sec", + 2164: "ddns-v3", + 2165: "x-bone-api", + 2166: "iwserver", + 2167: "raw-serial", + 2168: "easy-soft-mux", + 2169: "brain", + 2170: "eyetv", + 2171: "msfw-storage", + 2172: "msfw-s-storage", + 2173: "msfw-replica", + 2174: "msfw-array", + 2175: "airsync", + 2176: "rapi", + 2177: "qwave", + 2178: "bitspeer", + 2179: "vmrdp", + 2180: "mc-gt-srv", + 2181: "eforward", + 2182: "cgn-stat", + 2183: "cgn-config", + 2184: "nvd", + 2185: "onbase-dds", + 2186: "gtaua", + 2187: "ssmd", + 2190: "tivoconnect", + 2191: "tvbus", + 2192: "asdis", + 2193: "drwcs", + 2197: "mnp-exchange", + 2198: "onehome-remote", + 2199: "onehome-help", + 2200: "ici", + 2201: "ats", + 2202: "imtc-map", + 2203: "b2-runtime", + 2204: "b2-license", + 2205: "jps", + 2206: "hpocbus", + 2207: "hpssd", + 2208: "hpiod", + 2209: "rimf-ps", + 2210: "noaaport", + 2211: "emwin", + 2212: "leecoposserver", + 2213: "kali", + 2214: "rpi", + 2215: "ipcore", + 2216: "vtu-comms", + 2217: "gotodevice", + 2218: "bounzza", + 2219: "netiq-ncap", + 2220: "netiq", + 2221: "ethernet-ip-s", + 2222: "EtherNet-IP-1", + 2223: "rockwell-csp2", + 2224: "efi-mg", + 2226: "di-drm", + 2227: "di-msg", + 2228: "ehome-ms", + 2229: "datalens", + 2230: "queueadm", + 2231: "wimaxasncp", + 2232: "ivs-video", + 2233: "infocrypt", + 2234: "directplay", + 2235: "sercomm-wlink", + 2236: "nani", + 2237: "optech-port1-lm", + 2238: "aviva-sna", + 2239: "imagequery", + 2240: "recipe", + 2241: "ivsd", + 2242: "foliocorp", + 2243: "magicom", + 2244: "nmsserver", + 2245: "hao", + 2246: "pc-mta-addrmap", + 2247: "antidotemgrsvr", + 2248: "ums", + 2249: "rfmp", + 2250: "remote-collab", + 2251: "dif-port", + 2252: "njenet-ssl", + 2253: "dtv-chan-req", + 2254: "seispoc", + 2255: "vrtp", + 2256: "pcc-mfp", + 2257: "simple-tx-rx", + 2258: "rcts", + 2260: "apc-2260", + 2261: "comotionmaster", + 2262: "comotionback", + 2263: "ecwcfg", + 2264: "apx500api-1", + 2265: "apx500api-2", + 2266: "mfserver", + 2267: "ontobroker", + 2268: "amt", + 2269: "mikey", + 2270: "starschool", + 2271: "mmcals", + 2272: "mmcal", + 2273: "mysql-im", + 2274: "pcttunnell", + 2275: "ibridge-data", + 2276: "ibridge-mgmt", + 2277: "bluectrlproxy", + 2278: "s3db", + 2279: "xmquery", + 2280: "lnvpoller", + 2281: "lnvconsole", + 2282: "lnvalarm", + 2283: "lnvstatus", + 2284: "lnvmaps", + 2285: "lnvmailmon", + 2286: "nas-metering", + 2287: "dna", + 2288: "netml", + 2289: "dict-lookup", + 2290: "sonus-logging", + 2291: "eapsp", + 2292: "mib-streaming", + 2293: "npdbgmngr", + 2294: "konshus-lm", + 2295: "advant-lm", + 2296: "theta-lm", + 2297: "d2k-datamover1", + 2298: "d2k-datamover2", + 2299: "pc-telecommute", + 2300: "cvmmon", + 2301: "cpq-wbem", + 2302: "binderysupport", + 2303: "proxy-gateway", + 2304: "attachmate-uts", + 2305: "mt-scaleserver", + 2306: "tappi-boxnet", + 2307: "pehelp", + 2308: "sdhelp", + 2309: "sdserver", + 2310: "sdclient", + 2311: "messageservice", + 2312: "wanscaler", + 2313: "iapp", + 2314: "cr-websystems", + 2315: "precise-sft", + 2316: "sent-lm", + 2317: "attachmate-g32", + 2318: "cadencecontrol", + 2319: "infolibria", + 2320: "siebel-ns", + 2321: "rdlap", + 2322: "ofsd", + 2323: "3d-nfsd", + 2324: "cosmocall", + 2325: "ansysli", + 2326: "idcp", + 2327: "xingcsm", + 2328: "netrix-sftm", + 2329: "nvd", + 2330: "tscchat", + 2331: "agentview", + 2332: "rcc-host", + 2333: "snapp", + 2334: "ace-client", + 2335: "ace-proxy", + 2336: "appleugcontrol", + 2337: "ideesrv", + 2338: "norton-lambert", + 2339: "3com-webview", + 2340: "wrs-registry", + 2341: "xiostatus", + 2342: "manage-exec", + 2343: "nati-logos", + 2344: "fcmsys", + 2345: "dbm", + 2346: "redstorm-join", + 2347: "redstorm-find", + 2348: "redstorm-info", + 2349: "redstorm-diag", + 2350: "psbserver", + 2351: "psrserver", + 2352: "pslserver", + 2353: "pspserver", + 2354: "psprserver", + 2355: "psdbserver", + 2356: "gxtelmd", + 2357: "unihub-server", + 2358: "futrix", + 2359: "flukeserver", + 2360: "nexstorindltd", + 2361: "tl1", + 2362: "digiman", + 2363: "mediacntrlnfsd", + 2364: "oi-2000", + 2365: "dbref", + 2366: "qip-login", + 2367: "service-ctrl", + 2368: "opentable", + 2370: "l3-hbmon", + 2372: "lanmessenger", + 2381: "compaq-https", + 2382: "ms-olap3", + 2383: "ms-olap4", + 2384: "sd-capacity", + 2385: "sd-data", + 2386: "virtualtape", + 2387: "vsamredirector", + 2388: "mynahautostart", + 2389: "ovsessionmgr", + 2390: "rsmtp", + 2391: "3com-net-mgmt", + 2392: "tacticalauth", + 2393: "ms-olap1", + 2394: "ms-olap2", + 2395: "lan900-remote", + 2396: "wusage", + 2397: "ncl", + 2398: "orbiter", + 2399: "fmpro-fdal", + 2400: "opequus-server", + 2401: "cvspserver", + 2402: "taskmaster2000", + 2403: "taskmaster2000", + 2404: "iec-104", + 2405: "trc-netpoll", + 2406: "jediserver", + 2407: "orion", + 2409: "sns-protocol", + 2410: "vrts-registry", + 2411: "netwave-ap-mgmt", + 2412: "cdn", + 2413: "orion-rmi-reg", + 2414: "beeyond", + 2415: "codima-rtp", + 2416: "rmtserver", + 2417: "composit-server", + 2418: "cas", + 2419: "attachmate-s2s", + 2420: "dslremote-mgmt", + 2421: "g-talk", + 2422: "crmsbits", + 2423: "rnrp", + 2424: "kofax-svr", + 2425: "fjitsuappmgr", + 2426: "vcmp", + 2427: "mgcp-gateway", + 2428: "ott", + 2429: "ft-role", + 2430: "venus", + 2431: "venus-se", + 2432: "codasrv", + 2433: "codasrv-se", + 2434: "pxc-epmap", + 2435: "optilogic", + 2436: "topx", + 2437: "unicontrol", + 2438: "msp", + 2439: "sybasedbsynch", + 2440: "spearway", + 2441: "pvsw-inet", + 2442: "netangel", + 2443: "powerclientcsf", + 2444: "btpp2sectrans", + 2445: "dtn1", + 2446: "bues-service", + 2447: "ovwdb", + 2448: "hpppssvr", + 2449: "ratl", + 2450: "netadmin", + 2451: "netchat", + 2452: "snifferclient", + 2453: "madge-ltd", + 2454: "indx-dds", + 2455: "wago-io-system", + 2456: "altav-remmgt", + 2457: "rapido-ip", + 2458: "griffin", + 2459: "community", + 2460: "ms-theater", + 2461: "qadmifoper", + 2462: "qadmifevent", + 2463: "lsi-raid-mgmt", + 2464: "direcpc-si", + 2465: "lbm", + 2466: "lbf", + 2467: "high-criteria", + 2468: "qip-msgd", + 2469: "mti-tcs-comm", + 2470: "taskman-port", + 2471: "seaodbc", + 2472: "c3", + 2473: "aker-cdp", + 2474: "vitalanalysis", + 2475: "ace-server", + 2476: "ace-svr-prop", + 2477: "ssm-cvs", + 2478: "ssm-cssps", + 2479: "ssm-els", + 2480: "powerexchange", + 2481: "giop", + 2482: "giop-ssl", + 2483: "ttc", + 2484: "ttc-ssl", + 2485: "netobjects1", + 2486: "netobjects2", + 2487: "pns", + 2488: "moy-corp", + 2489: "tsilb", + 2490: "qip-qdhcp", + 2491: "conclave-cpp", + 2492: "groove", + 2493: "talarian-mqs", + 2494: "bmc-ar", + 2495: "fast-rem-serv", + 2496: "dirgis", + 2497: "quaddb", + 2498: "odn-castraq", + 2499: "unicontrol", + 2500: "rtsserv", + 2501: "rtsclient", + 2502: "kentrox-prot", + 2503: "nms-dpnss", + 2504: "wlbs", + 2505: "ppcontrol", + 2506: "jbroker", + 2507: "spock", + 2508: "jdatastore", + 2509: "fjmpss", + 2510: "fjappmgrbulk", + 2511: "metastorm", + 2512: "citrixima", + 2513: "citrixadmin", + 2514: "facsys-ntp", + 2515: "facsys-router", + 2516: "maincontrol", + 2517: "call-sig-trans", + 2518: "willy", + 2519: "globmsgsvc", + 2520: "pvsw", + 2521: "adaptecmgr", + 2522: "windb", + 2523: "qke-llc-v3", + 2524: "optiwave-lm", + 2525: "ms-v-worlds", + 2526: "ema-sent-lm", + 2527: "iqserver", + 2528: "ncr-ccl", + 2529: "utsftp", + 2530: "vrcommerce", + 2531: "ito-e-gui", + 2532: "ovtopmd", + 2533: "snifferserver", + 2534: "combox-web-acc", + 2535: "madcap", + 2536: "btpp2audctr1", + 2537: "upgrade", + 2538: "vnwk-prapi", + 2539: "vsiadmin", + 2540: "lonworks", + 2541: "lonworks2", + 2542: "udrawgraph", + 2543: "reftek", + 2544: "novell-zen", + 2545: "sis-emt", + 2546: "vytalvaultbrtp", + 2547: "vytalvaultvsmp", + 2548: "vytalvaultpipe", + 2549: "ipass", + 2550: "ads", + 2551: "isg-uda-server", + 2552: "call-logging", + 2553: "efidiningport", + 2554: "vcnet-link-v10", + 2555: "compaq-wcp", + 2556: "nicetec-nmsvc", + 2557: "nicetec-mgmt", + 2558: "pclemultimedia", + 2559: "lstp", + 2560: "labrat", + 2561: "mosaixcc", + 2562: "delibo", + 2563: "cti-redwood", + 2564: "hp-3000-telnet", + 2565: "coord-svr", + 2566: "pcs-pcw", + 2567: "clp", + 2568: "spamtrap", + 2569: "sonuscallsig", + 2570: "hs-port", + 2571: "cecsvc", + 2572: "ibp", + 2573: "trustestablish", + 2574: "blockade-bpsp", + 2575: "hl7", + 2576: "tclprodebugger", + 2577: "scipticslsrvr", + 2578: "rvs-isdn-dcp", + 2579: "mpfoncl", + 2580: "tributary", + 2581: "argis-te", + 2582: "argis-ds", + 2583: "mon", + 2584: "cyaserv", + 2585: "netx-server", + 2586: "netx-agent", + 2587: "masc", + 2588: "privilege", + 2589: "quartus-tcl", + 2590: "idotdist", + 2591: "maytagshuffle", + 2592: "netrek", + 2593: "mns-mail", + 2594: "dts", + 2595: "worldfusion1", + 2596: "worldfusion2", + 2597: "homesteadglory", + 2598: "citriximaclient", + 2599: "snapd", + 2600: "hpstgmgr", + 2601: "discp-client", + 2602: "discp-server", + 2603: "servicemeter", + 2604: "nsc-ccs", + 2605: "nsc-posa", + 2606: "netmon", + 2607: "connection", + 2608: "wag-service", + 2609: "system-monitor", + 2610: "versa-tek", + 2611: "lionhead", + 2612: "qpasa-agent", + 2613: "smntubootstrap", + 2614: "neveroffline", + 2615: "firepower", + 2616: "appswitch-emp", + 2617: "cmadmin", + 2618: "priority-e-com", + 2619: "bruce", + 2620: "lpsrecommender", + 2621: "miles-apart", + 2622: "metricadbc", + 2623: "lmdp", + 2624: "aria", + 2625: "blwnkl-port", + 2626: "gbjd816", + 2627: "moshebeeri", + 2628: "dict", + 2629: "sitaraserver", + 2630: "sitaramgmt", + 2631: "sitaradir", + 2632: "irdg-post", + 2633: "interintelli", + 2634: "pk-electronics", + 2635: "backburner", + 2636: "solve", + 2637: "imdocsvc", + 2638: "sybaseanywhere", + 2639: "aminet", + 2640: "ami-control", + 2641: "hdl-srv", + 2642: "tragic", + 2643: "gte-samp", + 2644: "travsoft-ipx-t", + 2645: "novell-ipx-cmd", + 2646: "and-lm", + 2647: "syncserver", + 2648: "upsnotifyprot", + 2649: "vpsipport", + 2650: "eristwoguns", + 2651: "ebinsite", + 2652: "interpathpanel", + 2653: "sonus", + 2654: "corel-vncadmin", + 2655: "unglue", + 2656: "kana", + 2657: "sns-dispatcher", + 2658: "sns-admin", + 2659: "sns-query", + 2660: "gcmonitor", + 2661: "olhost", + 2662: "bintec-capi", + 2663: "bintec-tapi", + 2664: "patrol-mq-gm", + 2665: "patrol-mq-nm", + 2666: "extensis", + 2667: "alarm-clock-s", + 2668: "alarm-clock-c", + 2669: "toad", + 2670: "tve-announce", + 2671: "newlixreg", + 2672: "nhserver", + 2673: "firstcall42", + 2674: "ewnn", + 2675: "ttc-etap", + 2676: "simslink", + 2677: "gadgetgate1way", + 2678: "gadgetgate2way", + 2679: "syncserverssl", + 2680: "pxc-sapxom", + 2681: "mpnjsomb", + 2683: "ncdloadbalance", + 2684: "mpnjsosv", + 2685: "mpnjsocl", + 2686: "mpnjsomg", + 2687: "pq-lic-mgmt", + 2688: "md-cg-http", + 2689: "fastlynx", + 2690: "hp-nnm-data", + 2691: "itinternet", + 2692: "admins-lms", + 2694: "pwrsevent", + 2695: "vspread", + 2696: "unifyadmin", + 2697: "oce-snmp-trap", + 2698: "mck-ivpip", + 2699: "csoft-plusclnt", + 2700: "tqdata", + 2701: "sms-rcinfo", + 2702: "sms-xfer", + 2703: "sms-chat", + 2704: "sms-remctrl", + 2705: "sds-admin", + 2706: "ncdmirroring", + 2707: "emcsymapiport", + 2708: "banyan-net", + 2709: "supermon", + 2710: "sso-service", + 2711: "sso-control", + 2712: "aocp", + 2713: "raventbs", + 2714: "raventdm", + 2715: "hpstgmgr2", + 2716: "inova-ip-disco", + 2717: "pn-requester", + 2718: "pn-requester2", + 2719: "scan-change", + 2720: "wkars", + 2721: "smart-diagnose", + 2722: "proactivesrvr", + 2723: "watchdog-nt", + 2724: "qotps", + 2725: "msolap-ptp2", + 2726: "tams", + 2727: "mgcp-callagent", + 2728: "sqdr", + 2729: "tcim-control", + 2730: "nec-raidplus", + 2731: "fyre-messanger", + 2732: "g5m", + 2733: "signet-ctf", + 2734: "ccs-software", + 2735: "netiq-mc", + 2736: "radwiz-nms-srv", + 2737: "srp-feedback", + 2738: "ndl-tcp-ois-gw", + 2739: "tn-timing", + 2740: "alarm", + 2741: "tsb", + 2742: "tsb2", + 2743: "murx", + 2744: "honyaku", + 2745: "urbisnet", + 2746: "cpudpencap", + 2747: "fjippol-swrly", + 2748: "fjippol-polsvr", + 2749: "fjippol-cnsl", + 2750: "fjippol-port1", + 2751: "fjippol-port2", + 2752: "rsisysaccess", + 2753: "de-spot", + 2754: "apollo-cc", + 2755: "expresspay", + 2756: "simplement-tie", + 2757: "cnrp", + 2758: "apollo-status", + 2759: "apollo-gms", + 2760: "sabams", + 2761: "dicom-iscl", + 2762: "dicom-tls", + 2763: "desktop-dna", + 2764: "data-insurance", + 2765: "qip-audup", + 2766: "compaq-scp", + 2767: "uadtc", + 2768: "uacs", + 2769: "exce", + 2770: "veronica", + 2771: "vergencecm", + 2772: "auris", + 2773: "rbakcup1", + 2774: "rbakcup2", + 2775: "smpp", + 2776: "ridgeway1", + 2777: "ridgeway2", + 2778: "gwen-sonya", + 2779: "lbc-sync", + 2780: "lbc-control", + 2781: "whosells", + 2782: "everydayrc", + 2783: "aises", + 2784: "www-dev", + 2785: "aic-np", + 2786: "aic-oncrpc", + 2787: "piccolo", + 2788: "fryeserv", + 2789: "media-agent", + 2790: "plgproxy", + 2791: "mtport-regist", + 2792: "f5-globalsite", + 2793: "initlsmsad", + 2795: "livestats", + 2796: "ac-tech", + 2797: "esp-encap", + 2798: "tmesis-upshot", + 2799: "icon-discover", + 2800: "acc-raid", + 2801: "igcp", + 2802: "veritas-udp1", + 2803: "btprjctrl", + 2804: "dvr-esm", + 2805: "wta-wsp-s", + 2806: "cspuni", + 2807: "cspmulti", + 2808: "j-lan-p", + 2809: "corbaloc", + 2810: "netsteward", + 2811: "gsiftp", + 2812: "atmtcp", + 2813: "llm-pass", + 2814: "llm-csv", + 2815: "lbc-measure", + 2816: "lbc-watchdog", + 2817: "nmsigport", + 2818: "rmlnk", + 2819: "fc-faultnotify", + 2820: "univision", + 2821: "vrts-at-port", + 2822: "ka0wuc", + 2823: "cqg-netlan", + 2824: "cqg-netlan-1", + 2826: "slc-systemlog", + 2827: "slc-ctrlrloops", + 2828: "itm-lm", + 2829: "silkp1", + 2830: "silkp2", + 2831: "silkp3", + 2832: "silkp4", + 2833: "glishd", + 2834: "evtp", + 2835: "evtp-data", + 2836: "catalyst", + 2837: "repliweb", + 2838: "starbot", + 2839: "nmsigport", + 2840: "l3-exprt", + 2841: "l3-ranger", + 2842: "l3-hawk", + 2843: "pdnet", + 2844: "bpcp-poll", + 2845: "bpcp-trap", + 2846: "aimpp-hello", + 2847: "aimpp-port-req", + 2848: "amt-blc-port", + 2849: "fxp", + 2850: "metaconsole", + 2851: "webemshttp", + 2852: "bears-01", + 2853: "ispipes", + 2854: "infomover", + 2856: "cesdinv", + 2857: "simctlp", + 2858: "ecnp", + 2859: "activememory", + 2860: "dialpad-voice1", + 2861: "dialpad-voice2", + 2862: "ttg-protocol", + 2863: "sonardata", + 2864: "astromed-main", + 2865: "pit-vpn", + 2866: "iwlistener", + 2867: "esps-portal", + 2868: "npep-messaging", + 2869: "icslap", + 2870: "daishi", + 2871: "msi-selectplay", + 2872: "radix", + 2874: "dxmessagebase1", + 2875: "dxmessagebase2", + 2876: "sps-tunnel", + 2877: "bluelance", + 2878: "aap", + 2879: "ucentric-ds", + 2880: "synapse", + 2881: "ndsp", + 2882: "ndtp", + 2883: "ndnp", + 2884: "flashmsg", + 2885: "topflow", + 2886: "responselogic", + 2887: "aironetddp", + 2888: "spcsdlobby", + 2889: "rsom", + 2890: "cspclmulti", + 2891: "cinegrfx-elmd", + 2892: "snifferdata", + 2893: "vseconnector", + 2894: "abacus-remote", + 2895: "natuslink", + 2896: "ecovisiong6-1", + 2897: "citrix-rtmp", + 2898: "appliance-cfg", + 2899: "powergemplus", + 2900: "quicksuite", + 2901: "allstorcns", + 2902: "netaspi", + 2903: "suitcase", + 2904: "m2ua", + 2906: "caller9", + 2907: "webmethods-b2b", + 2908: "mao", + 2909: "funk-dialout", + 2910: "tdaccess", + 2911: "blockade", + 2912: "epicon", + 2913: "boosterware", + 2914: "gamelobby", + 2915: "tksocket", + 2916: "elvin-server", + 2917: "elvin-client", + 2918: "kastenchasepad", + 2919: "roboer", + 2920: "roboeda", + 2921: "cesdcdman", + 2922: "cesdcdtrn", + 2923: "wta-wsp-wtp-s", + 2924: "precise-vip", + 2926: "mobile-file-dl", + 2927: "unimobilectrl", + 2928: "redstone-cpss", + 2929: "amx-webadmin", + 2930: "amx-weblinx", + 2931: "circle-x", + 2932: "incp", + 2933: "4-tieropmgw", + 2934: "4-tieropmcli", + 2935: "qtp", + 2936: "otpatch", + 2937: "pnaconsult-lm", + 2938: "sm-pas-1", + 2939: "sm-pas-2", + 2940: "sm-pas-3", + 2941: "sm-pas-4", + 2942: "sm-pas-5", + 2943: "ttnrepository", + 2944: "megaco-h248", + 2945: "h248-binary", + 2946: "fjsvmpor", + 2947: "gpsd", + 2948: "wap-push", + 2949: "wap-pushsecure", + 2950: "esip", + 2951: "ottp", + 2952: "mpfwsas", + 2953: "ovalarmsrv", + 2954: "ovalarmsrv-cmd", + 2955: "csnotify", + 2956: "ovrimosdbman", + 2957: "jmact5", + 2958: "jmact6", + 2959: "rmopagt", + 2960: "dfoxserver", + 2961: "boldsoft-lm", + 2962: "iph-policy-cli", + 2963: "iph-policy-adm", + 2964: "bullant-srap", + 2965: "bullant-rap", + 2966: "idp-infotrieve", + 2967: "ssc-agent", + 2968: "enpp", + 2969: "essp", + 2970: "index-net", + 2971: "netclip", + 2972: "pmsm-webrctl", + 2973: "svnetworks", + 2974: "signal", + 2975: "fjmpcm", + 2976: "cns-srv-port", + 2977: "ttc-etap-ns", + 2978: "ttc-etap-ds", + 2979: "h263-video", + 2980: "wimd", + 2981: "mylxamport", + 2982: "iwb-whiteboard", + 2983: "netplan", + 2984: "hpidsadmin", + 2985: "hpidsagent", + 2986: "stonefalls", + 2987: "identify", + 2988: "hippad", + 2989: "zarkov", + 2990: "boscap", + 2991: "wkstn-mon", + 2992: "avenyo", + 2993: "veritas-vis1", + 2994: "veritas-vis2", + 2995: "idrs", + 2996: "vsixml", + 2997: "rebol", + 2998: "realsecure", + 2999: "remoteware-un", + 3000: "hbci", + 3002: "exlm-agent", + 3003: "cgms", + 3004: "csoftragent", + 3005: "geniuslm", + 3006: "ii-admin", + 3007: "lotusmtap", + 3008: "midnight-tech", + 3009: "pxc-ntfy", + 3010: "ping-pong", + 3011: "trusted-web", + 3012: "twsdss", + 3013: "gilatskysurfer", + 3014: "broker-service", + 3015: "nati-dstp", + 3016: "notify-srvr", + 3017: "event-listener", + 3018: "srvc-registry", + 3019: "resource-mgr", + 3020: "cifs", + 3021: "agriserver", + 3022: "csregagent", + 3023: "magicnotes", + 3024: "nds-sso", + 3025: "arepa-raft", + 3026: "agri-gateway", + 3027: "LiebDevMgmt-C", + 3028: "LiebDevMgmt-DM", + 3029: "LiebDevMgmt-A", + 3030: "arepa-cas", + 3031: "eppc", + 3032: "redwood-chat", + 3033: "pdb", + 3034: "osmosis-aeea", + 3035: "fjsv-gssagt", + 3036: "hagel-dump", + 3037: "hp-san-mgmt", + 3038: "santak-ups", + 3039: "cogitate", + 3040: "tomato-springs", + 3041: "di-traceware", + 3042: "journee", + 3043: "brp", + 3044: "epp", + 3045: "responsenet", + 3046: "di-ase", + 3047: "hlserver", + 3048: "pctrader", + 3049: "nsws", + 3050: "gds-db", + 3051: "galaxy-server", + 3052: "apc-3052", + 3053: "dsom-server", + 3054: "amt-cnf-prot", + 3055: "policyserver", + 3056: "cdl-server", + 3057: "goahead-fldup", + 3058: "videobeans", + 3059: "qsoft", + 3060: "interserver", + 3061: "cautcpd", + 3062: "ncacn-ip-tcp", + 3063: "ncadg-ip-udp", + 3064: "rprt", + 3065: "slinterbase", + 3066: "netattachsdmp", + 3067: "fjhpjp", + 3068: "ls3bcast", + 3069: "ls3", + 3070: "mgxswitch", + 3072: "csd-monitor", + 3073: "vcrp", + 3074: "xbox", + 3075: "orbix-locator", + 3076: "orbix-config", + 3077: "orbix-loc-ssl", + 3078: "orbix-cfg-ssl", + 3079: "lv-frontpanel", + 3080: "stm-pproc", + 3081: "tl1-lv", + 3082: "tl1-raw", + 3083: "tl1-telnet", + 3084: "itm-mccs", + 3085: "pcihreq", + 3086: "jdl-dbkitchen", + 3087: "asoki-sma", + 3088: "xdtp", + 3089: "ptk-alink", + 3090: "stss", + 3091: "1ci-smcs", + 3093: "rapidmq-center", + 3094: "rapidmq-reg", + 3095: "panasas", + 3096: "ndl-aps", + 3098: "umm-port", + 3099: "chmd", + 3100: "opcon-xps", + 3101: "hp-pxpib", + 3102: "slslavemon", + 3103: "autocuesmi", + 3104: "autocuetime", + 3105: "cardbox", + 3106: "cardbox-http", + 3107: "business", + 3108: "geolocate", + 3109: "personnel", + 3110: "sim-control", + 3111: "wsynch", + 3112: "ksysguard", + 3113: "cs-auth-svr", + 3114: "ccmad", + 3115: "mctet-master", + 3116: "mctet-gateway", + 3117: "mctet-jserv", + 3118: "pkagent", + 3119: "d2000kernel", + 3120: "d2000webserver", + 3122: "vtr-emulator", + 3123: "edix", + 3124: "beacon-port", + 3125: "a13-an", + 3127: "ctx-bridge", + 3128: "ndl-aas", + 3129: "netport-id", + 3130: "icpv2", + 3131: "netbookmark", + 3132: "ms-rule-engine", + 3133: "prism-deploy", + 3134: "ecp", + 3135: "peerbook-port", + 3136: "grubd", + 3137: "rtnt-1", + 3138: "rtnt-2", + 3139: "incognitorv", + 3140: "ariliamulti", + 3141: "vmodem", + 3142: "rdc-wh-eos", + 3143: "seaview", + 3144: "tarantella", + 3145: "csi-lfap", + 3146: "bears-02", + 3147: "rfio", + 3148: "nm-game-admin", + 3149: "nm-game-server", + 3150: "nm-asses-admin", + 3151: "nm-assessor", + 3152: "feitianrockey", + 3153: "s8-client-port", + 3154: "ccmrmi", + 3155: "jpegmpeg", + 3156: "indura", + 3157: "e3consultants", + 3158: "stvp", + 3159: "navegaweb-port", + 3160: "tip-app-server", + 3161: "doc1lm", + 3162: "sflm", + 3163: "res-sap", + 3164: "imprs", + 3165: "newgenpay", + 3166: "sossecollector", + 3167: "nowcontact", + 3168: "poweronnud", + 3169: "serverview-as", + 3170: "serverview-asn", + 3171: "serverview-gf", + 3172: "serverview-rm", + 3173: "serverview-icc", + 3174: "armi-server", + 3175: "t1-e1-over-ip", + 3176: "ars-master", + 3177: "phonex-port", + 3178: "radclientport", + 3179: "h2gf-w-2m", + 3180: "mc-brk-srv", + 3181: "bmcpatrolagent", + 3182: "bmcpatrolrnvu", + 3183: "cops-tls", + 3184: "apogeex-port", + 3185: "smpppd", + 3186: "iiw-port", + 3187: "odi-port", + 3188: "brcm-comm-port", + 3189: "pcle-infex", + 3190: "csvr-proxy", + 3191: "csvr-sslproxy", + 3192: "firemonrcc", + 3193: "spandataport", + 3194: "magbind", + 3195: "ncu-1", + 3196: "ncu-2", + 3197: "embrace-dp-s", + 3198: "embrace-dp-c", + 3199: "dmod-workspace", + 3200: "tick-port", + 3201: "cpq-tasksmart", + 3202: "intraintra", + 3203: "netwatcher-mon", + 3204: "netwatcher-db", + 3205: "isns", + 3206: "ironmail", + 3207: "vx-auth-port", + 3208: "pfu-prcallback", + 3209: "netwkpathengine", + 3210: "flamenco-proxy", + 3211: "avsecuremgmt", + 3212: "surveyinst", + 3213: "neon24x7", + 3214: "jmq-daemon-1", + 3215: "jmq-daemon-2", + 3216: "ferrari-foam", + 3217: "unite", + 3218: "smartpackets", + 3219: "wms-messenger", + 3220: "xnm-ssl", + 3221: "xnm-clear-text", + 3222: "glbp", + 3223: "digivote", + 3224: "aes-discovery", + 3225: "fcip-port", + 3226: "isi-irp", + 3227: "dwnmshttp", + 3228: "dwmsgserver", + 3229: "global-cd-port", + 3230: "sftdst-port", + 3231: "vidigo", + 3232: "mdtp", + 3233: "whisker", + 3234: "alchemy", + 3235: "mdap-port", + 3236: "apparenet-ts", + 3237: "apparenet-tps", + 3238: "apparenet-as", + 3239: "apparenet-ui", + 3240: "triomotion", + 3241: "sysorb", + 3242: "sdp-id-port", + 3243: "timelot", + 3244: "onesaf", + 3245: "vieo-fe", + 3246: "dvt-system", + 3247: "dvt-data", + 3248: "procos-lm", + 3249: "ssp", + 3250: "hicp", + 3251: "sysscanner", + 3252: "dhe", + 3253: "pda-data", + 3254: "pda-sys", + 3255: "semaphore", + 3256: "cpqrpm-agent", + 3257: "cpqrpm-server", + 3258: "ivecon-port", + 3259: "epncdp2", + 3260: "iscsi-target", + 3261: "winshadow", + 3262: "necp", + 3263: "ecolor-imager", + 3264: "ccmail", + 3265: "altav-tunnel", + 3266: "ns-cfg-server", + 3267: "ibm-dial-out", + 3268: "msft-gc", + 3269: "msft-gc-ssl", + 3270: "verismart", + 3271: "csoft-prev", + 3272: "user-manager", + 3273: "sxmp", + 3274: "ordinox-server", + 3275: "samd", + 3276: "maxim-asics", + 3277: "awg-proxy", + 3278: "lkcmserver", + 3279: "admind", + 3280: "vs-server", + 3281: "sysopt", + 3282: "datusorb", + 3283: "Apple Remote Desktop (Net Assistant)", + 3284: "4talk", + 3285: "plato", + 3286: "e-net", + 3287: "directvdata", + 3288: "cops", + 3289: "enpc", + 3290: "caps-lm", + 3291: "sah-lm", + 3292: "cart-o-rama", + 3293: "fg-fps", + 3294: "fg-gip", + 3295: "dyniplookup", + 3296: "rib-slm", + 3297: "cytel-lm", + 3298: "deskview", + 3299: "pdrncs", + 3302: "mcs-fastmail", + 3303: "opsession-clnt", + 3304: "opsession-srvr", + 3305: "odette-ftp", + 3306: "mysql", + 3307: "opsession-prxy", + 3308: "tns-server", + 3309: "tns-adv", + 3310: "dyna-access", + 3311: "mcns-tel-ret", + 3312: "appman-server", + 3313: "uorb", + 3314: "uohost", + 3315: "cdid", + 3316: "aicc-cmi", + 3317: "vsaiport", + 3318: "ssrip", + 3319: "sdt-lmd", + 3320: "officelink2000", + 3321: "vnsstr", + 3326: "sftu", + 3327: "bbars", + 3328: "egptlm", + 3329: "hp-device-disc", + 3330: "mcs-calypsoicf", + 3331: "mcs-messaging", + 3332: "mcs-mailsvr", + 3333: "dec-notes", + 3334: "directv-web", + 3335: "directv-soft", + 3336: "directv-tick", + 3337: "directv-catlg", + 3338: "anet-b", + 3339: "anet-l", + 3340: "anet-m", + 3341: "anet-h", + 3342: "webtie", + 3343: "ms-cluster-net", + 3344: "bnt-manager", + 3345: "influence", + 3346: "trnsprntproxy", + 3347: "phoenix-rpc", + 3348: "pangolin-laser", + 3349: "chevinservices", + 3350: "findviatv", + 3351: "btrieve", + 3352: "ssql", + 3353: "fatpipe", + 3354: "suitjd", + 3355: "ordinox-dbase", + 3356: "upnotifyps", + 3357: "adtech-test", + 3358: "mpsysrmsvr", + 3359: "wg-netforce", + 3360: "kv-server", + 3361: "kv-agent", + 3362: "dj-ilm", + 3363: "nati-vi-server", + 3364: "creativeserver", + 3365: "contentserver", + 3366: "creativepartnr", + 3372: "tip2", + 3373: "lavenir-lm", + 3374: "cluster-disc", + 3375: "vsnm-agent", + 3376: "cdbroker", + 3377: "cogsys-lm", + 3378: "wsicopy", + 3379: "socorfs", + 3380: "sns-channels", + 3381: "geneous", + 3382: "fujitsu-neat", + 3383: "esp-lm", + 3384: "hp-clic", + 3385: "qnxnetman", + 3386: "gprs-sig", + 3387: "backroomnet", + 3388: "cbserver", + 3389: "ms-wbt-server", + 3390: "dsc", + 3391: "savant", + 3392: "efi-lm", + 3393: "d2k-tapestry1", + 3394: "d2k-tapestry2", + 3395: "dyna-lm", + 3396: "printer-agent", + 3397: "cloanto-lm", + 3398: "mercantile", + 3399: "csms", + 3400: "csms2", + 3401: "filecast", + 3402: "fxaengine-net", + 3405: "nokia-ann-ch1", + 3406: "nokia-ann-ch2", + 3407: "ldap-admin", + 3408: "BESApi", + 3409: "networklens", + 3410: "networklenss", + 3411: "biolink-auth", + 3412: "xmlblaster", + 3413: "svnet", + 3414: "wip-port", + 3415: "bcinameservice", + 3416: "commandport", + 3417: "csvr", + 3418: "rnmap", + 3419: "softaudit", + 3420: "ifcp-port", + 3421: "bmap", + 3422: "rusb-sys-port", + 3423: "xtrm", + 3424: "xtrms", + 3425: "agps-port", + 3426: "arkivio", + 3427: "websphere-snmp", + 3428: "twcss", + 3429: "gcsp", + 3430: "ssdispatch", + 3431: "ndl-als", + 3432: "osdcp", + 3433: "opnet-smp", + 3434: "opencm", + 3435: "pacom", + 3436: "gc-config", + 3437: "autocueds", + 3438: "spiral-admin", + 3439: "hri-port", + 3440: "ans-console", + 3441: "connect-client", + 3442: "connect-server", + 3443: "ov-nnm-websrv", + 3444: "denali-server", + 3445: "monp", + 3446: "3comfaxrpc", + 3447: "directnet", + 3448: "dnc-port", + 3449: "hotu-chat", + 3450: "castorproxy", + 3451: "asam", + 3452: "sabp-signal", + 3453: "pscupd", + 3454: "mira", + 3455: "prsvp", + 3456: "vat", + 3457: "vat-control", + 3458: "d3winosfi", + 3459: "integral", + 3460: "edm-manager", + 3461: "edm-stager", + 3462: "edm-std-notify", + 3463: "edm-adm-notify", + 3464: "edm-mgr-sync", + 3465: "edm-mgr-cntrl", + 3466: "workflow", + 3467: "rcst", + 3468: "ttcmremotectrl", + 3469: "pluribus", + 3470: "jt400", + 3471: "jt400-ssl", + 3472: "jaugsremotec-1", + 3473: "jaugsremotec-2", + 3474: "ttntspauto", + 3475: "genisar-port", + 3476: "nppmp", + 3477: "ecomm", + 3478: "stun", + 3479: "twrpc", + 3480: "plethora", + 3481: "cleanerliverc", + 3482: "vulture", + 3483: "slim-devices", + 3484: "gbs-stp", + 3485: "celatalk", + 3486: "ifsf-hb-port", + 3487: "ltcudp", + 3488: "fs-rh-srv", + 3489: "dtp-dia", + 3490: "colubris", + 3491: "swr-port", + 3492: "tvdumtray-port", + 3493: "nut", + 3494: "ibm3494", + 3495: "seclayer-tcp", + 3496: "seclayer-tls", + 3497: "ipether232port", + 3498: "dashpas-port", + 3499: "sccip-media", + 3500: "rtmp-port", + 3501: "isoft-p2p", + 3502: "avinstalldisc", + 3503: "lsp-ping", + 3504: "ironstorm", + 3505: "ccmcomm", + 3506: "apc-3506", + 3507: "nesh-broker", + 3508: "interactionweb", + 3509: "vt-ssl", + 3510: "xss-port", + 3511: "webmail-2", + 3512: "aztec", + 3513: "arcpd", + 3514: "must-p2p", + 3515: "must-backplane", + 3516: "smartcard-port", + 3517: "802-11-iapp", + 3518: "artifact-msg", + 3519: "galileo", + 3520: "galileolog", + 3521: "mc3ss", + 3522: "nssocketport", + 3523: "odeumservlink", + 3524: "ecmport", + 3525: "eisport", + 3526: "starquiz-port", + 3527: "beserver-msg-q", + 3528: "jboss-iiop", + 3529: "jboss-iiop-ssl", + 3530: "gf", + 3531: "joltid", + 3532: "raven-rmp", + 3533: "raven-rdp", + 3534: "urld-port", + 3535: "ms-la", + 3536: "snac", + 3537: "ni-visa-remote", + 3538: "ibm-diradm", + 3539: "ibm-diradm-ssl", + 3540: "pnrp-port", + 3541: "voispeed-port", + 3542: "hacl-monitor", + 3543: "qftest-lookup", + 3544: "teredo", + 3545: "camac", + 3547: "symantec-sim", + 3548: "interworld", + 3549: "tellumat-nms", + 3550: "ssmpp", + 3551: "apcupsd", + 3552: "taserver", + 3553: "rbr-discovery", + 3554: "questnotify", + 3555: "razor", + 3556: "sky-transport", + 3557: "personalos-001", + 3558: "mcp-port", + 3559: "cctv-port", + 3560: "iniserve-port", + 3561: "bmc-onekey", + 3562: "sdbproxy", + 3563: "watcomdebug", + 3564: "esimport", + 3567: "dof-eps", + 3568: "dof-tunnel-sec", + 3569: "mbg-ctrl", + 3570: "mccwebsvr-port", + 3571: "megardsvr-port", + 3572: "megaregsvrport", + 3573: "tag-ups-1", + 3574: "dmaf-caster", + 3575: "ccm-port", + 3576: "cmc-port", + 3577: "config-port", + 3578: "data-port", + 3579: "ttat3lb", + 3580: "nati-svrloc", + 3581: "kfxaclicensing", + 3582: "press", + 3583: "canex-watch", + 3584: "u-dbap", + 3585: "emprise-lls", + 3586: "emprise-lsc", + 3587: "p2pgroup", + 3588: "sentinel", + 3589: "isomair", + 3590: "wv-csp-sms", + 3591: "gtrack-server", + 3592: "gtrack-ne", + 3593: "bpmd", + 3594: "mediaspace", + 3595: "shareapp", + 3596: "iw-mmogame", + 3597: "a14", + 3598: "a15", + 3599: "quasar-server", + 3600: "trap-daemon", + 3601: "visinet-gui", + 3602: "infiniswitchcl", + 3603: "int-rcv-cntrl", + 3604: "bmc-jmx-port", + 3605: "comcam-io", + 3606: "splitlock", + 3607: "precise-i3", + 3608: "trendchip-dcp", + 3609: "cpdi-pidas-cm", + 3610: "echonet", + 3611: "six-degrees", + 3612: "hp-dataprotect", + 3613: "alaris-disc", + 3614: "sigma-port", + 3615: "start-network", + 3616: "cd3o-protocol", + 3617: "sharp-server", + 3618: "aairnet-1", + 3619: "aairnet-2", + 3620: "ep-pcp", + 3621: "ep-nsp", + 3622: "ff-lr-port", + 3623: "haipe-discover", + 3624: "dist-upgrade", + 3625: "volley", + 3626: "bvcdaemon-port", + 3627: "jamserverport", + 3628: "ept-machine", + 3629: "escvpnet", + 3630: "cs-remote-db", + 3631: "cs-services", + 3632: "distcc", + 3633: "wacp", + 3634: "hlibmgr", + 3635: "sdo", + 3636: "servistaitsm", + 3637: "scservp", + 3638: "ehp-backup", + 3639: "xap-ha", + 3640: "netplay-port1", + 3641: "netplay-port2", + 3642: "juxml-port", + 3643: "audiojuggler", + 3644: "ssowatch", + 3645: "cyc", + 3646: "xss-srv-port", + 3647: "splitlock-gw", + 3648: "fjcp", + 3649: "nmmp", + 3650: "prismiq-plugin", + 3651: "xrpc-registry", + 3652: "vxcrnbuport", + 3653: "tsp", + 3654: "vaprtm", + 3655: "abatemgr", + 3656: "abatjss", + 3657: "immedianet-bcn", + 3658: "ps-ams", + 3659: "apple-sasl", + 3660: "can-nds-ssl", + 3661: "can-ferret-ssl", + 3662: "pserver", + 3663: "dtp", + 3664: "ups-engine", + 3665: "ent-engine", + 3666: "eserver-pap", + 3667: "infoexch", + 3668: "dell-rm-port", + 3669: "casanswmgmt", + 3670: "smile", + 3671: "efcp", + 3672: "lispworks-orb", + 3673: "mediavault-gui", + 3674: "wininstall-ipc", + 3675: "calltrax", + 3676: "va-pacbase", + 3677: "roverlog", + 3678: "ipr-dglt", + 3679: "Escale (Newton Dock)", + 3680: "npds-tracker", + 3681: "bts-x73", + 3682: "cas-mapi", + 3683: "bmc-ea", + 3684: "faxstfx-port", + 3685: "dsx-agent", + 3686: "tnmpv2", + 3687: "simple-push", + 3688: "simple-push-s", + 3689: "daap", + 3690: "svn", + 3691: "magaya-network", + 3692: "intelsync", + 3695: "bmc-data-coll", + 3696: "telnetcpcd", + 3697: "nw-license", + 3698: "sagectlpanel", + 3699: "kpn-icw", + 3700: "lrs-paging", + 3701: "netcelera", + 3702: "ws-discovery", + 3703: "adobeserver-3", + 3704: "adobeserver-4", + 3705: "adobeserver-5", + 3706: "rt-event", + 3707: "rt-event-s", + 3708: "sun-as-iiops", + 3709: "ca-idms", + 3710: "portgate-auth", + 3711: "edb-server2", + 3712: "sentinel-ent", + 3713: "tftps", + 3714: "delos-dms", + 3715: "anoto-rendezv", + 3716: "wv-csp-sms-cir", + 3717: "wv-csp-udp-cir", + 3718: "opus-services", + 3719: "itelserverport", + 3720: "ufastro-instr", + 3721: "xsync", + 3722: "xserveraid", + 3723: "sychrond", + 3724: "blizwow", + 3725: "na-er-tip", + 3726: "array-manager", + 3727: "e-mdu", + 3728: "e-woa", + 3729: "fksp-audit", + 3730: "client-ctrl", + 3731: "smap", + 3732: "m-wnn", + 3733: "multip-msg", + 3734: "synel-data", + 3735: "pwdis", + 3736: "rs-rmi", + 3738: "versatalk", + 3739: "launchbird-lm", + 3740: "heartbeat", + 3741: "wysdma", + 3742: "cst-port", + 3743: "ipcs-command", + 3744: "sasg", + 3745: "gw-call-port", + 3746: "linktest", + 3747: "linktest-s", + 3748: "webdata", + 3749: "cimtrak", + 3750: "cbos-ip-port", + 3751: "gprs-cube", + 3752: "vipremoteagent", + 3753: "nattyserver", + 3754: "timestenbroker", + 3755: "sas-remote-hlp", + 3756: "canon-capt", + 3757: "grf-port", + 3758: "apw-registry", + 3759: "exapt-lmgr", + 3760: "adtempusclient", + 3761: "gsakmp", + 3762: "gbs-smp", + 3763: "xo-wave", + 3764: "mni-prot-rout", + 3765: "rtraceroute", + 3767: "listmgr-port", + 3768: "rblcheckd", + 3769: "haipe-otnk", + 3770: "cindycollab", + 3771: "paging-port", + 3772: "ctp", + 3773: "ctdhercules", + 3774: "zicom", + 3775: "ispmmgr", + 3776: "dvcprov-port", + 3777: "jibe-eb", + 3778: "c-h-it-port", + 3779: "cognima", + 3780: "nnp", + 3781: "abcvoice-port", + 3782: "iso-tp0s", + 3783: "bim-pem", + 3784: "bfd-control", + 3785: "bfd-echo", + 3786: "upstriggervsw", + 3787: "fintrx", + 3788: "isrp-port", + 3789: "remotedeploy", + 3790: "quickbooksrds", + 3791: "tvnetworkvideo", + 3792: "sitewatch", + 3793: "dcsoftware", + 3794: "jaus", + 3795: "myblast", + 3796: "spw-dialer", + 3797: "idps", + 3798: "minilock", + 3799: "radius-dynauth", + 3800: "pwgpsi", + 3801: "ibm-mgr", + 3802: "vhd", + 3803: "soniqsync", + 3804: "iqnet-port", + 3805: "tcpdataserver", + 3806: "wsmlb", + 3807: "spugna", + 3808: "sun-as-iiops-ca", + 3809: "apocd", + 3810: "wlanauth", + 3811: "amp", + 3812: "neto-wol-server", + 3813: "rap-ip", + 3814: "neto-dcs", + 3815: "lansurveyorxml", + 3816: "sunlps-http", + 3817: "tapeware", + 3818: "crinis-hb", + 3819: "epl-slp", + 3820: "scp", + 3821: "pmcp", + 3822: "acp-discovery", + 3823: "acp-conduit", + 3824: "acp-policy", + 3825: "ffserver", + 3826: "warmux", + 3827: "netmpi", + 3828: "neteh", + 3829: "neteh-ext", + 3830: "cernsysmgmtagt", + 3831: "dvapps", + 3832: "xxnetserver", + 3833: "aipn-auth", + 3834: "spectardata", + 3835: "spectardb", + 3836: "markem-dcp", + 3837: "mkm-discovery", + 3838: "sos", + 3839: "amx-rms", + 3840: "flirtmitmir", + 3842: "nhci", + 3843: "quest-agent", + 3844: "rnm", + 3845: "v-one-spp", + 3846: "an-pcp", + 3847: "msfw-control", + 3848: "item", + 3849: "spw-dnspreload", + 3850: "qtms-bootstrap", + 3851: "spectraport", + 3852: "sse-app-config", + 3853: "sscan", + 3854: "stryker-com", + 3855: "opentrac", + 3856: "informer", + 3857: "trap-port", + 3858: "trap-port-mom", + 3859: "nav-port", + 3860: "sasp", + 3861: "winshadow-hd", + 3862: "giga-pocket", + 3863: "asap-udp", + 3865: "xpl", + 3866: "dzdaemon", + 3867: "dzoglserver", + 3869: "ovsam-mgmt", + 3870: "ovsam-d-agent", + 3871: "avocent-adsap", + 3872: "oem-agent", + 3873: "fagordnc", + 3874: "sixxsconfig", + 3875: "pnbscada", + 3876: "dl-agent", + 3877: "xmpcr-interface", + 3878: "fotogcad", + 3879: "appss-lm", + 3880: "igrs", + 3881: "idac", + 3882: "msdts1", + 3883: "vrpn", + 3884: "softrack-meter", + 3885: "topflow-ssl", + 3886: "nei-management", + 3887: "ciphire-data", + 3888: "ciphire-serv", + 3889: "dandv-tester", + 3890: "ndsconnect", + 3891: "rtc-pm-port", + 3892: "pcc-image-port", + 3893: "cgi-starapi", + 3894: "syam-agent", + 3895: "syam-smc", + 3896: "sdo-tls", + 3897: "sdo-ssh", + 3898: "senip", + 3899: "itv-control", + 3900: "udt-os", + 3901: "nimsh", + 3902: "nimaux", + 3903: "charsetmgr", + 3904: "omnilink-port", + 3905: "mupdate", + 3906: "topovista-data", + 3907: "imoguia-port", + 3908: "hppronetman", + 3909: "surfcontrolcpa", + 3910: "prnrequest", + 3911: "prnstatus", + 3912: "gbmt-stars", + 3913: "listcrt-port", + 3914: "listcrt-port-2", + 3915: "agcat", + 3916: "wysdmc", + 3917: "aftmux", + 3918: "pktcablemmcops", + 3919: "hyperip", + 3920: "exasoftport1", + 3921: "herodotus-net", + 3922: "sor-update", + 3923: "symb-sb-port", + 3924: "mpl-gprs-port", + 3925: "zmp", + 3926: "winport", + 3927: "natdataservice", + 3928: "netboot-pxe", + 3929: "smauth-port", + 3930: "syam-webserver", + 3931: "msr-plugin-port", + 3932: "dyn-site", + 3933: "plbserve-port", + 3934: "sunfm-port", + 3935: "sdp-portmapper", + 3936: "mailprox", + 3937: "dvbservdsc", + 3938: "dbcontrol-agent", + 3939: "aamp", + 3940: "xecp-node", + 3941: "homeportal-web", + 3942: "srdp", + 3943: "tig", + 3944: "sops", + 3945: "emcads", + 3946: "backupedge", + 3947: "ccp", + 3948: "apdap", + 3949: "drip", + 3950: "namemunge", + 3951: "pwgippfax", + 3952: "i3-sessionmgr", + 3953: "xmlink-connect", + 3954: "adrep", + 3955: "p2pcommunity", + 3956: "gvcp", + 3957: "mqe-broker", + 3958: "mqe-agent", + 3959: "treehopper", + 3960: "bess", + 3961: "proaxess", + 3962: "sbi-agent", + 3963: "thrp", + 3964: "sasggprs", + 3965: "ati-ip-to-ncpe", + 3966: "bflckmgr", + 3967: "ppsms", + 3968: "ianywhere-dbns", + 3969: "landmarks", + 3970: "lanrevagent", + 3971: "lanrevserver", + 3972: "iconp", + 3973: "progistics", + 3974: "citysearch", + 3975: "airshot", + 3976: "opswagent", + 3977: "opswmanager", + 3978: "secure-cfg-svr", + 3979: "smwan", + 3980: "acms", + 3981: "starfish", + 3982: "eis", + 3983: "eisp", + 3984: "mapper-nodemgr", + 3985: "mapper-mapethd", + 3986: "mapper-ws-ethd", + 3987: "centerline", + 3988: "dcs-config", + 3989: "bv-queryengine", + 3990: "bv-is", + 3991: "bv-smcsrv", + 3992: "bv-ds", + 3993: "bv-agent", + 3995: "iss-mgmt-ssl", + 3996: "abcsoftware", + 3997: "agentsease-db", + 3998: "dnx", + 3999: "nvcnet", + 4000: "terabase", + 4001: "newoak", + 4002: "pxc-spvr-ft", + 4003: "pxc-splr-ft", + 4004: "pxc-roid", + 4005: "pxc-pin", + 4006: "pxc-spvr", + 4007: "pxc-splr", + 4008: "netcheque", + 4009: "chimera-hwm", + 4010: "samsung-unidex", + 4011: "altserviceboot", + 4012: "pda-gate", + 4013: "acl-manager", + 4014: "taiclock", + 4015: "talarian-mcast1", + 4016: "talarian-mcast2", + 4017: "talarian-mcast3", + 4018: "talarian-mcast4", + 4019: "talarian-mcast5", + 4020: "trap", + 4021: "nexus-portal", + 4022: "dnox", + 4023: "esnm-zoning", + 4024: "tnp1-port", + 4025: "partimage", + 4026: "as-debug", + 4027: "bxp", + 4028: "dtserver-port", + 4029: "ip-qsig", + 4030: "jdmn-port", + 4031: "suucp", + 4032: "vrts-auth-port", + 4033: "sanavigator", + 4034: "ubxd", + 4035: "wap-push-http", + 4036: "wap-push-https", + 4037: "ravehd", + 4038: "fazzt-ptp", + 4039: "fazzt-admin", + 4040: "yo-main", + 4041: "houston", + 4042: "ldxp", + 4043: "nirp", + 4044: "ltp", + 4045: "npp", + 4046: "acp-proto", + 4047: "ctp-state", + 4049: "wafs", + 4050: "cisco-wafs", + 4051: "cppdp", + 4052: "interact", + 4053: "ccu-comm-1", + 4054: "ccu-comm-2", + 4055: "ccu-comm-3", + 4056: "lms", + 4057: "wfm", + 4058: "kingfisher", + 4059: "dlms-cosem", + 4060: "dsmeter-iatc", + 4061: "ice-location", + 4062: "ice-slocation", + 4063: "ice-router", + 4064: "ice-srouter", + 4065: "avanti-cdp", + 4066: "pmas", + 4067: "idp", + 4068: "ipfltbcst", + 4069: "minger", + 4070: "tripe", + 4071: "aibkup", + 4072: "zieto-sock", + 4073: "iRAPP", + 4074: "cequint-cityid", + 4075: "perimlan", + 4076: "seraph", + 4077: "ascomalarm", + 4079: "santools", + 4080: "lorica-in", + 4081: "lorica-in-sec", + 4082: "lorica-out", + 4083: "lorica-out-sec", + 4084: "fortisphere-vm", + 4086: "ftsync", + 4089: "opencore", + 4090: "omasgport", + 4091: "ewinstaller", + 4092: "ewdgs", + 4093: "pvxpluscs", + 4094: "sysrqd", + 4095: "xtgui", + 4096: "bre", + 4097: "patrolview", + 4098: "drmsfsd", + 4099: "dpcp", + 4100: "igo-incognito", + 4101: "brlp-0", + 4102: "brlp-1", + 4103: "brlp-2", + 4104: "brlp-3", + 4105: "shofar", + 4106: "synchronite", + 4107: "j-ac", + 4108: "accel", + 4109: "izm", + 4110: "g2tag", + 4111: "xgrid", + 4112: "apple-vpns-rp", + 4113: "aipn-reg", + 4114: "jomamqmonitor", + 4115: "cds", + 4116: "smartcard-tls", + 4117: "hillrserv", + 4118: "netscript", + 4119: "assuria-slm", + 4121: "e-builder", + 4122: "fprams", + 4123: "z-wave", + 4124: "tigv2", + 4125: "opsview-envoy", + 4126: "ddrepl", + 4127: "unikeypro", + 4128: "nufw", + 4129: "nuauth", + 4130: "fronet", + 4131: "stars", + 4132: "nuts-dem", + 4133: "nuts-bootp", + 4134: "nifty-hmi", + 4135: "cl-db-attach", + 4136: "cl-db-request", + 4137: "cl-db-remote", + 4138: "nettest", + 4139: "thrtx", + 4140: "cedros-fds", + 4141: "oirtgsvc", + 4142: "oidocsvc", + 4143: "oidsr", + 4145: "vvr-control", + 4146: "tgcconnect", + 4147: "vrxpservman", + 4148: "hhb-handheld", + 4149: "agslb", + 4150: "PowerAlert-nsa", + 4151: "menandmice-noh", + 4152: "idig-mux", + 4153: "mbl-battd", + 4154: "atlinks", + 4155: "bzr", + 4156: "stat-results", + 4157: "stat-scanner", + 4158: "stat-cc", + 4159: "nss", + 4160: "jini-discovery", + 4161: "omscontact", + 4162: "omstopology", + 4163: "silverpeakpeer", + 4164: "silverpeakcomm", + 4165: "altcp", + 4166: "joost", + 4167: "ddgn", + 4168: "pslicser", + 4169: "iadt-disc", + 4172: "pcoip", + 4173: "mma-discovery", + 4174: "sm-disc", + 4177: "wello", + 4178: "storman", + 4179: "MaxumSP", + 4180: "httpx", + 4181: "macbak", + 4182: "pcptcpservice", + 4183: "cyborgnet", + 4184: "universe-suite", + 4185: "wcpp", + 4188: "vatata", + 4191: "dsmipv6", + 4192: "azeti-bd", + 4197: "hctl", + 4199: "eims-admin", + 4300: "corelccam", + 4301: "d-data", + 4302: "d-data-control", + 4303: "srcp", + 4304: "owserver", + 4305: "batman", + 4306: "pinghgl", + 4307: "trueconf", + 4308: "compx-lockview", + 4309: "dserver", + 4310: "mirrtex", + 4320: "fdt-rcatp", + 4321: "rwhois", + 4322: "trim-event", + 4323: "trim-ice", + 4325: "geognosisman", + 4326: "geognosis", + 4327: "jaxer-web", + 4328: "jaxer-manager", + 4333: "ahsp", + 4340: "gaia", + 4341: "lisp-data", + 4342: "lisp-control", + 4343: "unicall", + 4344: "vinainstall", + 4345: "m4-network-as", + 4346: "elanlm", + 4347: "lansurveyor", + 4348: "itose", + 4349: "fsportmap", + 4350: "net-device", + 4351: "plcy-net-svcs", + 4352: "pjlink", + 4353: "f5-iquery", + 4354: "qsnet-trans", + 4355: "qsnet-workst", + 4356: "qsnet-assist", + 4357: "qsnet-cond", + 4358: "qsnet-nucl", + 4359: "omabcastltkm", + 4361: "nacnl", + 4362: "afore-vdp-disc", + 4366: "shadowstream", + 4368: "wxbrief", + 4369: "epmd", + 4370: "elpro-tunnel", + 4371: "l2c-disc", + 4372: "l2c-data", + 4373: "remctl", + 4375: "tolteces", + 4376: "bip", + 4377: "cp-spxsvr", + 4378: "cp-spxdpy", + 4379: "ctdb", + 4389: "xandros-cms", + 4390: "wiegand", + 4394: "apwi-disc", + 4395: "omnivisionesx", + 4400: "ds-srv", + 4401: "ds-srvr", + 4402: "ds-clnt", + 4403: "ds-user", + 4404: "ds-admin", + 4405: "ds-mail", + 4406: "ds-slp", + 4412: "smallchat", + 4413: "avi-nms-disc", + 4416: "pjj-player-disc", + 4418: "axysbridge", + 4420: "nvm-express", + 4425: "netrockey6", + 4426: "beacon-port-2", + 4430: "rsqlserver", + 4432: "l-acoustics", + 4441: "netblox", + 4442: "saris", + 4443: "pharos", + 4444: "krb524", + 4445: "upnotifyp", + 4446: "n1-fwp", + 4447: "n1-rmgmt", + 4448: "asc-slmd", + 4449: "privatewire", + 4450: "camp", + 4451: "ctisystemmsg", + 4452: "ctiprogramload", + 4453: "nssalertmgr", + 4454: "nssagentmgr", + 4455: "prchat-user", + 4456: "prchat-server", + 4457: "prRegister", + 4458: "mcp", + 4484: "hpssmgmt", + 4486: "icms", + 4488: "awacs-ice", + 4500: "ipsec-nat-t", + 4534: "armagetronad", + 4535: "ehs", + 4536: "ehs-ssl", + 4537: "wssauthsvc", + 4538: "swx-gate", + 4545: "worldscores", + 4546: "sf-lm", + 4547: "lanner-lm", + 4548: "synchromesh", + 4549: "aegate", + 4550: "gds-adppiw-db", + 4551: "ieee-mih", + 4552: "menandmice-mon", + 4554: "msfrs", + 4555: "rsip", + 4556: "dtn-bundle", + 4557: "mtcevrunqss", + 4558: "mtcevrunqman", + 4559: "hylafax", + 4566: "kwtc", + 4567: "tram", + 4568: "bmc-reporting", + 4569: "iax", + 4591: "l3t-at-an", + 4592: "hrpd-ith-at-an", + 4593: "ipt-anri-anri", + 4594: "ias-session", + 4595: "ias-paging", + 4596: "ias-neighbor", + 4597: "a21-an-1xbs", + 4598: "a16-an-an", + 4599: "a17-an-an", + 4600: "piranha1", + 4601: "piranha2", + 4621: "ventoso", + 4658: "playsta2-app", + 4659: "playsta2-lob", + 4660: "smaclmgr", + 4661: "kar2ouche", + 4662: "oms", + 4663: "noteit", + 4664: "ems", + 4665: "contclientms", + 4666: "eportcomm", + 4667: "mmacomm", + 4668: "mmaeds", + 4669: "eportcommdata", + 4670: "light", + 4671: "acter", + 4672: "rfa", + 4673: "cxws", + 4674: "appiq-mgmt", + 4675: "dhct-status", + 4676: "dhct-alerts", + 4677: "bcs", + 4678: "traversal", + 4679: "mgesupervision", + 4680: "mgemanagement", + 4681: "parliant", + 4682: "finisar", + 4683: "spike", + 4684: "rfid-rp1", + 4685: "autopac", + 4686: "msp-os", + 4687: "nst", + 4688: "mobile-p2p", + 4689: "altovacentral", + 4690: "prelude", + 4691: "mtn", + 4692: "conspiracy", + 4700: "netxms-agent", + 4701: "netxms-mgmt", + 4702: "netxms-sync", + 4711: "trinity-dist", + 4725: "truckstar", + 4726: "a26-fap-fgw", + 4727: "fcis-disc", + 4728: "capmux", + 4729: "gsmtap", + 4730: "gearman", + 4732: "ohmtrigger", + 4737: "ipdr-sp", + 4738: "solera-lpn", + 4739: "ipfix", + 4740: "ipfixs", + 4741: "lumimgrd", + 4742: "sicct-sdp", + 4743: "openhpid", + 4744: "ifsp", + 4745: "fmp", + 4746: "intelliadm-disc", + 4747: "buschtrommel", + 4749: "profilemac", + 4750: "ssad", + 4751: "spocp", + 4752: "snap", + 4753: "simon-disc", + 4754: "gre-in-udp", + 4755: "gre-udp-dtls", + 4784: "bfd-multi-ctl", + 4785: "cncp", + 4789: "vxlan", + 4790: "vxlan-gpe", + 4791: "roce", + 4800: "iims", + 4801: "iwec", + 4802: "ilss", + 4803: "notateit-disc", + 4804: "aja-ntv4-disc", + 4827: "htcp", + 4837: "varadero-0", + 4838: "varadero-1", + 4839: "varadero-2", + 4840: "opcua-udp", + 4841: "quosa", + 4842: "gw-asv", + 4843: "opcua-tls", + 4844: "gw-log", + 4845: "wcr-remlib", + 4846: "contamac-icm", + 4847: "wfc", + 4848: "appserv-http", + 4849: "appserv-https", + 4850: "sun-as-nodeagt", + 4851: "derby-repli", + 4867: "unify-debug", + 4868: "phrelay", + 4869: "phrelaydbg", + 4870: "cc-tracking", + 4871: "wired", + 4876: "tritium-can", + 4877: "lmcs", + 4878: "inst-discovery", + 4881: "socp-t", + 4882: "socp-c", + 4884: "hivestor", + 4885: "abbs", + 4894: "lyskom", + 4899: "radmin-port", + 4900: "hfcs", + 4914: "bones", + 4936: "an-signaling", + 4937: "atsc-mh-ssc", + 4940: "eq-office-4940", + 4941: "eq-office-4941", + 4942: "eq-office-4942", + 4949: "munin", + 4950: "sybasesrvmon", + 4951: "pwgwims", + 4952: "sagxtsds", + 4969: "ccss-qmm", + 4970: "ccss-qsm", + 4980: "ctxs-vpp", + 4986: "mrip", + 4987: "smar-se-port1", + 4988: "smar-se-port2", + 4989: "parallel", + 4990: "busycal", + 4991: "vrt", + 4999: "hfcs-manager", + 5000: "commplex-main", + 5001: "commplex-link", + 5002: "rfe", + 5003: "fmpro-internal", + 5004: "avt-profile-1", + 5005: "avt-profile-2", + 5006: "wsm-server", + 5007: "wsm-server-ssl", + 5008: "synapsis-edge", + 5009: "winfs", + 5010: "telelpathstart", + 5011: "telelpathattack", + 5012: "nsp", + 5013: "fmpro-v6", + 5014: "onpsocket", + 5020: "zenginkyo-1", + 5021: "zenginkyo-2", + 5022: "mice", + 5023: "htuilsrv", + 5024: "scpi-telnet", + 5025: "scpi-raw", + 5026: "strexec-d", + 5027: "strexec-s", + 5029: "infobright", + 5030: "surfpass", + 5031: "dmp", + 5042: "asnaacceler8db", + 5043: "swxadmin", + 5044: "lxi-evntsvc", + 5046: "vpm-udp", + 5047: "iscape", + 5049: "ivocalize", + 5050: "mmcc", + 5051: "ita-agent", + 5052: "ita-manager", + 5053: "rlm-disc", + 5055: "unot", + 5056: "intecom-ps1", + 5057: "intecom-ps2", + 5058: "locus-disc", + 5059: "sds", + 5060: "sip", + 5061: "sips", + 5062: "na-localise", + 5064: "ca-1", + 5065: "ca-2", + 5066: "stanag-5066", + 5067: "authentx", + 5069: "i-net-2000-npr", + 5070: "vtsas", + 5071: "powerschool", + 5072: "ayiya", + 5073: "tag-pm", + 5074: "alesquery", + 5078: "pixelpusher", + 5079: "cp-spxrpts", + 5080: "onscreen", + 5081: "sdl-ets", + 5082: "qcp", + 5083: "qfp", + 5084: "llrp", + 5085: "encrypted-llrp", + 5092: "magpie", + 5093: "sentinel-lm", + 5094: "hart-ip", + 5099: "sentlm-srv2srv", + 5100: "socalia", + 5101: "talarian-udp", + 5102: "oms-nonsecure", + 5104: "tinymessage", + 5105: "hughes-ap", + 5111: "taep-as-svc", + 5112: "pm-cmdsvr", + 5116: "emb-proj-cmd", + 5120: "barracuda-bbs", + 5133: "nbt-pc", + 5136: "minotaur-sa", + 5137: "ctsd", + 5145: "rmonitor-secure", + 5150: "atmp", + 5151: "esri-sde", + 5152: "sde-discovery", + 5154: "bzflag", + 5155: "asctrl-agent", + 5164: "vpa-disc", + 5165: "ife-icorp", + 5166: "winpcs", + 5167: "scte104", + 5168: "scte30", + 5190: "aol", + 5191: "aol-1", + 5192: "aol-2", + 5193: "aol-3", + 5200: "targus-getdata", + 5201: "targus-getdata1", + 5202: "targus-getdata2", + 5203: "targus-getdata3", + 5223: "hpvirtgrp", + 5224: "hpvirtctrl", + 5225: "hp-server", + 5226: "hp-status", + 5227: "perfd", + 5234: "eenet", + 5235: "galaxy-network", + 5236: "padl2sim", + 5237: "mnet-discovery", + 5245: "downtools-disc", + 5246: "capwap-control", + 5247: "capwap-data", + 5248: "caacws", + 5249: "caaclang2", + 5250: "soagateway", + 5251: "caevms", + 5252: "movaz-ssc", + 5264: "3com-njack-1", + 5265: "3com-njack-2", + 5270: "cartographerxmp", + 5271: "cuelink-disc", + 5272: "pk", + 5282: "transmit-port", + 5298: "presence", + 5299: "nlg-data", + 5300: "hacl-hb", + 5301: "hacl-gs", + 5302: "hacl-cfg", + 5303: "hacl-probe", + 5304: "hacl-local", + 5305: "hacl-test", + 5306: "sun-mc-grp", + 5307: "sco-aip", + 5308: "cfengine", + 5309: "jprinter", + 5310: "outlaws", + 5312: "permabit-cs", + 5313: "rrdp", + 5314: "opalis-rbt-ipc", + 5315: "hacl-poll", + 5343: "kfserver", + 5344: "xkotodrcp", + 5349: "stuns", + 5350: "pcp-multicast", + 5351: "pcp", + 5352: "dns-llq", + 5353: "mdns", + 5354: "mdnsresponder", + 5355: "llmnr", + 5356: "ms-smlbiz", + 5357: "wsdapi", + 5358: "wsdapi-s", + 5359: "ms-alerter", + 5360: "ms-sideshow", + 5361: "ms-s-sideshow", + 5362: "serverwsd2", + 5363: "net-projection", + 5364: "kdnet", + 5397: "stresstester", + 5398: "elektron-admin", + 5399: "securitychase", + 5400: "excerpt", + 5401: "excerpts", + 5402: "mftp", + 5403: "hpoms-ci-lstn", + 5404: "hpoms-dps-lstn", + 5405: "netsupport", + 5406: "systemics-sox", + 5407: "foresyte-clear", + 5408: "foresyte-sec", + 5409: "salient-dtasrv", + 5410: "salient-usrmgr", + 5411: "actnet", + 5412: "continuus", + 5413: "wwiotalk", + 5414: "statusd", + 5415: "ns-server", + 5416: "sns-gateway", + 5417: "sns-agent", + 5418: "mcntp", + 5419: "dj-ice", + 5420: "cylink-c", + 5421: "netsupport2", + 5422: "salient-mux", + 5423: "virtualuser", + 5424: "beyond-remote", + 5425: "br-channel", + 5426: "devbasic", + 5427: "sco-peer-tta", + 5428: "telaconsole", + 5429: "base", + 5430: "radec-corp", + 5431: "park-agent", + 5432: "postgresql", + 5433: "pyrrho", + 5434: "sgi-arrayd", + 5435: "sceanics", + 5436: "pmip6-cntl", + 5437: "pmip6-data", + 5443: "spss", + 5450: "tiepie-disc", + 5453: "surebox", + 5454: "apc-5454", + 5455: "apc-5455", + 5456: "apc-5456", + 5461: "silkmeter", + 5462: "ttl-publisher", + 5463: "ttlpriceproxy", + 5464: "quailnet", + 5465: "netops-broker", + 5474: "apsolab-rpc", + 5500: "fcp-addr-srvr1", + 5501: "fcp-addr-srvr2", + 5502: "fcp-srvr-inst1", + 5503: "fcp-srvr-inst2", + 5504: "fcp-cics-gw1", + 5505: "checkoutdb", + 5506: "amc", + 5553: "sgi-eventmond", + 5554: "sgi-esphttp", + 5555: "personal-agent", + 5556: "freeciv", + 5567: "dof-dps-mc-sec", + 5568: "sdt", + 5569: "rdmnet-device", + 5573: "sdmmp", + 5580: "tmosms0", + 5581: "tmosms1", + 5582: "fac-restore", + 5583: "tmo-icon-sync", + 5584: "bis-web", + 5585: "bis-sync", + 5597: "ininmessaging", + 5598: "mctfeed", + 5599: "esinstall", + 5600: "esmmanager", + 5601: "esmagent", + 5602: "a1-msc", + 5603: "a1-bs", + 5604: "a3-sdunode", + 5605: "a4-sdunode", + 5627: "ninaf", + 5628: "htrust", + 5629: "symantec-sfdb", + 5630: "precise-comm", + 5631: "pcanywheredata", + 5632: "pcanywherestat", + 5633: "beorl", + 5634: "xprtld", + 5670: "zre-disc", + 5671: "amqps", + 5672: "amqp", + 5673: "jms", + 5674: "hyperscsi-port", + 5675: "v5ua", + 5676: "raadmin", + 5677: "questdb2-lnchr", + 5678: "rrac", + 5679: "dccm", + 5680: "auriga-router", + 5681: "ncxcp", + 5682: "brightcore", + 5683: "coap", + 5684: "coaps", + 5687: "gog-multiplayer", + 5688: "ggz", + 5689: "qmvideo", + 5713: "proshareaudio", + 5714: "prosharevideo", + 5715: "prosharedata", + 5716: "prosharerequest", + 5717: "prosharenotify", + 5718: "dpm", + 5719: "dpm-agent", + 5720: "ms-licensing", + 5721: "dtpt", + 5722: "msdfsr", + 5723: "omhs", + 5724: "omsdk", + 5728: "io-dist-group", + 5729: "openmail", + 5730: "unieng", + 5741: "ida-discover1", + 5742: "ida-discover2", + 5743: "watchdoc-pod", + 5744: "watchdoc", + 5745: "fcopy-server", + 5746: "fcopys-server", + 5747: "tunatic", + 5748: "tunalyzer", + 5750: "rscd", + 5755: "openmailg", + 5757: "x500ms", + 5766: "openmailns", + 5767: "s-openmail", + 5768: "openmailpxy", + 5769: "spramsca", + 5770: "spramsd", + 5771: "netagent", + 5777: "dali-port", + 5781: "3par-evts", + 5782: "3par-mgmt", + 5783: "3par-mgmt-ssl", + 5784: "ibar", + 5785: "3par-rcopy", + 5786: "cisco-redu", + 5787: "waascluster", + 5793: "xtreamx", + 5794: "spdp", + 5813: "icmpd", + 5814: "spt-automation", + 5859: "wherehoo", + 5863: "ppsuitemsg", + 5900: "rfb", + 5910: "cm", + 5911: "cpdlc", + 5912: "fis", + 5913: "ads-c", + 5963: "indy", + 5968: "mppolicy-v5", + 5969: "mppolicy-mgr", + 5984: "couchdb", + 5985: "wsman", + 5986: "wsmans", + 5987: "wbem-rmi", + 5988: "wbem-http", + 5989: "wbem-https", + 5990: "wbem-exp-https", + 5991: "nuxsl", + 5992: "consul-insight", + 5999: "cvsup", + 6064: "ndl-ahp-svc", + 6065: "winpharaoh", + 6066: "ewctsp", + 6069: "trip", + 6070: "messageasap", + 6071: "ssdtp", + 6072: "diagnose-proc", + 6073: "directplay8", + 6074: "max", + 6080: "gue", + 6081: "geneve", + 6082: "p25cai", + 6083: "miami-bcast", + 6085: "konspire2b", + 6086: "pdtp", + 6087: "ldss", + 6088: "doglms-notify", + 6100: "synchronet-db", + 6101: "synchronet-rtc", + 6102: "synchronet-upd", + 6103: "rets", + 6104: "dbdb", + 6105: "primaserver", + 6106: "mpsserver", + 6107: "etc-control", + 6108: "sercomm-scadmin", + 6109: "globecast-id", + 6110: "softcm", + 6111: "spc", + 6112: "dtspcd", + 6118: "tipc", + 6122: "bex-webadmin", + 6123: "backup-express", + 6124: "pnbs", + 6133: "nbt-wol", + 6140: "pulsonixnls", + 6141: "meta-corp", + 6142: "aspentec-lm", + 6143: "watershed-lm", + 6144: "statsci1-lm", + 6145: "statsci2-lm", + 6146: "lonewolf-lm", + 6147: "montage-lm", + 6148: "ricardo-lm", + 6149: "tal-pod", + 6160: "ecmp-data", + 6161: "patrol-ism", + 6162: "patrol-coll", + 6163: "pscribe", + 6200: "lm-x", + 6201: "thermo-calc", + 6209: "qmtps", + 6222: "radmind", + 6241: "jeol-nsddp-1", + 6242: "jeol-nsddp-2", + 6243: "jeol-nsddp-3", + 6244: "jeol-nsddp-4", + 6251: "tl1-raw-ssl", + 6252: "tl1-ssh", + 6253: "crip", + 6268: "grid", + 6269: "grid-alt", + 6300: "bmc-grx", + 6301: "bmc-ctd-ldap", + 6306: "ufmp", + 6315: "scup-disc", + 6316: "abb-escp", + 6317: "nav-data", + 6320: "repsvc", + 6321: "emp-server1", + 6322: "emp-server2", + 6324: "hrd-ns-disc", + 6343: "sflow", + 6346: "gnutella-svc", + 6347: "gnutella-rtr", + 6350: "adap", + 6355: "pmcs", + 6360: "metaedit-mu", + 6363: "ndn", + 6370: "metaedit-se", + 6382: "metatude-mds", + 6389: "clariion-evr01", + 6390: "metaedit-ws", + 6417: "faxcomservice", + 6419: "svdrp-disc", + 6420: "nim-vdrshell", + 6421: "nim-wan", + 6443: "sun-sr-https", + 6444: "sge-qmaster", + 6445: "sge-execd", + 6446: "mysql-proxy", + 6455: "skip-cert-recv", + 6456: "skip-cert-send", + 6464: "ieee11073-20701", + 6471: "lvision-lm", + 6480: "sun-sr-http", + 6481: "servicetags", + 6482: "ldoms-mgmt", + 6483: "SunVTS-RMI", + 6484: "sun-sr-jms", + 6485: "sun-sr-iiop", + 6486: "sun-sr-iiops", + 6487: "sun-sr-iiop-aut", + 6488: "sun-sr-jmx", + 6489: "sun-sr-admin", + 6500: "boks", + 6501: "boks-servc", + 6502: "boks-servm", + 6503: "boks-clntd", + 6505: "badm-priv", + 6506: "badm-pub", + 6507: "bdir-priv", + 6508: "bdir-pub", + 6509: "mgcs-mfp-port", + 6510: "mcer-port", + 6511: "dccp-udp", + 6514: "syslog-tls", + 6515: "elipse-rec", + 6543: "lds-distrib", + 6544: "lds-dump", + 6547: "apc-6547", + 6548: "apc-6548", + 6549: "apc-6549", + 6550: "fg-sysupdate", + 6551: "sum", + 6558: "xdsxdm", + 6566: "sane-port", + 6568: "rp-reputation", + 6579: "affiliate", + 6580: "parsec-master", + 6581: "parsec-peer", + 6582: "parsec-game", + 6583: "joaJewelSuite", + 6619: "odette-ftps", + 6620: "kftp-data", + 6621: "kftp", + 6622: "mcftp", + 6623: "ktelnet", + 6626: "wago-service", + 6627: "nexgen", + 6628: "afesc-mc", + 6629: "nexgen-aux", + 6633: "cisco-vpath-tun", + 6634: "mpls-pm", + 6635: "mpls-udp", + 6636: "mpls-udp-dtls", + 6653: "openflow", + 6657: "palcom-disc", + 6670: "vocaltec-gold", + 6671: "p4p-portal", + 6672: "vision-server", + 6673: "vision-elmd", + 6678: "vfbp-disc", + 6679: "osaut", + 6689: "tsa", + 6696: "babel", + 6701: "kti-icad-srvr", + 6702: "e-design-net", + 6703: "e-design-web", + 6714: "ibprotocol", + 6715: "fibotrader-com", + 6767: "bmc-perf-agent", + 6768: "bmc-perf-mgrd", + 6769: "adi-gxp-srvprt", + 6770: "plysrv-http", + 6771: "plysrv-https", + 6784: "bfd-lag", + 6785: "dgpf-exchg", + 6786: "smc-jmx", + 6787: "smc-admin", + 6788: "smc-http", + 6790: "hnmp", + 6791: "hnm", + 6801: "acnet", + 6831: "ambit-lm", + 6841: "netmo-default", + 6842: "netmo-http", + 6850: "iccrushmore", + 6868: "acctopus-st", + 6888: "muse", + 6935: "ethoscan", + 6936: "xsmsvc", + 6946: "bioserver", + 6951: "otlp", + 6961: "jmact3", + 6962: "jmevt2", + 6963: "swismgr1", + 6964: "swismgr2", + 6965: "swistrap", + 6966: "swispol", + 6969: "acmsoda", + 6997: "MobilitySrv", + 6998: "iatp-highpri", + 6999: "iatp-normalpri", + 7000: "afs3-fileserver", + 7001: "afs3-callback", + 7002: "afs3-prserver", + 7003: "afs3-vlserver", + 7004: "afs3-kaserver", + 7005: "afs3-volser", + 7006: "afs3-errors", + 7007: "afs3-bos", + 7008: "afs3-update", + 7009: "afs3-rmtsys", + 7010: "ups-onlinet", + 7011: "talon-disc", + 7012: "talon-engine", + 7013: "microtalon-dis", + 7014: "microtalon-com", + 7015: "talon-webserver", + 7016: "spg", + 7017: "grasp", + 7019: "doceri-view", + 7020: "dpserve", + 7021: "dpserveadmin", + 7022: "ctdp", + 7023: "ct2nmcs", + 7024: "vmsvc", + 7025: "vmsvc-2", + 7030: "op-probe", + 7040: "quest-disc", + 7070: "arcp", + 7071: "iwg1", + 7080: "empowerid", + 7088: "zixi-transport", + 7095: "jdp-disc", + 7099: "lazy-ptop", + 7100: "font-service", + 7101: "elcn", + 7107: "aes-x170", + 7121: "virprot-lm", + 7128: "scenidm", + 7129: "scenccs", + 7161: "cabsm-comm", + 7162: "caistoragemgr", + 7163: "cacsambroker", + 7164: "fsr", + 7165: "doc-server", + 7166: "aruba-server", + 7169: "ccag-pib", + 7170: "nsrp", + 7171: "drm-production", + 7174: "clutild", + 7181: "janus-disc", + 7200: "fodms", + 7201: "dlip", + 7227: "ramp", + 7235: "aspcoordination", + 7244: "frc-hicp-disc", + 7262: "cnap", + 7272: "watchme-7272", + 7273: "oma-rlp", + 7274: "oma-rlp-s", + 7275: "oma-ulp", + 7276: "oma-ilp", + 7277: "oma-ilp-s", + 7278: "oma-dcdocbs", + 7279: "ctxlic", + 7280: "itactionserver1", + 7281: "itactionserver2", + 7282: "mzca-alert", + 7365: "lcm-server", + 7391: "mindfilesys", + 7392: "mrssrendezvous", + 7393: "nfoldman", + 7394: "fse", + 7395: "winqedit", + 7397: "hexarc", + 7400: "rtps-discovery", + 7401: "rtps-dd-ut", + 7402: "rtps-dd-mt", + 7410: "ionixnetmon", + 7411: "daqstream", + 7421: "mtportmon", + 7426: "pmdmgr", + 7427: "oveadmgr", + 7428: "ovladmgr", + 7429: "opi-sock", + 7430: "xmpv7", + 7431: "pmd", + 7437: "faximum", + 7443: "oracleas-https", + 7473: "rise", + 7491: "telops-lmd", + 7500: "silhouette", + 7501: "ovbus", + 7510: "ovhpas", + 7511: "pafec-lm", + 7542: "saratoga", + 7543: "atul", + 7544: "nta-ds", + 7545: "nta-us", + 7546: "cfs", + 7547: "cwmp", + 7548: "tidp", + 7549: "nls-tl", + 7550: "cloudsignaling", + 7560: "sncp", + 7566: "vsi-omega", + 7570: "aries-kfinder", + 7574: "coherence-disc", + 7588: "sun-lm", + 7606: "mipi-debug", + 7624: "indi", + 7627: "soap-http", + 7628: "zen-pawn", + 7629: "xdas", + 7633: "pmdfmgt", + 7648: "cuseeme", + 7674: "imqtunnels", + 7675: "imqtunnel", + 7676: "imqbrokerd", + 7677: "sun-user-https", + 7680: "pando-pub", + 7689: "collaber", + 7697: "klio", + 7707: "sync-em7", + 7708: "scinet", + 7720: "medimageportal", + 7724: "nsdeepfreezectl", + 7725: "nitrogen", + 7726: "freezexservice", + 7727: "trident-data", + 7728: "osvr", + 7734: "smip", + 7738: "aiagent", + 7741: "scriptview", + 7743: "sstp-1", + 7744: "raqmon-pdu", + 7747: "prgp", + 7777: "cbt", + 7778: "interwise", + 7779: "vstat", + 7781: "accu-lmgr", + 7784: "s-bfd", + 7786: "minivend", + 7787: "popup-reminders", + 7789: "office-tools", + 7794: "q3ade", + 7797: "pnet-conn", + 7798: "pnet-enc", + 7799: "altbsdp", + 7800: "asr", + 7801: "ssp-client", + 7802: "vns-tp", + 7810: "rbt-wanopt", + 7845: "apc-7845", + 7846: "apc-7846", + 7872: "mipv6tls", + 7880: "pss", + 7887: "ubroker", + 7900: "mevent", + 7901: "tnos-sp", + 7902: "tnos-dp", + 7903: "tnos-dps", + 7913: "qo-secure", + 7932: "t2-drm", + 7933: "t2-brm", + 7962: "generalsync", + 7967: "supercell", + 7979: "micromuse-ncps", + 7980: "quest-vista", + 7982: "sossd-disc", + 7998: "usicontentpush", + 7999: "irdmi2", + 8000: "irdmi", + 8001: "vcom-tunnel", + 8002: "teradataordbms", + 8003: "mcreport", + 8005: "mxi", + 8006: "wpl-disc", + 8007: "warppipe", + 8008: "http-alt", + 8019: "qbdb", + 8020: "intu-ec-svcdisc", + 8021: "intu-ec-client", + 8022: "oa-system", + 8025: "ca-audit-da", + 8026: "ca-audit-ds", + 8032: "pro-ed", + 8033: "mindprint", + 8034: "vantronix-mgmt", + 8040: "ampify", + 8041: "enguity-xccetp", + 8052: "senomix01", + 8053: "senomix02", + 8054: "senomix03", + 8055: "senomix04", + 8056: "senomix05", + 8057: "senomix06", + 8058: "senomix07", + 8059: "senomix08", + 8060: "aero", + 8074: "gadugadu", + 8080: "http-alt", + 8081: "sunproxyadmin", + 8082: "us-cli", + 8083: "us-srv", + 8086: "d-s-n", + 8087: "simplifymedia", + 8088: "radan-http", + 8097: "sac", + 8100: "xprint-server", + 8115: "mtl8000-matrix", + 8116: "cp-cluster", + 8118: "privoxy", + 8121: "apollo-data", + 8122: "apollo-admin", + 8128: "paycash-online", + 8129: "paycash-wbp", + 8130: "indigo-vrmi", + 8131: "indigo-vbcp", + 8132: "dbabble", + 8148: "isdd", + 8149: "eor-game", + 8160: "patrol", + 8161: "patrol-snmp", + 8182: "vmware-fdm", + 8184: "itach", + 8192: "spytechphone", + 8194: "blp1", + 8195: "blp2", + 8199: "vvr-data", + 8200: "trivnet1", + 8201: "trivnet2", + 8202: "aesop", + 8204: "lm-perfworks", + 8205: "lm-instmgr", + 8206: "lm-dta", + 8207: "lm-sserver", + 8208: "lm-webwatcher", + 8230: "rexecj", + 8231: "hncp-udp-port", + 8232: "hncp-dtls-port", + 8243: "synapse-nhttps", + 8276: "pando-sec", + 8280: "synapse-nhttp", + 8282: "libelle-disc", + 8292: "blp3", + 8294: "blp4", + 8300: "tmi", + 8301: "amberon", + 8320: "tnp-discover", + 8321: "tnp", + 8322: "garmin-marine", + 8351: "server-find", + 8376: "cruise-enum", + 8377: "cruise-swroute", + 8378: "cruise-config", + 8379: "cruise-diags", + 8380: "cruise-update", + 8383: "m2mservices", + 8384: "marathontp", + 8400: "cvd", + 8401: "sabarsd", + 8402: "abarsd", + 8403: "admind", + 8416: "espeech", + 8417: "espeech-rtp", + 8442: "cybro-a-bus", + 8443: "pcsync-https", + 8444: "pcsync-http", + 8445: "copy-disc", + 8450: "npmp", + 8472: "otv", + 8473: "vp2p", + 8474: "noteshare", + 8500: "fmtp", + 8501: "cmtp-av", + 8503: "lsp-self-ping", + 8554: "rtsp-alt", + 8555: "d-fence", + 8567: "dof-tunnel", + 8600: "asterix", + 8609: "canon-cpp-disc", + 8610: "canon-mfnp", + 8611: "canon-bjnp1", + 8612: "canon-bjnp2", + 8613: "canon-bjnp3", + 8614: "canon-bjnp4", + 8675: "msi-cps-rm-disc", + 8686: "sun-as-jmxrmi", + 8732: "dtp-net", + 8733: "ibus", + 8763: "mc-appserver", + 8764: "openqueue", + 8765: "ultraseek-http", + 8766: "amcs", + 8770: "dpap", + 8786: "msgclnt", + 8787: "msgsrvr", + 8793: "acd-pm", + 8800: "sunwebadmin", + 8804: "truecm", + 8805: "pfcp", + 8808: "ssports-bcast", + 8873: "dxspider", + 8880: "cddbp-alt", + 8883: "secure-mqtt", + 8888: "ddi-udp-1", + 8889: "ddi-udp-2", + 8890: "ddi-udp-3", + 8891: "ddi-udp-4", + 8892: "ddi-udp-5", + 8893: "ddi-udp-6", + 8894: "ddi-udp-7", + 8899: "ospf-lite", + 8900: "jmb-cds1", + 8901: "jmb-cds2", + 8910: "manyone-http", + 8911: "manyone-xml", + 8912: "wcbackup", + 8913: "dragonfly", + 8954: "cumulus-admin", + 8980: "nod-provider", + 8981: "nod-client", + 8989: "sunwebadmins", + 8990: "http-wmap", + 8991: "https-wmap", + 8999: "bctp", + 9000: "cslistener", + 9001: "etlservicemgr", + 9002: "dynamid", + 9007: "ogs-client", + 9009: "pichat", + 9020: "tambora", + 9021: "panagolin-ident", + 9022: "paragent", + 9023: "swa-1", + 9024: "swa-2", + 9025: "swa-3", + 9026: "swa-4", + 9060: "CardWeb-RT", + 9080: "glrpc", + 9084: "aurora", + 9085: "ibm-rsyscon", + 9086: "net2display", + 9087: "classic", + 9088: "sqlexec", + 9089: "sqlexec-ssl", + 9090: "websm", + 9091: "xmltec-xmlmail", + 9092: "XmlIpcRegSvc", + 9100: "hp-pdl-datastr", + 9101: "bacula-dir", + 9102: "bacula-fd", + 9103: "bacula-sd", + 9104: "peerwire", + 9105: "xadmin", + 9106: "astergate-disc", + 9119: "mxit", + 9131: "dddp", + 9160: "apani1", + 9161: "apani2", + 9162: "apani3", + 9163: "apani4", + 9164: "apani5", + 9191: "sun-as-jpda", + 9200: "wap-wsp", + 9201: "wap-wsp-wtp", + 9202: "wap-wsp-s", + 9203: "wap-wsp-wtp-s", + 9204: "wap-vcard", + 9205: "wap-vcal", + 9206: "wap-vcard-s", + 9207: "wap-vcal-s", + 9208: "rjcdb-vcards", + 9209: "almobile-system", + 9210: "oma-mlp", + 9211: "oma-mlp-s", + 9212: "serverviewdbms", + 9213: "serverstart", + 9214: "ipdcesgbs", + 9215: "insis", + 9216: "acme", + 9217: "fsc-port", + 9222: "teamcoherence", + 9255: "mon", + 9277: "traingpsdata", + 9278: "pegasus", + 9279: "pegasus-ctl", + 9280: "pgps", + 9281: "swtp-port1", + 9282: "swtp-port2", + 9283: "callwaveiam", + 9284: "visd", + 9285: "n2h2server", + 9286: "n2receive", + 9287: "cumulus", + 9292: "armtechdaemon", + 9293: "storview", + 9294: "armcenterhttp", + 9295: "armcenterhttps", + 9300: "vrace", + 9318: "secure-ts", + 9321: "guibase", + 9343: "mpidcmgr", + 9344: "mphlpdmc", + 9346: "ctechlicensing", + 9374: "fjdmimgr", + 9380: "boxp", + 9396: "fjinvmgr", + 9397: "mpidcagt", + 9400: "sec-t4net-srv", + 9401: "sec-t4net-clt", + 9402: "sec-pc2fax-srv", + 9418: "git", + 9443: "tungsten-https", + 9444: "wso2esb-console", + 9450: "sntlkeyssrvr", + 9500: "ismserver", + 9522: "sma-spw", + 9535: "mngsuite", + 9536: "laes-bf", + 9555: "trispen-sra", + 9592: "ldgateway", + 9593: "cba8", + 9594: "msgsys", + 9595: "pds", + 9596: "mercury-disc", + 9597: "pd-admin", + 9598: "vscp", + 9599: "robix", + 9600: "micromuse-ncpw", + 9612: "streamcomm-ds", + 9618: "condor", + 9628: "odbcpathway", + 9629: "uniport", + 9632: "mc-comm", + 9667: "xmms2", + 9668: "tec5-sdctp", + 9694: "client-wakeup", + 9695: "ccnx", + 9700: "board-roar", + 9747: "l5nas-parchan", + 9750: "board-voip", + 9753: "rasadv", + 9762: "tungsten-http", + 9800: "davsrc", + 9801: "sstp-2", + 9802: "davsrcs", + 9875: "sapv1", + 9878: "kca-service", + 9888: "cyborg-systems", + 9889: "gt-proxy", + 9898: "monkeycom", + 9899: "sctp-tunneling", + 9900: "iua", + 9901: "enrp", + 9903: "multicast-ping", + 9909: "domaintime", + 9911: "sype-transport", + 9950: "apc-9950", + 9951: "apc-9951", + 9952: "apc-9952", + 9953: "acis", + 9955: "alljoyn-mcm", + 9956: "alljoyn", + 9966: "odnsp", + 9987: "dsm-scm-target", + 9990: "osm-appsrvr", + 9991: "osm-oev", + 9992: "palace-1", + 9993: "palace-2", + 9994: "palace-3", + 9995: "palace-4", + 9996: "palace-5", + 9997: "palace-6", + 9998: "distinct32", + 9999: "distinct", + 10000: "ndmp", + 10001: "scp-config", + 10002: "documentum", + 10003: "documentum-s", + 10007: "mvs-capacity", + 10008: "octopus", + 10009: "swdtp-sv", + 10050: "zabbix-agent", + 10051: "zabbix-trapper", + 10080: "amanda", + 10081: "famdc", + 10100: "itap-ddtp", + 10101: "ezmeeting-2", + 10102: "ezproxy-2", + 10103: "ezrelay", + 10104: "swdtp", + 10107: "bctp-server", + 10110: "nmea-0183", + 10111: "nmea-onenet", + 10113: "netiq-endpoint", + 10114: "netiq-qcheck", + 10115: "netiq-endpt", + 10116: "netiq-voipa", + 10117: "iqrm", + 10128: "bmc-perf-sd", + 10160: "qb-db-server", + 10161: "snmpdtls", + 10162: "snmpdtls-trap", + 10200: "trisoap", + 10201: "rscs", + 10252: "apollo-relay", + 10253: "eapol-relay", + 10260: "axis-wimp-port", + 10288: "blocks", + 10439: "bngsync", + 10500: "hip-nat-t", + 10540: "MOS-lower", + 10541: "MOS-upper", + 10542: "MOS-aux", + 10543: "MOS-soap", + 10544: "MOS-soap-opt", + 10800: "gap", + 10805: "lpdg", + 10810: "nmc-disc", + 10860: "helix", + 10880: "bveapi", + 10990: "rmiaux", + 11000: "irisa", + 11001: "metasys", + 10023: "cefd-vmp", + 11095: "weave", + 11106: "sgi-lk", + 11108: "myq-termlink", + 11111: "vce", + 11112: "dicom", + 11161: "suncacao-snmp", + 11162: "suncacao-jmxmp", + 11163: "suncacao-rmi", + 11164: "suncacao-csa", + 11165: "suncacao-websvc", + 11171: "snss", + 11201: "smsqp", + 11208: "wifree", + 11211: "memcache", + 11319: "imip", + 11320: "imip-channels", + 11321: "arena-server", + 11367: "atm-uhas", + 11371: "hkp", + 11430: "lsdp", + 11600: "tempest-port", + 11720: "h323callsigalt", + 11723: "emc-xsw-dcache", + 11751: "intrepid-ssl", + 11796: "lanschool-mpt", + 11876: "xoraya", + 11877: "x2e-disc", + 11967: "sysinfo-sp", + 12000: "entextxid", + 12001: "entextnetwk", + 12002: "entexthigh", + 12003: "entextmed", + 12004: "entextlow", + 12005: "dbisamserver1", + 12006: "dbisamserver2", + 12007: "accuracer", + 12008: "accuracer-dbms", + 12009: "ghvpn", + 12012: "vipera", + 12013: "vipera-ssl", + 12109: "rets-ssl", + 12121: "nupaper-ss", + 12168: "cawas", + 12172: "hivep", + 12300: "linogridengine", + 12321: "warehouse-sss", + 12322: "warehouse", + 12345: "italk", + 12753: "tsaf", + 13160: "i-zipqd", + 13216: "bcslogc", + 13217: "rs-pias", + 13218: "emc-vcas-udp", + 13223: "powwow-client", + 13224: "powwow-server", + 13400: "doip-disc", + 13720: "bprd", + 13721: "bpdbm", + 13722: "bpjava-msvc", + 13724: "vnetd", + 13782: "bpcd", + 13783: "vopied", + 13785: "nbdb", + 13786: "nomdb", + 13818: "dsmcc-config", + 13819: "dsmcc-session", + 13820: "dsmcc-passthru", + 13821: "dsmcc-download", + 13822: "dsmcc-ccp", + 13894: "ucontrol", + 13929: "dta-systems", + 14000: "scotty-ft", + 14001: "sua", + 14002: "scotty-disc", + 14033: "sage-best-com1", + 14034: "sage-best-com2", + 14141: "vcs-app", + 14142: "icpp", + 14145: "gcm-app", + 14149: "vrts-tdd", + 14154: "vad", + 14250: "cps", + 14414: "ca-web-update", + 14936: "hde-lcesrvr-1", + 14937: "hde-lcesrvr-2", + 15000: "hydap", + 15118: "v2g-secc", + 15345: "xpilot", + 15363: "3link", + 15555: "cisco-snat", + 15660: "bex-xr", + 15740: "ptp", + 15998: "2ping", + 16003: "alfin", + 16161: "sun-sea-port", + 16309: "etb4j", + 16310: "pduncs", + 16311: "pdefmns", + 16360: "netserialext1", + 16361: "netserialext2", + 16367: "netserialext3", + 16368: "netserialext4", + 16384: "connected", + 16666: "vtp", + 16900: "newbay-snc-mc", + 16950: "sgcip", + 16991: "intel-rci-mp", + 16992: "amt-soap-http", + 16993: "amt-soap-https", + 16994: "amt-redir-tcp", + 16995: "amt-redir-tls", + 17007: "isode-dua", + 17185: "soundsvirtual", + 17219: "chipper", + 17220: "avtp", + 17221: "avdecc", + 17222: "cpsp", + 17224: "trdp-pd", + 17225: "trdp-md", + 17234: "integrius-stp", + 17235: "ssh-mgmt", + 17500: "db-lsp-disc", + 17729: "ea", + 17754: "zep", + 17755: "zigbee-ip", + 17756: "zigbee-ips", + 18000: "biimenu", + 18181: "opsec-cvp", + 18182: "opsec-ufp", + 18183: "opsec-sam", + 18184: "opsec-lea", + 18185: "opsec-omi", + 18186: "ohsc", + 18187: "opsec-ela", + 18241: "checkpoint-rtm", + 18262: "gv-pf", + 18463: "ac-cluster", + 18634: "rds-ib", + 18635: "rds-ip", + 18668: "vdmmesh-disc", + 18769: "ique", + 18881: "infotos", + 18888: "apc-necmp", + 19000: "igrid", + 19007: "scintilla", + 19191: "opsec-uaa", + 19194: "ua-secureagent", + 19220: "cora-disc", + 19283: "keysrvr", + 19315: "keyshadow", + 19398: "mtrgtrans", + 19410: "hp-sco", + 19411: "hp-sca", + 19412: "hp-sessmon", + 19539: "fxuptp", + 19540: "sxuptp", + 19541: "jcp", + 19788: "mle", + 19999: "dnp-sec", + 20000: "dnp", + 20001: "microsan", + 20002: "commtact-http", + 20003: "commtact-https", + 20005: "openwebnet", + 20012: "ss-idi-disc", + 20014: "opendeploy", + 20034: "nburn-id", + 20046: "tmophl7mts", + 20048: "mountd", + 20049: "nfsrdma", + 20167: "tolfab", + 20202: "ipdtp-port", + 20222: "ipulse-ics", + 20480: "emwavemsg", + 20670: "track", + 20999: "athand-mmp", + 21000: "irtrans", + 21554: "dfserver", + 21590: "vofr-gateway", + 21800: "tvpm", + 21845: "webphone", + 21846: "netspeak-is", + 21847: "netspeak-cs", + 21848: "netspeak-acd", + 21849: "netspeak-cps", + 22000: "snapenetio", + 22001: "optocontrol", + 22002: "optohost002", + 22003: "optohost003", + 22004: "optohost004", + 22005: "optohost004", + 22273: "wnn6", + 22305: "cis", + 22335: "shrewd-stream", + 22343: "cis-secure", + 22347: "wibukey", + 22350: "codemeter", + 22555: "vocaltec-phone", + 22763: "talikaserver", + 22800: "aws-brf", + 22951: "brf-gw", + 23000: "inovaport1", + 23001: "inovaport2", + 23002: "inovaport3", + 23003: "inovaport4", + 23004: "inovaport5", + 23005: "inovaport6", + 23272: "s102", + 23294: "5afe-disc", + 23333: "elxmgmt", + 23400: "novar-dbase", + 23401: "novar-alarm", + 23402: "novar-global", + 24000: "med-ltp", + 24001: "med-fsp-rx", + 24002: "med-fsp-tx", + 24003: "med-supp", + 24004: "med-ovw", + 24005: "med-ci", + 24006: "med-net-svc", + 24242: "filesphere", + 24249: "vista-4gl", + 24321: "ild", + 24322: "hid", + 24386: "intel-rci", + 24465: "tonidods", + 24554: "binkp", + 24577: "bilobit-update", + 24676: "canditv", + 24677: "flashfiler", + 24678: "proactivate", + 24680: "tcc-http", + 24850: "assoc-disc", + 24922: "find", + 25000: "icl-twobase1", + 25001: "icl-twobase2", + 25002: "icl-twobase3", + 25003: "icl-twobase4", + 25004: "icl-twobase5", + 25005: "icl-twobase6", + 25006: "icl-twobase7", + 25007: "icl-twobase8", + 25008: "icl-twobase9", + 25009: "icl-twobase10", + 25793: "vocaltec-hos", + 25900: "tasp-net", + 25901: "niobserver", + 25902: "nilinkanalyst", + 25903: "niprobe", + 25954: "bf-game", + 25955: "bf-master", + 26000: "quake", + 26133: "scscp", + 26208: "wnn6-ds", + 26260: "ezproxy", + 26261: "ezmeeting", + 26262: "k3software-svr", + 26263: "k3software-cli", + 26486: "exoline-udp", + 26487: "exoconfig", + 26489: "exonet", + 27345: "imagepump", + 27442: "jesmsjc", + 27504: "kopek-httphead", + 27782: "ars-vista", + 27999: "tw-auth-key", + 28000: "nxlmd", + 28119: "a27-ran-ran", + 28200: "voxelstorm", + 28240: "siemensgsm", + 29167: "otmp", + 30001: "pago-services1", + 30002: "pago-services2", + 30003: "amicon-fpsu-ra", + 30004: "amicon-fpsu-s", + 30260: "kingdomsonline", + 30832: "samsung-disc", + 30999: "ovobs", + 31016: "ka-kdp", + 31029: "yawn", + 31416: "xqosd", + 31457: "tetrinet", + 31620: "lm-mon", + 31765: "gamesmith-port", + 31948: "iceedcp-tx", + 31949: "iceedcp-rx", + 32034: "iracinghelper", + 32249: "t1distproc60", + 32483: "apm-link", + 32635: "sec-ntb-clnt", + 32636: "DMExpress", + 32767: "filenet-powsrm", + 32768: "filenet-tms", + 32769: "filenet-rpc", + 32770: "filenet-nch", + 32771: "filenet-rmi", + 32772: "filenet-pa", + 32773: "filenet-cm", + 32774: "filenet-re", + 32775: "filenet-pch", + 32776: "filenet-peior", + 32777: "filenet-obrok", + 32801: "mlsn", + 32896: "idmgratm", + 33123: "aurora-balaena", + 33331: "diamondport", + 33334: "speedtrace-disc", + 33434: "traceroute", + 33656: "snip-slave", + 34249: "turbonote-2", + 34378: "p-net-local", + 34379: "p-net-remote", + 34567: "edi_service", + 34962: "profinet-rt", + 34963: "profinet-rtm", + 34964: "profinet-cm", + 34980: "ethercat", + 35001: "rt-viewer", + 35004: "rt-classmanager", + 35100: "axio-disc", + 35355: "altova-lm-disc", + 36001: "allpeers", + 36411: "wlcp", + 36865: "kastenxpipe", + 37475: "neckar", + 37654: "unisys-eportal", + 38002: "crescoctrl-disc", + 38201: "galaxy7-data", + 38202: "fairview", + 38203: "agpolicy", + 39681: "turbonote-1", + 40000: "safetynetp", + 40023: "k-patentssensor", + 40841: "cscp", + 40842: "csccredir", + 40843: "csccfirewall", + 40853: "ortec-disc", + 41111: "fs-qos", + 41230: "z-wave-s", + 41794: "crestron-cip", + 41795: "crestron-ctp", + 42508: "candp", + 42509: "candrp", + 42510: "caerpc", + 43000: "recvr-rc-disc", + 43188: "reachout", + 43189: "ndm-agent-port", + 43190: "ip-provision", + 43210: "shaperai-disc", + 43439: "eq3-config", + 43440: "ew-disc-cmd", + 43441: "ciscocsdb", + 44321: "pmcd", + 44322: "pmcdproxy", + 44544: "domiq", + 44553: "rbr-debug", + 44600: "asihpi", + 44818: "EtherNet-IP-2", + 44900: "m3da-disc", + 45000: "asmp-mon", + 45054: "invision-ag", + 45514: "cloudcheck-ping", + 45678: "eba", + 45825: "qdb2service", + 45966: "ssr-servermgr", + 46999: "mediabox", + 47000: "mbus", + 47100: "jvl-mactalk", + 47557: "dbbrowse", + 47624: "directplaysrvr", + 47806: "ap", + 47808: "bacnet", + 47809: "presonus-ucnet", + 48000: "nimcontroller", + 48001: "nimspooler", + 48002: "nimhub", + 48003: "nimgtw", + 48128: "isnetserv", + 48129: "blp5", + 48556: "com-bardac-dw", + 48619: "iqobject", + 48653: "robotraconteur", + 49001: "nusdp-disc", +} +var sctpPortNames = map[uint16]string{ + 9: "discard", + 20: "ftp-data", + 21: "ftp", + 22: "ssh", + 80: "http", + 179: "bgp", + 443: "https", + 1021: "exp1", + 1022: "exp2", + 1167: "cisco-ipsla", + 1720: "h323hostcall", + 2049: "nfs", + 2225: "rcip-itu", + 2904: "m2ua", + 2905: "m3ua", + 2944: "megaco-h248", + 2945: "h248-binary", + 3097: "itu-bicc-stc", + 3565: "m2pa", + 3863: "asap-sctp", + 3864: "asap-sctp-tls", + 3868: "diameter", + 4333: "ahsp", + 4502: "a25-fap-fgw", + 4711: "trinity-dist", + 4739: "ipfix", + 4740: "ipfixs", + 5060: "sip", + 5061: "sips", + 5090: "car", + 5091: "cxtp", + 5215: "noteza", + 5445: "smbdirect", + 5672: "amqp", + 5675: "v5ua", + 5868: "diameters", + 5910: "cm", + 5911: "cpdlc", + 5912: "fis", + 5913: "ads-c", + 6704: "frc-hp", + 6705: "frc-mp", + 6706: "frc-lp", + 6970: "conductor-mpx", + 7626: "simco", + 7701: "nfapi", + 7728: "osvr", + 8471: "pim-port", + 9082: "lcs-ap", + 9084: "aurora", + 9900: "iua", + 9901: "enrp-sctp", + 9902: "enrp-sctp-tls", + 11997: "wmereceiving", + 11998: "wmedistribution", + 11999: "wmereporting", + 14001: "sua", + 20049: "nfsrdma", + 25471: "rna", + 29118: "sgsap", + 29168: "sbcap", + 29169: "iuhsctpassoc", + 30100: "rwp", + 36412: "s1-control", + 36422: "x2-control", + 36423: "slmap", + 36424: "nq-ap", + 36443: "m2ap", + 36444: "m3ap", + 36462: "xw-control", + 38412: "ng-control", + 38422: "xn-control", + 38472: "f1-control", +} diff --git a/pkg/util/rest/rest.go b/pkg/util/rest/rest.go new file mode 100644 index 000000000..a0fda2e27 --- /dev/null +++ b/pkg/util/rest/rest.go @@ -0,0 +1,125 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package rest + +import ( + "errors" + "github.com/rabbitstack/fibratus/pkg/api" + "io/ioutil" + "net" + "net/http" + "path" + "strings" + "time" +) + +var transport *http.Transport + +type opts struct { + addr string + uri string + contentType string + timeout time.Duration +} + +// Option represents the option for the HTTP client. +type Option func(o *opts) + +// WithTransport sets the preferred transport for the HTTP client. +func WithTransport(addr string) Option { + return func(o *opts) { + o.addr = addr + if strings.HasPrefix(addr, `npipe:///`) { + transport = &http.Transport{ + DialContext: api.DialPipe(addr), + } + } else { + transport = &http.Transport{ + DialContext: (&net.Dialer{}).DialContext, + } + } + } +} + +// WithURI initializes the URI where the request is sent. +func WithURI(uri string) Option { + return func(o *opts) { + o.uri = uri + } +} + +// WithContentType sets the content type header for the HTTP requests. +func WithContentType(contentType string) Option { + return func(o *opts) { + o.contentType = contentType + } +} + +// Get performs the GET request. +func Get(opts ...Option) ([]byte, error) { + return request("GET", opts...) +} + +func request(method string, options ...Option) ([]byte, error) { + var opts opts + for _, opt := range options { + opt(&opts) + } + + if transport == nil { + return nil, errors.New("transport is not initialized") + } + + timeout := opts.timeout + if timeout == 0 { + timeout = time.Second * 10 + } + + contentType := opts.contentType + if contentType == "" { + contentType = "application/json" + } + + client := http.Client{ + Transport: transport, + Timeout: timeout, + } + + scheme := "http://" + addr := opts.addr + if strings.HasPrefix(addr, `npipe:///`) { + addr = strings.TrimPrefix(addr, `npipe:///`) + } + req, err := http.NewRequest(method, scheme+path.Join(addr, opts.uri), nil) + if err != nil { + return nil, err + } + req.Header.Add("Content-Type", contentType) + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return body, nil +} diff --git a/pkg/util/rest/rest_test.go b/pkg/util/rest/rest_test.go new file mode 100644 index 000000000..8ce301800 --- /dev/null +++ b/pkg/util/rest/rest_test.go @@ -0,0 +1,79 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package rest + +import ( + "fmt" + "github.com/rabbitstack/fibratus/pkg/api" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "net/http" + "net/http/httptest" + "os/user" + "strings" + "testing" +) + +func TestGet(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("test")) + }) + + srv := httptest.NewServer(mux) + defer srv.Close() + + resp, err := Get(WithURI("config"), WithTransport(fmt.Sprintf("localhost:%s", port(srv.URL)))) + require.NoError(t, err) + require.NotNil(t, resp) + assert.Equal(t, "test", string(resp)) +} + +func TestGetPipe(t *testing.T) { + usr, err := user.Current() + require.NoError(t, err) + descriptor := "D:P(A;;GA;;;" + usr.Uid + ")" + listener, err := api.MakePipeListener(`npipe:///fibratus`, descriptor) + require.NoError(t, err) + + mux := http.NewServeMux() + + mux.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("test")) + }) + + srv := httptest.NewUnstartedServer(mux) + srv.Listener = listener + + srv.Start() + defer srv.Close() + + resp, err := Get(WithURI("config"), WithTransport(`npipe:///fibratus`)) + require.NoError(t, err) + require.NotNil(t, resp) + assert.Equal(t, "test", string(resp)) +} + +func port(s string) string { + i := strings.LastIndex(s, ":") + if i == 0 { + return "" + } + return s[i+1:] +} diff --git a/pkg/util/spinner/spinner.go b/pkg/util/spinner/spinner.go new file mode 100644 index 000000000..f8761d6f4 --- /dev/null +++ b/pkg/util/spinner/spinner.go @@ -0,0 +1,33 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package spinner + +import ( + "github.com/briandowns/spinner" + "time" +) + +// Show creates a new spinner and starts it. +func Show(prefix string) *spinner.Spinner { + s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) // Build our spinner + s.Prefix = "> " + prefix + " " + s.HideCursor = true + s.Start() + return s +} diff --git a/pkg/util/term/fb.go b/pkg/util/term/fb.go new file mode 100644 index 000000000..4cec27bce --- /dev/null +++ b/pkg/util/term/fb.go @@ -0,0 +1,175 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package term + +import ( + "fmt" + "io" + "syscall" + "unicode/utf16" + "unsafe" +) + +var ( + createConsoleScreenBuffer = kernel32.NewProc("CreateConsoleScreenBuffer") + setConsoleActiveScreenBuffer = kernel32.NewProc("SetConsoleActiveScreenBuffer") +) + +const consoleTextModeBuffer = 0x1 + +// FrameBuffer is a special type of the I/O writer that outputs the character stream to the +// active console screen buffer. +type FrameBuffer struct { + handle syscall.Handle +} + +// NewFrameBuffer builds a fresh frame buffer. +func NewFrameBuffer() (io.Writer, error) { + handle, _, err := createConsoleScreenBuffer.Call( + uintptr(syscall.GENERIC_READ|syscall.GENERIC_WRITE), + uintptr(0), + uintptr(0), + uintptr(consoleTextModeBuffer), + uintptr(0), + uintptr(0), + ) + if handle == 0 { + return nil, fmt.Errorf("unable to create screen buffer: %v", err) + } + fb := &FrameBuffer{ + handle: syscall.Handle(handle), + } + errno, _, err := setConsoleActiveScreenBuffer.Call(uintptr(handle)) + if errno == 0 { + return nil, fmt.Errorf("couldn't activate console screen buffer") + } + showCursor(fb.handle, false) + return fb, nil +} + +// Write draws the character buffer to the screen frame buffer. +func (fb FrameBuffer) Write(p []byte) (int, error) { + bufferInfo, err := getScreenBufferInfo(fb.handle) + if err != nil { + return 0, err + } + if len(p) == 1 { + return 0, nil + } + + rows := int(bufferInfo.size.y) + cols := int(bufferInfo.size.x) + + chars := make([]charInfo, cols*rows) + + var x int + var y int + var newLine bool + + for _, char := range string(p) { + c := char + if c == '\n' || c == '\r' { + newLine = true + } + r, c := utf16.EncodeRune(c) + if r == 0xFFFD { + c = char + } + y++ + // if the last column has been reached and a new line was encountered at + // that position then we'll stop the iteration and reset the column number + if y == cols { + y = 0 + if newLine { + newLine = false + continue + } + } + if newLine { + newLine = false + space := y + // keep filling the rectangle with spaces until we reach the last column. Then + // we'll reset the column and stop the current iteration + for space <= cols { + if space-1 > len(chars)-1 { + continue + } + chars[space-1].char = uint16(' ') + space++ + x++ + } + y = 0 + continue + } + + if x > len(chars)-1 { + continue + } + + chars[x].char = uint16(c) + chars[x].attr = bufferInfo.attributes + x++ + } + + // clear the current frame buffer screen + fb.cls(bufferInfo) + // the following block of code does the heavy lifting of writing the + // character buffer to the screen frame buffer that we previously created + cord := point{} + size := point{x: int16(cols), y: int16(rows)} + rect := rect{left: 0, top: 0, right: int16(cols) - 1, bottom: int16(rows) - 1} + _, _, _ = writeConsoleOutput.Call( + uintptr(fb.handle), + uintptr(unsafe.Pointer(&chars[0])), + size.uintptr(), + cord.uintptr(), + uintptr(unsafe.Pointer(&rect)), + ) + + return 0, nil +} + +// Close closes this frame buffer. +func (fb *FrameBuffer) Close() error { + return syscall.Close(fb.handle) +} + +// cls clears the frame buffer content. +func (fb *FrameBuffer) cls(bufferInfo *consoleScreenBufferInfo) { + var w uint16 + var cursor point + rows := bufferInfo.size.x + cols := bufferInfo.size.y + + _, _, _ = fillConsoleOutputCharacter.Call( + uintptr(fb.handle), + uintptr(' '), + uintptr(rows*cols), + *(*uintptr)(unsafe.Pointer(&cursor)), + uintptr(unsafe.Pointer(&w)), + ) + + _, _, _ = fillConsoleOutputAttribute.Call( + uintptr(fb.handle), + uintptr(bufferInfo.attributes), + uintptr(rows*cols), + *(*uintptr)(unsafe.Pointer(&cursor)), + uintptr(unsafe.Pointer(&w)), + ) +} diff --git a/pkg/util/term/term.go b/pkg/util/term/term.go new file mode 100644 index 000000000..813ac7a09 --- /dev/null +++ b/pkg/util/term/term.go @@ -0,0 +1,95 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package term + +import ( + "os" + "syscall" + "unsafe" +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + + setConsoleCursorInfo = kernel32.NewProc("SetConsoleCursorInfo") + getConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") + writeConsoleOutput = kernel32.NewProc("WriteConsoleOutputW") + + fillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") + fillConsoleOutputAttribute = kernel32.NewProc("FillConsoleOutputAttribute") +) + +type point struct { + x int16 + y int16 +} + +func (p point) uintptr() uintptr { return uintptr(*(*int32)(unsafe.Pointer(&p))) } + +type rect struct { + left, top int16 + right, bottom int16 +} + +func (r *rect) uintptr() uintptr { return uintptr(unsafe.Pointer(r)) } + +type charInfo struct { + char uint16 + attr uint16 +} + +type consoleScreenBufferInfo struct { + size point + cursorPos point + attributes uint16 + window rect + maxWindowSize point +} + +type consoleCursorInfo struct { + size uint32 + visible bool +} + +// getScreenBufferInfo retrieves information about the specified console screen buffer. +func getScreenBufferInfo(cons syscall.Handle) (*consoleScreenBufferInfo, error) { + var bi consoleScreenBufferInfo + errno, _, err := getConsoleScreenBufferInfo.Call(uintptr(cons), uintptr(unsafe.Pointer(&bi))) + if errno == 0 { + return nil, err + } + return &bi, nil +} + +// GetColumns gets the number of character columns. +func GetColumns() int { + bufferInfo, err := getScreenBufferInfo(syscall.Handle(os.Stdout.Fd())) + if err != nil { + return 0 + } + return int(bufferInfo.size.x) +} + +// showCursor shows/hides the cursor. +func showCursor(cons syscall.Handle, visible bool) { + var ci consoleCursorInfo + ci.size = 100 + ci.visible = visible + _, _, _ = setConsoleCursorInfo.Call(uintptr(cons), uintptr(unsafe.Pointer(&ci))) +} diff --git a/pkg/util/tls/tls.go b/pkg/util/tls/tls.go new file mode 100644 index 000000000..fb878a30c --- /dev/null +++ b/pkg/util/tls/tls.go @@ -0,0 +1,63 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package tls + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" +) + +// MakeConfig builds a TLS config from the certificate, private/public key and the CA cert files. +func MakeConfig(certFile, keyFile, caFile string, insecureSkipVerify bool) (*tls.Config, error) { + if certFile == "" && keyFile == "" && caFile == "" { + return nil, nil + } + + var cert tls.Certificate + tlsConfig := &tls.Config{ + InsecureSkipVerify: insecureSkipVerify, + } + + // load certificate/key + if certFile != "" && keyFile == "" { + var err error + cert, err = tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return nil, err + } + tlsConfig.Certificates = []tls.Certificate{cert} + } + // load certificate issuing authority + if caFile != "" { + cpool := x509.NewCertPool() + caCert, err := ioutil.ReadFile(caFile) + if err != nil { + return nil, err + } + ok := cpool.AppendCertsFromPEM(caCert) + if !ok { + return nil, fmt.Errorf("fail to load certificate authority: %s", caFile) + } + tlsConfig.RootCAs = cpool + } + + return tlsConfig, nil +} diff --git a/pkg/util/typesize/typesize.go b/pkg/util/typesize/typesize.go new file mode 100644 index 000000000..265eb74f5 --- /dev/null +++ b/pkg/util/typesize/typesize.go @@ -0,0 +1,26 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package typesize + +import "unsafe" + +var ptr uintptr + +// Pointer returns the pointer size on this machine. +func Pointer() uintptr { return unsafe.Sizeof(ptr) } diff --git a/pkg/yara/_fixtures/rules/dll.yar b/pkg/yara/_fixtures/rules/dll.yar new file mode 100644 index 000000000..b0ec2eaa3 --- /dev/null +++ b/pkg/yara/_fixtures/rules/dll.yar @@ -0,0 +1,10 @@ +rule DLL : dll +{ + meta: + severity = "Critical" + date = "2020-07" + strings: + $c0 = "Go" fullword ascii + condition: + $c0 +} \ No newline at end of file diff --git a/pkg/yara/_fixtures/rules/notepad.yar b/pkg/yara/_fixtures/rules/notepad.yar new file mode 100644 index 000000000..08606ba24 --- /dev/null +++ b/pkg/yara/_fixtures/rules/notepad.yar @@ -0,0 +1,21 @@ +rule Notepad : notepad +{ + meta: + severity = "Normal" + date = "2016-07" + strings: + $c0 = "Notepad" fullword ascii + condition: + $c0 +} + +rule NotepadCompany +{ + meta: + severity = "Normal" + date = "2016-07" + strings: + $c0 = "Microsoft" fullword ascii + condition: + $c0 +} \ No newline at end of file diff --git a/pkg/yara/_fixtures/yara-test.dll b/pkg/yara/_fixtures/yara-test.dll new file mode 100644 index 0000000000000000000000000000000000000000..937a81ad0163c6a6d9df2fe062dd451c417e5744 GIT binary patch literal 1633792 zcmdSC3wTu3^)@~U85n_hM!5w=88o)R3MPteQix6f^#mt1C}I?Yl=S)tL0o(8Up6CC4 z_?X#eU)El?z4qE`@3T+I^>vNPfx7<2BGqY#5 z4Z8GFkLQ*}y*y)HnsB4j+hNb}vwEecpXr&})8qLt%j3DBnw{Z+(ByTJ)3r z7aZitUyyw2lBoJqe@{-AT#x5>rw;N=bHnLWXL$J5$}sXuZw1Xnk+f4Nu7Bm8^77%g z+!VeEc`L34u2rZr0l&B7*Yz*xDX$+sTUV^xjk5h9=ym+Q=f}Y&q@Je0JM;dn8^&U)7@8&sk*!15&1o!uMEO$Uw5x-kCJmEQb?`Z-ZzwB6F z`n^rp6PZ9Zp5A~`4xMoQL%sUpW3HOOtTO%ypdRs@-nm||et3Ag;3ry;YuB5RqTcLV zr_Y??(XxijJwE}wS)J?Uj%IiNFaI;G%+?+KJ@E-sdU}k(K~K>&MH8;McH-4$FGFaaXulmO48_iQvfCg}!naOA z@k3uUSrgv!KbS zc>>i0jkY(;XbZ8pkSM&z`WM|!B)Ugh(u|sSQ7_iLw%(}ug`_0&Pe|6bh3A?vZyOZS zjCs&DoLLf8>#0+r=z+GK5%0e3{XJ=2(XQ*71r9%i%IY)F+1vvVJm%sHrCPjaTU(p3 zFn$YC#^A9Y@Yr13EnbDksq$>b`n3ucnFsOH+?|Y4^KRqqU9X|ZX^-vVuq?*5KYXg6~U7GigvC;j_mtrPqYlAogTAYn)DnMCd+5k-e zfY0-8R}V<5@Kz^R`2iN-(D`kVV`&vL)ehaydeIK_+g5B>sCYPuSOR&;hwbi%Jiw1n z2PUdnw~HL>X4FnXM*IT&(fmb6M(tQUCYk|7Buol_ppK2x$3)}b7Px$7;O2_krr#1M9{c^kS>L;E%-kz3 zD4KK2gv)Q9dEs}bUoiX5o6nzDls97RcYko8EAX;c>gdgd!NGz z&x}RM4s+y9{<#IP9$98|dbZ!R@JGD`wKu_lfEmrmW+KM~n|!o4>U3$#s*z1ujcU5l z-IMrJdv9IfTVu0VnInt*#-j5Gwfdu-1Q2~PdozD6`!`mTy;lG)&EAjS#7Sm!qZ!?7 zMt7ODTa21Dm{}>HT;efHu)>qCX>7LtJQeSbLP!ZVC5W-V#jwVrdR~3r783%@H6veU zELdg6CTGv89yw)J#qf&X5=xl~sRQ29>?(9S~HA=BblLO2e`pcO!jN0>& zVOy)#9!Bk2V2$FzQ>(wcX}VGSM?S8ZFgU&HOFS(nNwI$OUWd`&lIT^mBJ$yH1_1Iw3_vWjuqyjQYhvff*-U$ zHhWe4bk>etG(dn9`tXw+Zv~$lksc^E)`X4I5)IUzGBc*R4F1K>#wc1cHv8K^Z|@$O zvkBQIaO`1PSEkwgwf-jPpRtPNvBGEZgTbTc7qg(%h%6O#*g<4Nu~09c85`8AxZu-@ z4Q6yfc94paPl1xxz7U@L>U{&=*MjUUPsQ2B8tEO~nlT83vI=svPZRqc4`U#kvCL7@ zcfVosGd4Te-hYF!s0aIpLghD&JuM5fYv#$icH3@p6BQ0XW59syZwHrzPaD{Q2l2CD z=F}V4c7xt{AN|qE-~h&0GcY|9RM)9vH=%^+NInv!GWOJa%(uMVdb?V1-cVanpw21N zt&*&cZ}>O5nW4M6`8%#g^rX};>|V;9Ui1i`TBqjge!jTF)g-_sH``4*BQ{PqxV@LF zfgqNL0=1MFfQoj+&H`dkahUcWg4J5Z9qvx86K$Vr)D}tEnh8D9i>kiFa~_`a>ZjJl zzb0J|P84SJxzp9>5FiQsH-r!M8t2b!E8sa`11B8x|wZ_W znSZ6s7i9ZrUUN+AqHFW~D4Cvpm{kv}TZVxJu{%7G!jc47ZPeZ&qs6=Hp97Ik_bFQB z2f%thM~evi1)VHK@+qC1h2(D~SuMI{c^}kItH2_ZMfDR^6Hg2D(_#HIOF#KBU!iP@ ze#+HP*XpNI{d9$Xnx&sE)=zc%X}Eq`rJv5!PdoY4>>c_VrZ{%i?ERLVj1=DZp-?-6 zRic^c$TyoavQf*U{sIm_Qa-Lw7LGv$*q{JQk@TpCbg3V_>1(V%aGLQm?=v3{^cd^+ zdysrwC)1F8NGDH1vQ8&^BYCe*_Ca#KPWDCePMsWp? zDCBKHXM>EN)eqUc#CtBXfoy>OQ>3NGoStTk8W7o(-?$`{J(y^79c1e_C_v@jXx|O+ z4}T$OO>_W{;qtKSi9MmC_P%{di+(}7#G z0Uc$>uYwBa)h9k}Z(&-6%eGWJTRMNVOsy2?#kLzdZ z+bBN&LO)aKhVi*pp4Zu$cRI6X>nvOSPC}OWzms&ft%sjO;gzYgY-K#atc!G(A5@`> zoypgXES8vobHtMXMI_pMe zJ%g8`>DFD#FMbS@lFJ@+PEu*1mc5OY5Q`snq%UTcEKDqev=vzgMerDDJ5lOG zZg013AuPD$S!ta!U7e)1KlcGBBN#L)0UDJ6?Od)IGvV7!fHxOA&5Y%We_t@p7xw4X z!-Wh|Ap%g-oJKP`0`B`{S}1yZN8x{JZ!ZIi^SXu=srS>G@gd)o2arG;>1UKpHKGk& z!>q*bFh0RIs+|jQJDCdqxidQAjy_M41&O%-krv~gi#hFOnGO5B+NH1t0T-+|=tG)m zm@I&bIsiom)_zaLMsP7GBSe$ON4S{|wd!aZ@SB|yKavM7G?=@Tx&9jaA6FMcwwh@> zSG{+Tq@?9)m-+GMN`FUo8MU8an6Na!(m?0ZX1g?(rMaC;f5W*V z(i8++gJ3uQZwVTUeu&)oOmzCn*E9R$|H1fwY5;#`23~nTyz!N<-l~VF+S)44e&vnu zzwp?k9|v^?HT%@Wi?6iIi%$SR{(j&7{WnaotxH^UVth>I_`rIU<{%KV;3LIXN+mOL z49fI51ra$GG#0*qR8`@U_n}bXX*%n7$O3huOZc5gHr>cSQ^}H<{4>5&s{;|C^wJJ01MZNFrGMCABg9XcNZ+x}IGK26V^&2L7K&Zrm7n<*V=k zaOA^R-q=SryxF;ZTU&S&=q3pKkd(Qno3WG4*Z|ao7B}O6Xmi1?@O#otEOcfII4~F* zy31H5{cAgJ7+{5e1<$4?9KCxAz=u_|T6I!%wobs_{zF6ZPa^SB?ZRmb{qF~Zpv`7Reh zZT!-U^=0DMPEV(IV_dt%7~W9P-H1H{@{Qs373uRU0VTtZ@>U@i*aUN9OX#4+>i&hl z(K@E1+Zqm>lJ|CWPu|vsc#phY(RUgW{=99`hgrqSfXhX?^578+`TfM(ADdC(;q*X}kH#HYSxmm!PZH$2%a`0as4H4IoJ zVlEN>ulJu0TAcndG&}Tv=y?4u(j-&n*=d+;Dg9^{h;pkXWn$UfPgcp9=XW5t-= z?#e=>jT&UE89(62#z6QAGYSI{sTb4W>!|Og99v+BUoB$5L@dTSm-@@-DkHbBVA&LG zOxhc?IWDM?DM86d>Hx|PEW7i7DdMMBcm)-kg?2#0wEDgAeMZL+a}rz2XdQ8fnGIzJ z)UEerNUI!MA)px|_{;^GFf7b7AA6kfByfZ1tIq@7u-YB=1+J zf9msZ;y-3+rT6cOx&v1AM+`i3|E`X|mi-&6$zFkHV~yGONQ2L)eG6tE4+A0x23NHr z!1xR|M7UpG)!Nrs{1vKJwVqNjn_G0tvRC0TaTyh+)Qpv3)qkNG#p2j_1|gqEB`9vD z{hhmK7(^2=TFT(4P+o}Um{=B1pheRCbAQVW&RcCpmq`HKjM?~AZz9@_Q|s7&Z93Na zW~4IcsTj$Y47;0A8nYU~MJwp+cAKH2?wx@hzv&AS_~#lJ5%aZ#-U*MY)-0o@93!== zb)Zo@2h2jKBZ%>xPkZvyi#UeCj%j(j^Y-N(F=H=k9-YB`s#(R6dW^azGx}C2T5ocL zej7*KG@?C=94coh#0)Z zKtz?SzG-H(FVty*-em@O?l$U8U%$Ezo5A+tFPv@0?~!@S-q%3XM%Y}?Q1Rz>E(H5N z;R`RP6d(#p8t}>~0+akl$3Q855#Z=eZ#|;oJg<->lyrAc?>>UX33c>jWAXivLRD*K zIOOcmRkfaM)NT}mdAxyh*AYEDkE{L1MN0g;e|?GYDik;e$9G4n3+irS%F;IzL z9dn%N=FkNvm~Q4SOgY_%e~MF)nqn*SnP0hQoV0vcYy^rfQ0fdAPiw^|FjOF!18nD~aff#o~J1y%HlKrD=2Is+Tr1*asF-ZbI zIwgcu{@vV(Db}_+_75`c@c(h*{&X~*xWBBgk9~}JNaFafs~sQ0ySF-U@xSOF)M0(E z`$rV0pl7Om2iu(`5`O7_18k$y|CRqg^w0BIPX9aN{}KJhA};t-t#5F&?|L&A;~1o;N4X6WPWZxcNv=QMzbB6s6SNvZEYoxw$ z{qe=|`7>E-iRRG_BGx~dNu zwDbco=!13!r5ZoRBA!0$7$c}E+_jziEZ7ZsLlP%d6~4wqMF<6P>nKmEgI`~mN;c)<%6*=S&Wy!=zj0?$oV3gtkSLB=S(AG-GpFsK1vd3nab57-z< zAgZaFbv5;>OSX4!$$~b6XwX+q z`pQUOSr>``36{;wH4EevicxzOI_#qJvEtmiw(3z>rFAVDpIgTTS@gbKG9h*axEb3E z+zHk2SJ$JFaSH4f(mU;R4$}MWG@N;J76k=@N-Onx*Ic7nD45x-AUKB)WYILTkp7nw zf6!zgQw=?=IqqsMT=#3Pvnx@I125=vPN)|Is>Jb>D!~`&9Ha$br1Oy`zGOKL_uAORA8)rR?b_Idq1IR&Gm`Ve4eYY| zBZ)g8{veA!OaB_z)|08z_@@%n%HK4BY}NSu$OpTeYEu&qmGngo4qzO;wTs ztx>Q>(qoz${$t7>>d6Q}DCuyNrq!h_eEEdF%vpxzHG#iWWK}K=3 z@26&Mo3ZF1>tlGET!kZK5n**Qi6>Ki>N~R7v?tVZGg?ic93YTNweHJ%?>r)=*Tj^S zkZ8Zn5F4KxSqvqHm%=9Y>wmYN5@wm!gzQVngK3e?>6nEt!~d5fWSMKm za8#i%jGyN0-4EzGbEI%EQ0qM~$ zg4?-r;NP?=L9{J+32|V3T^7%kl{A3o)yHE<$If_s<3LaCK4b9)1c~Ci@CRGmQJetu zsC_t_QK$am;aKX(0I~mSq$iAEaM1sEY#pn!UnRvrEwahisIGYzROY6bVnKy#ekC0NnO`0SG)hWM zIyqg3oht5uw5?x3+VDbu%uu`Sjt6k)u{&Zn0 zy-@Jhy#C04n2-w|qz=y8g81NHK+ett%tzqvJTi!qc<{t^+#>6cP9?v&x2Kv_ zN$l{5_R4&=0Ye~DWo9h92^z;0mX}-XE)d9L1egL{FJ%72?&{vv5ICY4~ttT|KIM zDsF2B_{(1+q$F`gS8bbqSQ=2x78tzae+VlKF2Is73?~CuqeaeOUcdwf9^SVP)gV0S zmg|qCACMx&`9FvAKLGF#;Ni4QC4!t`yB9z#!|2o)Uc*eyhcU@aK|26F$GJ0U4=n7) zG>^!F>9)}>6||M?-vS+{Ro9fGM8FUIB~Rf3SWxgYYWSWTfdeFPT#aeNme*pt0KKAM zxl>Rv4K@k-Q<}gY6gdW)Ev7DL#)NY+>x<(p49Db<1^;Z44Gm76G)HZikI877hDh@+ zdTChpZM+xZN#~I0iBk?`j{a?`B7K$Qee4eXXQm+dOYc!K?Eopok4iVk>DfN($bd{c=gcfjR3#(Y+-@ zw-v|kdjhlU6;|KxXonq>ck$4MVk;w0OB-_v`L|Ep3epz&s0ud9Zu{^zwpEXHYPqeh zcr6gYXkoV*zGB$B28H8dII|StyU8KzjMwr80^#h&;)W0V7grxd=)-emtncf^4e`w4 z>H~CJu8b|+%C~EaYBI8mYSJ&guXsvP6<#o^nTbN|9@K$?tYT~=05s+=4ITA8#j%XW z8c%V9>K}?W6j$#j>~XP-dNJ2L$6SmT9U2l|v--U@bY$WPT7&9V9NFK^^hyS{6;=TI zOJPkjiX#oakk$8lbtO3I6Zs!x)BDLEYpm%LD%d`+`=!QjH_pZzIG&dl zAhi7Rwg7wlw=e!NeF#{$QUJxF4e?jY#A$`8`_^?{$U?@p>>1G0gtsz{jtYD?d(eyh zU;ZYlw!_~C_;DX>lNlQwU;vpaoSZSl(Gb#>g)5#K2d%OLTY`I=SQXWXkW&9uWL zG&k=^UK`(iX+mJ_^SPDn?{^(-x&-}J+-$7*ejQ&=sogTSJAA;F#2#b)2cEh)uac%L zkkprsC~SEMrY+VRqSWi3<%r*l{3?b~c5rZ|0;>vAKq3qKC4Cc-U7sW#BOmPOljZ&0 zrJeaOyr*6@w%f22^Lv)DD8!)~`Dhk7GmVyQ8vBNMLx_~`FLslmZQHtntwuy~@C}7E zeN+pqU>oe)+uSid(g6Fm9L2JAdWuoI3hba@tMHq51SgBzn#`fc#QB+p!;9MbS7OCF zU|yaXLn3wwTpxNu#%TPsnnT|z9=bD>w#AM=ygU1^qPbeY^;%jfE# zjN>m}E^G(}9Fe%j8uq3&tKO0|U6RM`+-^@<-b2b2qu?zGdVR2=2fesA{sJ$qF2yP# zh5liCy|>oC4kI&!!6pm5wB+ETeJ@1J#bZPmrmniHpU+s#nTb@Fnnw>}{Z}=e&+Au! zOdbf8_uZ~@s_^}_-Nw>lK0}x}G8Y^M?l}44^Hkgofr9^VhhXS@$mX)ppQb*Tj~d`$ z7S;L=C&1x!jYElsM;E}kb_#Jux<^Jg0mw>n()R_baVU$ zkv22%a*5dANuQv*e{e-yPP#h~%Jttd`^1INTIr7z76@3(S(O0biT5T%yd2gCXvf>A zdzQCHjb5hG7QbbHwLn|#{tAH-%lpNtE~6wbr9I-sO3U)@b6f3i0T-=5^HB=@*#+o3 zQ|J$@6vjJUWLPPfMBBY(0^&fw!WKTF8YHF|JtOnASAU%IiJ3ea68Wf-kTGBJ4>p^t z*p$J;-d?WI_1sjmr6Mmw9d{eGf6rj+N-SrWZLHZHKD9|7=8$e@!Aw=+ zKyu;A=OG>F=i*?E~s|oa!t(P5X=EDkfyHJ zZjohk4hLAI@U+#S%cyh)g<-R*L86Dcg$?HM5rMa%rf(J>fnmz6>_-@V1wS-{{Zbk_CJ` z>h)n{jXggHlNJj6uO<;jZ6Rb3Dg5_yLM**^vsp@rW>d8|#%5%lkYm)m2AGk;9jsh& zCn`ErV)`M!?;RB>e2#@v>nhF$XT=9IqgXAyc0o5sni3GO)r|)#^0ug%A7QM&MKp8d zm^bubH&LXVKs5BT~?GytbL$Fb_nz3qqFzz7FNb2fupqEuCM=9;$k&vT?D-Z*u*QQwN?NzTm1;T#0dmE=m*4#11?3O z$0&LV(F2tdLFH7!$cJ)fA=4A*RR%f-g9U?XAorZlL#8@^D^ed~_?>Wl;hzhu)Opqp zA{v@SngSYg1Zkp4^&JZqp$KCNF&%YVoa`e;`dEv`AH<-NxcI24GX6zAIMclP#TH=3 zsCzqRI+aR({{?<91?W`{Y$Wm9qB`P-3ZMk|0nsFWx8R_v3%@Tm{}=cfi>7l9Oc7b5 zbHaO797iHY90_&pM2%5UI}rOY+?#`g@K^6sQ>uw9_yjOeP`DL%L_5+0E(aY~Lp_V( zz#Q>3Y8WqPC1|jT^AuYuLU4JYq*$e5FF8_p>XUkY$~2p+sCoRCSZ=E!wB|i1Y9faB zgAbAsT4A98P}u0^#0K3nOls1MstewrMsD~s2M6AR5(B0+GEu7^wL0Dhz#{4s5F{ee z_I)3=i5#lJ(<6n~O9vAjZP!G>w-cfYrLY4@kF=BICrMEc;L<)&_LetXnKY}{y=L1(@&8ADCZ=^ldwKj-clqfkuT$4Fk*TTuZ&2X2a=C&! z8;JDwFk)H@&e57xPL_Eb2m@mBXr^kcoD== zcl0<2jNwT_F$69Tp}Smy^}DwCb>Y%5`d}SFGoyaJbScBa z;5lkUr10CiRxWCVY$=7J!BVq%AtCMtH=~KdRtxhmV7SL6*Bcx>|mKiLGl?I*t z_Y$fvL)%#Y{GJL{I|2`(Xer#~(eQwSu+QF0%G%c_kQ}r}+?$gJw69M_4AB+@lP1Ct zDb^;Y0nN#{libJ(V0tb3W_UXpwtw#yk*)a+a? zRDYpG+eMM=g-R2)Te;*Yhp}9bG`7vr5{nkxQ3O}iS+vl<-+;?mSf~jT`YGALQ zxpO-bGk?emf@fGUG?(yR$z3W9JJ-crv6TItONtY(_4v2& zWFvQ6DMOqgjQ}n0r6+eSJSLK&HU`Ac$tbr2R)Y zzgEBR2Vakl+jT3&%-qLHw##oXRsvkClqMG|0oMKwYgdE_F4$qQ65tHNfm!A(R+Rrt z+MuLQp7u6n$V$>yFI57GX4g_B2>xL^Jn^LMI6aMr_zYQp*t0M?K)j% zp7&I9a}o?HBrMhcufThJhlPq2K6(R;SSv(uWkE-EZfW& zx{BZ|jcLvIBbYLVBRIN4oz7ni+vU-B)%i##o{kj0`5X4850RuLeuZjz`_yMt7Yheb zqes;tq~Y~r9>@}O9N6lq4Pdm-wa&wRvcDeaIp^>tZAMk$L%#yt@Y#hca6@gyptZmf zcTpUp28Vk_4yP^g-h&E!;o*zIov0H2+tfPL?dA!;Ubr7Wcq5|ALDSUyeFBidUx;Zs z6a+HF>dNx^7U`p2s@X*G6mJN5QRCq`=c1?r+QqpgdeiH+&mj`Had#mfAA>ih>DZ)`x3J`y=v79;J5>f+GKpe}M3TQ&` z28IibGDl2c9FbqFHmk;xSE)gHzu^G6k3E$zJ%&Zx091fzvo^BuK39nmwlo< z)DKdz1w?|}B`esm<=tT4k*Lz;6fj|k&H5ZPCLVCQP5!3p-&n*TV@J)j`5u6TuzF1z z1|+n>f`$aib3c^_xYMGJ%?3X$Z-HQB>($9D5-2dJh&IW)9;}(BAp;JG0jX1pAd{qy zsbz1`4Rusbodasbev0dvq^4^kHGNOXdbM->Fc*_rrRb5mHC?=hK)q|!(;Ta%z4fS1 zj*ny5@>l(_iKce34e$q;CG?pzq|-H|LoauRG};YGttOZq8TfOb3rtlK%y;h zIfkPb>{Sp_cM!}Hf*IoivnmP3(qN`_0kgUTm~RnG5x^jR#1=MHVs<&s)TJ6uunU~& zZaB0b`*6xo9qEQY&$=M3NMdx1Ctoe^OD}ci&slCrY8k;iLom0yz=*Re2(8v&Ug!d5 zA4qBEPYJnu?sW%}k^?(b~<|L$jI&43&3#4>6Bo!u@`w3>83(U?Wm}MHwfG%J*fRuLr3?mqm zV7j@$(6nj(lxQ$}U+hd*xC59&IFz8$00#agTiE>BpM>Mpa31ag=R7wY@@F|gt%0!A zoi0c!326hi51>C2yFfY!QrZcdKrk~2=3Ez;!$~k<4Q4thrikj#E zF<#)lHKF`gLpij*GnB0$frtwfygx@&CKAXgE+Dvo5I|)9*KnTh0%xHcj`|lZpg}NO z*?^2OUI+$dO2GR$BBR-zVG#bv4q%=ln9Y!oiU=46J|j3dt42HvC+d7GJ3E>9)b@R( zUZ0h54lSi;0e7X}_)0i{u`{mX5B>0eLu!a$?fXd)!$YvzI6w#Aw1*kl2;Q`qv2pNu zzK=sg#!ok)D$X8ee6Oggr7%1RXXq})iuH7?ns8nFv3p;$XUAEbp-O>p8NBeza6Net z@xKORp+)`Ry#-rwN155sitF1qn`td%&MZ3bm9BPpgo7o~-4I+cM6bX^p0464B(Swy zSjtDazh2JY0yR#XD9xt_l*5_TlMb9{wARUvMUnczrPlc5Q$v6i>M{3&RbZYU%**0=`=G41^dpbItlsIB z_pv&Ozw7xDt0tTKP)>YLhr0JMz+3&1pK5_nH{WM4E&DfCgBwWktncQjU5cvfPNKl9 zeou(Pa^DvhR4id$tPH9#C|kXfLGL8#6(iKCVPKub0}rnt*vA)eocRe-Mx#2r5!e`_ zFzrT>y8{S2-YGFqypzOF42b(+y(p1#=$2{SyPrZhOd%Xr11UV*p9~D|i&KEhq@vlZ zuOp!xojvyUn>FN`k7e2QnzA#V(d}fOK|4+qARMrk>JKXb`Te?T&U3n|m|Rq)%|!fP zy3q73%N7F&wE&%uX5+jVJj9G_)B%dCHEyE5^cJNl{YNHXzb0LxUug&=HFp{nY?fK@ zPB=gw21IB}u>xnGw$Q3KQI>wzC71%-i#!AWU>F)L6zDYe*D(1gC(YNTDa(kuXVVXVSz%5Cr* zu8=k3G)Q3>d3#C_3ml*u9S8uZnpmWnfU$+eN*w2*5n!=91VYkMjeo`_09Si_VlK;qL|(1?G}xh&6%4R+)UjmPI+>_*solyQj@q5k=L715eb? zm8egR{(Oh6X+9PuCFFd{LyV!LcHY|U?4l;yt#+qKyU|%N&7vBjcIdLB#_0_v&G#-E zrxK$&=-W3bn8RqR-{3<;hT%_Q26Ne`;qY&n<| zcIh{(I23ZChpcDcXae)0^_BBWd@|05sQ!O~z5cZ8>?rTAcCg=^9jx)tuE{_YGhTk; zl#q2GLwEABp`$qK-y)wWN!v@>gFL;A(Q0z$DovVtynhqj zsoui52F$o>2Y&g|t?OT^`F-xqn3aOmOswYlCn+RvX)}ey5l11(4eUS)opQJ#1$JZ1 zsY8YCp};(hPC4I0a1q>{rW&8XdbwKe2f^UCoBO4?`~Hwr_!*0O(U^BQ-`F1LHfr7z zOO_1P8`fVW!#>7jjmOld&l6+*gc&pz7fHE3OVyNd@-T8Ysvq0WEZZ{_=*AWw;)C`p z1B`OuHc=Q6l=%8m70DV&kSz5MH#Wc;*d7N1-bZA65KxlU+Nu_OHV?$Um|aB>cvnOJ z+J$MY(na#5hWZB%lsXocD*gmpX;IjhW=xKU_Av`n80K$ z&nvgBQpm&>n9Zg59NJG?P|4}mKCB++u9VXv@wS@#dyG%M_UY1Wo$R7N*4XS7w$nl- zs{Uv{ITw9WPs?yx_Trh^)QVrkCwZB4$?@8NI z#hWZ4Xi`9pXzk{Dc~=})oQZ@~X&qox%V=I{cj7Q){M%5u5QiPGgO2fnVep>LlUNhDxB-5@NNs@q zw%;mdoRS{(2~i0F&+^1CJA#(Cl=1B#5c=c)6?pN>IY4YP+Ri`0@B7+R!hA}cu}06f zn03XAhC@b$G^;o5$4{ffu=pw{J2-(i09ETITK8S*Q~a*u)UQ53s!rYXTl|e5!@Cb% zMn!u@s_|i;U%46{m3tLd)`>wiqYb%52{<2rk`_{u{aEYR<|Cz zcja(nF=M#x<*#&a>r-(fqRbC;gMQ(nweLCu8`nzVCG$Qi|DV``#0sp>5-iJqjQc87 z6LRRQRsj-Nvjjhw=sLNj62DZ-5sFOg(Op;ULpOiQ(B15Cy1`vZp3fNSE?3(>PDc_~ zDwHEb$LHs>zlrEC@zd(o8%JL}wc=s`dEDPy$47~9tpkboU}4zIQi}kGsy~ZJsfYgs z5Pbk5ZbA)gj?`+KnYmn3ZPbpUir7Yw7gBvrl={lGP)IT;m1w(nv`rMF5 z%s*=B=jiJXs2Kkrn8>Mh)vayIFL}^d{A;kNx^=jfw+Ne^z~5-c->7YHSyLWLNheP27o1uV&$L0Z12qrGrpkj(`^ZvF(wu0#R8PxS@{imjNn zZ*wZPGF2ME*ATph-@tsE^+|0AAZ7Z%Wta{1mxQet$c18vj^rXqmJh^6w;Dk*Zj-(W z)i`{Us)UwyFnIp6x5oX|;eUG3QyYZ;w{~~)zsS|UIYszCO^rsE)S+p@tT6e%16lD5 zRCe=U;MejqTp+FFWSjs0lvkddDOEt`t4}`EAoB%e8aL?Ge8OCX+SRS!uJ8UT&h1*6 zmkV}PN&Y-6MG4QZ{-G|J$AGNDnS5I!+Z!G>3(5h}db>7-HDnM?+@V!ZT!M{qi@dF{}$>lC`P|>5}5DjH6f?~oJ{x0U@qgn0- z=nC}P4MbbiI6Nt@aI(_`){))i+P;k>8Yz04qI=79_a^8OJ{y#q(G1?-23rWM4*jw- z8E2s$#>afXkKgHi0eaGD;>X?hd2&+^XfQtQuD#P00$ z->~X+qq@GjT;{fV596O7{F3Cw|LY*}0-x%_+%X&tCLAs=Kk6!Ii0Xqr4UK zlJslSU#2cU4jMBx8UwpPpV$EjQzP;D!=1@LpGe#X;PEQ=jN=UNo&QKpeT!!2%Fgh# zePUrd{g?(xf9I-dHL59tU3<~>s2&vyl2q&NOV#+}bbpub{_M^$_4t@%_pg15VVpEA zZ_7iSNgu)f??(^uTGz~DJ8Y(U17Uem(Mj-}KQuER?*j243(&+gqtN%&oVzUVyhHl2Rk$2 zeByE+fXAy`Glu5BsdgPq$ph7@W%}FB;57gDu{6nsr}VI|>|9r^Mm1%yYcIMU)vJsA zO*Q^F{okegKf5zbTmBvTAEx_n`9){a>7S|l(L=np>&zBZZyZQTJLRu6>hUfR53&G+ z2eth5PAnDa+t8uAOQL{B7gSzx^9SXz) z&TjXBU*TA?yoZ-{=EHeJXEC~n--8+0=|1W7=BsVENseT78VTxityI720`VObh(DY! zuCW@YyRPU`@mW+b}H#Nr^po=cX zNYYs|ybnvL_4gQB$3K-zuJtU~t_nz*8M5|6d?j@!*xx}foun_10oS5%9(q$hV@FHS zkurY{2P}eH!BhHQHQA-`29oO|x$18+Tqn3cjw0+0;A!hk42{oUnkRXukYd}ChQu<3nQ@m zi}I<}Uk@FQZ$+^Tn@ta+Qyoj(soT5ahZp`u#}Dzr*rq?yF4%YGVAE&?n#nGT6O2aL z6U1NQy?9u~2kWB8>vX%dd_=o?{Gw;TdQ@*ff$p91yjq8?P-nkWXDB|x{AQ9(yUet= zx#_3u>JqubM@iPRhoySQ6Riopk5qY72gJ~WY%^Qlk>U{4=&eOmElwp+>3 zE@1p`xSQmSF<-W9kz+-~=ZFY&dKt48BulOIozWX~eB-kD9X!1Vk1v zs=V}c=8doPTk#^4S;v-xsHZ6SoqrK?EcwmWe;+o*kqnGj&P8SR0>03KN> z2POcJF2`F)D|N+(m=V1ksEyL^uZ{dJ`o$IjHfyk%l3cha?OXe?g>9?t@$G002oq5_ zFN$%iCb|89akN+ZS}G%y2N`52;&=sKZj#Z60IM*wdQ&}W;>gXFKD#crz zAFr0BBPdh5kQpD$M-0ZDXu*H7O=fq6Y<9taco&6TfF~XMP){xe5Z-`bD?%6f^ZkN5 zu=j@n%fm~S{P7NcfM&y%HcvjNG5Z`_i}B}&!Nup-VX`q2l#hJ%LE( zb^Jjos?fB@>?(~ai#W{zVV#u|&oZksN(kk0?^`-m0sifoI3^Dy_@ZNj4ojr%Yi#NQrdv429Kiv^Q&gY@n6n`l110;``4b6j#U;O!z!dvEV+_4Wi{|?1|oK=#i zN{nYV6$31O8?dp}u-}X{;`mo~$^IAfw8N2A%3{XkbVudX_`DG~P{Q}7la8EqU593%PaCO35oU-YS z-{KZt(w(>0+~AOn=8YuVeAyuzInCyUqkYK|dFGvFPF2a^TP5xc`84hs@^L(eM<|C) zW3Ib7bP&w7i+i9+Mp@Q4ffXW6JDvjoax+h(P9`XIcpS45zcZ@)Afp5bB zbb`*&Y#iBS!jNRn+)4}L%pr(9`f{P?QWXUdZ;5N$0tM+NH&2r!;O7&9t<23;L_&JN z`O+YpV;{JtN2Ks?0P3j#ALS%S4=NpSxzfk|*?i220su3cx|ItG)XF!bAZ8wLBz1qd z&8Du&cENBzpP0iL=O`(MI||0Agd6Jb!^37 zu${9tPL!QAO84!H6b>W4M(sKgq~OYLsK}`yeG^&$@5%)tgBHOdJflSktDw$k`dQWDG>SNYZ?(PpZOz`*!%|Mdn&H7xt1D}`BJBxbp(sV-8dwx zg|144L6v@?K#j%|_P@SMp(d*gb61i0-Ra0l{2DdjU~;Fc7oTAwoabXJ@!Sut@CBpX zuJEQ+a1*V}Gdg5GBbnZQ@4F`B!QhgbyOvJg&Kk_4KSmU+eR$gEe{ePw>PFiXyB_Gn zNxkp}zd2;v%XFp^nIY@)G&AiOGVu0uUC7GVTY{INaVz1}IvlhJpMpyYEA|x^%<@&d z!JT?oc4%t=O+9rdwhqU$4;RaP=e7^Wr7wB^ux&YrGmJ9MI|tKo%%q#tb_N`mj%jMdd0I`f zWCT-G0lx3J>mXj}w~r#_Ldl=n!_k1(+F1?fY9s}n{|i=k>>rJSqAj?wsmO;f2wUFu zuh=F;wD`PV(FS-QN2Z>ic$^uv{@UV_>9;|zTDgWA;B8vrD&WZz6-7avdT*h=Uy8S5 zx#ZnBNu(fg;DpQP=o9g4gKgVm#r_M&K?6QruQkB5?uG{JLIy^E9qi3EKh$6w)c|Lr zv4F2_g9a>y6FyTJ|qoXmNe@El0BiJFpeT z49rw}ov{ruplo~)7iVR}X8FvtCbRmOTrbd_R zxt{wt*TB@~nhhUhFk!}ek0i<>+Ed=qXq#N|5MpdXS_XLzZMDDIhmi*oM`QyppGy0i zu3s)EZ;hIXu+n*3IAKkaiNaZ%!hrXGUsv@LH5}2#H9ntk2w~;11RWV`PCc>_8*RTq z-dgw=CO9Pq~X*@SN01rANfo)rQOfl%xg zT=^T_6@o8?XK+gMxFov282%sxx&j^Jx_RTLe7P_b8;?swuM6Vy*+%4toFl=m9Db9r z4%KKy0BmV?6&{LWW4{D9duIzW37mB&>N(^h>&AN9PCb}b&aP*f(Lus-v9HT*WdW02 z4152eh9c8LCAo(35Zw^_m?4Cw=Z6jNxro=Sok-$Qdzv2P!mHS$B2wB;kPa1y|3kKT z><>R$qqgFZJ1+DbIfZk~sc5+s24A1hjz5Pns!qkWY~mwwJ|9QL!oBk9iNCz{P2!0o zI;42P2fsRdA%cY1?E@6Fh@UIdv}Q6#vm-ywj1S}Mqk5c2p(`IA;1l71i~jgtdnbx| zVzPXzl-r(6&@2FJ!YAj|Q-)BO_yX2PJw88s-XL8vdYgk;fGT~Xst#AN zU62o(rNkcZ)thJr;KMOYEU(Br_g9YWh%QgRIPgR80_^o|eJWtQ8{<=T{cmWJTX@Id zl}K)&IZ&6bVCFmeAs-KQ>bZL`JC<|~fpoE7;=kff8v3uGgXd}Czv9MC%R8;ycAvg2 zH(h!6kK~&;7>f@Ic3?PcKO~qdY|Go77)7PWn9cZ_994%U)J=OVpWU!UkV|0(sMruS zk8R?E^Ub^ox?tly=SVCG$02cZ^7C?D4X2MWGL}n-4u&lYw@Pm(Zm-^G7- zF<^e%AwCTzKs~H`(EsoD0|m!ZDG;5Ln`=7)-f> zYB;2v0;89U?&TnXQP-LnF2(AUM<5dmWw@i6w~~g;EqL zW!})JeKK_sqI)*U*MZX>T&sx&`8LU*Qj-iGXp(dBR?&K?qD~`89mw{pZwV_j(YdPS zp_D`qxxh{I^9YgGsgLi56nT@XLu4xY=FR|ZKxH~80&UP2nbbg)y>!wpPtn<=i53Rb z252%sk6nY(f%d~;RHxPIYA77bv!)_pSa~fRUo^|k-tPI2u1Z!4`@Y55B3uB50#@;9LG44<*TN)m9*I70lpddHu zOEjtGyI^W-1&NgW;rV-4|D-7w7ezZ5EV=)<82B^>!#iSdbcn-AqF@4tO==8rsP20H zhViv~yY`-N{OGbNVZNp@(7{XT#sP*$6Gaq&;>V!9IFt{d*5|q zi1Ql(X509|)=|r}a&e`3tv^!e6KwI&17!`aC&kZY)V)Pw)ztZ1VuV-V=sW6;(SoF9M5w$DrKwM=deu!bWLDSZ5TDg-`RP~=aX zL~E;Rut%A~&^|THWpl*88o82or(!P1%k4_uar9^L@#(v^D}8Bd4L)M3=>rGOBxx=p zVXb)3-O)9BHO9L(%0S-!?V#E@awi1I!K4QEClE{F3=_evEb2U9len)Pl~ny3 ziwa?iI_t1_ulPP0JcH93=UJWhi3Bd@QaFVaam9Q|=M$hL=o007V zvQ#d24LeqqSYLcHAOWDfMSI1P<5c<{D!GjU@dgp`2_5q-Z|@DRA%}UzqmKTTbHw;r z(5&8_!ckr1O8*DnbV_|c&~R7=;-K&Rk@@G?OsLknc7^JcN(J9o)I4z@?qRFJsyxD0F@Es$eWc()c)z_0vv?%mQ<0yznj@_0!G}F+ z{f`LF+lys9stNbc8BySmnqSNDA}>huO-g)p4KB*YcDN`^PNeprRX-AtXoLOBH_XV$ zjvorNydQ6Jji57tochZOG1{-XjW$oHVE+Xrb?S@h;P0wVV~f0oq%l!U2M@TC>cVYU zQO;ahnmxH>PapZ}h*5LMwwFQjlFN^K?xvo%bF1}fn_GdzcrfTgK6YXd^kWh>9g@79 zF%`xZfdS8g?Su-@K^ve4?iyc0C8o}D4(igTMxse#x0>)c*-Y1)3xu@I6t@Xi3o|m9 z4?x%FHbrbXo^hjKHqsL|>dfj^O7SP&b^HLH#kTcSQ}n-?7n0yr%X!$rAj` z+@@-R^{Cz4=&~^|=}8U64l?39Ta5eX0#PECYvVG|jLimLkFvbr^9tzS-n=8^ZJzBd zkXQgq5(^_^)czK|G%eON*qQ?VjdZYGaK|^X%&nTq_&+b^`=*A}z@p9qu)IJ2+@*$y z>EQjMUyB-k@~NwU=bBVMT`*Q&Pq#-1rxVzxUqL@RkNWoKE*eSubPu!^{NuP`d?MA@ z$GERR_zc7SmrxL`qNHE63cBHZLDa9l#D%UI?!{E4&%&4Kl@FKF&LXbwRjy5Fx+P_YO4Fl_w*(MgqoBebHw$9~7YF*2jx z&Ds4uYZ*F9gs3wJ9DCom91t-S)Cg<`K+(7>Ar250st0mR#OFPpdi-ymgu|*R%gm_H zLIZvt$J_cclGbRDQ-9Ah)Jl7S6n$2V)H6@Gh6m1Ac+@H$AB}lGHFclbKbeoMaUk-^ z%FeYHqBiXBJ`L@sPrJ&+sXglvDL-A854qn}F3YLWy0@6!n>tl@w5Ur*15r_hHJCrT zU$PLBwBcctuF)I&FaESsC&X{P5S8t8^XC_Yo}4#bRnH;y&#@xr73zq3Tq8687FR83 z5bl3OE%kx!ZcrCwh7*~gy7c_c$f#0{$|JT+HGfan_{;JW(c5>P-D|N^{S8AFGX|0kBWImim?nXI2!%|$2ZP@7&Sb-7_zrlL!8*)QaaQ? z()>1h2b`U2MR=RS5-=oQ007df^oNxt9XoCRc#~ZNOdfC^XAMfn_b!b^WAXeFgB-Az zSG)lE)*iukL4jNG(Ry;$sJ%}payEQrGt@!L$_z{*Z((NWzfXFe$k{bvd&;;sNu*c< z4;!@+@>C%w4aXxB_=dVZoS(o{h%J6II_fhLf(jlv0nwa1Hb5}=STH{3$@d<3>Hb@0 z+ScN=pth|IhA@q-pJiE7AOq60GMY@QN|NNt zbFk|8)&xH{Y+ygVH_kBjeW!%G^C4-vOi(hdMFG)2&`(EUv!7-Fisl1yFnn_m`lU6_ zQsXgfl8RO{Smq4-3ar@k(+H#B`EvY(3$S`+!i@b~vaKH=If$1fVcs->C zjqpJIJC~qiNnF{p8QVZS^id9chdC6D2(HlxOGB}Ulom9Hj8L;#(8T2}cel@iU`)|q z9QH0o^o=JG{OSl?6{iEN*rK26s0Xa6!%hd)>?Eoyvuspl+&)VdEEgGb-<)>XQze4X zE%ngHVEM7r3@jRJ)AtLs;~grMP{-oMSS3+2v7Ci!IPZzPC_3A&TJeLjPyeFch{s8a zdNM0!o2TkcEAZH8zJ$NT!!ynr)!l6Pq`z6+hK!8a17042-;kAgK$fCk2W~tWFXNA=XhQ&hi;XeIiq`QNALRZU>~e`QW*c@pg=QcDjylF6 zi1+Ks6H1g+iH)my3{5+ZLff3emTI%cV?WRlj^AVGneE{mIN!$WnQ7kpU~S7`o58uBVc?=oqq?X6Q=UX^`0IVkD1?o}SeyRgcQR?)Q@CWjC8GXlq znlCxPX^Qd_tN7va=+3;|a?1cYniD?{M;Z{gR9F4P>rt=a4+j0!?seK(z(!4|lWFw# z|Kpl1C8$4A7fgQKRiOPJT`=1{vCF}W)EeOz~#lxa`w$+jrhv_4M7eNu?&8wu^`bjRp&?S%BVpju9$a}4V^uQ z4~KX!W(Vc~T8;)JJH71#kNE!y?iGRLus`&Wk%se+ZM?yCNCPLoEZtI0p z`}lOa$CPg9gEx)YyjB+C&-C|=P#O=Dt>p7fB zj|g~E3b6K{s{GPWqtD-Kx=B4`J~sYi+!Dm52RlHxFF!v*akZ*is=-+oO^8SwFF(@E z0j+pySxu&j2GmUap-_w=8e zlI~4eB?*ekanGQn1|H(_;6KbvbC>*tKeu|dw`JQnjo!8_ZYulOx3VfbQCLL&bib>Y zGGWf56DPX&;%b0-R1B%)$zS>)8u;32m16b~`0l0H0QhDKz7qxC2CwSVUd;l-7{5vY z;c1b9`wlS)Nw?`rNbZNMzmV8)`h)A~53A%OTln_Fd?@`Pso#Y68>_v!W_->F=!@KN z#+CL^`C`47-1iE8&sgnoQ{cj!|J)`aQv&Rx=6z>#v%SB2B-br<%Z&Ks6tkXa=93?= zJ|jmJplA5q%tYGy5G-6FfH-@ljUNi_??$-FX&JdJpP%%vxrbQ2C{i>t3Rrbr>F+F~ zzMS;8Vzoc51}v-XY+$786MfUpcX>;DbCH;rn0_L#w~eHX9sjr4BK}D|c+wD($V^)zY6?+S`}*}P-r^-yXQoR9N1tiz8&)X@dV*3@VeTW*T@ z_4YN+_=Sl-ZUxGFDjAb$3f?^y)ugXPG8{WpWX1M@TsuC!BE{&6{Y=F01W+E)?6^#$%e5{60yK)|kKzs=TtK z4?VMzks5ETtk`={TNrgr5p;~rVtuTQn#f_g76B?_p;+gc2GO781G!P<7G(ldy2+ah zm^fq46@|9DP=aPa!>ZO!KGnaW2uvCTUft_Fd~^iHYOgdj-+qIsvEc5<3)QPTCwu%g zc7VX^r>VUUX!v*|dk5%b9Zy*jV&BngolQ4Z5H<02^GTBT7MsrmKgne$b!dKQDLAvO zv$>?TcDLf9nhRsKGtbe*lq$ShEu5re1f<~y)u7mTgoQ2`DDv|7r9q4 z`Gnd*m9W~xAIRYM5g)%+`yrOA!2|5#QPnP$14Eg1|?gOhnK#>K>LA!gfUrxrFWD$a40i zE;iBFO4CiE;VqaG;!X(07pXw0xP@_E9uNS7e+82{8FX?2NYRG2sBdEH>YpNc^&}l#p9=9M`Sw4n3j9@K3@nQ+Yun#X*=QZaD(u7@T`L8ox0A<5 zQdL~{&2GaV79Kjl?6>aO1#yQHQ%7O|$?Gct{%RMB5@KQzk;J&aY%R>Wkj$x=Zan@0 zW4yqMk;zcXruFB-oaxrmiI+z(W-aEswvoxuhMm)_C0nKyZVg#Wy6sv|T+`A8F|4W( z^}^yB#Nu8H{Wi!)u(&t+APC3ahu3L`U?koIuWLS?iv`tv@1r$GteAE~T#~ru^qgqo zE4ktQzxO!aM(RpNYu>fmR&xa>Vp`9uTEhrDWXynO{HAe$>}c(xutTl4-@u_|8s^DH zWrI}3%RIWxqI7pP!FGm{HI@*U=_JI_@oB z$gQ0)^WL}TaWh-Ubekd`{%qJvCG8qy+Ot*#8)Y{#^uVdO@nDmT5R1+G4@2?OswVYy z^6G@V%a_fxto_p{VsMZz+JD4-`c@+**lh4|U*~iVGdoi!sHF2s>b-qnXkRK{w~2y3 zR!USGo{M|3=G}x;)H&;)zJQk97H*yvCf&HM)OmLySJbpE+f8WX>84gA{C?xEEJ4=z z8ZPl(%r#CuxTQ>RW=q+*-iwd?DeH@~XaoIf0 zKQ;7>eJ7`sH_Ep1eHBqWoI3`eRj$3is~5E$p2;$4)E`Wz+X+GeOc-&%J^RZH9`y0= zo&!qt!3fm>R?INY(<03ln;A9My=jp*(8OOD`9nvUi*ttAE#~ouLv&7GR}kwLh>y8* zv0P23^!`{s#V6Q_2U{tyAr;$!&OF-TKNeneV72FdX^f{rRIpG&Mv6{g=bU5D)_u}8 zyg!HUF$H_u6bW}a6rB4F^1B@OLz$_HH(}P5p&6sGXWix5Rz)CQVxl@z1-K4+d+7u9 zb`b;-do9mi*lnHwh2oMCM@I`UMFOeBJ@+d#2SyCLtCxc3d6BF3Rc))3zBkAz~RnLxJMkMHNUd5mID}S9QbnU#__1f5~!_NMySGv+8tM)oay523SdewPG zMH0oIQA~7iUT-%`%pTkNM0awQZQTiL(Y9g2TX&vnC+2AX1ZPE*S+SLTL=qk*+hom1 zR5^R$@?5isr6!ok$WSHD`P$AxWgN@j#Gl9}LQpoU?w&nX!}>keHLLx$y7pLC@xN%y zfhiWnuUgIf4cr&BhID_;ddkhR&Rjq5(^~(oT%$TDQ#7ul&Xiy4eSb!0Zzto&pbBv@ z!fdUgZeqz{6U>zLDLGwW;1z&whRZ91CHUzwp19W&B|==z@bshE&&WU^GR!eDoIwFq zn5wua(uIH{>xy?$B_sc!67+ZkRVQg%6BDAT*Hx*m8gxZOkI0LwN?k!?EXXg{6#%0D zyipzUjDFHjCR_;Ne10h5_;fT3{3X_+k0C7rKW*OvD8LW73jisbH*U{+GW8Nc?t?9rU&+>l__z`RG#$5 z{WWh^B_*~@51Nd2*?M+`zz<*rXema_G`8ql1Jdax`-Eh~~ z`*msOt`EUju{}I*)H=v>)rDSIETBSO<6f1NRI`EQ4X~Sdu&b7C)okD-BTCE`LD*0@ z$BjB#WLe+b6~~#?=eH{8cFy*LxV}n`qj)0-e(Ph4T{!?CQkVjWz753`Q~#Ef+lY$n(iAg6a%|p zP6$XnAw`a?z{jfNT`k9(hS%#(piUR4iGq*)C~M6{_{nj9HP7_=507WPI8879#d}em zeO~1!c!7VydsC8~py_0N@26e7kQUBHlQ^PRx6ZvmD05Ct9ATl*GKOuj!hC^XGm?c5(okW;rSXmN#xth%|6dq%)(A`GxNT?yvljg$`tlHxq+;NkRlyYyd2v z$+YNR#|f{=)M4mjc+{GwY90}_zOeOB(VVY=twAQ!nU(P;&D!>?S=*k~b;#M>wQFps zYPWNwu4+{XJt|anz}esRPElReO6N(l!p(TE^7 zOXpq|#?q0P!dAw+4!{9pL~EFgKFL_^spXiUtoC;di<1ePI_!$JT=KtoZ@SEzkXe3w z*CDgzY0W^IZ=!V2j@?E$TsuK7IY|vtBM&NQ;yisCzZ7ElEO)M{I}Fv0ZG*AtAjFf2 z*(Ik%i1SZY&S~VJaN^$PKv+}O#cCIwCfnHgqRpV~6tCmtN0TLHHm@l{M@9mEqbbGK z{*y#!3q%R^&|U-kU;i0_^eywbdy%f6R|&|x;C}@&`{`h6qA}Qav(9v3&Gh-tx4ei^ zU^QZ?(L_zyiS|am{ZT=NYjC8=%40UrH>ExYD|5SXOcG!ICaqj~uP0Qx_i?vwZLz7Cx>u##$%c|itk>Tij-W||3e46%UPe|OsdL0{ir_tUPE386nwU7!xq37za#+)y zC5GqNJ+Ijt(g-v&$*lIIU9)`7_Kq2aZELion>-yE8Bezj8c^GI^2_vMd)IccVW{KlU{&XmpMFV4^;UUx`LdQl5Hd z+FI!2D`47tCt^)B6d+b9g%cH;_V3_H`j1|7iQ9|ZoAD>7zo`Z*G=JCl4Nf)DB&HiQmy;U3S{rmj2 z*i2HfK~beVgowaFJN`M~I$+1{4CFK~u+8NDIxFvAr|Z3_gmHuxO!$G?M{JCW;H#~r zGcU7y*Yb6bO@=T8ysz@FijX4M}cBDl%VGqi6&$~*enW&g@` zJZ!h)7s2@@d`i~->AM~eVtq^9BKlF^W=-LG<$bXQlyVUFgXHukwIy!D+XWGP~OhrbE;(w{bK zFxe#KjX%b1?yV(uU|(0h)sEYRVe+;jBd?8?iL>5_TawyYWqW&66Z0!lZd*&Pg2S(M zwmeT2kmfEsaWS}Dr}z`T3tCI>yo}L%$8 zWy>jiIGmVRjirA|b!^oU)|X&wZ=Mw^7R+64^^}6Sd9ls{tbwPd(rS~<(y>DvF!vHS z){=?GR&Q=TwQXCA*1Mg^92)Khq}oP0B_zbO^8$udYAw08p4JGt96bt2H4wk{3T^eA z3-2Yz;)S3~7Gy(Xs0NZ9?f9|18(P_&nbR^}CdpHUf(j8b)J$j1?l^rC1ex_ngqLcN zOZ`UHsUQqz^jOnckk+>6&c#WHl;VSk9Mcct>hzn|(rYjCvsxaoHA7B_cNvm{(Iepx zB{}f=fh1=SCOMPe?hHe79;JoV+E7aK$V@h>!DMFEuMC-O?I*KRLuO#Ek!Qn3HGL?Z zK~@k@Ke5s4K4N2{4JI~-(kC`M^<@n{KPLbOQeUS5l3!5<9Q`htq%En&e-1-PbRArJ(WVeoAhtKZ8i6>s1AX@6woVsda^*B~Ohr<qXsWsoBRk;`FVs0(LcfLVa>6}tgg5*J2jy4f ziu?`IMJrDUxp>Q3WIFClWJVyMxP_SS`_R$XYaEc(4Z=^-({*DhP> zL56&0wa){1Yv~>0)cftcIfzxO2F`(xSFQ1r>zzwcyo8q>+j`KNS76S`jP0;OMqmu} zyf$lEO^>zca)>3`{<_t61>MqFjSUg!24V#+5?8{Vyop|UH@S1ZVaG416xB;%3aC>_ z-ikJh+`CCLDn|Qp5p8`3wChtqFTJN$SZrWca$k48)pj9p#daRVtAaoAbO zx#J9v^)uN+@3UD17O)7cPN&qU4FoVJ7HnTQ@EigKTQ$GFUfv)j3qdDVJ9V6750oq( zc+?>^#ZG;e?z$&ljF9nWrap*kaDBho;?-yO)widZ`er3tK4EI~v)1+1EUb}`uby~d zg99UAxQ|6?W}_$E7=5!DJe|B-8q*EsC80LXtI7apExDjRngFTM#OM`KRyZG(UPGxH zn6)1Dr9SChrfhzp&ztL`&L)KkaDB(6TpS@l=-sfhKbputi+qt(7&!+=c{mVpjzpb3 z5od=9Yf$VC`Txvt%ap)XJyp!Cr;4rN#N@Lvj0WoBGp``3z*}g~m8u*x6kt}{%;1X$ zHV;^Es|Ns|>7tfhSQs`-`QIxHnP^P#T?0jGy3G3-axhC`-keWEG^`;FEHur0LEdjH zPn)z*H^DCP3x*8^X}+bt2g|}D0eM0h)W~9H#DIH!8b#M<{=3_1yxAzKOD|@2T#kig z?~KG`EF|+-xX(p`)@FNPod)}SiqL=v-r^l5M4(wxpg*rwq{zL6C`(ZsG&GPV1aWoh z@~lo7bIA||`l6QwwjtU3j~B_Znc2@y>0D6cRV{0&+Rwa5+)?CSei5{_ouz-`jakM? zvLxrY2d*<#%e=uajDEGvds)2QGf_~%_;Y-}m-qfE$qnPqWDzJLP=4?tFg*V^8!ZOv z@N@~=RA=)sW`lorHUkD<;e@q|$I@M^{Q}7zM)Y#Xh3iZure=0wuw%{)42@V^Zud%I zWQNboDYfG>gLdqWKu${}NpfkKnjV)F5O2WLbY2eenP14<#wWyTr+8mi>zQ*kgG4dX z%ExL?^InYgUR2YqAqsl(@392RAc%PoBf}BT5{QC+OzVQeiSrt$bMioQ{k+Qf^RiOZ z#TQAJuX7gZF612SdK2^FL6rSzRjWf?sge5O>AM&hS2@pndjDjLjXbB?xqDej3;QJa z-WTmMwmQv`6kFJ;t)lzvp0r&?vXj?Fc3f?-S5g-{1#-l@d{s+|B_z=qmnS6vExhp!I{ec_1^ar1&Dc? zxV^%?66dCJVBB8n{z6XLH#PM!78o*eZ{rbnoF6M6=&yL^Uie9_bn56)^!v+)kzbxG zZ#7t%4nZ~`y0meZle2F*TGz2E&SKPH6Cf(F7c|eWkHn`}z(Fs$h>*&L)GxtrsJAQB z$|(Y|xNh25iW`(t6{*2xigRMu1QhPn@{K0vT zb;d-i?YzKRGWpclE>6$h(|laSc_-=|U|})IEO|Bh)YgL|thUcmN9(~ntBnA^9228s z#-E7X_6@?TVWScvSci(-8!_`4E-21j3g2isL$@Hd z_6Z)Mw>PIjZ<{j^xSP4fS_9}LW?$v5!DE{Kmssuk(E{Of*rVeYc>3ZQ;Wz=X%gl4^ zj^+!K{j)!vcFcfJ?{xzBj%1Vn2Wua>zQ&TeZ_+%wj&#?F@6C= zckc$F1L`*j^S7`-k{$({B{qXF^oIxw6Du&=f6_n2%mSAMG}UL;@G1-hy{xd_w@|Z} zi^uNSZuhRXPxDO)m4pm6f7EXs^jA~rcTp0 z&U?vpioa23Yx?<6@7mM?o|;w%)t+7)tIa)DiI7X}ckn<^+hDBJ6gx1U+sAU(bx0!5n7-Zow>$=ffZ@BHQCO}HIIqNBdq22u zWo~m`Y#qCi<`jZ;*hO9LN<@^xj;mR;HU~fyfz)(c>|)g~>~!~PqdevHB$ zd%INjhZ5NzfJF`k{JdJc_bm@JU!N3s6}BVsoo+iD`edlY=UIES#FTCFg!(ImWV05S z<4cV2N{9yeB9Y;t5F?$4Wt#O1*6W_m7wOHtYm@1R-7O!b2g=emTnC20kIv`k=YLaD z#nJNff6r_hkv~_F<*R9qgN}A5l`OwgH+i@AF`KIbAv#Qy4fTo1n28eZc@4__SSYpv ze)4{vb@wCr5Um$d)TFqg1q%8V9`*Y$zwm|YFM}IxkLF!R$o&oFO;HewRB=oB0;;;X z9$DjUBbr2k1_H=sP#zh{9{(;ACsT{1J?!C-AkydE`Epf&^MVMvHK_=jemnVg3u|r> zW9(_W0|uhUO`*0C9KmsMyl&#`aBTa?=^c|t)Umpotbl~U(`mh1fNaP5pKtiU?|B$? zsx!rM=Qdm%Z<{FmP%0WPE|1mzL?v536?KYc^is3I%f-3P6NT5gmtLIvlqrtODCDdS zJ3XmR=1kaOQ}`;lxx3|a1qg5Z(-7kY)idJvn!>gtH5`Rk7%6i9Mlgyf7~zId5XAthkNVUNkZ;&IWLiAn-eg+j__JV+=X601nGugnT#(f& z{#2ts-tm9f@(TJ|*U?=5u^Aw?DdNb?V{9jg9!7tE#beYZ2ILtIg=VY*iF5|GV+Uq9 zg3Ir#wdA6DJCTotJje@g6o#M$bY$jKKRPm)U_^jLazuln&(s@|AAs+4JASzxiibu= zoGCshh>w_+>|L+=;lbX#6fts|lGV1xVBQ-6PmUikcV5*-b|cL6t#=nEW*5*}UHR#u z*mkUE0|qYx7zEKjN(L%VPG!Ft zM-rML9x*dHO%$136x(A!pq$r{3`SEk#O!_aVjnq=+oZ=G!hGSr7{g}AIfp8p` z0JfA<#Zw!TfO0)PxUU8%u*EGWs6OJEuW0mgi4!1;xD{i zmi}o4eZ-(?TjWZONN=;2UT_&<6mY}NWSDKY(rrzD5}^ZzLA#SaDWdmKpFvRupGHbd zBX5=G0sYP5cW-$7?nj0h`Yg{&<3JY%alOTWkSbOu5T=sIIc&YqRu}g7Gi6xjzBxH- znZpiD{*!vySAH{*HDWj07Y45pL)Hi5AN&JfA|KJQGl*2?zei)NMTtxCNY$5;;zs)e zHPK7i0%coM*J`;dNsmNOw4K?di20?S{_|pU8C|{LGDNqd^`OhexiHs`D( zqO0YcSnZMC68O|qyYE~H6K>QzHdgz(c{72G)d4_5S+oPO9s4^7VtllEy{A_TOL*UV(Y9NbG@ghA+k9$QClQ zQ6I^{#Cb}#@FW41S$Zixzv3cd;SMOUX$1cF&gR0T7!l`9d=Cm3g>W_8ry0LTLV}0G zR54s`fWsE~cbSIr#fvnQ6(tPi1>R5&KYkZWfE$p1Q5?vNPZ-BL zjh7dnU}jxP>Kt<@n1!4A;fS0mRjPva5VxL*xRKbB`W1ji6IZYa&e;-mR!5v|QD;L~ zhjYfBf;!srv_ElFMt%#m9?G@u`4TVCw8G9dHZbwk3sGy@YQx{y|JU+sqFv=I->j1DT1#)21MgKk?^=TMaMz{SpT^TW2f6AD9HE}=vqCio z=G|@d$8F7IitG!mXd`3Nns98#$f+Iqqm<7oF)_}$nt>z|OTBZR-Fpno!0<6^$>#$Z zUS_;-p8yVd;dsnApltVtGl(5wUh>x2Fa1d?K8uOQoZ&LEWOKQVeZd?u5_Yzd%Ez=? zN3r_4nhDKgqfQTKC%7c)bfs8GSQ*y?sTcnhW@|@c;(5S%AZf=ZNEaSWZQ{SFk5V@3 zaB#W#@-ptk8R5P@;&oT#Dw|?9I3}PUn37(^4uCQ)M5hjRw~(LYx9W}Yx14%V%bT$_ z+gSaOH`G6eZDx|6=%;P;*i;oO7S`s1Coaljg~rozpKUxXj|_Ry;=TCQ%04g4@(G#U zMYjbt>7FcR(%pyD^!M;j>1D&LfB!T3?ZFd({J)~#`uY1_785yI{ys06nDkqzCPq)e zzm0zDZ8Qz%&6b>E%a~S=j@gZl*+6}D&YF;k!VCAv2>y{!Y&rVvVf0&)4rcV*2I;p0 zm0TpCnUa8Bjpj9=+_FI-nz-KRPEFz1@(MkfO;ZiNDoyG{U!A=Ir`{N`H>1wpd?}^# zqq%Qq*{7t_ZewZz&KbwZx<7xEr_TEF)qkSXKA{?zbQN_Sx1e2kN-XwDnw(9OE9u>t zR8h6DE-?q?72SBr#teTtg#JouTLNEp78tu02HoPu4F3zqj_|QEctFj?4Q>2HiW_Yh zL|w-jNN07$O`-TGhby_!BgM0AC!filQ5?xHPM$wagi5=&E}swU@|*G{JwGb`wB({o z?c7NIY!%3;mNKaMLzWq>kh4a%c@#=xgS`Q_`wN=Lf+%9z%V>1&Xn(s0%mou2I4N$x zC|BG_Yet;|wBjaYdKf?*!1{pnR}7ZHpns~K6tW-+nQ!7j9`n^PO8I(_^U2~ zCf#XB7Y4!aH3R=O`3(&9T%4Hr5!sK!9N>tWbE4Fo`eZVaxHb?;%&LH;?}m*ab_EA( zH{qHEzIOAldg&*J>qCsp&qeNDJR*JC4NJu;v>Pn{L$sTVKBDo;Kn5xl47cr@=Nq)eS4uzS~}7R&a{)>LRM$8CcN!UCW9coqvZ`b-@E zZPIV_uXqkrk>5xYBgya5a<$G`ZKKEZ7zkf`=zv+;Vng1Wi z_s=4<7QUSwHU! zePX72`5T^`82kYFzF$p9G-L_*wLk~Ioq8EYsm-2*U)7sB&U<>{APL_{FQJ~D1mktj zYsG%Bt$FST%J);&5oaG@2b88peXRDXcO=EaV@~ZC_OanzEMcDmnS-`evbImrdThtx z4jvB__T3p3h_@>|?W|AcZ&{<>4=?jyi^}QA{6I1@ptZO@GC<}J6HPyf1>ckTQ#Y9p zwxA^BWsCd)DG(=6%oF$dNb74*g2FHo|7%(*5j#pHC@?4)i9hwAKHvq(sMx1^>I=il09j$8CC?H)%Fy{T5C(* zVp>>jKQ-T6gMOfhh{?FAdQ+M&^P-$qH;lG`!^gd^2soH~S;_*M#Rn@3nemkgUPEhK z8ou`_DDRV1+f%|e2oG>d+XCL>HE&wn_u3^PNRy3cT-j7O*(K#V&jO98pUIAYfKD}n@hfk^tBf!|#L?2Pfy_b!`KpZu@>Nd*`2hdQ!;C<`2I3>ov-{lo zW|ju>0sfUWV?_jBR|ezD^kP{5$_?dcAZE(_!gyM|S@$CotebiO{wSBqDcLGWE_@lz z`XL!i05EmwP@ z*om->er|kMf}S5_&9p!lU(u9Yt}>I4B`z;_-g>G3Xz`VO;aZA+InvCr17@etWT^1# zim;Q#?D_)o{E<{Pr=?hZL*X@W%AS$goH83-l}KxaS6Pp(R;eH|1ad^HS$_t@vYbBM zN}qlgpnrb9R(ky!Z`WK48xnD~^yzp*fcAhG4r6$`Ec`8cHbm+l+zeCNx7N~u|D@^?F0N#fSe^;)x=t{#>juA^z zOHmSHwY>}jo)#~fRx{RWFXcUf`Qr(re?OYYFCo10{b}*SX*I-B0!FXt6&p8RPNx25 z7(s-63;xEILa| zz3aCWcd#`B{_$I3vKL!)H$!z8Bl`c+0}MDC<5}cSygktJs)lS-sJgR--1|f{MdBl} zP}BTb(H#qxwt-+c(wG&wdF*fV=xc%>{jX>uV?>3ML88s&v(t+Pj8tW2q#8bu%&euK zmy?a1c$Xc(L9GZXcA--{^ml2XYFlcuMj%n}{kMmws12!o8j}raV_$yJj8BGMtjE^V zHpwrJLgG|AWB6*><)0m2Uk3J!fAfyB#EkxQBtIvW9EKGQFW(h={HIa;2W04vNZRlp zi8+J(E02jKF;AQNXslUh6$~XX_yCxP7>D>*v;|6|)0FxNkcWFYTdkD~vhK>U(grj0 z6|cJL^;X+DNXfH5%D>`ooBH28l)42wGwu{6y@|w+qb|tj^K6@le|Q9c=!0ioJPl@VCw14?*9|Br;aP^#;^Fzsj(Bmj|EK{x_N# zpnWMv|G;TbwFZ4mt`*8%d~|(mt+hzPg)#u&iBQBIVLSHb$WTZAdH7E`FDV~gHJq3- zO8S`MBwFd8LR=Y&O9-p$+%EB{Ho^`hd0}SA3$xl;gnJa#OdEP*+Yfi} zf^#DbtA*V+!0sEDyKkS2GV;4V?aX6+FF$K!Af+EQ`>hy8Gry{OlPPWI@(`Z-h<9mX zG;p4wTr>Y?K>ty~iU1$?7`t}?MM|6pK3=Yi0?ng5CEUci;Ga>%$%07DYb_J2wsH1u z=Ittk3koVVsai70fIa@r!rAVw4O z_)!{%Ne<8tH(0V?dHP{M+!^Nm0s7$rxW-ZS!)GOoet>?+8BB00Uq2kE9A-2t=3^l0 z+yNguuuT)-gVex#)SQ`ReLkb2j7D%YpV44VVHh<~JLG*G@Wye>u2$NR18IPow{D3) zQ#BQtm%}@d3d{`ozoF>;9-#U||NgraJ(G@isG5g-hO23OpQ}lV-mla(biE(I(UAZC zFc(WT z)G>6|9XCnb@oRsB)BG%pJDU8K5Wy`d5lB%LBFdly3A@ieBhmgbs%Wh(-vV8GN{VQQ zh2iRe?5{0fL`1ipe1sAs-Aa49@zeso*t=OX!Gwde+fZ4~sGsjxxG8PgV$ue5#rmf( zQ1%z_i6%4Knv8sN_#`m>Tk}DG^^5#nq#mpWn$&#ORPV}5nWOHg&5WgR_}_t10@ z3*_e81X6|CS8US_Cd#t;^B%-7q`5>0M>KhuSzNG)nvTnga?3{;0mPC1ptoU*{od?w z0NLr^m|D}n@_yK1q=$?9fE^k|RIer**-&J22t4>_SB+V<#r|4rTVK{7wSQAMq@0l$ z8kB_Q7MXy-*V#x_D_d)?RJoRqn=YW8Fd~BfSEmSiD$*@WeSd_D49Syr+j$pIQv9Uv zlfz`s*C(a@&87eM;Lm|1XugAP7-@YBGz~`PP}vgqsS`c6RFRsdafA+<3?1}=(1($M z7lX&|u1y-hfATt^GJ~w|L#_+?F7njKFd85R2ET?R%Qon>$!cu3yOHGy8Y>? zBJ2`d{pm^}&jW>fvzcaQ8Y2i=&#lQ6e@T;%O5_9~O|8QT+RA1aI}&675;KCtRCL-k z>*kZ}y#c@sF=pl{*^th%YYxx*8X0!{J`07GJloyp171sk zUDKnya&!oFIw<`4EQZ@sk(dwnTfhyyxlfvP$iS_4EZ3#!&f!3zbbob8;&Kaqc}5mH zHG6MXo(4NL@`pQNH$3&fdVbTVBmfM^kLJ!jk0X4(${!lMb6i$~SPRLYIgJ;PLcIpV z9a%@&ohr*C`^oku-Py$$z8p7~V&vcQ8oP6ERxQWy z+6E1@V`i_(7a=Zp-)fWFF5lng-by(N>Wuk=T;Y8z==w$$(4XU>dv%$bdFn5a*<*+- zgZaaNS)Np@)!8ZB{glxrg}Zh4P4tglj>>tXNbhf6dOhZe<~v&tjc9IQPw+;q31vb= zy@3qkP;`->`zC9OY0t4)2K?1K*w<0GyEvUpp%~i*(U7){b!ZlrOtxwh^SV~mxOUkAQk~7YhUJmmfn9dbI}x%Qvx!t~Ma0nd zq`yt!s(nO_rr-JSUM0@iegOtHvgMONu6c?{m_IOEKIfWoU@PYboZc%Q9KW|48pu%& zzN=wYethR8=F(&@88r0f7B0fv#kVB+tq6{L28V$-oIT(coB4-Kv!YDkE9L%j7&CVdwT>P0BMS4`O69@@b@IxwYsU z)E`c82*V_XX)Bl>i_PMUME*w-!CZr4b7ZYfFc=e=sxk4ic`l!33Sj^nW;8(tm?S@Fsyh6=e#Z!F5E2oUGy=7VaH25DUYv z-`}P~1J?(iopcF9%rW{ou^a@|vyKM%y@Fr1rDHk5&#HrwyS9&z)PS7WWn0r%>s6!v zl(^sY7s66rSBPMQXc#sU7w>1Y{55NJUZ58Fq^-PFz(4}kkU(&``-v2sNJ5%Ve=Ax= ze+xu^eDjc(JtpqEX*edRkdb`O?2xnz5BX=mS?vzSi`Lo=FTioEw(GgcK6oOLk3Jz9 zK9Q04S=TP~D4%t`3Hz=4X6Sm!=gi_Uz@-R`6 zl8j^*m|5A$BltZeed%`lsIfu36rUy>vNQ zA^%l0!4L)*LTG`NNAVZiQ1>*#B@A~d!)@rZLPk9%uQx85Q?q5!>;S@4grTK|YHETi zlsSm18q9=eksi$+0z->bS4nl%s=Mg}s*8%LLp=+i=ho$<2ukPEt3agY<$115pf=c; z05;amtz{SgvC%~NS<(2T9!4f8aYxkoXZDBQ81V5#=p`sIDAV{wYaZ@LOFn4%0l8d{ zjuH0-Hk}QJ4nLdPa2Sz+)lNji*FsA@Xqim;DQcrpp17n& z{ixL@Q+3o!e7{aPsp`Zz^Nd+JGx#FC`DcGLPFkqdw$0QOKwi*ZsbjR$U;fMPZ_I$o zH$>wdtrUpVtef*jxaMIE<-9+K(s zn!hAH-D@jPK3E!x_zkH~YRF}OaLvOHtEvB>sYgGgsr8ydeWcKTMZediAJ$(ZzeR`^ zno`1q$X`Fpw}G(nK(u>kw(MudpB$8jA*+{PwHhzo=YvDIjObW zQ1Nmz4f@dPpC*HS=5RrQWZCz#kLj(TNAAv(kh^sL2P56@Ws>>YP`uCCz7An$u{Ak< zT5Aujg01=fm9`)82rggj%Y6P}ksQ7de{eDFw2erHm+{MYTEy~8Jon2deE*WxZ!5;x zK>#`C{`0)WtBuXr?une-9$rY&RV!x(XfY(BEh=F)wG3oZoLI(YY@b6(8l=dai(e354-11jwXt$BJuC|oGH8tDuG8im*e?(FvFQ5j=7A~JZPri z%utNP8wH`>&h(Fv=Qu{8m-*hA{~|uQ1yS zq!z3-%SP8*Wc%7N@>xbLhkVpP+-T?S(Jy^Kk`QAKGE|(h*@a(dtq!YSR7UBxZC1O* zZzM4zxApeioEGZos!nhAf+MXZd1oR_9oMiRv} z5x@R8EdBVfGeMLzv`yPI^K-zVrP!IjMO-qkuCo&+eoRV%tTH5>deqh|AmLsMhzV^ z#b3rf*oYe)LO_7O ze)khb%l`9~gQrn9F`j1L+SH&6rZqLVKhtxLg*%j`P+MTceSjD4+g^2RF3zg1TyM`l zp3el!&n6EKi@HgN$+1Q{5;xzgXW9_3K6wyl zp9SCXD%Rr{TVRU)m}0(tL?HL)NQFs&FbVl1ItUb{8ZH zWz*IzP#l!a&}LGEvI)Lx#~AS+BmeI25^J~GwGABUU2+5Y&JB^X`g_8kd{eEwK%BXk zABi&(0n#4}t<3Eu?Q4=4h6rbm?beL9!;=n@o6C0gOLtqxCPvvC7RiTT=7P=Hm#yXo zX%)*H5;2Pca*5uSxM}~WG6YLcV69-*prbU4K9sqY&>G~?FlP9cqKq3}nJ4cMfl^r$ zmDZ`QMueQ8YOc=&8`zFa*CMjh^I`5Z&audT2_u5C!MM) z!yen2uZl{k#Mc9bW38BW&8qp2X^PwQzMgebbEiR#F$_1TNK^V|p}3lZSz`xFr;YD~ zUmY8Irid1Z&7W4*9ooNfTG`sV4yIdi)`@=_X2cNzTjJ6GVLw-WH8}v_lH_AB@Swk| z9pEv#NnQDZaLwxZ@qVD(A8a-FefA&0uW^sW-nNC`pzzE1qyHNG-sIu89#F0Ulm_9q z(Zg@G@VnajK>U8PuRlk{@9aMOG6rEVL8yDTX#6j0&kDQU!I%q|}xZwnNKkw9|tV!724 zwKr9k;I7YKg^q7`y>nt11K&c_&gE+J1sp$I~G)+I%E;IeW0fU2h>P@5~D? z?39WR^NPfOR9;G}QF>SkG(}e1UPLC%D*NKZbqhA9545lc+@#6yC4*{*GdOXMkt)n<6!P=R6#7mZOH`5u1jc;y;1u zPuRT`klqj_mkx1UTxG!_b*1MGRj1~|Wi%;1UikY+BA=L54<(saVDG_szeWC=Mt;q< z6(9#>t^8dLUk0ruy&MkWyj-<$R%rijYsq^2@0%DItht>EG$VA*q$&&d?24AHj3zGZ zjh6ksY9j+d>O^yxhXtUd^|O|oB@ruJvn6BZT1G>r?{HPm@MX3CvH)o5bO5a?4|liK z_DgUNl1GHhR?o0Cb;~p@nWZWSsl|3#_QDs}Q&~ZB%k7#&^LAsaj;$KOrPaO*c^2g^w)2I;O~H<{QBK+|_DJ7@63gge>maS|$d`{g6JUuw8v;y~2ga z$rU>?!fMZllvyH1gS) z#Wgwz3rqmWk_Amns4K-8v^&UJDxqPCpN+6ubPInItAhM@^En)=LJ|QzIm6>rMv{1{ z+*RF3GF}@~5J8_S*Pv-mG1e|sxB1f+Q=0oNB2-oJY>{ofJ*f+R&?g_yjNp<7qlI(kv{x$ta-i_D}sG+1)`p#Ab5)HcmXt7p~5wF9ix)5BYAWG z8C=QqN1(>^T;@{5L28}^wM(4&{9ys-^Uj4U#8RPvo)%*!21EULbJwv;A=A|iv>d=) z(~~ipeP;-+-EW(3GB(6IDeFq<=OZjrOX{E13+F)UTK(2+Z9Z0Z$$hDhvqEb$78~R~ zSH9i+$|NuVu(SFaX+{R^XyN4hh~TYkbP#ArzgO#BapuhW(}v z5g)^3V{WB>r=S^4n66a5S=BMk5}tl}Zs=Lf&4}ZrttEDPt`kDBolk}e_e|?J0SM-Dx@{eG z!Dr`N&&Z=n+Igty84S1#AT#X-Wosrqz*2#G~)!AvVCUccjx^a1-Ig zPgaU8;k*nl_wkncxT@45fIdKa<_vaC`>Deu9W$%S%Vud~=A|CzQ3U4jsM?LK9BEc% zjs}9T5N*45Y~Q@<7WZhOl&eC&NXo=<_Fl zf}UMvD+~8@kPmer1M&)v-w1KF4$j@c8x$BCuv^i`jIqu2jyZ}F%iME(7*B$1X!N7h@D2V&76Zv!L$)<)e zmt@uODb?_BE^qg~o=bcLe?04A+q?OSAi+QL2gKz~e;rQtmo_j&em45yzp?Zq|2Im1 z$?Si4$eVlnyHBM}tNl6^VfYNFnGwfX+IB=FpegbcQ-nFMBhU6qKdj`tjAOT2DPU~R znVQ$Xp~x)pGB8ito3T$*(;}I;*lDQ+jORlPjEH&`rEe%j9&_jW_@9$a>;_HT&ke?3*jHZ>F0Y zzsk>M-+YQ2_t71gFX<=&nmO)yCz@I3gtKNH2`o83xK#7jut2PWlTR_l%3jN|Djug| z=b2)jurGwT$5PV$eM!bXcZXO1Po@v9zxzLdf)f_AH@1IVHU4j2B;CBV9EZ*Nc^*Q? zP_@4A$?)^rxQd6^CmMV@K)?4)D9jCPCc=d6VA;}8LXd6qRDtEtZz7s)E5|aZpkWlW} z)8@K1KimbC zdMJX+1#IUGlf~0B%oM4~5@{x-_}5c$ffMUIAluEHX>2UaZ5hKlB8g27sPt=^+n!1$ z4$dnv4#{B1SK)=L^{}qv0wdxD@PCX*{~Bp5H%W{MCGLulFccg7j*&=TbU(bm3pF41 zDsyH=zM4?Q62*jM%XPPmRu!qQF()$o2Y`Z7y@8q#+8zQKH!JRE|7M#6$#(AMv=!{A zQV)EP6>c#9ZL@CMaXKDTa?k}X$G_pNXxRNwW`MFUXSjJck?$(+UC2@a}uQ2^q zW`A7<;ffLQU11~HkA=1*kyem1RDTAFyMR4fwk}$@S(P$ouaBIAQ(X}UBN;{Z^}k25 zYChV=M`e8UMmYBRT{=yH^oTvLN6Y>kE!>46n=juOIp+^#w%rYVVu--9feZnW^%7~a z79t>L)#b1}MBKf}^a)6?0c!kcq;MB_>kNc@*G6hO@r|4Qb7?L5CLcv6b;?E+9lz1T zvucSc{O9)q+xLPJNhy64(q<^{TkJNQcK(&?YL2eAL8zFSQwlV%k`rehd6VWU0G(lr@s+ zlNiW+4J2e?`GcA759aIU80fHuP(-kk@rfqt5&W?hvZHTO54Obc{!UT4)%G-hVI3pG z3HyM(e~U+{uM_3MK|_(U*XV){A7+-ZnJ_0h2@BO!=D>T>BGQ$}7NQa0U|IwE7DJkU z6g;S|;ocVwmmoz?7&>seh>zGef1T}5GA6$HT25tZ$m!og+WSJZY{j4oNht|yg&8qg z>`Gjl`V7jWgga-sIW?9Z=F!6u**%mrXSKcH^)QbfV!q1mA=*E!)GF}cUa*B# z{Alv`zk>i)rfSLGEW-RrCRylS8g~*DDiy)=ubj_9eGGwNNaS3x3zG$U7OOO-ZAw2P z^E;19+;8XdfNvOb$t!W2<&Bsl+45|sI7>!jY4yZLt;C*uwe5hnzE|kmdab+A5Pom> z?k?jWIgd{DNgG2ihL=D5{#B}f{>~tQO`{nFp@a@}MkMS>?L@jQ(p9OhN`wOUhri2b zmsK+T5s3*!IKUI*D&s^JIahR55Tlr}Kiaj6sPyZ^2o|JHDdaVuc5uv!&{1Sxk2Gs5 z`J!Jx;)VcO5;9CE@CsvmL zKepe;bO^>?gzHH^TS<1DLvnUIK#=YD(KUF$DsE^LfTu_@{>-D>h1B#3SA0 zu)eC1P{(Sk{Z$Z$wRWomR%^C2pBX=8esM`oY_sE>J)c8gqF*^hBBcb z^aB``X~`=vZj~vp-gNTkzP!)Qna*a;o3&}pu5La(K4!dX@5*!R24#nYOeN4bb!)j& z2V#m3P84(>&XTarW{F4k7;>+AIqQWs6?pV}hxhh@gIRCQXJ7PQJb7-G@Mv_5FfX1K z)foL`!gfiG|H9Cul6_KR{|F3^|5l-H01b%5Dv&P-b?ZVzR`uVANMycZH51^JmJT6# zYx52vum~HE@R}?UUa=b?d&?N(zKaLIkUK9g&=;x{|Jdq^^rMEzf{e4M&d|?}-vW7R z=07q&cMX9*UU2*IK*L;`&_1cHyR+Qwm}iv?&BM4Ik#4vVu^>!W*>;4d+^cmFF{@Z(;IMO0ZLe+mED z8xf>EkbGTsdu{jfX}n%e{K~}KBQ<-h_Sve;2(r%Z-PW%!ZLl8dZsVx?c~GRtyis(6 z9Vd|Mp12m_yHnJ~!$^5UYwZcg(RVWUw;`CFXn0eI6YekN^>T%uOI~{jPC|Z^t1A6y zDh|(WYc<@3USn_Rx_dpnww8t(%^nTwF_b5*dXwlv!Q^!%IVw}sTKni2nrfL9t4)8e zgo_L9c!&-%=VIGCGn#O~?OE+D8VxL;AYc2!2y?}voo16)P`;d2S`%S6G5uzXRc-%) z7GN-S595RUR&UT-TLrUv^Ce7NhR!_wl#%!@uLh3&-Ky{J8~c-23l~PQr}BGO zd1z*Vgc$J8*MzFQvj>BwM+lV+-*-J=Ci?5menJ#Qcq#Vsf`U*bR@|rLzY#Xl_gG5` zen0BO91`w)nyg3XtFZ;@iQ(@u1Df7IQ#xn1OAFVhQ!V;@l{cvLg(8`Ob-%PcFDLzs z>h46oi%`(rhmc6Em8mPxMi}`mN3ysLM3v+IHO-;|apt&diC@5+rEA$Ip5^xG3-sdG zTqWI4E`uK}LPGSo(D3}nq!}8rzEsYNEoel1{tLCm$E+P)l7n#}-q66rXxA3E#I3#e z%!YnvGm{oH%5E{~h(`au#k`D6TBXRO3erCA4qIKz%-lIO2PfZ`n;R%~=NUvhkcxc= zlGbP^oEW_tkBv7^Ak~07_cl>QX7L$X)@h{?Gp$VVI*O-X(kO@nJtePGZdR5b7z%!-~=f!*M2K?)aAN-V=F$KjHSqnlrO}Tf!W%&Cx z&Z{zte>PX|zaAdH|0<#wuz%994yAc=de*-MhbJ~(9io)@`%INjjQQGG;_stP)pjP%inj9_koy{D!=p-)_Ok*qrIAXjLD*1p%5!7Fl|{NEV(M{3^TC6*%13C_^L*d%76qT%wCJ zo_G#FXLlAy%)FF|Hwb|6 zw`VtH=EnnECEZcKVLUJFTdx_VYOTVamZXNfMU{)7S`4_~G0RlJsQsSwuhs%oZ7^<% zcx*POl1E~*`Mh&~{Itj8kgf)3J^h5^Uky(`^E~>g7i6~N0~>Dtz&;-WU~4e23EW0q z`5**odEGKvjuBSt$*)$siN!LXh$EuGZ%Ek-cjPF{4l*+K)}ox8xo0RBP|Hr4U%8$0 zReRD}W5wUV2+oVBs2Krzyf%$XcU+!HRP~L@2IMN!!Tb*kaRYx z_#?&K`{n4G;Z;5$?O>Tccwm?8;^`-#k6C^6G5q)tVXyhkDD+2W7o`5$KR#wuSc3-m zEQHv861jV+M`eR-ydR-)hq)C@%hP? zg6X?8X7!9-c8s-q?we-xXmv7X?&iB{A4Aho9zVS`aTj#BO#2GDKus{P{eS!^3K2J|mK=1SS`69w~!H2N1#CiOAw zuiww+nT8+RKe*v;iuN`9FO+iY)$k~<;k-3j<;SSu%V;?1{^D`SN13$z`8|}sFsY$) zYG!Q2Uk=?NIw*K@Pe1iAbedFf6Nbm5nG*3}_Z{tSGn1R(6$ZjLr7V4;Z}L>QYmrFk z`^EdR05p^5Juyzkd4>ggD`=wNoS%9cOp*KK zEN`7Jv7LgYd}PPRH0ejywSu3RtAZ=}X{~i10FahT*`PwIyK(myN@kik!cLsCo);Yx zkFygY{M`qF-#Am8b=e<$yxRLyM)EjB(c%YX(w)TqJqi}gS(Z9k3vPVe=S;&_2W;n* zv(4oxQ%uWxGOo%z8TSc(;#1kX+Df0GvI93-F&$mVil39S+v=Ku!7RU(j3MY5capvQupNOz^KU>}ig z!@Pvh)ka%Tx#fID=IL<6G;oZad+dow4GOVQ}4cJZuaY@ z(%r;O`Zop&cLt8(lu{i$eIt;$Gyj161UDysgezM4<$Hq}OT(+;d{1yjZ0eW&obLnX zfAhfkFW9f46NZ{TSwr5@+M5$}z1eQ9z201$zp~&eU92p)Tt866_r4_BfK%}Cm{ZNi z7?CbEq$u9WtI~e@hgLSaV+G(XgC^Q={A1n~1L2F+7OK^j@t7DAV~*Tgl4D#s*d)eU z4|TPz@iIsif$WmyqH+GwqZw1hC&kN_eRD)k`p@nIKLg4451Qq}wQnubRtTzV)%;ta z`L{s3Pqm;UazH9{dfX>~Niws0@H1xqJ=g?BRu$aCkAa}Qv!#XF^G}yil1+Yk7^^*O zdcxkjf~zUGvS0+gF~l=7+8$2A(l8O)>anizLJ-GlSClf7k7GMg~0dv*dVmF=lS{)j4< z$PdNNcn$Js-&i`m0(fCAJ&CZsQ%QqbY_)&ge0as4YK{r!{9nL_bzP!|P7z05vbXe_ zJ-$GegC?7h+5K6WD%AL+S~j>3tWeVjbTiv+`@L?ulWAg}Uci&@&gB8V*p$iF>#QQB zDc)i2A2ZLlq`n7ujKYal`=9BKh7VYx1{MGfq`q&!U*)c&eCi=?A`S~)X6;20F3NRZ zx}>iw?4=tBr~)J3lu-u!H)%|Qc6_Sg6CCoywBF{3=;0whBmFQ8=Ik4_1{s55h+<<- z3Za3GX|+B{v6c*6`w&R!M^m{{#%I@GU$;_y@+n#oqpvku0+8U)fIe+P|_{ zzX|U*R%;2f%nz&m3*t5LHZR^>cF|uO?~8p>Y{#jbuXKZrg+&lA+u$8ap7uq!oJ9IIL3jsI(ID>!g9@gFQTrAeb zVlE!#V&1pyxQ~>DE4K3hZFKPcLw@&qNYF!WZW*zOF7vuR2kyf7%Kp{X&H6~vWR75H zUurHtS7gP0sVjCQ9-wYh>R;hP){;9;jx1X3Z9qazWYEb8?psA#ttzTsW}AlC%6P2-bjmRb=5N(QZLl~C zoLWn+EjFiDPvFLsx0YUe7C8#;((Z?)7gpBgqD8K>magjqz%bNCs*gR z*>xQ_KU~&1BawfM3^D0bB5^X*u%8~w(>TH`5W?p5hWBqFM^QMB2(B%6vDcElkM6y= zgAJ{C;mH<<(=P)={jL_MiGsD2;vC*3r)ebi|l zww1T)mwQtuWNOBY`}KNGrH`8T`Q_+0->11jBfqSg*QxC=8A7e5vp{TTfjDqt^B4Hb zGI5p2Gj>zuI(T@0j0`f*%n-p5rTBG4_ zQ_9^L98e$~k8`s_*iqO_&z!`+k@e}-NPRFW17K`~i7bwfuj)y$gJl#r4OX0IN}1Hz?64P=m%6s;$9-5+!xfpieYt zRMaTeQn5(YR)hpmyaYA@)@60I-Y+fIwtuy?TCIpsZ33377cO29`bR;l&$?>yib`wV z@9)gB$rA`||L^Dhy!bKuJkOk&IdkUBnKNh3oJmKr-*p|}UOPKro+SszbP>MB|Nhu{ zJJT=UNcc1Rr6k@C^v$Q|_UU-SQ@quGbct$4rJ1!&n-YN@H%;=jSRxynq&op@KtJjL zvh)tktK@;+Qe~LnG(8j_Y_wrBL!X%B6d{ABGE}H9pO&=MQ4s(u0 zgU!uo-OM)9iTq1mE@ZFCKVflz>xR$&3tXS<0NMf9ZF~KfxQ_mJxE|F#uF&6Kw2p~I zG(EBd1=_ePnnpW-cEIzr{}#{REZ8+oVXlEomCR~HB(JlR=U;Mrjyk(Om~QigE;P*9 zn4HF~9F7}yKWZ zKna#!F_H`!f4WOq8r~lms-iA*36EI61geO6?A9oOlHK@N^yw~;KhUaG-5EZqj`B7m0kxaJZMszZcx`h9z{-@ z4uf(pr|JhWMO;Z#RIiL}2P#?Hc!-xO_6il0qIhr}IrX9yudI=sg{q@d^$3SxE3e}+ zj<`CwY(gftfI^3xE*=MI^()AO1|r4D=d%NSCE$pWWy?ll{}jk_$*+m%xBgV$C_FfP zTc(%*{q>%YF^zrh(jVERJ_~q3+8gP|t`_}oyBGTZ_x>OAg<=;Q*|NwnAMLMma|IvA ziX&g&x;MXlBER@ON^2@|@s~w(MC&ydBgjAYV&85BCi?Jgg1Mkg&wJmwH}QVHyL{>} zk`HfX_18>JYyVh^^k2P*zOczgHIUqRiL~Fm`RL==`E4#E2O5sck-v=zw@^C?d$a|0 zQcrzrZkGSMduZu5J672{>8Ex+Xt>1U!1HUZpWkA@AKP#6v_To~DeI(&8Qnej+5x9_f z^fEX|RuZLyHFf2`xbeI3ZrX#UzthOswS6NtOYzw0zpm~3X#1X>+<`{r+Bbb^zI_s1 zOk(0s)+38d6lub_>gpZ^9p5AO*mu}iL_yR+8UGw0g4clgD1+kBPyLPST={^8(){Q6 zyfpH`1GZYtx7h#iE*d*H+}Q5>&z!Kj`2WKN!k{kJ2VE{+%>QLr@b-Qf{+!WPfZ5Nh zjMo3Wv|!5atffxlAr*aLIxKk3ijQO8=Ktc7uQKA-b64v5f}8bRzeY^A64An%eXSUK zO{{nWJiP{(Ll3+~Fz3Il=UKNKe{YG+`UnPw(_f#;5g80741I=wJ~!YiQ_-`m-nG&-$Rhq*l41FHkkc$^Z=d=lOMplYcV3zi&xF~yr6k{a;6 zTlD>W$+bzy--hy1@_A(LgT(Sbz|_uA`TnZVP_fXteLpOO8pV_iB#1P>Yk)6>#}rGq z0zdZw3G`G2t|@&D=cn1|rp(881P$h|0G_Sm06yv96L*Ag5Ig&s0F(eO4pjQbSO@*b zM?%mxn_U<{8)N!o@NWQ#!f>IIMgFmXS;LB5!+ysuoxk+&{F$L`hwL3_xuhSu?nYyQ zmt(X4Wg4yPyNumG@9%d9d_9EkJ5GPkry}3K3WBGiFVC*|$>z z&Xk23E7aVVG^Ct-T|$M?GmJ;iU7l#YNS{sWUa*|QDzRBI=`x31Ga-OqEclr>8GkPq zPjh%m6aQfca=grEwf3z3c(K||Ix*eSYNli_c4}U>wrZHb{;OieV%g@%Rq% zE>&JHxn1)^woh0Z>_om@=nvZv-(BsOO;sZ=vYi0Dej#_HR<`O?^k2W>$J!j5tB{*{ z>^s(<(l_RwVkt%fy7dCcvchJwo`6%bgiso&Oe3!v+8uEB=a%uU5y3>9q5g@Jk1uyO>{O z+xc$-7CF@lJoh;|2HvAp#fhq6@GbiXH%b^nwM77p!vD(mH)I%_%Y$ z6f|j|QI$-zXA7=1;39j*2g(RJi85p7ye*!i{yrDUyak(JQ}-{$*v~+H$<)(eMlsd; zmaG#jOnS(lix{SW9d>(Y*kj}<@Gk|7Cb4d(U;sB_tp)Iv+jayzJ}3_%2v}cN-kP1j z`sK@0>j6x;@^07(@MGy{Ciqm|b4U=XowXC#D!>dsF%E#oy7G?O3Gi1AFmg*u9SrM` zpDL2zwYSXZ_o-{|?7Ko>(lq>yOcd7MajyPTTzR#>2m$UXz{m4|o&!AC0gkvg1W44H z0za7ttaE^0zKO8zt3NOOZ%CHsZHU_8NDz8ekpl2|NW4?X5&ur+tA9ItHG&%yOV-cX zyCD0Z>Euaq&iA~ozds!J`}%6oMx@u_~23B8R> ze%bbJpOHhptWGW)HDvin4o8m~(l#>rMcY4$tB1UlTqM}^1??l#Lv_vMC%AT%RepT^ zc{&w6o&);PEoV!28n#Y9m9KM!LaP6!#PnW;i1=!xOpC5$lW0K?=wI$1eov1At{ueq zg&~-akfm?7N+2D2IZkG|Ux!{h6Lx;$euU(D{|=loGJc<%=$Ok;7s9hx_x(f6JM|Ak zM-GW;{v@iRS0Ha=&D)IX=ugc+ENDUj}d z%N_P5I`VvW6g0otzEdjt4DXfEC-@sn{16b+0-9NFA7}7#ph{dN{ure9Ws4z7yIhb@ zAMUlU6cfztMS^WjHm_~?vA3*gjA5#g1Bv`t>KvGwZ<&xN%By2qZ@Y3|Z`ll?PHVt*g)@e<@ zLyFs;HPYbC{14edcLoaL*WW{`Q965b$J6ZYb0C)QDh=6KjshQlY^&DyPtJw69_vQI z?4G}Z^)Dp749yJwB2dHQW)F$^~OlD(t`N3thheL?pA4`IKGJ9C|*LDy@CPU zO0BsILj$)H5T3OICfOMRUfvkxdP5$;G_BvQrnjV5PLF*d48SIAg@g zR}-usQ?%^=y8iu#bfqrq!&LOupQ2!+%w3_eh5sPV^4@r&F5q#xCg&~K>$L|aRqgrO z1M(H!_LjMe7JfV-qa25tHqwfUZN`k(tufB95oxxDA>NDGexn&^->ihx=#rX*rLA^f zHAE5DC70gADWNJ4WNiH%oBJ>cD3Me6Zn}>)f%9r8#Y7*sV(L-s&Q&#$#j|Xo(H6+B zS(Pf->GaV}?-o(`&)>lI6Kmd01?w85<1Iq_+-7@UO}P8j!AOj}bGNjylH;0j@2VR2 zzosB7dR(IFcb6H0$j4jl{Wz7`aG(&w?(-sV{=mzHc*i;RRCJz7OO@P3qL&(QgMLu0 z(Ym&_<0e?fGDCk@W-Qe?@0N|%M}y+72Q>F+(C8)Mk*Uglq=J&sGxU>=oS>iC#knS) zHD^ek`13!OfJR1Glt6!XWP`op-#)}-_kdo)ZN?I3382@HUuF9%D7e9bNR}lF0wLp) z^Z@4fuEwS8&umx{_^!P@yO3vALz(O#dsvy>Q@^4P`j?53sjLoG0FvF!vYEKF8%k)C zwrY`lFneUlOtrzJUG(z1^z7E;XWFxcks5HvIa;5yCij1e@t=lY_J@$tV;A7uy2)u< zPFtO@(ZJpcD^a%S{b>MJA+ovkhXeg%c;UMjUG5(rfI5A;uLCRn-HsLa3&6^(BR+83 z$al>Pfmy|$=2fpiDZ7a4kZH)8yF-9>tZ5a1>N{JO%f3m8{@Jhcr;lsOZ+Ncb00=lcX~w=gb2-gHduguktH-EPruOIy%50 zY6Dl5{X?!QOHg=izy7PUR0vGErf++DDzcqdqEaNPYJP%#6&uy2xrrbHO$Piz9=I1r zK{|B||CL!ZPWHi$h6NAZw{L=Nn7=9+PQUJMW%hmdJ!V!p-k)ukoPaEWPA5} zyP?e8EZD?Xg|VB;dG!)cujU>3upe)k^m*;cy8?j5K1~b$s5By+FN+s%LKS6u($=#1 zkS%Cc({j7DM<$;thq`xJix)IR6IEwS*HP@A)lCJhrQ{t)lI%b%vdg=B33%GZEqwN29He=t1x+Gwx4F z;BXu1eK9BPB7ZougulP%Zz}rrgwle_<=5?&jNZvR(j|JEBuZqd-j+pv$B+JLd?6?z z-{Re0_)MTNA%qYMkxK-djHLK!s;Yg><^5&u=*{f`dD zO)An)*@=}1eiQmly(r(c>FClQQ$E^^ zYwh9Sxg+aqKy-`^9Tt>b*)du@mhqqLL*tRsJWEvbG)_PQV?~ z{)$C-{{=D6&h;^6xbf9x9Q`Oij!w;wBO@JJLgQenIGqM}Q^h4zA;~cjU2RfkiIDkL z5Mai?u?Yvc=kld!Z%T~N(wh52Y<_eC1kXOgWTilhx3-zX76f4&Qjq;6YibxWmcAUmkEBvG|! zyhKmSIxW2+$}|y~={fowKD}%m{FR^B&)ZlGJRu0=MdOG>)l6l&c6Uze-DmltKNBNusXaXOdVyP@SZWEH~Ugd5Cj#Jd1Ndh(YOGQ68jbZD23I?xa3H3_>)d6IJNaOzm zUi?>YpeVx2wa0E-(w!{LzJZWURDFJ-HKLUl)?>P3dRQ;Re^|uuUy^-9NeJ=AszlXu zf(HAI**o+#HQ+IMRI{JT*=Raobm*XDtHIT}UGv+*<}FiD&UA2x8`L*}@;ARA zwc;LE|M15{1)J8f<#A2${;}6HGf%~Y(nHR#IuX-nW49valKDN*-*e-nUC6&~0~PfI%AaFP_9zW3zwqN?j++APjWq>F8j&&gPzLINRbb~hH2w# zes-3ch~{NY(n4g|O;pYMk#YY8@E=n8n={m5bC#3TRMoQDrm?Si0fT7)C5^xOfgh^G z5%~#zH~`<;TM}NP5xIVZBz#;?Oqodd_onxfh3JTahSC29C29hts8DL;JVul7(7)`F z5~1k@de`||f1|cM2k>!7D_9Typ(ZE)og*E^C!Z2xiB4M0CNv?kQvK^s=NLIC4`o&} z*3A*fn?(r95$g;F#v&@x5c8N0IPnI(D}+bK!%o86<~w(UL?c;%PIwQQitv7>6MbrJ zyS{gUe}GzWC*Re(ttWSj@hfm~Cl(0IUo#09gJtjL$^Q(ZW>+J!cN|4Kjh}RL`Eu&& z&>xN+Ssa_ih4*qbRe^Ffeau+E{Tf-<{y=2SEP#RIfeP^Kf3 zR5t;HXA|%*(la4M28>XS^svv&Qqg}Y3GZpapC0hm)dKE|X*04~i9I8Itd7WHy^W8& zc9L)?d0Rh+mAs;#*rTh}k8z6q0fNQrIp9?HbB=ye!63ALjV+1D2jg=GddFyq?iJ_y|L2+pRUQ!q|SNtT4w4?PZZa$LTWu zHC26dB2SqHR&o7Fvej$MszB(x6G4zi3aq7(!u;2Yp(wu+67P~#Tp{_@kC+kuQ3u)V zecax`dScd8))Q@fWd41C&QC|)53X$YFC~fh@;vBR-V;@S5=X>tJcs3zY-C4GcJ{_u zGlD&m1)!{O2pWC*C(}%yx#jD5^bc5~Q4yIeqz;R$5;@XygHY#;A>>~Xg_E7B${yIa zSup=6mpvMJ=IB}8wk_BS_f@}e;sQ}A)zzPXu`CrSpmsR{YoK|ICWff&Iq7@L<2FF$ zGp22L=9gmslc=*406oW z5Tpj4e?hD;SHfZf%ktb zbW@RUjDn_+-6yO51=40KkLcD#N~M8H<+U()4N|`nhm!gQsa#M88~mq!8O$jMy1`#A z`*VuE5DB68tu}!%*lf55^iH6I31}De?$xFS1U<16vA&5?=+7_^trf86&@z=)F9)$} z&rp|6V-4`UH4UWdwa?N#LhB=_UFZJ>fdsX0X$W-^eO}-{4_W${?Pvliwgwu?l>`bOsV!|T3!?HTD6e6=0I zR3s9-<_nml98Kh43csIvyma8xE)wlLQlS4h(W2BJim>R~Ck3jvYoGjM&wtw|lm7DW z`s5b)-v1x~X%cSj=vQvrF|Ai&uHV$$=R_vqHBRY08VCC&%9oDpHYG$|V30q@s@U5} z&K9fpxWcc2kOU|Qg}h3Cd)9I;pS(j(zXusf9~;I<^vFtmf#63TFaFL;hv;GUI=6hD zp;%IGuR-|Y)PMo@dav8-9Y5^Zk;?1*?fr8d2LHMygaLD+vp@OMmph`Fe8{^{o%mNn zIvz&aWW?wI+OWt&mpbTuc7%5NbCBas;hoR%&U04&O=0$ylJ0-7%wdxWLoaa9r*z8A$*AXQWu1rCBPlzx`eH4&$?5+{&L-Jnf`W zaqVp;Hh<2$fTmAxpOevp(=Y7S_>}5q{qsU-{bSPs--++_pfdqRN5KOEg;Vln?qA)YWPBr4OqSGN(sO2&qnnJt`1Q~jNo6nl!mV*HS40BIhE7y z|MbV+i01v!=k4t*1hCgK6~NcFUsT-At>k*(Z1ea@evEE(3(i?;g;s9+{-*&(*Al`T@A3(jR)?N&gS~@3J7f zj9HcaOQzPyM@MN;m}^n=1AAry#ny`V0Y|r1UVE|3tjz=pUu3)HjlRtmot~|CMWlT| z0ByAt`!i3oh1DljDmCCrk;Z=rd5k9XDhc{0sfIU`CTpa1Y3^pNb8w{~#-!;tmqXwG z>;B~{Xv^3KkhZ(D--Bq})PA?5kmrA*>b^5jP+~1Zn20%EGIWlJfAex7nU0(>aW89# zUsDeLAnme)Gu>{>6165#b*?MpHO3U*m0ABpD{p5m?l6>QxD2m?Cy^ z9PRw0+HnF-M~>*_^gB_@ll*ljd`sQ7iK_AEN)1sF&HT(UuK_TO(|;U71Ern#A$2ac z+QfSDQnhZCl-qxDuSC`Vo(3(rfAVsOk?~*V(4v=;(xaI+ZM`=N$F>Sj3@z#sCPXgP#YO@GZf%f)o-p;4|6ckKA#yAIPH?X9VjeR!Ub;b7qG z9{?U}c};mzkxP$(<}&I$&3h{1^I93%#9waz;)@lX*ox_(nKyU1>q4#FStloFIC@t@ z!BjDn?&7ACdR#os0X4kv(dAtW25H;+vxFov%LC`Y~s4#Z1D62TANO|uE}n| z5`bQD0o`I~O8#f-c;n+3e#~$=0J>a^upzLFoKJe_&t38}$Z{Q~_?^ac zy1IO;J3l-WjRB*MsS3&8yA8y&`%P3ma4Msl_Qh^Oc|dAyKc%#Qo_|Mz6Se(LYGkqd z`Oyr9ROB4}&}oxwU{oRyQqh2@Y-Zs0p_v$pbA{%*Q|8Ef_z<|jC zN>jjQJg4pmnGsWwhm{TDR-iz14Fbu0z>-;lv(y9{4`q7r-{ta^-0vKIW7+k$ttS79 zekdq1kNUHp2ngYgM=-Fp7Mq`0K&$7BDL=x$M2Q##t_m@jErf0s4?{j=G5wHW9|Ev- z*M`7s++6~g#Ymjft6cgyjO@?vs4$gxqKh?KgHt1m)ryJ!Cz$&w?~Sn9Y0A}sLA4XD z+TK*_U))(-ll2y1X)a`f42pWt6?NZEMZF{ijuyD0+HfNy{wrnWJio!^IUuatnZLw6 z7Ow+uf_f?y=_vX&Y5{*fViF{onl2>NqMaZ>)~z z)oqYqlBj9&3o1bOeJ%-MgO#Vi|5X54XLN5sC;|5RPj&>G9)NiUHrjzrm=*#vGL8wr z9O+L)#EN;s>lfp(?*w4u9C~r=R}QpnrxJSvpmD4CQ&;h{>7j~^9XG0dG}z4+F97CG zq|5z79EJY5COEW(tiNP$BYcrR8v`!r_5UPP*l^M0)H}s84|19=`v4no$k?6a>#;v7 z#a?a#?)@_9m!*M!>C-17AYv_zB-sMvU`Cvwe!$0HwJ#~XzLcNl6Q5(HBM+0ll;Pj;Q%Ov=Pa8k< zPr1o9tclNuBy*m=TmAWaLh;l0QcvW)JFTu~4->B=O-iV%sY$S_ zm@iztWeFL8?9moa*K9F*Y^n}Q_%S?TU;g=I&~2aK=1Q5>e^%d{?JXL&XN;SIELm%i z90VUsUU59Lid&$aX$vtBd{iK*x$Db%6&4WCvon&XUfSZtEXb09MK%s@W*?Z2o~F`Z zW6ihuVu_f+VY*cRH7;mqUj+@Fd`IpO`1Y?y&_nzK+Yhg>x$bDq<@7vkM&$f_@J=^B zZO65R^UEhd|Ns8{Y~0Zwc`s*H`PU*L?>~rIr0$pIhu9r3r*+8u4~=*?@y>FU(*;Zz zELFUsMAb@2UOt?uOH^CI)0GKP*qnCB79H%b*`%8EWM1w%xy~Vu&-U) zpPi7m#P6xTZMi=vjL?u3eh+eS{`qxP+%b;6W1J^?Byc2V7~D%XTJs9Ku-TKO`y|{pg$}kiTq{4ff*e%q5}Z!k7-)|pwS>B8$`*~yIvZ^~ zpi!|_*QarMf~~dNp;3-}lIWtrgOpq$G=?i>k^rZ9$y>{tgbVGA(@vd4RdE=@XWeDr z(Ib`?RJa`Ihtp^n&{Cf}RPAuqU8A(?9T`e2@L$#nuM=7b&@Cx>;0-=#@jb*A`@d`t5+F$0kG@7#pHR>!NRQMEd7u#!TO5#zKfd{ z+C-XZd?`1;gz*FUg1()TtPf0t83sO!^-OA}$j`Xx>!oh64q^C|#pd6wQs$grzOO&! zx2E%LV7;U%!_*25WdH8RoB|0yGqoSCw{&FH1tBm~m!||^Wd`=+KNy&IVF*m~i(d;E z!>_M%mqP#O_lQ9j|1<nOkEArz>`9^}uZ;Asthd_+LGN<~meN9Kh! zK~VE(HVNg)@8z0IY^s(sX-@f**DV^$+0LQ~7G^eg!1GAIxKAm1)Sgdl$>!2@@F z3(xNJ@WmK*hn`04m%hG7A#u%zWq7Sl_d0S^I_^Hqwhz7eP~$#arwo0MOrOyMgMmgn zR`kieHNLqXdFC>>RliS?z{Tc?NBD*HM(!V+-)g?mKJ>xt2!cMO^_Qv z+)MQalc$>iti_0B@JSX6@Y0C#n!(&$Y<6$S_SD>RfvJ$v!Rt-R`wAWA!4}LllL`YA zJEUoy3De3KuenWZJ&7?oZOITKqg(%GFJ)wc^vpj{W;K_+C4QsAQ_;kJW`D7L(WHHH z_{;x~s)sxLHwu4E<*D9G4HsU++MnnS^#Th_YA{|krzt2QbP|N1cg!!=*k zp&K0TRj#_`w(PMIBcN{aNAUoifP}iR1gS^BO6KmASB#cf>LrO3zry1h0bOP+Y|-(e zYmXJb??^$7(=xCw@IOA-(a=*A8p?je(MP+1@%DIV;iPo=eytW*K@UZ=#<$U$0UWw9!({v&z|~?=PPGXz@-1i zKg{-rY{M?`K7$~?{t@@mI9<_7&@bCjSbU{L$Ox2vn0UBH__-ur8EF_yS{UjhJ4c%q z*jwy)4AtVYw#~g&nrGqZZM~tL0Fs;oh$!ku2C6^uD++%8r*dF7hKd}eTFRyRz*?AN?9!oRt!1F`Vx#u^rvW?$x z6oK^{-V|}-+?!nf-TZA^i7$($tiX$ao5bIZH-Fub(*mob{u%9ZtBWQ27=C}=E-$<4 z8%`P@=zk-dbhPCj)dlH?H&xv;v=s2>_ta%LTYIcorm4<4I+RX0oT!89E~M0)CSX{O zruHx)J(stptM)8x{E&N$V2%2TM2IO>Xc7}$Dt;V+J=}HKu}4wUdb8eQxsKLN4Pv32 zh>)+>aV*gsXCZ=$ii8Tgm4DFg`WVeX9l`QCgy)k3l+)0X3x$GWW%OQWNWT{FSX_k< zCwqk!eOQg5qMhlhPMuU=UAZ{ctZ1>->63e++%{8fLBS~cwpqL4xtO?WBD^#tG44QnEU~L|MOxf*I^Xx zA5X!A!+l!p;SZZk6z#Y^AI6iv8-Frxd`_=p{Jh$YWObvd`|}#AUB(;SP7km-5}SX%e6Ck>F-$5mNsPN~19C@B zi3C76H*&*peEM)hTW|Oiz(xnPW9fdBA=ketO6J(?v8R{|poPI$y+|-@lcSwQYaByxj^JBX zB&wDisbSl4uiiOWuX6Zj`$Jqao>(9>3$7iWKQUCM`J&?Qjwk#(YJ(DWG2s!}U&HQx z&j|RAC`S~`;?>xT%cX2atYZ+=FJU|9a9d?CWV-POImBZRXAS!EOK$x?E<06l=>@_K zqtPLkQ6~W}F3?vRsO_P{tUO_=2MvIZ>BwJqlJotd)c|8uCk_3_hHpN^xkD`TA23T& zQf{?Z7`63GEHS8t_#K~1HDRQ?vO1sq$&fu8CAG^m(hdJ(KR?3FAD-rqcwm1) z7&X(k*8W7*zC+YsI)CvcO$l{bDmv;29Yk*|;iL7M$k93NA(Mw^{6jhK>7{y=+a<>^ z%rI8;xG3z|z9Kc?0G=yfk2Me07(z%P>;ggP+3_c>j!fQ)e!lac0>C+Z7w*&`bDBub zBa>NK-A1Mp+x1Rt>oBA0Kx7AY9>gvX3snU(Zv zV{&a0i2ilA9bqY0>Zd6Ktfjaa6blem6w2lX=1p`qf1EN@u_! zUVp}Xx&I(0eTL_U;SDxvZ~ja3I#<5_uiTt*^^Zb=Ju0Y&9F#c@pdSO~A9WXhK61t0 zm=0yoBG(^&9WDWw6Bvc%{$UR4g;^mewbDPs6=a^DmVVp{NvBR zlRsCulJ^d;^DVacf@`J1uRRPpJhJ0NvlBa_zfZk@xusNNi{%!7B+KqC4Yb4*ZXU42 zt-PCLjoe@wT*kR#wr(r`kcDiq*$TuZ)s`0HXR7F4E}QS3_>uG zFEJb_z2PjyQt1dIP_mitbRN|W3^(y{r zU6c-fF$d{*xHCM&7H;kxOKd|y&cA=uTgQoK_eI^Xv)td|VW&YSC44vu1o@v-(8w9l zKZ%TzFR|voFv~1$QbbeE*xsKpX9JEzZVyk~1BG^m)l>dX3>#cUON@oZj zGk%*^$8Zo`vPx~$SjztyrRqA$b26r-*l zv&Mf4hgT-9Q_P0zy>#HYjRa=x%mUB<^;oGpj&$l=FCEmYg;Ixh1b_ECHw2jSSOm{C z3t6*P{`zcyVJDQI}ug8?+L8B!WH z8aHs^ZS%A|lM(>djuIL`psgN}qEP0?Q0elG%KkvqNsk0IS{Ev)_P;1-DfZ!xg3fmZ zF(Gso;Esjj~o1`(K`@WHpv8sjY%Z z=MT;xZ?~TSKVb%EYEjs*So3K>u$~*+1!|b*$|#mR_=|BUa%O&p)pC+})2Y+TV)M`4 z|MMkrZg8ODAseMKPs32yld33COWCoq5Bx+%UPICt#jTfj)ghA4{V;;EyE1Nnx-)pT)FO zGuS8gzd6_P91%8Z^j4nH(&3=C8X&AW-*TIQ)}9zDp4kBZe*=|)Sgq@OtATl8H!V~C zwv!+BJ3lyo|NJdzinDLz&+!h2zx_H?aG45DP6^>Xt_$8jOIa;fo)yYtc;6;0LwNh; z4(;1Ug&+;>OARtW8~HHy=T_uRVaG8;{CJ1>xB&5eh4_owtV%j0_PQukbRQwUdU6Qy zi@G5GCq2p7iu5=Y66)uG}I^FJD7p5PCF5B%`j#1NR_eFI<+{GtHw z?ZSIM;l0M;P_iQqekpio{3FZI6F%56ME_1xb-KFz3+JDk6_3n#e-6!%QqZJdlmh#u zK{ks90sA?xj_t6#gd5b+uVT&jxp!S(=B4hn3ASlbu^YhXpaopB-hz3H@rxy;g^n<0 zluRsgBQ=6KmPVE$Kn7_^7uWc!mw+kL-B1;_D3g72B2w?B9H>$4O*ae0JZgz)!zsVv zNPHr$`wrHpUU+*jwtt8;KDss)AC8^qnqEA_YYUfpm6HZdE%n;DD0SuauW5u@D`z~X z#-k#0Be_JwI2GQN1d&OjgGrA+euo+6bU6gM$DK$4qWlSm)qjpqV*F3Ta0>kUFRvZJ9&OzsOgRqskS zeK9zL3vD22PKQP6SKcZNt`pL{LDGEBpZp4kjFA>C%MymsLU++GUnt+j-li73A@t?GV0hBItr5NSFDeqM8$_T5t!gsmc{uLRY z(vf4<_AaPR&r-nfPE%raD*Aa5=R)%8d#1WFx|t8L#343_U?**rmgWxCZFgLGhryLw z60fvrq5R6I{XADY<&5CTtaH@C{HyV~9FURrtu|dUh1#}` z!c+L0!;V_sJ(;>}c_5L=^O<#k$Sld1S`L475%z79d;`@D5`lP}QjpXSgqq*TiphiGpyHKw^q%N5 z^@?<+QBmc;|2T!XY2yv=KAzn+UtBqAa8 z0C#;e*JC{;#^L+`4tWTS@>fde|V+1yo>#-scO`LCck=- z)V=&t@OB9<_D2F)#wLg@jgF6tvs@wyN1srta2h1S z4wo@1WMK2)GB=JN5#eA*DB&T#;`#A=%^)nxnE`pratbGsj&Z6w8Sf>@sO=N3B7$F%0oPi#LY$j?ofcz4z$IfYUs z#+mZHQ&QE-i?bWV?ycE%d3IlUt}=8}O&SQta?5aMfaRu#kKjn?_%CAE%lLi?EcXDs z4zG2?U-&l!_P=^rZ|c0X{y?-zk_YT6BjAS+`>=@;ajJVgHtV}|l=I`>jjNVRTQ)ZC z=A|!)=e7)K&+Xy0FC%_$Vrda>y+yI~SC;7`Zerm9`B*SzQ*FA}exp-};?@S1+t=9r z#*+UH)@eg)t@p-AS%akh%2BSvO*)kcn?W#9Bd3RhMJvVh6Q;y4jDz2t0mubo$ zB>itJvqMMj1hL7pE7X(lrrY82?>>^=_&?bHI`sRM(lff(?=$&(^E`G|`PT%b0?n9j z#J8PKM3bp0!?q_o{2|(M`|0}UPPYh~|FM7f7bxB~ZSO_A8C%-hVd26JnvdH)_lx`G8=X32bj#Zfm(|cw!EkEyq{TFH-T-&CT1Kx41nd{5gAz;xUz4D<34AbA;?OP} zW)gEZdgg1)-4sg<0Kb~#`OlTsv{Sx`soVoNkNmPG6`xqsGhSLdcr_7~!4vXhRfwI0 z2&yjkw1-}kXyv}zw&W(KpLvySvBWhtezgB4&ErDeI^=8O>z=WM#tiZDWAl%!NiI*m zJ~H{~x({m-UlcX$8C&>HuUKL+=_6wgJU``Er=*7>bdEx+Z@9fC`DyLoSG~%mlT#wy zG!b(W#2n7=h}vX%{*qss;5Q!M?Vw_F540IeBSfNU$yJhPqD`Hqib*@tCVNnR^e`4p zTb7U(Ve7(UB(y@ZVd5W+&YioprtpN~(S^wGu1k<5HI-``_9R9Cfe^nkA&7ThLwOM` z>8&Oe`wLhR2)|M)Dg(oEAvefS7w+KNwF|RPP)za@(~q;do)fC(W@wP!2_d~g?u*+2 zg?#5{`vIMQ;T#J3!6BA^^$;#s9%ZhHpELQJ7!KBF_-`PK9~~321N5O9|9JZO>A0ST z1CsG?;abgXl+0Z;fbQk<5ilV0KawZBzg;gPpvvX;^wOsk<@sxQY}VhzUnsiGGP-%J zA=Y;lwzOh+0?(1z^!U`DU+%rl~EE&!TDBL^RvW)J{yT{jl82@}}A*N@B&TykB zeo-j6IH+@#GYV|OeqQBMCT62NNX*7&A7xe9u;q%z+kQa7@Qr54Ao989SUL=aRI$dL z>jX-8rB@M~E&srbL$s&Y)AM5UhX^5%k?xM4RR%{ZcNKwav`QLofq0nMCtA3yLWVl( zyBI*S88(!5$UrF8Sfp;V)My)kW)1Q=(`Oil%G3Oj{UiMgCb|EakeGHsK|?7Q*#5Zb z%eZ&I=ia)UH}DhivV$?5-(&NqP2k$z_~-*#9Z2jc?|efkxhnZ3pK1pp4qgxJs-}U{ zZLS3;SZ%=?S<_FU{}tXsLNxshM`n{anhwV|h`e=jh|}c9?XUl#*^dr~ zzaDY3ekY4{&tGY>!C#Q;GCGL)zd{(qy(Bt|3$)AqjNk88WSECa1;`9vLfKs+6zC!m zFLlp63g;kns4O>krW4M$!Mem|Pu8z~KcI8J^Bc4~&Gp?~N5`)8muGFSs>}U)^nQi6 zZLutMT#!t9MilqZi0i~7s5&o+;cK{{Jn=4<&@K0pU%Ex}$aR}B$&c8rsj9uVso41D zW84^8A^xatevvyrm>)Bq0`upmS|-w^xFLQ!~M+GUXDp7^qC)B&v>O&T?D`PS;99$sSl#G=Ma@L@b)BW109Z{P-sp_JzB zk}*&X@*JqBv&W-b=jYNg&7}zCkcKUr6;jzh>}2}u9UMNaH4RI!%AF8QkN+OA_8$Y^;C|}j*lmk*>FV^z!jXwDz;Q)&vTfu-S(Zkw+cIk48qnM``K8?)&{<*n zv_i^p6gY6?+T5sttI!>aM^YBBpV|0$b;#Nw?b*NboTxfvZ@TWfJ!>mZsEEy4D*{;u zTK}~y;5|b%F}9aer<8W>N+geUrR&Kj0mZ%SuT8Eecj(9F-RN3lB$h&(kE3^*_qJn^ zcH{MYdu;nh)NxoRdyQ&tYwSh+d&lNaxlppUF^an7=BKM{e9M7I-t9e4naI_H#T|5D z|G%RT;13Tas#gKU#aj9^R&ssadf+MiDcn@^0#E*O4EP9&6T*kP2hcA6A2@<&pjT*c z`aeP~0qo&pLh{6n`O5<^!~im%pg4u%x zrovORndtxLJZt&7`$HAzxUYXB367A>xKP;oZ#u?#e=2_{smFp)280*(8~+})yMw0D z=|B%^?r>Vn<=|UEBD-rrEh`x-+W)_nP z1q5Az_Tb=X5f6`8Z~DOQmb7;1sP7ELq2h2ISUTHK3+ z13;8S(7Dxt{U$t^F|!x=+rd-(>GM^=D6{<4(!yODt!bnzbN12 z8@%zc`5cfOF!l01Sc#Wh$xrOYLo~6pZRy3JWJPLFLc(A$$a7(JmN@(Id<%y_qy-XU@h2vUd?lbzhRoD`rfNxu!f^h&d}R1f#}>w?=>#|&;Dq7QSVS0 zJ=n@?(~>{?Bo^HvD_9@?-X5~Q;m3cp3?=qto;qb1Qi_o*&qt(GaZ}a3J)~l`oNQ%5 zN-3#j-eG5=lxU}7BP}1&nn6#|Q7PZnXA8wW>$YUPo+s38EmANC(_t3!HLs5DsiW?w zqfUp}!gm^0Xbt1UOt?*bS2y=)E|B`$mNgwFHvir|oeI;JW#uhSg`v5LH_>$32%bgV z@5Gxop}@q8j_hs{2wgqK5cdk_Jjl+vWGd&m<~(N4YJURH&k2d?tg%XB^G|VWEEZ7j zc>`Bx_hKsV{D_vs0_$tmVC^nF_SBhLU@a!L^E%%f_(gUj{A2Sp^MBmd--W*namC>v z?JquqX=WGp&lRFj;{9>znR@jMi?7I#cltsY)5N0Rv1gjlaT(@q8dFTO$FE z{X4i?3wbDtlD)#tbc%Yfx-yuYq*Y<9-Hg)1e?Fh_zPB1#4 z_C-x{&5*S<+qQbCv%`vaZMyG%qm%1u+TQziP2#I~Eb)-#fPre$ho029f5B;~zAuk# z?oor$a&+=$eN~<6-<0{KU=hdXx#>k~Mv~*eJh#meig5Pk0ZzhrFH5KW6Up_M!8_X-YXPC}r*~ zUDDj0fPdJ}-3d5#qCAK>A6fG9zlf`ZX7uU4RGu0Zq0Y{LCxk)qwrz%idwN*0(!DFa zbPcAqU7b~Mzvvao(dwjQ3|WojmHSXt*`70^A5d#noxK@D62&H*Aq)hOKR z5A)b`h!%V3UcTI}!QAEkR4go!)mtQBQC3gFDBDz}*C3kH{CrCKA?IRUiexKWGNhH= z!G)yhzNpAUd7wG};QEco&C1^4sZ07*O7HaZXA0FyF|9wU^vG9GL2mK)Isi^Nu|rsP zkN*n}|2oYNoDAcS@Gs6IFq^&Zj9+*LGY@gR^)=NS^GI!ZoGz~`M%_o6Pb!l)4h~Ee zs$P<&J~*+A+GX?WNr;GEbveVn_wD6ApGqlnbTw`#5~P`3eqnFjd-Jt248zAN}0~6Ki*l?0MThQWX3}JB}Ch*L38sn;er6-~9n`$#c9L$G>d7uTGENZ!q2TtN$+6 zB8jHO@HQU|e@{z> z&kwUD!_(nQIKN9bFuzatWUY9mFZutg`*F;F<3zDRm+_-0Ikr$e6|JONx*WVMp~1el zhJLX(m~%c@2IOfzN$1bY-2PI(z%$7-M|2fa0%h1PT+h;>S}ck_*}(p0<0=WN-1IQb4PqIGzjJW5w)3qHUtII?8^6Pd-e}=7GM3CQQ{u38y z-50qA6F;;0Pwl7VZ2K8-v3`hDrY*Fylok9+y-V9J@}#_KT&josNh%^8y=p&+9{LUm zTx6@`J0{}tYq9~yt4n^2IR5K6)uz5`%d$A*^O<}m^Mkf}4KnXVm-y$AS)ipIpo1Lf z2eyK*~Rb?>h=}ozmPX z{yvfS!#e;PwRe0f)s(sPo%@OI_?F4nv6M{GANmFISVY-FSj@!9gz>YHbUT#k-#kbo zDb+{wZ%2Q2(|??I+H0r&OH}>%b4?aE8HdAa70pyjuoY^DygV+jN6`@pUF09aCV-fY zAv5O-C8dDXkIlNc2P^`~VEsJw0IzTzC(*fF=Sxq-@_~9BE@yFL<=P&0(3XI$tUDM;~|3Q`YUS-Bc)EwsE3Z@G{%QB#^a1} zj2#&PR#=6H8XW;H)7UEV`@ZPv#+cwpQmYj0?UKc&-+JJ>6u8ptFVjVz&$mJ!LWJ%t zB)X^1ZykN8;W8QueM&v;5jl1+9MgSKIkg>LxZbN=JmqZKyxtp(ktncwIR#Zq$3e|K z0E}1OExa4w^0pbLG#=*7SnNoxoiljK$&lxlshz4&3ND3WyX4GQWX?sQ@zWnn{2s?M zT#$ci+W+DH0`y(Q?`9C~j%?aR{O%C*r~XtvkLr-`->Ahr_aM)s)4lPXYvu_zoywQ+ zUqNtZMza(iyw&{u;=Jem!?rEez=*XFMisA}$ltc^Utmr`#8et-maEYO?vaPaV=CEJ z{L#_r3kz$uEoo<;V03KM%bC2#MNR6IiKA2F$JIPTr*du&kI;0l!8OUhV%x$6p)EFx zX&6tS^5<$|3*SCY@`tGU%v{*~%V1>u;&&q9tqmxRK>AzV(+4AOXZPg!Q`RW+$OB8HVE%jsgHmKPfve$JtkO#T57bf` zSf&nQ^4<&+Fn>%?T#{qAKlz4VOf8=pbXjg&bNC3?7NrOdGTN752Nkv8$x3AI~3(F8e#hTdq~mM z-E1{>Ge303|EkdZfakFOz%9d@*&4E)G=-pog{wieN(eE04DIU0#g<}dDVq)le%`$_SWh$6u7@A%Z^@8H-T>k~rYmqGP}Y_{6nX`i#$ ze6qQb*rTyN<1IWZ_$M-!c>_0i&vtwv+~Xb_yggp^w!O;sAaU2^$kpl*G-%z2-ar;u ztG&U15Ed9mWSQ(~l-rrGs$2RBuV=3e{;AKY;8kx?$CtuQDt(VlRFJ!`gH~P3DZcV@ zN-Jvoj`!^K!N1OuI=+NTvO8ADQXFgfxf=Rx`}OX>&TJiDLfM@ENM8mtdCOFt=1lz` z%9sBV%4hq5WQjMz)nttKNa#C=`HODw6=X-@l>G><;-d_Dt=27&GnH#kBR~WwbhQ%n@akM+ts?!5{WeDb9 z{bU_tm;0K+JEi7l_t<$-=!IvJHPFm+Y<+X=nJlV4cUfNUACf*%wE_FaaMF)*>Fwb? zVHHXG*e->BheCayoh$zVm*vDuLq!fGi~ruyB71eOz<%)`;&XRT|I3_x4{>yv%v$;b z<;HoO&hc6{#9FXI=IvKn6ThR$S-Ui|(|TguP3)Mz<=R=JuZ%5*6jQ7YbhQ!d8{VqR zMdi_h_A8g#a_CeuNxJv-aBBU+5nSo^V;6w#KX`EFFm!eHXTQKU0;)2ftz>D zy3Mu86*U82_gaVuB{5YKd!|hVWr&rv<8U>eqREeQ@9NJM8DJ_DHaUG-dyeyalA7&y&aguJ!1&!VevCL}Dx*}y zsq<2OmwS7SSq}A_ssw=6h=((WI_kxqS>8#vl{%YC$((pwg>!P8VcS4BT2;*1wl7T8 zXv=*l;V}s!ncaSxJ%kWmpB-YdrvJWPdMvsLQQ8(#8<%+cTXQYYaVs!UT(f(@3zKYXC)+$z@J_Dyo7Mwm(s zl2n#k=x+%=+PX;KHspp|LvoKitrl`iExDmoEG-{@em30R#o{)?AE}pksr54 zrv_-tdbUi5qx@~(QuSt-(Q3!I04g(5fymAB92ShZMvk>nDpe@b(l|18iUZ1dG$^MI zdsV)-b1zs|_)`wjX!_t^P+KPb{1o2(`~lB*!=I=VcQf0;P^=%;WErC2$<)D0@NW-wmGwLQz6v`r03HuNk2=00PkQ_9p8kk7(Z62vcb(>Mm_n;^DN7=U$bj5)8vjI_ z_cYtJ`NwHtvE~c$-f;aX_V3ZeN9M9-%Vi6l&J)ngVqelfuZu#+|G4r{FegklM^r3> zXJ`?#Vwpq?+hWrTnK{2tL`=3ohrstzzplyK3~VN3YqEX0Rqf#vEj_!xJ9UIwRfWV=##Z3ENCqlAEnj5KJ$Iud~RgnGhpzAA}+DIhMPA znABhW@ZMMSLrcCN9AAK1k$2b(N7|$(3bX%laHY`L1T3noqWMJHnE7wikJGpvm?2iW!ts_I|hU~8Ls+8{e z14lja$Dpu&ZJKckQHQ0O+ET-=UCfSc?i-M}Z~98~;cLn(s@*8~syfN$UK5>OFyrgq zlod;`7A(jlx2^dw9-F_kV|9l9RW*y0SVE^g>FK_V$s!%wpL{u-f%os54@^EbLH%Wq|4@=~e3?yjs< z$%&))S6u79%t)80z3dUaS)>OP>H&W|gWVVlVBPl?4T>^eSoTh3tZ*W0d_Ynlqq7aN z7u)bnO{y851Gv|6qx~@fAO|^GN$87h-}?00k~Qz)6w|MaF-K<1*P^0PmVV&)kr73S4OPmFmmN6Dw{t7 zCW!rSg5)RnIPu+&?TePFYgqk5Kkv!(!}>X^`++*Rap#Bb`E#yYA0m45{*gQO58l~w zHHcKxqV^f$=T!9GllNDcpO%DJm>#GzFNHx*)7da;$qbSeLpK$@oF~L1JQx{K6Wm|+ zyIiS9GN!pY)Q!t(r+Q72QUKnlIuwj%~DO7RP=edW$$De0&9aIOj|79#%n^ z4E?_1djIni4Rm`nq-5+3DC>{yHF;oKVV*(xOl>aHZ;lLQ0!hvf1zFG#DEn>)c5Xok z%uXuY516@+45FU_>~n%4@vGfJfVOQt4M2b0aqI*C3ua`GUuSZ?)QM$|bVw_d)gt1z z)29rc;(~_$MkgfKzqm}5m9xd-WI>o6r2^LGKBb?BaKQ=wCpxj4=dgZ2&t|)QX|~vE zh*z}Fu^vO5D`Y*D=6h{UZTUe^zudp)`-b&pBSTmpC@L&wnwQc2puGS2&7%zRok$4M z+IJ_AbO)*qRJhbZO=@@UPMS%Ij9il)6paRI6i`k+ZKs$0`3{K5ok#s@hK8b~`WKdA$$~|K(ck!5tt~wFQYtflH zufFp2hHKMT^sM}(VQPBh*9VJiy z80z$g{Y<&8NG?qsUtihQfPm(Hou!S$0Lo9YUb1h%3y3bN@A%&Nl#cumklmTN)5Pc6jDvb6}D70RmeX&KIfY9e)5rywVVSNRHFn{SDuKx zs?nPmu&HX|35uxF5`|pyqpnRyU!$b0HLtK%%im`!?H*qhNs3u`)0Ce>B`33 zQ#fp}KB!H;DOYX0T^85G9&O9uE%b_`o|;}%TiG5<{6%j~)49pwmCVT4 z@1C!obFdBITJ%Scad}{F&l-$nNf%#MOPXuR5qqkR>|af3fJUWW;a5%5_QXGh_;d{s zu1U$gpew17y~9>~J}wZ%C?hAEK9kQEtoC1u^!u(y|Zwh257V$FYH<2_L|V;#aY6Knn@ zZwR|OkA+8u^rdb?won^j^OS*qq1>1*A3q@9*3<$;*Om`Do_7ceiuT zJ?GqW&pr3tMQ;B-cNM)3-qjlSOFo&*oKumky2ZAfxrv3Z66BPHC#T zehtWIn3SrXrH5$qh18RT*)=l7{_)t~B=O@$sNvx;#CMHR?(_3As?!Hv4owOpQJ< zxEL&{mSH6|JJw8WUR}RdxMSW$f$zswhmc~c-p0>BO8bv3|CRe{enaZ2ifP(%UIoW zvhOMK!lz#063rOFy?^t8>`wub#pnK z5PkA$Iyk>$_F>E8moPdQ0BSI4ME@XL{>jnQAWBA`>_`+ovwjg6&=$JndGjVhBQ}rO z>^dCG0&|zGTL#IZnDXPjWs7=W(s--#39?_89sRQ2nBl31F5HoF_SLcZt(Ugnh2 zEp@e_eai!S|7AKW{6z_IYCUi`RvrBRkklD?h9JJGXx~xj^3=`QRj3Z4=1oBqY z6Xz*KI0VGNwu!Nl-0yd(L${1+GW??{%Q=8C@MmnN1ma5xVVPhuGWQJOW7FH5S>k7d zxYIFszAf~89nacBIhh*JFX!kr8#!d3km_miYvzqatBr$E7IJ}(Rx^@Nfa^g{vVGvARZVL%4@a1i#v#bpkFRN+mb5FdBb}l zaEn#zb3x8&(I;KUZge?U92lw-!CBznLC)xt6KS71{mWdo+Fli25o8+`R2a}N+5L~T z_4KHNx&5?I{V~V>xYlW|&iK!&BkgABGTU*{%z+ z(QuK@hq!!=mxl0hl&ZUkeBgIUcep`2%XHSmY;iDcP9>+hgLa5ZkM?xz^K^a2XMTi7 zfHJgRGyY9RWX zO-O`-=9SUr&r{qV?MgoL_fSdm9QP>c|GckqZ&R_Y$y3`3iShqA)}Od1{!-1YJ&HmP zL45wVD5*@J)BH?GxCFk5;B$Oa!|_5_fLxxYTEa9EqDpY+&2a*)WA$cgpQiFTJn7u4 z*Up}Kh`yvvdZ@5Oh%I2+TcSVg@V}@dePF%o5($4N{V~niKMEj0R-+&4GHPCd{}m0v zBc$ezVLr##?Q{I7eSh~Ct;ieihKd-L4xtG6oufhu9_g=kIVZmp%4s996B6{#QodQ_ zlU9TJR8Rk&7#VN=kcc`tY^3t>IXuj?;!V{*T-^EREpX?5zJHC$NcIP0gZ7BopDLBD zu=IFb9_#>ATf=~8W(1jsi!|O39W5WjRXu^l|70wl{{%G|b++*CGLO~V^4&q3e%Kv~CL zY4v`{9i)PF25+X{&EIsj$N4aw4MjNz6iz=qr-M}H4$iDolKUTxl@3Y`jf|yOQ}u1f z&?griA=n{rXzy3h{=Q;pvJ>L1eh(M2pwe}mq*~MdOaH^1uJ?RCx5I`=XiMQBj{MjR z=5OO;*s9U>EIgH1M=^`-y`pI7`+xWijXYQ6Rjs;e+tNPmL$hlcn-Yx;+A4qi<87%S z4No?jbfJF=Q-PP!{HvucJ|p&>`y{Z#6a!evQonytru{8{@ta)uc3T%r;iq2q7rP(! zV~t!l`Uplyk*I=rv2*8?FttG%w<~kz%3?2lw3j{)7%nWqYes4HlNHcDb4n1lDw;C) zIS~*XxSS1*Q%cxB80`X~$vg%T?5ElO{9-D^ws3k$OJeRW!}O!kh5PfdHhW`#dMj0U z$Hqj}nq+heNAI`vPqghmsdn^>r)LL$t~Q$}t*vTnn3ya)rv(4|HPLz6JWQ6qmMmYB zAVA)ZH*sF?@3nQDlpOt9qHt%DfLh!EK&Tw`0QHU00~$Ltq-|x^`pj<#FXFG|3MY+zVWFrlK42U6_bi_d1 z={9*O2Z*-HbGTviMWdl&C}Nxs^FR5tKRVe;H^zVGrNzJhb;kG*8{@vmm%}a^U+75| zPR2K;2B0lR|K=l-np1-O<(k5bXdco(&`ZMv`dm-%&MNWDv*hG}7-wAxWv+#*&0?S$ z<=ZH#F7>Zs8nD5JnGaD8B!=yvLj89K6k(shd};g7``G>TMBb0s7L9{5cLDfxeXhc1 zg#FJ=^m!jXhPi)F`kVO^bdy02Ak25{tDnlnNc7$KCp!N|qh(IeeV_@};9kKeJre*( zIUBy_K^Xq(uJpJxcxAx-*((|Iu zDt|R!x!6cI?Fd6`m^f4GDL-%N)6bn8GJ zI)=%@g)+LCkeV1Y8#A`%1ULnp0FPmI;cWF~&p_qeuVF#_N+1TD5wel&a{--Sk}x{Y z;6=j~I6pWnXyGa-0McI`)D8qmC@EdIyoO%?TC^Wj)%oG0*o2M54mmBi? zkGnfXU3bC=-ulAN>@W9)n^Yr&-@P}V><+0qwkY`%-U==L;k4fVOKai3tKZGpo!H6@ z4k_(K8z}HExB6GC|IGTiKU}JJcmJGJb=`eb5uN`iW7R!A`h1y}#Fk~^ycjPOuKu2~ zbG}~w7xG+=VK$vh5^8@$!-8IQV}XP_v96j3e0{F8PWWG*(M|}MGLOp7(LKKEz~qVv zj`qdPKfA=8!-v9J8-k6=`^8_7fYvugQV5fFoD0mLnD>_nT}!G`$B-=ul`HiLIHY!wvy-zq0BNmt~vHWC9>>18>^!?0?; z5~Q&|X5EM+?Stx|H=`_1Wxo8l&hn2-fM*%SKW4-MKrLDO>P$`0{=E{OBp+&)cU=NZ`VPrL0u{V(f3-KIpaZ1p#F ztN*6%_5Yk}Qub8;dBD$LOUc#R0Q8Kts_4N>-w2Q^#q&@h6_V!rtATp}Z>C?VSZY~J z_hW|Nm-wa{F7{Xz0qqq7KuN>w7T)ru~ z2ft4LoHbdk1HYpBp(18SZNPJxXC0H1jw2Pt--S+K^k5yyh#sJhJd<|K)WLlcf8)S= zi+{RFzWlhKl_xpx_lka;o~-pCVxzCJ@}^X+5b-Hqt-H z#AC5NibY1iBC6pH@>wtbYCMo9CQ_QiRx4PB_-#0TLEm?;g}%?(V<-BC|7pR}ZG6=F zw0!FWfj?Ef=y%v`>qi@}Ps%4H0Sq(AI56Ic*;Mrx)xY{bOT3x@BnRFX*~sT+I{8&1 z4YC3ild;TG+`QF{1C?hY+wN7b!2>+?x;>+9ynRs@p+;}*rgZuDuc6O}SfBgu-x8DQ z-SvAESE43o9`khSuj;WlKi5q1GATWHnVapi9AB$eM;Bh8^65;FG|oz_jFOelObt`v zde%jGMYVHQ2_$zJzF)Ju`-4^&#B_?5KVEdoO3fs=nr(rAksjG0x4$T@AqgS;X_TX0 zW5XEsbCZA4?*mQQ;5MyY3OGcLS0R4pf;5>)8Km<&elXOl+Tlf$JCrAusM^^uosL^# zW{EngMF^pDSjd_3?xm~()NI2>GM%wZ>6=v~+ANPJu|H>cL5)RL4zBe<9q+l#x#Q=|{x|g62 zRrVAZz?uOXI)jb|_A`#4UHK)ue5if^#2siS7*5t&01mE!vzH&eU z9t=XPno*Pm3;&i1?5nm3$k~50C1a`C0^1RG2iCVZkEcPF3&onCOcL~D&Vp^RWN;}Xljv9^75u_{4G>-J9+pO3#?T2Ph4(o4Mm4G z6=Z2(Xj2J4c-!+gsJ$$BOjUo^<(ID;wIAqeuONAO(>VTG?QtF|D4D9Rakrv|}22|DC z!gqe|iGb@R+Lpt@7)B&#feZl~z)gH?8EaRlJ@DS8>`^d>7fMaWmif*~ZU` zrh*$fD=rQmQ`P4xf5Qb-S;DvFO{M%TboiF;yRU*zv~4L~bW8E#0WytZ_MW>-6W-N- z^gB)lx8Lby7Ynk=HGVtqy!rMf`!(;-iQNIf zcXiXh-+iW$3ob$&)`G+KYvWta(Mq?6?+4|N$KS)PMRl28= z&-;^HJyTt+Uk&o??ebK(I&#=7*dGhhR!a5DE-rB8Bd&b0E5Cn`=W>@v1Ply!TKP>m z*S{~gG~DWNe@$N1#%O93*{P_9OV*VYEgcKilgYfKP_^ossz-g! ztjXH1@G_4!ia?O@aWC`KOnWvP+1w_cH3@jhV6OxJngN7Gn3(qE54GSRiYGHSZUz#f zP0l{9wrVvNF9KQc^LlOh+M4tq@s-+^j6Qm&^3>o7hX=`~+Vo4c<*V^{BB;fd_J?bp zCQxhvx!SBxZu@vp0yl=#?|@paFXPz1sQ-Z@SxT?j1)bl8jj(C;=o|G&AK7UsTtFJQ_4?+*QDgWteGh|n(i z0~5HUK5mX#imR}gyo#2x%6_j8f)}*n7WN<*PjoET7X7bOVUI$!1nJ5)lIOJSFid*x z-kR(7)#ubRjP#4Rm+n-LVYLCu;6`&__`Vv;)7Rg7yUT4IOm&|<4{8Qzl2iIOb#!F;QjY+|ZNk#6u z&Dnp^Xh?^V(nWzUXX85dM^La!lt11h|A}AGP&b=z+I85Qw3SJBh;R0Zl(IMmF)uoa zXXsWgdgE$vx*D-#?dT1pGAJA}Hj}kjS+l-;yPE9x60VlQ^6^1#1tda9*W6eL7?=H} z3GQUAEniWa-g&9rU$@`|^4F$cNVMf(&t1{fxeU~!@Qyn#0{H{nXiQk#AiFOmLv|4IvPyYO8#kiD$6HvL|r?O#~R|2!o-th_dR zQvnVji5+iD$sWnuZxnpFHob!QIsKuC*_FcfbOpt*j-}~PGVlpWA~?D535g)~t(#Q& zaqg!OeQkPM`wLCgBMuN_qI`3qH!XXafU4&%yO-Cc8p$$o+A#K~N1o&GFNzYEaewT} zytfJZy}$dFerQz;kdZTE>Zi|gQlhN=$I0~0r%XV7Wvh(f+c**Jf59F*&a0&}4^WkB&;Gg;~I>!n(|IZ;FQEmj3*<<3|ll&sS z;Ox`rdxhpsR&x3SY~uu}9^&ecJK4hI#|O_k?XwKGS7X|#g0Kt-E0+3C{}N#C=)3io zUZN$CpoKmyl^A+C7X?5O?<8AWmfu^wn^ z^N`cCV~Qtdi$0%7FUvPQiv{C@L9>$xMD^|n3p*1<*j@71g7&)->3>oflP3LlW?n~_ zxBjJnaPdcmrHhZaOq1wA?&chE%ogM==CdtUS^2kVu%bw9eO6+Vwg zZ&?n1>3*&eiOeJVk&=KX`TD|I`Q9@0}oq-_$is9mH0*0W(#fAoW&85maE1(XrHs+>;ti-8#+OBNk(Q38fczEfjXpCWB^}wr1g z&mNI_unF}l@IgMSEkG$}U-otwqqCi$g94I> zc!gf;*R{Jb0hxP`O`13i#4cSiVH$3!6E4#J$8>680Xm^^`2-~PiU}3Ly8=94d9f7m z>6IPv1%y0!W=s=lUV6OSCNTd1|L>TgIigewB2G1d3LMmAI+-Tvyo?_ged2M082*mV zmu44z;+Zkp%8W1i{zdUs@8|Uc!ZbwZk0#wqPnZroIQ0nDZx#Uc1$w$vPZyFNz{+nN zmV-Jcn9TP#jwCTps&VtV4m`4L{dqoSt8U4Rdh?hjT4^rGw3MODFFd2J#SA(UU(*-h z?ST%KhfiZN{}g~ysXHiT<4$ul4tp?v(``6@*ppgnyD7*e`J>aW<+z&!omwWPLt_SG z1t+(8F~zXdZ^7wR!R~CNTkC-#&i!!M^94UWa|aeAXlBAgjZ39TiViZ9xmjQdusI7{ zwrKC*B2?KYO&&%f0&?v8!(^=D%&~4l?YstceRF@9pGYXdjTo5m_x7@POEohO-flNx za0$jST?ovRnHE%c{0dw)Ro@jw_lh=aj12xc=uZY_?u#~RC>uTSbmt&OGaGpeb9O!@ zX2`Yjn-;RjzE+Ez=pQiJci_r?N|Aaq>} zYdIQP^^Ia&m4{8SLk^41_~yjulV?xqcj=_`85ihtZT8H;fw$lnJ|{Py5YGa30D1;k5LGnb2YkfQqdLAw@^}<4E9N41I%yB-CY`(iFVL znLln&{TjyHVw6C!jkqMeBfjcA_22kMw`Llni|?liH;Ao!#;6)@pY_u0SbyY6DOG~x zkFHo%d*IRjSOFE4go!Cp6?I9K`p-j3jw>_-{(#d|n+jN!swua~cA)AdH92CPdE#DL z5BzUr{pD+%qe;bHVRcOA@)f5P^Oes(i1F$FWR6~ON=%=+^-=Rbo*R#ww6ZIAHT#Yo zQbPWY$UgjV9&81rbsnsebENl3FgDkUma>sQ{LImeKlV^z(YmP)3-o;N@`6X}Rf#CO zmoop-(*7W>Z{KZXgF^>owb_*WDB$)cG8;A;ce){hXY(!OmWU$rC^NGg{;8Te^Ye$z z4e0mhpCBZJXNUP_>dKc*OJ;P5f@1##nzvJ+4dr6+(5TEzFe=?nP30wl8lZJm6-0NC zaB}=p)vNujkX){eW_qG1cRMM)KF$+`W@mMv3JE3A6xJiUbWbq5|JF_N8*|krzT8BW zM>^hNC#yJQI5poFS*ZcluOSL@{#)L1=|9LJHT4EucPbu;<*7JnG3B1d?^(pa3uEOx zqkOC1&D^=KFGeznTNaAvZYdCzDF|n#F~Ovtx;pN>aS9SvQ@^owhv|eGxDc63gLH~GMETm zHvi*>Q9{W&FMYFvbJ#vS3RA%)_GfF&DHl9$DfZHc1Tmx8HvQimD9i}$^x6v$&yo{fg zUv=4zcs7)6kZRn78wU1Ob{fD8b>}gY*MG4;FVwBNta_k!`1uTI(*NV*dHeTB(z?jK z*Hd5gKl|M7YnNbK);z{bfl2+Dcoqz4c%!#QRwe2;osm@{YY4UTF`Ir%Mxb`f3jas| zYrn1wj6T|D{dAui>F$G}W^{5+qf^#g*rI{c{C#&#eqzmdF^w7^zyTH+SG;xNwHi^I z7f8>UHN`+_anHhMVB;=#%G7vqM8V;5UD(IG%;O*E}He2PwsQ{kQ7-9M*QJ z=f#M|Cr$6i()2Vp?Md%+O6UEmGh6-ry~LnJSSS$6TBC7%1lqt8s;&Z`9tYcf^fuRqNg|H)@B~ zOP6@*xq6Z`F@16KuSF`zhfc%TQCDjiEXEmQA^y&x*gy}Ke{s+JqR%DWisb$RcQ)ydXwwSUx{RDF|Sxc9O zvJO_(31p3So+=yG-?Bx2AIgT4KKgY~BQk=9|K88C{o&zIHqns(QG^8Gb4<3JY`!cz zoF8*Nsy`?sY7bS3d2d@+7l*IxEmy*~T*B;L2~$aE@qhg=4Es&A4iiGX2+jD@{8f9| zFykx~OPq+QdZA&8tv|e~ehq{{VEVvIVnT64j6A$dE;gZwP8}}+Cc2ilXDV-IDp}4b zZYO>xnQ^L3AJC5UHq96zC7y73umbsaePaE*rXqxZFRGty8W~Uq-GbShMGV=$QBR?^OGC+1-&XfB*;0= z+I^gC{=wX-+NJGnjQMp5P>-Is;( zQ{wNt%f{gc;UlJ_1$UJ&V9jeBUKHtGx?4jorA(g|>(g1mCx_o(^2zAOKQZ|3Mlq|b zi}>W9?3!Ena!)i}w^?ZV%Y&NWkw2R{QPnl6hMoik9X7x9DWcut_QB8|p9VSl1y%2R znZ4w*b1+fyK*-*DwWo6-PCo4AXjz*Dj#_c=WRJW$W&s`)vGq5z_fJCTd(0}25`bomHhH4SBa-#<$lr{&}v&J$mZx1 z=k2xVf-(EE7uhJSj)?;e)xWaJDGD(!yQql@qfeYo9`wu7b{wrf@G_OCj`d@rPqId? z+TF0P^N$*L&R)^xkJNtE?kfj2tmAz)_r%i7rFPcm06X_ERI@Fo(4QuwGx8J?AVmI| zY&hul$O92_7vVU2bTycBs6pSPOJHo z!iN2V?R5P6J2%u-H=P$Pq&S`ORGdPrzn9>tp}9`4{zH1nM*bR(<#dn|Zv8(I{re9f z8?Ni7(DtGjli%+M-97ZHJpEm_p%?w#qwJ!eUbXFs{tkOGM1Q4u`eW5}1|)|sJ0!>T zsqm-2`#48-D8)v08VuYySu%&nF4~MxWHuG)ebV5MyKpZqYRQc7(uk{ism_a=NH3`W zdU!+%A?QBFy6+Jg+J}RK$tM{mItS%5sd(09Xp6}Lqu|&66AD)UrE1PPyl>himH?py z%2A-$9@hP^jtCxjzKbI8&(#|TqxTL?e=%_3&)Wn^;Qw?#H1Tmanh1%%J;&h<*;{MudwFl;PI zRUe;`4Q&2qQ5E88x=k=_mB;lqlQ#m+>}PonCfiLxHV8)xnCr(h(L0`*((*x4X|>qD z38xgZJ4KuSMUGT;8Q>Bei+C0?n;F|qg|)b^3~;8YBl(4!KLW)P+Aa!v!63~mM2w{^ zEUGkfy!&U>lQS)GUBUz@r^;Zy)(ocoX({n z$uUw3{C%(FY(#G$Xt1{ciOyFboq%Mc%?C)w4*YU*gL%;qM6OsE49^|MD1>4rQlJk< zM4n%u?~(2L$wb!LkCcDS^h1{MYNe0KmwuYT4sb|*(SzYID@E)T%@2+K?b=|^HZme^ zbo^8Jkh{Q?b;j2}Uatr8>esSaom&*5#$FP|`r~1i6-n1WSFV^Q(yb59QeBl+xie~d`@~$WU zhF<_aC$G9J27SYSx5m30{HlMm0ReCW0gm@Y`S;@qIjjyE|5!(cJZ@?5&xr;L{eyqF z7(q&!>0@U43T!j}ZP@HwS}sGqjP;80YiF2)f%NKSIYHu-4WG*LS< zY4d{CmzzGKabf?We1+)>2hIm61LtY|q$6+ZClgs_Kb_$Gj#m{vEMNFRhAwxx_$?bb zKfLBM6@8y|!tQkK^8VcEoqf~CrTq=R0IuoY>3_p(zKsypG?|J)}wp&J7{UvI=1mh+?1)r#q=94Dpc9TL;jtvqMJeabcnL ztK(ZizeYe|VxTfff~lgGyj8++71sEt(*pzy4;qJK|1$&())7Y57cO)}sC^qBQ>Ek9e#E3W zgyGtM7unsjjr1GB?|~>WyS-{*b2bL$57hwcujwjKzRe}fy2xodafHT)u6^C#`Uh}d zkULU2vyq}@p%vXw68yz$Lm z)yJOu;!m~*d-w*~sKP%-*=9^2s3FW5@Jou5xqbt!v_ICX_C2lt0{bmkn1RrO|BGAb zdBy((qq+46W7D|anct3t14h~N^9>O~YjAE|7LxgE8i=E+C1+%Wm5l7Tlmg82?yXQi znaJPtF&$~=2PV!A*{F9Ihz6gvB>J`Pi)v7?q=*ec?%$Bo{cEPkF{*)%{rEP7Ir;H#*aU;AI@1&m(np*PvXN!^l5@V`(o5Ta$mhrY6(YWaY1Kb* zl|=fj_kaam<=JzH`=X!dg6~S0^svuID)&MAOYIyUT`2cD!Nh@=RCRfiX~XmEn@v26 zO3YT(;x0!;!?byI`+mwV%HY=S8pw}?WO{BReNRdxy7m~6FE%z z(sOmom{3^CILA;Bo^r`#^>1Db1^=0o0@k?n3}=6t^AtV*t|mNCSK=SJm3Fg{`@-RI zW5Ux}9|#~>=G>)SCN8|SZ+W9x>2qB9nD@!_a*jV~s__5vj@7d*9Hr?X)w8$iv5|~n zz+rv(9v^R0xiehTXN50*v7O92Ue*w8zY9Pwce8HSDD^!)Gc@6)-kmP~n)BVGyrvE4S1Pu41~1TU%Q0fF#Bcaw!1M{!?FaZKCMRWEEz1 zNL%NvbWK>@gaKM`jf&~AruT+m+-X)VY$AVJ5JMG7{Yoz&2N6K~;AI>8#*Get--0}a zp*k~Xfp?=}Bcc)tF?Rl|Z|B7TXstv31z5oRB^7d9j$1<^ag-WER(^v%xJ9%?n_Z;{ zr&>V)15Ja0rn8U)MY%UMLsoqdU5LG*BX^?S*j6{FMZfS5@t^O2IA=1!by22!>P4Sy z4Ys=T4F_O%=6pk|b^Jj)v*2ZeUr155B=kS_)?K(_t{g=sKFim<8x`J>fy)*5bm<0G zUhhz;XXz`~#PL+P+ea_(H;FD{K;?U~k`J{+ zY11K?cx`5?Ikszv+N<)xSrh4^7vyWH%VAYfGV_bzoCRk6$H&~uCpqc0@LQYSjNg-2 zwf-uC$k?&SuJN~%t&WdqU7VRai&~I# z9F>K@oqc!$PdceO6}r++zivO%Mxv;qkpq%TSiG~)O{7;Pv%`<#g+m1dj=Y4YMEW1e zpeM-${;sSx084C9>zS9Aa_pt$jN=`o%=m8_{_SwQAsD;Vd5PBuAqBW4!h`fFJ`c z2VxU#pPrN${V|7Ajy2)HWnOL<5nj}hgHnGmLRQel)ZLkKEYVQX?H9n=;1>k_G{xjv zP^88(qdtHjh>e^s`e3n8W|vrN@A6AdG3_8?E+BC0Tt$845%M4zs z*}%FweER@!m$~4%Vmq(|3P+QrHZx-xA7)@RN~AyLmPh)Zpt`m1Cr7{Hb*vD_Pv=<9 z@5EJ^fcWPwYXuaz&jFvzOqia`OrD0^IJ8q-JSsv3mT@|MVLELlxxKF|gZ+(xc%uKB zeW_GD(kXwSodp+AnWVqflP+2-`gLgg!OUdllxdx3SGa6>o;ibNClosSbh2t?bm0I4 z=7mIgn*w8(?{x8#b~04Yot7Qn&sb~=jT(z-U4(1%(8OY-XS|>Q=f5<&&mFCJqF?(r zxekeC;O9eaeHie^os16Sy1Z|``1mMG6<-8>6h~Tl_3sQT+`WJIs*!-I^BkXRMgO&k z;|*|=R^5snBrJu~Px0N1|q)K=+x$h6KkE=|7c`X0&1D?2S(0_nLe!cW3 zNCY}KP`U$o3z1IkXwS+TF*%(^xkWJG5gwbF?VWN zlk^{ioc;uKPuL5aQH;j2<42(E@c)^ZPzWc#7~I+9LipQ?M7mXzN`jpx{xO*(IiJQE zHajHpj=O{VJ%Zlik*%Ac?N08$4e_`ctod^B1A`9u(23%GyFeSh>RaCU(7V42U%c%M} z;B%_q+;^OE^bSGA>kYt)P5^srNc)E&Lj?7J%>QZt1DBz-20SK(Gdl`vUl<~~8N*9# z*2os_(2(yl`lXW(^Uh_Aa=(J5T7&{*rNqZXKo8_c%2+VmgDtKHTj+t{$&Lc60g5|& z01Bph9V{3-k(?}Z=-LyKyNc#6Xs&lc&y7xS%eW1Y>1Bof z?>^T2pkn5W(&cI1h??KTRPl_BS@S@nwNCBgQsUzUWGA^WIg?z$5dLQ*)`2A7E^QG~ z(f;?Dhktg@8L6J+W%41AUlq9sL*&HA3S2PkN&A01>)?%;!+$9J@>5=jpOi(j9PwY- zH=+wLQ)>N#dM_h4{2vo1aPw|U)6=D_7z^r8^Rn~Y`crnk&aI8?B-hCjJ4B(Is!g3& z1!i*2L>~LwK=`4@SCPjbXfv;wa@tJv`32}x`(GzvmiTbTAc$-gcSURg9D;~w?744? zDnsAUd219Us35WSZ31g1tJcrj%X(!CO-?bF=4<{BKz?#}Umyo#-UWI|2=qL0dol<(^xv!;&H)uEqc{=wQ55Re~Xlod^Zw+cOK#0BmEBhb(F$#3?73=SwF0_{2OxL zmHC%tT3c>7)*+F8OYS6DtwRIv9ndNbL6qsNUl6s1T6K8bD{WlxwpO#jpqNm*-#+B` zwq<)V#+ttlPI?8pc?*8|HhGp9cOm`IiI|m%-;$chXZ&hn^Z*g=XVy}6HuB(aLWazt zwC(@)|3Yo*qW1J)#h1MsHv#fLyD*f$pYqQO@;jtpW4*%VJcoOiymYX!P1^;2o#m9s z=d=HXjaK0yy(+8>vH?2T`nznq7ldlFcJ~UhRVW+w+YPjE8!Kr440`52sJ(z}B>iBh zn00UM>!I$cf#+NU_uLlBX)byX1UV&>43tKipw36G59NddY<=X<3359cq29}=*N*^@ z=}-O=0Tl3*muWI<8Kkv2Uw-#$tNh`xe|xF0c1TdMavB&nI)HDwE(8X2%>4mEJWaIb zb8Zz*<`L?F@jEeSUo_4%I(R1&RAz9S$acMb8veZ<`ZNqP>@61X!k_LyTK-0;p@Ycl z|1fCC3W7shpYQ^5?t3bfdAKrP6l7kdR)p|Ma{BLTknX2}{QeSN{3O?Nr^5&2q^5oU zH&l}iU1?C0bwM=^bOUn4{7_D#jrWA2JSJeed)0P!7?u(&%&Q%wlNNj#d*z!zKJk}&4af$n)c5O6 z40Y-stbcEK$hiL&lSf`=6eGnx@7pE;oITH`)*UsA#pa3Vr1eOIcTSo$uI4Gn_pWwL z{J?&}m}#^2LD`BK{pOl%-g=LD3pI-M;!NIktQ{F8kru|LRkKJ z?Fc#th-6T1o-L3guWg2+ILH+*1ik!|jP-byg(jxoxyz(Vu`USY9{sn$%?_FI4-sP# z^x5p_6MSeok~9)-c3O3^*iQI|1S^IseP(D;Sa+eNtm$0c2e!{vB}uQ@jve~Iyg{3? zisOI1!MI2GoK6aMO>4<3DjF_{Yump};#Q>KBagGrW=_;GQ`S}a;mfbDQd=M)HMdRB zWF{^(J51LSs&J*KMHM@#Bdv<1O9{D=pP#cbEKFG=pwaH^h&Df`dce`I^lK-mnqA!} zU2U7ql)G6;gsTe_YxVd8Ln8~awzo%M5BBo*KQP!$fy31Aw17NR4(&kYzxZJY?OmImB&*K@Mll3Bw7nLi_9b*l|4<~?@*IOUC%tIX z->g#i4taZ;`pRaFdRiIUV|~zR`}zUzxBQl*PHOUo3)Vkn`|b#0hvm! z-!_GEnQUlKt~~IJB=J2h=lWDArwN!m*}K_-HQgkbb(Q2FxjBsNpT%#B zUwbX9v+3LOtFt}rkAKeWQ}2SB88SWS59`yZI8|ouvKcm@dG)l20Qd@B!q&w+ZI4A4 zEEZu&Y@Gz_v=2#UQ!)l6cC443zmC}UR~;?;BX(oiABns90GO`L6tyOzPj5!i`X=Q~ zvNHckeYN;Pnqr)#^MMaKIYcgG#4A2g15^j&;@Qh)>*7t4aoSRFo3i5cX&}<}#HQOr zMqNrR^IZH}##ZiJ>Y|#_@|i`Kb|N765`Wygu0fLzQ#qt#C~&;eM6H}i#B9&~T^|x{ zZ5H;LM=b*oSjE#+-*l~(tx1H#`-gT4Fpr2*F+^SJ4k1m9rQEVXebSe;+%Vvv`q=># zpx<3m3r9RzU+Bg-%!rr`NA(K{Ck>PY8PTk>gO9cgrm_1m32heZv#!XsXsX&Xu9o@_ zBbA}NZ`~cD&?xxy*NH8F1>j+lU%g`4eiqi}O!Ip<$Oc}?HkWM7-%!2(u_~rK$PF|& zaNzjkMAznw$3ty8{U@kwoEE{NG+h=(wO??pPyI=#fJvIl&H~Ez^HrK)ulTq1Ae=3u)3*R*&gv%}lR!pzv(?p-){ej(l z&>a$S(IkYfK((+-8Ki3)J)!;S!d*5m8e2u2VR*gB-0ZviQZL=D2yCl$M!)T2RacCJ zHk--chD2%p?SWn)B^kbn`?+GVZpAo)JDbBFu$bux$X91yz4FnV_8HYqoU|_X>(n0( zaLKNLZ0q*}A@oC8dM_ z?uE;y0^xgZ|0ut|j=-ay&qQ7Jv_eHK1l=Y67@BQg$ms2sIO&ukH(4$9$c_PsUJKIE ziMS7S1Xl}Nm?ix(i|GBJUzEDyS+zg|^H|usr$7WolP!rItCOP#@6HkZH?WF6ii3w^ zgZp{v+$*^&{F1eol}`y2TN_fQbC@Q=3={)lxDM24zA3;lBGrMB6ci$wRZH@+U%I)YpZtGMkl_WC?v?#hD7w#4T-9a{A{evobZb!IKX2e zVgH)M1#wY@^JBA(8U`iM`P<$e?hRTStQ^>1{sPw?fp-povNmvSXsHrfCA4)dG&Up6 zD_i|dcs_*UafP;~K0`in1DNE~OpVNRjSSOQq|%J72zjn|`XbL37>;Ls*K?d*(;_c> zvZ6a-#9BcQ)jj5?vzH+i4k|nkr!aF0At5_?mA7L7Dvco=f2mCjA zqc=){EcVYL3ZalwK8DnaEnxsji9FlgT(%+u!nZ+h1H7*t94rqSN~#mBd)yqW5q(5d14jRm4TrZvY4H2&%9=-!SHfeAyN$r9j+zKrz za_=&wZ8(ri7= zT>VBdqy0vSSM|y@`@qN1c?&e{rju{r?MVnrFH`-UuM!_7Q}hPp@kX(`Tge-}6QlX_ z+``n67`+SsrcKcW6DgO-jw0yjcoXcNY=%Fa36mr<3k(OD8}2~$l`?xh>PJCWJo6+D zWX#XeTIsp>lZ2lIw;{ZlA3|?Fi_JenHL~=(y#sd>H`Cu#r|Nm_$j8HqaI$ls7dvD$*Er>ZmZ` zI4=#k@L;T()j6O47(u8U5~ z6cw^#T^N8Th29ZRcko(z!bc4bKIZezJ{f#R|2z1$nI$@4;JM4h*Sdf1X05+g`e*Tr z|1bUv3H$P#-^MTLzX$YeBzj8^)zhDc01dN>|G^(E=b|5ka@rC0x*(@yg9m;1E6C}; zg-u24KUe=V{|J?0%ain9NCEUuyaKUx4tAw(Vyp9|m;5sQm36M>hce9%NNfFJyea~> z=xDQv7RGWM`->dptn;GJY~%^^(Z^n>r*Njuo++OaArFI6t6AB|_a7AgI0xZ*PQ zuYKPey~9gwEyUFkCFN?7g&MT|PI_zOZ^ zmWwR@3;*K3shS@C)wXJrPc>Wz4AwmU&m&9nfU$=YP2HlBmh7x)wnv4JndZ3;qV2Y6 z(?XcTai+u@&Hlp^)xSA|iF1BO{g*!UqKiNFcC7J6pHz>Y%-4^#8larOB7e;4d*?>V)7 zhW;RjEAZoqKH{Q}xZ0ZPrM^$dwPvZ9T-DYRUf>k5B`pjFrUafVLM8-_ahay>I%bu@ ziEMHV3fd|zzJtCLTX>L(;>pE{=+BV`e+PVd`I$3Jmu@_bOo`0ZI~$t^$_=c`g{5!> zR*bz7%vv`LKzk{UrsNVu1aNktToCZY+P@}t{5d)L$Q54Z`z6Wf&$luQv%ZL?bTJ;S z!Q)z4+imbxc_RaNMvesD7+Gfeg%-mFl7;W^G|LqaQ6+Y3W~|%-^f$vO1YUA zcm87rw6m@c&s!&A`j#&rIQp3``k5yBnI??P>{=gxru`|d(t&}={M+u~gX~MTzx@@k z?4{8v=(;u_vH!lQ+0Ut#(PsP~bg~EypMOo{%^#JAl0GSjX~+j#1NHLXJbNA5CF(@? zwN`fgfpX6>!vu-`evYXAsHQW6S9m*CqvCR4=gfFw?&gAmoityB05k&%mMC+alw*L~ z{?Ct*|L_I}D0(^LCsL4%bF>pL$PZ$nfB0ILZyfIE1==eR^1SqQW;mb~R0i=E=Itg* z>7`6Bi|j?nPMLFcD^Zrbpc$b;1)nqu&bmJQ z#>TUCWDfV^yT+pj-Eo0 z@?^Hi0&^lBv3P@jP!gBy!&z3x3C`q&AwWFNbhrG1^hWhB@IPtdv8$`0_3hmmQT-~% z3^(xNn4wvpvyqkI%|vmM|Lq?MFK#`Ie&wG}M*kfCwD^yF3s!0GZcjprGMj0A2p2H9 z;ddr3un}wP5?8AYJZV+Tx6$Sw>ot2UVK`^Ak0g(N?#@|WPu#3e!Pf0cU9l9og`@A= z-XJ!-I$GQJW^MMIePfrbYB?9yq_*N1G==4y_I+BrW-3-egy|hrv|R`(fmpa2F__$C zL+F~@6;dvHArS1!x6@gmrL#IU-}zZq1rsI{ughcd1fKT1fE6l=nG}W~^i~wDbnv$U za`DxFSNxCv5PuuH^h=1CmJR(2fsTU%9;Liac!e8I=d4!YCh@Tmd9F$oh%A86p5s2?6CgpGt({)@D z0B9PW^XhX-%pS|}$`EC4i#G44??#4X5`1vhDM`HE5%l!~Kdg-|e!hLPgb>d69Oa&Q$-h`Yn1iV)ih=e6 z=ga1RDXC1&oJ2F|L*0qr0*QHRw|b+|6;AE&4lVjnZR{^FTFpu*3m^jn11=kf=7kns zNOOi>;ii<^o&W@1W~Y4X#Y$ljr)1N>dcd&c)cp(W3UftgF{QZ(EcmRYJK1J57Lv0% zbh-?W$~T%vRn z2Y*-abyD-D29&p@#qD<~Lw5X0*=q}ATzA31p@D=|C`n{ z(Pv(GI4Yen z`k2NA)v=JtryUJNm7Ch%2C7eM77Hv$aHoKc&Tr+-OKW4>=QSA0Rd4bgV6 zB#8%6&c0UaTbaz4HcX}JCG0Par`Ycwj0N-OPHwn!GAR1{kG&Nu)K{>ugSoQ}$`uOB zgg%4>f+TmEl6fs-rJTnSyslxTy=h%i2nW&uiE{RXHDW!I&`x#z_V(Ol)dq&;l*>%b z(WkBoq@RrZ2E%(>pKCvjg=;s!tLZ1jm7Ba3%dIxol@N-zfV$>>Ni;3kKaXON9i6Z3 zk0dtx9jj`i&$QM&73Uq}JfeZgWAlJyVJnunhNF_v-!D%%uU%4~Yq4#sEdwyjVUE1E zFp;e@jHPkOUDduQ8GUAXjS-bH*Jg*6CaYG@K1HHuR^@+FRqg~KC$%(=-!uz7XB1`_ zZCuN^^?_ZPZGfNqs`6EBm>m^LwRHkYIh(+QV43yUoq~nGVkxsbRzXdzOVn@19*_C@ zF(sAHx3qs*E+(ddvCLb=ROpTVgq-|8akt0DnoCNu{9EfMM(+fHiRd#sEmimPpvlcf zDsqY9ladmpJ(}+`{>bc$kWF-L1{B5I&IAPkA~e$y3oK7m5mQ&E=Y3t^DTG951w&4F zkV`$IC|oGHb)_86_K`L-C{O!tmUQj=gM)(x)Z*ImPayV+ zNVb@IOP2cIR=p2&BDrt*@Sw2Vhx|3Gu=<4}({R2Qc|`eu9%RrfhE%#K`pb~tVK^2V zO8gV|ceYiqByn6gcGr~USudp6E{pY7N6)xHA9q;~b=7SGhK{{Ie;u{{Ctk1;s1 z7XU{l1V>-(bAPyYOM2cP-gF>6iZ;*hTD)_@RL20?4@M#_XJ$$iuC!}lHt&V!bYVgk zo!o$AWgCa4u6{k4iJU|YGW9U1(@)P18)VykZ|zlExY2F=4XCZ$$i-A^+}qx~d5dp4 z@PMYc!>)S$2J6mN-7yt+Mw_)BKbf zWD#EOWjK;mH6;!2C9BRTZSbA_DOr_>HN4)!h0Ac?C$Z?le`@N3GCeO_!w5sR+AYu@ zII3QAgd`DG`X=fVRhIdsw9R>vih>5H!S*A~aBB7qP-K8upJjL&7r5GsoUs`&Dp30j z>Hb9aiaz1Cmzs%4qwQK}XVP?6FV}4!dP`|E-+ zGm&2#qVr*aPL?u0n+{9Si6IY_4znrS*7Ae#H$+wpxzl}27qQ4sGM(xEJOtN}HojZD zj&$S+d+Z6l5#}&*c68VJlD` z@9jr&I^YrB{qtwT5pOMtQMDh%C&fKQczQ~dgx+BC-Ep4`XBhKNrdJ zM0cY@h`(&igvT;=m>N)1B;+6iW_iu)+-dzxJXJi%(C;4ra}8 zK>5maWIBnr${1?=fazA>w4 zCxkgNmWfhl!@wJQc5XCyORp{G@vR8F znJi%rgNu?SW!#m`15&$xvII$DlUd0)zLqi*mc*zYlgq@Cviw6u{$ZRw&=|bx73*KifP;abP<4Pwo7e1<0KHu6AT~er&pa zv-w}NaapuM73w*E{J~<0%;ho|ncJzCHDM-og(=>8riaoP{qX19+0WQHCjN|L7|srv zS|wUkc=tH|V{dCzHOr@VA%2M_ZT0d)F{G@4q^h%LOJbTB)}Tl~R0A+dS)o5R7#{H4 zY_8myAwL_(cqYe;H>w}bl@vC7Tsp+JR9nBN1KMBBtTH+~?96f=wnV&+iN)~eEw0_d zvx_SPapcF;W%XPRbp{sAsRn8D7;TH_AKBjDNnZiLk^sKZfNv(|s_TsTn3Jv|9s^``VT*W{~C{>3q=(^A<#9Vy7q*&~tm z1^?{J;7#a7x-JCg^k>X7+V?`aKb^$_gRb$Q{`2|9SV4i@R#$aeU;JQ98jN|y+`->$%(K^A3hbi}6Z?9ZyZOt{ z`4ySRw(*o%@)3V)(%ahJ89K3Yc};p}<&L)Z23CGLF}H-A$>QP7DdT8A#V zz*i)GuzT%d`{7WcvAw&sO%pQfBjQRAlHbPsohsbPu|>oc)nd96?=dBJN`-IU8j=ci zCKXN)nni^Vyy+xDoyNyWh225ji_{OsT@?jOiJ%dtyIxYk#&=|}xH8FLj7NEY{zrj4 z@H!0W&V(7A33&V%|02aj;ddV!FwbRM1YUPul_Z3O%n+apx$U6@pn+Ckb)rQJi9BA) z)QgH3ndJR7PH&!y_>2C>AxHiz7XSNcW%6&lQj*}ssnF|yPD#*f{2l*8@u))KnRkEu$3rUL8t zKa@~6wLWl3WocmjQMy%1D%)VTQiXn>uVqJS*Ob8#;r8VyfGpl%^x5KNFR=!#@sa=*lP4LTK0MNPY^{^umqL z-`Tk#<6)$z@vXH3RzfPC@@3d$45MO)H&*GxD zZt=ZEzcVWXY%Sc>H~p4`=+~ccr@{EW3qmlOyU(}OM|$jeX()2H>&h{`x-uciHr28{ z{!^=MuU^?klFgs3wx|+jph^$n7En>CsHhCsA7@KmRAiG`Tu(7k5ils-6%~C#MNZ*N zoo4>={+v4k`tgT$Hj?Q^MCVonXj%2Tn|m?85P;qPX;S+6e}BIz@dW+^ZFHZ5h5v0| z3K|-?@jt--b8M4RsQ>vr@t*^g{zSUfg|UxsW2JSYEAVodNC%6U7puU2!ozpS;wK<4 zzo@f(cVr~c$Wf#;Z@i_yH0?{IPrTXX+n)!53Yy zkt!IU9xzt)fDz~*vSSbMsQ|-SX^i5%`wv60iw}&o zG6=Ky!jtEPD;XyFIOM&(Vz#96(xaSDkIypK77OrdFuxg_BJA_aub^j!30K-bMlhe= zi@^GZKhV#6z+Zv4kke}L8j|)PwW6R?a)qhtpB%}eS(ZUf z&FPw$x1y$$LX^tG?Pb3gvt<90Z?ay-Ki{9@0r#HubLB49GStgZ534Wi7C5P22nW1) z&Ee)f|8^02aA{ZMzGSv&e**I*2pM7S=vYRj1odsk=o>#H)#*Og0BKG7c?-JDZ@&C% z1*H7i8#ot7L_>U+0C!(Y*V=i(GDAW?G3Pjx>NuNJGu%p{#nkzH2`^j6?!$}GR7M5b z)Gc}L95ym;#iC879PxI6{&i;-8>GaziVS~{?F_gA z85S3crg{PX>NFr+PdO2e8%ZsB{6%kgwm$`t8_@b`;P4nP+XzNpiKafU8r!zgulg^O zfR`}FD%~NjjL8gER@^DkrLvb(Lo~IWzi#WPs5UbUi!Z*#go3Gw-tyce1=Nx@dW{CRMS5%~i)>S`77uT{__+IF3k7~O`aNTOFEpl73bDKRmrsWYPDEydx!3`ALoCEkqUPoK5{sCSQ!zA zNkYbo6RZbp*R6hw3r9-;Ai~JRjD9<1p=?3;=loOiyeU?4+vh@rjBY=|l`9y(wb6w~ zN}QNvsN!vey)hfu`m$;pULWEM00dHap1GmKUj`kk2#g?Ma-f&Fl3Gne)7;&8zgo~g$4oZlRn3GoM zJ|b?bT5Ej8CA@}{+#TXfo0Kqbbdk^(x*+4wY&$KsS$>Wkm>Y*w|V6VV`jrd)dvKPBNR|=`AX<#XmPUKD-|J?YBaG2rMdX3 zd?s&KkFoDD_MEo~Kkd)vHTC~yeyS8R^}avW=m>abhUDv#!1Ad1DcWq2k|ECNlC36U z=a|@BxIskexUGC=5>?75NFg>`CufTWXd1ggQMoJE;CItLkPT0KveqP-n3I{3#F9LM zrew$LHzj-I-klpnwN>v%Q%6y-rM7%^lB>3B3s-YY7}X>!KDf{hEiZ zozU^aRVFS*pKjr8TXW}16Cl@Ea_#oYUm;MbI1f~RnINa&iTt9ROT@C?%uH(&#esR%IPR-PP57@Y#=Rgb-ZsRb5zo?K$uauctFZ zGDq3TRg0#74rHziM$Sn427zAkf+zhfn&Hpb2%3^L8D{ZQQ?n7$y*V19Tm zy6}tQ5Ass)kuSQuRLO>o&9CGg2PUt26!haFxuO0_t>BbkDk{*tJYF#eN$u8gGWv++`P21U-AqSn^0drP87oC4U$%?_=_D(Fl$!oq>DgbaT3IY$P@Nq!W+O-ZDZ zcGTiCtP{E;Fjlp8HaovpKu_Fw(_W5{n$0DRtLRlrPIhB61WSa6nu60Rx_|Ologn$q z9l?F$cZ$Pr5BWh)ycmepwq-1^?<=Ss7RrLQ99DdhWPtOEP8YKFX-Uux zI9~du4=Gf@80+G{#ZV0GG7al4oI;#=^6+(|e*uw88E$`nUI@3cR`?~DW!w<7`Bd%V zuWh={a{u3iP;MKEx3WQQHxmAD$>?8>WyHr=E1=0)+LZ9$`yC;1_#(uh=4L8DRNToeP?3HWKPjM1?PP>g< zqBCk0JB)EjtadkZr$>$PH`%xa>mL}68ND5^(txD!FTU$Iplaw2RTqGL!q;p6b;ytY z1$>#|H`tG64IP*E?8HwTh+6AEFJeW&fx!o^3S4mP?fg49*WD#SJD>b{cx>Q5vwJMy zeb)Y~tq+h#g+shkkTjhd7YGlGmwdJ7VoT*B!tL2vpe z|IkC&?O(Y_$=QRBS90O77x6c;Ps-!G-cP=Iec$VY?p)--*~H&K(Qi>v6N2d%#8L2Z z5jRLbcU5?oS}61HYEs*gSHdTqMp4>tgYHzJ!|Kh5D6$=ST!<;u4gannTZN%}h9ueH z(5e3x^+Li(Y~oK67@&PY*D!RA|DNE-(B26@fhJrR@R7(C4eiwn$>mqCHSq4shQK>W zP47;Jd+{gun_gTxHk8eJ@oJFG_2NE>fp5$cQ;iIDv4ea)F+X^I%QK~t7l8yZGrV!9_0W@iSrU-Gv*rvq(Kx6O7oFH(yMq)BBz{55}@s^1&|c+uvYdC}lkKurym!5S5D zHl+PEZ6twmF&w&6o<7d1l#13llcbqnA-{2^75eMWmQz(qRo4%+YTF;|3l_ph-AlvL zvlrEo*-J0tG*7r*f#mKF{C8O$jrB1;rNr-sD#6TD$!Je9+7nQ9YWEInH{;qZSFGCp zo>Rx73vW>|`W}b51(*IcRX@KUkJ0%9Nz^eJ?!WGMu5dc}l3#Iej$+@U?f0Mxg*zA^Vwu>IB+i{AIQ{$7gEN(2Q; zZ?Kb2Yqz#|{ikwM^?~u`H|r1RtE}1FTeiiEPe9krcwFigTJetGTXv+j_rK7hpghr3 znhtPc=ix;o?8FXn$3`R+cPv^!nBo<<)Hc)lE8q&s-Ez_-+(?V#CH*j$6cyKIoAgL! zLpYN~wp}%LwN=g9w?w~_nqO!r+BT_}D~lU1ihsPKupS#a(N4Y0AAZH;=k@=8*3Jb! z%HsO_8(<+Iaiao_iW+Ogpw#`cuda1q8 z+FEV3-f56tKrA3u1+9wW-+F61G1gmK1*zu!{?0rbo+PWaeaq)V_Ic)+%b9bVGiT1s zEMR)=A91&I?Y_#lU1M`%LyNdo2O~ZUS-gul{lI#|xr}x?WQa@uy=^jOa`7eJsD!pE zlx_A`(K6G_F`yyJ88 zRR{1Is~e;*&xBt%U--8lb+oT{gW}u_WHwy3=wHR3i!C82()80Uhkfe!nisy~k!-rU|N)??adjWvnTxL8o&`Dd@t~S>`nXaH}4#9x;^DuTZCZ{)6+c`JpFkk$9 zu~e>-)V5xUCD;;>pHud(H&%&B%vA`tU3tu>TK%u$iti#60xv#BwR~TLD`iELtN(Css@>W7HNB z3hKsd;E!{EZC!;pU3lItasArHt#$v05m6>YSwup>CNJ@Ogb{>bEG02eP7w1Yr8l={ z^Uj|Vi+`!Y-S`8M%jtlAtzHL!_yq)BUaqOqYRo6KQfLilK$6nEArX2Hr*J_K_t)0a_Nn6BC`(hr7>tMIIa#EyZ~gq$d>08HE0RccpK&RNs;j zQl;0l|H0iq9ONI?W15p+zl8qPq;xRDf&OITo6pltX`oiUpdugWOAlK0VgR}E9Q`A0 zQqHb|g^QU}N-vn&1{Vstxs&Zv8R`H;9E?fgo9if%jTinh!ZTl_^X^q5Kvb2H*FA8F zW!r6dB%8R^tsR+8V3)-STXVEr&N7KchnHlyS zfWQg}An~Rq?}I4LEs?krzW^L5HhRbEhqyKDX`-uSt`#vVK)|TA;I0AsSm-xK+I zgetk~3{k7!L15$);#;zfWrZvmb&b#e5mghvi?G3=cb3i8Z#MUPY1!2|j?9I%2N=5Nd`o zM={n`(3*zM8SAc813VMXPt&OV-kh#jNp*?EZ}CC$i5|ti)?uzx$O58YYOd?EfPt?5 zU>w8Qst5aFPJKyU^EGa~g$DWh7$QQPef~%v|n#K4%3#fqakr`TMMdW4Z`eh0}vdW-e8 z3q*Z@sBL#CFzr7+!LU(T_mDHEQrUDb>F z6HS_%+Nm`np`N>!Z+g^j;;v6%<><7cPX1lAJvlZJyKI0B+ik0azg4k@bw9a0Yw?Y& zr(9MVv~C@_;<+nd+WQzyr#@&4)_l0T+~Y5>ew|i{^wST;Hhy<>`x~DIgBj)SkYjs` zloQ?93js45UvpPP2IW+CGZzw|PmbW+)#5jIIu;=rZ#W>5QKIba!_nk+H~6|PnHI@{ zEiV1L^;Ete^<^9X4?wIB>ADpxzka)0DC}b2iU6b7TbkNFr!Fp|^+-54nXWyQpXzii zIywnU$t+E4DHUdtXwd#D3s}z?q9hJ=!3EZ#eea7vYbxx*1=jPSVb5)RnmiBq-=EE& zdwsDRrbddXlWyB7>fK47r*~iIuEz8Ck1?x9|1?GPF)nkDvEC+D)D(1}zrT#AWY5+3 zgP6eQbg@RV0;6mGFpR1|4rXyU(3CRJumfuBF-*m>@t++S0XHsi#~HY{?r;2M=P%|P zc*3;&@c%_Z)n$FZq3Yb;Afs-AAe=3&xcEVny!eMNNy@cg2Qzul(F3>`sDg)*7$V~y zJka-|R07zbPmGU~=m<;asjAESP<5hFQw4Bw5pO&F7mK#{NBbt0lv!(v)3*CUU=FXJ z7k#Mj^QYs!+_Z~KE=7M3k{TitsB*}3?+|J#NO9N#zS%&T_zCBLc@oh-A^0vR^S~D8 z0hZcu*u?*Qb$^^%ngn~KQ>bfI>DkaW8gl@KB1+44S`%E2$k&Yh&S=!cNbQ2u+#WBpq)wpZo z3E19LkGW&r=WR zg8%7!qVYskPxev4$}v-?R?nJMJyQqKKB)Fr&Y5;@-Gy@&l3{7tZ;SFosf9=k*frH6{D1WPI?8Fmo%7oT^$y|H0~0>zrv*kGb%II+|@P+s9Ju z^@93oN-4FJ;Oq;g&YCudY8u&+M=5%MmFc0eYC55j?P}2I!L^#l#h`hyz@YgNf($g* zY!x&$7oI(}X689_RPPNcxN^?fa~2-EU<_H7mVI9-a1||~KC;*QESherf#O82B|W$k zceXS~Eph);lSLAw<45#p3s1UwZf%`ry}H`kiuB5|boQ!iTd#8QickQK`}$t}w2$>_ z^9y@KQvn}OpXP*%blY$zZES z^pPAMAG_x8p$E6u&IZ2X8@N`&osN%D1F5PJ{J^zi)v7qGQV*^3=N^8RI8?Px3|j3u zRC=eWxrv?V+9hnQSPyPi*jh=>KddSrSA#20J9*64rqz9Yp3w8L3ay+sZO*K-XU|3l zEiH>%o~b8In_EB26THa4RPP#n{>~1KvgCq49g{ zCNQNsMGNEFhK@y#XhbpN+!U#Mi-CX>*_e|&yGLlB-lFx>QyGy+y_-~bl@g}PE6?0S z2@91#;D8dop@bPqINT>pRl;dXIEyg><2@^Nk*zwEbQ>Q>>zm>?NlYm7!Af{aQkkuK z>lqUERKh7f;Ta_qDq)5A9tU95Wl^f0{`>^`osBy( zuSGtJXteZ*u zvP@UbJojwra{$fFvVmd3PLV%Iz>696;0_8m!Ip;h$Mx1loqL*%XMQcqud%E_Y1#Nx zn_e-g)N5v`Pu55*KF6n8T;NXUwid}jN$7@2>RVE+l~c$*E$GAs%7bb{ClqsK6CcXD zxqB&Vh~7ny2B(w@(TS)(z~lBhD-qcoc{)JQ;El{V4JMcyh45==G5JXzNJPq54VOZA%=+U8R5>IRuXSVg6YX zApN1cu%p#~M$h^$?5rPNEpJioQlGo1#&T;0Q6sEEFE=L4T?#aJ{X=Li`hoo@B;eam zP1S<^4fp?}ar!rPQZlZ?)<2wd#x2Xg*jlY~9X@yiFV;|wwbN##@TT=-F!L$-v4Y{7 z03e`W#G7mC4LQ8~d?(_q{j@m?F*eo0SAvz(FPwA!*%zFPrB+jRT=38_7uH|E;6;-Y z%TAPk*0+ymD|;r{@nLvz>DqqF-#XiW;P0L7Pxb9*)P8HU*$aZ!oy}I4&0`cc7>+Zb z76h%+E~=k)!MU0_hr?MHL{yhuAmBY93_czadT{-A0&<7}()75#r{lW{h*Z_bf51G! zg;OoVxJ2*?F$q0@)!gXu3b{PaNwcQh6U?H7;hhJ3S;=3k_9|raio`WHT1%#C<2QEJ z5Fhv+3@UKlRjPvpIb$^Hs_5(lC1dUc^kHzWiCp~2cZYC9gy}PMqU^BprO~z=0l6ySVVJw&Usn8=@)YZG(9@+Y!ply;6j%vrj~gG;J#w_ zC7bK_a!B!iisv^yi~ZXPM$v z;zK!vB|Cwz-L))eLvZ>ams>Xe>xH4C&SZ)|^#3gNBkVtnSM>7JV#K0X_(@fr0XHxr z+9|o|_;2*d<7Cr_t-loj?I#5tsI8ZDvg(>0UnTOXm+8O1YgK6RYh;<8`f{xPg>hLn z%4O%W$>^Qn11{M@3#R@;*7vj#$Gm|_ySRM^=YDE4@qC3AEM6&;115B-lOk3S4<2uK z{k;M@{sn`l=&%qh=%vsgza{30x%1VQZ8ki!g3y|yD^gXNx9JYITBZjvdW3OXrENqI zb+|>W7`L>yq1HdB5#~dq)s4fC1~R?=`kwt+iLOPLxGS(-^dZ{zo>LlqJ9 zj=P^)SH#<4QKfwj1J1wmiD0bdkl4RB;est4E9j(wB z@hSS~%^`&JfPiWPwNDxJD>pa3ME*u5>N_p3E=_djGTDxbtY|!3T%`N!2)Jm}@}W>U zrLi}-oDrP+L}dL22$G2<2P%s=iv@filf3!z%EXGJ_eeH$G|YJmy{07wYAL$+ga|e@ zo^k}2PGds7mQ;FL87Stl! zD6-5J4nl(Y%8>h9U4^Pkb-I}bCN*|165r1trx4&3!T=rkpGMG^ODnAL5Y;E$>j!(akYE01YmDqNzYyhbg2+J{%Kz*JMLPIFi48(a{n&o) zqh1Aw7%C4^lfn$X9nAm%XNsB@9ZBMV111|!alSi&ckHFnJ>3&U^A#g|cdjl>Z&~zl z1BDXVfAKev#4h^qFZieu+3S?NUrZ-^7sM{xk94Q>R10;~WaEw7hs36jNsp}6#HIa~ z&d(Ueu|@boxYLExuRb2Y$=?(sWeeL-vqj^>ViSVw*aFEMaY8Su??rQiDc5$mMxhXnC2}aEA`aDJ@+c8#fl!C2ys=GKlrA?l4q8gogxj40 zQuKx(sSKtw6lK?JiRvXYI!8x9SWyV>*Z(G`dd0}hV8IM7EN{_aAQ=3#J*I)r12vz; zp-wpB=NKIDNH;jH0J&Gft2=iJWp&M zvKR4Z1p^8&oF2rG7=6oU}IR7IK zq~|~tyMJPeAekgcCJDbY0*tBbTVN5``*e?{M*nbN7u7d#6(4vCm3k|cL1T&k#{Oy@ z(ER#YRRcB6pGBp0rv&y=R~gQ`Sm-Lc^iCgc4La04tZ$wW(bs$TR!DDLf+2S512m3c(P%fIV9*4PXQN3>HN-JEsrv4g)e^svBRg`Q0Nzc03KguRbx(thA z)p0CbZ=47oBkZ6BD9RLSv0a$PgfRJalaCDTy_^%F2gNhv7#hXe^lUx5&+aNSNug}M)mAc(R#FyIu)O00+BmS)lI|Y+f=+-^>X$D&i z64&etdU~QE%Dg;x9Y5j8UmPpyn}refNeI(??joG08h?soK4Pl`l9lSz<{-R}*(G{` z`q#U(K>Z9lnx5JG625_JTZJq?|4lS`H*Y*sEu0WF;g1h1)O5Xs{_R~T(rdH^s3aEw z8KN~GXb_e%@ssiubgv88XfeZoF4)XZ76HBfyCJttED##wA06X+pA0g~JVn)e7Q@_J zE$|um>rFIhJl1|cRUDkFVmGP?4=3I^9i{(Bx4Z@PpFi;w+0B9Sb8D(-Zdg3mZ{Zqc z8IIuQx+r(ghP@djG20IrJw=6c!s(ui3%1ZDu{IM=ygo$X(B-BkzspOIM&y=V!`8wU zKxhtAy#It$I{t{JLWjg3w;2&a>JDHtRwye;chqpfdJ?;`81E0AQujD z{3(`q-_l54Ns}89=7fkq(|e+2+a8N#lPI_WWOLJegAKzXU(L^dCm|@&=|T@_lmG2t zuwEjp7YpnBiSLn!RHT(LcdjY|{#7w}c4ZfyjlT4#9}O1XpUNqN@*mtoZSr5=uHS6< zi`)ggi={S>@r-Rok!dWc7lhZgt!nHvx2Gw{(vSEmR5Iq$&W1VLG@KUz9 z(3H#4vgIVSh%ekvvVKeqpS)27e6aQ-t zbUHmlT9ZjZ_~Zi>D#+*dwDnf`XA_O@SwWeS-29bPBe-V(zgx)M8HF;irpSJ-e187n z!SKiIZsJ&<`NI?|9$$X4in2pEJ+{O>zo$eqL~MAkrTfY8bU5GN zXZunpER$S+pLwqfBALt7Vnx`Z4fn3s1S`L)hA;Dck&%q-g2sUSysv-N2#Dqef82}x zQQd40@hO~MAx>wm%AfpOiKYhr z0`npTsHlbU~ub;5> zo%k`r{5SYPNqs{7r$_5YU;cOaVF><&`nQL{LPp=(|F(XHflsLa=4kzx`2QXJSQwvB ze`U1(Dc$Oa;|GRwh%_}2&viEYV{Xt>moZq}j^AYd>9)xvjRDYWTtRs7^brkJ3r?Z^ zx;+^6%B_}MCtB7vRDFJ{Tu`s)_Tmn}i{AdMavIC$a<0V@Q{S&uF+}alQ>%B-*t~ap z)VJ0>whc{s9D}YGW-oY|Bs!T0hlhFS8u|k-JL&8Z_i3u~jFT{?xfc z8ckar41V(m+nZKf06y0N1?O&JIpZDX^-@jnby4k?L!&NJG>ty1A_iQudTY=Lp&ut4 ztZhE1@t6277&xEi5fDq7UnYx)7NAtv+PAv>J8u04;NbIygC70MHkOSdTA|Z_WlQyc z(zvkb%|Lc0rcP47S^wLk0#+kx`JRhEshm(jfEMLP`1uT553XW59b-$D>`&d)L0Bc# zUFnUO#Qq;cV}f-=#_0E1=hSqz9q(q}mp2{@0==q^{2XYk-BxoUi_-x#+ z7>Odu3N~bD$QJZO5}z(ECn?bz1Q(e|MYVX{Ti%LO3Lnjwt^4J(%)#9N}t!ebw8 z+e9wTwR)5vLw;;aIJ3IZlX&%!-LZjvIGXzZ4T8HOt1A<_(bWAPmCsS{N>9D6m17BC z+dpDt%7}H7=w*w$k8`@){)Xsxv2wa<@%BAIbiCp|SytX(Liw0`jukAyh+yTm7cKKL zuKjj)nnq;E?oxjb^+rBYLPrro_9m51>Mm=>DnJ)q=f#gmhymx>1*;QCBv8?!PoukUfSLDJ~?FTk+3- zb-;~mpZs~dP0mMIURq09PB(a%=h<%Q(0|8)LhdOnHh=d0W!&=`oxkSm2*Q$K z_QiH`c}xovr8ywMBi(Vc4U~r`2(K{e>eOK z{za$vfPdFdfd7WQ1plVD{x|SfSN6dFk^c_=e*}Ny zs2=dwV^I7j_!s;IJ>kC^Ig+4vm){`D?H~4?#OFiI!5Ub-;;qQk3-s1n$df@+w zy8Q5;T4MbF=Klu%K0V?8Csx9z!rzVm1^=Q`d%(ZzC%}Kh=Zyc`|2Obg+ZjqZ{crl` z$bScaH~jYz|DVzW{uAfs$N#g;0O9}t5&VsIdQuno>xp6iPw+4J3wpwT^_=|h4>9;_ z-}v9)|B;bB`JeFjPldl5{yCc>K4xMM{6BF~e)vx%ln?#i_SxX)LWC|+8PW*e7Rkm> zvSooB?x{BBwp++{{JkTvo-;*%*AGTb9}FC32082QkAnoku0YVfG&20>9slRmcD0}A z>!D>r8I@^b2r*(Vr40j__|d-As=TdYU%AQF@OK;y=evKjYfC*WHC^>oxkz)a`ZLWw zp1ytzo~U(W`eFgu*)J2YW9@Xt2~2^;B+1FdcFd^ow>12GQTzEdGSqFArmVG?bYd@NPT!rBK1T2cw}NOAAdFI)Xb$z4{hqW ztbcmq(nH&30b%8biN>k?w%uv1JbZ;*T5I0uXZ3Hl`t8Wi>^@5`xBBu(DC!KMPdyLbj$ngcAW4dzq zO((TM+j})l=}_h}FP4GW3P6P$uD5azW4!Z$V}r{)H9Gd2H-k zlkJl{F;lZ{_IUOu3?i`S^}F|ojn52R!+WCXRr1)^6EhRn%^>OWleo%ZBj=jK2;4}} zVd=-Kyj^)Pl;*jDoYQ%)*p{l=vYwtMn$+eA>#H|z<2QCZ6>+)kri7gtaeTU(k`_u< zXAY^7w9RTu)+OUMP#Cw(7Lvk<3;JEIdieFKhzB@=US;FkuU3c$87%CO$()sD+DMXg zuG{tsAwVFk{d$fCcN?`#E}^stMV)V+J0B$}?JU(mtwn2>`oNK-vIVNYRyFcUjkkq0 zDh#Pq$B`Ge{Xx=_Jr2=vecI-j=-^<)e~6xRr}8Ji5`?}BL|+yx^(WI<)}Z!N3rfBI zOf>Chx^O`WDE$~bH!O(blX`=v;9Tx+K)pf#$3*{7)jh~FoG>Wlglnk|+H*)q=Gx_Y zgod?^*v~`9$?uh<3ok5y9bOopP0kQD0^&Pgn4QxI*NtDe}pWFP+b^;($m zaeXqWWPXhrsj98NH{w6y6GK`hjdqMbFVMrf7iOph+MG}@o%@S6&pD)qOD<@dui2xY zNLAgaI_h@?(@%&6;FL!etaWo(f^2^qdOx~*E+n>2zB`;SIV7&Ns( z<%GgS)5&^;m!Yw^0Lm%Mu)JX*oZIv|1$8*K)zI`*qN&MhnoyFe+Gnje@9fTtm&8&E zmN~sVRaIc$*xWFQ%J8d?4VR^qfkhIS%VMac@6k-ocWe?KyE}HDKQ}ii$rgR#cxps;Y3KODRMp>B3wHy3sK}N)h7hT6tkb(2AW2q&z}fiWdy$LxGwOG z?+V{)8>4`4n89awb}wAz8MGEPJ87-U7+dFJxsd-C(Rcsp{>xoFeF0i#4kKH!dQ=Lz zpdALr43rKyHi*K0A26D8nFWV@qzvEn0xM7eGY5z1P2=;XrUWD%2TJb zVL?T#eowaxS>0cT^XEraNNuNxW^nJxMb5c)e~UCRP;Kw27V5~{&b_WHMr+XXz zL*N@09*zPTOOm_QSND?_BGu7r>{A9kcVG_Sw!zB$>B>rZ)BuRq#(p(UJFtfp0io7Hou`u_!^$? zzQvQj0xXR}uFWJkxtwd0l0jx{Vf&44PXRxXZscH&{^p1OHV=Py`fE2#doMO#tl+rZ zj@80%!;XeI!#JDk(rwU@83%?Rb)}xXvLy*gZi8ee;J`~(!1v0aFTn*MsYbNLAzVe8zg}SA}aJGnN0Us zrp}VAd1yczz8RXp^c+2wSka%oJMG`bBBk1)FevBWbgB9L=$rLVXZ6xng^uv0BI>9v zJ5X(oqW1EZ%J;YzCA$!fueQ=JDIz(9Aiab7wJB(+k4ieTY*cd%Wz!?eT-?`MNC!sl z?7-Vt9=U!6JCnDNY%S_C=sj1{QlSw|;QUrH^YmZ#UuSS78`stM8}WYIbm&nN!ZAJ* zjH;nbFn;*v@tOW?OJbm|TZQ6P{}N4Y)G$6Bl#EGncwEzF-ndCC*<4q4kj%o_8u%B; zHk`qoZfz@Rts3EA+kw!RjJeVIVJ?d#BM3PX73GqBini(l+KixJQ;_*yNbP79>}41j51PffSPT_S=LLqaH|5igq3i zb^Ft<&|>Skdol$or4$(4(kcw@PC~g7q?SriuB`8B2C>@Rp#{*kTr=*4ejdA+6F;z$ zM+tk#w^=->wlQdRT1i!1VW0bxQXV>Q7M8ooT9nj@R^prrSHml3gtxDC=lro3<7?Xv zaFs(wp5Gq$p{X#w_;3N1Kfah!eY)AFT};q+G4OYkW`eDx_gwtlXr;MAP2r z;qZLlM3bGW#Ke2Hee==xok73~2KI_{X;~{7#MSWT(z4a|vb3y0zsBz2E-OrNWIphL z01Sug9~`2ymT`|=AJ%|OW*#WJ$11QHq>Sh~77#>C)NweaAfy(3zJN(XpO|9aC&1uO z)%967c0>X5gHnpeY(CLwQK87y3;{P~-IzWs4o(yh6H0xCqcnq0OBEoA?&n%6LMpud z*SI>-I8fiA`4lzMaB&6m`#+$@G{PHFyp&fSU!G_>OF)IKGc75WCMf4>B8HMW&i7rN zXwtzm4Zkj1F8I*jiN;~Z8zFYm$D5cI%aB3t7fSB`K=Uif#Qv`M%@2UBFG z8>k9ffZ1>R?wYRF)}*nNtYI3J2ASWmfSFa}tz^`#9S9L!O-jE?DSWvmlVqZ?8cM|K zKVJj;8j{Hp2rX_~xGy~r5|ZuTV>~K%y;-bof6kX&Y$f~Fq{o)GKO#P#;a+DgxV=^C z$B(~bOLZrd%;d49z*?GvALtT|+Hor#SO0%)H}j}U*?PmRipa+ybUam6W-XkMoA^nM z)?~&2^e*;cn3@hyrmhB9O{aOHbYh{c89DQyjT2~3(TNgG?ndkFZYo5zi@oS@=s5t# zj6XYAeNcsbh6CFASv7k42%XufBpwvqDG!|$+|JXKjFrcIHxgq|H=AkCxG3dqKbRT> zm+*h6o4-k#ib_psw%tn2EdeUEeSNCxU??7|KO)z2Sdd;1wnXmL4CjQW+J<>7gtjf{ zS=Wz+;P$^@n}O~fx^eG)H;OQDsU%wmY z&Ni)X|6u)!7T$K z(;%I#K2*4SR12(!Ss@*&kV-^4(}5|rY$74P-BaG1AW@|Q-%{8Z8db8(B_HlRfe7;DZ+wA z?W^f=UH?#36tA_}?Ylt+C5QRUfdTceuOr$MX_MMx9LUY}x`hyTwQ_QrNBkzf7E&(0 zT#_p&a}+JslbJpxA@~UI{TqAb-3OFveMA-24_M!z2dEo!OZXcR^ql>Z$Nb2J~|~(O=^WT1dUm@Ovg+u*&D>aNlE4IOlNe0J>3|#1xwX zT#TH_Ndl1DS4j`&zZ;|*cW$=OqAwF=CvP9vseUfPGPW-DMkyaa5IEQVlgV0NV-I4K zl`Y@HVLqS&eItFgr5hvFA?lp}9A-mLk*&yQyT3=aI z>VCgqS1fktwj4j$W=9r1cD+ozATJd-Fg7p|H#Puc=3p>>2Gk#exGqG_y90;z15q(4 zDki(Pj|VEz)qx>M@OTuJg^&P5=vxJkY9Gu~VYS%8hEvNGzeCcWy0GIBFC#@yY%>F59@<;IYdd{N`Ff1(X3I_dog=SV@K|5SD!WxF;U zwkz1pXn-X3KQ~KSp>O;C!bcvXKPR7}t8I$!0vDI>!>L#WZss4U@h3<78qrWfVTzr) zsj9!+NvvceN0+EiOR-Ia`!)X%Z5UfkMlgvs4f=-Bo(oA$7gEWpZsi6*V(r#|eJ zSlX8d@JIYA7(MH+5y})(&Rxkcs{ZRcz)z8W(8CNN&&LLn&Av@iQicRMW=EeiR^r6c?X%86%}-BV9I+iyVYKAWKo(U@;^rl<%x%0jwP-& zLsO_r=>(NbuZ~UauUd^8D6AUIeqD8PxU2WRFxA~bO8cE=(@t;E{GpyU*TtXxnH^IU znh$PmaW6D@tZkA2is1a3Hi+AHB&mLUAI(yN8v=j2%{&PK^p@V-S|q*wnefsIwcB2! z+QdVz79_4++jcn5Xt_h>Kidkj!GGT;BKR*9{(CXjQ$L;}TB~QwceR}WgyRr(B-!<0 z<|W)QD#+Ya3{f=dStsY%!b|tfPWz9fcebt zBej{RPY$!8sLA%Y&o=jwNH$XdBgy8j5lHlDQd^8EK!{&X$8B5j3Eq>u_DO|Qa-jy^ zUGN`O+K!I8zNnAl*xFR9G+u1y>uSB$znilX99w8MS<*}4y?OP|v}BgGkd%1ny@AL@ zt|)f`&4&N$-=)Mo>O$^=8Y<|ta=J?(`@guK?hmL_9`Yz{OvYJ`op*5MQWizT1 zD`u7LTb*vJNxxd1-ddA>c3gV3T_tV?RUJ11nC}sfUUN;^zVb~5)qYRRnAG+@i6wud zr#x}K&xt(2pS<)!XYkU{dLIoU*$56V_v!`zO5=8M+@b}=FDZn!={3Do)>e2=Xm#c) z4IN`DpC7|!n`5=vX)(p*Ea(PFQ%Bu5wE91a^*=9Iqr81p*{AHXA&e^nDCd5Ovs?@` z%3_9Eh+q3jL$|#cCw$l|=7w1v=&N`$>bvdt$eY_eOsmN@6d%N5%Q7)+u#(QidWkP( zcDTn!(>JYDCK~@LKtnQ2U*Dkq7T|eFT1Pd^s>GDcGIda@Ssn=vo_IddcneugV>h44 zPF&v#EvO~m%3qH{1y@bI4MkbsUy6$DxbeBNHy^MI6OGfTm#6-TrqLP{jBqlgrRO~1-omOd_NWIZp8-_-JH-)T z$E;LUcC1O;TcE{mBOyiqrmy#)DtsgdUrP_|0>UO`TBJ2#IY@ugO*~59*Sc$w5H$3u zuS8TYTo-dI8N527rBg0Y0s)gzCEZUsud$q=m&h&hmcfJ3AUe>?`Ojp7!OFsmmn#{T z+&`EHxVr@cUodnGM8)o9T0Dq2Z`|8plZI0-cL!19s}&T|-R&Y`Hoj*M_}c_4k z+bgt7wq8Bp+dwvQm4@)$3>Mr(!I$tTIwCrMF%+HpF2vxF1Pd%iq~s1kN67ve%_sIR z{g6?r7!B8b4%vf3wtwpbX~8b*r;_pjDL)UTg_wu?5IG-fdjSyCAEzEkF0?h#q+;#i z=voSmqmb=u$}N!ET0jURAj)p$kmP@0v$k)^F8hdu#ngud^X_hs6w1p-<(+&qG{Q%v zj^oLM)?Irb>7vjQOd9rkd+w-vI6_NfqT|DCrOF2W=pNNw_?JjNcY3%w_<( zTkzXa+pqr|$!73fNj54AFM4+;3%*?i-=n_$JKpGCqF9T%?z~m7%Ab*XV->idL{>nt ztbpPkRshPna9zU#kg*^fU+b7y#%~J`9f$w+W`t6w!fZUNvJ?>oHuMS%7# z-IA;se@>z15W-Xe&^K?%Zb5v++Bv z5b){vW*DjH7Aq|O>n9XEn1bXV+M6=k)16ukL&Yb4j;T*oDPyU>;kuMAmanD%tKvqV z_XkAgXIRGEe<#9a;(Mzgz`FY+;pf9Uu3RfV+4xuAi4e#fWEbK_ZgG!-(YDu7zd~1j z{A}{`x7ota4B=;nNU*?0eIL%AXtL$^%(CS|B@>r@8ET~IaPs(-MSF%=Gw8|ewc(t+ z{`qias(w>$IjVkL>f$~H^{ayn-3gaTD%Z^YH}wU>iSDeXwaTJBhxCih-`qvRFJ9=0 zRw$woox~()1?GorgDD!xYzkvT8k$bBL(|unHRzXdrNj+O7sLpUEhR>w=`0nb_Y_3y zP#=X+RHk-BAF}GRf}-jMGPOJWSA;uMZuoZy$y24fyCKJG0Lqk^^#irVyv8D)ILicU zgyxAyQ1g8>e;OTWKhfk5=?e>wQen(AX-p09+0}G##8X^}dAUD?0Q-lRe}`#`UdFm9 zdidFp#yAzi!5=eYxvq9KcBA>nJ zju(|3tXg&yPs|--*%kN>a+AVEMcdbNW=Xc_x7I+egiRQC>sbQ(V^=GqF|#KP1)Eu= zQQ`}PQXmNZ+qX2CxRy7qzVvGn*Qyvh{1d5L$q;1wGzhTTO!cCHdVobx4?K7lq5sA1 zmMe1nL3(N1Oqo4AK~ty%e3MA{bkX_$`MOfjy3;r=p~Y=x5v6Tlx0z;s`LFcjH@uVc z|2pmF$i&~}sC1;M>F?^k=3e4RIF)rs=4XconW|fF8WO81cye6&!(dIvSGhL(OEm@0 z@z_6D^Vk04hJRR-9^62d6S6GRrZ+Nw-IU1w)M~4mL3*&LLlcF>lG{nE$u5b3p7H55 zbU*4Sp>UgP~$$}hOW)Q@Q24$ZmvmuoyhmWyZ?a8y|YIbGvf<}ZwV++v;XRv z?4Z6imCq#_bsD1PSEUxK9HfsXE(1rt-iqDlEHYTri0t6Uz7G(cozhH%0CNPft&|$v z;|6J`w>rJqt>P~BfE%p^wfiM6EpEmGAo^CXVDsVE?yp9&HIU^U6>(N}G;E=%#ERy> z@zC?yWiCGK!e&r3Jqv`fBJ|wTO{b~GH_XEs4V9zr+euDm=H<7Wi zSnw4JzC!xYr~BN!6S58uSp)7=v$zUT_r+@k&+r^P3y#4d^_rUiB<3)me!z(p}1f{Eu0ZTGxmKdmwW(4WLt)$PiHbj6s^ zA96V}8)lzDTFTyRO=&TnXhHIP$lFwhZU*862j$5-G-g_b_ z_`3;4Q2F=zax6Ht2pHCWcHn5%kmgi#Sd)jRvqJHx{1WiTP)rRAkYGaY<`M@gFiJH^ zEor}dCk$@e)8e}K?uF2Id9K@uS+Bf-tb;x(rS`7 zB$^Je2={-IP4jQD#UVi}qg-kZ28 z&R?31Z@zsXzu#QLw@vfn%Jv1jCZ+tnAaUsCAbUnd^O*yOasoQ5O(Y%s@Oz0f0joIv z&K)Xv`UZyMewRKaf6LJ3nzFsAVd!Bx#NmKxhbTVnG8r3XU*J+&otN6W%X7ERt{U*% zt-5ElU?Z0_^bOLF1+aWDd}He2isp%B2L`Q=hgb|gXxHK9Yd-AF*TjmAGavmfP_Jnl zm>rnj%reBY!#8G+ub|aoL3ZGZfTZD%WlvDTK?>^U26<#0PzsrS7*2}y;MRpP^5rO- zina50vrel^2mWFp$b5SPgHb;sTtSQVv@bmsj_0;3nWWQ|Jov_+S@P!j?IRx5`_I5+ zdl8tT+@^4UqHyy!h`3qkox|<`PGIM2qL?daHZXo)hs1%G_P5}yr*Qz7;t<4QKFy!` z-6^YNx~+RASJEX5%wZ>;ll4R9gPrwZlE>WNWSDnqAK8C9?n3NYS@=)%`@*jB<4)6= zJ!}ZRE7BZfl74iT&?2}&!rn{l#x$cH1H#_R{rgtlDVvIxg~hr5go5mbBQ4jgbcEez=zb435BUfn#iVwKIL}3B^DR#q!&CH!;4T3KhRtbmv8irXpl|;9MIF}YT z=1%_p$MJ#M-*aMD`uSl21C?X?q0k-T$f&}*;1}KK>TLgq4JIFKY2WC6^$Qf=ce*OR z@aWJTd%rHs!xN&FO4tdp|Vx`_)dk4Kff2ZVn2FV9sV)5N#jjsL`8XXnFC_`J=xBhR{ zMG?AusymPNqN$yj`-fLEKkboitDzLj%noTtz?T!NPr~(yrXSlUY`;wWcVEHjlO5d3Psb*AFZX4EsV9FH!IWuQ z#rL}-ghveF1vq08O?KlVs`McB)CN%Lb3JH*l>2=xw@s`K4roulOwoAX;&Ezmv*%A* z<@_uDY*zbV&+@thX7q5e>>`TMDR_~}BxK0f)(bz`r@@?jcj?Tedq%;EY%RSl(l-H0 z{Fo?8tAU3w-`XSeKflv?-sJV_i4VZCgdcTQ@9ua z0x&EK6*!>4UYXWLTbtUs11MtSFPL;IEY=cAwv82^_2bpO$_I^qdcMKFi&s+ISna-c z8?5$Y8?*Dv4|nc~zd+77?r6pSa&lY8Drz!r)bAFOPpsb~%L8fbuHdyg%(b$)^`ib#{cdztQQBx(X=VptF z=Cd4GiLpCe62ktwVWD?&;zqdcnwQX}siV2Mt+5>#>B}%hv&0M}F(Vv1OdMq0EHM6? ztTGw#Q+#$a7rdP?;A3%0S>JGf5iBk5Pt3iy>0ibu#i^=i$%wsY_eV9^KE-r=G#g$U zs)`E0N8Qmx4}JJ&p0s`&0159e=snT6KX2}9Um+= zsO5d}9KR0hG8jbaSj_FejK#95sf!g3(xf%fhN@*THPUpoh?9vIk2dk{&ripuO#J)g zWU4w6-NDKe10OHvM!pCf-Z=+j~Abdk^l9S`FLk ziWcD7eK4>~PK28SsmksCoP8*GGa|yoR&c({v-V-p>yZx%MfLFyTm9E>MVDK^Sj;`l zhZe@?wUYlFC~n}bz_1>f{UrND<1b@>bk!k52~fY3JNe4Jex*pzX_h_+cH8d{>x};7 zm(L-_Gtu>f?&6ZFnjpgCKmHw^fqA#i+#}Y0fLppXWM_a91w`mVQHAH2yI}8+6&W7C zjm*NX^ye29-}Ms`V4OxW<-)iU@LNE2umh9~PaDu)Y&>&6q^)HNL{%ZO2QgXRC%%vAgiMApcBDi2v~E2sv54 z&1KK%NZ+gnmKW=H4+B72>U0K!4~bxCwu8MSKR@vDlUt}-nS-L-od6Dl>`H-R zV7V^~soHP{Efzo34XLazJT0saUQQA0~g(ez$xG?0u(P39gy_K|-h2U=Iqs&lCQ`FKFpcxttOj3CCa4u9_ z>_>^YG(#LkO=j_OJ~j3p$2}mL%_o|6W4M!Zc9Mm+tvSy#f2bc0_0CMptY%IAWmjM~ zw3O<3^BX~Biz-QE#pT4&EmdJGpvFRbKcg^Jwc$!*isuDSdY^AA!}_r{X5fueVQyI? zpr}E$*JPSiaZTkD^S`L%AbpQ=;8)(l{x4NjUoOMVSy{Bb7oLT2MX zf0+ZUkX>~MB(ldcbUAZWiOaqxD5b^CID{`Ty=_hBI<=ds+S}ULKS-ynvt$TxugIrO zsYxnT+jg#)1utN&#-FVZL6GN=Q0W6#1T^e`FC4Me_2Lo#L>9)fH~W6lKR?m z1ZccpxXCmdGKirAk|2ALpI=ozkw|?XT;k?@Y#Zl;JmwFXnY&21s7Jp%wvED+_7owo z=E~_peTcY}9h@?nlpbPb_E{d;+AWl=ouVaF&X%^G0u#_seET?#=e|32qP&TxRTA?=55OjZ4~LA_yT-_aY}{-FUby&Wfj zr!V+zd)d;6Orph&FM0ZgGk)eVA~LE>^t}sj?x)gsw6(;}o{+w(Q%2PmbBY*MT_Fp! z?XR;vO8R%2n$E^w{#<0dJs5<$A3rHL-*q4UMw0xFo5OFn!B&oZT3i&5|r zAFc4?JOn5WSHtd_P}IFR>Q>57Bs2FID_1@{XC8aM;9ygIlF3xnYnQ3NTq-6>N%wi| zGf5l#rNOMHz;()+&L5#hSI_c!`!C?bKWxo$zc$5d7B9w!`x#-~-GBQx++Sr-4$g7E zF+N@%e_Tp-m1yn;sHxY5ev`i)yrSJ6mrzr_*-=&=>GpcU+C26&M$S%^$zQw{9n}H&3=cb{|q{hlNj**@Hh!?AFO&<_R&9Y zpcnb{P>q5aIzA@>}XSJGs|2aIWl%bQeeie=V< za8dZW1Eg5u|2PvlVhsaR=@mU@_IX1;+`@F4C8vv?vK&W0oT|EJp>zt1`nCgTAbXDE z59%ZLG*hc)9{LT^sIBiDsM-Zb%C01oppLnZ0jZs(`6EwzOiE)Ub9i%-i=+LJS%cEJ zxUHwsNDrP!L#e7Fg8&~EO#9YS~; z^TkZj?L2W`@$mT&(QO@9!2YAR;lmMF+9GONP(oCj+RIMXgOA~PqDlO%X@bRn5A=Be zzQdi2xnTZn*Y=Blb-34$D}Vq=&fnTb{VN9hQH^p*=5lV>V$DBS!=s$f#pcd=ag!9@#{fE!Mb>}e&GGh zr;GQ^XIk{QJ>%aci0Lq~H3t2_O^satx+cK4A$Z-7zUh%w&cX!ct)M)@l^ZX89Ea1& z7x4wP)PEi?-7k3Lg|>&%R;-?jM&e6uR=49z`9$V{llF)e)bB^$j??f2Sl)NQQH=A- z*2iB9)5KeyE}N%FWdHlBUm_pu6|WkrnzHdPQm?yBiMNBjvbJsBzC3uh$N*u3=GfmO zIQTD@i9i3N^+nDX13+Y`RQjnz`l%!nKTi^zDVnUGLyD&IBmSsdbA^SC_$_K+caVGZ zMev5_Yil=ocJKVfj<*?PH{mEe6Z_9?ecf^o_<1BZW(58ta-$#i{y+P^$FCyaEx_!Y zN34IVTIk@_2}ql4fYiR&Yk~ao{}2QI_HL+qSmc}%pSb7oD9{htWWtkLb zijGxr&;al2k3{r?&0b26?^aYomszF?`aet@DVsKx0z}j`M)1|`8f1z&N!Rafs4W4y zA>%bVyiFsq`h_%Kst$18T#fIZZ|IfV|K(@Cn@m{g_1pW>>ulUrdSk-$(En}=u#ir!h z+qK-jKzTJLW+;@yZ3^ot#qPj6a^v$Y6~NPzduz(dKa*>qo6!-Z)gF^fL$2)W(cc4` zyCC0cWMJ6SG@u#O6`h*^V5`o}B&F`yEv7a4M}4EklyS3O6_Kq&(m=#tyka?LvHipv zhm3{Np_havGKPnm{Vjw;$h*Z!Tp6ucK!?H-j2H%gvY#zSr|76{G@WDtngYOcuQCV)A zLXF_-C-@%r?SFn;!YZv5JuUefV)j1M}0`7+oQrduDs`_w&RaKaV2%6`cEk^;Bo zJ9t8lE{oOQsX8z2Ug!3Q*-`fZdHDE^ZXdCq_$ns!THVdM3UNJ%MnWSlfB(MT$NHDl z_&7@A<0w#En{DpU?v?Gm=U1fHBp!NaAl|=;l@He+mfm0ysa&0Qome56xPD!-@%=h9 zRZcrq^@sFzkGB0&`luO4JZf%O^lDv@!~P@@nU>HCu;duMW#boJA0ePkNSgT|cr4cr zKYotwk?S0vYog_PhFtC)IKurxs{lRZ*@gmXP`}p{Jpwa_w}73=B;c7p8Z0+QBUQxA z% z`97BsJ+!}szxN@!yH_g}`6ypuV`nGj(XxAU8(CN(3bK z<>at^uWkPZHfaBwOS^+PvJ05QJeXgLM#UPKMIOvzPki$aF$}mHAibO5fp~rQ2!+37 z^j|53`(6{8C%dCl^L(29xQ=4z*NA6=&X;F{oyx~a`5ZdE1@%+v)lX?o{jig{S@Rf_ zkDti8&uz9|pA$U}YJZxr@fVD!@*s`M`6XxvJ#A6+Xol$q`&@pre>1tMF$ zW;+I4R#yfbr6RJ>+MaH`Wc)nt)nne+B!;i#q|}2+jrIs)eEKysM5B&WXZ=Zg z&ms8@QUB@)HTUWY;K=X4?B;)=?sB%_*N^1k&_&xd)G44I=MjE*ZWB=gxbmkMT(2t! z|GJYoY5Ue&BhpA-?02>8<$u+c@Xx$YxJMrZIClVWwJ*wxpTy$3RDGBIwbV4`rM@sipE6=OaAx>5U-loZ$REq& z`7yn06IBskFilgbF-k;SmTe$0tf3U?cy5%Jj#AlCHl^b!+#k7jgBWHO`XHtAi?b_L zR$Whq`c(_Y_q&OUK=T)SqB$Ra6N|;@UF|>mw>|XhyW6k8u}DJg|3x}%6Df*uz6X=m z9>WlRCoV8oXZ0^tb^la#80SfY;MG5MA~!W}RpJONNbG~}YpUn2r<=C_tW&dmvVNU6 z0oYL-@*`v0@hATM7bBqNXk+uc9S^AK&|2366D$bG{RhYmAeDkwq7w(ve*3lE8s{}1 z{^pJDiaV#1djO&&3dBx)E&Nrq^X}(hxBd29OZn=D|NQ)Niu%aHJb!v3yz?BU#5Gfu zonHBcvN6Yd#pC&7$H0#|GVw)c+%EZ5PoN6?cTXcjyd(T|opaYLFyT&#qvIJw1b?Bzd+&-y~d)0qi8upzrt*Ymp z>cqXr^vPM(k2O@?u$jTQewU7826Yr&K@;u2@-9ARzEEc<@f|JR)5sg6+P1}dvmr4& z2ZJdv%k#c=PRUHK_m7Mj-|vKd7!{iy)|3J-2WMTdFqkengLejWQEc$3JabIY0KV6+ zYiVKs9SL-FC_?#Rpibjw+;dkjNE)7;YgW;nRMkdub;iFjaS@L8I98}C8pO|tr$%fA z6pZnsH>IxV)BAUaDNEwmKXCx*UpS(0T;kYwz?{)1sN8(TbD8+#XDK$Veh(c--L%AN z6y;MyGinel)v%K^7t1Owc_vEeEi)Rp9EtZq{OkE&IPME8Q4&&i(RaUUGQH=b3Y&tT>9Chj=ZzJ!<7z0jOS09aBJr1i?wQlyYVqKu;i%2(r8J#ThdV6{!7CTNwYeW6g@QOrx9%-W^pxl zik7!6fR?X7@BHJd_D@2YL+R$BWeruAoPp%l5${hz>XE{=>*6y>!8(V(+}^tY(d%v^ zHGbCplZnL&%Kdlv_eux8D!NH3QzrMXAiaBIq32*1n0`|A-@>=H-SwW)P7Y$k2AYm5bZ7scP;#EtzPpEPeX{^}w@8x+;(SbAH>67W)<5-;z)M!||_26`Up* zWYWt)M2i`1Xa*a4;KuCWV+?fW)V=6fs_Gc}1xF2}!A$&;(}u)4Hl*WUB#|>MPd8>` zn}Uh?3|2#T6?e$w-a*H@pz_(v_QI%nH>liE_jY5yG3Mv3Ub18g z-p}@z+&fpOrug=+8pA_Q=qt->EZf8^>xivPBX3r28H2|E^0WTYkDDSq^CJc#v`a_P zRq6=JB^a@_{XW@d3rV1>vdz9p6JiwTb4edhAM>W}EDs!uMk|wFPrl5a5hvD@2YRbD z+<#iRIdOG2+x3#vD#2J#4@xj%IT^331^YW$tECN9Pp$*E^`)4xX0*zv6~N_!qEr@Y zQitks4jjiNT8?Wt>q2bH8~SyBmJMaERkZ(xcE}2vmv=thK)#{VTV!-@hSI63h4dLp z4o6YYxu1Ml*eUucKXeJ|Jd72786NAQn;Lt2_+xc#_y=AVn@Bkw{1u^tc6B&ns%m$5 z0smV4IhcHVsOnt4Ub%ex!$2)P`LYIQde4CE{4s8!1)HsUkV;uBzxo=q^v!PlW%MB% zzb9;Irzv`3VHZ<0Lk9h8``ARg>OvI&FEiY+ROIc8@kkkU{qlDaNz}$}_v%mW!-}Yg zVoPzplK=2+I};@|gg>tj_wb>`Z9Ey-J@#LrCEG*zZ2iOr)A271+9opKHcS^?%0p^fLEXM0GfWQ#zrG|Dy*VAXqSZmrVqmIJ6u* z;xp@G<*RN0PTrXRdKUbdNg<$3|5yV`8WYV1f= z&77#=g>j)-@9j_L29>ca+;Tq(^CPo~utH?DF}`NTVLNCc9rF(T{hZ`QS$u_hE^g1J zs&-So>>v4>1S(zB4s1q&UO6E5M^1##nVz%Uy%vLo$(5>l=47c(*&m6fcLCS*6cCz$ z1w5FfFc!K1tppVAhCg+5U;~5s4!7|{4-nN{ZPhe=KdgaL1qzj&ujKkmFip|(hUQux zu3c`~w4Jf?>AD(eeu&a#Z8>w3mX)(}JJR>q`tp9sSZasjPanFM^q2y^$NIe#Ks8B> z;k`<3#d#hkC|bke7q=1}TH#(Y~d^+-6oB3EXZwonAl$>nqs<*Gk5=xTRJ} zFS(y7;qGvZP6`*UZbsNb+v^Cd`{@Na`MspOMZc3b_0*LM8>%j>73Lcsr#o2VtY2Ls ziybUMa}hb!S`Iy9iR&A-@zYT>gZx3JS&1SA&b7hYm9zw_ha$`cFK*7WPmifVvU57X z?7`mgaWv5NG{F*UmS3l*px90RHkS8M?AsL(Fj+$j?W)T`YhRw^{}5z_?kUHmKH>nz zuO~*C-+`#590}0nd#+UE%3Dnl+W0|BQ3Pvqi^zT?@ZWtLGg8w1be!!D^EA7aF5y#RT8*GpKUI zd~^PSY20QuA=)ls=NpIQc-Cj*{^0AWE?Ck__Hq4_4w}We%7K4@ZPY7XAsumu8-AhqOz5c3;RUV_CC;{7q z1~f2$m@GYpD7;kFFa5`>t&Bw(_`RPXiqkEc{~u}R0v}a%?f(Qb5S4U-5)A@v)btvB z28&A6v5A5nG-|3=so0957GEuDB3KkBOahF@(V(bUye-=Lw)Kr5J`x`UuvI{-C_Yi! zGfq`}@ephN-{0D2lF7r~-uutzlR4+?_g;Igwbx#I?X`7|lvg|CX;TE(`CM=ynBhg+ zn9*B_nr~T==1yW8Elg9?qBJN4XwWx@NiMWK1=Q3CeH^GNg)Fs*Cg!neN1uT{v4ViC%3?eI%tFgt|8Iqo@vN;J5ZMs&CV+CG-r>R=ad8q@=Bl2m6 zOvC=*Vc}j>Lm91$>`~!_0n=EJB6yL*e_FTgocWz(&`OP{Te!)K zlb%dZLKG_b=`rRXy&v|&%JtM9Jt{goTTy!df8YPU$A4!bWjFslf_1f*bh9F4#rSGq zJiQeMmu|`uGhrl~BV9wUqB%+eeX;oAg%dtdTVe@2C(M<@;!6UawhCpkxsz9crUE8a z`K-)PC#l{IoW1?gKVrcxy;3^rl!M#{Qu$^pMs%Fd}P6*({)bF7sS4z=yS zOgM|7Y}PNb@9ZMCUT=NFZ=;~llm86tW}Jow ztfeD7Uq7x=Q(#Hm(K|W6+N@G{iOxeoFaM1V)x8!$|MB{#%w|b94ynkh+!HOd&>4P1 zUmd}ttT%d9BeX32U)B{Ib2jMp_fDYo4WDlvd>Gyf;DFMcwlGG8mEqc>{d1ZV8O|sA zkuyW#{iFTD*lnSp^xZ0K!8V#-XLPVQP@gUj?zL5`F z>qkIyO4%NHh#>vZnxfk*IRe_v_$INUG7m3{EFd&r+Y~otld9#HT3V{TP-#<<8R0kj zfvvPwNlwfr`RFqUG#>2ErkUoq{}X=6x}iJg2hKg_x32xUW^<{RT^u#PNeAOgK2Se( ze<1slMvKJ;q^5mCC?7#H)$3v{vY10Iv52_>vRKqve0D_6;Wwx{BOD`(-daacT@g$%{ajH_!&7Y?t5enF1*pG{o=-) z2>D(KPI3`{&NY+N;yyTph?d}nGr`)HPOR-B^WJc+u3Q!u@Z22IdKQw~a;}7q=G71Hj5XVl z;3#8`V!a9cY$sxCYG8ZOrl)NorC|dHag#Y0f31=hk$Mup(UD=a+>Gnog$lM1?30=? zCm^t7N5frQ6Vb0czodmv&0kd|Gxe4@f>0Cwnp&gH!!60r_v9qd@DusXe*=+pxXpnc zpG(CP!a1;s5?>X0h=Bev!i{Ti$3G=RJx$uY`a0`exb5yo?kCVRR(J5i{ZHtuoCtMK zHuH*4M19%bYwSqrB>~<2&?PrZ3UizC6HBCo1uS(`Z%yw~AEQBw{vb|lUK9J}X`YGL zt&|Tq{=|nLnOiMQH~3c-xtMUSu=gWF)Bt9q)BMJ(DY?0ID)u;ylMTkL4V)^0Jr-Ri zPJmyXP>e=@)V;N8W`OKvjm*42Jm)Ws7pT>lM;-o$xefn6cDx2!3{8gu&z7!P09cMx zm?OBack&_Kk~7qk4U(*v{C+KRY%`H>1NkYu2;fm`ozsuXj~X2sV|i%&G4v<%4t8qK zD1tr=5Wa>PA?=Zcm^dR^4rp^Cbf&F z=f3qtUzeWWowod|!iHf*tSdOM<0`rc*5Iv`$9w=(J8Q zMLs*kmD+K6PN`^e@`5vEaAF`3&3$>lRetuwoMbF`kRQTi@M4nfgQ)=6enwMHG7~cf zlZ?Vb!{6XTwxL7)!d*1}WHo~5iat%#OM@G4w_^9~*+|;IR1H&BtN&Qn%%@Jzso$D8 zFHGi|IqOeW-kph@WY)|JNk(Czak}`&A+$8u2ylZ3T_Y~~V@@WUAU9uarKNJO%Gedy z*!Zm@vL!eGhg0cF$TFAq^xijqKTK}$8=fWof;1bw@f#t9T}X$}US<8cAhJm#x@)AC z(;%U`dnkw$oNNfkbsgdls9YLN&PfZq&6(DgF9k}~m~aR9Zfwcvs!i%euB$ub z`slOWo-a8W-Vz))o^FoFrRBc+*I#7$Z~K>sy|+6fg8wq7+N-V%MX&ndPW;Tv$}&5V zu2Yto8k~QkXv@ilCejx@xSOUoCYLE6hGKc=WI2<(#EYlgPtJ*`|hEq0J2y6kh~qH z^bNlg|3~;azLkjGdoc4W`_Ib2wkT9$X2fEB#ZCQ`DiNFQ(!3$0OCDWmn>RMYz#|D^ zehO|diCa|G9ycr_W|`fYMX|XTGf4`rg!@l(n1;5C#6Qa$8z*)yOq9n>Hk!?_ON^UQJZh zJiBQ;+O!8y$KOdAP=fz(zrAKEzvPf%JK}mKvNsRE$(;Tg|J02EiDuS*N`})Wo`e%ADHr{j^Cgy=LLK#5et-zXwH_ zAEv5$EtOcQHJ@pCG_O2PURS@an!MJ05Tan6QRex#Siyf>~vZplAGw0c|f<41(Y-A>hcXdJbGqW(fHiE zd_L<{t-Ye4iCaNv6%LW6(m&qRtjwobxTP!9>#!Vd#!xxE3aU*8>2L7tJ`QBeuT6f$ zA;5xY>U~n4T^ou)$NF*QJQg+oSl@i`kM&!=BxG!|MP-gnzEIQl-k`A~Umu%%dF;rQ zHErnuV@IwYn|!0DEmJ&pqR1!NNj>KxsA<-id~{ z$R|9z8lTO(R^v8_92^!B{LI`rbmojUe~cnI5&QX%;LOZ#ukl6sFMr=wM;t`v+mF=! zAY4C24%b^^A)(pzV-uUVZbA6!P}k7lhPPcS|D^z0mo>KjnD1=s(!7Vd0*;&~7RRZd z<=sft51d>1Qp1ZHtV{o9?tg@`} z1Io*{yYNu$UX-odzuGEtmr6jUquKbYO}vv=FU_I{k|35eQCETK1!OiblNThMN4A8~3o+rSM{EP3Z$QS%Z6Za941!wbc zrBB{czK|B1p|=6Myye6Sa@gj{GxEvXyvA$C z*#Wd19gAzd!StV&T?f$$n5u~l%EfMl-8`M(L~J4jl4d8R!DMNC2!EiQ&7#9j(dNsT zLmYN$eAb;C;=;-yAugoBg)9|lD58tzYAm=3X;(KYS&SIIjx1or_8lvr;;RcOcMe3u z)sN3XXW1gs9`m6eX|I%pe6(pRsuw@`J{vn=dltEm?n!%p`UQ`M(y|%|Eu7ki!X<%Pw$*c21Vj=5Gpw z|7@ueRK zC!$Sb4FjcG8)^wptqh~!uOAEezLP5KWQHsQxy@b%-017u*!j z_<%IdyPx1v=cTeI#~Y0)b@kfo;V^c>IEg@zwN6 z7SNKe>?Wpmf_ zgGJ4(^*(srF+s&BMZojBV6L1I-*SSx_k=k`f9TKV(OagX_QV{B08|2(5}K8=NTNpd z9Qj}3dWZe%TmJt?9{Zp14E<6-dqO8@z3iLhpF<4J=~3>n507ODQANIKk+E;sxTC@I z`{_*@w=5cJs?sDpx;EMFC13QC&!%TU3I6bQlKZhI`Ca|~h;Q-be`9O<56USMAO3># zc2zMU8wNVLG#D>FTe^?KII$GDu*0;bh=(z;cRjgtlSX028BrG&ESPs zG(3*+3qQW6l&Q!kx%Woxu5mp4Hx}_&u6?f57QqM?v2E^+H@hmL*hP%JqNi49(`7K9 z6Mjy4To|_!`wCoF2G8)6evx2XpSJ{+N7Ac%H*`~i^;%z>{l%M9*Ihr9h}8_TsYBLB zy_VNqM<)%uMpKu ze>eU6f@g^CTKCm+<&@!uTOY{&OZm-m$!X3oK>B@;pdO#W56AX%^3(t6ew@RA4RX)k zc0NaPYE*Njtw2H({zL@VX5Hu<=j1K1K|;9$__1#Fx3s)gK7y77&5| zOGGF;qwln0UB7*|f1&35#6QPNf69!lABJ$ER8?B9P9-`XYq^oZuir0IVe*bU8Ii+lm~6bMy0b*35q7 zW5Sr%2T||=pYh%i`9i5=kE#96s^v3IGhG-GSN#dFJ06@g`K>jZrc_98Vkh@1)gltK z&s3cLaIyJ&mBmGa1G(_bOJ2W_(98|?UUE56nWQn^5lWS0hV|Bnf!aPiQ-EL8x)z3# zDp|s#gL`hJYXV!#w2VSpugG)vW7mPl<3l%P8Y@hKZR8jZ$T9e0B9*>o6}1k`0-&1n z?Jt>FuR|XcN=I57JgXx=vVnA+a7P9xnr#_j@pr6tEkqPn3nSlmE!|$k_17SYfuzitd~!^G6%cX$iDFq1e-2&?)BC;e*azbXt(=$ zdedi){mXwVxrz6QCEA}tnBU80Vy`kZ+^gR)eYz!Lc+NFywW+3aiP4n5ta0HU_{bN{9nfp^hBm!7&F39mfo+XjPdsh^g6QlFgq4VG4*<9j{6L&U9w!Qi>2x-_VwJYacB;yU#EtpKN{1fv(PD{JJKva!lj|wmQm4w<4d% zW4B&w$bKsyTK%T~#EVU3!A&b&8Nid7ExkVx!4V&XVd<}sZi9OH_si*xZLBjw!Wi}! zLnHC(^j#8#@y(X&NoqHN^W|S#MEfstgvVk zMhobhKO$&~KQg~w0Fp5O(=%Yvl;_Ve(-Y1Nmnexp$C;lX6THu$YdFVC>Chw!b^4pC z)iaK#l3}6o>H=Cux(QjdL#JOqM055;fpRk7m_oEFOyDTmpWg!0^dFeRZ*pDRC;hmd zVFrFeRZVKnWI~l|H1rKttzPah(nd1)11=EU045es6X%UyQ({fj$sq|b8`KbT%SEp( z4eB&{ZYu&_^#izAn9p`lg&m|W3_jZ36%2Kcp#)~DXF|=|@yk^q`a*JB2`XLKU-9n% zfU}D_?tE+5#s8f;n_TDlPpM+A#_orFQnmHUgP=SLM%4>5Jcj$r^u^q5QjjKQ-X@FT z`~Z9XDP7Q$^NV2V9<1Y@0$nGoxz=#nO1}KHq2_Y1WVl zP=f|bK>a*sTtlD2V9CRM+$rs}i<+!jT9{Fj+E1;nYyr|qI|d@KBBn_|uxp2)nucg_ z+$M1ewJE{rB&@sYjLHm+7&O{cXh$tOb2sHj7jc73)4D_~f0$Tjv-Wwgs?q&H`cths z%JcqEVjF(RRH-8M*R-zT+IZ}RtLR|EfhsWUoH|B#UFWc$e#kK{i*@tcUGOqnOmyWE z*H^j0KaT|69^`zEsu|hJ`un7QRF%0tywixo?&Gvcj8#27HRU=s4>U6=yT3UhUk@S_ zWAv472I+zW43ErYq+%c9ES5B34U%<3y3NE4x6R*I;)6j{3$sV z+l04mSh3>>3!Ht(z+UZJuLz^EB#h$~>ZP==a;Hyg-%^s1D(g=3>e)2yD7CMjgU~VK za_VQFHZK@AS@Y^2Mo9yam!AHetnEAJS3AE5JzW43hxb&(V`FEF3gHJ?F3(Y2V+3xL zdoic3y$jgMDjLPfu|X!h81h1mf>d^KRvNsz4^(+;@ac~rqw}D=E;8zcGw6?cx9|2@ zJ=)hS*#}1Tzi8hM|E7Hhy7nFaAGHtv6NmMp?|3XH*Z?Q_+mW0NngCE|Nwa=UWSuUg)C%bd6kd&ZL zHU3|4zLGwMG5H_J7hkiT{-;ZW=dUnyPM(y5oHPLb3;uSZk=9q!6_ASj^PrrRM!(O5 zDINVj(rn3s2|3A(J{FKHhdwT7f;9GkG=ueO0x@0m3y0}+hg%x#=9;+Z#GDeXiQ~eG z9sRDGWfj-?|@$I+e&^~k>xLc5$VpplAps}{r=dT`i*{*%stTW4vv0N z59g3W7h!~NebGmB*5?}NZuYbBSYyKQFQq6NRNZY|B+W?@q~ivn?Ch$=o5ttf|0CYH z0<9*=1x(Gw-2Z+II8gYhM&<%0^aL&!zXPcks-2z8_`2XR9EG^ z^;Lg#)-uN*AWcC59$VQ93c!qAw5;mU#sxD5Kq588-|>Ru29ZIN(32NjKO(|@w~)kb?h%Sw4`qqY0ug%y483 zdDR24wq7qXWGK#OS&gFmIe|8?}0Ry+_@`DMOs52L&5Ct$ZH{xpVfayu#?r$-Jedq3lGS@8Hjuo&i>Yn=I|a^?fQ*`{fRdB7*w%k7>Wkz`W2)P=1!+N9&?u3?ha< z_pYd3n|!I$RHS*{H7--DZ9(a*-~*BhWD~ehEl0clNFV)e&gaW4|R0#BUO@$ob^OVmAi+P(r+qd$D3Rt;$u+7dcN_V zTrS`?L>wVaheu`}cJreP`Ano$MQ|=@**haPu-lqn>$isPTst27aGLOMaB$78%vkB? z)q?EWP0mt>{hOuP0`#c8(83Xi2Hwzb>OqKSw*@V9IemWjR?-qiH%AKV*4oC{p`Y!L zM~*9_O^$PF2-!RxlK4$QzlI%+tnAq2sgM(%+Z(1}ig?Nfu~Atacp!<&mYmCHG>az1 zLKEhK$Z#7sL7NeXY6KW9dADN(89YUZelFdex#Dv2*WaU3Vew(9(4Pi(BTj0Ak#x6v ztm@2dLa)*3c|T-tK<3UeRW{4i?;MXEFjeD)K0uOeTV$KR3qDsKehnOyFLkd>@Od5rk*b;00nr~TKZ|e zmoS=$KqX6gml+!IhWPc{`9-IH3RhpzaD=I~PR*8f*~3nwjcq||J;TFz^dD}ZF0+9! zMkr91*=V~@_bWc))YI^5Lc01F>2Y=&P%9F_!Krt%XB z$b$DY-VMC8Q=)OS>26x*7kxuGApVWUYx6(Dx^{lZLD*#I8V~N{P%E+679cQ&zdeaW zx(dB+7h?$8j8BAO2;gTC^yiguCMF<;Xc9uIN&-IFB~puGZ?%zWpcytY^}Ja6>$7dJ zOOmAy6>PhtBurN4tGy8aa3@MFq(d#-$lgeBMFe^`&g zcH{>EtCP(5tc4gH?O$D_=?xb zGt8r3ajH=cy=YUQ6tshJi?;Uq*xnIk{d0U4>TE&Vw@5SCO;3-ZhWHQ(~96} z>@Qv$LnQQ4l5OB040)ke3c=n-x?zN`o1k7pd}Ev#5Ny>Wp?jltUd(@54bWT8FoRqa zlxW+G)fAK?iO2sDFVl!;^FW^4r&K3m-{#v||55P3OOCJe{HyIm5Ne)M1@UP$(V$?A zdZFIp|Jda^*OFMH8q4fmGK)`1=2uFIX@+@nY*Bk5RL<~R=hO&Iu8hG}^IV0g+m31c z?7}Y12@&+P=$ar$b0W0xmPq+xu?$FIaQ1vh)%0Ob5Ft<1oOgLN?6Cx*qJ)wHm4Gs0 zJI~#8u2=vxh`w5S-_nN*&#Rz702)`Kcx>&jHMvrb`wf^c%gJ2D3^44psWc! zP`cBHqWVyF^JmJrjpPNJ@Pae=v<776vH2yK1iv~MKD6FAmH-{z4>?*}q~>QHZ_vSc zN>igWDhW)UVsnl#4bT(^SAjwC*v@$xYTfU1v-;-xVpy%gi7#2iZ=PIZLxJfAIG^4a zPKc1vpM`;_0t$tNa2f_cU}wD0A#VGzi!AlMf|lkLR}N50=S!vYj!z~*HK^^qZLCq9 ztPK?mHNfA3NmX16@VsTU>vLoiS_laIQ2w=K;H}19ZFR~wjs3ZDwL_H_84Q);S6}H1 zwdatEY&XTRVWTNCxLtglMHKanaPp6fJT%v!;vnh^W-0;!t3Cc4pT5 zLU>boK#cx8KTOs5oHDT$e0BM|%9U6mT+XZK+~ZuMQdwKPC|2WRaCibxHB*XP$`@K4 zMYkNLyG#d`r!LN~+7PYX;7#J7Sava@x8h*Ph01L*fz~#7T!_xRN+n{EJ>NE~jLi}lPl-=AX~iDZ`Cj~jmm z-pfgqh#mM%Nmf4^ZN7#&l+g+V4Qxn~`O)HRo%ku|T?Iu4)zHw$oD0-BH0qN7mtt$B z6FZQ)4f|1a_o!mdg>6+GEe*S?Y**$0FZqOFU~lYmNrG$G^vt|Y6=M%|SLAkO793OA zZi`Rtw%fm?AG&}66g*lC4uUv(J+JLu7zcxj{|T%(h20hp%8XC{9^R)bXq^6aFL>8t zO$@eFv5yRA;U5FkhTfs3TmW$<@YQ5S%XG7^Q9YHJD>{m{zN=M@@4DI9F#|+L++eg= zzhze(Osb+MNi7SBF8Gw(Lr}wGioC0LTa z2#~ulVbpk(3OUbLXcr=$*k@0>`suK8Un{B;D}TVJ`C2O}>K^g!+e9o7Y5bEn8x5bs zQ0kIX;-cT*kv<&=()FBK-FJ5?rnd#5wkz&nBP(s~Cgs(i;MD5+_;<;2JTGOvuYSxj z)E2^mTGvKh=5KCxNJTb+ZY<)yZu+nMP#%o-!sks$mM6oqr8UM%Y zokEjyiwQ+-T;Qcn&d1kpQE+tD<|uCLfd+H{@_7g z>biG{VgFNQ;;Srq((98miyZ%%u07j(yi}vdKA2&^-EVzs=vT)2s@i1KEO(;)6(}#b z1)}JQmv*rAtH^FIRzTI6wZW?6W$KH-sD9$3x;pb5BD5MFlxSx<=2+L~`{uyfoHyPy zMy%_zV$$5##kvgUeZ#k+fVOtijEi0-i(a;_6El}NzV)waj%@Pl$B3z0;38}(uU@{H zKq$#Cd4l9!{F@bG(LD-D#Prbo3o}oa{EiR2*86j0^sg@exW#hM=#~2dms^j#1}r0! zC1>*55jjiY?QuIapiv5qS6iaj;vYu@Zf7o8mo`5D?j&M+ea=v6{m?v3xRjbBtniZI zQdQQC!L7rcW%Xpr?ZTvZ7baPrL~PC%D$1@LWA$Ztv_cmKp+?0uNMcJQnrc>);Cp4P z7?3|q_c{#RSs=Xel6Xzg0$J<7V6U*&GA}i-S~4Wnk6D?;!IcgNSpipoo){Fk5>kh* z6iy~$->6%h^h=($672x4}!k*5!;&cMEp-#~~Qr#bX6$Xia;vkVYz(ZdQgh zuY(%k6qn~Bt2ZvBNy3{w3GRsO&9AIdh2D&wup}*Ge`i?u?OO1XrG}|-AUqo-@&}RO zX#D5)=ChspY5}#HE%{zn+IuZScMHbM0iy1=u^W*kaFJ;mw zw^Y@lqgb+JX>c>P4O&>J3)nR%OCt8g1|b6cE3T6C$E(c9_17MkrM3R(`E0Xc7v!@0 zMZqoMkteqTeVtD$JS{KB*b%5i>{uK3L>|rD72G_K(R`$jwV@bHFaE;Bzu7Gci&*lq zD4atxyySi5aXnM+=|@^VDKY+sF6!!)FcV40B!GagS;{BS#ijQ5PG{7k&HEA%oH$l! z8`+x?^L*+4n@CQHQfB3=0Il68y`i(pBig*^{jYoZ!v`pF@_@~+DbMgBFT>d$v1&NB zd~G|ENOD!C(n}Sr@{;1ZS~^j2VQ}XU*`pm(erT#LpT&dP#aCrE@2fMeP(^;x@--?q zwnUGr7ov%;pazDG6Mv^kl{D}jr@yQJkvIZH4=mS8GT3$tQ1*+ivfPpB_Be?Q+|oId zBMlwt+b9Q-qHTj)g9K|ipP|t!q>qPY9s=ZpLm}Zh0|BK@$vNqQkxR7cO>xEKJ1tc| zie7!6-nksPEFOE}=LS*MB(%KqY>d}_L@0`x)y_hd${&sVaJ7N%RXrG+Sz?@?&vAEm zdm`hnWAv!{Z0geV1zK_WC5ID$ldI-IRGF#+P%LCQ%Gj@i z$%yqn`h%+Mot43F@y;gZsIe0~9s=-9j~w4h3z+`9@a)&6JS1(&xaN z+m9$crU&78- zt-5lQM~89jIa_*4q?CbSPfyC`dif^$DKvE)BK@8nR4RJnxloZZ2_})X@-gNdw4JIp zN>RhuOant7coAs&B}?>34WDT_5xE5a-cS{_6O5IAZs9hj&i=W;T zJ}BGi7{0@5mo=AvDsJo6HSEbQeqO^tEm9e}rM}i^Uh;_1ohYo56I^nku`jFN(ewSq zDkZr*vn?fXKi1#bPh^0K{i6#EHG-9O8sf+vLvvtgcDbOLiQNpvQSh7?>PBI(6 zm0>afSb+b@rQB-t---NunvA0ha7VKk#kRu|tA8xZ-`{5xI*JR5Ioed{;=mdNqiV?&}oL~yo%EXW=GE4l! z84++T7=65T{D2`j9Tx~q|2W}iM3Re!0mhFVw0}1sC#f|5!Kg40QQ%AjI5evfbtH~2LZNwBAD?S|T%3`3ORBD4@7)^P{tM8IP5 zg7HMO1P}FtIFE&|ccYNL#?S6|r-fPi6!*R0uVP(5V6PUFol5f~7ZQYjhbxCEzBU_w z{&nKc%9_B)jq|OvAoEr*irPDs4mmC8tN!S$>ljJ!XS~`_C;VaX5BLRujy9jlOcsxQ z@;*Hi{G!d{2uQ>Z`al7*_qW&qO6unSXw!NyYh}aL_cw)=8^|+5DAbkliPWT#~deGUJ|Gfv*8R z7kRT?P$_zgN9R43+{pPYqvqLNmN?`AW_pR3k*0Y)Jn`2j?Gc^|c+J zQ;6APUw4v&2Yi(#H=SU~HWcL~Gq!U+$tbiu*{XERzv4L?F?$XkbR`csASVl8%?mb< zq%6t&V0M|z$Z?-o8mvc_q2nj^YRI;(hB9xGRS|5V1`@uqQ%<=kbATT5U$Blji}no5 zrhdVt{#7I=b)j1E5D5j2(b<%@x|ILDO-{-IN;$_BS(;5ajfBCD>f(#8&3FGSCkv)? z=;ua?h5q_qh)zd$TPgR=zaC=23jdlW{xwDXYl?Ve1&qLe|BtI!h(}_4V!w|hrpWEH zk$a{bZ-E@S69GSvh9u=H!o1rPt!_?a!8lC32PSDhT3$fn&E?d+MDsS(ea%vrt8q&) zYyQRhi5|6F5^8F2a`k#L76xClC)N_YasZN@&nnrF=#Ed6y`Vf1yUD2P+T}20?7wLv zBz8L==A;1p#v5z%doW#EgV1KOldXvCN4QBH!V66j2b^wwWR3OrBpM1gz7ujPuFxtN z{FDdHtbdPK-Luqr?RnIpZd0IPe9?r%1`vRdMFUVr@m{oe87qR}-+!C!=ymla;xENs z>gM>3^Wnm8M6bP7yie?%dw(&e@Ao`wMBVS({BuPlkr1Mqb_82kE1qWx^OgWcb2%B0 zof$z$sxL~!{`!{jnOBI>!Dv!rBfXtpvi=z?*VZ(g2=9NF7Oa_Zus3OqckY_%!=n_+ zIC}Xg73!HQs$Pv=y&G(bb26mNi~b&ZoCjtqRYkgx7)$TgKU=cvpNlEqu;=bM-*mJ` ze$A(~4#D2D7e8`}l5`11@K57-`bkp9szo8k*&{4U{Anq;Xnp30eGT(RBDU&HHLCds z_VKhnt{A}4`ayg%f8M(t{wv_=?AA+(iO1#rhWqZp9-Ao0t%2zF>yLKb+F3*l-rAE{Ju!d_ZVop^s$DA{2H?T%84U;I zU=5q2IRaQtHc~u;#tEJde}~RS9*P_QGHHIT2bC7{5A$l$py1^t+H{og3$$Xsm4QOl z^2?8WTm(9Do#=aFC=d`2tr(qjJgXW;Q3Ly*kP6w1)G5G_pG@hSVIfu%Gg><>nw@Wr zd(N`b>-WJDIKT%#?N3o{UR&^EFMqkWdb4W--5RY;$i!fX3{ds zoNm1YPQ(=k2VCQ@BusV1Pg{c9Ms{(GvY`H>5MU&)R!fQwww9m=*ur63Gpa0e%|vgs z3vc>Uf6CONYCD$4yv_QQ100!!n!=rY%%_r7rhf?n(r(yHCn!O|iD|i<@r$;nI=^Jd z8zp&Y0p_))6ACT)3QYHhZ==vRz@_xDFmDk3NFK=-R=USQQ&k2^-&A>w#|e@Ou|>fpq;`O)TwNa9Z~9^)TT+G&9@u4;3%>3AZN(@R;H zmW}L4e2Yhb(S(q0a7%SlM?;A}y`r|Ny}oEn@(3vQh)UZ^@RGyiU*`};!qi>Srf(P# zKeDV1kQF__Z~k&n+I;1qrxAuo!PD4g43&ZKT>DpEv4iAZ29c4Zm+2zL$#8M)+7KS6 zVDFHnP5%p<&+SYFu1ms`CUVGkSv>YHPYb*gbmSr#+JA2i#kf*4C}cTAL39rPg%o7f zT$HV*CE>shh8Xysu`x(rVQd9NYzg-`D!n8GvKun45>{rANGVsAzv*HzIkd)4dj9k> zud&@t9-FV&wmSOX?de;sBwUgScEPSR!)j0|aP{S`T8E+xOfvrOaBG?ttrJ8Un+ z+Wh2e4m7|&#FbJAr%vLhTeb$z-qgEwmu+aWQ$XYh%G_FpC2L%TMY&q$`0TUOWZi^{bxK#_7Ntbz=l+|iD zniP&P1V?`f5uFVjjfncDpWll1`_KW&N|b4PCvtUHfma<}X$L0CfOvLo$=X4xdrd|W zuWC@Q_+>L?^O&E#b2|}x8AQR*foaDX=DQ{8vbY?ZvDXKNyofb+=AB^n7j)rp>q4*g zGp)>b`yV?ZFSGvuCdBeNh6kPs#v^k)S}mBzCH8^Z&MmGxgET=Ek8En zU?ikt+4ZNeUlivE$hJj2N`*~|-26&N^5rgJbAC=jTVHH%wT4P+(cv3xZd0WHO-}pd zh#+|HC?aUzl&qY-YOJMvesE5igOu_yOX;o;Fo@1_Wfm9aq-1A0FGz(c<9sM3{>5CO z`+l2K{dP)vW?20+EAb?f1^34=j9la{WeDX{=J)1)tUnMx=PuTuTQITey(5~ky&RJY{88*4%osh!pQRo8G2l`882=jW z(T|Zl-BrKyz2DpsZIV#V{)QFC8f?6OBpNOUYX7)mwQUad_$R|%G3-;7lYS>-C85%M zYsn(puuMK;Zf|Y=9eSt%{f2O}%5===L*Ty}VTNa-!Dp0%c*WKnFxn^%wPZQt{Sk!4 zcrWhANoknznMMMQzS^iVoqV;_C7;#5=TJTqCbxc&JeccJ{_E46f~}?VEv4=Lk?g7? zto|S6;+qZlMI@s?!{FxSq6D>o`=4B?8*)2l$qx_fH^8a>k*?scaxuv|RvM zfRw>)AQSWNghSX7L_9_+ZinM^qz`AR2yQ<(>wjY?$$|q(u<2+bE?=X~jT9f8KqkTclN|7{ZpsTTW9Wic6id%1rcYAhAG*Z5=O(sU zw^oVoB-V_gIUkCAyWW;fku&aN17n(&Vlo7^zS=(E9EXn~kD1icYEkpb<9bed;5qv= z=C#=6dcwS~*ZT2QMAcH53aFiACyr~u{VBDRTHdrM%yoH=i7uM|fu5SP_*nR%eVkRk zli*ur-Cpx?PPYv!W@@-Vx6bjq2oM6Gjk$fMXXsB!3Aj5I7M@Eu{j;1Rjic^oMP`>< zySilaa}mw(p)jmDPT@v8o2sqm4O?;w6pji1)(QWp-Lav+x|AQ~5|3&7AF*UPp0(+j zgS*7{8yylieV$VR3r^O*!St~FDb|t;UHK>VOcHAAR~Nm;O_p524%+>j%|nwV{lYyAZZOxGA8K5K+{fW1GX z_sr@g_Hd3pH~-a5a*jV?Il z9@{nq%;&f!-2Y`x(bj}x!(`PO4eS3QBn@6=FoL=2AyrM5r0y4;`JnVG(tq<3y(T5V z_ye&Nts5ujxBa7_HdVB&VE|Vj6;zG$FRv<0p9Mz^Z)3^a>9xBNy$+<@ z?=PXWax#O+_v}YBgVX;b_3ei;UE^~Bwdf%GFSGIg)knM^seDw~!>Mjk(Xo~~%l1lk zvRKFV{NaP`>lT~rg2zVLNFKg92i)QmM*kWL&_n&Vw%y|h>h@g8O6tA+p`i)dgDeS#sx^v z&wc3@0KMdA+aKIl)Y|R-Kdqk&9}dCa-r(*mu1K~eHsp_v98W~F=`E_C6FGu+T>@dF2?Hp)Pi;9Kg?3mp& zP7c3rBMRry~;oE`Kk6FCJw#kO=Mp#!R!54oAs(=1AS2v*rR$uV7 z{kt-)|FF~MCmt)J&^+u=p-vb<=*-v2SJMv+51Zn96T-}PioRE3dXoJN>Z$%e-@h}S z+imCFkIaM(NJX|)16MR!K|!##f*^>x~ZY$9xHB5eS4>W^x-#I z(7J|4sNveM1~?6Mcmkob5J;|c-buUkwb__p=Wo7ccVQ4Jp#R9e_tGiQPN~MXSmRr4 zP1LXkzoKiV%&FfWSZuhTjw9273Ni#EHgy)$7yd?PiPXF6FywBzPIUjUqUcn$v=3ej`imrL^+q#1)}XSf%el%2=PCdX zCg|qhcJKiyrT_#eAY;c0nV~7gH=pGG&1!A(?GZ0GZj}}9#e0Y?*pvxU6iWjaT(X>rR zt7~7Zk?@P(NU!0+SPZtK+!|4nXrv5&#oo${dp^$^#1kbn{W`RPEfeHRVF?XT#J5EA zIXG2Nn5xcCMn2&?G^sAN{LR_?f4m4sy-nrgX)7cWg0k^BK^TVL!|ng?l?(i0i5)VX zlB)?axBt`i@<~_}@a)3F8Q;@AAWPs88FDn*SFJ7X5woF7&qnfwYrw!k1q7 zVe{+W!iV1aweEC^!P3Z$nH|PP7cD{2;GBZUr^LTMu3Llb_H~){NLZHrZCQIv6{mO?Q4a z-$Wb0PcBWUzk(~VjQ;|Wi`4I8-TqoXYGj81vs6(v(Rr<7(ed9ahtKHc&G>zA8=a}9 zDb~MGnDQeJ-r~lWNJFP!{7zy_6x&B5Grv_C6p>R#P2-k00hbnz>}av0SymoD%3_x}N z3CJPETeZf*wN>~POn3{-xv>w-aU;+-{8y{J-Q*+n509@l4b_(0zGJnQ9L$O37ROkT zO{kq)0+{vYzz5mCaH~OpKAyoVPYo_ioq#BAo6HR3EpIEMF16m%e@eX>u_aZI&jK)J zGcVUoiS-Jhu89iK#DUsJ3)-KDzQcRbS>hZ*=xiGR!kV}K2qRuC-ZTYL5FkNLc+ zpz*K}B%`0exW_}XQ%b$6&!=+Ai-2TZF}O!&ANed5d1{Jhs8|P_>?JDHPSyGI}Zp@SSg|$QNhkV9oxdbNqWL+l4Jb z$s2$~n{PR&W3PSTkJ%3c^Sz;tz01{*WaP`GB^anY`6PqVP~!TNjJ$ReoWv(tLN{Qg zV(s=#UxVh2jL-)*zrN1fciRv5Rey9A?%W0cTp7GQ^5GLDd3&n=oQvR9f41$tnh(Qe zal=9828`=-j04B+Q^IKTBl>EOEC@?jtYX5{y8g0BmyBVyH$(B!=AXE)X?%h8W;yFe z&gYBmYyyTT+QfVfy6$6Tovc`EgoR-RNoew%xhLcrJG*fvF)cSiHTdKkn@rVf=N0 zi)`!}8O(GMlY2#+>msVZ$kC>lf~3wxwB>FPSOX4n5q1B}$>9uuvA=L}qk6^#{anP* zUJ>c-tULXBMXVyCCD`*dQ2q9-DAzZB6-z`;{4<9?!iYx+-iSe8NCd95U-NaV&+5<3 zV+5(3tiO2Vi>DlOFgheJ8Qz(>^0$Y0DGIUqIXD~bZzEQERh2@&*^C%pqei}>FF6RJ zpmJ5GCrFfCH8RbZr(pyY5cwJCN7%U^8DPQX*d1&i=B534i^o~-Lui^rKKCqF9HRrmn%UUNa?wp zC+R9x-WMvbQpFK*ql?&E1z5!1{35m~f4}Kx2J-JB)m0fZ^+Bx-X`wzE_6+1BsOedU_Agx2NAKm-V%4ANB94A1C&F~t<6Oj_|CSRW zd<`mH#N1vHJG+Sbo)MZqw)VH~pVTuhSnnbZ>J_oVMeNcm;sFxk6W1Krj_TnGVcz8bZm(b#OoX>2JG(E=TL8sdzzYX6&hxKc7Y7X~@ChRITWGH-> z>ZT!YNoKFh-=atjPM@BBy4HMC%&8`c%lnN^4Xi&q+N8c+V4+af;Ok1m&>J1 zy{qRMp28(k=s0_;6`<+mJ9?8gcrX7QW8f=4_C|ajz4wL8hca~GEaMQtODe1S{_SM4 z*`N%#UTKRAi|Cgu2Eb*{{zXX6>q-GRM{d;D*;4HLae{cB}lT?3MFn54krk5dx7CS$8 zD9Yr3iqY~%vBtZc)pOO?myA``(Oy2|Bt)qXI}lW+-2p9@CloEH8TCs`71vUt0y$nM z?bZh8e5xT>rQ?eecx?&(hBJgd*ALo>Elg&j*^~S9GZqkoEi|xABWyPpDqZh=!N>N;N;Q4}SwGMa zNIQ+2W@#Bi!2Cr}g^fkd-v@39Eo_D@=}&el>98~?}O&A()4LMJHn(jxT0(WaT28Pv%dD})fhIWes=ul{5i-Pw-xKAQS`k1K{(T#4yyXdf|wV9~*N znAb#O;AIBafi$kkm7Hj0I3V^Q+Td*6M`v190exerzh->3`$ZS1Gjl^HCD#E18T}JX zi%Gd*yo1_k+i4Ek*kba0I4oVNlym?$WtvQ%0kH&s?2J@+`w{fX^=ev}oRq@9^e&QU zy4s(LoRX{k8&2PY%~;L01Sc$q5Oq~)7WM7fH~tTF_+KX)^i2P!T9Hmxb`7qOvhe74`xBW8cCo{k_K>@Gw?kNOp8XGFnBjKA8}r za-~w99x^^wlmJfn6K#HNPi(ZIGqpl7o@>Rd*%H^BacuW)#(x;eEw& zmnwKl$p}jS?B@=pgT>ED*`+e*4tR1GH5#?ZEgc2J7KFo@nf7J-lX?SBJK-=HRUb&I z_8iR$*b*B3qz|I+IXw=}KUvW2Y)02xDsu9UIb3`fbsPS@lnXr3|1AS~R`o<4Fi5a2 z^ao)jw38+qZIKLd+qxgI15%v-tbX(QmVJbPedAv(?6dp)DB+~CX+SUNrj9T4Qm5c= z^31zUQ}>kEMbeJyR^W)T)TZyFpiQJPFLg;_t$#$Jg#8_A{o-SjZO0~8jf2xBH`FHI zK33KbH-q`d`d(>m)pA^lB$x5Lr2n{7b#dlJehV|m;83}&D*wTlBx0GntXf8pY7eWy z{Xp_4{Fqe3T#qW_IU4`RF3DAvH{a ziCB{h#+3)vOoLI-B^Ps^i?JOGViK{Rxq!PFzuM$c)n4i-Z=4U(#j0Jz6-0Q+<2~45 z{e(npPlYz@IWF`l!_$=eJPS#f$J5hODC0zhaj9veaY6C^yZL#%76)&*m%?DRd+~zh zykuTX#9q2nMfax_UgkzG*&ghNRwr|{m;72Ex7novubBfa8d;67sNn5M<0W58W5?Rz zE?lzUdQ1IMdRVt$(Qg=$ww2vhvIdhaDD+RVG#ma}P)WP+$&0N?T%46)!% z=}DGyvzOcy^tYgP@IO6*P=*X}wbq}Oh;6+?eL9Rms7}N_v5!IHlG8k|{itfeDgCn2 zr$}$z!T*DbN?leB$R5xprrr0HXbroIe&4EkDf3_=_IoR?KgCsNZljDWNT!8JuIYvS zpc{L}REuo~zsVN8*Cdy>y*Fsg$ToIQN3Lv34;Z<6O!AqUwxGCXoV=yCb6SAic=r0^@sV#S5|qImmcURo zrWHn;9ZpFHAZ{!Jg3J<4{O)@`ibghdJ28mSrX~$5<9CR4Zc}hQz+vz*bA#mDg4pS$ zjEyG5ieRma;)bk0+uLiw9roN{Od(yyvJQ57-M-;o5oR*&+fx=zq5PpAi+}yJ4bZpr z(9L`||M|JNNRKl*5?NBy;3RVDYJ$*K8Apt)O`@xK$xD6)K}DMeD5rmswzTR8k7Ef` z_4y_JaU5REePM~V!VE{p>{zB|sKjMu(Ep#|I2=^GPDn15+y)zr#M;98P1VaukUAF9FN_XyaPl3J9YV73;Ovi; zDBAoZW1ODkris-nLl(foIj)aN5WofV9~`c@B{19k-*qE@z=LmAx~N86eWdg zf9L`wG?3p|GbfQ>QfLEfagrrwROi*#ks3z)n1w?EN5UmeE=$Djuz-O!SJhOfuQ&cA zmI3}wUuS^wf{S@cPqU~QUT_v)nYKjie9O8W05Ue6`7;3Y==K=J-wMKx4sI3%ri*U^ zxL_|8Gr_*5jaK=J#Z*+W#X*r`cW_@Qc&gE#zCnmi4F^9_%4KQ*%Y(9@9~*N{d=+Jw z$mLLz$n-wa?jWGo0TI)xkGB_CfJ2DjxhF&Ws_A_wWki_aU*GaUXg>ps$Xz2s7NSjV zEAW5D_|@HNBjs*auR%1_T?y=!V#T~;BDTL`&uIz$U*j?;5&MCH8ZPCoIOt(beXU#H z9bVk9TOziN#j%-uG!&Sf3cb{CnEwD!CgW8dUf6JXBDQX!%3wuxbYbRI;tPYv75{>m zYMJhEd_0@LD}*dzD8bE2fCtmdMhAb^Yl1!M(Ln<6YW_Lf2II2g;9|jB<9RNLl+mq^ z7VwqU>Hrq_^IWrW=}#Ent)CkIdxBJb+0XoM;Y;rgz99XE-{-=&FoZ93V(?v~*zWLs zT0!3d-#=JfU-116{qBPAbW4{D-%}Oe1>aM%2|D3hr34PX$LZC6`v=4tO*)QwWF7 z3ldL#tHT7qyQsw?S&$ZO6BmQg9D5egLPXP2JsUJof9Cou(d0b)9^b!F18qP;zJOUC z9Sc8^L3QEvAXB!|7{4f=@6I;C8TqJ1!+o@BPQ)IXx`W2I{v2|4;6nsooPOkY1d{X9 zT%RIcIg6Du6>03O$ECg~w;rkq>skM#>tPu)2&M|D$hd62g|2;7*?hL_Ig#X3ekaI-4tSgwN)bwotx0psspp8TcfDnzSL+~`tBR{kk^{ha=J zm+s6ur;@&BSwnVz)T=6LBmUxd1Lgm0Qa>EwL1t>epBi^)F=JiD3uOI(3H_dcxNFrNryI{-MmW*!6n3 zJ2p>`+hRB9u>kwdKlLaw2Y2+^6H`s?2A;z2E8Tii4kYMPabC1(dDy4X@x_nvD*hF1 zzSRO*<@*&M*Cvo9mQVN3Os7zwx}x7 z%1ixj=E2yV;YqY^Hh;2n=Uee%87L1U2N-5*{TtcSkkU$o$ZR*bi+~hDUK>D1QoTcZ zK*oK3Cwv$vN2!l96&if%clb~0?NJNmBTQwrF!9(+JD}>YlBQ(a>l6y{*l*^Gy?z*v z)$1kNd>woQ@-OJ7`i6P1aJk!3$bSz4C&}6)^9TXt%+)HEM|H9Jif^wjwI|~}9%oUA zb(&onKMv5l(Ubnhj?Vf|xv`UQ(421*QuVVr8lBK?^Xszba=|97$lXq*eWkT=y@VHLur0nxs7qN2&|c zyYiB1yriS!$BE7TrXHt4Ud%NikRTD%yFy~Fki&R{^NWglloVh`VfsE^8n+lHU!1Y? zeXjlW_oR2DmiS_&1P5`T)sYqpZfV?N4F8D6EjOs2%~H&&1y{bDLk~ic;Pa=g8|x%& z_(gB>F!>kedz9+V)F|6oALnElY`U6ay2V>!Q;f*DEbA}D*hy>TM zgPq*a!d~owSDei3c1jFkWYMsGja%bX&7FU#DLB{x%Qv5O@v;mGC!2ZMEr5Uu42cHL00YPGgvkA8rzaN^xbNPV}TyXZw< z5Rl{X_>bQ`1_XW?HAAWHuqZEpi#by@%a zZ?M5s@C?J8cR6$EP}5OpETGPG=x2D)c~i}s3dM+uz9DR53hKt$faBrlyiLUlyHzta zGeq!j8yKJ#f|R0^sil4n#|*Uuq1^BJdS9Q<*=IMJet-WT58e6PT%YTDU+??teZ8;i zT1)nA(`Q#YP)DxhL)+noTsgjK8g8}Zmcwpe0jEkz2`8K}3rj3<~_SUwXI4GBf=#n?$A`-Q&h5=i^@WRf&D>2B#kr)uNv# z`o%@RxHzLhR$v+#0kK8ZDuA}G*Rfuyb>7au##-&!Dq!KC+HTc_rQ{r@(nN@sepDB+ zdx;7vI%wY~FViIFVik2P8Vq9eG5zMew@YotLF$<1?JG9p**s^g%A4^uv*GL&^_Kc2 z8i@ffFSWY}XpBQVk1)Obh71QgD6}|+^aM{WWLc-j1)_9hvwUQo<1YN#Mk@GrJ)aI;a^bE?mt8kji{w>OyB!0fUuJ& zr2-VmTX3SKaya`{{qB`4mJ1ZGLy3Qm>k{H!%XAd@Eq7xqmoze-x#roUqjj&DL!|Jr z%MVp2zjZ;rlkxk*POjjv3B5eM_vNSaFK^LHf6LFoz`mi{$@B2w{Xb>6^?#c9w?X_1 z>js!Yg0XI>>8g@sjivvwz7R@=Mv{n>?f41+*MXzK#%J zoZErIp6~`Tn>JltkrF1kBZ?2FO{zL%r z5a${V?WOPnWi3>h6UEohiclUFmOR?TzGk82+J{2*GNwWzSnpUw{6pc{X1yINN<6Sn z{`r4!CDAqQZV^16wdlqT#snQV%LN*o)&Qq5%`VWCSmYS;OL?$pqzYDRUV^>JAfuhJ z_R9kKgi+nOq~4e}yrHKdpaMz?g3fX$&2ijsUH>RcMP^?n==032S89@=n`2|z_Q=7G zC2xWcpvNIKjvk)Tq0v1&RYZDl#8>6t&Q`!Pb5%jP5WjgY4QiMat(~K-Mx4G@ft6HXX&Q}&*;zg z^2cuYV~xEWdG9!b>&!YQ_kdK#wt0>}bW|p%-oYnz-Tu&C4$A&kq1d)H4gutjwC#2X zxX?9taNIS>nM^eFdH3)X*U*=U4_u$`{Nv5ccyG=v?B8<*HJGpI@jCtz>)5N1Zp0q` z3uh~cYqz}UZFxb{AX{F_W1<^O_7ig!wOCUBl|f3Tiw&2 zfTo(*gQL$1P=Tn_ZP`AxdQA=JpuM^+elH4ux;FJx_F=I=FCqf}Iz-1ouHq4&SH(0J z+CM|f4tfPF$A_)vmLxLQ6@Y*A%Tuw%^S}UkQW$zkJSgP{1^V+|Wcz4|Y?3$jggZ?6GY zlVcASX}+i~^%S#8J2~Tnk(bf|cfK0yT+@q`MCuuAS0dGI6HM73FgNBEJ0|gm=OOI$ zCOK}|1bl4d@%KrCwtt7c-9Hwzf09i%Br>f+QKGu5xuh=DW&1;R@=#c+YkNUhV*ch# z!xKn)F59V;wA3ah)UqAHC1Tu?ZP87pxF#i^Y%nFmZduELAfjI~#cRKiw^JK_9@?pW zH{)cTOi}+)5*i*-*Zk*nWzX)@Rhyoz;|fUJ<6D9oD}8t5jp*ag4<6WeS4ZCZ7e>Ep zjJ`QF`ptQEv(>fr$>7G2F8{=z5PiLGaAS=fuA5BpbLZqc`s7_UYNZ`aojF_WPSjSOjo)NIDC3!bL*5w0{@nfuS2tI|aX z4rTIRxxz;*c*dft($FR3TMY9y`(95s)fCSArzvX}bRFFOuPCI(KK`rHF6sqDEP1tF zI(R=Pcr%Bvm-+c9X3L-gcn!ZhegRN&Ne$D`cXRtKvJ%6W`dOV!V$du;z&UuGGzTUgLo z>7_o!R8u7n$y(p67(y$Hyj zx@V-kY4(ygoIDmxtcETzd+W;BMQVKP&0sTMPS(m4?~aBV4gh>V`$roGH$5I1AQQ_k z@He>^Z4XCY?5h_Kx)*1xio6)f3)%l=yn9G2;9s&<26dSsf_$@d- zLTAIuf^y*6@`8XNdyu@9P5PGNLY-l99(g-N^c^~B2Le?e=h{I-@y0sBR+SD|o|Nk* zM5NP74v`oBMu=Q?C7ktiULWKKe!;~c`T_hs0dBw_?ax9fz+Y8Zo@rish#-YQ4xx96 ziUXD?-|^Off8qiUTo?y!(S{dKa*Vdoo7yl??M7~0t9R;3ik$cJ;YgwCfS#_2L*||a z!VX}=4I#l0Pw3~_8DipR9bW94^+!95=m0`%R19Wcq#c80ZP0>HSHW29xqA}M(aPXL}q2NOBhS$8-+&iupbkHk9mr#R!|R$gT0^mIIp(Ly69CrxQX zg~d9`xe13M+S!Y=Fbr}}_vY<0I(c5sh=jA*u3ygQX5^Aw#8zx}@q^@Ucy$gnfb8QL z)8Hl)um4(JtQ$IwM}9rVVexoLbS2(~q!Ee6-Z^A97~)<(CS)c2xFs|bCT|yl@zArI z#gH!fXv2 z_Jf=pZBX8!9Tw&LFzZ-pL@)%@M&CM`NwMO`mk@~ z>04&xxl+(#rTZ#f@5UqGN5qmx8i=-j<#Q^c^($~?lRI##-o8CF{Pqt4OVulSz8BH+ zvMhlk3#y#^df>s(EI@$QUKi#+*M~05r=N31$GI>|Be+d@GwB6Az!w3{xI$*7ez@Iz zFf;pYLvQhgms3|}M5&jl>+yDO^$zbwq0k&{@=a#sE!LJqZurZtqr}Gq?!}^=s3~EH zTO$K#NqWKk1H6Y?jM4eO#~2;K$Nt#Wa=N!VQ0H)dxdb4E|1{|aA8Gp6)$ajn-E%ch& zh2)OFjo$o^aD2-9as-&&EY3nf-@2S0;@vdOh^=6ab{s7tTQlFu?_tiR7&-hR$*&Q& z-pTL3KH(?|$j)*cCd(oGPGHMGiLk|j5bk4)$<^)Tqcf3FJA$yFi4tc=<&&NI1NnT| zNucY;+;YofbV5e9DYnCjZ=n-Z?G$8_!?rL}bIgIL`qu|aWxV9HL9rmZ+gLF>cMs6H zcnEmH8Dov%8o7I%)ch&WX5=zXhk{RbL8unvR6mHf#?BQgXz7OK0fIZDbsgeO{3uUZGJ=qPyU1 z=bI6wA={c6Ss|k$r7^$4VICJ`*Aa8ihC*8KHqva4lU7jgnb`Z+`2R7D@T&B$2nBtX z9r^E`?6*u+U>22{SK`0=qUsE334tIi^va6I7lsWfBua;^6QzRpu69>FCvCEKKSlqfkWwMtQ9B2uKx3R;-vUYz2j`l0iQSU zIWMkwDpIfJ7RA5*1YVNAI}Zao*!s;u{D)H3Z;6!s{I7(xJK3`5mXPpTD!FV(OdqbW zaQ#$MAUkc`u*tz#*VufaBf7i?__o~-6 zADpOuta+T$r3-iNsEhsb^V*zfhR%0r{FPNUx&7~rADbL|_+OJVm-kFQ{6A9*UpXT) z@l9<>GqLKl6jxj8w4{)D$y`#(A5{d%8+vL98zxugSfClsf+Qd@ju8~2??ru*J4hf{ zXZZs|65<6&h&)rJ|MUf7ZS$Ig5hyHx2c-FpzZmlW6x}RYOJV&Xu!6op1X2H7RJyvh zXYGSe-x#45ZghdaSuZFDaPa(eSM2udBgN1{1^!(@F=IILvwb-0L^#WT!$ERUbi)vX zWc+%uz;)Nd0weo7k^|@u3Rk*K_iE(ax4pK3Mz!f)mwq$BFZNx&1&a#HnA5D2I%jVl zR$#PVS}`Klt`p@@O~KyNHw1BQw;gb$H#`7Dy5}R zRYo*y0ktky%fzhi;Fq1<@3Y$nY&&TDA-yG&{F={#EzNouoayu8j8lovzlHllf;LCZ zlX3FM>&ybg+JDFm`uAfAdbO3W>#EDQ-7-K&J8=2xsd~>re3b=er;%7@^8cIQX3Pg7i*r%)x5p7u<`d(GZ&n| z@|+{4R+rD~GW{~OdRj^IucxM4N~ae7dur;4+?`Xk#PLgVX)?thd)aAft<6VMj~tek zrq)bB#h;z`gC)_tMjp=&^bnK8C)xf8ZQiJvklQL+0pDWZW1=S$%*IGFXpN&-MxtG+s%_E+Y|)i~?YSxUZ&V1@ zb&h7U7c-~uUgUqT=l&(zOmQqZJmQeC3rc~1^UFy8TY4LcW!eKD%2;9nl|Y;3k8>aY z{kn)cf%)+t`B;$66axL<>olTF@xxKA!`vF@e+I>-S+So|%G_s;=qqqe8@ zIA1DKh9$Ga2l3F?zDEw~0R3kB103({Te5@U9is{@Cs2xI9g?w`KBJ!yLO7Rj~`6_zAv!(v+w@X-s zpd`CM`)6e!-y=7)E%j#Aq~20Zn9teuUb>|$_Fywn;&<6pV$MEZ^~Slwz|ZsZ{>V5Xw@J-A8Ve5pYnr@NV;(UB!`m8h)gA~%+$(;E1tQ)-L}0sR^r|uvRUY)lNH(Z$d|_FW7s({4gtUvfGG?pr9Q?YV%TD7 z^^L#A#RL;&ZrKDO{|3;&qkJSLk%kL?*2@J0@SDLi;B|ifbwUmPX>P|~v)T;fJ<;QGj?#-ec|rVSiHo1Ce*g7bK&I)xp*HvTPWgdP5y~kj*z(uI zx8OokE0ZPoi&hr*i4 zRbB4pYz=jSP)w|2vhLE?E$5w1W+$X%4?e2kZu_s4T9y6^d35u4Nt*E$cE=X~Mle=? zF1GkgWg-O)o`t^Ubrs_wJx#7ujRj5n1=|?}`a!Nu>m$F?E4O;J^$~gbq5kMNR#m6Q zq2HM5mq;2`wNmdG({CQ#ihELk^`}r0hVi`3FG$FcG)?;m>c3E-2&5sC%1U7oo{HoD zoz0W!=DjBP60ubo%{Tv5JI_O9a+)&tnV?p`ImhQL)a5S~7sM8h5>luJdltYXF|kmE z$YE**`!vCl-6JKC^m=<~{bzrKNUy90eT~9*HTZkTNzS*B>>4j&^k*!TZVMQ-?NRY9 zQo1(#Rp|-g>T8B8$AMXvP4%o>WEi%WC2%8g?*NgS{!2-S!z_sWVPU6$-!Krn)YwyF z+9d)FG>LWdMk#%pV^i>fak?Td&S30z$~28aW+u|7*C+VY=M(>~CrvM`TOCmtghLqr zKdYVw0R#VJq!hjK{dA<5Fyr54#lkTN$T-lIyd)VZDf~J7&JT)r^2BhropeGf9(sGE zIB}T*|IDDIbr38E^l`va6`I1_ z92CQ*(yNnQvC09p-DSlB6ReU4x{|L)`!5KC{s*;UaVYzDX42^Vt&1X!+xX_3pjMH{ zO8(W=+O#lIQdsqmrljAoQC$iZ-cmQ}?88#8n~50a6+07m3ey6oIOe_c%D^zIDQunT zsNmb|jrLiMab6W{9V$n8hM{uw+_fpk2CfB&%CaDOwysr{YZ&pAxb@{TC#jG;1B4HS z(AC5#1UDG}Kcp=q!N`G2_hR?9?3;8^O#gzOf-vVSJX7bsH#1!pe#!_fE-|vQ$9yci zf^YrbF*!hWmt`YpJDke=w}kn0@x{*xM|d5i0mN1^rKG@*&hKM3xYq zI|LF7DFohf6}-?j)qGR`8kSg7p&;%4gEjPpC6Qt!_PrGgbt25M$(5{Zjg&N!ebD2H2|_g*%ypwB;v&b548pxoqMJbq84<&W?J{W+gk=|z%W zG|91E`s$kv2wW-0-{(JJbzU3|CmJ3XD(NIdp?H6k%R*V$5orWt#`$knQmemIY>Fey zAyKrNe2%3UOmwXFQlQ)qUAfvTBh?wU_M)7BvA_yjPlGjwXaDZU4cxjHB3~UQAhrml zR9|J4Pj;2>6Rn&vM1g;|6^jt zOT6v4KmM^9Z~b)yNHYtogbLd2qCVk9NCH@- z+2CL1Fy&ny`3?vQ{Dt;i7*75tnR~F_pRb4%Gc|sGP|PXk-@9U!Q64d285cldQYiBO z$d%l)AX1@`fA65=@&I1{Qdjc2TO%c9a2)?`V_dB6Ikp!6x#GtUfb^F^$Hyh+1Ufi4hhOS zbJpmJ9Uj#fW_Y#5kp1vAA@MYj^w8{@A%_s}5hy_E;SFqZ&+5dOEY$PE=6 zRLfIrZ9y%^_OHc%M_q?$8;lCya_-OXqI_fYddiB0;ZzvmLWi=CM0rux%D*Eh>AFJ6 z&$yCHqsWzJ)BN82pztPxpTc{)!lz{-_%(%jW>8YS5vT!w0MBPYrU7%*Egb)dwyF*i|%RFEUpFMgzX5G<~%@w zO!1GR$01e-9p@619@y%Et42e zK% zm2{5!I^N>%0FcskjG}Ct$fl3lfG_#beUb?AXRPCATyg&Nmr<6nYt$(5wMtc=8e!+Z zSL=y<9|DQPT3agiA>V@gN+VvnRC_dyA%x7{kXa>~mXi%vO}x?OirUuNOjh@Hmd!sm z&$gd)(a=1l{5`K}{1Qe<|H~Er{JIF+DF?&9_2)*u`O)N1k6gh(@8%0mwA^{=m@x8meg)p%|iH%-Lq!sX~eFmXVS!vkczO zroX#D3cTpKfg`B&f4bhy{;;!Rypqpg%#m+C-Zw+@hqJ?3{atG)of)|~&a6AbQU;7v zmb<}aML4an|L`!1$=RBuTwH#Jnk}<@3oa_jmcCA(D_0p%lr=>To5rv6i``#S9hj~r4;pU`gxG;eZ=J-S8e6J>E4&B`Je-oFB+12Qc#^2 zLN#ibdLn`(ADz9{f2qm#XPOWYvR_;~yd6N_Ol`#rZvOTvMJn1}TW(~%+ZkAbI%a6s zI~QN1vjAFPf6z~Ns5l$=h50dxg=^0%0~TriGm0p4j4Vz<6VOCXels2h~a`#>o>CY0sNVJBv=@Z6aM?Z&Q%V zcw3VmQpY}-wbdJH*k`#eJ*5PcGKx|NIE-Rw3Di*kpOXcmSO)ejho+sZ0s+c!DB}j- zQr0`Dmx2>{)y1WBa;tv1m*(fycrG{T5!bX6m?XjthPt*#m-CKrLl8xAE5jNi?Z?`4 zAS`YE8Qr@2&q5!7&xZNO5sg`T3;hi6%l6+_YiGl>SHvNVpXIxH%*|7FFa=n)xg!5hfZ0zd&`0dP08Q%R4Fh1c;e+(Fj zQwv6$!3Y3^?5Ss1vm)d=hH^c~76X>*9QzcIGOZrq5LcuWBp@C&KpptE3^aqmG4w9? zpQ-^gwzKPBrw;5hP80;$%ZQT0++9?re`Kv1Rdvm!sH%T@EQdz_6i4bqSYS46rzHBl z@#7MI(`Qv$LI3GS&zb5pQn9q6)#|a#S&|_A_5Yu#bbon7L2EeYzfL9w6^!$ycX?DV z|76>5t?d6aM#>he>>A3dwV5jEKj4auc{EbYbpA3c7BYhy9!wJsVLxc?&)0j#oKfG% z_KCkJs6TG?|H0LN^d*t{VO#QtDCXbfIy5CZdtAy}%|HEEV7>>+c)u*Lzu+kV5~P($ zgr|wG9d{5&kX9!L&ojO4cRXxRS6veU$AVOM2ToWktqX8r$aUBGXPM&rQ;~0M*xh2^ zgbwKTN3H4uW=D#N_xv-1V)cfQwG=b@5Iyv7a^UPeSpR?YU;01(12WBZZoc4G-;cm$ zO!E+=p_d`k_;;?g4osXADQFziDH>AJam+$j>@-tWUfBq)|ficUz6!X8~I&x@d3_`IElre~q(r z;Of$-6L0*nRMdqXTWPQ#e_Y}S-xU4E;A*gMY}!;I@GE$TTTKy|;ms_^q|KOHQ?Z1$0F#Em3+3!EdY_z@BX+62y={W4S z({(N$ru-wc_Jt~mFfF&Do5pS%`?eWb=HioUO36Mdk$H8iusQCa`iTXv=I4L3aV19# z;O@W6Ovlr@q9!Rs(;qf_FrBA1@5C{u9X1_<;#Opv{GAD~o|rC-4Qgcs8Xh%mXJJJ=_x*DsjF=@`nqX2)L~Vw0-n6c2bSi~HM?&}!3X{dpN8 z+NoXsy{t>q@6Ys`DL(qQ5z(9%|j*v9v-(*(>cm?Wv45ye<=Q3>@0+dK>( zrIDE26t$N7rGCc8R>0FE6ITr5hZJ~Ni%qM^>PIa_YSS@w02x+J{u_aw>_^cFy)=(r z4Tk^NqMKA_pY>bPsf;hTHS&S_mVBrD$`j|A{)b&t8&;L;6V^+(R7-PqG%R&ua86VV zgc84q;G;B2iTZ}_wncN28_JU(}H~M%{wgDAJQSBEh;+O)khrFP0#)Tm$*)O-%;| z2BcAft<@o5nWaLH#v!528<78{?pJy#D2hfeQSVFrcd#64zDm8MgOtcUASAf>d0@Xl zgq(SY2waR{2^w(bw)kJ1L8nLMI~^h5;Qe0&Q$RnsZx+3#|FDDw34mS0vs?-J9BeY6 z_}GriPa?s$XlPB_u2xWU`LF~>cjx{h*sHvhViKYfJuLN~e9>7NI-!^OD#f7-aG!ND z#73bPq{gm!lq(P`7Rmrm=K&he0izYvp&Ib!p|%Jwu72vQo(v2R^>Py0j#xvi3}|e*42W8TFvA^m*K1g zerlMlE~7Jp>?^1Xo9n2D6#T0FW>iT?Cd;r5QJS3qX+Woc9Ef+!dNEfl;Aefu#gOic zSiA_)B9{Z`hlvT!mwQG;Y)XBbOe&tK9FEc%`3r@h!IrLRoR7hRkk*nUc;3s`XR+*ZiYO2IvZ>rW- zU@8@iugh%MBm^NrX7o%LEE_?77(6shzRGf7koP6kEO&*2+muY6y;b+lJ3`MS#-K;u z4E(Fa;m>=8+>fhBjCocN!i>x%)Z!_zewgzEg3cr|^`x>Z;zhmjgtnOq+`JGquA7j) zMM4PN3j~xA-u67K_c%~sFd*xycVGU|Q_~flQ>&l2d}M+^MDh$Mo;zOq6FoNvsdstb znraI@npS!g9&IKcZ=M{ilbbvB}*Kugo9*0yiOJ5c}Rd zuU2LGJn*35wpqwiQk!~_9;bHvPpL7d(UtW>`7$Bg$WdtcMM07CtVvCw3ntknK9=0!a1>mvd6#W`5+}#0y+-QfG*y4M2lR3$DiSdFRVh&K9 znL|$_9}9JykB`~@F#oXfBfs++?;b3wQCDEP0bPHxB<#9Gz+u07vol;_*D)<3p7Tgm z5rOM|JF?xe*cq%!lO(Gq_pp~)8163P_LO0*TjtR3?R3!M_MAKyOFQy+0v`~w4d9b+ z3d-qK^_pgcc9We6Ldbcc?X&Xfr_V!hC{b5MI ze^V|`KNIqP^gCQ1XHH0G4I^Z~xAFyCqWyjL=qfYv-tF&CzgT`2A{4$#gs;g0`Nr=M zQJ}NEtax}@e-8xJhP5Aevy`PxDxoa>=Q{H42dX~zs|X>nPXR5@0`krOa*!J>KLfZ0 z0l3*jl$M*cht{U-c%Jp8W%d(!QXWZ)k&%+|xh_wp|Nbal86}V6it*WRnj+&fn+f0d zT8+V+^K* z4OUG2G)~2k+VSf-A%pWj#=ODSTi(uRz3z8D?;ZXIOFv9M*Hw32zW>03hs?;NZ z1ODJq=UV@~3&6&|@(ka{I_Uf+nOyaIFYzD!E@`d)WUS+L)zY&;YiW0~OWv-}d&w6H zSu-rjAkoTU1lNYO49{IL9c+}FlBNB>%JJm;|5e+O1Rwn8U%Vtt?x zHL5nY^y^pKN}-B>SL(x}_Q!Jf_b)+LQyZ9i_!y=7z3Ydx$ZdZwS>h#ZW`I#sXP?lT zQLngwGq6D27(cp!~MTS+SOLc(&l=<87-5q;JyhnBo*eu6ULw)vmUu~rNyjCSK|DODg zSQ8w?V-JoyX-3D}O*b*oQlvK6|JycXkQ3@On$kqTBBKjo1b|wEGgOX^xP)w=!2fvI z+Oo~h2;<>KW+m1NskvwxVLFf1J1$0-IQ(g^5blx>XfhzzZjL98;Yut}5?&LvkjwQ(u zX8*L@$=o!5>ZEl=rR=o!lzhqCoOc|i1E&MmKR6~g!O6J8FOx61$;OYXFSTobu07iz zSMv@Yv0BL5_m^?Q{>LQa41WGwpC|_jlBfM}7@ZuM&`+=fxTsJ9V%eW@s-oMW;}q~; zQ4k1h1wDK^*pXJLCpgh+?6avY!Fg7x=el#HW1mRf{7*i~6dj}n1}vR2DILh)?G-Ix zE+0R(^*xunspb;nv%6iYbSC_L*bc_lNEF2tS$*T+?H!%I0tUnNH>K2+W#}rC?DLX|mF2V4J9^kkV9#mWaoz!ujx|f{PI(k38 zX#Xl$5P!PjH&{HS-GQt1GH_=pp53TGaM;s~^=lFr;13*mBB2GJXa2`;Hp zqq`+)60%sY{Zk1ht5-A_=m%Ow@d$QTax_7|V8IsPn4&N8H+{l=7WXn9z`3xJ7dw@e zREkq*ibT0h8m?L`fJl`Jy=UCw?}0oFQ2FS}k)&`L{|2Mc-f;f|6VQ?7etV&eaobT9!6#HWYj8T`FcwlnU_dX-~9^qvk zu&i?Vm%Z@i0{P1RM3L%D@rxUy^vm=voi6ow26`9J%1yXN};c%6|F)5 zRmbzxNa5+_F+^}rMiC2Zr6Or^KR=u_0Y+7v|9rIA&Qa2s;~Xfd6ZxKYw8y)kU_ARV zG5GW@_V2=lh$WZFY-fsE)v%yQ*S2v?T^LR82Luq7`Yi|Z1zj#v=gUcKxP|TX{2{X~ zf9Ed6YP4OR9iG5mc(IP>1T`#GW&)b$pVMSEwEl4YN#&H#Z>4oGa}uWvt~tT145RIU&q^L?Ga zxxAH=%IhqD<)q49pTFKK+?t$J90Bem2b}-XEaQ()bIX!{4SlJ-FV$A+p?~0{GVdbzdu?1y?8i}GPs z?^X$kon!^=s&I&jx#@2d%1r4E6WY`v%HP~pC9KPAQf!*OLhCDt#HWZe%ieUtj4NR; z5iHR#(A5$8Edr0Bf8k&c(@zvf&W8@QqK%s_>X7BxV87um5bYJM!Sk*91q)7N2We*3 zG4oD7zqlYc1qY$(wlcqPI*Jt`BydE@Cuk|ee>s<%BVX3$w-zugd( zluVGtW!i3gaNu);Xc3o6He||JW-pTbXZBxgU&{sueKz7jH_bB9`*MHs?G)rK_9B>= zA$aAy|9q8Dko^f*7J*86*jR$z-ldsnvbSUy4FwAKg?;MV4a2{`EP-IWjR`Y%cEFzj z@?r7a84``;YmjP|24wc9_}W{CLk)PiWQg;!tXFXYH4k#|jW#SubqSktKCk7X^&ykm zCfg_gR>!T$oW}sCD#0@R(0@);uZ=CX*p}Yuc*V@br<0TZaJ+Z{{T1ujLN^hvr;`tL zs?SX~d&!4xwyUeXblXb3O;l_11E!A=j#EE~ zWeisqx5DkaRL;p?_Q&v#->fEI^wQ%+g>X4__2XB3&PyNiWb5iBfK||#OHTT-RrLig zb(p--bOTwW)z8jNK$FKg|Jhz1P1JQgQCETptcxA_aEWq?@d}WQ692f16-p4hBp+=+ zjz%gz>+e$QTl}$fIQO$aYqygFVe?aa>fq1j_nd?nj6a*>@Fs#ZOCt>XQ+xvLaNwqZmYNG!toa7i6zk^nDKSCM?)Q_eP?87fLDtQn4ke7N6+5J@dUd~UO=;q38$9apmd=DuVuQ8xBaM$}&Qy4ck_ zBz)`x#!haDbP>b7GYadfAB!cw$T#dXR>x+cr>76uXKMA@xrE;|=o>(Un-hycVHU-58+S|b^@MII}bs#aM*1Qn)aMhs0~SI{{7$6*x$ZJ2UXmr^u##%qc`et zkYtfUTq$B*4CH^lKFbt8dTOM;p{nnqpuT#KulzqOBZKXlpsrSd71RY9#M$F0?LWzT z|0)4UBQ0`@OOPBwKPme_bBvKr%?>-&;IFvOfOznf2oRDv*>^gk>Ocx$OcfX$WB3;8yG|#Xzt}z#zUB3hq)eeDa z*2WfDe!F9h1r;;vGDTI^dzhfa-$n}Q`_p`;vb`4n2R)NLkJ>c}X)4sTg4pI}tHRjC z=EH9=v?cI!R4#jsOj`OfqUWT*j}m;<6Ji~&fgi8?l_HS%F%HLVVA!B1>4h!!bJc8q zG`=G+arFy^en`T;IfrRUuIcXNI`pM$OPfZr8Mvd`SgmZ_#3!sLE|Kxv>aNRl?W-zk z(ED%&WUMqO!~6TJtShJTT|s$slL#T`UT*YC``s7)c|}Rn`MpDVjrt*UFo;~h=;)C_??}Tr=WFV2!1smez5&+@ZTa&BDUg`B6ZY@ ztvH>tN)#|&@x@`X#$-Qz1@p>0w!i&q1NL54j+>O=dX^j|9E_r)5D-&{Ah&-v&`%SX zQW7>id;D!4`<20?xIH>gkM<89jkiaq>d}zkk;$SzPLIyuk-zsf*8Z1&5NY4&c4o6C zuYS%VlI)ReD&btc0rt)OyL}~P$wymdLy7L_4+Ia1=etAb>6iQnH@lMjT@%_C3SqmRUpx|$Bq6(xG}?piR{tqj3U&xc>jDp(cZ&&4rdXa|8J2|O(ashZlU zWPSK33u;t#bS=1@P(?n7%KY{u7qu8G_$+FHU2`D|3)$hB;PcTEiUUoF9uW=niH+*emE3Ec_}_n=qE@AR|ok>I-Acc^?BTzu4p{1FsM27)a4pI zXn_{?>1{W=Vz`{?4rZ`xNbQv+CvC5g_jZPAs^;5hc89Iee{>cq;BynTucgTn6^ykLmn~ekfu)+Cf zk*etzxz{*`{Eu@z)|0Wd7xPr=OuWpnD{RGkBllj4!SaO$i#~&!{JA9#u?99DS-OV} zu)=UZ;~`}>jPI>R_+9;Z;Qk^Ca=_5W9*9u67ZGk#aS@U^WK;(+FPbOH^Ot_ z%OT#-22VQ*YqGC3JT>n{*MDt}qfF8i8RVOr7sxL_5I$1E zX-Peeu%31z!*ke7l4WKDCu$hK+mJQ%r3125_IlT~sWq9)*yHn^=5aNQMO?#&0af$M zx$h1v&TPJa<5#8LH~HV@7cD8w)y03_kXOv6PeM+Ly>#?T{71j2qAq^mFX6`yO+yuI;*o_i zi}|usUcv@FN$q3<=%HTnl|sDu;e@~n6AZ$VyGkxUy^i_i?pKDBKSr!q4t;IPx-pYji;4;s<#xHL-*b6Kw5@I-7=0JqPCOsT;F~?bD{F{!q8`xw`J%tRPr3 zwQzHeX%Sa{9dAgUAhV)>iPi`(@RBx1O3M`eI!76;=Ps7tMo0JvrFBg z>(n#dFO3@eOzOF@d%FKIeC%IRTe`QGj@_NQRz>9V1#~eQuuF2`bIF&;d3vcReL+=8 zZMwERU?q@b3zJnjXHK@>QAEN zq^l1_pT|11V|r}GZRT6DYNY*JvbZ3r*D#vDO~>yuSh1%xDX-CR>|e|nMY^k0Ey+!m zHb*VGn7ptG_K5)c=PxpM;DM;9R8ZbuC~u4U?c%Dhn26%yO(?{WU3F+9gWEiNdu&mtOwILS6#DS3 zZ2FkBobo{^5b$w}KdQqpIV&E4pLt#OPZ9iV5e8$>&7(6`?2}O+*bp+xx*1^3Pf|?w zY1v}B^hH%2N-x9Y*`nsmJWkRd^{S1afmfyOypM$WfeTZmTA~uxaR}0#T?Wi(kr()p zw7;K5iLG!WoA$0owk6+kkXX09?Sp@>NmJ23dm*ztAFxgI`n;tP6w6QdZ~PVr!W;i2 zbIg7vf24I_)E0k!($IZFbk&-D@v7+z`aawy}A{`}j8lG^P3X zt@IcFWH9)@Vj5X-BQL!l&3oQ-R>WV<^{*?!j}OyEQQ&%Pux zd^nJIX3vKO2Y1cv>EQ6Q4XfZsScFG zZ~!4CEF-vtHF^H)B>PZN6)=N#Mj+`kIb&R71Pzz@4rQ;3ZU`!hjznTB-1uY(DOUbZ zf{OSqo_wdLsS2*%I8GcFs5yi|iWgWVi_7`ZKjCQ_l%Kg)saC8XCuzyj!B0bs=T_lo zU9rMT&lfwy7X6G5(xY$E{)$PdPJpiU>{zo}4Fj}%I*$MQY+p*%t1rB^;Y2V*fk?fn z1Of{j)B@>pE_*?u3;TSSF5s_e?7a$t$aTA)-%kJ7aj@%A&i4|52cvKE`Or2l+A`^( zKY(K$(uFY;2rwgyfVpAUA1y`{h*BkFuM*!)Q#WTiL#ld`tcCIK`ZNQcB_YcV)_!c! zInV_dW(x)aX!=8gEoqN1zSydl{&~(452-n1yEn0-T*K2s|GWQkY;~v1MboIf#a6vZ z_OdX_sR37eFPpM{biU@tc~|N>1o2>Po2(XZ>vU~R$W1>B$QWGuv#v087(XYX-AZZI zzaG=6dccW#5TLKbpD&ZTqSd_wxu`2&YVt?+C^G1ON>4LE!3clpaWjvpvR29ZDTZ!G z>R~qzfggbMDmx58_doe5$WM!TksloY&kLFTy|y-X23pp_&Cf6{0f*;s(t%g4 z)%M^*s4mHyTOOH18*pLZ&|Vt2VpWnXm^E~0o4%_yAa<)k7fQDOjk6^PM-Y*~{%7|@ zkP$LvH>9c>8Fa+-= zDk{g<92Hx%SdbgLu>F2!G-t@1#q8^r*s8w{nfrCNwHh*q)RfGWB2T9s0YzK@l)-Q1 z#PKkZ{@*S&J#!n+FmK9ZXJR zP#SWh2uX-(W+;S_MtuUle|GLH-+%mVFItWq!!&(lX@rl z>XoStsW-N~y4S9nx4T=LP~WgH|4$TF+&r$P0Y0ln`^a?kW^n$wTh#5dJ~cV%w-F15aKVnyD+x6jC%$8oEa6s3Z&i1F8YC8F5bnuO0sRilPD$atIp4wDWm&u;OriHDmeyNwS z_9h{I{YjSG|5Wx10HbColQrP zm%rzgQN|&th7uYt@P9Z>qjWqXLr8Wx#OeF(T8&SVyNAaX>v;L(?qRX^y+1U9!SCb` zgx^?)@>ugY9>q-_$3?nHk7^(C8<5=tR#q(qD+5t}_J2e9;Mjg2Tz6tQOp-ycUuu#Z zpt>}79a#Oz%l)vLP&hk}Rr0?)Mn1{iC9(GDcClBi{X6u=fAmn1Esy<=^U}ZXNQ5NU zH`Qx!{kIc3ARpLhO|GDs%O%iN9ITQ~J00^YlVsG+c$vdwmp_mE)G6p)ntNZnm z1Rh5}LGAT$0$t;$1O3E0`Pd@qRMJ2;?y|bD75E+#*Ci*tzL%*)Wxo=HUFW5()r70i z%}-2K>-)MdHnM%K>4ksE*W!#gdIgDye>GeRNmTshXBN7#;4^ zQ*yOl6*Q5l>@7}8P1@Qn*diIK$#T>rJ7eHMYr}0#_xq>g>tKm242TlV8{tp7zW zvww!}iBz}$7%G%0@I>k{#TO@V=bz{v`N#3gFn-mU5iM$aXr0i216Io0PXDS3>UtS| zF*Q0ke=HKh|2uyuelPn&iXq)TF=P%J=w*&yIRu#YXA5|8!Mm>5pLHNRq2%ksjsAZ& zf^n3hqmLthXx~KtjT9ike@8%n_m!6H*R8MJ5v9LUUl!lY3rvxtKh0fv(*Fz6i{KDa z;UZLG8EqE$$Ma)?l*>TB3u{C_rG)y;TxJ!VWl5jE*JLQxUTc(kBRY;Btvb@|)qp*` znD>?+x9#J6`XTmju=wA|VgTv8J{)di|B=VMRY0MCSK8hpu%9RGA7?N7B>OHdp1~x5 zlJplRF-Ops%>9m+v8M3JxWE>zP+vLQp{lc{r*$jWUb?v|wY6rY8OGY!FHefsV8SN1 zKh|`L6lx!HTARLFpG;=vvNm;-E^DMzTU%EL#!>6wz4a%X8PoWAI`^hv$8ta&Bi#gR zn39toC>G~nUt%3c0iE77@P_pvctKp0CnsI2BKU8wz>RuL%G(qa9$rm+$%2ZH=n<(n zQ>xm#VjX`~FH%#`byG@e9+G{lSv6c>#?E*=xo5bBQ#=vcYGSp|)n?8v1nS|sW=EPB ztFCI=hwP|{b>Awxf-k!REKCXfgT@))psyXberL|>Nqt`kzy)cV7@F`;h9+DMG{yvK zi>z@K8fiSngy73#+cb`%3%M6nDEF-W-BJrJNBei%vMr-3J+)%mg=BPgp75HQQqmt7 zG#uKhbvb~Dr7PLhIdk(&x*;W1-sz5fWLY>v17 zY4`2vF;0+mimw9sa%#>58C}dWd|`lvRk5=4wL*k2a!ZJjfliGY_N}Vk$TeTZT^ve@aGDl$TQg?(w>eaPX0<(hFZ! zk;y<3vwz|yk>0zez0k?Qj>)m^Yvo%qkn71_q^_ikpZQZ?Z$|cM{uiA?JYF46x3#125j>G%w2tPDF=B<~HkAR(87}8fBQHB&W z6TVWEC5y=M&U4HCmn^9&Y*l8ZOc}PNuIB8e$g}kG+fBp$2PvA{64s8GR7p%!QHj4t zE5{{@!MLrr1`R_xlkXPC7CmF|U}iAj81d{@!r9}dzy+xe-@kCH@U6JckzDz#$$gS4{enbOmNY2 zeVNYRi|BrqT`56N9I_O}+VA5gtNNr#op0wWr$+YWf3^S3PoX3H(IWqOw*2SW;hX`{ zf#fRlFP;DD4hK$~VbChLv3zN<4{d?Ksb*bX+KMNFa6t90SY|gCEtVWd$-4B!u0(ox zqIylNV;{ANhGy8!WVeW0FKaeaE~HnRJ~dvG+yyU+mRuFit8(&>>wAmk&xH_5jW&p# z>I1Q&4%>Z(uZA=#^8*7`V;lI0>|c^G@0#4+)kNaQhU^qGBibmEXp-c6*;`OonFFeA zn)2WC`d;{lzSY0s8#`ULRWC7W;d+X+)4l( z7F%=;!UAsFsSEqgGuRc!wjcyNJ+;J31-)GrmsJuE^0QsmDh|U z9+V@{vyu4Y+gBWpw_DLLl{L3D>~Sw5vYQkMV7rB%>@SYy;FFz`<3fxU#nYFz58s2tG3>d1SSD z&%~GfWe0$t6REeK;ty^I*Z-noU;U5P=g?b`gE*fq;%M&6&vf|ZmZ95d0=Pmn=tci3 zKFd(gpPzeA?dRm9=?;JA$WX`K7ST{IepKU!^C8OMpxe_y;4h0US~^Jl3nyQojUO<` z*9h)xeULs;b8OWtV>w2*@J~5Mxg~qhD*MBJ1hM8db@ViT0pFi>18WvcO6{syNl7yg zr^w8$*KzgVcbzxx(^SEMs*Ev`M@bq9BidRCf>94|{WnPkx1oqZ=_TA<13Co9b*FVn_n~>!oM{>q=zl5$GiS zJ4b^L%GyEvP7Gr3?FCuqecWMmlB_ox|+PCh6Q; z>UQq(STB=^-MgkPGa99+_pHHHP(O_3yMV7b^Nh2K-7pJ9+$nndr9T>Un5By7tbJG27iu1%(gMa~znm*2^XTOEzs1@)w4kTXdE>PV zMjke|j3U!jL{3p-X^nPZ_1%b5^YQ*A!yP|B3}Z70W+?85T~}E`9Os(O{n|f~PqSy! zCcvQ&$Aj)5e#e6#?#~s9@ikyIpkb?Cpw~}sXY1Des9`M8$t6d-b=`0=CE zr~Ni7*!~M7Klqk;Vi{h)7ot0$NPf)G5EW2GAx$c9gj)o#&l=kclR zH1eJ#GHe;6Wq*${lm9h=sFxd`+@O%tA0!B5?2S0Xk&{wPXjkK;4EqhI{K zpLRV*GjgMN$p@&{yZFHQKj-j_Xm4&PELw-lacnH9QJTZVFWwZp?LB3E(K%&rVKlz*=^vW#)8YG47h3&*|EX2fHKAVa2b-OY<{&=UBxk7!%h{ z1<*f|50^{ZOhenuMB5CDM#x|Bf?2*9U?KMy!A|y&0 z((x4x(fcWu;C68Ra@_#?(1{K5c8tbrLA`x_nN(u&3x5!j6|G8-{F7Z{P{%$M><&f< zbnM%mcy&E%608kmCpwf8#zfOK)sM$A?}AV-J#mxmD*OsJwduu6LE>ahCM42}HE}Sb z6I0VzTvxqy!OA+0j!Ny!-RGP@Oi`7F8?5kH9IAQN7zw(^ZgpxNISVEUTAj&3O~8R8 z+`w=h`^;T&C;+?>jO_}WYP|H7?4I-tk=tiFwq3c+6icU=)%*p2(`~L|`YX=?DYgxl zN;T4|H1B?L#(RfoVd1>4TDF72ZC4DFd5tTHd|I`+pSf;iUx5KRpej@R*6RUM=LEHv z3ZSnRriK_pTwxMJ&01a;K+9tf{NMq~r=#n{1e?f!mtoq$U55kPJ(tMy$#G z*5Bu7So0CRKG~zRim*mn>Ce-wTntByW`Ya@_CLIgsps75zVnbHKxQvQ%E^C#)%_Lx z5p&<8n>+bK{?|VL7x`bmbNr2eA~>+0&@j?;EJLnkh@V?k7Ft8_Ui|nve5m_{A=*+- zYp*kFh0vtzAjdhZVX4SjC5cSK5cXGmB9{5Ly^Pz-i`SG+MJb|yTX|YAwfd#l)qmoG znuc0Uk8%@B(mSiE%+)lrsIL0=vCQ}E-K#2|Zd_A7HQih?mGjT9mhV2b`jyLD64h^C zF(FZ%jV)TmBgnPg6#P%PL8C#eZGRWB_B|b9IsI29)AYqk80$!`DBV1FO1fx`O3VLb ze?QUg$M)HNQ=+u9`DcIiQIIb;HTBBW)Sq*$Q&Ue*P5nMQo2uL+)7316nVNbcd!U;7 z7NGP&c9vzG(!l;^I7h#a*{|<+2fq*5?*{*u_Pf=;Gq}HdnTg1G-C@S zw@h4Kf*vm&eCs=_iSH5fS#THM!(w4<(J39Kz_^H0S*d^BfBvJywK8t-)>W7})Xw** z+Ug&y|G|Q_zWO0KsqB2BeoY^d7hVnq=tm%=YuS`8cb;DE)jDaJE*hUR-E}3`UJ8}_ zMUIumAUvac)b}Us?S(G`!Tvt9Yb(MjmMOFfmiF zdO!glpFH+Fc>V#yq5ghwS3lx#J9z&kNBww!ddnFzq?c4-cf9qj`q4jPM~z=MKhrw3 z^xUA!({IQF)7|N1g0zWhFaZ$yYm;f7+m9|cLvH@6wtxz$HGg*6g z#bH!9E;Qlx)qWJ$p|39QLNWbw@ zkgA<|N6Gfv%#d+g{23&3udlzMIJ%8KwWcRrsFka|+T$J&tQkcyyxLJRN--v}-& z-K}`s66^Wi>OAL59Tw<`ZXXZN($N_yr#P3Lfi#704*Re3k(NGeCg?WXuPHOPFy8v^ z(B|StaKUNe($bpLj_y~6vDvX(gHW^A8+quoKY5S8M+QZ6yc)oduU)dVx{M(W;nLC> zoi(ilwJmRG?AvnC>OF8R7)$&%{At2+9Cl(FXKBT9YmhJ;%mQnL5x8?Hr=EHiZ4VAo zuimg=z38oupb}#3X)dm;Rylkdtu|9kZ_Eb$66))YE&Yep&J48BJ&e;8-(A8$q1O|( z6(qoB*vg!V?I!#uwNl!uFS#hQ3!p9cU$)AaZgs`;efkAdaZ|MhND58Ohm2Iw_}A*4 z!hz~=m8Wm=@0tQ?0z0d&nOqOYPiz@3{LY=q9?FGYX2k)*EOo>7P?h-F&oD|_(r||$ zNzkWx6%qQULZ4Kr|3#xSsSx8*8VHKA*J`5j72iDl}qBq&R8d3aHN-9 zEm{^fF{uL8SWaj1$>=7w@L_$H8TrCF(v6Np@R1Y^m@GBz;BNsj;H6xTW{a-2oJ9Fs z@DVR&>P3mJ_*7pBf2ssCn!pR@s@%VNvLH9I0gMQWUdyMEr=s^{aAY`r40y`1MF@Y}0HzEK?=R(zaYT zIzCiKozfi`xY8>&dqa=mm{9v0?#$f$`D`6}i-rAXCU0&)B79j1++v`$R-(`6{&4l; zB;F5h|C%oWc0iDKBCiFEm1(`y$%A#`1EvnBt|Xq0L48yJwZ`wuuG!O*`>Ef^`4hG+ zdfsxmi^1{xrvdznnLO|;^DZ!0ydCge?;*~y8@n(=hq?HXvQNlR0R0L09nEqSS;p9F z9*KmLK0>9;I*m|~)c(WNL~8%x`bm%6ai$t1-l(9Kl9P%3rA9udr;x5(f2!oF?5#vd z)#qP*U|+RfC6(SuSQXP@dLovzv^vOLxvqVb)NqaTu$LN9VSGEi%f!IPxB-EwA;;2{ z?)l$oMTYh;<_kxw08p4l4diL9Tn)co5C-el4kj0X{VL3416-!~uALfiB5NkU{x;9; z-&QKloXURk*~7$G>5)UMpQ9{cUA0~p)arjDtxfQO!Q}5p2#e(pWfz-fbBfC^l7Y(< z7mkgnH%1<;Ul^-=oc1>>;ukZ58FIc&hryxiL!Jq7?Pp}rBqS5&gP-H(3Xm$ zfQ8wz+E%vQ*VHXvR?R)sTU7@t4e^a|(73#55Ru{v*^>$lg-)bzpQt@WW4C0(}G zb>Ma~G?ZC2qJMr)^Aw;4&svd$eZT<2Q$U^UDiw-#TV?#3?5Z?M_$ONRqCUtRucqBX zBm9^7?+`b=Wf07`M}xH=TlB8#>o+o1%sjbfxR-osm)I4ke2*kX%i z^{Tqx5M^}${_Et3mYXfstg^MAaAax->f1a(o@fT;q!MqEs+&7WQc~s>GVjc~$xP8u zJBXSd#?`}eH~v<{%qx5vjpIO2=!5z7-2D&fp7`uRrU-W((_aw|Io4Kqee;CK3ynGb z>@m`iC-0JmEXXRf{hru+5BvTdaEA;ks}cHD$&Ia~W*Nxt6PxRQ$%OT(9WS8s0?_dU4NPqyOP5L+OL~XkVfd6zb?^`K~R(UCN#RjBG_H%X2J}P;1 zRG2)PU;m-52mY&n>)Z5w|H1k$WDM5-=5NyXGWDII1b+thJ)bh#UeLO7yttz2;B~FK zD6~Gc#<>&_PM$HehebCUDdPkGhhtzP;{Tc%ZhkND28A!SXb~0k<)IG(W8+7?OF5Aaw6|z)cS{?llu@(f*t+x3e!T;X-PYs6O{P~`6$1I=#$MFvY ze^Us5(JvE)uSH9GiM_6kEZ}e{f-sC^0Q_Fef zrIu{cPiExjzcrjYVlT~ihNBP2TF=>%UJ8Xc@*k(j(G1M7`c@i}v`iQB580p1QY*@P zQI`Oabn0en$uNXD0dXS@afYS7nx`+~_W8ThJSKi?(XVfTpSRyrNe<|z`Da1?!!+?n zgZQI?a-#z~cpZ>I__M&rb)9y39+%6O>#}E|5QyoPp;+B2y0d*m#}YlH|8ai9MV)l= ze%DD$pSaF_D3z5?F5yErK4BlLp|Rwf`aU!AnaPayf)&B_pk$1p1lcuZ9Pd@e7Wxq{ z>B8Sd3Y3&V1B^P2%|`AQ`B;@hfl98Za-jo+dulzhPp$8%;(ti3%-{diOIdvZ{lG-N zGF`h~!N*j*Z=Z@$v!U(iOTjuSwj;-EM_m88{WS1C=MBjC0rMZ4o8fJ$pM`iqyGmR1 zd`D|jRq}-^mpK2#aOI*y!ax9gng9Fk4po()s%kATyBLq57hZ7&V;bwWeeu+$s)*uQW6?@7dV>d$>2|85y^yALUU%@g|4k7f@s)(N}NhYbFK zL025j{G(|wcSlHp!STONT;FGZ1a*tZVpronUUl)SHTxA@F=TAlfRCLL?9|r*J|cGBn8%0oueAToVE!*bqzlM07FDDO5AW^K`-##m zuQ;j%zV2P5*!;KsZ|)GI^o1cgY5Xgr3woNSV2b6pT^y^PA_x9@p zR@VJ8^Xs|${AY6@cw8T?7E0G3{wtW;G1eC^MG)KkKi{>_ zOiq$P>+k>aJY>#gpS{<8uf6u#dnkEY8`PV|!M7#eLiH9alXEK4gDc&Yv!o7g~+ zqj{RHR!^G>P-s{!%<%i<$fgJsS$#wpYVUKxULtslEkJ0|3<}bWtTQ814NT?#M4J{+ z3rR8T5iJl?R}Ao`k_zKa*wEf_OL?k)4I1>uqfMs>h_sUAli5ursMPJ#Bps_!8>Rj! z`wR9?mCJ5Kn4P8p1dGpojB7;r?i|lnwDiD^8jMPn{w&Qqq$5jC3Av&N0FwVxrYzJF z8U^-yoWx4kuU7Ksb@##I9l=o^u%y^F>MK7$16Z9(LVSJ2ZbKs`WIar9njIVYB*%X{ z|Lv&ZAlvZXMoC=q{<3xYMbzvUrtZ+aa#EKLBarb;qB(&-BD&I z0aQJ&1IuO+R=HISiZC|!2QzKM$c13jAkBDFtw7uV}adY%k!Y((LZK1gweC*@O z(sR@qX)*-$b=@c70;mk zksV5~tk&-FOL-pCp-cLhoy#=aN)WMzT&w#1YT0!xHCT14joLJ)iUjRQeO$YU!T5We z5}-#K;3%-UkBx#He%cUChzY^hXif-rVsbdyEm`$123E1mc#*op@sh0^4LDjfV0kYf zM{ZW;svRiaXgEA-#EhY`1-y82}DvlW7_QgQ_^S0C*Nt&is&1(U^X1*|w6 zLR+&!{~f#@-+XS_i6VXlH{#3*I)p}$A%XWZClO0s!2}olV_Tgcb6;DvC-9IQ>qw%# z^QTZ+IG5P(U1jC`o~nf1vYa3>ze{d^(-Jbqg#P8JRTviTXAg~6|L<64LF>r48@nxAEQTQ~;B^dehN%iT7AT)M!~;o2A2lCp>>)MK zm|@q*u?^Rh4WL+@1Bh6dCZQ4$@_Ftcfm*l~o7&!}`u>6C)Ie zb5-G3`iZhV>xZEz^Y=OsgMz2Wl*-+@9lBXi}OU9-qzAY2vKgj5!ktg)f*|cN(#J%f)b#Uz2tzh z@y+x?F_f!B)BA))gGSU#;Ggd@LqvjNmNjZv211k4JFMYAk^*HTkpT4hOZ_@SLXAnd zC0)`2u{jf^lM)0c$vAAZ5>GkaVDD~NToF=ey+w-|r4_jnB8SX^Gl%C;H7zV;R$ zSn{YdB--0P+B6yo(`bu>bJUv>;hUlsvQ9Nm?yx~Gn@qv}@f1${x`_*mG+AfE7>{A+Eml z@9M6D@_;bizw$`+?*Z_Jp`0efhZ#cVZ;v;(;MCVcy}YRHU&jmZ-x_@VkRjSwHi=(O zK&sI!$B=abeYr{x35w~){D@U}4_1HhqS?UH5nZkh5pTKbYE~-*v z6=f>-4t>AEh0&jXEd(E*75HzAHuz2q>K9z%oj%Td8}aoU+?W6TX6Q>1$-jp${oZ`1 z`X{?mSM;d=I#>Tz-ut6nq2t4aL?q}lnPS4pSZmY`d{q&c_b2LoI`aPCLQoie4+=hP zr5>Z$pi#6mLoHFA*+I}T`AQA4jX91n>7gk@b)w}*nEyd?WI^J|)l^gvz2P(dcJ>cm z)ph3eH2q!8Q1McIj3^qTXy7)xU+lKgh9}MdyMs?epA(s_0RDAh)X_-h$K)PTVUFXQ zjvJ zODe;D{`m(xvsfCK#lRv4#xT}+4)_3pzSp2~P5WsQDD^pl?=Yli6!8^I9h`29iy*2(a3SIQ0c;eHGJ zmSffzPv$-HK3+<#m?aze1^#(7@esmk`>3(g!$G2+am@}P!M+FQk6bqzW@WBaKRz|0=AZ^RSvffAZJBSyOZ$9;3Vm5$6FK-Qr zBrVFm>>{5+tT6}z`(gkD_GPQyrz1CX|I}X@eAp^zCBhFMO-nH;QQrVur$1g{D#W_X z#ggl(TLS+mNB7D%M^j(87!R`^5Y5Vvr8sS z9Xz3TNo{&o;dt%;i0xS$OKy^jqK_mRlVpoFf-|;=%_%$0km@tECF@d=*_Pl)>gI7KRkPslCQA_PU0kqrN~{I9MGoR zSsC3OnU~S8>>K7^GPK#RZZUuNs#_?DFa)Sq)WJVhvZCY+!erltM6Jb6f0Gm5RD+^S zs2WffZ5*wu^pz~Scy2;+XXbEjDUlWT_7^;eCpLN3Ni8lL28<#kofO7yWykXtF01>m zoP4tFl{led$T?i^l=CUn(?xFKy+*#DC%u^&!G;m@Bv$zbK^h%4Q?vdtZpa*_5BNPu zzai*#lu9r*q8qxUjiC~IZb~GIj0wnuI8mW?mK(+#OQPTZQ*_3YEHQF@W`BNcY^5d4 z6-uCJYHi89MQQ?rbQW<=c2`Bw#?^M#(Z(A0sxQ2AS0&NLBeBYoM+I8fV8vJuub1^#GUON)0FqjEl>SU}|AH!# zF5Ya~D)Z!@?cB;+-VOEPgdqz2s^GJnqkNCL{8b0J@yFF_4RE&^TuQ40%xqvdC+e_E z+8C!kbHn_%Msy*DYA`xcNOUra%QJMOo(KFL>uly{eQccYX2WXN13NE8bpy= zGCyQKUSbyXlyCQnCSHIwG4=}({cEcjt*+w1gw!8ffNlc4d*dVA<9gPWl@Xs}gQP|c zP`DZxZDJajU3unKD!z4_p?pP!ms+MzW2uFTb8@FRrwT09Q+OsLqd-`({oZ9M7P-z@ zsK-^4oIWqjIG3+@Wr0HgAlb;xvJ2DI81Mq*%D)PyH>!jW+AU15!=2nEdX7973j5a6>u& z-Gi<4ws1fJ->$&l6qL+T($Vk4GAn-d=8##6sQ9mf;typj9z;q12zCMX-w{ALnxiEF z657lavC|j+!4By2w}k2~){E1-f#E;l0QR2^l|;dve)z+?gK@np{?W!zaSJ$nAWTxW z*bFkfiTz~UbK|B^F~e(v6$|uP1bc##{<(;eA9XPA7tY2ySTLX8>)#zu$Wio<*d{V> zu^XfR@X!N)PrF&?iWYL?u0A1U!Cn1>D~dP5XouG@p|!FUbOhVpA`iQGE3eHr>k^M>;_jo=HDzo>V6s6F8P@v?8KDE zJxhnB%Z9E&gA2HG-a%8+7!jlgx+o-@Qfl09`aEgZ!r< zZgCh_`_x7Crd*3E2t%xEzt2k&CrJ-HASxu~fl8D8(>8wUj`W?@QunOBP-cDINIt{; zoT$X!s$un0bQXJRzqU7fdsDZv-#6cG;Yft`yExsEw8=Mjw7;+79+P~4_`H``1w;&2 z_h^42X!W%YF(cRNCbl=k^Bq=Dzc_QA41u~~8V4_ol|MMZi|$$Z(awdCtGVwmW9=Sc z)5e5RAm1lSPN}8BOUh$VM`YC<%yY%pR_uph&T&4)r-v7z2|zIpj%02f3M+~-Y(O3GbF^p?7j8Tf;=6}OuMA3Qh<{U zAEpccasJA_*3gXpMZdW|Iw){N_?^UK-A@-3Oc+`xm1aUx@f~Ihg89!6@D0?g4pnPW zQ4DFb{Su9|U#2$s9l4z~1%14ZHMSPSJ@XR(E{&!~Al7(@AFF}f{3tz=+z+qn)#wcc zx*)Q=Uv7+?i*FpNZ<9r@u)WD$8kAOQ((Y@q)WFTngLuIpVgests^zTSp=__F#fi9Q zLRm6Y;{QIFut&;*X}5F{MQY%_Mj+}?ZdprbI=<{{fHm zCx)7Qy&MS{0;XaE`PxcPDX}s&^e4!CPe+>kgZIwL9gF<1<)WOW2rv|^AEpuaLq^j; z3K~|&x#jTIPA+0hnZqy*omlP$ zKVi}Hd@O4f#>auiT9l5>>ug?J8W$TFz_w#CkB8`a%~!|WjQPLJRNCY)9nM-zXl(9`p6$h5A9D^1K$6J{hnlIykFacZRxA2yBZVR`AP-CCue4+z}HYn9-VKU80 zzJ{yC?R=^HPx1;gjMlz@zjG$QTSRCEv`Om^up(BXGhi4qbOlC;p012Rq0sp|LWoW@ z$at)z+4fsIWh#7?U%qZv1wCM(XXI?_)J>+?9JR>$mIh}ylgp*+g3#<1lCIvxWH1EQ z#4cco_h3c;Y!q|K5))P$jDni`(g-Yb6hi2Ge}ay?S)ZE5Pwvq_4aZ+5|7WQw|NUjR z>6iXVzwzn|M&Q>)8{ZKMFtJrHg{T&LCY~tfwqX8Oyu`ej1$CUAep89wgZ&;};sotq z{&l`fHMdYI`s4=#5<7cGoA$P665p|B|D7kFJ^3)U<`ok=QR(Fv>Bs zTW${&&=w7p%EqDErRoeTE>R~}O_yZtA2 zToP?0Pp=b0m}UC0f<2cz(T2k73Oe)`$%jQqxs3v8vo$tu6A~ z?Fne0*2O|~rT{6aDUOm}H_u%U({;1H1W>(~07OfI_n=m+8x;_q!?0ghTVdG>BAiId z6fO3@F`H>;oNQPv#*xKwErE7s{c||d;uylbV$HV5f?Pu_tG2zCh{C6{eyR8;cd8f zzH|wzIbfqomKqISXmDzRV9Ih*4f-WZjUMO%F6$dNacX@v!z$VDU4;D1Yp=UWt$6kU z7kp{55jfl>{$_x-TI7H%)+^AAKQRY`HIKelnyY~&d8#Z<>A=kh)?su) zmaRjcMmtkY9ChMg&%z@FQQ>1GEP_@@H3{EJ}@X4}j|CwX^((o(Q)?8nFBmnjPiMo?#jS2c)voY0xmp z9RjPImSQj{De_kZev6w8>thx*@EGVdY}7>m(X!iGRHclvrO7zoqatwikO+ASZNxbkM;VXYneVUF${}56Z8Y=LQ z3p}zZ**cwgqCdl3tOu;{!D``V5IYnjD+SJ{iq)$E=5NCaw|3i zMNHUk*hryY$6Fw~Cfp1>Bmc)7sUT+sl6!$~6WM~Cwdp2p(g8Iqv2fg%4)C;;hXwvoi?b7X*TifRFUSwj_A5nkNyKzF z@1OOd>6fX=2{XPea#O7=&RC$q=niC!9NcoLquPm1F9JTO%_pJv6_pYO>P z5MXGRK~2a3&5ysC&Tjonz>=o*EPrK*HWk{YV8z)BBSH=lm=hpUsw+dpw7nvrQ>7^A ziE#e%O^%^xLRL-#@U>azReup1KL*9jnLV+{5C1Qy7W+tpGw6Tt;Y)k)k7HdD%U7%Y z`Q+cJC&J$>^Qz2~tdC-`CffEzr6`$>x{7NYitkhRci~gvB_;XRkwSNk`aSX-N_0`n z#UNdr&c$H=1isVVFzzsL0!TIb2Z0gs=M#J46k^YA?0?VwRl4Js8o1})5rm?K1o*gO zd~KOt?4@$*I(D~=-k8Q{33{)^e9g8Y>`Tp$~z+9RGl*WAU|LAGw|YVU@1uAMGvA zK>=HFNDAwRN0*H)9y4~JrU#y6y@0@x zNh{{Pe-o~{Xk(&X&Jk9v?T;cRQ%p=;W$~TlS8T>g=HgHBon%_H@PlGw@lGe`42y6mBc_}Gq#-Cy@^eFA%#4Ub;;9t5Q13LISqcE>fmU0eYBIFh?g z1ih%NG}^R6GL%?;4<+cdC2@)p8#P-@wd6DU58(CCjm`K|sJFB*`U`Y?#&gM`zsB{$ zH!lkn1EvE1fuLAUKTDms-lBeU8>!!-c&G{Ui%(Zozt-CvGyUx@mR}r>{*&{s`0j8OpF{EX`~Bx!v3DK|)oEaFmt$zK zB*D&ieff{WeYr`X{73&V>dKsf)o|mht6iPx6`?x&(zO383e)~B-rFWw{s-?1VVNy? z0f#q;6&gG)uQ~0^lW*^c)hdeq$-A5tevlpHt0?F?BJP><5MPa}8`)B`BXcT$ytcOy zmFjPg?L$1aNSTT!6~ljK7cJkKOB@L&zuG5lg$PIaWgwopR|B3y9?Q!2iQI+_UbpM~ z^8El*L%yfWzE6>TpOVM^4T@-${E(p6{t+OUQNadlJ$Mhph%6q#(r#&p8O4*;2L9+w z4&IG6y^O%NAeo=ts&#rW>ELl%QXjeI>OtI_V?1!4U7IJOsT97QnA@xPgXj%cN&`}y z(x(;YmE4RqyAvqMHDw-$$Y4Ea-@P#U>0p^Z__s1i;9mjb{UZqeJ5dD)8y_im6wv`K zjYc&gQgwPoT{LyQFmWM~UUf0H%-U_Ajpl`Xw8P$a6;6_z;DuySkqcOnIU2$9`vMNM z*nU!I95b5M{G*UuKdHAszBv4>vK1S+Pp49ZsOxKc|&`5*i8dNP8o;u?KiWX!Zq}=$_je5pL&V5*N)0(-R zXX0JWEdlS=8t)`J32o91`)d4S{|$-vL|OdxTnV5WgMSI}na?AhD=0IP^KyM=%rnm+ z*CsxhKS|(6{T|?s?kVf}<@*(hO5#06C^O{e=@0E7QXa12L_%XMO1qab>lP$hDfiul-gNGT;+V`#yY)ZRZ8NAy1*Eh`=_xLE%f;BmqRwgW-%}8>!hnxs5|?z zHNvBG1vo4(N6x7D7?a(3U+W^wUG!p z8(M3nO4Uj)dKwd#vXS!2BLyN^Lt6pXmmRTriKHs3Q`N>t%#z6E#tX01Rf+$Qp(f%ckGO}}Uq>B|$BReRow4s>(U}ca9tjN)U>q+o~!WzXa5G#>p zvGPxRtCB>ww4_Bsl-OB%?H<9Zn5ND3t+_UW&)~`bT9=WVvdT-!;_{PK0U z0PWI$stN2%cHCV^c&}K_fqSe};Hu}Ni`z~5d;}R>U?t^lE z<=I(Lg|?-E<{~!s-eQ32EV!*PO8NKf$fBHdng~%~k-t$?`5_0=`Co;MeuIjoBiG#@ zq9yIJ{zv6{i_)ENX`Ofd5Bv%4n>7!FzA>+^j&B^YwCTT1FY6c567hoon37<3+_)qo zabtZN1OBf>h25S;_%lC7&lBG_*bc8dr<`2Mn##Ofa`x3pVE{nv5yyGBxnOzAv{) z(VZX&h#8Z-b=V%-xa6x#S0TDZT|zE$aH<;AwbwsJRZ8G&Y5%?d%Dn(r2bevrM}GLC z3kiux{*kvC$S5!}e5>9#-Rwu2cZuqE77OBU(ah~93*s0&BJ^=>AiXZiqy8N)C)jb( z*i|kO#!wGTP@7>$qz6W5BpJex_85DD;r;=JOK6p5W`p6+4Z|jyUO?0B2?>=ePc&r4 z(~5??{aa8LXLi$n*np}F4$7F~AskZCR3 zSN5tq3fosWYmC>yfNg{K$tzyl&Xc{w>xEw7Itedwc`g6~rQ=)hO!A$3X3m*^xdDHu zfFB?5%i4=B^M-71DoDUn}5gj+SVA$rpa_Px=iE{iw6g3F~2g`e=C$ z^qC_1xcMvEo2hBfkF7hAkCh%8KQ~g)-k0rrY)Y6ZU(VmVeVw4IdS#Z+_FNn7`h(ej z@usfx3SaT6o{e7jx1j5pCt6HG&Dc>(D%oN?8$WZIv=X!~8O87|9r@RUzIM+@Zl%3~ z00Ko)gy1uAWhl^qR`SKDVp%KTZQ-L|?%Ys+Hx5q9w#f9nTk zYS%vNm4>KF%p zXq9~ZoCuz$r~}0GHO)pf$WY^@T8wH^S)2bm(ZP3PxEHnf!FPgYYYe&f_RkNA^Ny+BRtamPQ%qEpt|zoy5gv0)yeW%H;H$;+`V9VsghjWCZ= zbBni7EfyE3NT?#darHLC`1>0|^f3DV3PRCPj=qB(2FdUUWlvRkW#C~sMBAN*@Zepc z`piQ(J1FK5T}?5g-&QE@-|B#Pdq4=>5!9{um6zp39CsHgvMtXnM1K8<(IWbRrOJ-~ zz+}4g<191VF}#fQ&?#qi)sJYS`E^y-i`#SGG5PhXt{1=Oyn>@xceO0F=cTT_zGs&7IL9z2MRX0*y{WHyg-^p+r+}-Xi=Hv9Q?LZ~Wye1v ze&`mRaZ2d@&?%v#Uxg`R-RU_+tg_cm5vLne-wZ(|iLSJI1{PRB9L!ISaZp!k7}Ui6 z%LV==@0(csW?hJlCKf3eCt_yRZWnw1qIP%O9V%#Q_ZS&IlJXm6Ly_Vy-_q zfnxr72%ujgD0oiGrgEaMQABE&c@;4b@j^TqxtaA#ic_F`GkI;wm*t+^7>bws`#Nwh z3g?4~l+He$2r#snqvw79G~^C?GK8F|+2Ah7HM(M_{W4U{)T~`tXOTmHCR5O#^S}HV z>EKw^ubYzj^Iotf8_Gg2WGqa+CiAkze{3oG*YY3h-w{s#CQAQ2`G+Wty!7lo zBqJte(=`8}tx67&QdZeMd~@I8LTWK*YoI^t>#ibCOJQY_mtNBkd(zIo0M~Jf&R63& z297fOaQ=8!E6yR9x+9i~?I?~-eSSjK#<_1!C~TWhwPE&Fn%_|pE8G}6w3XfH2|#R& zE?Q< zFG~EsCpzswNaJp>f@oaR(P53_Al?H1!*`7fW#Q1hY23bW5b6O`tCvKTl5`Jf++lZy zSZ^A)jaaeh>@@B>uGmXqduRUzn{g4MA9Dr5*#?;8%nmEoxm)34u)UX*kYmbqf@{47+b=~pT!X83~t zF8XPIZ&2|!IWWfc38~QtaPWt1GWb3!4ZXPyB zu5|QquW(vhTXFR9^{m<@!|H4{=Pu&6rU>t42?YGts?ku2B671mKBA2@~`uJH;VI@@9OniDkfA%?w2sT)#P5`xbrTy&; z55LU*+|Nq%^286Jk7Gtr4k#O>l>$`xDkB`t@8=~^({VWT(Z|V_p4Qe1gsnuvdTU@v zBxUpbAHuC(a4P~wl~`6_2vs;k?`~~)j{MA<3`Q-g>sc|~^O;rD+mVkJ)@!KlI zID)XTF}$&?SbYQum&1Zz3*ItGy?}uOn}ibNZTYx+x!>*TUC%*m|pB&lf|iz z>3Gf=FLO=AL}c`UQk9!D`^zuN8S8`w3@tQO`!xYj{T&^&#hZ*0}PmK4y#dJMeO5sNm=oD*j(a&a&j%azNa*US>m(&iY^81E9l z&U6OZZS0$y4h+8YD*H1sVke}rN0G7vP8_bpphZ71nCBY4mUJ4;141oJHS4DmA|(mX5?>zt&r6^MN=|wF;|P`zQI+uFOdQ;vVq8R;Qv^h|vra zBL|cJ#KS1fcLUs2d!{3JtxAsyx~8G?P84ad zz*w9bD=HGDA9dxi6eWKBg}b!&vJ}<41aDO&_<~9g&Q*GIP^nB``i`Jd%$Cv5-}CS%hf?qCdepXH*8H~nFkF3CZJ!#YLOML2j3@M zA70{*y(L(WT8Ej+(u2k`L5(!osl%>GMqW?n-cq4yxc0w{D1NWt$4+9!%CI{uJ=@|_ zm%Uy9P%WzKXLV`owqb6>a*ftR>7-BhklaAEW+S?7=^qRyFq+ z_BWzH^zkz*cv&&6>H~}d4_a94c-oM$+gl0TU&Z}$542*I^ti(D(Z|m% zJ4$<7>J@jF@HOOiA3SQcj8ATgB{_1*AvHevb`~dgX_$~CRNOW{HX${n)vH<)UAVuf zM;|oBb%madXsi_vNz%-kUu`UZ1#|>|5>wr4dmJdDS|p@ zAJg!*tMlrL!hd^(cFL zE8L}}PjL!CSau$|Mt$CCnDlGPD&?e!t|E7Zh^%7TsA?&{_yDow8?ofpm=20=Q~pWY zd=CeJ1Fs=Y`0ZoDgq%g9k`c20#@!Jycn%z0b!T}cO=pz4;jVs#eYY#} zXIEf9-O~oHc2{$J*N|w_1#(=nnx@G_RMB7Y!56E#62|bp{2PO^UtZVGyD6mWH#^&x zy0AuDiI`?reX9mDgi<)ayH!DQt#}BSGEYoVLEEQCzuIbR7x{KdPP<{EK1^Lz>7`Du z(edyJ4y%$Tu8pP6;NclOY>O@&q=#5B`DbDho1n4c%5I)A4a4eFgo=Z*=rtyxl*SJZI;`xv=RJho8``eq{Y;;ZvU#pOjdZIQs%34(h1%Xv{ zy>r&7VJ4+KC^u8~Lspf|kic1xSlO%vwfNqy8w1Kl8%J89>;^Hx`ns+88_k;XEe0Ho zK9X%TXz^*iphYW~ZP8D6wsx--2q@%plZv$!$=h%wg(BdGf7aQR1fCgqLsfPS);H&F z;G68?oBhl1CW?JOIE|r1?LVYug7z6tINcQcg8pwfNlbx#TK_o;up7l_S+$!N+2=iU zad+jrC|0(QG{sSWCx-xJWLIRPO8iS$M&+!*zGM-(KC`>pcUH-EIDBoIlB&hP;S!6w zUC7}NqDq%z^t<@3L3LL|HNI45*EGRK=1zmip`$XVf+k<5SJr(V2&d!DX&%<~+FhIE zdw&V%Be_V#LBn1P$HdlFpCmqvm*I@~O9H8sN2e50zdY*Dj2+F-Z;?)vY2qKkfm zjKp{CU4Kp|1Ui8%M=`+9M={f%t^W+*|6(Tm2UYruzo5GKu3`0AAE}3I#dqx+Z5q_o zN<)u;l2xZ;Be$BVbGF;*e|yNN%*z>-tbRM03GDZCLD?^Fzi&z9x8I7AWQ{m1F{{mh z-F2m(XSKIP(>u&uWvw;_`g>rG)vn1}ZQ*B@yH+ny`I#DJuY=h#^G#+P}gr;65! z!TvUmI?CDC>D@?SxwUt=uTrC!7#M~7TEqSe!AA_;%+j77SlX_3@0ROxR#qEnsFj%# zFag7GrwzTx)?K#>$5k#A9fEl0bY5f3nZ*8_scQVQeb=QDbuZZA)R#oUzpx7e^|q^F zSKzlKv&=)70y;RrFM7U2+%r<9&V8vw!`DX6#@MYNu{-Pc|M7(4O;9vStR$hG?$&y}*J1Y8pBz+#4}yHvk5Ic-OJGq;{Ho@xY&DHTLUu=jFL>UA z7`r2qf9Tx|@yqyQ@;_X6d-KOje&FxPZ*qsOR_kgT^CK&C@qz#GuL}wY+^~bxVLb37 zL!2mK8e>)?)Pls-wPJ(M`W(@ zUmet;O-@Z5X5jy69s?yOHLbN*x5uh|)|H|!SKDSYU-D_M`A@JQnpQrI&-z{b(?8C= zSB>^kqf6E3lEmB5wzmf)-Z;7KjXwQ8RO1`yUmHH{V93hi37r4*^#$b}9Su9}$-?jP z1ff&&HmiefXQ5J~(d)iWffg=zL=#$M2MoQVHz@dPa0KIjz=>YBKMw&Zx@a$Zd(9rb zazj}5ZKc32OafPw_<$<`-aGsKPG0|ohu{!h^gI`dxBIreJ=pLV(C?+hJ14ij)2H9R zh0h11oHK|4rH&~4x|_l_UQ4tVb)(_mJ;A=w)Jb&V9+XIKZ2PoNVneGQmo^VdymoS8 zUA%d0p(gQa=!Lu1&{0w_=lta+Bx*WGKkDlhT}X;|L9(rF$AHAT*JOfAn|meKwe6IM zCDzBAPcIB=AIDj~)J}I7sy%(Xv3-}=KIaZmkL;GB*)ur{O8{!74T)C`K@cVn3#s|KKzH`M#_7tKrSi%JjiQzn2~S!F=qXHfl6G zm=FFrHF;ql#=& z{Z9S&PPupnySddL$hzU;8OO4n4{zi1`X56AL1)7h*wYG(uJ6OkQFU)pRdMvjCO}qu z;0H#W72#F&uj^yE%iPn;D^6`-0v*Ptgz`MqY#Mi^y7L#Y3kOSF%2_{IheRnl1~Ifw zBT;?flq@ku+0xLi#3<#NU;a}^3`mfU{If~wBVkly#j4(0kac(tWZGDZ+H-9jXl-o& zcD9X~w(Wn{HZJ0szxp<7qv};#I%8yQQZLnoiZ;G|wZ24j9C=J-wDBpGfO3QN~RhY{2-Aiw7t6yFw(FF0Frfp8alPiEze_~oYh zy$$k;W9pBH&l@l*+IXv`WO%-po*z>&s=hzZq27EwM|LVQH@On*IM<`bT!o`5s_GBr zLl8JMsBon!tQ=WY{{+vE$>Pt;_IvoO{crwdS7Q4P4X4rMvmeogwnb5w{OPQISD@d+ z%n&j(1=m^S8i>`%ECyL9DCoIPONsdoJ=b>TL?_brknoelR7&A=0y>xa`&`&{dg~^O znvqaCmGlBuEIeQTZK+!2nqGyeA6L@BX_Bxexpzlm3^!{NdZd3XwpUfc&HUVMm8`YQ zIK5oc@F;%^g9G8E>DlbBj~k zvX7;*kHcgiA(=9#Qq4>C2BZ<8xUThYOW%dU_CsaGIAyv&}y+=RwNt*O695 zTSdWgj<~)f)C4<{8gI3gfMO#zRqJ{6a5?_sCm5Xd?Y@uH=LSz;&36{Tbs^}2O zvBt6m{-eRBC*|!8zCRaa4|Y9de*FF@d)oERML)q9*b7xe$45)3c)S28ul`4xZmudJu5`AMkF@MYfi&!BziDQ`>uZ*+QYu~H`f?LAP=C1R$-(&wq z^KElxv9za?=-+%)F5>`;=egz?TX4#_zhySfe~JAY=Um#I{^N>eO-m%A1K(Xc7=d8s z>GeXIOh-1K8>~tn^pXQ1%ZS3%z_-`!Rgi36+DEdUjyz;vq9}b)lulOqmB5#oo%NV= zq_K;9^ocICgr_beNBOvdkv}xh=dmm(x?BDt8&V5^qq6{fRiGoPuJpWWOfZ>mmBQrk zNTDGVIZQvP$R75CK#8WwfuEhjp_)|n)*4ZP%1`X=aB=Yje+Fx~GpBXpk`Mklhe2QL z<_s#$`sWx{2Tbaj&x|(CJ7@ld3frIPN|exD-uh=zKaPbg0QF-e-I75NbAOTUUG-xN+%7F^qBttM7GQpL%E z&+GNn^= z+KBaD))Hd<1Cx!Q7i0ga+h>Bwo42Hvr1P(d?Lz7?cF=akJ$4PAaRoyVaYy~4=dy3(1^kw&}9 z_R0P;m(zMG*Lr^b%=r%OcacwxE+p)Gp5$!pFou)q=Gz!im?fBdvoNGkeED*XGV1nu z!uqK6(CV{?6l{yO^X&GCC@#Omi435*?%eq^ z$cKNI%b0&4*p%9TRI8^XGQz`Ku7c|S&1Cp{&xN)wS-h;I@=o>e*!VWcL@jy4b<&oL)Ae=$JU9pnbFUg}J#E|Kkb2s{dyl{tIV( zmdC>~IbMn|p-&N~PFWGnIcryq{M;`k&;A^;@N&}a`z#dl-#qg(2EYZDHS5orfe>h4 z#BSW#4Sz=D<3CH7Ld-6s#`cxF`MyW|Ndlc>YFMxy_Mq!9%?@B+U;QC9eYyBkclk+2 z{!!@GcKCm0u}-Iz(fSxwzx)1Ey$OBJAmp^mQufsEHJV7z??^tWi^RVR>j%YGJj`VQ zCpoM=rR1-c&Ultq6V#1hc(m0^UQpcF%X+7gt=qil!`H{rOwN^NYd7Wc4=Yp#|FCXR z&V2e2UHBu+Cp_rdhg<)MO>sd^^m9O&_oR%NvwF(Bc<}E|4oa0w*xW43pzOK~g>v>< zA!mIm0kdfFzDMV*x5IA%{bA(9(fs@?P%Y#h0b#xk2)e(=|w77DZ=_8Hi*$J zglMfhmQFh~I7Nv}O^M$86x^q>|C=?W zkIct;@ecVoH%>>5PVb^d`S~}`@z0YDt&t6_p>Z>9+&8I!rp)3imT}3z zcLRT$&n-KU5zALpA;LvMKBTsqi~Y?b9EnTU#Px+aSd_ueQTtak&e4TF@8{oruWR&x z7Pj}YQCSV%jTahgcI#n>zWJ{XLSMAjS-nVOABm+l8t?4B|Ne#(6gAbjS1fJ^!K!$uxm`ZtR`nOdP18bkPR@9T_u(6jx7 z8rvW1VLNx*zn#>*?+NB6Kmy$(Sfv4~S|b1b>hjODw_>Gr+O8AvzozznZSCcSzpH=w zN=(3J{mYcT{e1s2y6`=y1&lS4PZnF~PY_?nv2ymM-2Crj)7C{k?TvqeZdL z)-H$01pZn2lO9l6+m_k8mdxWt8+iter^iyKPqFy+2hoLp<=KSPfDdZfcK_sPLZe$g zE+s-p{>-YmM2ok1ZT>(G@GM1>6HMikXER{sgmmq^38{WM_vJ>7+)6!CYl*;xlrR&BG^wUDo@2Lh$-OWl94K{G0e~@xL(@PAtrEA|L(dd;{5p+B4)V z^GA%WdpsC#|L_AC%MP2_`{8GQrtw?koa}>pO;Yu3Vs#1r#Z@%eep_DUO^2zWrmgi2;=j{- z#V03U48QkANb++aKmI{{XQFuEi3gr|AgAVZB5{ghoDFrE97`-eqOcH`>;!dGIV+;Y z5FCmddFMhg2B*tE?w47NK%^#MjE96wq=&Ux81UmOc2x`N@qcxT`vlX-JFQ7Ly8WmA z;whl>1@WEe!eISs$bZ`n|CWl;fyFK5q=wYl$i23+G;f&>zeU34&v{?gOkPon6@i5V zzj6hLt*xSr{Dk$pI@*`{KmIm|_guf4pMQ(tNgf^2%$4AiQS=JgDoay5=}{b)_QBlq zywp4nm?V6BMtVT++9#11Gp-XTSNL*m)y}#5ddXp@^Nv-~`oTsP9^o|uW zv$k_COD062O)OKG``7kCR$*sl?JtVkrGB6La4*u4dmBO?LQ9k7ziP0#7FXeB*;VrPxF?`Ae0=4Y$-2Z}nL z(wUU2BYggSEd}AU-DKNX8$8!O|61*cEwN;4?I&AeBi5+5s`OT5d91idg;)5IR%kxo z?u~ffE2LDbcvT}VJ2&i!2(9>s)8U4!Xm|L}wlUZNkJTW()LO2kDd^DOyic}zBmOFJ zs6blQvZk@a8}YXszO%=8>HTRd_9|jEMH{33l-++c|Kww5n?$Je>>a^_hU_U+l{QWwf7vKn% z1&}V1LBo{?NE0|pSQp=`V`HcoZzo9{r*wAD7GGL18Ob;wPISsd_$nX#U+h0Gd|b9O z%Q*j=wDD#9=hJ)G4sg=6a5y)Hv6)mBBPAW=%A3Fk0#;#%CmI&M3$9MA9Ka=`P zg}<9S_g}`?rPS{uQvSPlnUVfy__W&!wK(W^4qs<|-^9$zzghAd?C<#u!ZGZxJ{+>Y z2k?#HTP*wg&q-+6%siyX3*QgA!k6_wOZ+n&Y=`!Mt^UCdEx6$zn`Q-^t<;?qs92ALbNDVgPZ87WRM*F?o*0=3Ie5T7IVJi%CMf@7`gMozlf@zU`aAf3^rsMf2MfNDUEu2-z_$X{`g7bj zH^)QY1n{l6Uhob5{{`PS!|?s!S4O`NSBKg%_--Ooq?3MAf9vR{_>cdR`{v^Xp>G2C zh8ukK=YI+MMHlKMF=TM2bY!}0#Pn?cL=WiV0~6^Qhpri+!3EQEFTUv<)QQ%0HEQvF zBQ#YlJ8>p1Qhn(mwe)8jXVNckoEJ+MZJdynq$cUPkU>>UT&?ZUUuc!$guicRX(|JLe;CYj)=p%@_TqAm6T}s>`uDHVr zJ*eJ6uMk~?7Y1)z`x2Mv?Nv8$7frY@^U%y+i&(;4b2-j$_EP>ycQSvx#BY?L+rQv*zqD9>oerwgmW20s8WB?ObGR^7g<22836S4s&~@B z>`6%EjCR+!+MUhdV&Ia$xi;IcCjwqj3LLCBSag-qGh)$_LkeCaKW{ouy!1FP(~pZM zP7y+upS7Y6|4!BWn{=fY{%>?8oMm6R=6u<+p?FIk|2jYYb$kf=)rJ0w5nfn4ay=vX z-m&B>jb!MLS1CE;qf|aUij`I4Ka!k(Bsu?RUhSrLCseJx_Sy;Qi5*--6B-MjnYyTL z$2Zbr+VF9Au>Z^GA&~NnZ>$~PwgZUKqwycjQ{hGHb(cK%qnVn*IHa*KQGJq??H5a4 zP;N)@dg&|6W68hzbD_5pD3*LRGX?sbN{;t+JS2YqJL@&2GH?n{%;JC#9^k}O^!Z!B zRgK;?*ODH3o8G4OXLFiojuUX~SuhC!bVJn*67$PCzu)@06LdGy^V0L!e~|fEZEj_*8=eNaFTf`g!S*FsQi;%H2Wp}&*;LhFzJDOc!Hru`ia@ijSsND$|W-! zji-+pxv<=;x}Z4Pc$>r|#T=AObP&goZbkgZ*lbzkveWPyFSc?qY5iIJXJo-SAB6F* zD)*8M+dh>*@cb&qI?>IbY}+DV0#qvKQXbOgm;3MPyXw7E6ov5z1VgvO#HIFSvzBjrXl`2uGR*9fB3Wk-zM~79Mb2*LFOI{*w;Z8TjkAKRzDe@zr&Ah*J8q)VfL#ZvJsM&4bannV8c?|s zE5~EoUA$C}Dj>uH=@0!LV`yzzSK~KdB4Ny)|L0dtfMVxTuq))BWzh0kahcU|ki2+{y zZotbQKbHSqBlzfm^e3dd!$d8ku(YIt@j=1?gk(E_KuiO9K*Iz9pI_DVua02djsF10 ztqQo#`0B220N$7L-*fpZ69prlj*G7vB>W5!)o1k5JHUn0asC*)d zm|;xWY`*Q?A}_fS3v>-jDOY-l*P`_1{b%=M5BTQs1BSJ4O|*W~E;dU;pgNR8XNsjq zrcb)?ulk9D+I4{beWyc@X*qi2h3^F5gLj?%S0o)?=t-@ve$m~(ilc)iQm!xazd`-& zzs(UKZ~Ypd7kkMgEWb@ymyn%X-_J|&B%vg?HKRE%Y@5ZGcGXhfmmHrT+YNr{kM(H- zT$Br2%1q%e**YZz_=f}_+8@dR9$e&uKf3T%QJ{+rNsS{#0VV{an-c%co2fk=dF|&R z3u5Eu{=ZPOKp=iGfBQ2lw`^jlT(S7}#?M_juE4+I z8rk*3K_jE3pALb1@XwjtgFbL0fT;US`jS<&`O>-&%-z!`uB1eWj`yAh9h`Ooi5t7i4>}Y0A&cUJ=nuTb*0pPxgvmo$A%>icV)bwSi)S zUnGhyzx3u1)Apkh{5vv33;~IQeRL&AuUI>agY52M;Ru?XY_r&>sK`|39 z)1Tq4Xj{0b)p#96@h`VJ$QmbyS{$IN4!R0);{B<})AsIeZ;nLy=`&2OT=zbVUJL&~ zK0aLocqm`kBC_)KQWf$H=rokZOo!m#$r>Xl(f;cYTqfolgpfEcf^k03?a1f$AG>Fc zI^}xwX7I=;;Sc2zwM{)WRGX-P{sq;Q8~y*``u*YIaz@y5gJOUQo7T8uGs4A;s7F%F zZ+4(R7M}94{F3M9Nkfykm6_E6MZX070vm>V!UB~_2^i#>G&AU9jebxzOTVcthVgyj zDIKHV^&$oIGr}N$w*m1o2?5U>5qv$mGk9cV_Fv=?^gDQb5BiDz`qYvB0atG8DWP&k zzac>}N56%x*b&{U^F@E(bD-Bx4^?9HyK%1QSIZds3+Q(@tnHznAS|o{{{&KLHUQc4 zm$Tl5fP>9umo^uYu{k+jJ?mDQ#6OwnXoY`L+J1ZsQHR03+}Kn-EzOX>el?95L2*o| z_NTwiSZdG$E3`I7!gHMoIIHoos{Pc9eKOCq6n zsinXc`ae0xH5)iC#WdM|w^QMK@;7H3^&<))|JZ1!&MnDo@;$%KrtE<*1eK*rvIPR8 zMsO>|q!PZrb1aSgGOvFXU1*boJ?5{nEBE9tiaBnDjJQ@Tr(A+gLv?U$A;BpiFFK@L zj84VH2SmTZjkU;W);(m9wF#m_KmMawh`}xrgW+c?iinT}N6tlSm2;ZNHXo}YU{pZS|FmN~uuWXSl1`3-@7Vq4@l(uboqZVaX* z4ax87XI$8)IOUZvnYv%c`lP4zLwZ-d(S>xJ$Jv8IsoDzh9l@LkTW%`H{_vfr1tI!+ z!*i}?e1~T1rzFznZOT4e8`neqYhNP&Ve3g>T0f%;Z{%}iWV-O1BK)S%UZuIsG9Q#pf*{ zoHJBsGIHlYeP$&d8R%vrE}a zT-l$vvVC3Iifq}rUCLIfZ2iU5`SLDjhmTpb#A_PTO0UOl-~$bC(aqh;2gYOHT^E3}Sy3Tk$ONHJcz zu9fnd_Pzc*^1=U@wRY6c^Xy1CUcCzx3+l(J{uJW(j zr>kUI*h8U_EM)mfbeg4!>cK*ET1!2fQ1tbZXYxfBbl+z(&`Yc>HveENGr#R`_#dAD zXILQHJ;;?8KKTcby|e!ovy@HLW&Arfm~+Fth{UvC>bmY`+WK7o;DT=Trz4NG_l0^` z*8!FXCYn;I;R<_c7Np@KTrj@*hYZjn4)Yp=9G#%;0c!jD9DDN9U-5$y;a4o%YQzQ! zTEC&o)*(i}6;)KY`}l!?pn`z)o$Y3?g7?m=&T|Tfd#`b2u97d7pYHJ$V1QlHl0d5r zCFq5I`M00q*1H7pmG(;T`TY1wbm0-s{&vx{`8ZTTJ;cZlKpPchp454)DwKg-l3bk#G5<3s;4;~n6+n&>in;r>9`vd z%_^W;o1|{6#Bcfz318KF2_!cY(nD)|YLXhC7>HF65H2OYHyNe=Z%sl>YEK1-*`x*! z6yMgtm>Av{?#kKL?^pLXc02u=JXK?gTaHJ)9ts=2KmhXU7p`NE_(PT|FcPZxca3*k z3jz$sQGfd*S)mI3=B4MNdU;hzu&w>+P(mYJc0maWqu;K^G&F7e1{|pxz@aewU~| zZ9lP*)OFXJz49@=UW%8A)x|CBkW;3@xOK4*P`+b@eYN~Pmj|8U^W zVe0)f>TTMrspJAL8hak!Y5oNOxqmPeSYj_ylPgk>Y~wPu;$!}fO>S#@d(haC+mint zKXUC@W_-tv+>!jG?fsJRBiE#hj;|%k`cv_R;FoR@xG*I(?6sE2p-TIKISHD9>?qNjc%3Y$KK?&7A` z-?{v;c8F|!TST||)3P+{W48yAHZXptm@&Y`oBd@);)v=>5N-t2yTj>J zhYE*BrWp?R{w{>WVR~`9d(p8p^x|N>Xy66$xnsa=_wfVCY_Y-pb+@LP^PonHx+QM^ zApo95qC}372n=>FUv{xpn)9#uW$9yo-7)kS8#1#TY1&~8 zA3gAV5IpZSJmm*e0vQ^WbJ=&(&>%<4{zl6Tn>==?E&9ZOobRZa5+i4SBWI?FG9%ZM zAE`~R)ZA&6Zh~_P=BTDR1jSjd`teAwU*f_ zN~~kBXc0ev4RW&6kG~z%IsGW!7Z*{Vf8vdVmo{h1H03R`kILNTrR{8QLGGU#fveT; z4_3#@nMrbn*2WD$>dagK^WDSray?{>|EQ)p$Le`{ES*19^s*}Mvnsw4R8iwm8hI4I zMF8ZdcV}JmypikuA2MCd87%3@=wE~kj^Joj2Q{CtJ=RInoVrs&(p({~M`}I-)AZuH@7#D7Uq+C*1Dk&@l+M=#8L$9L4 zolTiW^t~!?X7sxEEqEYl>LfV^9udCjAyLm}cGfoNTK}v=d$BLkMdkHGn_Be@3hQ%S z>tDS3;5gyrXyebh5#@)qe-$x{tZ8>$wxmVn{7b)^{aA&dkM+dbkjllBtIiryx5j^m z=_)X>QB?_>LA6?~V=FqEWqsk@gDV~Hls`o$5`a@PlegRmmE=+`JH`%K6IY+l!>m)t zl8g37;?=Vk052z>8zK&QeIY-4nC%k%d)=iYjjLTt{ztiXy3}vXGLuC_99m(VeUHrI z9w2oj1$(d{tnM^=BwHdMz!$GR{gaaHSb=WFj>Rh~Q-0L=fb#WkwE)jW0p|WHdewH8 z2o-w`ZA(drAp%&^{&4E-Ayqe7aK7ne1RweLOlKlLxIZNAM*@j|(6^CL`ET7Mrlz99 zb_&zh8A53)Rl~keOe`#edA~O{xexNb|39ohpC%@D6kQX=a+Vw0ollaeetp1@g2MTJ zD$@4Pl7ilmKk~PuwWTVuhUfD)<%2Uz0rb)T_|$+6byl-wXSkZXH54X}(@!KgJCY}W z{uR;w?y(RSk#rLl-9G7Z3tR&NjZX4qfoJhY?eN=G^CfSUq)+sY-e`*hG2zGA8uNJd zUz#w~b^9mQ748u!1X;Yd%{*FUI5=60S6{n_Oz~EBJfx2LGo-^6(;BKSo`IKhWv?0a zeV6y4_4bFoWY*{F^^6Nlme`i>by1RbtJc*Qx4qUYv94EpbFjZ|<{GoV)YiV5s*BiW zR8==rU3U3p->rXmWUE^sOBQGS0l}N8vo4-p|C9KYMbqnVTD}LZW-3)jVn-jv&FtgZ z-@oj8^#i6gTv;^ZyV1rWyl$wPedX-BXwyYp>-l#tb^c$p>2#jx+?s-n_I9=k%%Aid z${F!zmSin`7qaJWde9%ke2+Uc@*ey?4%IS~l)T>^U6KxdQ;|F0Cg)G~t(B}ml|)k& zu-93I?C!r#yhN|2%iK=k2*Bq#gVIZK2+h6!qYfEcD^RD%|64d(-*YrTt3FCKx{ujT zHT5H_2AefqM>?{Et8LfGL@QX}N6PaZwhw!vx>>A^HvI(y!f;Xxp|v4)w08W;@VQrOMa74mX10+-c{IO6-Fg_QqsWbYxIbvMM4JTL8Ob9y#L{P|qyV3gdPIOvNG;LJ303Q6Rg)b& zCzh=Hgj1g``h>&j_WvYNeYt9=C!P0*PwenjFZu6S@^7`t?Xl$h{+;8MDf@|~pT}VD zLsY2D_IcEzgBsi(`8s5=Mp^G9MCTsriXjrW+3)>jOYK)-rgY@Co3n5;M`EX91$q3J0QZtB1-H$`8{Fja$I{Ag z?GASc-p=ztHRppE19#9gpsVSO4q*@xUo$us+xbK>?oIB1*94E#1jWD@lBHcTJwDz@ zd9Pvh7JgP){By}TYMV#qrT?(54&aL8^wwioSfAX1=eXb3H9K@O9$toxT>hi@GKREk zY$g~#h;!--{XW;qk^~NUXk_IdNi*&D<*Y~k@!y;;tbfgd-PHfg!T%@qzkS_q>c8~= zsD4(p<)zQS|C{>np1+&=A2{egpigw+97$7``FTb~>Bx=m1`(#OK7VzRfeIjf z`R@CF&VBb%MhFQ``|!-6SrB7@P@G(cfA~$hN3p7g^GTuIAN_l6+N7u~QGKRWeKHex z3#rGyDfeQuz1W)<^}JXcnap3R>Bzh!txi{e`~PF_OrWDE*1w$q&A>^Z(!JzAyK`_mJ~%*?V?UMjRn~ zP9|wozp}r6-1(iLvyHBX+@u4=pZSrYtH#I9ak@xOW5#|uQ$Foz{YP#S`I~t*Ej=Y# zX-yfOwIA%b?F^mR`MCXTG=s+tf1~v!a@JD#aX-hnj||BQy3sryW!SHtT~CPBTFlhM z`jT-?g$gAjO(S} z7Sq2_A*lG*XAOv#iRAWY*BEP8bcwOH2i8^;%0TVnOm1G3E&KATge*RU2&YauEyliC z_J8G>bkaXiGun*b)O1(6i$+JIUvcB7!Tg#%YY;J^{w5I-ZI_$+WvG1G_5OP}b?TEb zA=cQX=C23P(Q4vfyzu$TUeS9>qXqOFy!Gd1N=TYqIEM&TOdI*tzAPL?xRqRGc%(LY zr`?!oY*25jl}MLXOE3muO=X1nO2paPYA}ytQ~SrJ=WVRfs*^WLxU^DLR$1zn7F4;D zG&1v^sE(bfyk2f+AE2y#c>VZ@C*Qb#G(UKP36eL$Xn&|h#yg!K?6164zkEj(e$9EP zw;(g^k)Ocao4v9V=lN!RV!nnmO=q3F6MmV4HiOkxie0-}FthO)CQ966Vgv1e8M?{!n)vQv8d0wldXC<9|j9xI`p*p9Pvah%E zYrUGJZ=y2$w(iO%gr^L@q%Mu`v?oO2)cr@r6dUa+Hg4*#5-_;jdGj2-DO1}ySBsEmoQ4pjg2hj4t@3!oNQ)8LcLVd+(f&+u}yz=sfHN8 z-%5HaeapDpDzpq0PUQq`4Mfh@h5FPjow8L2`gLpahzdCk8boF*j8Piu+SQ&}5$&?k zn(W?|?6hT^R^`$PMK=rX~+62AJi~GC{da8 z8Kb`qOXKUWeRgC06*Sb}U`KyvxKjzS`de|_f2+Tds15y%@qSF;N4zBSf0GdRH&e+w zO^DQnsLK=x)QtK~sX#U_h$h45mo!$R{l!rYbQmgM#j9Qd%y)+A#*pGCII2uZj7^I6 z-QAkd;!e|FX;ul^(m$)(8B+0nmz$@%Pmznw0V}&pkrywHqsUv2+_563WtB*%$T8FI zVU>x@+vMr_M%KjbHqK{ZHwuGrh1(GrwvD+p3f3jqcK} z$gsa^@2rG#1|J$K@X~y@MbzKaMq7EPSN*^1HzyJLU!(FWfnwcAJd=2oU`@Qta^#xkSlkMONco%?4S{m%Ki zLLHR!YqJWAbwUlRHTo1W}UC_hz-8c);cS3 zf!YG?{@p^3t!RnM`pfxxK`fTFsFL0Mv{jDfpN-|5gqHJb7nu-@@GVQiMp|u!-fwk+ z8?ps@l&-&FY3uY@R@Z)s^(A0M{9kLA%B{VtB;#WJ$@zx;umYxnuXxgPMBxw;{A z+>#%e`WuO7g=wqAiN?!Sdap)*zw2)s9{vD+1sg%#u0LD*liXOVzaJIAgJJ?W`a1RgYz*tzz&(p>8a6fayG`uIjC04p69WWvGO z@RGC&E~C;GaX|yRpn^WWwH06iSOQjnAHc6*Bd8m3ij+}dxWI6U;R?ea41YD;X!t*2 zf9+g(wf1AM415dbfDb{X!mTtXmAmPraWuUCyOVC-ajpl`1}Q@-!U;(H zlkwmR*%D46U&J_Ve#UWM zE!R{*aoS!+?rVwrF1xS!>&?txkMnEpdaA-|X>zlKYuLTIeje6X zCfCB`xx80p`!#@D`>+*$#~e9M7(PPA>R1^Y$rmoonxM2234)o}QxKTwBM9cYqaYXy zljrhYm91?L^&J~QU`ilP5aRmJEQP0nG-^KlH+=>7xrTuKi3`lj|KU!i{#g^k}E%tchUdNvHnW}BR^OC=ek4xu^|6-k^E8rdAtk$ zhptJtCU56-kixSpC59Oc)pl8D_2{h89g`^8-@}B6D4 z4#S59g-@3%`^waa{&>6_!x!+vS0Z-_A6LT1b%)`@g2JaumHna8xf{Pc-i_f)PIS|s zN*EJ9u7r>44#S59g-@3%`|xP^c)YXlS;5U#aD#dohgpFi)-k}4&NoK-D5OUxdVt~S z+FnZ8HF$xZU!W1ij#lupG}9IEJsO-B*ql&!?Bs2IFTJ`*2lQ^e{OU{dblmNhs|zn3 zRkYW<)F*#zlaM$s^${+!RX`@_oz(TZ(-gv^eKhK%*`w~7B)DgTyr`Zd3mS!}4u= zJ{uT&jf}~TA=_0C_FLg$Ox+L8v4VcJ3Px&F`F{81Y%6rEl{-(H3WZZ=U#=O3RaTH$ zL#cd3P62Zmp_A*rV0F({W7?FQO+EJ%)kOFPXItHS7Zl!Lh1%$J6MZw%Jy0&si>_5- zdWPyfIL2=UhereM>1c@7zSQco#(f?uQ~U+BPGB2rLB52HyP70$WRr{4{v3X5 zftz`4jYOQq$GwX_PWA#FbB%GdzUqv!idxtYN4t}Y>+jby4Fxg~Qv5&fxhks?}scyl{E$=o^}dGq8q?}>_@1oLFNQlVa%yoL6SdD2e> zl=DPC%#+QLC#u8ElP6UPICfNrnkQ$g+H#&`x=#*L)$csY8X#-z`nQi56EJ8oEmQ75>E@hcAW-pH?iygzvszobWjv8oNf#V<&v3U-J*c=k;%V zWlDsqmUr04xxxy)o;5@ELetC&NjKbgnPrl0(4B>zENP6f7@D5-JkH08vA7m78uXDm zNEm00mHH?Yk+cYcsJt|O{D-6cqlgGQ>LVEuGL_vWLT6nfBAC5D9PQub^o}G#j75oH zKC_G!VJ}w%iVs>ex>VWcZj%VwZb#K^w43FL&?tQnez$)W4Zq4B)1u-^i;An8sGacZ z0pU0HOwze4E5leasYl^AHwu5$BH=goV^Y%TKRZ!iJW%*$US;2}zD6|syb==yal`NS z&!XX10cpa|mGE<|7k)h;{KlRaeq+fCzqwKPqZSFju^$uuTWj3#W1#TsQe}U+B{uw+ z>@56l|1KJS`ewAaxe|V^^}??Qgx}cn!fz~j;Wsx5f7Bx3H}+$~|B3B}9|MJ7mnwU| zdf~@pN8yk5zkH?ll3$dd>h%$$QmLReQgKBpuJxqS1EezcJgJN&PbzaGsiGE<%Gi&Q zYV#UbDhwo*E>(7KDY@n(FIQj^Bh?Pd&mGHmFeCCMh&}li`in+BuE@u=o_uO6{Zyw^kOky3_B{Ex ziIi&QM)E~1BA>AzBVW!>Zv0^&`E;qW0~=%I!(?~J7l@Uw#FdXL@yE4Z{OJMm8GD|5 z#*!C*=0@^GEh3+>A0yx3A6@w{kbJsmf3iPg<-=rm$frJXG<~X;F!9F~`MB1TPY;mK z*z@EwmOS~)jpU13L_T9bM!pF@xbk5j`E;qWtNw_U50l*@Us9}mNv?dl8u@gsFJF=) zANC^hVJRXXH^>*UXyn6wjC}X4_T-ZR@+C>W)#{Q*%NIS`J@RR@|48|wYRrj0U5$LY z)|W5Ekq>(j`LGm`j~nERSTyosKSsXU-+S`O0QpiRUvhoU5$LY)|W5Skq>(j`LGm`j~nERSTyosKSsWch_z0mQbay(kS}7<$cOzH`3A4_TW35hZJ zPlrI8{HLptPuKeLB_x{q4|@^$uoRJx8{~^vH1c6TM!pH(c=E{r`4SRU|EW_SQ$NUL z_sEwNE1xz!rF=<9a^=&}8Rwb_NvC}#14lmDi^?ZUQTcRZ9*+^Led z52M|G&F`@9tcE<5L@(ZKg+^)hMc|06R8A>fEql)Nf-w(%A1Tc8Lz9)p6Y`ke8`obS zQRHd=_|Tg?vvRi=UYr*iqp{KLg+uh*zW)n}7oGgk*s%sunM-8hNvK#m7=FR;&YbcZ z{IYIQ#1}?nWn+Arb$B7hj{CtsGzqDFusmn9ushp$%*RNc8_wCWuGJWE8~toBlC9C% zY%AzY>1lq1i4tj8(Z$?~ep&p|v|=i?4g=fXsC6TU-3M|%BRW495sej&37RO;_FUea zSzucNn^+Fb&I>ND)%2Y9vc^t0b&GaguUn*bnpu(8y?11rdA8sPjk6zXg_zP|o`>7l z%6Z#U`y@R%Ut(OKC^m;S^I24vRaTd3EBF_?0Hj$#?d@=S;?_Do{a1PHScvh_ol9C{ zC01J@xf8@a+^sin<#|XD%#>3OiNs6;F1{W)P?_^}rqK|=(Jmv~5{mY^R5BmZ8KL!0 z`<(U8w!;3wmr5k_G#V6VuF6T-A1BvtZ%X$(kG|z3Su1yV+W1ua0|v&rXX@A6`n7{h zQqm)(T9ea7slFHO+GO90>wI0U%I|ex_ac)zs!Dp)%+u6Lp|%Y{ZJIw&>d-cOHOZeV z|E?Uobw?k6Dtsk9o-yy6-5d2jdx9#`wWWJazXY)bfKij<>r0GzWn!A+n#Vx zLK3SmQ_oxIEVE6$#QSlj`LT>FRP+9jodr+f+rzluE!KU@Cl3;h2Y)SaJxKOEXc6PV z%AI9?F7zcP@#`#xs&e}Q zj@kCCH@a$#S_%1c0`uo)?~IZ8x*el+(MOnM*FW?E>9K$4s%-t7VSc`0wbPUExb*%( z=V$jf3iKo|c3T?ik z%w#(bm0tSdp4^zjE*H+e9qj2*z&0L(^HoR=w@-VCt>5K{liqN|Bs!Jjs!MT1e!2U1 z$UvKMxO;OHus4TxyO1L_7bwv?s<3Xiw=f-YdlPxUy2Jhw^E;X{ztesb^E;a|zsJ5G z^Lv^w&nvgDlX+fwQ|7nZ=VIPu_$L2CoKR2S$2II+(N!ipq3HZWqti>!sZQE9d!e9R z>6EWV(J8tgiS89u+QWp;6H0JP8^kz;=(Mb<+`FHH5MLSWFEYbC)K{D14|P6!NMpT< z=F*+|L$OOdS68S(mhe%{=tP{P_Rv@AH&NYf0qRtPy7ujQA5 z$ZFS8CnnWeGcl={Ey1QLp9$obG31<)W(Jt*&+G}K{Nm;uE5H2JxGIP-AMk5j zbA87_icPOszZ3J@wQR}rHgctea^LT|cE{$6-^G)6fq6 zz1LXQZMxTIVHN74Y2@b?$DYPWLj0P%twdFquZ)*;LmR|7d#0qvsqd@T#uJn@Ppxcx5PM6?OcppW0%RS zPsde;6^1hmOFq~QO)=1 zSYgZ;8SdC_yjbefvBbPtZaBm6Wy5&xHcXHW?%niRNH@mp1EdtXiKqyWLRUGzQhYbn z=$Eg|?A4+JGJH4v#C_?nj=9q5&(5z*U)g%+m)}?R7r(qTx_@jMy+k(~Q;(WO8lBQU zS=Ms5Yu6v85Vd!GWhrWrmGmfkhmL>i;JQ6FKckhS35CrxJhkC*>HkZsvL;#kST^5a zJyDaV^koItTZvy=sXYg*~uTbkXH ze_)*=Jf8>;HB$_a2sOLq^d!ao)H9OwB$R*PUyA>H;y;j4CIt6lEn!D0+SLt)>Xmk9%ggPaZafn4UaY;XZlM6k~ca zbCvt#Jqj{~nHV%N6jg9sshk2+p}o!ZuK3;=j#I_|alI>ks=|HplIvaZtX1xl_t)u% zTX0+9nWo$fzi*21FdOF8T*XSm;1;XPdMo!!-%ZcEc}NrNzCac4hFcz(7u+i4TwWqv zQ?4FY3C}4JRWj0$OZTTinygW&&C~z{q9-T702o3vQM?N9k`|y2>S8CuJhZsoDAAL8}xa z+??)*0}JxB;TXTp9}Y*1$?wYgYz2J8s{Af16+!^e;# zaYS16Yg7p3YD{^D0a-&p))0_21Y}L4fV}3a*G)RR9u`xRonr-moUX~fF=P!!AtL={ zI<4yZ7t-O^>;}V48r#!B7YspZ9BP=h%+;`-#-T)T*Lqr(k+Zck z1m$ZzJ!=MXw|?Ag^y_Q=d{%|If8Fdj>udc^)+*ii@wF~BTh!{uH1nydemJYE^<%nO zUacRGXR)P~emtM$cYeH{Rp9)1Cu_KV*iY>JcU>P}x^t%=o_V_WM!M%ly60-T=TExl zbGj22oFQnRNH2$K288oJrg0>q*iyGn7 zzzmZ%vZtX>v~AiEGBpFEYWpfuCeI~)9y+gaYgywn1&q(=s~DhPimqYK*i1H&nZw|T zUmuDcv{$OiyH=+uWbUVE-u#a|l-DK@<{T4aAJ+ofkFY^|r{S$tT`dRY;yiN+r-tUM z;B~&e!0vPfP{WdZL#-rY0_Z1zN=28Do<@G!PrLh8^Msc~6;#5l?B?+L`J;*xMHRyCj|S%^1H z8jV8PNGdA@(%_hfcBsMueDlYe3I#&GoOnpKoUiUX(gb|+FD1GLwxmpItzAYYC4HQ% zdD_3!4rBT?w3ihcn3f+pH9bFcdd3LLcXcILI3mw?^&+6DNF^LxIG1k;wR1+#%!n7E zzLLIYXQm-FS68XUo>ELpjmoE9-!he%L2WnBeR!WCFY9wgDLSoBICaVMZ4xL5w83|N zR_DBG#R1z#GijvJ%S`U%!aB!=z})OA=C4wWOddvC9+`Ap?t!?eXc z=51vDdei3DVSZOr=H{d^4c{s0k6EOt2MJ}<8dlU07|Or11`fKkuA7tUg5Rq!T%JtY z%5sKexv`718jqc>n9ghI+FUK06HF?6#0VZdIPwmU?VjeH=~<4QVM-xkl`JK$qP8XX!XLhOK(dtK3{PD&}c!xyK+Dy`oxQ}k>bS^84^#C z*CSJsbXgSr_B2(lZCbBQ<<8nu#X+b~aXrmW7&%B!A&P5!yzG5J>ta6fMDtV0U&nfuNQDoa)>S4X}(4y9I< zY8z>}bBWY#GAH@Veb*eLs`r$Ax1^Z@K-My=G=KWh)0!VHQeLP|OW0l<>YEbk7&;?O zM^XG?ReqOg?eV2`<$_C0P{rX=GCZkJwOf&CKjQjp7y}#VLvO@1u-d0z)ZRCYx z<^6<`lRjVh%0r1(m(7m-N^ADfr(J&4P4BJR+uHP)0n8Z_dU)J4VmE6~A zZIYE%=w{Qru++WSZ8$4un*Jpe@!pNZN2pYPC>!`-VM%w%L zW5egus)gKMX>Op(iOY@A;57ObFMfSxl+bbVU&vCGqHK`cFRSQU9W!atPv^pTSx)Jl z){Bm+iX`_E)ZJ3zILSRlA)8O|-K2#7^U&lVkKi7+&2lc|e_H!y!Yfd$J4`Jek ze^=9I+jQdJbH9Opd&{4u(&sM5UlszlZ^Hf674h9~FyHGeM=nO~T9~|~P=_EUX_Y|H zVY5vmD%@&vH9J$&>QK7Lrr2|4fqfm(wukAQnRT`b4jI~bSO~I@({s?pEY?TR1c%O>X2%;CH zFYGa1P?YVu7xCS+&R=+}QIf9rpi%h#LHT@~0%;GO<0aU}$=ofURpe2&JLOAE`IpK2 z?e)G_zN~$(QTg)Ugil*6sQjy+-eR*G53zzvwdzgjtFSdSc)Ci`Wrbg9-%d*r%-aF= zRrF#Tr$MT>qwX0OPNrFr>A0aba_B^rzBoez*r~DA0=8eO@N_Y&f!o~5P0uU)(sxPS z3ZL20yk3f_e-kf${-OMP{MYjDapNw^|Nbt^|IRMTPdvo5AEbI6rT$;mFXPlNZ`ig| z$z`db#f}fT{W9WIDP%FaXr|@FG9A0}Jul^cX5X!4n6-D-k2-S~FaE}z(b)eSRqKc0 ze|Ap4!{T}0R^EFV=McQ_732NBcz;(M@8i}_zA{!NHVy+iAbXIJ_P0XWv`?jtw8Bei z*%l6UR5V0Me~e1N_*&|wV6q}sHG-OVew}~%uA3$bbvl6=UdmHJYZ}r0GSy0`eOJ}$ zoBDf0owS&j?Thqcr1`6$DOfe*RO;JZ-cl)+)l~UU`Fedk*@!?BfJlR&HNEt0?|5mp ziSjkoZ1iB>rigKGy{KTX9DA1BaLTav?9iJE-3T{y|6TjQ)Zarrw;Qw%4wgSnwGRsG z%P-|>VYXS>+cQy1^pbAuLt0YTxMhhG72ysGEk?l15LUS5heV%q{)ydPqNu^7^N-(A zA*KWo>DX{<%Ct-spPj4h+h$6gJ`VVo#x5M<&qfBn!*vm;Fm8)E>hC4`KUJ6NEZggkz zH|Iz1TKc$VW}Ng9H+GH@tapw(CuU{ClyyYAXNkqWc|izk3tzCp+$wHxz{`jn%x-XLxGa z1h)Dq0B8}VTA^+#&QENu^#eu!Qu9bnn$SeGk#yUhTNzGlt-^kLQOi(aQby?P78zGkluYbkr=0`Y-xMCW+Ql zKCQ>^)B-iFN7>0wi)}qBe(Y;zdXl~_&BnG&xs7Q(HZZj_5bN03RO@L!e*NPsdwEyn zQ>RZ&BGvOnMf%lQzd9FPDM`IXa;&5hGr7K)3a^rtBPR7a%eZJ1*tfjliMf+Tazin@ zzA7<~Xdq_1^z{$h$0`4H`#5D6+sC)PzKf#Ur@XR@?c?1bf9iiNf9kHv|Jp9g|MGt! z|1OWeP(EyXHGcVA^-|O2^U*3BRL+djpo03IDkC-f;MUZcre?H1Fb7Fs7MPn+WiNYC zj=dhumC;}E<8OI1YeQOF`x*OpRnkkC3V7@d> zT%F%_!!Ea9UU@;mYS_Mw7d~GZO{m6g7DP zRAy-8(BAf($g{2J+qd_}v@xCIPVdIJzX5}4#Qt({DJU#%wMWo2`7=vp6^TWGc# zZI}twD*Lr|XyeysrHzrUHZ0mFQpi*k5{RaW*kAGV<170nZu*uWYCf8-eGA`|SFsZ* zpB^%IYA?l>PnVeT>D+%@KJ|SeQa(jT6_mT2QH5+q72a1ydAn;xdF{*w<H=2U>tskN~!=6gGnO;1{q4d=I_>%fV9c30MH;g7?8&;0^FHcn&-X z9s$$Az2FWI22()*Oaj+|LNFR!0WJl@!B8*|3;_9{FX#n&fE;ivI0|$DS)e1>AM69t z!JeQQNCbaFz#&IJ8|1$u*%KrZM8jt2h%hk#7b9_$NRgO;EURqJwOgP790h-fGp4v z><{(<>0nRL3?zcT^y#*}3H%Oz1vXd>R)R0VXW&z?5PSeCz}sLZcm+HUo&t}8hroT{ zPH-y-f>KZnt_MY647d`E02hK`U=TPH^amE`4Nd~Npc^UU=>&amVw3KBTxwcsi+5?l<< z2j_yb!5N?*I0dkOg54eb8*~LnfWyE+-~f;T+JL>l9v}tOeJ*STe}Z+O7W@Rh17CwL zz!LB=m=CJJd*Ds*I(P{@3!VToz=Pl(a66a=ZU#4j8^HuH7F-Q32bX{gzz}c_C;+E{ zQ$bH~A~+r#1C9ip!NK4_upej(e4qtL1qooAcA2p^g7x4Rum*e&z5&a@Qt$~_0Oo@C z!CT-B@G^J~JP95F)4{#q4iE-YK>$nw*MUMX8e9P`1;fEmFc1s?`JgZ81$uxSa4a|q zbOBkQBiJA81Jc2spczO6e=QX@f#1Qezy_SRzzn2J`!3OXfr~yBKZ^2jKbFc_}2?3VQ@dV3)}{70aHK; zxB-j<*ML#rGH?+%4-5upfzv@A=mSm$CxGKXHaHv{3Oa!fpdHv7v;t`$8EjuHYyp3O zwcuy)BUlAifMsAY_y|;kIpAF|8@vWy1kZrS!GFL5;BHV3LZA#x1{1+}a4onBj06{h z^TD~`Y;XqX2TlQg&>j36bOlF%!@xn{0FVLNfW5#TAO+Md61IXr!8%Y2egfZtufZ2! z3HTVy2UXxb@FsX2yab*FPkVIx=%egSL1_uw0_94rN&fCXSKcptn4 z-T*Iy=fIQT5ilLx3+@16Fck#AByb%l1f#(f;8HLg3r~q$+ncx-hJa`H`3LXOY zfjhyiAP7o9F}NNSfid7pFalf%hJiugOwb=#pf@-Pn1XbGBw zB=Gmg!e+1m{03^k58zwy75E%10w02Tpc1?TW`S413*c$+7 zGx!m#0xQ5Wuo!#1}1}vU_7`MTm?pgi^2Ke zTyQox1M~x@06*vs{tdcOK^Q!67z?fjmxD{d1z-p`2NZzQz^R}o zI1wBVjsZu4&fs8hAlMJI1wPOMq=E#nZK1FctOvhuj4itjX;0kal7!HPlfnWg02Yo>=&;#UvW5H3N3&;W; z!Tw+$kPh|)%|IgfYk{x{{0@EvHdqZ-f-k{m;8U;=d;luI+h8Vm1w0R)0*``+zz|-I{@G!U^+y!m}w}2_21l$0| zfos4ha2dD=oCgMjv%u*f5A*>igA>4UAR8PG4h5Y+2ha}e4O)RTkPNn03tPY+U@iC= z{0LTo6<`@y3_b$YU=DZ}%m%N47r`^&aqu7T0Js~JgAgbKlfgtV9$X8q0wcl2;Cyf{ zI2)V+`hin`A9M%*23^4s;4p9yH~?gTHefHX2S@>R^MtM7Pp}Tuf}g;5;A`*&SOPu< z^FbAO54;Ip2QPtV!4qHxco5tJZU@uA&EO_*BbWfjf~&#h;1X~F7y`}#1>iJrD(DGL z1jmD8z>%OcI2arV_5*E!53~TOAOURCw_x^0upayZ)`0K9H()tf3O)e~z+CV?cniD% zUIx#BC&43NI=C0y0m5J^2!Ki8I#38kgDb$LU^o~G27&<~AM^#iKo5`ujs-`7E+7kZ z1p9-1KswkHGy{p?FMV=mZvwxAUx5u)gO%V*@EQ0NECe5b3h*|V30?uugQviw;304y zxD(t8f}j)>gX=*N7z3^ZBfy1V7#IZ31pR>pdV`ZdF6ai12LA$wfK1RH>-Zkz{%hQa2&`6hl4{wC(r@31ABv3APppg?fMAK-U9vr zYr)UpN3aU40L#E)@DZp6bHKY`Hh2xZ2%Z6tga3dBz}=u6gg_aX3?_o{;977M7zr*0 z=Yw;>+29P&51a!0pgZ_C=n9Sihk=8@0U!gk0egWxKnkeSH)8fy@F!RYYQaz7JMcC5 z0xSU^gZZEeya(O{uY;Gsv)~CZ13U=s0k?x`;AU_WxDiYMW5Lzna&QT_01N@=fC6wD zI2H5+CxYX_G2lqh85|4_1p9%uzz14@RFD9+={q)iBUlf90c*hb;2W?UECru{1z;|C zAG`(L055~*z?0w+Fdf_r?f_vh6$HQ}a2+TFqrny6QZO701p~nVkPrHTUZ4la0mp)) zKo^h&I)eSdJ|G?J37UaK@YnmoCh$A>71&@kSP8xapMg)oLhu2o0B?ht;1%#ZcnUlU z9s>7)JHf3W2ueXQxE>UNG2lut0$d1&fkEI*&>vW!H#iC8f^Ohw@Go!($OP@dzMwT| z37Uf>@b`PdX0QSL25P_$;9Kw&_#7+(AA)(H61)Rufmgu`;A!v}co^Id?gF=gTfh`h z0&W1~z%^hLxC~qb&I5zNS>SY#2l{}M!3p3vkPQw8hk{O^184{K2CYCENCw;A6}Ess zz*_J#_z|oEE5I_a7<>e(!5r`|m?A zUOKJgXh4L;1Mt#+zajiVK5a0z$9=TC{h9?(up}J8`L1#5eN#)`IWnSLh{Y!-JN#&wHbGvh!lt`<}&H zUu)+ZKiqGrwZfZqw{qF;rswvj=fnm;TQj})YpT`~DtzJ8HdFU7%L>!%yyvyeQaJU| zsM(sEJ+mWaR&HNbqE)DKwxi9O_Qg9qAf%@IPRzGW+Tr`isb$1z8!MBFT9~hjP*RmX zAWIxkS()aWwZICu)$wv?CR^bYUHvKLzL_nwO+a~h<#%a&%=gWzKmqAZedZK*{pV<{ z|BU%Qz&EIZUGcOymkoBLE_(zqM99k7OJVP*1@DW2aa+H| zR3cX21(+1%*|zsBmO`(QqYaa16Y100WA>LE)*x$TJWy!-EIoFVl@bY*MT*nf|G7Di z(&YXAMutZff4*6Lvi2WQS)mYCXtRxE7P#sVi6y>UZzd^*Q*^MMKUGPSPt_8PP<}_> z!?HR!-{f2Nt8g^8K7mBg3-YwxZXn@6{Wk%(e4u!h5H&$6ogFQEWcEvEF#OU zVwG8pVk}gowYFZct>FBc>!cfd-Hp;xtx0wfJ(*b0=YwrGR%*NF`xQ#vvPfQ$ z=hoM6hO6HK?J=j_#C)>~Ow!{r&Ez^X*|fFbFp-lBY*Yxr!oKEqD1?|isUB}4y-ilWXj3E9MtyJ9(BCUoP^ z-!8DKqodGB)S4g8`$qru4cxo<`n%Q9UrZjx9VhQD{M*gDPWn}<>>*_}8!=@`(E(j6 zx-N*xpz>DhzP((^((AX55e+h0vS61}i&?#Gq3HF;0 zp_yZEkY-Mg(TpQNyzs}}-ykMg7wHQ=ze%|!beZP!ZA%Hm{!N(!Nz+kfjG_I;^;&Jd ztNR=H%3g5vr~0Gb_mfUOODQZio=yJCRU|2Ih!&+Ql_}S>%#z<}_G-7T^76lzK2^vl zNhy6s^S{z3%h^pL)w>PTm#bGldUbaCX#B+fOeI9k(T&vGMES`kkjh_55Tp2v9w-Q% zJ^9Wz@?B`;%jf$r31o!_)YT?ibqiHIPTb2kvx919?f+4-?Wm%kt-3i@?!xi8+ONzC zerRpd{@hzmwmL0zGm+G&Tu1OT6jhm3u?fX_Vg(o3XFff0eg?%9hrA8SKv{mfCC2oM#`RGH+Z7bzPwE5`LnL^i6pe{pcNi-=w!X z=&i!|Z)txhUi;CMig;r_|G}Hy@n3N4bRIH>Y*caWe>=#bmwmKr_@Q9dYk4j8ue}T8hN!ER%393OU z8ZUjB@~58c_0m@BX!)a>+bio7lrhnu{5ZkW__jyT_;CuwE|)($@qfRj{9pHdL={C5 z|8FquiEH zMSkC`A;*j;AF-*buDGbR&el+JiV4o-)pf-r$X?muw#mL(3$;g=&av#GO=0KPerAuj z*QA~Izl$jBlJQ=I^)mr3Ep)%1QZue`t2CO3;-w$&oP^jmG%d^6@cX7bWb)4twL}sN zZ*a4ZBr@{kL^2Th=aSg`Q+uD2@8jnmPwZ>66E|3ib9}Qtv~oWge-sSHiE#)YuC|n!WWxQYU_4|>NJ{r_t6!E^&a?C=O>Ki^(4YtjyTWqQGn%1`H zJmt~n@0A#;X|t6eiVi1qvZF#$KTP90&E||1K0xI=O;hK3Rg}7tdjAkUV>I<)YQpSh z=6AY%&&Dm3MWerssH?QlQ`gpqOn36Es98plYi9blGA_6GxmKlAdYq&dU;fvcl)rG2 z^jv+9Jjksq(r1_v`AmgJ!}*FmRBN*#PGK9uMBlZBPt~3fuN!+-aDx?G%mIZsZRPg% z`)<8J*7b$D(%tobEB8}h*$Nz1Qc=(9^{g3$RjrP}p_ETZlq4a>PwcnzmD6-So*#!y zvMo@m(`UlQQ`Mi#e1S6K(sbv9#o#n^mR_i>vb6d_>ag15jT=&*X<3zOR};DAG+gKo z-DMs`dqj7cedyV${%JxbDemK{-4WH&8KM`a8v_-v>Oxheac?%<(D{o~%?s0w11Q6B z;2p=uT*scT^kMnmb%CjWOBeV`Z`7@C=Kd6P$B-)r(_WOu9o?Pf_uclO+9LBxwMmFN z3|7dExXE*g&nmQK|G5?|NkZ~2$2{uT7#X5C<4VZNc4eP!L|K3+1B~oE@XgATD3uar zT2_S-VOo~4DA~=uVrDpfJ9S=MA*EXrKQ}b?`F7*NU2c>O@qA7rKvP!W^H}-(dKM3S zvxY{Lm~KKm-8@Hm&OK)==WbH@=xGEE)dbyMnnI=0$=Q-jnbvN3jr-}aZ^|pCv*QG# zwvuQxX(%{PZSdNstl%8NU(-pMrM}Zg;dFH4KP5GL<{0k-{rw{faFd19s!xlLb30a+ zr6U|EVMF0?Qt6qZ<&1t#EDgrijEF6>xwd!WqZ{qo$$(eN?e^xK(yS6$w{l6t zcS#m6r8!AcVW@dcp>~sqqP<}f(b>tr|K@mZQb%cxa-&jwikhRoDese$7j#|XHv7-! zj8UC!;HjnS#;$d>2Xc&oKQBBckso97gB2t-)d^paWGD1dlP4uFcb)InA#yA~)LX|M zoaP@y{J|CsF&+8jLr4yupE=?afF~4-*m3JaZI@$d%n{9WXcEezS4Cv8YgUUvnP$JUFN3@CNz4pyptc-y?e%k>BTya>G_$w8ISV zR!b60dBMr!SGu7s*yTKK=a^S^p2aJ7ki|n|H2CE74K?_5wJP7>@1}g~tWk=l%7vz? zl&0)646)}e=y7v?(5b)dZ9&uF)Hv39W3o;azx|UQ(x47*u~+D4xqZ$^HEaqtHSQ`j zYQLEIl`t__e||-YBbyRONo;=Mg}#{uZB6>$I47kr!#8t4TYmVorIcSucvM!V8S|2@ zc7g9Z`evSMx<$EP6|IaULRn2u)c!ErOkULZlGo02Ud1Idvay4upRH`r-O>z6W* z!fRix$q;ArmrpQP8ngW5J6SaCcs)zUrV3D#&rp|)<=+t5Gp9&*%c@(-yPe(fA5*22 zGN3$Lflg2m(c*-Jn9#UB8K+`=Qa)BM1)t>p0GSRWm>sg}%yvSVQTNk|UT{O}mrb@U zk$fCfK+{%kn0S>*p8n3p4Nn&J;Zm}Qs090Lh0RE=Xo}U8oXf#EM?ZUA?&>G7F0rPo zVyoO2l-1d23Ylg2hjD^6~{dFLv^Q(>T=A;x^ z)BMEN*1l5Hb`EUa+gJLhNm*MpzO82R8g3G-yYwsOzu)sIv0(tA3HCQdJrM_E(NNO5;Qa?Y^>_2~T%V6|uA~A!a zTrqK%Q(Y*|9M4SkJI}Q~oCqYIC-9!8o=Q!nqBzX7=cr&(KdWkmR=F*KO9QT)>B0rx`ba-TzwSv8F4fJQ$5r zJ$y>NBBj5AoWlKdW`)BaL&WI&NM!Hj~m`FFRMWzL% zS@WKX6@Hz~9G&scia6y(MjZbRj#&C<{#_~mOtLE4Cz4v+Hcd^H)1o4s zHHr@vVd|C^_;;qPki84a8WFMd&jPQ#rec&u{jL1JkFV@#l?D3MPK5_e;lS4Yl>be; zN}b`{0e;_Kwvm(noBs^<@&ztTWw-zq{Nn-hgc0C@0^Op z>^|FEr#mmGv5TWtuGEoPdWnkUc> z*%PTS`zm_O30$SdBwrE8EBRVS5!{xq9$fBnU%0< zPyl%;U$SAIui~~uSIYls&S1e5(xV%RcQQS6fc~ z=_@^5WjRHtD*Jr;WM`|JZmQZ)p~fDi#M zrE3%{&rIWMrAvx>;GA49#P!0J>8O6Bo_)*u5-WG5uXIE--zX>zN|2bKwbk&k1|#OL zjHxWt;BlKGsw~dNADRiFZW=1q8wM<5~=?$ zjMo3PZ#DJOPM$X^8g^p8u}UaZFaBZYYXdkPK;RkXPV1XXiSY{`bSr(?(SOGbz{x( z{8-U-NzMMz+PslQ{;3AlKAEVMDa!=Lzvo9re&?R&jr?vcbH=};X7_n!X8hYR?iMw^ zJZjANsLadlKZmH5o2?|#u$9}Wears&ySV>!OU1DL_IYlWs^34d1M;z{{SPl`FgF!W zedDlZN+pWfmg(&4sObiZw9ZzrZ(5+oVJC3{*}*)?QC1?+Q}wCsVt97B4Wk1 znJoO$PE-iWYqV^lkOkEiO{VFds_;HV59|!&vF)zsDh@|A z5T~TagMPhTs1*gN_v+Ww)IGbmQLDJ3_D>^a15L|cErHmQ=$rPksW!}{+WuDfxYySw ztJv>$puc4MI$z*&83s26eroQoSexSa&8*80ryf(4oRAkjQ6qUX`zHEkp6vDCKJnew z5w~!)tVq9nGkYcab2g7{FJ0vPT_+>`U})8drUg#g5w}lH#rRcD`(=BQ?^e!p@y+a) z6uz#+Us89n?`G4uaN4iilP3-J%{((Xe67s3pFD0y&gw|>%GG-ZR-IhAs^uP4f$vVP z{I1y^9|pcZd5^DDgXWlksIHF&q`WGy+E=-{rFo^9d1a3e)#z}ns?8L&TD1dlG^tv- zx|tl=qbl;US|7Xia^+7QE4OzHuG_S%)5=58*BIzdT3 zCUxt4zj`YXNq0@e|Jt3#f93y0{AYXdpHp9w|1$oYa(2h@uY*pd*IkN#$7-YaZ`$A3 z_;+4z6#q@V96$aYN9t=$!H+r;H~v*p>?r=#Xvhg{EgpY_NeCwYOH3w7xU;|9() zsev>YGrqAU$ydffnZe4+%`F4-SJAS`oSGDzU%923a&TZl$<%&{uIs&$jO+E|CHz1_ z=eoMd+X&^j>G-A4rUVz&-tC1rd0byYyLVvWIybbzIe6v;*%9UYlLHG&rVdE*9w!^W z>Lr(|*?4UqR#aEV>78o7+wJ*HG}*27t80bbLYHV9G|Ycm(?bh4(@cyuv+C3$UZ+8E zU-(No;R~24ubJJFRnnD=ZDwgUrFMm?fqj25x$}5EUSH%qp4&6HP6c#+xMj1v+{Kfe zhcET4uv1R)&8)}|x4P-wf9Kdx32C$w=xMs3_@Kvian3EzAqGAJhI1Y)>wn zD~rikJk(fBj#zxovG_POYfLrGL{l2YVQFYcU z+ql_1`kndnKiBU;@BF`|-|T2JC|18WXk~<}-!bN=(eI`D>F9T;e*Gi;X4W>hf;VL< zBUqsa%w@`oNm{YjyUyyt?bt(oGYi|)DXC@`F4OwI6?2p}) z#5pLNzSqIATL0he4r}pGR+kNG%4A$SzjhK%s2Vo1;cRtIknC-G`EI z;sF}B%^2Iu3YmwJUT5VPRb0EYytb43SUN)oR!|+_r_^6Y5zcBKH7-D4kn;G)R zo@Z|6J2!9YM(ckyxlqIU-1=D{zJ8vkKyOg1;}#c2;=28gVDKQwdShFPzd> z>+~0Cxu7KRTl;Oaf}dK!FY}a5m)h2pdA=zJ;b87tMA@X)*5o>VT>G~2e@$1J^w&oW zzM2|^T+Rja7O%4Es{02k^OoA>`BUZ<{+u7~TbJL($lueHm3d27<#k$@S2te~bU^>` zh1q?kW=W~lR$avz;SLX;5pFr8e>iUu<%{cSj_au}z}g)ZGi}@T4}O@pv?j09`u?4& zTz$Xp926fu@qKIYn*8AWyx^ApQx=TVVe_BLYX9Iu+0O4&?byCCeeA%zK&AX{nUo*4 z5^JmTrz{)SeBMo6C0t@&u#&yQop>hFe%E+zH1)S-C9cn9ZLQi(0VZDzAVvDpx^Hbg)^#^plKSkNM)iLIx z@g~=Qjh}5Ib%eY5t4M@!cm_ZmACU;I-i z9$QyCi}wp(nOA0ZRVoy{VT!Q>YYl%~xqKNsC_G&MYtes;5oFv*; zy-f2<>9f0jYhKqeI7&CB5U?-NFWQgByxhK9t59lQ)`)XV(lS5er!NjUZ&*Ijxev@H zd^@`?AX1`_%?$CVQVglLa%Q*vG{Xii&W*bFWMVx z-T=YG!)kwzTpD|+l4VlKmg9;B(Ezk^Hx!*#@)j|yAv)?0GgoF`gddTj zM@~=bq;n0@>y(@W-zDlK^3P&RuWp+a_(fWb_mdp<-C0??(!^-FJ@g97pU*AIA5W;5 zNNgBOv|sfBgN~|DBi5A9cM@fjhA10GK5sb6O?6{W*N{RRwFGj<{;1V|Q&ZYl;a-VW zFkn()B<%B&yIR%enX+brRTnU+Gh!Sgq?Zn#yxeOv5Lp^dF+Q}a zW~<94D|o%86$`U6{Zn;JJ4d#w!*knG{81gJs10mUNw6SKIdG|+-)X+Jbd{KwJEu^G zUgMp{c}rL4b*k#0`&Hq;tZ<49{c3LM zBOsN$4BzMYr<(Ol!&t_sNqueySLFpZCl)T!0c`p{u9rss25r|nDrjn}IbPWbNKk#9 zUTN|{BO_;;VJW9e$P*3Pt8AtmRCXwEpA6Qz02N(xYI?`SW8?T!iW{PNfw4zfS8n{l zn%!*Q!NVt~?mJkmL2J`*R^`@It8)9vR+rzc@ThAM7kg*t4qLP`KC z;Z|Ff_Q_geZ3Q)bJ0PuJxUFMmwZZ>#W1~JWfMt(^u38%Kg*>(Nv+dFi4Wy z4`}5@(+$Y@<&T>`GwXl9lS9#AFEouald7ni0-FboEt2q>bvoeFE2#lfv_7Jcbaqmn zKC#%ereA2HCR#jc_O+5HNxg$4$-uOJp|(|6R=As~nb|Kite(x#K-p}o;Wy;gZ%OD! zLTAOaBi`F1$e+iZ(nS7@FW)!Hl8X7^$TyhvC4Ip0buO0@^e%gw=d$}UU4w>qf=RaPn>?8?w*stc>%sO~nm zNR~(X*3HRW)_z*8*I1ptu)1vTTG#VUa*_^~tE*PiPYuZL zV#WvR+WNv5sy)(Qr-ZMhbul3?ce5{?qep7qQ8@-=B~YRJ{O~^cOEpRrI&rJFeuChjsl)KE zp25$0zDtLpzwf0V^D2KixNo@I<2@Ju)Thhu`Qb5jd4cVj*M3lQBmvOqpW^!L|3ll? zz*SXcZ-YRhsRv7q{3uK+%xcj5LSq6!J=CSBQDd|zQd25xNC8bP(F@8AuhHi;S(9Z= zPU)om%S>wmF~uwgbu!bbRBAl8YehEsS@Ay4+UFe3;T~(||GuxkpWJo6p1s!EYp=ET z$Jr;jC?~Kf+;L=O&$c`>LkL`>KYG zLdX2lp}w@H>9vf1p;}zZMGfxHIrfP!a7Usq8eeA}pL9*IC%)!5VL$6{3A6K&T&Ot< z1k@3&Roo3qQtfarUvNt5=*r~F^eOP4jby={jNr^w87^Qx#^NB7CsBudniqBhV?fq5 zvj5I^C~4>$SZ@F3(9`&VT`>D7(mH2ik~AZsGBYI-9X&Op_ayeL4PW4Z(G4VK0bcoQ zTofAu$mexy{$|)+Kj5}+jUyA~_BWGIp7DhoG^v9eBQz53q`juhhlq9QS+v?5u43yP#W<+Mq zZqxktL2}gW70efk_oCwO!wZ4D+#NBybb@3FBNAC%dh>`RbZyi(G1Y_uL3nf8wi$iV z3A&=WrZV=taLXs&)m^P0LM=|XRYvW`;D?p!<8LtTiWYL{66SuYH+IIWe z7+kA1mRtol9dLHzy!G#k6a5#`KNrwH7nrsWQQ?nY!=NKxg+K-i`#-8xJSfL+C6_^V zUsYRQS{)j=oVx`Qf2viC!GX4RWa(_Axr>7&d`o&{&RuwbDyl4W6h#&cTx_F?e!F z?|TY1lsLUMlu!18`62z-K3dHzWS(tDR>rpDh-cci`v>3w#E*GMq+$_co6&zdR|L8r zuB8)vV0LVd*}pt7eM{&F161HpA|0 z+qJcGhc{2D#&l9@@piCYf?npRBoTJ)h=j^fY}A}(5|>c`r&U;&gVdw~Z$_+zC#Nyx zRAoZFwfCRL_ui~bu)8!2q*~qOasZ!xth&zo*7L1f&sVpOMwGk~**lU|riNHPLH~!P z7YxpjNY$9$43@0b{@vM70$5{=tO$7!KHbaFnW}a&K(j-l=r?-rW0@G|z~IKN0~`$r z)aCZA&&BXfU#R{jEG4PxliT;e$o`*azRkuH?vT^G>AK*wRZ%%RzCir>0r8dYJ~!>l z8DG#ZXGq@aLz|s7vj11%UJMstFvAD0I5ao#Z}zU=v)X@d+80{zUBKoB{*~MJa4tTw zhk(d{%rOLCFnz-|43kz(K6HF!#phc2SvVOu-hXH2v47`O-4LH!)!vu(rM9pMES(oSg$F|XCkNSXs*ub7E$0-dC-%}U6u9FgJ+uv=tK z8zYZH`o>tCh!B?3``^C6WcVK#dkTt=G7OZ)p@q~+b6FFDhO&U%z{Et%amMBZW&?Tx zpvR}ED0)UtU?1Ew!fu(qqY>6ymV>2|yTW?!?{X0jbE_`NsTz@(leS4)$QNsT!AYqj zD<{WAqP!VwJ&IG~SHboIqxLs4c>uzskmd38di?$OE^FaL$NLJ>OA=P>qxG{#-e7-F zour8P6f93}f+bn)TP<}?=BnfQhpSvHOOjPzEks6&VKh?al_hB$RZ z8(%X;yT1l+RE}L51EJB=`z^fwz_V4ig6o|*3`e&neQ+6s3)Hcb5ZK`he44W>oZB1z z?_)1p!4+Od(|j6EsuYc^yJ6z&mC^ z1)<}pZR^A(JaGso><&t91#*qg&fx9%-Z#|xSWQmM)%YmuQ_1vG)n%t9YUNksdfBO2 zT7~SJ0{tmY2O==Prt}M5VbP{yae)5gYsobfky2Db2rM7!-O|rzcVVcs?>W6G3BnJP za-FRuk7^S2LQSGpEYb8*C!a4nm9Ld!w5Yi({fdW~7L=cZ+>47kb(D@m=TX^A1azEZ zo0WD}o}7=%8rF9fn!mH~Ycq|cZ1FtAk|~gXidKFH$g)$TwMEwoF;c7O0zwbok9gID z>_*d;q~h`Z%^BtBj=o^ljZzYpCZe8}5gj=Jl!W!5=0O7HRkWvSE74y9qvx_&DX!Re z!hvE9af}D#k6qaeYBUcT%~#`}8`yNAze4HMP=5uKRO!z&OK8ALe=Ww3tOX50F)2az zEt5%WIp1k&5aY+EB(3{j)gK$%GAe5ed=h^7k5zIUzj{!;Hpf;=rSP5t{3h}mXzGSE z@85%8WP1jFq?UP{gI+~n4D3R393c_lRXu3F+!jEsfEHk>6~x~JqZ+RgAg$!uS=tS1 zO6&&BOYFvM5@PcJ^Pw5mfD5kq)$to@Ms$Uhl=MJOuSPu3mCDhw(HBLa|0%=;+aP^` zEunglXoA@^tJ}2lSLMm6B?s_icoXhHS9J}}QMhG&Ayr$;$b$?KO{+Lrr)1K!YKR2I)Z@3(T#IeK(##rJjnf&GZ^=4uv#lhd zO!7h_*AYN?%tq?0m?WZ_6(cn6Q@pajY~IUchzB*inQoN_E6jsL;|^(yQBq{@$LW(ZgFxFJutnP@ye&X1MO6N zGFqD1&15D6MW^s|U*ucRwULE&=h}^1wa#c^V28i;=IpK=WQTv4nWa`{BbH_{)%L9xK)tCm)w%6?>y7hIo107;hdVn1PpKabyBPSOTzO z%nZPaJ2LWn^=BJhU9Y@LgK@3?Zfz zqkECf>Y7>-hwI^Ic+>s7*;1F&I?#_13+m`DWlDAE8a@&LX82G#ZC%+GBwQgY>$H^(k1CakcFtdmdsM#E2wrm$vphL^W_sDlk=XmG zxt=#EJ#7tr&DeT!bP3pNIr%Y8IxIZ2Rc^YixUTHvl#&bdz8E&Ic{2+C5L`2=r3{UWVjoct^fm~t|yWay<$OS-wvJdkv= z-aN=4FcT_1pC{ISdpwJauy#XkQkfi^=5ZdQA3O_P^s+keU9pjF0&q2Q#e8uz?LBP? z_e0@}Qc~!H)jSGp)7F~VVa3tEq5pxZ7>SpjUz3}VdRyx={LMdT!9VkQ8nS@@UrNxf z+ocDu#)t)!;IH{f^l&TIqyE==z|J>wG3dBAl*frUIN3_FZni5`X=b}pm4*qR_tWG(vUF~3&&;L(NVFTPiIv?XI01`s;Y0XB;xEqxT_EKf!=ti z(DWa|Xz+SW$eWw)r0trH@%h(8n$5ADF09P%gb47OiTbFC`c!LF^nV{WW38~)&TnsM1YV9Z@{6i2`#kWLV`Ne8FcCRiFht;*ZqT~gM$NK)xs2b z8ehMKjJmnE*Z9ydeO2vzNFnarH(BYI<1fM8bXRcT`S|5O<}n4J;s2S(IC7ZVvA9O% zFz9{psQ|NuE_mVw*b4^E<@?5L7m7v6VbqPxVIEan_I=N4jPb^teX3rq`_&YeEZwlC zxR4%*Y?xha!!o(n%o13P3?s)$if@dum`t1FOAg*wVGWJq9k)x94R$ySTQ-AnCSUx% zF>5`M<|y3s!+z6T*hd)=lq8J5zgMxl3>#HiG@_lJrwBTT5862?HaUwU^rJct3p3C)bop}YX;AY^8wj>5esGM?26qREwi{o$x z=3T~gN#c^V#s5HEN`9%#}N=Wluf zcy)TP1MWbTRBXKuR_(r+lU9F!SP#h3MZ(GW)2W?UIHeYXxzJ;zqTB#wC!@7R$infu z)RgGlw8l*BnT;V>1?pYNgZbvrP#i-7EpzA(#qC9^P#jM2*$QrSgBh!|;*O#Y=5HaK z$}IaKwdAYNryx0+K+R-o>kmdv?}-IB?XchmD-2dj?dGP1GO+xJwDvb7@YhJ?X^1kV1J8*rL>a9}$Bl80M4fSa4@qQsc3T7uCSA*7% zkdOI?RRCR?Tt;5t^!Ur+%N6*YQeBSe)?f2Rn6*UI;0&}jjCJuY_OH@@(iZKA;|;3E zy*W1VGTr}{q#1Z#D}PBYqDkWkaU1;QTQz3_R$JS{hm~(!v9jn=+*sK&mG|t&J<30F zzIRD`T*;B8e#f*0ufPY7*c=;mImasM35JnZUs`PrQ%$~P_uzAASZc*g`^~YbsqzxZ zZp^CAy;%>A!+OD!rG_LT90@E!-}Q+H8AIV@*erMwu@j?M(tF1HD~z2oq#*07_&%lV zq=8JvVwJJADEx?cBJ9k!V7K@J6|pM2Kq7OL2xBgmX3UY0>|I=N6kLi2Fn_t>GKvE? zjx|a5sP>Po+q4Bqyo_vs6;Rm|^T8+EYeD)al-LFt(ex`Qr+;H=<(O17?p7fZL-SE^ z4z#l}J~Mw4s~koZFzI08d!!Qpj{3yLEJ_tzq2A$uGM0SLx#D!|QTEvRe*++cdb zu{0R#zrOTg5nJs^tPbvg)w%~T_ezetk=rg;WMWpca?BNAdXFs2qwqzX^DD$SLX@xv zx5W;(vtG!ae+JpRi9+Alkd4t9AsbH_m}iOKG}^SWS{r8zqe=Jst6|4do>6Pxcb-&(pnxi{XgmXqRxDb1n+a zr3*!uLG0tEFO^aPsEBT<doPHkf7;`b=D-+8dG0cs1=3JJl^ZLPLeBbXZG&IBw|8 z6AE{(Z388r> zQPmRk3PGU(&J##AN+#-MqQ=(fSnY zg5TpnVvAWj5~jw=9n%qE;Ioh8xy`W&mt$d1JH~+KTGSiYaV=R(L@OlVO$273OL-0% zDC3#7B#6Ik7VG49u{00Ne`^cBLmK)g&PvqC=P}ujvh%|MtcMEaiT}o`W@z#Sfjv#ilhOj&;?nq>rOJO( zmBF|I0r>^@uNR{GKUV1gz$!MM_LS|Kwr3^AAiz_LQ)#OvzBW%(iO(utbtYz9?H(W7xcR@(vjIz6P zP^nY|EBM$z-q+Tq#oWuU0}d5`z?F0vC}iMum5#}2zJCpaqx$)2|8ZFWQ9d8=_H3(W<~A&c8!1<5|WHhiV- zZ;bZW;nYoS+T_$W#&V@#A?i=MSHsYM%;m!2%R%7X6ZAKCXbb*^jwDOCJlqK@Z_mN&?DZx@EGsiw&r}~m6IJU49nSYR;g^Ex*l6XVSkbU_+r5P)+%vnNSd3qctG6 z7W__05~HuPI(sdW*quEcOLRpzb_6M(eTA<*u*b@tMobRA)#dBE5qSV)KA0QWlhb$C zHNh@e;Cc{WBBBB=(rcT~5b0wFWCpX87J`}b+eZ9X$bkQ#F&5AZ!KZQ&Nigohc|Rli zipgH0A@4jr9^4TaajLPu||lq^9@pyDBYwsVK$rM8LbO3oIkVC?AcpyiEakyM z`s7g#B?aT+%>AOw=HxbwO7uh{n^UYyKU}RdswIb}o^GO^V41WWwAzuVhCkisss?my z%vQ^UvbR?;d2oDu)^4>5FZ3ePly%Zgt^_6UXbfVc!MGvrV%*MQ{X{QmS{I|RLfu$Q z9s~&efV>A1{By2Lg1Jr!BtM+s1Pzu{wrzh>GKyag`g zV{HMG5pkP%p}aHR;KCC8EjhKH$L0naeHi>fuDpc386Vy}$_@O};X8XK=BCxo_<$}E zjbU!Go?Uqp$3rH|V9(iZL9D7^FFDD0{MHB^`8ba46oL_U8470Pb~#cVj+6yep`g$; zVg!3bFR32x({oOvK0E*D5m~f{?^Z>7>+$c*Edr;)l|FlvBJM(GxiS)98s1z;yP_aM zS{*^~G}2lDR)u4lJgqLY^SIc@TeFpbG+2gIdLg7nmYpc|4v8%AXVmL1kX?Hz@H!M| zm2O3JE0BkVC~p383D(c<<0_%Rx1sg&%QelRS3nx~oQWiBrDJW7Xa8UFs~;(e$NQW9 z;S=k z?t^tjWTh=9yv9!&)c8L-(;Pop%~As^RgWNO{-P~O;0})e2^zdWTa?C|(V5{Z#g=^e zT>4PRKX{`-b)PtKupf7D+MpluD~78E9LuY+7`3kGfhfH$6KBHlc-6ZDM(}oHvHN6g z!E?L|FH!CIT3(@QiQWj+|HAGPar&J{Ci8{$VC+?#!?NZV)2SKFA7o*C6)QOC@~7Iu z`^4U#i@Uh56DmJb^H0PLNY*-x!;}hY(3;=V&FA?}7@q%04-ULvTRYgE|C5IoTo11i zYA?5R8O*uXmh1CRDh57-V3=l3Ij7+7&r+A*5C#(5IYX?w$~QKOC|<+|Am=B z1+hRNioipc8@07(N&bMF@$3X#sT>nKmTHw%Ai+`g`V9*A1;93E4;*(HH+?+-g>crd zH@r6TJ6vD1frY^X)QFPbllQf1Gii7jW5|C8y)wP_6uzjqM-OJl-pJ7x;fU=g?cdVLbh)M4f3v;+<^cyJtdm4_D5X#OTzBt}T&T}rzP`dx|T$ke!)b3jBEIit|GJx znT?1m`O8u)>yRVkA)DHA5hF;0I=vnTidrqFo8C+Y8%CXd`j4RSCbzn0vEv?vZ=Rrj zjl0bQTE8)04$M-pW`@fKuv)5Z^jf<^w=5^$0uiaysZ!j)&rnx3o${(J} zb+IL}a4>nuNm@L&q9Xja#J?x$iu{|(+~W&RQ2l7T)%=BD?TZCnm{;c!va-bo=w8qy zDtQ7OLgio5j%bVK&^2)cj4dy_G8UHoRPr*=nBVKhzFy#je(W>!beZ=WxC;n)k>MvcbzVteLLls~g*83cfr zCJ&kZ0~#)O=#4korgs}viH*+qNhQ>fil14Ko&8oSEgS3lIDEcHD$8_j?1CiF8zGXj zThRVS)%@XOWWJ-A!(AL_rZYA5X6d~}sn~jkDX5voV++J)$$n8-cXwejn1{kxY!d8e z>9~mEuQnw=E(sCm9#Flu7B7WWP`p zM>PuMLm_&=9w$leJ_!yr$s9$L{Xl+Bo1rwi zN>oX{IoQPU@wr&ia`{WpD;P;2&#+?Y1xo?ibs?_UlLeULmmTuf|2tZD}EuY71|vGd={vOsyg-%kSr)S z!Y77Y0$|vfoUvRA23nE70wMGYyP_Bez}Jet>doZz#pa)yLnCBh-rIl4OquKZK19Np z5XWO>#s!1zU&1~Z$1y2YoIDNN3dXRs^e7~z$fkHvG;a;SLzqOw6WUpJ@?FZvvXk!y zah`Ehpjsxp(X`l!xzvUN6V)q9BsA#q$wJpcZIn2|wE)+Ce*%hy|_A!Y~&`0AEXHVF)33 zUiQ=Y2M($0A_R%fHyp)wYhfBg^H+~*FfZ%@cX>muID@UoqD$A)M{)SM4TC$*iztJV0^HjEHtX(JFP|qKf?>EfhvD_3{z`n+9@+2aGKmO zk%pK>zJP>1_yCq|s#gYeX=194nMw8OqHMHNqm&v6%y^XeE7TYh{WWUzfryHI7~16UV1fV+JkOOO0E)N2pOW zNL50Omsyg~UAI=_>g)bXHNw8k@6tg=p#S~$U(!>q6mfbzwV_5Mt8|!ovuHPF23&M87YB%A(pDVd`EY_xWgYpEIc0`BG-<`l3mV1%DH z&Gdsp%S;7Dj6_g4u9WA25}egyrn4;b#O}z=iRd73e1Ts9qqR}LzqioA3hYpdkbFgo zU~=?TuFWVqV748~gc>kH?}+hns3pj?TK3fHO!?Fn+i%QG#mrr?c?276ZD==xnzY7X zuWzPOIyy)UjZi%i4)QK3UmWBZW-cs=4KVzIqEBDyL|3T=j#BUa#Zr&?DE0rQ)LUJp zekp;J{vy~b<`+wyf7(+0$IsN3v|-X*?LXc@E2k0rk9X86{=iXBQA#UC^b(bd`lmZt zst%2F@bvpBzoh@Kt~re+p7$))QR`Ej#md;TRqH2i`=wH6p03mvAb+v6}FG@OC_ig0xa1d#Uu!8)AuO7-6 zBwtO7GRL!_u|#9YGMY7VZuBN&wM7R2Wmb6?iIm9qTXZo+bV2V0+04_?O;U&|&5~F0 z3uE~C#1=)Ei+^x{fNl*fl_!^xEgUc7Z;Kwm83xxn4xdD2!moor6ezJ<7ADv}^G)69 zGZo{wS(oxdzk&Z{&xu<{Ks{~y*MxS%iJDrXBL9eaai$vap|{1mM%KT!Xuj|Uk@0fT zO{$->ZTf|kvE5n>!dzz=X%KM!aMy9uCz@8F{C-PlVPEI_(Y6dNZy4z<1_226bK)+u zKu#M$UiLzAo0bgWt)~&dzD{9j2KIma4ry<~_jcNA52JYJzcLaQ@qJXp3-su*BRYf6 z4l}>R!2Oa{)E=rXm5pJj)FxYnjNfcnippSx;@L#zRrru1Ixn((0Q2RXPgcuTdm{y8 z&ldaLV3qp)Jr#a>Gxl6531YEMh8Pb_cvDJUI!8i^FOmE?>ApB5$t9iq#|w(v`H#=j zDsF;Fw0gkf5Ds7(TLj>wbGF0C6IHc80#$thOA#HR7m&zNqy#;PKiV4-HIY%OYy&$f zY{l{uCM`c<@zH(5xw&0mZ1dBEEl;oV;e)w5e)0ulJLRO+%(#4%_C|-SDVb?IW^}+P zdMCUJf%Q${jo7b24{Q&wrvfv`Qv!HO1W#BHS%dmJ3-xDLR|MhaPt=P*dm8qJ{0w`S zjH>G3Cv)vB=E4|171sXd$(nI1IrFxkGdPsy8h;?BY95mR{r(d-mkyXxo&GFN z%snTaCZs)Pd5~#nQGBb@++$5@-gQPQPn}7Zv^~f@EU~-zqgP^!RcI24epg*+sj*%I zg~qh4UNvYI>8kYpa%wI(!ggELcbD$zL3R1`K0tr9q`e(qV3 zjB%{jzaLhB{o{TTUXXKZZw$=BcmVrnQ__B#xmEYShY=Kfaa>-HdL#BX)l<@rPk)u* zg*r<{0cbxg9#xqqy_hLTf)fqAY|z33zfnB5n?!`l)+*EdGo*C983=pJL<*@&-+`TX z!dULu@!{c0L#fcv{X)y?%U;8TmfbuCT_iLkKvnu3prN5Y{DC@jJ-`UmG|( z@z3?@oRPzWZ2YMP|Hg)|;7*vNM>JhI;oW|oq<&oAr+@@Q)A?nZdDBpbqc9zZ>KiRT zy>$Kr*fknMyG7qy?ADZ@`Kgk><2mZFc>Zu-!qQ)9Z^VwE%}s&XK3aoLQU*Ox3fgWe zzXA5O?KjmacE*#1e6q#K2h(rF z38Q^Gehm%NkZCxAe~riCGe#GR^eYj`RZ-sc6Xt-v|L`nEh9X7=G!&KSlv|U(dECR8 z`y+2H?#ST{KR$C;@id;}d@wFbFQ)5#N8(TwXzJK|mzY)OYHM@i(dE6&o{2yUE3gJd zS;K{x!oBDbJQm;>8~b=cSulcXH?HNLlfC;nf}ZL{%B`*wO-T)UUF zs1o^%2PUbkJPi$J8I)SNSj~q(Yeg`ES=?U^INYCkk9uY719K9OlPPEzEAS&qmfP#N zKK`=uoyCj$*Kmlm3>VkK9Yr;1YdUL>Zscg#Y#a-Q7#nuT1Y6J$yytOfD2G>}p$ytA z^24qZzEv8+H)xY_4O>uSQ{8x2Dm2uxWTU_yeuYM;q1cBl4Y5rG>Eh@mjA?6HX^7uU zuU?OeM=Md;Zj)Afb~84pB=gw6Su1~wX(K#dDoXv89_X3Oc4o9W2b?0U{1tVb8u|ln zvZQ~dRppsJ8a0XVVL**J<-dZBN-gMGn4)9T8jO)JWm-A(anVOY^F%$vfB znvnd`it<%LiYaIYLH=tA+%yTiKkzlpi=n#WRXl`kfU#RXtX*w2P-)GLSYxc}T@xy2 z7As;M5INIV#$SBDR9=*{raPbsI0&S^#xZ<%nfA=uM3*iJQguUNs*O;V)@|fdYd;xwLD#VVu z&<-v~86_|M#19B3MwN`MrlvB~tdI=1%8$WgvrTRdwrZ0QkZtl?xDJjcCYHDL zr|;Uz_|(zlwXIsbPsWXxFh&~wrd5H~YH!u%sA5*TY-t@FlrIH3yg3SlH&^j{!^#NT zsi2Rf=Ry7QeT*i7`Z)oEaRbe@#76i@tGPi%5{HIL`{ACzXm_XT+3qSHBqX=x55|pO zM|Gf~u8}8YTpFL@@uxTN){>j30=SE|lvoKRTJ+nmKZ=>K!H5=L7SCpCF(?qC#Z*9q z&K0MH9aC}cVj{u}vQ=`F}@QkdH$F}Fz~ zH>vWC+pZrxi>8~lqxfOY%}xcCF+jgAhsokptuw(;kKKQ)KTnP^3n z53gyizgvrOo71kpQ!Uuew&18xVXj>3Ru22XOU|NY&^_=Uj9?jmsg*MR2D_d#U<~k* zv$To}S)xk8=pp$K5~5|i4|X9g2G7;-7;{tfyllz!a%@J5V6O)PG%h;`v{+bz@y&`> zZ5i+XS6qttrXs;Q068N8?SmPUL^JMdCi#Pk2FPzW^18~JHkk2FB0{9BDk5|?1w}ZR zC!x*?2N)JGg|!Ch7As-VVDl+qjVYm$*nCA0&s6h#aL^N#l$icYn!4)mN>JeV(~GTS z*2*Wte@RASwV#fq&qPFoN%(e)Lor(h_Gh%?dDnvku zxIP%L=>n`7MdoQK)f-eeQSTHXoSfIZ4cok5%$xTM&|+D*h8#dS*pa9}cXapSP_MK*a|bVzs=d8-jf(;9HBtj6lSn*m7$ARQ}D9Z6wJY)&xldYRfZ zz2|jxb_{|(+J^R+-cvU&!bTt}2Dyv1vM6(66;(@{GsDmhd0o*CLn=d%NMsEDbL&d1 zSZfBMA&fx$K)^2wiSJw7OTplcWN5rAfIlmN6r_%c`7sz9`3jrnwn zJZ%P|41E8ZkCfa;H&tK;A|;jtqLUS;)28ZwzU0v8kF`X)m`E8W5*h2im90Qo*Dmzs zu*>7$Bw1#>Uw;z1{!j`x{t8;{HXxo?J!7mpg|oxWpe3A92OQt`c1@1)bJJg8qo24N zK3C%FLDLm6XjL0?CX#>Cets@$Jv;&a|3;XIvnhcQKJ49!L_fo&YT1%d9#REqPGb6& zXH&)`ZAzMgoig_1{Dy4Z0ewmj2HTvYuNztmE>wh!3e9Dls&d80Av|Tp#|h$-RsIw@ zilFuL?6+(tXXa0H#E^d06Tr>%MR=VF@c*#!OMmHO&PY|M{?aUKJxpi`&KjGs>O9mN zRAhXU@evuKbB*uNexO~rv97m2skQ8{#L>d?6xIRWU8~^7a$pKmOO+`g|1)|#WHlvJ z4U85|XvjO#g!rl=^$p*^{yWAm!*MJoiIs?ujhu!fF}MqjlHHW1KUqomcn3Rs|ENO# zB@D0ca2%m3N8nT+5uv>x8h>t;aIc1m%&towuVLMF@~z;*8dV}P*p%E;{5Y)LzR!W7 zau;xK+>0H?Lsv@k`M)gNxE#IW(Cx53dyFR{|B(9mlsfSrhLWZs^Fa=T^oML$=}EF% z4t}cgFu<50?@yMm5KW)Ox?21&Y3MZxzmz=%vcnG{G$NVm7~IdAgXV)~JBl~f->fPy z15^H|&pH2bV{@}V{daHEKb^0b*^-gSGW0DON%9L7r((=zD28mvYj9R#|E>5w<1x4= zygF-L#L&PP=+fweO|M`-A^4^iZP;qDi3t4!vy<1U{(S-mtmYq9fo!D;aOmIHkSEKh z_-7@eH?bS`YuW^LD2W8 zc(jD_o8?4jG=}bG&qgKac8{{pQl-{JfBMC5Nqgujk65856J5Qzco9mrklW9SZem3@ z8Kjs) z_<9;r^CZ10L)uR&4Z+xl+u(rWqe|AY-jeL5fy)d8mL^qdaG7w_#Y(m8HH%}HQ(7}G z7_xy@BE2eDaNE#$X*=*QMknk{z`Ve$rTLp!d9W8#DZzUajWA~l570sHq|adx?q7t@qz2H`P|6O}a>+nFfcRuP zsj80GKBn@-1S1=KNqShk4UKH5I1bgh(9=3zjN7zF#i34vMmfc2TtUY0tz3(-pW#ni zxRR#b>ie}2xvDdxR{x`Tvo8jdb4+nlxTLas&I5(%VcwXJr4HK;7SX zH;pZY{)2m6U|h5ZB=31_BZirximyYf{9N92H*4=a!ry@2A>n&q#I+OZxo7S17_PsG zE=gR6iD_6DG|W*Q`l_L*w2ZoSh@j2kf8kYQjPAV@t~Iv5&bG;%g$=TAy`G*i0#*G? zO=v9cG+vR*S4#IeMewRaZE*p-!Q;l|wdRTObHfXS6tiOu<4(Q}iD7tDs|d;U(n+C8 zh{q8A?`>Fb-zRNc&`mU4~>PFb_Eu< z6|IJ!#fuo>r)c)Z#ud~T2OVtgT9uHE3rV3DP>eBl7pyzArP-a{f3y7!hixMyajF?E zFTosFgP3Yi4P$EUSqauev>Ht_I-?fDhm0?G!iTJ|3h-**+QJWLA^yW~X48;3qKrf+ zF%@gFx$B_j=gL`FcbnugP`j2AbBtcR<0I(`)5EPLtntM-zekI^{s`(MfzN+MgCxJYE{+iZ&&Er&O#%Wqr{p7)AIK|Nb9w;cRsgLg|4H$q=p!d+f#@>=n$Gd3JaW3Y~2hiqL@@ z;T^h=Llz#x+CoHX)7-F*4PD{`U)3Ocu&05^#wzB}0M65^aEkt65VhCs+f!0Wic90g z1}JkQT9n85kK|vHP`T`fBBe@PT|nL72ug{?_ow%P6CPsMzoAjZ2(pw8d=6y*Nx=96 z#t|G=;jck(K{ZowM&ksdJ9_NlkJNL_o=^M_qo>9bP=OvVyQsdZ(ovi;-ou1$_{$bZ z<72$Q92yvD8q!3(+Xq)cw2m*?YEZ*yZwmwt5*Q4t^5LcN1U!HG1ahZU{FMM(GU9m> z>$Z!>u%C`vp=A~7sL`Tv??d|yhcT*hf-NXK%pyr5(FH;EY77Zx>Zx!3scb}9)8 zz0#O5Kn5zQXyFU43<}(cRjAl{3i{VYt+<4Gp;2X#Gn@zezU0L4Kd8X|M~%arXT6#> z@!a;GkU#y`@8^nr@O>!gFOTg@XZwN>A{amZ%e{lD5RS3H7E{=Xv_wR`QHU#Yi(Ka0 zk7*n4$D&pm5-t>A)eALjH@XIY37c$GqN6Ql zKyF#FnxymlrVhDrO*YD-@V}$*Nmug?ZoZuI5k52|GV8~hT{3sTcbjt{Gm_Qq9B2oe zl3Cf;Y2?O`<|2Fm)gHFA^bvW+c;bE7#M$BwtgvGLdbOX@yWFwG_-Gm1I9fGWLv6wc zQ2%KSGtT4dn6>-c>d|{BE7}L!bW4b!NW)D##WQSf9=93Cw}HPI-VNM~KW*V0l|Rm+ zePS{We@w9p;73QJ<*Kn0scX>HB=FC;7J@)=!H(&#oaGz}u3<_hdp`!_-kYR?LLq#0 zH{3>+1vc9F`!kjv%+YOpF)b}KURzYg_JaHJ_>M<8zdT^Q&1>w_tnmX}M_cOnQbQc> z?En{v#*6ni&*REPbNobS-)rGQ6D{8!MK^DcH*%Md10UO?Ik$%4vPJCIvT(dCvFl{%H96-et%^HLI;`Q{90#^Hk&!N}pfg68 zf~Qa(coc?|K-Ygh#MLPut|IFBSYz@Qj=8)8dl!m`=t`AAy<#p{WAZdE8&CWTtrH(1 zH*N8kQaj7UxG;!T`7{bF_pa{4-jDu+NX!`r`Q*FE+QUO-!$QZR1LSkc% z(~Uo+EB^_ZFGXUE|5b5kQ;@2TUGRfl!_U;WWU0{w?BJgE-hCJn?b%Qs<4@nWl9Kas z0>q5M_u28K*918Bv+>!tFvdyaUaA(Lm+{koXEl*Vt1zXIa`mb!8}zF4{pr;L)mIPP zlAu@NL$rZg&{v;h;WG{7`?ctB8GoPx-_V0I6Zpl_z%gSXPGE6jcniuKrpg;GKV7fn zPoQJ2{B*q*Kj{ApCf+THyBx>T65~3^m5y?yi@qhU=|QZcz(2hT+VF3RM=F-SALpRq zkMUk;_z;eHkFkwM8;sAlaQFoMVLLOHD6ADhF-fN5dmQMa-UaNq@t94`4C8jUvc}n6qndQBwhbD$>a9!fqPotQO@;D9%R^C`)#+$; zXnYUrsy2SV8D`zy<%qrIKk_s3Gw_Cy8#1e4~^)(*ZA7ifz>tL zk&t75?_wDB)#xZeKTSHmIV&qXtMCXSODy!#2F?v%e+&-ld=nIt_Gt+|hqJXf5BUU6 zmZEa6Y~xRV{x4L>CGfh44D`O)ZB(N`mXgA-#6E#TFIaqM+=@w38vAcKS-KnJ4qL-7 znPG&R7dq8te24~7ZCv**^aFq3=%je-$Lyb^(gss#gK%=PP8RkNsh_`-V>(9eS_l>C z?Y{HWkIa?Bh@d1a-d6e*i+aIBTaDw*^(ol6f(n~TEhI5(gRZZH>gGXp1EIRxS5S4x zjck=+ipm-jLjJ#q4TgCpILsgg5JWO)h`TsZG`}onG&*BcD_kpje8kja2JaHHQ+F9b zEc~uE+HQg#f3ANbL!YhkPZ_M^(56;?jl?(%$Co3W>;Ug7p-fU|uHQrYmSALIs^6U7 zJd3yE9&pbAHny~2*X3Lp7(kvm>|Pb1Q@F2AIy9NVR+ph`xwUMS@SCt6fBJ|Qp(d^3 zf2eBIWriNO9)?A21UnwvOe`>hW84pA8I!v?!g>;)fHA<#16RWdAN?~`{u^CBPA#)h z@X+MXUjyYbUUC84tqYar@gPp&dV#_R1sPEY{i88K=69x9*>G@nyixfXADk=e%Z#P_ zQ7g~lbwM$~45JFiYO8b9otZ)h;~wKGKonzKI?Iu+425hxEZJxhY*Vz21QKff%$%+c zL5J#FGUXb66M2t6y}if^RbgHooagHS*d6Cf%t2OTb_Kab;uz{HS;M{Y#uj`@K?Hz* z-augj$HVU#J8HrC`>n=cyxL#sJosj4t)=auHDhU5_$+v5HiD`g@KdkGztPW1qc%nH zMn`CqUG+p&^{BQ445!u81~OPHI(qndEmnUzGrx$e-bzR6#hU{vH4PL3L3CMZBL)QfDYpiC z|0vI3mIQa;R?7t#ANwK9y%5HfR!uum32GFP%E=>WK9p0?a{e^88|`Xj9R#I%2ZdfsxjQZ95Jaz9fp zD|RANxd2HIru|F=Ace^$)_zWllt{u10-=`JQ_<87T6)+wJ+|^H`f$> ze*^!a39fJ-A%!>pD}IK5v~46TXm2wV|7RBG3;);ZRo5u~Ge8Od#R>iGHStOS+XDu( z#Lm_5c>#(uj?_>HG|lk)5V0QrHz_g2nu{T3(=NA|-~!)=F#d?w(*Dcwqwo#VKH;pU z!L$*-^NdI4(J<(n*-(QW`^dYfXeT&^N9(%dtutUj9H~IVNn|>v>~q53_#76cmbXPR zeOXj2eB#k%uq^fdl~yr>Wnp<=Fs|V?WrS?5I$ITSpLgLhyD{QjkSDPvt3Q1?A+?I0 zHkjWF?6;9%8{T&E_qu?MbMp7j^%j4rUXbyTkcBSD1p*nP_?u#bHQFzEI)r_DgHwqj zY!4S$u7It5#-YD&2*l%PH~$QYDiJi8ac3math~VCTEQgugj1u=@n8 zo0Gra*pN-~DkfvOL*(u2BScPdK@Jeex<3hjKU`<=x7-WN9|^W^gHzbAM1Q3Ov+VB~ z8#2!eazG?xr3>;Uf$XFB8)bvVc!3>yGeX$mF0k7KZ1>X+{zBJUBCobz-gM}1f(ugW z*I4fq`4WNj6H=>q*an;A1(xFillooi0_!DUgPr_cVM8W(K_17a#9I7rl_$P2Qk3sUsA!Ug%HKxQfaF0#S; zc!6myFcEfw3+y%lJG#`t-&UU`azlZK$bWkyLVxit$N>VmoRC_@bQ{d?1vVoRYy&1i zTm17^Ke95$Ir%%=hD`N>`~=HrTk2QzSLlMADE!4J{@%LA61MRc4`Dxafrt z$j0kV{fWpaF339svhI(<-wig{axbu_Bf<8qbqYI3z)A^bwU6&|EdKJmAbpXLl`hCH zf1t_qQT#n+gT;7(MMr`ScY&=Eu-#8M_`Ax6T+NXobkbb$f)tVCU69>Hf6ED}Rcsz*3G4R)doU7g!~Zz-_n`+5 z@;yZU!WEK*zZ4hblLA@yi10Vl23zh0_L|EW#Z>m0??WkJ#|l^}!7TsK-GX`F32NI6uI#s;qOKp ztZtl#uwz{MllE~+UW>H)*Q*5Vae`U(dosh~ugDAXX_rF~{jG38P87&2#ox0wSRXI2 z6j%EdVJEo2dI{LkY6pMWHe|zC50S&KIQ@&5bi504Cq6ch{y!nLim$G+g!OxY#ed`k z!<(B}pSQ|sERPA;I46HgY{*nE$SwHpbW8p#RZ{4Jyjmb*6n{f(u*NYS!hW*T2?mMa z|6E{k0=D)+hyM2d+7h|i3-Yo^{_15Nv1KyveNFvMBBWL^-v-O_0z0_U$)7}zIWDjY z0qf@EFWH7{`i+OkaW4B){?7$DOd#u)2!9`7f z;Gw@GE|Hc0GhaC}z{WZG`#jAOIn@htQ6yxc3$l$s#wh-RHdy0z z9>ONLj79XwyO1~uYJIwzsqgNYA?tST*bYEOXS%CIf;;3#YaOeVY9rz z4!WF`*xwu%*dPJx=Hzd-4cT<9hsbMQa#o4>mlPM|mm!K=7ZCn>+hEJRz~Wv0S?YJ6 zyjp1q`+|U#63l8Jn}%2-=XpWS#@F0i;+wRON*CmKf$XFByUPZP@dC?p8H-fLa2Hrt z0oz^a;IESnxjNTFfB$q1Nq1!XFkW!Fg!k}aGJm?CkXpqXgDqhvd4a8S=}-7u>H@0} zu)$9L#@mnyUXVMVcj_;O{OK;pVFKB>NcfAk!RmY-!am@t3^A5d&pUY3-@b~pVi@%0z zJVbu_MJJ?)9PfhcE|AL!i49h5FuxbrxJa-K=9^6_Sbq2~>TjHrzhO3HsuyI?)qcff z3SE#73uKJq@4yw7u#H!H2pg((3d`eY!lA!%8?xF9@^zOP9sEtUAy?}j`s?L#2_mww%qjA0f%Fp+{a+hwk{8$>SNl-@ z&jmI}zy>?{`)jHta)KA+n=bp4kg2;Mzigz)jSGao8*Q+<(H_DcdDf{v2}Y-$aSHo_ zfIUtytA0=Z%Hprc3v%poC#2Nx3K!&ffy`3;J!^yY@d6v~vOj5T6I@_j1?=d22Y=Z% zWWy*Ak)QpOlRt?c@h-@n2dTg1ghc=Ma!Xjh7uXxioM58A4YEC!l^+BJY@Cz7B{pQL z7i5p;oRFfwLKox+fs9f74Y9!*M|ud`Gm^g^F0dc*5gp`zWe)xAz04B1+6%IytNn^e z*FWtvnO6mJ5+Tw5wZXEy!0KJWT)^hI!0r;TZchG^ZOEo<50RHY?bM%isZ(5#7YJnC zJmK$yOD$oSdx1Ub(w|huzNehRb`-Euf?4h3ZW}Vs3vzTMWTgvo<9=32AI0A}Hdu@o zShcIMiT;MWz$yf6_x%q3*7vhSUY+Hkzy2hI`W z2Y*l6kPVq0BKLE-)H9hM#JeD$6v*X-ME}qKIf;F4HB?!PX1=wkWE*4i2QOSWQq&&$UiA^ z-E85nw+*)33+#KBGm!SX?+;F4R|!}t!L0VN=^{(yJTJ)0{@@f@;%}u3a-u-?QT*Ly zgT;7({qZR$m^8NGF0ft#w)rCCSZe|{EfFE6TBejyIiWYU)=?HwLmt`68@rXu)1^)VaG;-oqEhEY)1im zoM2Y{KA&ufT;v5gU ztSb@zX4+uOy}*iH!Cd&;_pnpgu>w{~FsprZw;}VqAir|CRB6AJF39!**+=nLn`8+a z;|2C|B-n5l*t;K5f4hqv{7tqYSEqUC??u=AujtQ^Evu~jZ?-`C35ou%4K~RO?ANZ& zmgsM(3v7^p4R-SP*Yhor6TBevA90$rv=7|{`Q>hk+&Dw{yU_-#8|oo!LL}I!YG?hv zAYhLZ%&Om$c;#D7sT6rZc5$^|2}vtlkmCh1OY!%t4c5mCY?v#*m;7pi3+#6Sc67Rf zzib<_VTgyw=}$WKCm|``1$mZ0E+-`Vzr8GB{a#?1uJ$3uyx~Eo{yzDTl`+oA-x3=# z)eG_im;S_M6}li_704LH-w+$Daj=K5J6--y?5~Fl>@ERYdyhkZd-0h#Rln6SQdcDWebB=acDWbWxvpeV<$pn^ut5PUC79Jd?zSQGydZO2_9s4F>~kbM+? z=h$E|USO}g^r!Ma7g$FD+x>e7f9tzjBCj6kp}*TBA&tdOkvHz5{``bQ|JMea^=b-?Bp-jhD`8+{KjR{D*tmq4iL!3yM@11=UT$nUEv|@0}nZkMf7*7$|>wu zJ1Okr1heY*S{t&+3-Z^IkSkn}8wD~;@pmlI;;)Yv*d4C;q5Pi<>^=cIdY6O0CvC`v z0Ujd1;A$Tta=Z(2fIu!MB>KNLnBNPmjVnKu_Oamsm;QE$|8w&9`8k%zsa}wN*Z7r) zT6~M}p4!75~!11@=1uTRY96zsqgNYA?uX4>}=5fAs;U$Y%-U zBtoM9i=0?F(=0ErCRb&Mv!CMv`=EjP>*nNdwhh^o>LGGmq{t~Q$OQsfcc<{z+Xh?i z1(qMl-@Zzxu;~I;N-(Q^Y{E2-5;@NcvSTD=N-zjn*#ot{vSd16gEe|*=L!A9^ z7uYHR+kJR);2uX7}%vB)X%M1k}Z68&EcfGA-nd4b(u<>XK7Z>bBcmw*j+ z@;BawOz?v29|@_uAa`!3{u*x={-SNLy30L;{n90@@ONsVQ`pA@>~Vrw_4_=Qxhav0 zydbYygbR{fUSkkww02P&PAVlwsRPLW>{$Vr4m z|JMe~@&dc|Q74!L^EocC+XSqelfUg4##ACV_45$<_yQ+nn<&ozx*#tQ$hs-Q-%J~9 zxffWDtN$V6r~CX)VZYc${go2T(qF6zi6!|NSdu?!6qe-wIhzluCHdS0V6YU{vLwIC z73U?Xp5ZEI)h3qHNiqEdZcrNd`jQq6V7DLjhHKCjlqG^*Lzz++5enKL?V*p51#UwAVc`pAT{4I5X zy)9sao&3$VArrhHL#`$*?N4_>&KAhVNy1-m8>}wHL)hNZ0&@%nz(^L4H|BgUM3--DQLI@dBISO18v5OmKm{AYex)I{52kLpEIO zA#$CoOCqf!-UYcpAeR#o@f~F%MN zkslDqNrcoYa&53IFR)nG`bV|?#|1W0z`8m4`x?zi@z->rhseiWL#i@>296`j*ms{(*w+QDlwel-7-mD}c|lIc=Y3n&KZ>ibbV1%BkbM+? z2jGB|urXd>M!*Rs&UCm7>_P$CeT##?avO4WvWNZ-xQ0B0KVz;_bAxm4FR)^0ysnloC0?3-VRhk|%Llx(jlmKsMei{LQq%>iT#HJ7T+2 ze;8PT|C{3!_BH`~oM2Y{cDEsmydZR-jFiE8hd*P`+=){Nc`yG0=rDW*52gM z-$^7nioa?v$StnaZmF32_lSvNuW!@C@&u*(@@!O4r!_E6jXMO6w^}K-`?wFS~A#$t`b)NjT-2!N@##FD0ZVA>|#qTRoVS! zIl;tTq`AQE6tI8fgWZxj3TRy~OK#bfTlO33_{ls1aZ6q9HzcWgQ;AdVO9ZG&fVvY1 zGJ}n{=|k0KR(aSlDj)kpj^~b;!So4o8n+lZI1&ED7=a(`A3YcQLm7iXhx(GbRNZ}{ zd3P$lkAq9=u$!0BnbcTPZAtN~-uymUHRxwR8{d-3CUuC^SJc-R4l#u`$uW4klsMN;{o@cdHJ&Da**c!<<gkFN(6=LJxITUEoF_) zq$ZMTOX^HexPIzE&@1r)2IE^&V@Vw%b^PCQeIMy(KpQ(qjUcs!R2ZAl;`t4v7lSs| zkQzklMN)f?$n~Y9=YcjJB6R_&MWnWUE7#|co(9^uhg4TmQ%S9FlIs&l-wN8e9u#U~ z;$T%18A|`dc|H6MuJtA}Gp_9g9Qne1)^$lA1{B z5UDdk;rc$(D-YxB4pL)DZ6S3$EY~-Xeg?F$hSUgBFOmv>Dc6^hUJTlJh}0lbi%9JW z$@Mv;=YclvA$0+%sid|Ua(x2nX`qekNp&SPn$)^Oa(y`ITRB>1 zT|;W)K~OJ}x)T(xFD2c9XCET<`TjnRC4$2BQ;R{r^G}@p zmefR2he(|X3fK3MUbzovcaR!OY7433d*%8D($9c4){q)O>P1rFkLCJO(u+YG50M%~ zY7wbDd*u2Y((^zY_mH}P)KpU2K9cJbNKXT8Tu-VisnMj??Uw7qN#6q6=ufH*sf$Uy z{GnX$LD~n}=uGPSe}HOB>IqPkA0ILeUB$ECk~+8x)FDz;peR4-OL=w&sSkI8+Cpj; zC|ut_`h1>ULu%s=P%o0Y6BMp5CEbB%A0qX71E@u$@`Qf)~kg2MGv z4}gAW8_s@9Y9gsaq|O9|>-$Krd=F=LkQz&B3#sE<<@yHF&ww`8kQzbiMN;89=Nqt`jsx7G}KvDhx=&N}4TT%x%f;vR13KZoheJRiGAoXD_ zs4b*sfx`6-q|fKsHKaDyfO?VCouF`iDd`S8`w*$u-vPCVR6ZzNpF_I&Z#a7ospm*d zC3OubT%SPt@Y^_hJ*me?jV3h|6s`{^{n=YM+n>||QWulz2MX7FkbZvy&UPkMLaHsP zL{PYX3SagKy|W%?za=%9)FDuaG<$xfB8_hB=B4l6#HII1Rg>BX3i(HW(rbBk?>bN` zN!>^4Idwgm^y56cUX3js_U_&$MNiouY>ZD$|E&G zUH|qq(1UsQNm7@RN+)%Jy8ik9fbPb#i%4}Kl}f6Oy1wI8&_}U}a%eWG!=!qVItU8# zZCV3*H_uKb^**TtQXAFvHLF3d<=OG1R+9Sh6;RKq>(7&ZoM%UqT1=|xub>vF>kpAG z<=J7Rrjhz&6{v~odO7LRJ8<^fmClW=&7D9a_W#7!*p933(~V}XKmTy0B)lt=%lI2K zfAMvK)i}o2ncCr_989{gasM)&S$q9Cm1B~x)49cL)l=$|uh^72p1c1f7?-`lTg^{x zU$`F+@)J$h`4672x7*14FiAGS#%|#LhvTusCN~7vD&|U7k1t;mWEN8RKmT+FzF&{+ z_MZm9EEzj_qBm!6X}ftAi!rn5LT(V0$h!)#-{!W>Ot!Hv^abZLn`W|&%Xl8!uKzk$ zIuYMfQk*R0=WAGG5swORlt3{lxL$jrJ&v*A>P_S-9~($VtB04!!>@@l^gt2hdCOG7 ztQ1OK&Ff?q^bp3?=R5( z^=I>Zy_`2IY5peG9d_cM1uhgv#!7BF9h{ZoL-8wc)!2X&_!K~>pqZa^;Pas}gzJMo1n-iP`#Hav0) zJA!{d6$V)SW26B}4QOlQkJh%qhnP@-31BL!PJ2U-o>E&Iuf2gS^Sj{ltvAHz!C3re z#8zu-+iPp1s;g@c#J8*0-l$?lz82f%x|TG=j^B_9x%x z`4&0=k3+dXEygGK2i#EHG#1ewB*e5W0VM$q4a%Ia!;v$e08U|ZB=YE1nQ{=@X;&$ zvTBW(5ileiC78-dB6IlC4GIAZP)!C@qjtD96$_1}P%%1h$ZKI6TXl!-g>H<7+n}W| z*mh{iTR(^Cn*4_gsGof5C;$Ht_buR2Rae^yWF#o@3_>I**3d=`N;N2IQbY#~dV&K6 zMGZpRcqyWyO(6qO)COh}G7bl$MeBvnA3txjwTd^O;3Wa%s)$hWf>zL~XB<=n{-{T?39HXC*tjWEDa&UXZg?vQU-Z=>;ETnseb zmJ13D|IsYl!xLGIi{CVNy^nSn{NoO0_WK;54v>Y8qNNc#2GOg6=v6OO+r5jKN6Nw; ziwP1rQrVo?AjU)ORgK+~?Th4=3%9fq(#6ZDbkPkC1LC(s64Vij(QoYj*hL;E`{3Kj z+)qCGuKh7S7lP4Qb^*VEf?4*F(#r0{CfHOQm%wu0Ez2FdMf+07i-4_;vIu(%VYM&c zt(XW&m7Um`8ALvZ_G@n(=8bX=w{!72%7D%s*;{7f%oul+ymMD`6lQ@qN^coOe26$7 zSQdNRu8gDD_j`ue`le?Hjz0yX;Cz~^p&Hx%lcE};C7Jc(6^`C!|M~o9|K1UZ=RGGM zP(!WXD{nu^2#Yz8^EkS60Jb=jLv$1rf@UQ*f(KoIT&^taEV*3SNIz*-kR(KY88ZG< zGDQevhw;m$^wyoJG~jmo7|Jt>eLeQ0Z?>0SMGuq}KQ@!qOd1HX1}4$$zOxkvg+746 z@S{g~%kfaG?vsPmzoRDOyqG;*Ymedg+QZFoO9=t;FU7Zj`wdn}UE9+Q$Mb|+V0-bq znT-IUzIF6uc9IWI6>h0=uIi>}xu-|1AAfs>9`Zir-ltpuz$~w?%FMqPNVN1b4r1K8 zKc|R7X8+=xRQp???XNO$1;&C(aZO}IAvuIUBf%!|vSk2WK-VVkPqE!kc;8i&4v48F;N@HnCko=h)!8e}so(Q7qshO3 zyhYbFC)UxT6odB83D*d9oF^8urCnA}$$I|Rpxy8k+7XdV@K36Q?f``G5PHZCBp<-I z60@g!s2h%uhHPo*3gu`86N+VjrA5Wx=B9lnMo;`Hg>vdg+W1zas0L8F@x_2aexq>= z*J!71<&mpq^r@m;ep=4wOGP}N9$^&D>{;gqCQf_+EVdDgZT1tOgB+}l(47Ymcrm(w z;B!h*C_^wn8_f4kEY-Pb$^63Yss$!ib)|urx{R>Fje`hrgy|%i))&MW(kY_G8MRz~ zEn96^H}XsC$2qYv{u~5E*T!o7UEt%ZV>t+U@C4ywOaL0^+I44pO;x05H#H4%(@RB* zGvq=tV2l)j>+n>%A>ugsjhKUUGNUt!O7Mwc6d4FJ6#H7SiZ?k!F+^#hP%Wq|foK{H zS7I#&!fE;b`kfD*fLioqXpto+($CP7F-9*Rg4Wg9!LltA&LS&t zeyl%e_4N<4Zo*V{;<;R4>J*HQ@#iL9GNa!IqhD7Ls!A* z?4ot}#NlMtw3e#``ZJqhnG|&XIUBd3t zLen7#nUJAkdMF%Fj!VsG8b1%}LauT;O19bf9AqpQ1@{!l8oO)hzCRib4EXzPG685k zTj6}{9c{S?()sN-p7OA+1x-nRA^#lou!!Vm&#sm=>;6slzn&vVkUm?G;H0@8PxRY| zbGs9Vnfq1@i>~7dhlql|H)g^*0naXbhgp^2!o~@u6$qe$)^V$*=4K7Y;cmmNqO4)o zwMF2@r{@SahVwkP!~z_`7mR*xUo}}B!uKQ@F2n&57NS!`nm$2KN17ZGe87RgJh`;c zwl$ItA8N^m5#WQ8IXe0p=3dAMGDHy`U{fkNfE>weMx7)@q-Zl+Msb?wO}Qi)d5@-K z1h4WtIYm*B8*}v!KksbTKcFo^2GtU3sAQ110vsAtVxM^lL7~9~gPZsS8VAVrmGXF` z$ke)B$xkc)-^=M*k*1lF=cH-yAVl{h z?w(*7U>qbe|nOxOUejvla8X*1edSp~;3|bV+;~tnQ*x zbQB+rZ>kSy!7U}(d!>hujOlL^o<4iy-@yY9w|X5x{ivWj^=BE=m*8f36kUB3pTBqC zzQyc{@iV3mlGkxG(q3H)M>6_w{N+Wjp1mcOP%LR5P(nV8w( z+|KvnUw)4f6d03((YNCF^EW%Sd_U{K2#bANzSCtaNU~l0{_cIzZFrRPn}=YHcqrw2 z_TBha9$gQ$@@LuQ_@Tc23g7Vfh9wjT39IhEQioN1e)LpER`+6EI$pE;Am&F_c~P^< zZ*GCk!zqiFz$x3LE}XLZe5r^IQ9EUZ650v(zJ-EzlgPzxlGq7E_jETRS?m!o+GBTs z1Q2)09~qIS*_DcsjD~6W!8l3Pr!b_Qc^xj1Of@(;6o-#%BNtu@rN;r$n8?p4swZ^N z{<;|Se716-u+yZiT~MNhUs5qzzr6mniGaEZ8E5Y9e9q7cIZMSQW$S7$BkXnNz7}d} z4UZtaOEO}JCP)>=DUSd0kmfKl3Zz*cDWVpEM4FgWmyyN@E|e)z-khcc(EIK0FW;>W z@(aP7C>Vf;WQ5!pfP5E_1mC8e6ln*R)X3(uTT_4lu~LCx*ZtveR4_WWS%;&>^yNZe zgrxE783gL?(*>XqR(t_3<5M2Jq)eQFAjfQ}xJ^3fCQXXaZBbNFbH@;(W~|RlxdfVlNw+tt=80p|;#bK({++}~pMOHwG0)Da z{i6u^)NNYG8OvRX`4@x*_PvV|%J46}Ldj_rTDkc_%ukc~!9wy@+(#irB0&3NCg+?Y z)x7M^LesC&T zub9zw_EGR3&Gze+z~jOs9+}~PLctG8rOc0$#^@-SAi-FKtNrVoHKTjP9ziy!( zkf77Y{w1s4xHlcgC%$S2$6qX=LvnIcZKnK{|IUb`z5DGXiousWDANCIx|LYw6U&ke zSZ3zmEZT!M`MGjrFS7 zzzpUW>`hzSVdV?@T@fX&nCyZouDCXQoSBus#7T)%XJ&J!F7mL_?sg0LIBXdBXu<(F zVw#zM`NF?t_0&JCNAeG1#`^&S|Iu{M!KZQ_n?1(;qZ;Kr=o7>#yA1Ccx`alDH&mz% zlOyN99*byJL5%p9BRIWtPG8XM>HI?mpc0x8p77RsHJyJD6;;ZDwbA!UdNZz!`d+>z zGXb}*PZBWj4=(}NPbLAo1VO+RNdl(P&nibKB}tJ{B;wSq3f-qv5Q6xNbRjht;*xaO@SBNi<1 zGNNJ<84(_;^5;{Myl`aN`QLY6@z9l8Pnks`3C&}C1We7x)` zpMDIFOP)$2z(|YTxIIw+Jp8>~Kaa4ho}@u!DYTfnj3iQQ{-xaxo3F*26-mcaOcavV z>YWcW@>ukL%9}|(zxkq<&sWuw&kw;`#m{P&$(i`8=RZ>we+7rO^B?e+T;M{2IU{EmxZKC5nH#=iEQ=%C< zo9avl)atDZrH6kn>u15g8qGhOP`T3+Ox$kztR4Q1TH)niU%|%pHa$LQpMQ(rNb=8k z!OOo#CXj!dE&%^7KY0F0e10_HZxcV%GJd$DNBDCreeLUJG}Hq|T<9Lyxgxvv%7rF> zF1Vb@&`_D==`oS7Y&Of5Oe{BJId3_B|A^l!KVC)otY(O+mQr+hk-glR>i3MU!*Xn5 z;lfh%oOq1OM~Gr)pwL6j+oz2KtD^mj?4Br&&1m{Y>bC)v13C`fcywav`x$9!-}rix zp07UVrRRu0ke;z2pyzUzo@w$g^m~|DI=oqzn2-&X={>*ZhY2@=(rjp`rqYJUv_}?(aiWCaQ@IWxkgf%lmRNRJruRd z8L?dSm{&!PPi5z~?`Fgj_G@(#&9CQq(VY5wqPh5d+P4gmwen&=5x<`8<97pUtux}+ zWxf}`HgkvW?cuk0RT4iV1;0mb7X3#Z&Hp}r-9p_kKf=T_eQtE;mUi%)@VFPh!c_kC zf2SS(ZCaVc@3d#U{A<05`0X9^6Y%?I2K^jK`*vlI@F%cud%J}K#)2zPAFJ~Bp{^_q zNkP>~!VWI6x_2}dtTxM5PR=#^z(%c1{L?=1TH2=WMa++EZigK6=6cD|JCz(KY|Kaw z`;8SzYV>~EOO5BplNw(JK#g3FG-sAysRwFfpHlHR8vY9qfgo7P&#s-K%{wT<^^#Dj zYv5_$uQfgP{gmtXXWt#NO_1F65@W#{vuy3;Q_Vi#fN+x7_zZdLJ;+`N7+VnoBbND&60m%f+YNcvjmfxdHH z>)&4f=Z6gP1^aJ?cVnTh6*rUdLwI)wBXXCt-`z2EJTlQ`gD4o3Hl)a_uyTh`(D9PA zki0Arlh0xnfFX>o zSeanB;${;IopMpqsF-~fnuuLeo{Nu?RJq$;Vn;CR4whq09hH!j1RI^0m zfvnCd;xVEl!7MQrR7lw?pF+ZrwP*mbRCj{xe|!LJhjbF}hi|Z751|g|P~u0-zjb~0 zlMEu;sqaSkO$0SVJxuJ40w2XhwR+29(#W~&DRyN*mamb8>Se|t4W@M=((ktwjpNc2 zCstVIGNXIVvUd&ZTcO_2fEk+rmajCz-Pr}mR2}QG+~J7m&ww7OoUhTGS9MM`-a02t zAwVK7N7r!ab`Hise7n(TxQRsLsx|<*pCe-+a0aW}u>a?JmLJqZz&idbp|g{Wfl`BU z)$*JjX90{R5HkXP4|%Sm-BW8IoC3-6Lp?{-wb)o2;dM~!siJy*9^g9O<nB@Qc3! zmrF9JYG(c1N@|9;Lbk`lszE(%Tp76LYATJMy23hxwwSm|7Mln`Irowx^)i5WKoh3$kLy{a3!wuA4Po~KDErao-uv?4@p(>qpV{D zkLJBpO<-r}T7rmVOkg3#QWb6Xj%9kuK*R!&)-G68fVtndPB9SlCKL_?lJRVR{109E z)j1-rU+Rhqm>6pdyF!=s{M7X*CY=nIV8@GzV1cs(7+N+-OAR29P_o>tM5#;BzV&AK zwxZmlYmuK%HkL+sSg^xG3On>HabL2fi6t&=L*aq;p&o`t2{Hk}t#^iK1}Qj3&(Q4T zlL0*GRAc}OA(nbF(MZE04^Le-2hijF?^}@^ihq|uG(=R=OGzlaQ543)Eyq6mI2WG|*QL%>+XAE^9+ zC9ji8pZJlu5{x}2Wo8vvu&apf85C`)h_0@PZVE=Xu!bLeEnzEsZf0L7OnMieZY33T zxmfPc5;-VG5zHNCY!EDwW5`CpoXk>{b+IxU%mTKzPPlPX4!S8S3WC0*LJ$N*)7y{$ z-BzSZVG8O?vy8}mjK{u}6L<$dZz^q9RY_5>cm#)*}#rj}igez#;usp=@nPzfJM? zAVkQtHIcE9NfDK)%P2v3A<5V9zGKd22$r;f5aC%WAxqC}PdiCtyPR%R<(m;X1xWiA z;2^D~_t~m25H4S#uk00Zr4E)G^Mgfq0II|}K~QE6E)BCT&F1$Od?%xY^Z>E|-3^Mq zS3%_8F{3+z(api=7kG~%7=15T_Led8UWDnvSXF5-Rvl3GZ&=yKMtC+ZUu+d!a&fHh zCy;##wh3Y|ei_#AW$n*~S)q!f-AY_x86Ke`4HwGi3sSO0 zq4LZ9h~zlSn}S7u3<6Ac69l$=_~O`YpZEtEi$2dX!e3(waB*zRC%LFPyr##X@V}2A z+guU;&(TH%GZqXpyC@*t^(_j*8G+m##=^eTL$t7S#`2C7r!hr;jBKhsf%MKrTZ+~^ zf&|V8Xc(0s#h;^B!Vh*xr@l6sdh8i$1%q?m(+oxr(vvV4!}{$$Ki7;k!;s^^e21%Yiq8G^gwZr_j>j?~vsxWd zFFWlFw#ReQ2k16O_}qUfg_MLHW|@#kRU0Yv*vAecrBD#-l6{5S_C?(! zO#9W#W2COn*XZd#OTbIhfJc9pq5-SXir(apkrtE>@DU6}yJ#}#1ekMxkMP@$>f$zW zW_10I+dn{jHV`#>5+A~kk0W{TPg~2Hw)&TiksbY0dZ(3%h9I;4ZNEb4pK7j#AmF0Y zkYzQ7)6J}+I)QvtHr{ZYJcOl;)-BmI!K$5F!8fDltXvFGgc|!L{3CeF5H--h{1b(w zzcyBo5JyV!Re-#lA=r~6 zuB#~P?FVJ;$IpW+6@M^CZ6bTxx@~%}xwuZhwOc-WF~9dAhVzE5O5PZk2BDGgGLGv` zciS{@)a!XNn(}_se^Of9Hr$=^jdXi5CR!5)$W8ndkerdmFYs9&T^Zy8)OIuaFXR~F zxx09+1@GV2;aiYp+gt2ys7+|LZ#V_2B|iM9uotc3eq+* zi<>Y|huJ(dcH0WPTK0K%=RIcGHUkysq}&t)BIgH!Wi3YdTtFDKastC+T~-)~KZHmU zf2?BsF#_?&EV+pI139Kac(%R^i=@tYIqy@ds#n^sGK-by}c+xf9$mt)DW{68uE!M8C9x{B+60npI=vUGS4(N z{iP}e`?_t$A@z5b4JhbXSaH(GW=cpzJM)g`=X-|&XrN}luP#G`oIDydsluCYw2NoTUNYiL27rBAr zbv68fT%+rtTznSbvjCsP_>`cpMvwzNKq8I8^!>e}i!sD(1V~!j}TF&7Oh3=&mr?oj*KqY-h&F zlZiLGF{6V>$lhO(4eT#}(r2WABTgXQ^C_v2c7PZ6StH& zL)))oIW#d1LoV&X5imML352E;*POOzUwpBIs`*7SK!BVM-MXK?K}2?7*JjZ;Pz1s7 zc|;QGO&@{`Gr>bxPiZBJmhZY(DHv$X`2R`p+;i_D=brsYn>+p7JJWHe+3sBgV-Y{L z-O_R~1PUfltpMRpE>yxW=JzjvT3o;WPuW@7{kB6rZrcjDioI_|Cv8ZY{gC<%v6pG} z+gMD&^ik5nu_nPCV>9IZ-9uLa`#wQfG4fIDW7*2kDFhPjKp=d9VA4pR5AC9n z_9Jx@;6jzE-KL(Pk0Jk7@zB`yu^I8Ny#oq;;YAp=tTi;kgP6ROOGz1uZBu2=U)yaV zRi+Jo8Aw4dA*A4D4uaMNT0MyVl`lEyS3V)=_qoeO|2HQ%=)W+GD1Ox&DE9H9n3=z7 zy|rz9KKWaFCGfj$sKO65I_ZoXI)Sv2qy{R(Q&=ak-}!Npbq_CgV4wB4z<$$A7xw2c zMO31&J4o11C+x$#u#-=OAlD4n6=bmvJ$)ORH2f?CP3R;wzPv4$in_G~gk~zfn4gF* zr0<5@`u)q>fEN3k>iaH|s zmxuQL0K=ow2}sZ3@k-Eq41FO6mG!r9qx3Xe*(3NAI-*#2loY^t!7A8SNFN}dxQd9H zcY@b=L72;YDl3>==~HP_Lr1xEx1#I~>-oTyctCACg#r;QFD->}ovkdQ;|iVmqn=AJ z)lwj2Q0J4|JkvcWJ=5>me9zgpMW}N?0k3 zo4SnrgyF?ojQQBbjG`hD0vDx>hGTJ)gp*#J6=5AFV$lewa8X$0-ID7@&Lm2&YXJ8a z+5-51YFc-3G);1a1@u@SdI1YU|=Lippop)9FgpUSSI=b+bG_6kw6=NCHjj$=qtA%GSOh*ZB-@iS#C$}++X z5kFb}cM>kT0^qbglx3+9PzevGNB~}SR#Cl>O|u;tDf(U1H(?)KjwyG`D8sXWOBxLE z=K-}A5fD-9wksmNitN)DD~gG9MB>m!NPLdGyN(@>bv8t-I2XC0e>DzS`t9tuFY&V= z01s4HHOM34UJyBK__$&wvcZtLa+HJRRAA~j^GbAvX(OhN7X^?XF56=?zRpX~jyc2- zR1J_DW8}J6udcC?U2`Cm zc;V8uJN;EX$M2l(w8rn|$X<=-1+T_uLFNo-yv>FVYat(~KS*jlZ+f+U8ykDHo}CaB z84X%5ot3HlqY=3ucl)iP!gIabn12lA&sF-$G^NKTm<~rXD!tIhJ{Efkw$Z()Z=PG;GbG{VKe6OF#?~sRHGTG_;kZ{~J7=l1MEdoHED_AJ0Y0+k{rHyBX<%idci z1=-j~R6@7u{oT!q;Z|vH}D*l}MF&V^4lhi(e%EPJnKRKTT#Q0;a`3 z5RNKJkUbPTu*#Zs$69~C7DSiT-ZC$iKe5%doQta{t>Jd`wh%gzg~iAm6bGzfXVQ%G z6Kwd(-{3r!6KGjrjDr++I^rmN>jQ2Jxryr~ysqROqAX9!Lj|0IX4`brjAx5c{}LZ9 z^5y{l2DX3IV9ja~{*1$Y`z#b8#lL|Tv?WhF^jWUxQ%w34l0Jn*L7oB>2ii^^HbTc0Y?M%qd|l;)a62E`H0cNrN{^A zT@z`kg##9eK)IjxA1k$()Oo}t$}GR|gp9|xapb<=J`(vw_EeBq%d61griCQ6BMYjE z0=(ul4$=VDYf*s^KB$^XaZ&%GeH++Au_-P)vIxCs5fm=e2^Rw30&5f1NcX3YB%)Qw zk6*c&?EB=Z5~S`>0nQh?P`8(W%^U1RH%sRfCa4*eMEuH0KnN(OLFz z5gXtSMtkW607}`TqSD+Rno8kq*&_Jm^n+MBWP(-=ED9=d0n*=p&G{y z3!JnVJ>h0PCe~&QJ4T#qSLi3eLTD)lu9r3(1#hp}_yO(}+Dl;gb*Az%s26NH!xhM# zCh^o|r2JCY!_FKPW_A#geb87p1OSw@x%Rzp9W)jQ{)ojj z`icExf%q3Ijt7B;Mo$7IksyU$^V0V4Ztpt9-2O}_5k>!?Hc52tHTqVys?QLr)E)yz zr0Cm*yxhOVkqmgH;n=5}sw({ov%z*<0PJD5eWKeAobl8AfJ}5bdX|vtbyC>A2D_t( z^NHU;&jS&$tF*w#s8z&fGnoiPnIF|Rs}v?Z+ruOkk%rcTLInMfc3U1QD#}0X#p;{F z=UUj&)bV}{nyZRu<`R8x zV9yK2f=e(HT>>JhG6~e8qQ+9kyxzu5Z5lVTH*jX2e9qRhL$!_w`=n~{jD`Ql!k$8?e1!v7t_}Iu+cyuYWtVM3d8u;K@%(HPiFMS|!$0;ac zB2_xqgxX&FK18O<$V9#bSk67fZ;|z(f$R>w*RuE8fBDIUI$Zk*1wvWedMSdpecH9J zUeLN@1sMV5%Ey)R0FZKK3?z{ioOCYP33$+>DrM{kqy%^fB8QmX*d0*kSJ7iq8eyb%lbdZ*MlbiN| zu)hE zYDjuZG=TWB1&VDVzT~IbK)e=Bb=TQAvT%SxIHlR2}>WFf?CZ4Doj$A(3JsEDquz}qWm z1%r>#-1v1E-Y>C!1`{_d`p&RE+gW8n|5MTW#d#c!--Frf&9}ZEZgoF>SlJgw_%K2T z3x^b)^Ol5)8fZl8F0yj|WL}fi9L(NKk;VJrPdYLye8)AJ_%Uo*N0dMSSUe{HLUd|Z z9FC<7OsxyEZZilmY#|J9{0s5CdTYRXK5BL8h8^o62+E1ZSE7Ye13b){fJ7qVnjrFI zigvmNjOaUrbPH;u{}~p2+l+ox5#3gSIvhnO50Twk6M$Pu2V&~@U|88#hJ`KcP`=9d z5m;kHnt=?@yJT=yZ4oNH@q|kajdB^Bj?qA;MS6M3qSq%t4Y&@h&qP#cJQ60|x)NB| z7FE)}?U)RAtJh4ebJm3imL{+zK?rjP=ir8O*2O;+;;D6gyBI-Cd;_mozH?Uk^uW4O z7P1IWi0DDQ7-Q)|`|Qz91p$ax!jTarpz;j#H4}I8qgt|yV<25+8;qIv0UO5^SA>@o zu#hUiLS+gsOQGh;ClH{Ro2b-AfigCXmtyUG!=l@UMdR2rG#I-S8<8H1H#m*(LqZ_U z5lCM(rEMH*g6FU=r-*=@QC>Y(1YvgM|Lr0DcE-Ch4+qY|{{`t5bwmY;-z=T$Ds<#q zxA8Z?Y~knBWz=xYV_46ZKq1z)U^H%DdlJh|@hfhR!dDaTjE=x(v%SU#;x5OeLK%^C z`w_{oes&hlO>SrXOvg+G5H8H2FK{ak@tbk^IDDeyG13vMqEw>*ej~Or8vcgaue=%% z!Tzjsc-P85?P{(L&Qc2?D5#K15y4eo!#%Qw_NIy*S^axrEiT8H`76$)!#`r4dAR!K zH)ei_sV?TXKNcD@pOIPbk64A9hHtKMVf-@n$9nW=+$AR~qdsJZTA;MASp+QyHxNRZ zrHj~xnQoGrqZ5iq$MC@MHn-PkNaXVJ8Q8Jb@Sl&m4b*P<&qv(`YB&7nqi!Qm%!d^2 zn~a&w()H8U8X;|1d zH)~kg2S!6L;tma$BnL#TZkSb-T>*)o{-CT7sy?u1EqbMR4!8QYh6m35TyVG=O@>Fi zx1!WzB(TdG6dUYswV4SSslIZh{xbD77hf;uSA1Sk9z|qjp&4zZ+JXU|2c=xdqwtGe z^%3?1l$sGnxBGyB%5XFP$MuaFEJbD=n+OZ?;DirK46N0w22}NNRCR#;lB_P^%o>#3 zL*P^e%c)VACV+sl6$b!FB(;ZTm3T6Kn~U0Y8^*I8%kv`HWZ9G3Om&ve+n71!DzUe42STF;sTGlpmjM; zs3=F{vUiMWIQJbULOC4T4WNn9hgn72!UGTgOt`ZLlpGfAvMtG-3V&<70Iw{7JN)X$ zS8#`4bMY12soW zc<)Aw_t@yDlCjYdryKKU3pbnbw!`65S7+7ZU0PfQcgUei)EKN;?G~F={hm6<*@-2m z{cy#xC|?=F0vf|YJvaG;k%^VxG%N?;MKC&ka-m(WqE#n(t>DyB29jr{*o!A;pcs0b%O0h7 zC)+Rn8aRULg~&hHd;hGEE*3iFZTi8j79{bK)q-Xe;`Tp+v1=w6E#IQ$s#??TdeXjq zahwW)urL4d3aU8lW1MMRzCZJ)V-dIfC|e&(bfpS3Xiy*mN57(?M21#S{aUgRF)3|n zq1|)De-L)diiNBZA%_%1fj6!o$WkVo#$RkR5Up?tP%0xiJ<^?x^;Sb10UBKaX7iu|Ml95b&`%ZG$cm7t|Ehzi<^Spsp06oUeI_si4cm&^Zv&b2C+wG3sXaEy!`RqmNA{+eYPfmmF zs`oWx-QG*w-Y-Y*X#7m_UXFY3SGd<~|Fb7rv~ha$Z}w=^{KW6WD@~rHh39Lg`9}@I zLC3$#`ym&CTbO@HfmHMxEYZZ2x`2WxET!(lX*m!K?5munnr?M4uqEYFq^) zTh*-@MJVxR1$J;V|D}f{DlZaVnp37JU%pZp%4)6k@p4?%#kD0x2Cum*KLF``; z;p4P92oS~&T9H%Qw~$eACG8rfk$NH+MUpgst~eDO=@>!TFFcd-rr5Wo#N3C78sqR> zk%mcfQw+=~)Sx+O8_aZh=-Koha{DD6r}8S|Z5qb0D{_oePhDPabU2lL+qfiq=0Z;G z2kxU>(>~F*n9gy6FlWuujOl)uRE)53!o>{f*&&Uz+q-FXDMO|N0MUlX@&LLM{Ou_y z3uW(>R2|i!veohFj7oum7$Fd&iQQVhJAbDI812trs>IV|Bgw|z+lFLtTF&d5 zNpfwUd_j_2Ub1EHNu`v1d7+0$f+x&1k#CM)#jFCTBcyYvW%;hX)}hjDO(h|F5*JWM z^mkK5Tly#_kkt--egpc@ebr+`&?#5UA+^!Dh7p6O~8V&;Wj zn6gHP#m;4;^@`~Cazuk^U6GCW9s2P%Xw0DHIi-D)>O2#dv^w8`O}kd`kdpweDTRjm zI0{YcSS1@%HM}Ts_yoMMxWkyn^&|A@FGT1!JXKoL$p6mI6#1)BPYWL`Nlg7}Gz@~` z2h|qBr6jV}&CiJ~kk>vSkJV=E9BdQ`8*nnsg*CiGa}Wm(PJ02UQ~yi)7CKtbl_KCE z^QCj3|9?6-fy#3PN~**&$VE_6Fz`#@t%5aOF6rsG`=Y@I`0?su6tY5&Bt2`HcS?R-hvKP5cvDgE00Nn<;5X6l071 zFw2%s7z#d?^l`QZ(}RnFfqz;YB`!!+(0akV>iHK8vdrJ$(&%L{kE8R!TR(Xx3ld$h zCYOvgfdu8K(Ej@cE)tB(xI+q{j!&ThiEfM6DMRvkbqXCMnJHb=qKg|+yFf&WWM)lU zv4R8(e(@^k_Z@icVnPRq?Sw8A6MtcG`~p#Xnfdza_BY(#AuhyPA5+KVN5*-k zne1a1ze(bI->1J#p@3r|#3X|4>oczK?HTK*-@{9OsSc4S?hRNk(KA znj7=~&;$EE6&ek%A?|68m8kXQUCZ$+i@UYfz`xfau@xwQVS;k0{L3&@CQUmLZGzA^ zoXq84o^v)r15{(i>ip%gtRd0XL9KSjVA%?zu^PG8!N&Y^4jUBS(y83|+na-%&N*yf z)*_6KM_$lmR6FV-AVX3|&hs)B+<{$n_ZbcM;Y!LWUbGEZlZsEL>}emzb$r@(8ZQ*~ z;oLt4*&A`x>b^C`{7q)>4Tw;FMnRKQ1}FlbXMe{8F*r%z7PMHj=VaM&nX`OGHMmA+ zM5`o>g=&s-;HF_BIKi@|#$60xnZd>JuHlb6;b_;SU&3-m)~hCZfeKx~{WS;eYN2oX z1FlOdSVM*q0Aj>08RInJiHhi3*kvXDAVJi@6^alb^{Sp_f5>Xr45T%_z7%|^TTY%w z(5uH7nNd!bcTngVIMvXt3F;(N1y1go@oT}~jR)#1&M^@77>{Z(Tpq}WfH({DtGV$d zM8eEQ)vjD8nPXjX>Ka1A%G_{SmeCl%R1764xIM~mHf%B)KZi7g2PW1@dbBg|Mk@FB0h{pmjsD&phY^8ZX zxz}X(Ci1dZ6Fi)$cK4g0Y%^XCW>f@Tu~Rv)9oR_+(ZL|MGJd3Wfye_2Vnk%M8^zSC zju_#eUMQ-^PXiORW@)DKS?q=q&Te?$jB!;JKbqu+Ry+Jw4xlK zH^6`t+W+jNm{CHGfzkWN+sa>d0bg(pAQg*Q*^d-ZRQ>}*QcJ!2t4N{V|J9g-OuSFL zC?|$rw*fh;^lws#O$XwP=!9`6(OYWY;jdD^ZT{ziW-@`;?je9kIbC!g4LOFmJ8 z#ZLr7bND%R89gC_As%P1B*VV66Jua48rH3a_Ta;0|Lpe|h`3?`NxY%SQ9@A4Z$h)~ z7uJhr<$R}9MQIj{6o7MR42I8<*V38Fqb30c>~DXEQk7B?J`i6gVL@G?{RxIAJPQeM z!}|D_Sn~A#{t&+hP2KE9Gefdhn%TRsdqc}GREtgj6Qq7vpOwR+ODcA~%W$Tmb@zG0 zdbbX@a*SZ~o%m%)0lr!a0ODuhThPim!<@QLofLNs9*v?7ZH2_A7bEE>hsDMF;~Q}J z;&SDbnV|&B;l~+CVs=C^y6GH+O);^7EFrJwELE7%OAEpS_pX6f;-!q}`FIv@3c}aD zOCAu-cpX9>)0&cNFVDeS&>x|c2MK|O5?qayXh+_=ts@7vlTU8ry#2A#)~`G2`1wbp z@xRddzO4AA(!Hzm`BAW}Q9I)%=M+;CN+6WOg+ zHR|xy@Z%MFs6)N_jwRW2RVtQyNmZg;QMH=+iS_|}Qi9ozq~?7}QmK6EWnHL}$=^R! zm7-nTRA8T}6cA^AGUfW+axgp2?}LV^eOT<}x!48zdt_))vwkbT@8-$vHPWDkZ=L*( z+9+;=Hp}w8U9bDJPeFfT^goVJAs7>i9ws+V{{DA|VSLBCwKuKdRU84L7k;t`=>59xC+Sz|-Z@|(j?|>(yD7}4bI|DAgF*#t5lmY+ZXF~FmfB%3W z9_O25QOA>SZ2L7W{+IV}fXgIj;7qY^eZPmM#P_OOjjGwFbRj4u_oc$*8!X8#7;*+kJ*~&h!V>1e*^1gt)oB3 zOQSH16Hy>E7yOcr9q9DY} z%z$ukL9lH3WSnxYAY7h2U9P}1!k>dS#{4b?;mC_i}>Y!vPQ_k8SUR?yQOl%30Ey!PP z`VnY-;xOifwv6E`sndR>O6`w1D31;RUd+uDb9M!q&gmFCX5Z52F}s$^1TZ7V&)ZL@ zZv6l@pZ*Azu?^$n9hk4Nd>>rgKF@_dqtDzQeYP$m-;YY+dwi^Bxjeu42hs;4P1w&! z_ILgFH%eosQT`?fz*mjp2%n=qeIN~908$Uf-II9}=vIH^X`X?H#yrW)-m8<_oN5-C2YS@zuPq_X4k!hv6^d z@W{V+2~@;JuFS271y>?MJ*UWgD{f|EGXSQ+Vb=Y2t{&RdCy=5Wj!)C1p_b|v{6NP@ zpcykS!=O;}Qe8|x`QF6gNzJu)!#3DE_~$GBiSy6D@ki*t319I3<3~ukG$T?vIQ#nn z1vb33l=ioT_E){RfD9WwR)D#dovnqT5M5WLv1m&tBRm6-;9Ks5VqP?fZZQdhC-k=} zUaKbDC9pW9SfR;b{pX{q5*DJ{zdOZ`?MEg)Rrio-)a8IDtFFzd!6w{ zJLkJ0JdvmxC0eZMji}h=xFh>?%rawn20lFcX+1&Og=K9>mN>g~1il?%zwz?#B?!fH ziqHTSfx6pF4aW$`G2YCU4`fHNw_U<&QZ)Q-vwKE+nMNM`p)`; z#S@k0RgrkrjGyMnbNak;OgUuk5TH9!tQgk7bud{cHQHU&s%ljtOG+cQugB0g2i=)uAV*PmrS6ZFFO z$o&p5c$r7tHkV7kWhgUiFg8-$Iwah4uiw z9|EHA3(gi&7T$%8+c1H57H{%0%jC;C7PPRNp7Z(k~P-9mWi*+V1yy$(`*a(cPGX!%yWue~Y# z2kti8-)#i_)}={Dd-8Dz^qYd9#f)X5U+<-wex)38X(cIj6!ojtk}?=<@P4w-)M zY)_@%Nt3+vdmvlrw|%fnzdyg1q~8?W#rY?19}@l6a+Vw3TF3aThVfYq6C_Nc>t(QH zeRX&bw#rISA7%3@^@@jk8|(LYSGDmf_6HfMj`hM40CufnLiUk)3M;=cKWi?|Bl!ns zRBm8%lvYxop<4jJ!(IDCfAeZpn#_ z%;n}ic=seX7cfD_0KZ;|fboT%-~}7(FZE0+6&g5_I*h%xwaq2y?>DN_GsD`*JJvAkP!8tLH$js7u4Pa_3+G~+NXN}Z9LBl zXaNCDf^h4hfzaVFF9<@d>DOhXmiVeYyk5yjuu6n=lJt{30#f8mS8Q`a-KVzc`9sS&7G`z%?5Q2GZ5;NgYoYHZu z*y*KAGgehX-K<7}8s2GOj}440FneXRuYz|>#%Ltqp-^bWX-F~TW3-;kmQoF6ssKSQ zp`r#T{V-8Zh|_hmQb=?nnu2^gyEEAqLre9xOlJ6+ShTxyK6)0PVu6B~Y%A}tBrKK) z8eW0bY33S=vU`(y42o`ZHwWy7n~_eutL9;o;8V|g0m^4Id?lvnY~CZhD7?L(yFNMA zXyE$+cv*n&VLpMc&4YHf!1P)8ik-?-K~uw((x%^f&ac_BQ7zGRFy;sHQGwLS-1V;6 z8%Ke?R8&ub1G7p3UuE`L$L7;Nv=%U=AZvOm%o z4NbETN|aVj9!+D_f}WOVBQ42Jk{X|DfBI+jRE^xSd?67$s-3493K(GD@N$VPED%5wW z3>=@s!!Sm)FZc3EMXLB`40E%Wd;P~$ygZ5XJEp$`z*u6kT@E`SGr0QG9LJ&C`=o*) z9^=DKbMBHFU5EYA`{Am6-&QCWXLii5RO7_0P7}K*{Q!RpL;WJ1!UKChN7LNh6`APS z`a+Yd5H^OK4iT~y(rNkclKmQ)+Fl@~V-L40R zv;;s)u{3%LfH$L8)n3Lbp6CYF@t~l@SoB4wol6T%qkMZa%4l}h9!GFZbJvn;484fs zFD}F%(-^YB?2IycWQ|DRDq1dBIR_-XSiK6_yO&c2XI0LvBFPoG@jvhjSpzmbwNP1b zd+Aii@2?2tV*6#--n=f7hyeI>-cge-v)6dGhbG|pTzlp&n);P%ARRdNm!>{5|MdCS zg^cfDjQK}V%;6t)8hMgq;%YI1d;}DDgVrpo3QU@SfC;u?3^o#p^Al`w_zy6LwnYMp0B+j8<*U$}`-`JU%nO~#vF+LN!qy$+v?D8~0)++4!x5 zLm!=~I)c=E`??bj-JtF*P2OAS-W#s&J(;{W+r4)>?ls%vS3*2Khvn5_RA&8}rQ`!N z9uKvFeg%j^HNA6$M;Y-*t#Of6wRz1>Hi`$fARE=$nD#804h;ukWHN&7J0z>oJ#;;7 z49p9%pJLQOWxstaE4^Uf@X}%*4uZ!(7}z%$v>fM6$r>`dj$DM6=`GpZhUhEpe(D%! z+b~e*yMS=PXz*=HJmu}s$_uThe!&tkpp#4y8dtKNU!j9<1$?&9UNMIKQ8Um4vI%1y z$19DD_WSmCSvF#g!?Fe2g`LgMt9$m7w|Qiu2kB+y&B#4W)O$t1j|;q#E$`7drg0+r zXtw{p9PBfdX%r4Bv`Aq>mcEC2X8!WU_WkHTxyVhQgzegDj z|KV?-jkHv0uF)`Dx~t>b^Z6nN@RTR;K4=4^kdwLWP8I*aZHbn@!fk2zv zm>-9C=Z7xwjI~@q!C1;a8<7`rL40%Sh`*Sz{}EwxY7t+d|ALUP5P#@Z)4|Jh-SRDc zRx|VcTRqG~m_yc``9+$oKI3<6?{M?+Z6Xmr_H(7>(p8SMMAf~IllM+?@7;uZ&GwVn zvo((0;-SQu`O6aw!Cy%4co<6PuSp6*V@w7LM&vDm31FC?B4;GNz`obKtQtZGD@rK* z(ELh2W1CXY0lPAd>d`c&=xh^ARsL&=%US3G_y-K}rPNeb^B?HZ^#iMo`6G+Y7VC^G zie&m2&y`{J#8I_&ie7;Y)oe13BiB&zksRg0s+MnciA%HdRys7h>}#Rt)5&|?+lPGg&~6L}2F=a4GS|Lk8*SJc9x9(O+uWozc)4O}IQ4ubgLKjhN3*+_{r z_T-0NC#?;eTDKI)`X=@eMZfkUw{UzyKLouj-o$%bE9lq)b~zS+ z@rqz`;tk?aV0ZsFT^^><_=RqDnjR5+{vv?`i%M^WJaE8dFipSpQNkfhNe~9Ts8nb# z$4Vk$0-Z19tl{WtO;*+%+D!?PY{{!X;C%C+c0RorvhY@j#dtsq`Uxd4O;hXgW6UVT zWNJhK0lc;G0ljIybZQ1JFpy>MZBVUnj~q}ya}Ge#VUlHc;tiNq4id(uy(CUR*4)`1 z7C<*mZDoJ*ikiO;K-{2Xu}mMC?UUqe!dpSU@tk$>55Otpq;vqsFM&hsJh2-?8D;^p z2pzO+S%FIZuBcY=kYEnG?%uR~TmP!1s$;Fkt?3wL-##D2nvtv%qx||eY)crc_fgp84v6ch>VjlcHNc6gdhmXUL{;g5 zO5zJdGkCk_Z(=>Sbe@3MGu_@ciAj+dZJoN`Y2xv2Zx9?9TCg%huOCA}&m3`BRs>?? z>x(aKTUnI}DI;t=k1;R!?Qt_l%y!REL@A;7(2 zRy>~M&!(F_hDP&e=j?;$&!+45&mZ3b_;V-6+7Ew5BSE`A{tUotMi zuhhbs3~VYzen$Tj_w1=OzzZVRoo?$IEDG z!OWfwss5wl3J(SC#UGPC-^~YowjH$mKEaSarM{4u14TDKXEYq6d;;o!a`gltfOs1J z18|(ok0F&`#l){M;~APOP_5c5`_Z`T5&TNpXYB8F*Im3%o5aKy>`#?{5X4UT`sLT6$l zM%&U`%Ms$^MOx+T@tZ{*UrXNm;vdevN_Foq$$PK3_xkf5g&!T~KB78S{!dWh{4<%K zjLQ*e@B)(Y5|3LulFvAS-e(^{sFi&`_611xS;>TAn;qAhZ#A zo#b|9y+F-lAb5aCEqv4v6%h;=2c`wUTX-i-Pw=IhH**b6pDJyOl&t`0#PwT(r~LOx+HxYXmr3Qg9L zOuEu!4M}+}a=21qsylw&N_IS_y^bwkgRY>eBtuI{aA=u3u;qL0XRdA>v%r=A4@Ecr zlDzlTeCJ+&buaH>^_*RFohQ)L%Ts@RpFHwTZ|;ycJ{+mv_5n@#$ymyb|IT4nX8C@b zG*6#@O8$H{?^$_|qQ(jF9gk@o-z->ib{btB-g5lTC552@6gWs79N~K*MKaMx&_dEg z?p)NFfr3)*3>4&2#jzL)7|9egc@tV<`676sTj`1AyML(q?zd-RQv%$)?l_maW1e>? zxP23`?~7WUINl|7=B%0VpVuz_wNrrmlkcKaAU<4k7f60v>mUi>MOT4o7p#9ScB|0@ zD~JQALhPYOl}B?N58i^>Uuu%5yfB8-co_c;6&yV#zY& zHwSN6rN=K)_YeMI7s?}z>4RBRVvYC_Q==SXdO!GRu-T)6)|Ee!|KYud8;x?RiFM@; z{K_^O-=hXQ`%B6BD9=*gK@Q;}~*x9LNC$h*`NUEGH8k2WF~;i@{$>`b{2 zu}Nw_?*9PV^;?hq^T$%kC?&b}a7#M%XVuC?6b}S?IIA~DLohZDR z`Q~T5dVj{N_hh;{Vh_Z}H~FSC(V^T-3Sc8^k(o(_Xoi>uRk{Rba;C0)qcUE-G{e>K zp1cVYGYy$D=>iN1nD+W6ooI%a>}ZyW|MY4#?lXb5M8^3BmajSOGOtd^a252%N*X## z#;4&uSbyS%*pO=f~>L0isf}KN0pO z@n?m)k*xvMp7kvas^^!^Ym3@aiGKi952Im;NV-S4!HrsM$~m?fyGKU`%TN%XuPHw0 zUMxAK|9(CPAROrr@AlQ6g3vV=uT;5U#5*Vu`lN#Zl&{Hfm7*GMJC>as%PZ~*)%ZxN zNq7%rXfg$wj7VCG9<{g7QW%lCy^3h|32l4NgvqI5-`XH^CcChFFPxi3Ph5Q>!&Q*S zo_{4_@(|8Jrdic=yUMGvsRTCm)W6`RmG}VEN4JSdy+1SC_k|*FTVY(dbb$O0Jdz|N z-2=P=Ubew#`Ari2)SDeI1_FY?U8x@(CSOD$4V}rEK}i$Jg-4)*AqW?S=PJ1zhQ5ng zvA~iY#-0-ENNox92OWfP%rG7u8}%oa;CD{w2>BgFe4BU{7ji>?lM5yqJYq-Z!jBb! z0>m>jeh>Ua{D$9@0l%EkfbE7K(eQTT%9b5QS5yZaJc7h8H}rUG>co~r4tb*)nPd%u_5PG8s(J~7`O{sge=lfLUMRVRoVb)6@A3?yrAnKd^SZ4=8|Wp-Z4J1TQJ-kF7s zKxyJZp1=NnM*M*`@%Pl3vVPOuw7FgU-}#^8PfCFN$llmP;~q*h5m1K`PNq1W5@Nj} zBYHl1;bWi#H@$C<|A&GDdbN%)EQi$; zQYGl1h^1=&4QJlbn9dEr@U26rhRtB!voh94&?boLuoVPbs3z$IXoo$5`?KIiAvLL3 z8+=hVXQL{;4-gm`Kw`l}gu+ofhK8_lm1(@v0lOuHcjttT)E8du5T6J(O=tA{- zK?kf~Yx*9}lULCQw7g1@_XrPphpS6nJeLOWlI0s?X^Kz0+(U7lu-LpRjpDC+UqRvg zX~63wdT0)a-s})v`!0TDrvG<(Yv~);|jQxG|*F zjOG+=H;uk6?Fu!{*W~8aheUOd_((=TP$YP-EWFB2bpx$RW@uVnc$tS*y5{nOH_~V| zC*>8Cqi`}~vmN|DAk^ncLS?2;&jZLO=wme8ODYh`1JlUx=dF3L6SDHBuADSt{}i)_ zYKMC8W?;K9rd4%{WYKwv$Ao||0h)mEYbk1)hFkB{H0(Gsord?no<_ql=d!({;cHis zhPe-ehD#L>X=e7_+dg)t%@z$I2@oyb1QFrVTRY91$g3+A>okY%vh6( zlv$G+mGik-W|(KpY7u+q7||yilz7npE*|cY5ABO8(<7zssDe$}(Z*%2d@Q~n^0D&} z*VoePe}$h2DV+sm!)R#GM6GuG6F$uxIP+EQJ98l^Osix!oXC{4!Q2Otsr}_P#o}TL zS_zd&!ti~KoW3g}ik|k(?gk=QtkmT} z->!9)Euh2B6=!>iS+$^Nc%3mixU<)tMNp-?D|Qu0yCOA-OWJf-~B{7cMJ{V5Y_Zz;y)bw zsJ$6(U{1i?3((PXf@;nQSo?-c+Lr;l4)NoZEnGBBGv}>~Jd&^7N<#018uSn(;ncf1x*gK;->zl)Dg4K2 zfn>`e^1t~Y_`g8$za+IjrP7oCu41s`EPFxzketQdcx7f3C1ZS57rcU&FukkZR~BEn zsF3~gg;FMPDbmShXkl&?YcPKu>EzIH3BtgvGse>L%?gA4j0uQJs19#j7x8#ke2PFrO`ZkQz-#OX~yEoiJ5@A%dV-7u< z9Ugey1lFJ3$g8DzQAW@OtnOL_dPwrZu>fd{rz3jH3aU>g%ab>Y!=>fY@s5p*Qi}Zd zUs29x!S_7JS{%SsgDqi0rxO^AOM6J1#9ZMU;FHi&KnLfV?E#m79Yetm0*P+c45ybO zRW08~C#mn)U!g1a1h@;uMe(62bE(Yy)AJ_}|C$L9$nWNF8RC@#xA1Q4pDXzatMV%> zR$;f?-rzo8u;BdC2sdhiS1D{<>rO;4zn}7IifU)JO*O7<0! zBZOC>eA`774pD7Jmn0fUxKKa!J6y`*ig+zy;C>2?uYgA`eS6g6bOpX8Fc(+B)$8>R zWGcTgWsoOQ(etOX#2^!gHh-EFL~f**x?M=$RcKkQk=**9`jEePH@3`_dPQpt?!jdG?dcDzJtqMGGlogcr(Ct1=0drghGH;emxW!;BaW ze-Zh5NejX`?}6h1$dtX_a2N$3t2r`yPzJ8b)o*AV$#VhJm7;;Q^}MSub#Tf1dKYog zwy;{60xPeK5aVirQjOJffE?-`q+OG@>VGmiKyc3H9VAA7qaYP67F!uFm3Cw~@x?0DIdP~Y&tBgfORbTy-SFAf0{0Yf2Rz$ z?)OMtjz{DD^Pl@PSqg+K>jAf{GdZBkXBzyeP-7n_9JUhbE9z%Co415e8agjQpln8a%-!=?R58=w1|UJBZQ?&C=h{?SmJPdXLzCA_}!E>#ZSuby(D zq92qDl22yAR!F?fC&So2#J+#WE{MhYMi8pmv~EKe0PcAL_jEh)r4&eqw+09UB$Iy> z4A`Ja)N^E!sqHKGpOaF0?i!o__qV{90)?8zB`6dyQ5Hv`|SwyQRL&H?L_`< zIJ~%@>IwW&OOJhgWMYebd``pFG`6@GGV*6|8`%b1Ky>BzO34D$_(CqafK>LBCT*!x zjk{zENZaSQk{XII-XKrqlKyexOA{UR0@g|~k4 z_D%A3wcwBu{!wk=S5jWVaY{H2!Y9a+vv;LqYQ)AI@rsf+v6c`EN9yg<34%QyFTE!2 zB$Z24d;@Ui5}Zd}lBRt5UxzR8T08LFvyJe*9{&FfUu;=B@NNH^@b&%QfbSac7Xrx= zTIeo}x8iRV`CCfm|F+hF59$OX#83oEYm(Ed(AG8u(rMG={pl%MsSvyj2t7CjJ_YWh(wCCX7xDB#zMhzqGvxe3aGoJ|32V6p0fRYAm!yj1W-~A}SH7VG$-6 zl~$~{v|>R-q=+E_6c=I=U>ruH;)-oiDNq;cf(YUg7TMgur7GA8itvs@5d#W{n*Z~h zbKm#AvxVCK@5|>yX6C%_bMHClo^$TG_r34EFgw`_0y}TRfJzWW24@FrK7%LeTH}op ze((Z44`AS#Y{i4~f;C)uqStTN>-o42xyK{a4bHC^#A_9OWM}%~Q1{By@XcL6Q5@=3 zdA#oH9xAECw)KM%9`pSoQK@U@9D8FekX@b zybm`cFgp`!Qtp%f9pSqXoq7q?n26|`iijORx9((DA(%a`9XxBzu0ddQgAU}+inZ$G z#x(zRbX)mZu{M8jud$u{*Cl8Pes#WkPloFVn%c_#>#|HbHuy^ST5|bX|Co&R%L!SC zbg?cyydwPNTmA+f=icDIuH|>V!o8MUzbQX2*1FicB?+IK6nI=oG3N42{pD(E*s( zlI1IO%jbz6N%9?+rhFXO&d$yH;FEY#=9LcllK9Ve%Queh*WYrin@Q#$<#VRRmVXk~ zD)`Iy`KPw;`rr9)@Y{!J-@MrT)4W^!*Ny#j^)8|w|8;}Ujm_`r-QvG){cqGE{A1MV zZUG7UX*#~n`*W&#ywF$Sd5ljvr9hjzksKggv|8cy_wpWQ?eosHsyLk8c zuUq{LwDSAf&t7kJhFd^-e5*s~cV1e&*Z$KnwlWg<%lG)-f$8rw*DZgY^V}QK;=3H} zUQVzdiyw82dxIZuuXp~rdp%u#I`Iay>LGGd=A~(->JlXq8- ze=p8dzSe(7#`?YXALO%NzyBO6{$nXqa{Tff^!Y+RTb>sE{M+_#{GLc+v9QlDh~rN)1oy9xik_@V8)eu!Vue*Jp>Ki}8C zC*qkuVEKU8JPH2m*~fg_m;1}N?Ki5uTSkWKUT2)3|3k!o*|R%CeqG^tjOAVQ)=E{{=2q+CQy+ zr+EyfuYX$F@e9g_f3(TyUs`|H3p3U4>1T(}KK%B1i1_*_R^BAX2QR)}l$x7lmVcg= zKdJtv^6TT8s{g2juwh#Nr^VOhSZ9+gUrKyE&+Gc6j)eGe2>o4^zWl`e`VH(==5e2x z|7!b=InRHC-+ny*8f5!A|8-+O*11rR|GH-%gKS^rzi$1f)gk<&mDdR+=%?xUIuI*+ zlGX2-S3A#Qe99sJe*0-UzHZ0jndI`J5?_41&`X=VO8oMB`sremegAcAa{In2GB0pIFy_Xa=S zSALtOuaC+}ciq^3se6r$l)n7VY5BMHcTPKgQU7>iaHjIL{`DE_xBB(jhu=OA5g+-Wzjk=j=Lz;_?8h5hCYSHo#}3<9rnldDhwzVi zUT2pa|C*Q|-Yz?mXK3C<)}=GIZ`1L0TAK2q5?_4%%1fKPI{fl``dMR>Y5#TWztgPz zY2)WWuai!XA9x7;ZcmH%+JBAhqy6Q3{CDxv;{^M#{Euqw+E`lrHLcvs8SwM4)1|7= zkN1@yX!?4qw0L9x16*vf{o3+Zdkm(pf3>Fo|8@0`!RKZwU+W)}v3{#xZ+QNb;=@+i z>O6x3+?(3>XO@4vl|L!|H5Fg4YO4N=62gY{kEHUOim%Pml%En`acX%&ewkE$Q}Y9l zV&PD7|CBO6aG34;lJ%$U`$UPK0l$5C{x#k9b^hzde%c)ZpBtNQ`zk-)+V8+a_{Tu6 z6Z#49bs82BC9B8lzK41i<29{(NiM&M`QgrK%7;pP@%1h*ZSpGd%kTNmH8z>{U$^?{ zY~@cIU$-CXdP}#B(wCopeCxIUHMWoT<30Y@cu;0sy4A1GK7K-c&<*V~ z#MjfjDHqQ+Gs{2C%AZt!Q}MN#Y^a~1{yiD$Pb$Bu`1&Z;-XuGCN_;)Z>-t2_1phun z{@OQv`HA`KlQuU>)}OZT;NF?q-}A37wy*Qsm$4sj?3P@2U`B=Y5~9eLejAgYM-F z`0YQqH~8zf^3VIO>FWd2;*I?mXR?1=eyguPVAwPfWA9tLUseG;fiH!Ao`nkqu zA4&12sr>7>+fohGbAb0!`~GDAweoeg@+Z~bRD6B()+Xw|Hz90T|41sosrcGAP5CMD z^$pwii+oAtH>JO=x1{o)lE0p3`@UrTY5RV8dZzaG{A-OJ=ccirpB)098(U-hDnH(~ z-?T&c$22cTPL6*~%nz)-IaNKLdCjse?X_)+{+pg37?q}csMHr<-|^BWuMWTbp8w3Z z$+Z8v)z2s^f7Sy3LO<&)hwti#(r)9EVTmC$c!Swad^AzB}j`Hz*t7}I8()tJY%v8Up zpVr>=%1@dfSbk#@4lp6L?@x{oR{qsi{-pTSRDA8*RQ;zVgbnK-N#!>cU$;(9l`kc} zUT6FM6#qU%{#up3{KWh~U{Me|`H`?)7x*$AZ(gXJJKUn)6knp=MnBg_kM#4tfOdKgRzMiuA`?GxtTo z#&vvM00g`O#eD z-+Dev%g1SZ@lUnzNPqltYmDB27Y`~}+W>i^loAio(h^yfYOuQ-UIgimHRrvs{UgtR z8iIvuFm6|&p5^J6bo!ad_fvf9>#zV0=eF=&E=bxPr$*q-jVth?O1L9lfvSMm(X;V3 zi<}RGm$%BIEqj&mRiwsu$!cDxv0gP)y!^JMzW0$nyU=?-C5b2h&$Rqq0REsM`~IE0 z0m+w6%$vB48^tbEOX0Tum(0nT)0yoLJ((BqE&(g?`~1~<-_z9}oK~Xm$AriO#K)f{ z?NB6MD}f^9MHxBz7)=O84YO~Vh!&mEKkyYEsxsQQ9eEy(_bG2#tqmLIYida#?t5{X zfK1a56Vx)+8UHkQklI0Hq$ z3Cv0r@xN2+C9uxWN@pwHX04O`^F3{+eo4_k-s;>P>fJXNPRs)qs&>|~uJBX(LVDDE zViVqHgol|03*`C=o>El3zge*ML!s9ca7PL3pM5yqRu6^YvhJPS|E+jMFe-b_qcP#% zj}+*(K@;G`FBi;_0?#ghp*Jtx=#-iF@~QZ2jSK(O1*JGN&AgYaE)a^}%W9?)kA@hA zohxyJ7Ci($^@u4NZbhYr8nJ;UPpQTqaVFWBj)Z8xM!dawGfp5d;#RGq7Vx&SiWLg4 z44sHqRCNCW$gBtIY=|F5VO`yz*x`sj8-r0{0S$++l3rAKeF^d&ktADKRM6;Pgj84pg`H`HWdQLDOT{&=WhEv2Bf@f94B$#A=<6whMq3`nJpiLU zo3pL%Tt|Nl?+cyy@6vERL(ycnRW69OZGL{Vw6P4*_KKfEw$v~l5d)xAPA)U7mw|QI z_S@DL;4*k8{+6rA6#}2fKkUHAfAs8Dp^ab3c~+$-9$-zd0~{vM_^#T4?Bx%aL%$K5 z(n#Ou4jx7^-D9IkI&mK%qeI0Fx3&!xe;1tj7#fuO4;FuYOQ5WH_06Sa#cOU2go+Q| zmYbpt@UKnC*imDw2uhV$z;fZ1j06Ap9>*}kYW%m)GPm=^T^btunmhvY#eSSAz);#H zVzpq=)mH2}5yfBDOjDL909}>|LFoOQp#nEP<`qZ%q6KW3NG#FY?UPpAfTpO6bwk(X zEG|VS_VvLuomfhI!28+3X$M(b()-b7Q-Wc10mK5d#^}(A8{+{JfY%0V?j%ZD8j2{q z6)$040q-}x+z=`TD*7;VBH{&4NK?I{=XPG)PaTPgFgG}HZTyKtV59AVrW`a4bnW1@ zRp}F;50-P{a@~~`T!I7bz`%hhcBAEhuQdw%bb*hB`SOizXIcpQ+R9F!#bxWUS#YfX z_I!BR)g?)Pk{02$&%r2;k-d2BgfXFLyOL0(=X+?x_dp){9&D4_PvTl>DB2R&+O9;O zvyzaN&{$7IHs%ScKX5OGMiB~~hE{AdyqBC6d_>yHlcv|TKQub0;ZL3O^dJZpf|0mA zMf&!CmGK}UhhZzjgFXIxx#!y#uMJj*d3&^HNm-=j9t43s*=3QTdvcN1>?w*A72o;BA#0ozG6@A1<{Uid@0D|7XqvM z#JbA189QNIW#OKC%ECAA$-W>wG>)@1-_}bn;nD@+mhlV1WpQZgGUPFX;eAs`(3&P;s9dWjb!0F;HlbxouhaN9R<2MIMqH!)z7`*@FEMF24qi`#b!>Pv-*u zD8-*TXXUwiK5~bQsjHYkO8Z9#6l~D^0Q(X$Kag;b{6O-6sQE+)lz#OTi-5;~KL?z};XIfw428cdLwa-*je#Gn+ge^?otdc{TvT@2^1HDd5#D&vtQ$B`-%peR_h?lro4^+>xiVp;*^H3}D z!)h2iP_b-S!Rv@OrQwSD+|p>z`tzf0@xK%%KcO^h2L?Gyo!FXCiWr> znj0uhWpPl=9Qmw3x)W{x_8GK>c4#qIW%7PHBDb%%iQRv&kxDP57>H?1-Gz5%OK#eQ z0;FK5Z+L8=_~Sn3dcz9OYl>lt5qz*-j|n$rYd_G(hqh=MKCa5l2Q0&smB0tL=EBY| z3r^;SI6NL1MkALTB?(-$9eK=wX@M7SnCEU*u;dYRnOojHm@P{0Gf(Iu+g0} zuapoLf7)fj3H*W43Hi0W6Bb&dMd4oXnOL0jT?v$Zak|`v0W4kos?+$j6#H@_e(h$V zh+hsr5x?XUV(u5Z#xFfZjoATM&Zb{6Du{lUMO9wNBsXzNx&}~8Gh9`zBD2^qhB0TB8F@)!$wBvTE^*(q zTTw?vDxdC##JR!gxsn0wZxx(=dUN-DYZk9X?!Lp-2Y?b&sy@)X@*3VF6645LOii41izgEbmSz-9yD}MhaVg`q zG`isVO{liEQ!Zh%w8G%4ZQ?UQZ^^C7UD|7HVwKco^??9)Avfd^ZDLm{wNGNyc%1hb8|CnnS-M)W*Q^Xjz*10OwqJ~VD^qw%%EPtRh(T@Z%wEvbwjvR z^J=Oou;GA>c6N?!`QVFfVw0d2v%gdIfi^sJKrxXvu`8&t>I1Tsy)hzC zr3a`!&^A~*#8*@ZS5B7uv28w#PVFMNxLItQ&nkj1HjBNl*3+gE^Hq9Rq?y{^OL|4> zGj;_ln(M=d@Ou`2M?*NQ@BlVYTm#kG_Q6pZkBV2Rp}D(}ON@{xq$g13uo-Hk8E{KB z_bJDD8TFOTryLtvQx7T7n`ul+jz_p{9d2vA1Z|%c|5#4h_?f}P*W>5%iRPNT>kC7{ zmh&GSriQ|1$ByLL@q1je;U49&KcGIDAFJLU4Bj5WR#L%QJ!dmf3U5#K1aZ)IHQ=R$R8DD+Ed~>4YA5Tn>{EbA(Gb`|26w^i& zC{q}yf5XJKhNIZEPzER~CYn1g#qS>VY9HsbVX(n_u=b2LR!HbD7ypN4$9~1@#v!R$ z{KxL^Z^K>$4=DEEoTxp+5y9FCXbwz0${7zx75iawX{PB(WV&&d=@+#}ll}n1F%!7H zDra4|&!_)l#G~My+HuS@VC}vX%?%8D=s@ z))=C}6}CVQGCx6nrJQo#uwc#W^ur9T>~`rWF13rR-hX7U<|z(z*dVnJ|4iG`CLy!5 z2SUZFY_h3hb5__VcRuG(UAYULgHjR_SlAmNF3c1s4xFI{Y5b;0ym#j&uBWWqv3)prNjgsdk9D$b*7+ zH>%8-fmt2pI%mScXt$YV{J^DUgON{uQ@&s0z9!^D7gQ%uy51}!rg+a#3XYZ#*D>~qnLoA9gL_pr+1Cz9 zYWfh)2w^S4pON5#CX!4s564`pDhlP)ivZ#c)-7Qi?kQ=meX2`I_wKrsnkQTe=TBe5 zB28&DEPP)gaxx#n4|PA*-rW-=gA_f!l?_65%g{a>59VZ{eC}^t1Gh967kOVrKe1JO z?t}d_akFz5@5MuYb1(a!*j!%EP+y|f{#OsZl))F)=X$6Z1Oz{Hd+_{|%@e&Q8Gzv! zqP0eaCbPwP9th@)N%7_7XHERSNx3p;&g&o$?r>K2iwptP`>~T9`yYHeCRqDGOWPA+ z@*%M&8b%jT1*PqYzNk>GU{>RQ>VLi%rTBa{y=&;OZENV=(;IqI{m(G%f5Kkb|1bKV z4raWq{m(o9YyXqc-eh!f|1F-8xexxt@A!lKAqk*!HI(OuZ_=59B)a@GYD4^IV zPM;wv&ooh44xwE74)--iyx|DeIqCB6Bwm{GzVBelJIY_4*=Kyg@6(lcZ9;jQFGG1a z&)O~Ljcq=@%*iUfhNUa#xP)@<)pEwV|RuZ~NstkL5rw<1tk& z0SL-iKsYquJ8vP^J6|K({*93=mR&U_p>x~I%}h3!Njkk(>dbN>XMYe>OlswPzDVW7 zW0IURXZtx=cPWpFY0=Hh-~Y!xtZ^UO_&!tSzvTJdW{J1{H69o~$>{~&{`|Qnf0OaS zGDc-@fBq;F6>oojJ{b}85Z~_pxlCgx-Q;2>4deY2@~D7exIYW)wuYnA8w)K|V~b|) ztUsYESqO1wXJkXOa8g>u2Du-#$^96LxcAGQ@eld@R?_9(nM z7P5(j1Z#@%yDoMIQPIHAJmaU=ZT0z=@zc(i8NVoFk||EV>J7h1o^gpdY0xdCbDBc7 zUYb(KZ3%^Z{W~dS{n3V#ty@w#c_g)vvl0q<3Wd};+q)u>Ylh4%b^cfE{}Hu+bm(IX z@-p5ou{>ceBGmgPL0jF0Ui0}VL_|B&K0 zgNpi}IoF3jocPO<@dpI|oQn@BKCbwR`9Azf#Q!209~OMMi{FQv!2cA*hs0Ej61jfL z+4Odj{0|Af$i>f7d_?hkrGfqUhm-N8f^TRm@>gDgy3bMUl~aAZ{}RQr|GzvL{?dP_ zzUK+&*7||x@Y_C+#Zcf;5S$u39*tuGOrOvBYa)OB8Na^ZuhIAw8$!Q|_NgJ^5)x!y zINIlN5(bj6kp33!(~pGHN%*}It|FlT1Q}msEAa{vgS_iSn5}a{q5J%!H?27HzjX0^ z;z&H3M^`cyLv`3T{sh`@q`N|`k-*?DATPUWT6Hge%Vp>+VRw}0>@3uiBl1^gAwZcM z5S01s3%3rTrpz|jRb9Bgj0H`xICT!pu=VAENfc(6TY!BGkAV zt4)JZ9^f$`((-wUzkRFrXML2FHQ}gG@rS|c4{&MJfN;ynp~f}wcg~H@$jWbQ>|1pp zD?cl%{9k>m>iNJ25KPWxO*+4h{TpHT0(NeAdEe#x+w`s4(ulCYTT!f(RTdr8th9K; zgsb~jZRNFdqg7d0(SU0MCN~dNuW26pBW<<#!wJ1ct!c?M?2T(;7lI`7tHmEqI!0So zA1HIRq}RrHPUk#42narE-RK1IwTuhEHaaaW^}s;yhF$nt=Zro9W;_CRow0r7yr0DN zBhkRdh-;(9?6So;ol7VCwBAeR>u?gtxA9hNG3xw{V3fz^2CKL6?~7+&Hma&I>!S%r zs(fDur?ICGpZ!eyZC=r)6AdC z&n*)QqkqjpPrCf`HhrT@vkDp;e|8Wn$0lADT}Jak*?43h8!Kx5dg9q@T0ZkbBf}5o z4=zz+xhOd#&+%zt7{IdEspe@OSREPu8q21fQ$>@zo%x6Zq3~|vo!R=`xfh?%y%Uk& zA34?dti$?(gU3VJ_p7qg_+fhgn5X`c*#CV$z!yI8Jk9if>%a?~Cyzmb{oke?(*L!F zqe}m0`j0n2MxePddNyT41#D{14;XIs-FBN=4D}y)v zM{mXVO2J>2j6YHE&naH|k7E`4&kK|4X8+M48UE6Ck)LqwKk(bvnCL%1a0Z-X`j3_T zbq0U2|G1C83h~SBKNgX2Gzq5vs3xHm3G6@WoDQAv^^A<|%cemj4wE* z*ookY6H$pkV`F2bpg@IUzk6?mPe;IT{1KKi9W&2D%m{ zVucc0>H-#y=`NXajs`wh>&E6ln}0!bqJGSLEHQ$k5W5B{&*|LJWD!&G$f4#lX2{@s zdnjb++0yczyhVwPY1FCn+nX`xo=zb5`6uNX0tSp+$B-9P zrBmr{!%PfljDi?c4Q#q*ir6%#r^0Ouocm;HfY_W_FDBLv#z$jW(Ippf$kMLB>Gr-m zWWluIgJRWg3HQ!*FGX|KoQV}P-W-0L>OLnlhg}A14~RB-7@9jD-Y>7ZPqtosU5ay8 zoelOIbbURWcKF;D_y{*p7e0LZNN!Sp{86V#BnOeYk*Dc?EuEV$PS zd=+z{?I6A(!PV1`qc>$cIk+Q+x46*#ah+>4@JN9f4Zim-^>;IaovWNdSbi4IQm#Df zX-#p4ad}fH{IYNvyre#OM259U2IkyX49;FpkjoS3k@N!9uA6u*9ye@8HWYglNxI*v zoZ6OZP~aAEBZe~?8-HryNA;*j(LFZD#$tsyV#7U%P56l5qs|F*bjJsqGp&5mWFRH} zM+X!v(e+Q)X#9}J4dJH>>ZLKwC-fq%C=P3$#lfVqotg4DF+=<~VEt@`@v{wL3q#Nt zn5UN#o-nF02?hQ0E7O1UVt{Z?#ITu-i1^NybR8`ON82^nINIwQTyMfXLwFCc@Y7Qs zb?MQZt9sJQ%D&TgY3OF7`LQSi*}4Jt{g}3@;=OhxsI(Q*h(2 z*|>j#>9_w;7VJr9A7Gt<$);M3@NH3d^y6P$*Vq8yadR#Y=EkP--L0<9Z0 zqJh{kikxGRoMKeZ73+3q;Ro>1p9#ZW=Y+{9GN99qc({bB(K+&q^VXB<;Q3Tpq0tV# z`|%&39_!r>ZstG8u&GGBdpCc=yZ7@G-d)ens#P=}^q=tV-<;w1Zt9TvQ|~k3(a+PP z>EEZ|p4e3JZw@)(-_oLJ^*wJJ|E}(HwD3~lDJcw}t|{2U`efDOzVFE^;+AWqgP=vp zJ_aG?y(}yEjMEF#Da_)2Zvi6tbQXgxz8|>6t<$MF9m33Dn|Z2$o>d2rq}KrgLR;5o z_lwU`{>_Lib5fFB#<|L#%$66(m>9enl(bAsvJ z)NeF!*fZK$z@IQ5&FY$ks&CBBsxV7gen*Aqiyx8ROa)XVjUT zgf}v-boTum+U2p3$=b~{KQNWDCY}Kq4gBV6)76ZA(*%*~J~O|gsSQxU!h8c#Ch0?1 zJ9`5!)l~|q5VN9?QaaPI$f&AuP-G}NS)zZT)2k$d$4tMQSGH=&D&!hps;`zf}9Y?rF8p5*m+GNBhiAuuri~=TW#Di7MvD%3s|s`s1fsUh zP(yD+;+S+>gIKw}t*x%#KG;W%e;AD)+uAfb*Y;H7*Yce2j)L)DoYwf!gz4MwuWJ9$ zAzuPQ`}HEbA;@^k@X)@GD4p||(Uno3*SoYo_9(W1JM>``q3F;#hdyH$B9GIH$w~Pyh!taIleHL2&WedE)bWdpG_Rxu3Wtm^TCO#VR9gkA- zCtQS=4ZF`MYl#HAWdTY>91U&Z;pfW_oD({3Z74bbmmp(yXx}DkF4sBt@4kLmR5zSA zjJ9^NG#}+)erGm(5;H{d)43Br>YVH&(c;*_FZ2A`V)Bsk{(XqnT+-uRPF1epE784! zi#r}sv$=9N6NDkaxSq?*yVFVR_sLTfhOuboR%{RvTZ?s0Is(FV_8L9Kexm~l=4k$U8iP>P z&MlM``71l}P}F=PHQ3GHNaomo`(!@f%B)Lla9PDRtDd5P!mgCIMwc~212+Wqr>tN? zE+G|Ft=a+{v>+#ko7|Q%A!!Wc=4y=cO-Ky=TY_Hz1`QA@D%+@{=o_7RQo9*XCY6Q>u(to ze6iz@Q8*tMFXx(MfN=zF2R%5=zD#A;^$$;MN|xs=e!?{p)Zp3&N6yyV!t3J?JBO3? zQqMTj@gGHIjnS4jKP9iS-_OwqURC1@=vA- z<4d9Yr8@x*M`q9?;^oZ+CFt2=&$F11YU?LL1%wyosACcOSVteG+TZ%GuMoqXS-ahB^OrL*@A678(jeanb3_g0=jFsfkYU zst~DfT)wEm~y;ZvdDnm+d^!%=) zOML9z7AB41M}{OYy?@T5;u7X>VMGH&ozM~@3%n6uhHFFP+h}n~(#G>!9=aqRX3q?V z=0~K1Gpiw#q=v|>kPB2C&x@0#SmUKcQo4{sDj#G}(k4xy!qX_fHR_e14ZT z9jkt%lW*erkgYpC3!O6mppIUlFk1QWfU*dkgY`Pn$RZc0W;u7u`ge+iizVc+5It4* zmCi?Pkk8(qP_6ePrG6-8i}o+i(q*c4&Y`#n8V~XlK|?-adTa3;{)PFAJLUB$bT##I zt68oU3G835Go_P-QRi8+d{s@#H>s_E4=+mA5l*U$3s5B%-cD-$h>6}B(N8j>rap|U! z$~JmPL~@O*2tiG_JN2XOrEQg{4EAwxQ6pX0;=_e=`S7 zCmf2hzzg|;$KI#9G(teVk@MZtR_y<8ATCFWoqAk+TER0omqo3CPnK0<9#A^`<aVZSUnf-i#BW4@Px1rc;WX;eHa?2Z?w@IOw*FY? z=mY3%Ile$=V*>d7y6Eh+B$Jl;qaM^%`&O`O?An#mOQhG2m|pcxNHZ4M7-HxUFnZ}4 z)lW$E(`k2##p*sr0}$EWIFzg(NSCCa4!d3bNa|L7pt15#R1sdGlKpYi(7K@hDm;ck ze7Kz-q9YUq%N@$FM-Ho34U>paA}SjZ@kN9ZsOz3zK~JmkC5$hK2xqly4f~ESl9;eC zNpsHJ?ogZP57W)Df2Pr#T3LGirM0hSuP4~oXS-7L_rKWJ=3TD-knE+iufZ3JeVxaT zO!jrg9oD|a{LD=qEith4_JuVgj9-cRyJ}^Ef8M?` zMSr?9^r!jfl%1}&=%47k$I?45amNrQkadQCZY`tQ+JAChy+1A10HlOhW81YLaC$1Lg zq0U>i)@_;{(;D+<@1x20<%!>H__0-1e3kTMuO{oP-AUBh1&!Y~#$tYtN$($N#FuyQ1@Wcu2k1$9{B-f< z%M+=of*&!hd@S;(|H(geech_5?EfdT|3?=aIL7#g$yhs?QNK@r`#mdi`+uaSGX6k7 zkUVt`?~v$#D>~|soYn+yH)++JTE-*ph!Z*Mg90`=E6mH}ti$=u=B&=ojQ-t>^-7xR zU}LC-5TsW~^k2*mWUM;gao^0MtkpZGO0LTOzeBZchzoy?hIkVOI}l&c5c3bBAzl`P zU7T#NBMQ+BoPYfuCi_#@*R7_c$?-j$XupqbPmWKwCi-^zzR~q<84o0PjdyN$BN(1m zHl6=3B8D$IpR_UR7@%ui5CV zFOswotQ~B|PcS??D%OFLu}avW3{%`uF@Ap}HfBc8_`OAbv;RlMhlVb81fM3{vFybqnARFSc;2bbS0i1 zV8({FeSxuUj>fJyS8V(D{D5ummLkl1^B9eZ3wfjGmIZjfY^rU?>yJV^;&DS@6u!W| z-`NlQULyAWTypDHpP=@=>;T#~^ZLT{@i}e(wejVI_`Krtlork0f1#;;{g;3Iwd`{f zjXl*BV$CMBHpoA|VE=X808w2hejuVCtfUUaa&EKQ>U*Tyf1QbMP}hQenftGy1$2fx z@m&89{a4hp{~1l%|8>g}?f z`}f;?!kiH=xHeyi?`y>v&79Fx^8*fGY~D^7&EJ-4?kV#Q<^80&Ch!B6zD3H@$2FXH zNVat6x3}1)n}+96GVgE;A52AazWoL*XXYJ}4gKLxY`OO_hZVcfpLt}6UoyXt*1n6E zCfN69TT)sqvwgdx8qLqRxev@UVN~n0+5FOo!E|w`XYQ|>av`D?(7fj3eFv$dNH^&vtN%tq(%wvhTi&^fSpSnuq zV2AmWHy<&~1X-+v3GBl=TNvr=#|OF&aV1!wSR7_5=LnaSse09T-;Bh@o2XkeM_Ga> zn{_y>@Kxr&In;0ase7Q`g$TFr#up@8VY__l_kz99@8A7am_EJ+89J(WLejC&2J{P+ z1Ri}MLBD_ecZz;jrqi$3iPfTOiK4MwsBV0cArAhxUxyelj-e%9Y}9u$7+LabQQuO0 zptaDmx;>z~^8@*_nkUa+maSJc!l3w6j@Kbnwp0Cyp;z~r{?A<8 zT-$+#h6DFJ%VxlB(ZF{{vPAb;8g9yRW^Ps(mfZ|d*oGbgJB+Ot4XkRF#DsHJ1_qj2 z23l!Jm0Hr*&qzs^wDgt4*A<=J>(Yy|#PEbgOD}&QqWoM-+My-Yw@5B&UWSs!*^kr(`)KZThcu&iSo@mRpg8OL)$Ol3&nCmB^wUHkwhwzg^MSpgR$TWd%773he(K)H; z&KCXUyP}bM75$2$cRlUZ-S{IL->qBx#QD&x`zb{aQ1q45XC&~9MV~J*!$tGyvGasQ zk5lwFCky8r6h5OI4I54SU99+sYJKlh zp5)*C;ICaI-}O&V^50zwy_fP2G*N z_#zj7rQ)lN{1!idzaP(Go72tW8#WmE+bF)jw*1~FJ^6#F`rGw^mEXlL?JE3_(E1ly z{6pXS>lghkwD@utKU(n}$T;M;_#P6#-1ciL@>_h7i$7ZN%T#|oEj}V{?c&Aux>0a;?9o#s9v8A208cceD70w~hR_DSorozxVH+{2Np8 zyWX?%yZEk(@2&b@WbvQ;%wIoh%W@W4e7TGNxQp=LpNvC(i+{6C3Vxi$7rFQuihth7 zZ}F#o=hwg3UpI?yc+1Ftrs7Z1`u8sIXTKQf4Ulo73;ul%`%g3eQ7g~I|iyy4`5o8?gZ}C5z>c<}=_HXe;F8)A) z@c%d)J`(6@@lW>jz+J1{H{^W%I1GT~bExz2vzt&0k@2~Aw zVev2L`1$8nMb0>jFLLo?3|{5$Y4P7D#BaV^rS7yeJv_S^foC;ux6`GdrtU2j?WU3`t=FEa95{N}Iy_M`T1@#QYQ zyW+1Sjt%;*Zk$-+#=L ze->8D+5A!Tx9d$Szl$$c{O7DV5}0N2KP2Qo;=c}vdmH5%k;u~H!@;7!A{*O}r z_s;R;|92{Wmzit8sOvcw|AgW%()t%!{MQNoC-%3{;>%t9rHYTR;b8w3|Eq+Nm-@fO z7rFRs#kWxWP>b)BF!oaUExuuuk^jXGBLCwAMgn{P>dF7}oqqjm{$pkkD8GvzrFg^t zB8&f98u=~0+{L$7d~fO>3!cBl10JWd$hs>6_a*GL^WYJ%( z5V?CPdOcy0z@9&Qavz>BfRO>vf6QbC9WDNgw*LM@`#+0ss5kOIq4)@ak-#UjJ^9~Gh`%cT>sEdjf2rd8EB+CS zzx^bC{qS<+e->Zv;(72na~zv=A9>-fjw8(uZ?AFKEYivQ#xPyWx&_TxqW zyH;BHUHp1vM&SP<#m}<%t_}YFL+odv#h1JI8x_AlFVU z6^H)+0Z;y#-G2U6{^eGF7k`f84=VqQEPiQ1{-E*C;>%t9m*4~ZKd<~(Sp3d}`6uz8 zaTZ_X;vZ7{c@!MuAB+E8g8xhZ(aqu;mKpg&ia%cK-+R9&{}&1EFZ$bsb38&3Jan*?f)#k+{MpR{PScS{h!4*OBg?D`&)dGi@#X${T1KS;wuy4 zx76Rw;u~Hv^0!p{W&$ID0*n98Hh=re_-)r~R(==%3^Zfq-*%s;zlT!s3oX9f#b2rT z397%j7Jt&G{`zJ9Wt_zqx%f7U@2~ig7XRnXetawOe~WKeYUE!E%clJ)zQE!iP33>r zt5$v&KU(o46u)hjC;tzh`|H>7uf>Kjni!XQa z8&ERsN9}j6#lQZszkX@IaTZ_X;%`&@dx{@v@dy9q$BX@Rv-pM=jr?5||D@szEdIhT z{CJiBWh=jn{}?R`{*O}p%lCNlPf4&psehrxm%I2GihrKKNZ@9RuS`h&C4U=d@kK8F zOvT5^cqA~=;t${C=U?03;u{tl`L|{fqtSAE5Yq6+hSF|DDi&GX5NA@kK6vH#7_WM<{-z#UJ^lUw-Mox>3_yqe36S^p!nzMxQO2tf8)RX^-KKeX7LS+jQp1Ts%f8tbeoWf276#F`@sL_UmTx4bK_**P>?u z{-EO9TKq2)>|g9}*Nawu7e7hy5yih5_4Ie8%t9r_F``%N0M=;#(%f0yw58>4KD ze=NS?86$r$#Xm`4B=CO3<9|ace%A|Deiy%kYwf~;35uU(@mFv0>rec5p~aWG`1=*V zl8TQ6hFbjH@A~nGGO&M(FLLqcD*hmWc>ZPaWeNT#?bprX8~$PB-;095|4}r2$bW|? z|CWU5FUh}mEwb{v_{SALLizv1;s*r%{;T7Ei!WDv<)x_mOvO&N*x#hqU2d^w5G$MK zz9m{8Tab_Sg4hW;+UI2uoYv0wy7~IQ`Rf1{&&TKBT0MWo@hf&wBjkwoITrOgn@DJ` zgvH>%Sx3UNKgu1a5VC}X;YygvJLZ!RRKf%jxIRDj>JI|BgOFMfWPN#Tv{G*-^?KfR zv=XX8;QJ+)leY4EfllI0gGne?!Z;F&NjOdk0TK#H*l zAmkgo*b)2JcLG^Qj<&c z9wMQy5&|U5AYu1@f$R)}Fo}e}D&a8lH;RM{l+cNUi%IxlpFqAQ!vjDN{uU^46^W`ApEV}D=@|_=PuqgNeLZ5Ab(RxJ6>tW@}@B)yt_wG+o|m-Co=&pplB%DdYrwsz3pF5pNxKjz=kjw9)?aBKPRC8_Nod$Ds9hv1CE-yeY|p>v+dV5QM*MCB9608?|6}fE0wT}%{h;R!v>m4B`gDh z{9R7k`Y!~zk3|h8VTuwuv-V;Vwr-Oj_4zi3^B<%WJAVsYC zo2?-HSqXn<8QVZm{VA~>?|O%K?fXn%kK-=#w~VyMm39r0Pmu6iC9Gy;50TJJ2^W(v zgM??cN->X+Fo}dKmGC?n9!0|8N_c{Vi%EE8i$KC83;;p+8?D3|mUAlaI$8<6aTodP zK-$Vr1^EtdYC}S~68=Dj58%y^*l|ku6U%l;*zj+Gj3nek5^ht%X5O)ygljCZ_1 z!oU6{kdJxCTo8o68A`07{_f&kXDVSCVB~KKX`g>0$h)d>(-;!&QNlD5E+wI_5?;9- zgb)e4Hw$Dg31^bY=RTIA7PF|OBwVG0*ICR05^|NWm;60I!fPK1ieU5X$S_dKvqQz9+(U8d zT%5{fp37w(xm=Dzsg=_<*_}scs@p*X_gL$z6dJYwaA{XV$+iS(=6{eGluMf#4nAp-$ z^gm_!w~c(?FE9Q+MA#6L7IN>v`{kF*b=hTBS{bax@4@~_cZR}k-LfjMo8p8ISWd!y zVxI${3Fq5apcC$im(+Uc-;bJj5p&_RXpg$t3ad@B8tLr|zpH&VX;&t39u`pEJyAnG*A0JpIuqSYdZF(cW&4oSXrPDk&({wH0 z3Dmn<{|H(a&ExY;=dG8asL`(RKwIhU=Yh2LF}Jmy2M`I;JvhO@?>fk`}t{|lTp6yaP~1nMqVb(am@ z;oNp?CtutzTPw#X|tj1lEQm1bK%T>)o40Fu9eP+WiW?F zGw62+=d2QKAQ~9se>INFrNw_{59h9$d{tKMFJ}{iY;~VKf58+2{5M#;Tjzh?02%Ms zjeLibE?|}00?M4trhC+YVLbF6UXvI69-C|E)qUnkVYT;ZNn0mLN!wFOs>@K)Fk8|U zTGHuS(qCCpBv8i>)*B7XNq8nEbE=m;u6*E`?5`BPU1x-DQ1ovt`i71E870mLtftu=pz#ZziPH z6Br5HYw^$h(_cS@XZ*AHV-;^Eq;6OIMHc_7gb8g`ki8rwb&C9 z7NgU;oUM;~lK)MRJPmMfCAs$cQ=AI~f-~_0Ga>Z@e~soZPDp*qUzg#RJ0bN?5=u#E z4hO{@ACS)(D|pAb-JmH)UV#hZC5E_^UWsz zB@MfGH9B9vh;K77&t$zjQ+ylGIrqf zhII$t02u~;nmqI{g~*)arYn*PSSke+u>h{0drjxhpH%#Z^3cxUaR<)-O2&r;-|(=( zU#IvfpgP^;HDjRBO@;8r`>n&dLpA$Rc3{!=TNl#;dFBzqLmr$I`O%AfGkE}dp*7r< zi!y`+g;EeOLJ}+heIf;_`Q0)f-eFja2C~!PZ+y{THEx7@2cZ5bsra>5lIx`U&(47F zor<544!`XMfBh6)>p!^I%3sniDDtPn_f5c4Q$~ItK5PBMy(xb>d{zeh6c4{NYuKRu z3~C`9F&z!8_=Bqjwxu>;&eBDcf{6$lkX$%t`3jh`r29a&K^P|kZF~?00{fC!j0Q#- zfxk?Kk(~Gxcl?EV8`pw#FK$C^ulYM4S?ky7vLw4bZVC>5 zCrlCWh1QGl-z3#9dLE?e=)rSBjmv3pC89s>8qhH)Vk*6iKP^76INb8J488S8k-v1`=I41=@ZAznul0$ue;8FC0>1w#=6n zmZpsc?lw#3bk{$5^m9}wai8X#!BTYmeiqHhU4-9^+-4)mXrJRi!2YN2AXx8!0?%UA zPve&ySqefJy#x9$Qcl%qJ&3p5K|-O%;&IZCz;>JpQkQG!&E;()KtTQxo1wGxJ84-L z@}@JDunYu_Uwf1G`FDuVzj+Sd#^T$d=F9EJHB> zMo34VEJG6tD8nm6dFp>k#PssCHaH27DM&RrK=PdKWj+(jr+W>j(~hQ9>1NkvW1`_6 zrEaBj(=%w+_L;PhUjCyPJ(BnLh>e8-qdv2L%Iyy;<7VK+{cA;S5EI(6`=^X2qtGM- zrysIsp(;kRD42rT8--f|P9`bex%T0t4L)SQR{#De(+@O58SQ7_1N);)Wr^71V}^Xr zDyZWpRFb^M$Frx?t$q8WY*~vvJ-9#0hxme!wB_l{`=jjLbO2+>ZvTKT*ZIe>>Gju{ zYn~GQBN{ky75fIG2B!XM-_ul$P(?!e|M-K3UfpN@J%}MlM06q2rZE!ScC1V)&AB^6 zNtfD^ey=5s)RG?5k}iJ5SCTBl7?h!;pe^YrDT(8oR$AC~TG-4Kw!W)PFW`y?JONkV zEdoBNE0unxC7rdx$Ciwgp2<+sWLwhBTG9|LY5ujMssl*jJjKg8sZkk9>SjwSVoA*Z z%=)KlE$h6czOqmY&L7WE)@S#7@@=mZ`R>z|Og&gqByc1@Xb#apW5P;8S&6poK1HJ= zT%hQFiat%zpN#giR{ zKp@tSS$yzX`+eUfvADJ**m9fl1i!XQaX8qF$#m}|)j_dvP zgIlcsu=pYuZ`MD(M_?o{*5dDZ$&VMy>1Oc_Q6s-u{}fmJ$rk_nRQxV8gTeLRF5awv zdS3C{uJ+_FNLc?PL*|7RU+&_~`lsU+|FXr8`pnP2Byr;`zR1O!^-r@2#QIT--=47k zR^DIfX7LR(jr?Z)(+I`4v-nM~`|FpvmR)z-_IL4S{ZqB#-yiA8|9XPwiv29K_;MF- z);~oQKg;5;O|5^N#TU7Fv;L{S;)h!NsR`2^y8g@J8zM%2v;OHl083nX86W$#}X3EbkoTs%qPLDf0|9g{Un(6PcM;B3xbSK-StmTk$OFE zGwYu&0)hGU<)ndMPF;O^6>b_#f?5BxnuKB!j?+7SAfb>1v;OI9mU}b_X8qIgB(x&I ztbclmkZ+cQVAek!AYmH_GCnoypJtNy4(~GSpGJT{{+5wu)<5mzO;3beCGpt}q3ZQG{BZ@2)myQ@@iv?^{;bgXuwYpu2>3TOblj%hA@`Ki zidQIaUc8eIj-q`r7lK{QKfGN>M7bf%WlqH)hIOHisf~?Ht!g(^z7s#ugGh|A%$`q+Lz$hanfV2X4cL5Gr{WvzYChVu4o|sIL1oM7oDwdaY)qRF|^-L zJHKM$7>tcD-!v8~$Jkm%KKam{QB%b!q3C7%L*Wh7%Q15LYsJ=36@27245|(`3(k`L zjtQ)mb4&v-9`3Psg~wh$vRCI!c@+Fj%ryMX@cgGt$G@1a8VgN>$L@Tz8R8SzH=#PJ zv81M1DEgc1C7h{ThNj30z3MdxM(Z$*3O>!jX`R$oe!N9l80s$$zO*B&{N_;Yn#vKS z;r*eXt-)*->|>4E$08I~8eU!&uBYt9CBa#1N@^M_R-IqFwgU48IKlm?+h_`?US|sM zq!{W$O=Y0v$7iGIaMmFLc6aEe*UGYJ%id+Rt1J4SA6|~?%5a469Nr?WtG8HL+AhIN zbyf(%;2#GY5D5RkS*)wfKLsw#@$}s8G1m+>y>S4sHE^1}<~(YyNqocQI_I0eq7~}= z183ua>BrA)RsZ?azdi$|{<~BEbHNgMF7)Z2JgYCT4YH`~dg_|i8}c zP>SpOHiVAbUltARKR>*_^u(`2wV$3JJa=O#ynlBY+_7(P)^g&{#e?-{ajo(frB$EN z0!ypE6AQ$A4te7P<}$Q5S{bd=4Xsm#NR=DkXn9P~E3|3!3JpU)v7xHky@z?KT{p*5 zUkT=i)>D5k`~~{k@L%e0;t2ScPj`$dg*EV6#D%fw=dj!Y=OyDAI{k>mNk|^}3sJUA z?X|m3^?D+ji6ulhS7@gbSi^~#>c*qUsDWB=;4^G=nBT6{KEfOfI%FW{Z-ZNrgvTOy@lvGcz(Hee@ zdTQ5IRAMHua-i5()qyWCrB%rdzPI4PC{C){$`AN_NGHCaF+Jxk6vhGYCm+vAC_SBO z_+rdAeZX50C^-4Y%C18TQBaU%eM5(9)#3B8b^$nqq-LfuLez{YIvSWT70(F(fh+Fr z9uNk&e#Du*9w+CVj7D;9cmy`IGs%Y1>xX9mLO-w$K52BvvwN zerFjQ^ywF8I4yj-40fGYkI^O;V(eVy^;K-^-!2f%(r{k<`qT^=xz~f}<__w%s zlVUd$dk3uTCY%OJ%{Ut`q-isXtSQMpjv_>JrpP-&cs`W#{BzVAI#C+P5Sp%OlbgPf zHhEsMFn^moidL{~a_@i8CfywTi78-3E3ZXfoNZdfIsbLG#`K5K8n;Vpr073#LoPDI z8n7qvH&pfGzwyW;SksA360Iqi!{>7+?sjhp^>KcK8>hA4<*J=)FTh9mse*cbM$Km= z@apBzCw1_rYUEkiAUzusqt8(Vu&`dNil3Y!;`*bKr5iAF*rR?==t+=!PL;CUb9!;| zZizl2z_*_AFxWX7YbemGptr$uw5-Z9B(TjJ>x)<1+BG!dBcw$66D|#1ySW0XNaTu+ zCBc_IKcaj~C}Oy)T1BQ;t`3a9DK`tbK?1pMWfCFvH;x3&2jk9jOTUL|Pks=poo@|2 zMZZvCJuZ9tlk;sCQ1Yss^;9CXw?uSpKA{4ZFPbTY&nBEx{`l6r4K;F0@JOf$S3>DW9JY=uEhP% z0-|?Ibx0FJp%KFhgNvU%mI5GSkcV9*Gw`(3DTf)VxkHG2Pk(%_@)T_n@{OjAG4UH8 zinM7&ia}??-Ew$*%|*B?SksmFM%#~^)B@>637)?V$qv@GC$XxSJyo!lk6^U45nL}g zv|vbZ@x#Ip5=ppcPjH0}Y6enE#J}DL{cK72Z!OGA=w~8Y9GHI@#)>r}V{zaP`N8vi zwf?};Pn)6E8LXA;vur%Pc@k8$?&7D-3eaFpsbLNAA%sZRRvy7ZYCxPL&D@$cvR^+n zKZ?G;6QgK9W@3vh&%-f3r2IR0sj9dREfTDi2{F;8$bSW+(QIM9c5|@yUX&FHos3TY z`p}5wtOueVMP0!fYR*;J zxxa8#7Rjl48qqo0{@n^{ZX*WnRlUB%t(7=VrVQXxNX=dPqd7x(WAXz%bQDGd!2S%_ zUR|=XDppxNM|1L;#5Jwf|Dw4lBJ8o()TJF^Mlf_N@NNA6U1~JY*DS+v9t^q4LdSUq zwHVD={VV2h@=!+2DVlS`lTE3oq9d19#(xeEjql-~UABqihkNboob)Pt)e}9U3?Fr(R9Y6o%ixo2n^|Sx-{|#5O^gEN9fTh5_Q2pSF!=nOtn*nT4x~l-weJRNe~E9ntzy{JK8H;} zcT;hp7l4>H1#24+4N))7B(CLj_S74jgM)wJ+e_dgYsS!mD?^d~2zV_E@IBjU%}49# z>vOR_sOE0m<`WF1$?OC~odfKss$P{NgSEHf>(t53$5&29lMTlYCLAOAN$7*}sSy=) zt~-5@=bDeC=ke_KJOoC`Rr2vkXdyp&%W&S(-K5gJoTtqLt`L292=jMpVk&Po^{go= zDaX$>Q**)hBz&vxvx7RR925$d3<-s&=8HTtv`0jL88KgH@RV$+NRNdM9#P{R@+)m# zwTh;I-g@#7r}ilX!RQi}<=lwx@uv}CZhwfA+Ozbq;x))aaCLUnHfau@uBd`1ygy^bs{s7#2he;9I!Ofy)3+<@t>Yu z;6R!=ykBzzwQARZLlG@3Hbj;ORjcOdw^&BwY~D?=Ie~#iT6w7jCYBY$X={z%`jlw< zb7a}78!Ao^mYIUV@sYv|Kg#^ZK9D|)gwry zs(OvTc6`dcEmQ6tFZWiA7~iw9eagiFl9Orau_B+{N;pP!3rs^`;MM-^J5vPd;t5iB z8(yk#r9f?Gq^a$n2-@lpm3JhwQWskyX{%cV+$LQS_i7R2uALmL?Qi%M9$nRF@@tIP z*9seF`k802bjC#8R=5q_vKIwMUR&7}5wJYD8CoQ>9BU6|5H_zJU)ceGbo$-Km(5M< z#om<>gIAA%rIYe$z)*0-2q}1c8dXO0j>?ghb0k_n?AGF$=Ig93Ef{)e$}6N z=Nwu`@nMzOq43AC{d_Wj&n_&cdVd@D=J8jJxXyp`=22BsvahbFe2+J;B*>H$U+(uz z&n44`T4!e%X+Ueh%}ibT@zpQHPj@0xaE1$`zd}X+hekD!*B;(V=RDZpwKYe~dF-mGs62>!yUzj;y z@Q*$X70cDBOQ^t>6*m~&s{P=AE%e$z-NP5bQ3|Yv8bKh>Wgth(aK^%?zn@*BlDS%w zS!OfmW1cxs0k>bGx0PUS$yFAU6Dq}L;fLr3j9?T; zVXN`j19!C3JKEzT{)sbjIuy216qYnYoML}bFaMm5q$orFDHe~eisv#<#+>yzNS-un zA@{}~>hYES&Fo7xud~}>Re!FGkApRDAgaj(DKn{in3YCzI?L9KW@#g0q$F^VQR-?o zb*X4V+9z0ZvEW~#3GEI(dpBn^@XRa}eTd)cYTgaj93_PYHp-Af4ph6MU^KaIu>@DL z@M6@jLy=k9X}IAbqRDTM#&{nr&MpX*o>(6`aXB9&?fa;7`Tldtj;kkqcxkhG=0d*? zh1ZvbF|^&sutj#u!f(s?ANlc}-RW9Y-+@{Zyt4y-^j*U5d_)#v9}5G!f|(|!#$4HI z{s``;E~P~xe_w;!H5Wq4Qic|X>EWT5_#ev#Ct{IejNK6v83bWY3~tGE0J&7H%#Cx^mt zkRz~PLHy(DEk979dWm@sZGG{xIE|fZioXzw^ab-K`?8`q&reqOf(T5J;8c)E^9dc7 zw5%*evR#1}7*3ylHHXsUvnxAeQbT4Seu;s}5v4gS$IwS)za{H z_}w>*r}55L;;Q2GmolEk)mR4{qQm{XtOpD@9lj|D`7Aop~+zGt2p@I^4c4>Z|+u zLK0AUFB2cL`0L(Gg%!P)MoSO?)-O|O_xn`oe23=_vDQNYMX^?~;99-i^5|N~9&3qj zu%DLI`bqxKel+FMJI!Bu^DNsCrMJnt=-)^298(8K-5d^Fg*BT>Ie3iUJ4+wtw>H^- zxMW25-=IpUIWsY2$Jby9_Gfkn4@i*;5O0r00TLhigKKPlz;;4aTGc2>^iS=meQ7jx ztP!Z6zvS=&6L|15pJJ&sz8PLSbg8Me=W4%tqju?*Q)-7SjZt9%6~^Sgk?EBFrQghXk(=zZ_)0t zWKsWpb8|*}i~9E)l~_|9f2VJ4WdP&^zEr@khEnGbT?Hvcp|9Td*KP8bwx6N}{tykdxl2XO`J$8tr#$n;P8rA<&GA55<4HgcC{?-zMFr{{Nm=xtbHLR6Jh40{s12WrC1(kcO=l|Nx#bY2jM9M z+YHq&S3xlaCEP<6%?h|H*IjU-mQCF#wT5Dh#p@H~(z~5a!K2fF^LvO*XE=L;&zpU| zNZdJpA(_ADJBlU1*>qb_LbuYd!N!sqQ{uQq7Nv$3F zR=&6F$NpWbpJ3H*QgyfQP?f=>TW)8qa|i`N4U3%VfVhtyoIA}TP|`VOy(<0@1-FWWQ0*K&TTcD9)F_*)vi^2!VqP9<@1&IUSb!3M)Fe6qR_;;6zkvVHcwGw^( zRr%M$D2A3*`QkZU#z)5OwLSgB$TN(c1Xp}T75NP~1j&&jC=BZEhRff#mVz2xKVr&C zG!Hn787A&DaUo@(kJ?HU_2nl%vc$UB4SD*z!Y~N(OsQK{USR_rHHD zs>DC?g}*A{uS$K_IjTkO$%J&jm3f7i`R#a{pK<=p^h^BYO-Ob%ZIyc_jq95m(PzAcsJn-aV>2NexcTEWn~Nsz9O7`8hW; zvSWMZlb-qGenlL*5@_ScWjT+QggwR~!AM+fMQ0@8$ZrZ^jZY zlPeqghm6~s_pv+|hHMZ{s9|n>I!r-t0==;ObzPzQ@&N4Ta=(=Hh>E z%f0C*sefAw+MOQRca6AM_Rs9MtWkuY5_5$s*GxH-PR=hKT{Cx3FZ`@g!=J_BqKMvY zAVS@Q^#7=#IwsVQyeBd!dBWU#v63T$9PSa%iV8{#Ha@1SjKf`*zE@4a3!TAZqkM3R za<Bl9B7K+C;e_P?D+hCE0&V+zZ zf)O@fXKdmUh^X_)7;08pm;;+T4S6To!3|5z6UGaUZ(`tL{@k@v$gLtnI4PD{cF7IK z#Z9*$W2p;v0Ky}39cg}7AcO%E>nGE`#WDyy1P`J2T7&WIe@Rs{rta|riyzKme|jl_wQe9d1gU!06yy$x%)bW~2lS1PJof_Z3hRr9_=!P%-RRJ= zn%QrU7EEQoQpNIvY}y}`-z8Uim9lnhvZKtQWYGz4IqRiv$7yVUX2Nf?Z8`E4&xrfF z%(exrO$o!vXkF@3>nWO+JrHtA6vYq?%yj>560{o?xnMo%-yW*>Gl~kV(a&nOK)n?Xc=Coo*ulyRijF{AT(A87-!N`BYuy}Us%Fp6@#Bi*=NKq z?{Aer72N<)Hb+FsVCVVgLv$B3f%utavO+Bh3y0HMp?WNiJmn6RI$ZXQ-)U+n{t@6z zgy*2~IQ270$rw#Bo?&6vhwFb@AKT0NEojPZ%4sz`Fy^W197YJI^TMSE|qV3O}~}CXc>I;vGT-6l=BnCZw#avdQ1%H@QqCy+5Lkqby_maTtQpTS5J^+92_}ec_zFj`;fn{)&G^=pbw)H z%An{M$uPU1(o*il^{cA*WGAmB2V8igoNSW=$hn)}^8HS<@!5bp{2UZSYr`l%LBIzT zRQUzlR8;aoSbS{8;gG0cn8AsCMAWzW?e4y6QR;0 z{yToS{7VHfj%$jJ!Q4yhTMUUQ)nWXk4pW2lRwoiyg!(AkL_E{Pr*WN>a+##SM8&Fr zYD?Euq#*HWYCbF~-JQCc3x%D8X#LBWB#=K}_ZegZ$13&FQnZ?%)c<0hf^V=%!4CS` zf&E|yg!mH#O_Yw*AL1sBYS(gY9@lfmG<9aBQYXBoJyejHu2?!?(M(cCCF{Y|AyIxD z?BhedRgdUN|5D9=FU>rpy>U>aaQv!9C1mBIU@hW0SMnAgdF9y<&TF1ygNsR&=Q^6l zWRlp-HDFSSYt|{GWd5cvq@X0H$@~Y~#V~ROK3w6T@tM}K2MI82B;Cb{6}DpL=WjSM zqK_zoCUnGCj3tw4aRkAH;8E6eWhDIIw_2>c9;!3JLjwv^3bcuBk{0Nd!MysrG;1kh z^;c$a%Xp`D#v)QnO>j7fLnUdZAgr@zr#cpdQ+|I+Q7p;FEfez)VR}!Ehfx+YbyMjn zNDU6Jx=Ecyxy2u@1KKZPiVjMv!(~tU%i>2B;*E0gM!CjU1!J@~?4?QVST|-ph-)!d z1LqA8XqHRfc1V;bIa|$$VpC;-9!!+?#M@;mA1f|)tpqo*s{gg)M=v=3rAt`>V60M` zb*9YjGTxo5ZSC?eNE9B>`1m;PV6H2Tjadu4Mu%uq#WYNz)jM9#D)Ya4q_mx-Frcl# zf?!P+{VQVjmIEVZ&uXh7P$Se<2vVr3e&Wo?SX9;Qm~;~7S4mTu>r{uPnq;LHsDy5a zN+gTs&Fo_cE0@wK!HvItuvq>8=Y@rqt}y7k%)w9+BGIPa7VM3TqSJ6R9PCeC3OjJ5 z-wL^OhgW4smVkKNfrp}7j);`q=oz818{bOUwI%;}aDtkt;3xHWDJD3q!l>A4TT69c zhts(vbqZpUF~}JcjeHIYM>dx0lJRbo0^OOt`jeyjVf=M>SO17?{Uf{AKPy}R7+2ro zuTLLL-^u(x+6O;@9VJz8yaXQmbAP|C@#DjV)6ezUON+m*=U$9hf;O{H$SkEGSTPf^ z2?!xwMcct5-6{ThjqVn37sX#=D!4}liof#4A*wxF1%Ge_r>dYq1vj{YPlr-)H3c?* z8SV;?&|7Egt*@jZlOxl-CHPNN+2gKkAH8(A3TC*1*VNR$Di}pUaP-w&eW+<;=3h_x z-J87<YYd#z%V-niQ7MyA|Yf>6x@wdBq*NY?$D;+%eY5 z?ya+*N9?C^t_piPa``v}(dJ{CDEuJmM49sF7jIEQT087Omz)ofeu}8nV*W4W|HFnK zwp*n3M(t#V-07m{@uzn%{?2ElTYQwW47{dR-ggIl&$97zEVG@HH>uFh$;+OfS3-Fi zLF6*ZOL`iz#9C&DakIQ!#rnoAJ3l{f+BJG^b0<+q#v(f3yA%4(>;9hc*Jw}^?EP(! zq*dKcxY13|`1zJ}LZ|tm=AoRgN3d_Go-uo`i?#G=>Ql?6hB2gvjbZC)vVZSb<+C%s z*EoG(PW`wi%0X2t1>T%|RUyG(mUKmJVp)vENfsb$hb%iYnO8^-Q=hqmDiqnO+&1G2 zzlCI_v&kYlo2-`DD2eHS7=h)CH3=55i#z?#CrZ@5;(Or)5d)h4!b@@q^GeT1=A9`- z!_tOrO_MUJ6fuK6TjU^GlvoeJPztseVZ!#HdX$j=7x<^**acGB4#n#1OL!#ew9)?j@@R>`UpT6vPd4Vhu9Ew zz{V{Ve&y#=uGRYbB)4FZ%=3gvH7xcI5vjRQ8S|uYqy+l344^xlvC&ZMzSBf8{8tND z8`-A9N{uXxQxZ&MIWzsCXy!w%17CFHQn$#2=D5V2iUn3l@N-sB!>~-f5UMI6>~2?n z1~NgJO~SJg&Nq++?R~q;07Sk$k~<7xYMk0H#7|SZy{Y+Q3%$!9Z@Gjf7P{<|(sY_$BQ57yiTnu;qa87 zV(bhC8B0O%u^HG1Sw{4+teP20En6pfK);Sm!~FiO$=5iBG1l2J*L=pbNjw0*u}V&GVFJG(i(=(XrDqkm5?Nq+{-_lXO{_@;Ta8>>Aj&WtDUhGJ|g5T`cU8D+4q?-Qy z-$)boJ65^Mn2v)mfNcXK64iOo?ZAB24}NxItSFx7t$~j%a^kA7U<(JHV^wN&A2a6rl zg?g|UPrbzKN%ZpXF6;QRVu-;ZyqsRg`A2~HU!O0;=4|;t$X+&ntH5&dlCs8IcOyRV zxp}6OOIlgG%k$>FuiNDG-TZiajd#~ixLb_vHTCIBT|y*GSmUkzsd|G|y^Vdu7C$Ew zt({c!*@`roi`lF^Yyy_)J*(#4R@}~NU+vOV9=W(WG$-C(IC%(bb)*b$@#FvS$oQ+6 zmX~v?x9FsOVwFp$JfE6HJ9;6O9B^Rx#N&xK`$`@}EvEZdudFFrA^tV6-r=PSewjo9(* z#Vsr(XV3cclasb*BG`WAXWr~@I#9Nk_z7E(pCrMU>L)hK%d4;uT)ch8jD8B6=GZzB zR3#=y324em)dF@+@~#}R+N)=7(UU3t!_lOU=qrH;@t_<|=h_rYcqRBLb@8*ftAL|UaM{o&2N9}!1Bt0bQ+)bmG~0OPcof*dT| zT`fbAh5Ep97A$)EU*X)>%TbzM);OJ`Kbp?vrC7`9#j)O}m(<3W^sbG!A-7+Gej8!q zbjdhDY`s)Mf;}7Cm1p~1D`8<20X_5$ME$l+jnk!Q{z#$V1F8eK ztWBPVfUou98YZH9V*9r#LYgVs7#3|WuYuNASI67VX8$u}cGAcjxA4Zwnu_)tDA^vX zT;|Qb$dzm_2gTPp?Lld!YpJ6BNN>^Q16Q7oO5o*LoCT9J_p;J&_=)4t56dJ+5M}9B zl2F(Bxox3-F#jL#Jm~Ye(+}S;uRqie73hbm<&D#K&#B+9gMOmZr<^jXXdjkNG)^Co zQ$M)#^VaBjNoqWz1n;FT=eKENEmx;-B`P+YP+R%37k`F#Pxlt(9pu*MsvFzN>x(nE z`36+PX0?_kgx<3$8Z{S;XArKBN{9yA%TsrR z?aT^)Z{-(SrjF-#yyLItdAu%@u%F|qzFDqXs>9Smwkq&uJ%&_@wZv90+x&l+z=?0^ zr_|b|>B3QSuRam5*JjrD;#>Q9vl^pnBnU5lA++ALiC6ru-u0J$hC*0^y3nI@GwR<*Z_39-F3|{!hmrhxWuD1RoBQ znAZ=Hh{K%)we71@HE0y^PQz#7kcN*^$5KF!Q)(E$u;yX1C1o-iQjE<{M?N4EFg;;@ z|Dbb-iltpRZ;&cZs~|CDsb`runc8T6kSo(7PX9h^Gb;2E`)qE|H9Bw==^gx9{Ab&Z zb{%{k{-*hbLG5u3e$2+qeXursWwrYa^(Qxrarv-?>w-UoZ~U5HcvF4d9~S*E{B3cM zg1FtY2Wee!uNAEgCWXHf>^E@jUz&X{dhV+zng0%A7dmU>&v6m`f?(n_9VYSXv(e$z zBtY*VzGR0qcGBOnF@EDz>D_x63?a0Vj5(|0t0$JrQu)U1-@(%vNCG|Y+`Nod0(g1D30u^?cn`&-SYb?(S^A)U)3>P2=BaSfN!8zt7+TbX8+o4urM2SxKYZ@jIZ{#6$LGd0M;%Ik}^YP_khxgqHnHD(5`+*HOk z&fKy!QQi-^|G8WI1mVb`D>YJZq9E4C|AMrP&oo4#TGc8DnwGbb_($|i~d-g z$5Mf^7`F%Y@Oc7 zWSRUKdZ{o@1oA2ow?Jd?9hq-M=?D(w*2Z!ZSYS|$(1tb7g#u|5)o^WyX@l!R8PWkQ)6Ry!$V%U}+H-I7`YxXImh z!!exe-Wj8OG7h%ZYXh|Y)AR>!uytemk4d~zyY$^&I3yg@`5YUJhKxzpqK2-V z8YtyKV_NEp$5cKyeRu8&!ZjU1{V^>BkS`&Sd>y}g@<9Ge8$N?zT_PA|W@gLt9Fx4M zSB(8uizy`ym7dQo98>v;H|JztKW$~+e&wU*#`eqAzM@uMA-H_-=(%~vjGlYhG3bg_ z+C8T7XtZMzFOEsPQCsS-|B1E&TU+cNnV~irt)vztWC%+{Tx#g zg87WevcM|}!2_nz1os>1CTC3L7v3yO$8KI9LRYgNXOg6NCjIYMzU{?d0(8mm38NCr ztCs}Yr-gTH^-2Hq;t%kgOnXfUaIudhxNn-pA_6fVr2VmtC8OKD? zQ7I0W{M9$4YFjE|i7C+Y1_JIwk5Is;GM3l}hE(vs z*1j6LbUM}m0dohmPDNuiau6;$O0#o?dV7&SHoi6ejy!NNDu zH0kre4rtPe5Ir+ZG-^18oMzDiX0d^Xv^XAz49Sj|FT#ar^9_5HS2|`WyFKA(^0!W8 zFL8z!y6|Vzl@@S(>m>C9TrjwBZ2z;9N9;K!d3~s^UO;)J&f%Hiup`b`;6hx$o9spK z!uv#(-et3P>SMecT&&Pv>vy zM0rU&{@0WJ^Jc$kI%h>E{T@ad%?vr+7h0n1%1B{w{g=X4))=~kLJLeaZ;*srgo_#0 zb!L>bkWML~7d+q^U4VsRwOcZtqOyZ?NuoNO{bAKExPm>4VO~)uRniqQCp{Ur!;EH` z7_7|SE_-73sh&Qn`fqvy|4)iyKM=^+*&19DqooAkopb)relZ^{G$ymeU165t0}Cg3%74hZ^%E~|4mIB!h}Dt5~Xl<@<49pXaad4 zPE3PqnXIIGnG@7A{mn&=e}p5W{3+reAf8BA6Z9kF5kUCU-ys#7)Pa0Z#^2ci^vmXMeDO)-_r-WLgS#U8=C-Hu~_kd!^DaQ<^(OL zIR+_F^|GBhSvLQ=|6MnbXtJ>LPQ-xniC8mRxW42L;e)(b!z!uW{Rv>{X$_ zZ-36VY$R`llb>?X%^(zWp!ROhUceiTCk|^UY@D$>u3d~<7hkcxFSBps9(!co(*OPGMQ6SUM8 zg33?5P~w+d(ktN4Oi+ zFmQY;P*1Jz!{4zD2T)dC?fq!wsNFt|B`3*DZ5h+tUh9oonV#hjeQ`|kxMO4NO`cC* zwFKCQuG0>V*pMxL?i*t&|2}1!T+6G#`@d7u-NDsLX03&m|dC zrKFeGjiXqES*8SLo-gDt;}IG)LYdGB@}TG2jq51&o-`$J+4?Xz*~CK>=Ba;(9Egc> zJ~a52y2CYrJRCs78!2^Cw29M_)8M9&e!K^lGm)*+tB+SB^QsHXJ|Ug|Zb11s~6 zWj$xLW^dfaCbV8U&d=C2RATKa8ndI{gv?GAOF0H@~l!>)7b=cVbnJ zw=8cCR+KCuM$+Nmxl_CqZD+K@Nz{OZqfU}lC0MNg^bcFTIm3ZQLB3goxll8{aL^iQ_s`rLG4$^$pYvw@MQjN~ zla$!$MER!uCCZj2IM(D@h>ElRuZG43qs~WsKQwuE>1TzOrKos+(JtPz)+S2zhe>@LJ zJ_=qzpZHKE@ocag86|cs(K5~Np5y`X|8K$_itl#jjvevGy+TOmZvZ9p_lWC&3pS@L z81JSqj9IEHS#;6I>;fSzwU(_@Izb3y->6Sf<+R2}9&T_WMpadRmJ*^kDR_Qk2yrqJuFcnpjPOY{a{{vU z^cTEvWc?uimNz^dRXxj94I4V6Gu+Wgd#3+CK`8VeC;djSP=9D6J@RVj39jcG#j z2jR!U?&Azab-^~4BGVj>pp~Iy{_lStDF{=XSp^~gqkK+Aan{TAu$*8QI`P(6Wc$ zaxqA`kylM~$$pxuV|pdj7Q0Udl&BZ1iUoqgh+Mhetdm0upK_?;!5HQcy%FX#*9W7f zogA7&r!s@n;Q8TTp>Gu3*^z#;54v@W@6a#fcMY3VXa}GakILXB7(00An(YQvnSd{s zAt`CquK)Nu2N8T2)X>L|SL8I1aBZjg8T*(ui&o0~1OXgc!pwjY<`?&0r3Uda)d?1g zxlMX$kk2q|{**c~JC=y&JO1`e-;XrYh<^kLC(6CoA6#w2o3?l#r^gXtwbQ^(3>@ya z+(JIcHgC>DasX78xVT_t-rlEliVEUGw@SPmU8R=qz1`u&n1CTPw%POO`w$R5BsQFJ zcWI*@Duo>cAe+ara&9?Xw=(DG zTNekiD?`|<5+Z4R5EGkobK@f)+)KiFyGkeciG!GPPTt!lju`xvdA%8-a1>uL6(cmF z?0+PLxDw7Y76XXkHR6)sf8#w^=?DrL>PqWscc}n*c&pR*1p8>k_EYOTI8zJAw?S03 zIS!G3;-^mJHCV?L*?A27aU%V7oXLc!9A^V${pDqa8j|_-VJku=tz>6IQXF5++6Bzb zdkQ^pEif~u;FDg_*7+@|&+!_93(jN&??ne&_$_F@q@VA%Tm-g-%_+lI<@G-4d*7M3 zvZi;Q?T>M8)DhP30HHjC;lLL<{;Bbsc;PUP`11qZQ7)O_9nQZn@NLp>labI*92-z%1%T z1NP=T!J6}UMt$gztk*bl5Af(U|47T*3^{R9DTvgRTC@!&@s(Pda!rR#;`S2VzXG{? z!<%F0HQGF1T-gg9Ga8vXn{yKo+G{1WXun39%S2LX-tP6nNWG98aK{CGkgtT3!MyMc zu`V(=Uu}dk2oD|yqO~5|l9fR_kwFGgY&o?D8C-s$lR=|`wHL}@T_l6XqE%TLEGabP zoG9gj98Lv8t?bWO=gnFpa5CfWJu)+H5Ev>;tPc^;;+uIhn0vSuz8Si8tKA>TbHF30 z(tfwsCLdz=i8^q&`_Pq*A|XA7l%2Uk1V_yCtM7|!txbTlwpL=kVn&Z5V?Ekj|6GsJ zaQ;h1(A&~;gQI*C^yG*S$g$p`3YoC0w!a)Gapa>rp$bX8t&qXG(KOCic|cu|UmMwn z$0h%W$pl7pgY~KaR(Dkrd^WvDMKVDRj!WrdOq}|$B&GFR@L!QRKWm9|h$9)u3jK4F zcOdk~8~AK4ISBo9o|RvF-freB@EIKvFy%-VG)xs^_hXr zZ6C618)uN{QL*`qLE3A8ghA&++Bcm=RLO%3)9GDAfkB>K;Rc#X8Yrr_;{X%t$fGG2y-9kmQI3)`IM>xsEngC48GP7`7|0AKyI-Hk0Jlso$}j z7~IX0W%`rg1)xt|O@SI4C*2}OJU-K*X#(po!-!8z>&l2wS(QUZOnt?Uo#6b#X!tI! zx)`8t_DAQRUW9MPa3iKepTtF5{<4daiL|&=FOl3@a3W5t?kWlLV zIA5%ex7}R-isSA?|57t8MfA+42VW!sA$?~@5PIzYHu|Rr`K0s@Z{hNLi~QK!Sn2V$ zpfa~E<|pfT1!ooUL#2xn@R(G%))X@K+OPIk>fJ}*(1ZPk1d9w>yF9600!B{RB%K`EHc-vLpU6YW-c-sZuUHV+y_*OE( z8_rWF`kr=tDIoW^G&f%ab|7n#uUw8ghF1)>4uDA)`fjzn1IWh!vY>P;#&p4aRPCxo z!||Vp)TRF>WN9Hgq>C;4%c-9rj;?E3Uw`Tj_(O2cMchNy_zC+MM9>bbv5!F9WKnlnDAm!pzOf%!=-eD|@2Cf!(@heu(I&2pF{opE4s(J>SL0?OA?W zWw1KspaOIqtm)mQ-COat)4jWX!G+{!R716pRgsk+R^j1sTY|?%iXPy-kP*8hWwquU zy#Kb77MGJsgHBqI*ic&HIJ-_-Dzehjx#bX#=Gcv=QK^p-YkfAYr{~MQ#M?$URQ-4G zApq?`4J}xF|03=rlTW5Dv#aql_LFoz6#6&uwrjlPzu}tHNPB#3{STyG2WZ)QF7`R~ z4T?qbuQiHmqft=F&EJD3-=b};2M5E4K))DQyVB3z>~F}G?-ZZG+g0R6*#vj}4eBQS zC!Ky_`^=nQirf$iRzK%VSa~$g8ey2^O?bsDC{`E7Dpz`Q_R?F)^O)72gJ*mYepCNa zBgmwP)ZyfuHKnB=a&+jM{<^Kw>!$vV{z20Cx`xlcRGmpfd9Hz}Q3GLmabXxIf^CX} zOQ-@o3YfWRV>F90_z4?NK33DH?+ndOqrU7d6JCTvWVxGjN1rLh?4o8tzmu_X{}4+8 zmslt{VhX9SUA^s${oZ<0SNm-)N1cJxiW`~CKi=x+;c!^g%~Aox?M#YFf+wuL1**e{ z4+hi1f12%|X`r$PA6I=1hdYLJa@2r-ZTn2H_esF~mgKMLd>FuTLeAvEUK1UpPJ+5m zVHJHHFzMeP?LX1>sM#e}hjpxd?1E*ifn4z6EaLkc4}_~0f#Z%`mA&p+Km4E54+!=| z3>@~g3J#wTZ@baEOW(qhyo}+_opb`4LjeD{*$I6WZiw&Vms#1Ckc>6GsH<_hQhaOU z>p=RPit2X$uB5-1&fOPVq<@q7|E_YnxLgk_%(M94bs%X9NOJ2p@wS`1rlkh+&H88; z-84=`>wlx8f$j=ca5_M_58rh#<6Yk}{tWiVEwF&1>Gh9{X#c;)Qr&GV=09&#`}!dE z*FaG)PZnMqu!8pw2+!x(pSvu%yIpmof3l`@t&4xWhGRth<6)4e{H+E0O}byWc!MZ$ z5Uc=Cd`PO2G4D*f;CrEaDRzRCFXk;-S+8jUE*oNAT_D2aA>SP)gdI?*FdEXPm~bVV z$h|ou<=r7$6O>EPYTfond}La~JR8}0X`VvzUzlGQYUFRLyJ%`fZJogKT)PLL&96&i zW{{jZJ~jr>BrdGP z$-~B1ugW{-h>{p#6L*$Ojz-duPoi{sRxI&aP+h6~+j>}CQz^JpVN#GX4#8+nM3XF= zBGqew6=kazOM-$MG|3`mUU|jl-B$^-1|r4dvhb_L5Uqxm)V1^mv=-jWhNjj`#%Io; zhbU^uEi+e$c@#4JVs4~?dzi9K{a%kVO-D>A(LUQXau-R_lr8=J;C&V>37IAb-)}7+ z&}lMgb69k<^xyuwYW%nVQR90{Lx)VE-Pp_v`yn~4!8CRB0b_o^F)ZueObHJ-&`(~P zPy0GQ`uJ7E;w-k=D-SDm1*|E~tFwmr><8S|86m;x5glA8XW8a|a2iZe^xYGsp*zj}Qp=pE zV43T3LroIt3ZYqii1KdYM>6xKmt3a{8o@9xEM3A4o9Olq*XP;SJg-4AfNB{76UJKh zh_zhImYOw_$&cqgQCq;qm0l&Ovw2JCkbGP-yMug4Dv%FC2rdC3`0g(F@#bw}nd6OR z3@Qh71f9(PILqi+)@699>wqCBV>vec3`j;9f&#yVZ+M~oDnFaTngjRdfA#*Br~J6o zAE5{IT-YptMbB&q!srQVgX4Dv@H1Nla5iAb!P@D&t^MGB_L0Jh$^3Jz2RS+Qp>h?n zLXLU$x?P~;D(iu_QPf%?hz6e6l3E2v2FIzJ_hl`b?0g8Gi;#YT4!Jhn0~wf4@I@SZ z08J#n(|*e5SW&FSt&Jl;`72)~@tS_G=bZ0+qfj#7T?*qP-`m0nz2+HKHUBlr6a&B4 zZlgeOc+ZKN=ivzNlXyA)E;0DmINScizF#gc@x7YQ2sk56PV*4kaCRmVo^VXkq2l#2 z8VA0?5UC%FqkMCK_-OiR&c3^d?t@cVZ)JWg-UQe|0Gs-bu4zT>gNAgen^!TXyPjx<^N8V|2tJJ z%{Lk?_2ZikLQX$vm4BvBk9l)mRJJB>n6qLm9iCB!(!v@*$>7)v!>}{-T|h(eEmzMt zm-+RUvC0YPva3Qqg}%Brs5RIYZls*be6MkNe9O1%*HN{egTJ#IX2b-+F>FdnzZ%~% zX~r?}EjQHvFINqm8{t;u;rEzRAIMnzv)sA(PEP)}rK$Em@hvxd&0FPng%!+cV|-rI zkIWmxVd-iWtiG)W@2OXkr=q#4nK|xV))^JiQei0F4vQw;=(k|n75_~*ey&K+VPUFM zINlbAPr;XyLPxS#o$6s>>Tu>-QYdCYt-Y=bQ+^nWx2)G*@sV@BFx~PR22ftmBu?{# z_K|(UKfIPFa@TT(mig>!bLRgX)#7~7DpgDLm9JH$-KWjkP1zbo`txmlOCkR8r>?5W z>Gi$uoVv0mCy$Nb7lqCAbDgU<_1new!arRzd5;dx*U*%uZYM{?XGl99hYst(5N}TAQx>sE^)|D75{puBv2&)cwNC+u&vkfA(4b!3QkHzuWYL4uHM~L5QkJRc z$n6Ub>$%U6u6S^D@}4x>)>I8D%cvUcp*{|g1_P1_jI7G~$c)TVRLmmQV{?~Zl^K_1 z5ZA~%Q9Z5Wq-@i6GJpH+f^r8obtWx{5zADsTu7;iz;cV@-sd6$ha84B6|0^45$-_n z56*l@r=9$9xXWRFvbxH;N#+N8IK}O}G1OR2@W!7|dyiwGdQt+xwtw*SexAm+5I302 zMe67LLBY{MmGqke3@VAY?dLV^t*Z{IhsyCYK3A1+GieQSq!0mU+2wf z0iA^4%Id1mT2<4#_gS^4jnq_FTq>VejzcV&S3V|L#mO%H%i|+2u%6|=dd)SWMdBcv zqe{l>I8&7qb=*fH_8?HXVH?5GUIk4}_nghL(tp2^j zHDQNZBo10RM`Xj$G5H6Ws!Tk~sFzJa$Y&lTF%h6EUCt|X>z0XdWXDVdsev1`NHE1u z`2O6wI!zmm+h?wrPdmy<`28)BLa}Lp_#NyxWgC=V(W63f$dXq7%O_GOPOFo8)Rt~U zKeI5I-Wr9W$W3s*1)A2HvW>kJ~z>2XPL zNKeg$`2hy!Fj|s-tyVW_lEpyB$!gJb{-_XAfd&KOErod2^m+BSGO9nEJ|nggU{pBl zsb(-Q>IDCPJWV71eZBY}DDYeMl0Itw!qjL7+3TDwMMR;eY^m)ZiGD;B4(iGp!61>5 zh(d=|>zN$(zQd?@9%Be87-Ad9!&PfT&#lG_Z>GlZ%P~Wo{xj6f!pK`+LNkVnQubY| zwd-x#;gn~JuMxQ#w!ZSY9vRK$qz*Vv$LowPpiVCy^sv60Y$`2=TQdHYeP8<1kdj8iBxtchY3Zm70?1zT8#I(@pCIUb>u9W}&|9Vl z;(O~G4vb)I4Ss)^`}nqvVCD)92thnwfx8Dv$Esp7|Jl`%l$hrxkQTLe2dU{xjhYcv zs2Y4)9(F@O!Jz_$!!YYg_za75W4Yc>4u0-Fc?e-Ni+nL{EKxHh_Ir1!K9m!Vi_&tH zCX4>Kn=0L1TA^FolT!;mc@*khT(MyD;4SUABel|u(LdV1WW^tuyJ-E8IpTpG>-GZyt{A$(GPf9%qQL7HfBB~-n zl;H6bJ3~|*Cth&A;GsPrDmcG~QpsO@3lW#^q3Tjo_!Hm4YREV#zDT|_@oPHE9^wA! z6#kU%BG!RlPFN*&RDjiNF{&^)Y+et@-AC0xZj}(^?+m$3rTS(=h~E?Mb?i7!oCS() zX_^-9wR`sC@tZ~Q0+l8QRP|RSKA^2zP^>)$1%_g}RvCBNo2%frWKjW!$G+1UL%D8ucX|SN|*n|5pZjq33 zDN5aFA=cy&%ig}WH5k~#C|iTJgL@czs9Ru=eeDp=P7KoLPRAf6J}R`>snDj!MIih% zb>8@d?f>EXEdBiW6A~O9*|BPtU2zj1x#!=pb>vUhP`X6V`L}YG)shTl6-Yr)fhQYD za%*r#PtYcxSVDdF%{|0&X!E z`$Z7!c{RTAEhX`h+ujFYulYH+T_7#6hv$^|tEwxwWJGVe7aDoN;%?RYUL8wZ++(z* zu;Sa!Z@5srJ5?&IkrMIDR9>sEgxjtg3Zt~#(Wdf*%MT7CIFX!`Jt_qeLOOGW2C!UT z&q=S2Z)0=2J_O2Kt9li4t=9T|_%6*UT*gOM39<(CXwmW93xEkSeUsGt1oeITN2FW+-TGK0u$F5pQ zJa+wH=$KGH_2dCZ`fuj;mrq{yus7#&?EB2^Pwm&g&YQ!BR$1C?qG^?FoAHaIygAn( zXEV1y)2Dqeuen$(I;$bQJ2ZwFwjv&1tS32lN*b#{+%xU;5 zSlS=Z2(pPT8KNih2T~dz>Afd}&5*!4IuF?Xj6)n|#C$bPhZqpy?7E$VvmFNwitrar z8sIT?3sPa9D4hKeiX~MAj}y*bGW^-r^k2Z=9CaH)5nRX0vMsP@;0>RozD>0dU(Ex! z21ow~jzE#utYd#kCNVsP*&tdjsuBP!yaeZO#PY1>rJ38`o_e%JMe-QT|*%acW7Z>_SQv1Z!AmcIN_>(E=gITG5L+pjqFUO(gi8jhy!jo%6D4p-eC zQGb@|_Tur(?T0Pt^Lzt26eDhpy4g#nq~;g(ZgRxQ6T&Y4!hKIY zl0Wn^RXKQpstYe`-!$dcncMfAviMc&dIo!9Y_x2s=ow<>ud07`roPvFp58@2jL-6K zRam{{T*JP}fCU9$n&RN`H)&yjweVu&s~?&CJaa`y`sJv;_jC^y77q92nB;xc{i zIPo?Cgy50A3E(AfDIdNuxK@{muS~u##~enUOmGwrQstBcrES}s{6E?4$bt^uRnPH* zhta~3{dTU_-rqoz&HQMs3^$?V)n>rMU&fsycNi~eUhAP;pinpK1=fiNw)$T!9fxdr zO&dz}=&4fk+*ge*rVtZ7aih6oztF=~qf1msII@;rN>{TpXl#k)&4S?6p=K}iR8 zeMLEL&5PmIP|n}1qduENi}*Ic-lijvKkoPP=3L0v=eXL#Yu;a1OgZG9)c|@MEmd0* z*53&^azy~*1qC0?;#lYodleM1%pr48EZ&{!w`k`Jt>g%fmO5XAYv*89`Byx{5H>nv zWYbYT16okvHK&CobxGG51;(ntK3>!J!$-oK_rxX+^8UbDCS6Ovv@R06QAf*B!P28M zFonmwBnP~3H4>}W#YXy57+g~8qMCrt1fS+OO?f{x=X@r@CX~(1V7srafdfjxgN{H!7o$6Zg^R(wMf@4;Kdgh9hG5#r8{%Kjp~`MkhV!>? z1E|YMT_wLqr?KzuEt369o$CsAV}%p`eBJr(@jn-Cgxkch}!*-jlc0 z;GQbsxOw{2u;j9@wcjQej3L|zJ?|G$6pJ^Cvh3H+2b8na1SLiXyby+5Vb`JqH?A>e z^%3Hv*mSa58*g!0A}S!pEKvn7z3v3xBxx6onE{%JT4U zGseO|@k`N9Cywlhe;+RgzY5X;5DWs`!5gGFI^O1CiTJlH^@4eH9Vy)uc4$ax3MnoA zy03@o^TO)Fn$ktt;$|G)DS#jPN1u6f2I-;p7`i=$m-4BKIYy5ip{6sdA(p|yGB*eZ zzlK>ZMd8S%xwXlIx$Yv56MUga_&5D!{gWpaq|91{DG`>I7ak+^wKzXG{0Kqfs_XmL zKx@Cz?=46vhhFFQ|_pgM?Wd6T25iq)$7c73uW*1F4y~l+M zH+U^v|tm}<0oRCpEUVgXs|}d=0-`pI`MGUF(#v#Bvo>FO@33_ zCbnZ8)?H9{C&x+J1~g{R=luUAsMddQqWM2zewWJ~c6Kr>>ZmobOs2D)wuG|i$7!ZG z!k3+}%G_oA9|`RN2rc3fIxj7(+$PUqHqEZ`mgD(=$*trPl9+q zC&7bHu$HBQBhSU015M{Oz9;!ig!o~HZP!}cQGT(b?K0&TD=LrwuYO#9u|vqhFcRoe zjM?`;c?bE$o7W2HD8IO)MhpI~Mp+GvWOqcvr9#hf-3Ot$b{q2>A;pGZByuDPYlnPe zA0dmJ*Ho*}9*So$h3-b&{OP-08&z){HN>XP$T$Pj3 z=SXmCS;ZLo1v8cLq#AO|GRA(~y7Ncb?xII$^2iqE9ihyNLy5z2ZGa?O84T29{NGG= zI{2?!26;`V7GURR$j;wL7za@~n{@o@yi7VipML!tI*tU3e%D_)>C$XY{=}C|S^bfR ztFTLi&BX}f%9Z9jU_E(TX^ZFlfv-2RIF)%wS^ ziS@C>hrv;ObTr+1b8%v!iHV`bHi=SvApJr&xbOg044@9kC4WR(>=*#(cqDHqIuUVU zDH9=di_smHl9)u|A)i5WD1FO7ViOAulTHg(@2R&@Z=v9@$KbG7%MGj{RB^uH-?Y!* z680Hfp?wB}N>Z;ni_|Lv?A1Cd)PS8B0o%;6`8%~5d#w>AP8TJ10C`u4Yx%xOVns$} zgyT+)!=MI>dV}Oe(OB)%QG7PR{LlOI(A}pgq8FNWIdG=`lliAy5aHVd3)3#~5A$`F zogjQ8-{CHzFpSax-OJw^Zulo&{8t8P z)-x64k!Py;)r(VDp3VBD8jm54uX@9K@)uGMT1ap3!{><{J$oX-v6Z{vsHLmLQMe1X z&&j!v@Jjwh^tmB>?75ux6$9HvAZ1AwT)el{j)UR;ny0Gy(MUIWlt-_8eI3a9u ztWZYnIH%*6pc4&08o&JO9GdmjN>MUAKj$cnS+KXpRVuTV@y{b69&zhA5n|0TF~kx= z5O(FiZu_QCn>|M-nx2I=F>?OomrJ)e)>P<>7ll9zOmO2EN&%JMX5xCtXGw?IW(V=T zR}A!Di7(U4f!5zHrrk-4+87pi;QCmxjr^#PvsFFs+=@h$s!bG8DMXX}UYEqi;-XDFc`-f#K| z<@S}}rgK@x>>@A^@7Y0MU>vx#Rep%}+r--li=V8a#@h&kZ-+ccOyp8dLUN{KmeE)~ zhZ(#yg)IWp3CB$>s9u>@&R07#&LK8T9{NtlvVerNZb2fv5cvlZF!LLrt^5QO$i8QZ zwi(QuYizW=t+a&om1B3ImxnGF`@m0T>8(xZ;rg<#8}m^fI9Hw8VG}`W+qU*JAIEG2 z9HFDgYx>j(7a9&~n`CyexT#IJ;VxXijMuaE_w}0oL;)-Poz3vfA&`UEe{qiYqou;N zg&8CBw4+{h76Cj^%qfy=1}9#=WnZtk7ne@e{f7BfIK6^i9v&52HCYwii*lx1xKX1; zv)!f*f7bXX^&1V6&a;YVM`13B$7;PT`UZzQBqIL5h|>i3va?DcSmQF)JHi3H;Q$!a zVHy}*e2~~WtnMN-`XCxp&>DbMe5R4x$T@TKv=6Zn@OPI;&70mFmcMnXV5ex!KbDxv z8fD(i@r^UiCLT7_jLg?TTd-xQ6FW?l>FcUGKcNMiXfWu24w`$MG5u4e6YST5q!Y<# zO7bUo5;xOtx!&ky&uPQnm+0T23iy`uArV9ST3(y{BNYF?sS?E%Bmb?3_+lNi2O(O1 zWrR+*OQ2`PFG&2%iEBT9T5>oeQnILX`44Or&`dA#&w8JbxY>MYqG zj?%d6I|Tbo9st16q_agc|LkaW;}{*mI!K#Zv=cgh62gyt3UY?#DA1Q$WxlW@l+9j9;W@QGiIm#{fOx0^c`E*UsKD|>XPT&s@K zEY|2jnrAUX5FK3gj&58i;dZKa`bmwhTrbKsvQh5bQl)OUYzX&0_W56ob&V90;F5VS zv35X_?#U@TJ;HyDRvbG?oZ`R3y2_lj1It@GLN)EAc9;Jg-@*>Mi`j77Lzpr+_t%Ku zIGLKxc;AmLi85pn-aD-2n(vJtU&!d&fLcykaQI@{DW)AS^uPLhbN()Cpl@Hz=`Z^c zEe@bx{I6CX1biGbsZncs@;7eL&o&t$q8ZHtqJwRx+%C7)aDz)^IM3Z+LYd6pu$N1} zbGah>sG;D=oUcR1(kGm3X33bk;C)aL-UnZNhDJI~C7I~Nh@Z2wtgBBQO>BbF!zFTE zr!|(@7$VErU+rIS{x9cWQyv$v;r#0ld`K?%F{h)a9@Ho0B79}R+{i64n z>!V*oKU#k}^q=BecBfAY{-8g<<^6$EzRlkO4dtX8S~uBj!%Ti&w!h-wjfd&)AEb$r zUT5#S>^gh53LftG?(g;Ple|k0Uc)Ksf&XXb31iuAdPr)_Hc}FlJwhWlhaJ-CnW&YM zdd}(VSnX5o<2^-vC#RJ8iLGQ%DT*eI9$fG#-vUqGiXL>TJ; zx@xG63RvGF70l(Ua>_}hnNcc0ZT5e`o9h3eE-Ui4mIS>>nk4=VS>4;I>3TDn|GU39 zeH<*E<2D(fZK?|Pgi!2qILflsY^v{OP<|n$`%G4qFsmp?{HKkPZw{&vTJ+O8M0e2} zos#xDWSDi^pKRwttdX*ee>6eoLyVK}J5FP36b$m`f5gS`XElMHn&yyDJ8Awr91ojC z*;^oYtRgT~ehaxwq%~_*$hpjX7a@lB9fQxwQBW~fX~*d7Z=Hc{Wo*2)^K6`uMvF~K z0@c=qpoJsAL??F;Rrp;v4xYNR-Xb_0U(kb7!?`mVr>39I(i_L=7ktYmnSatL4%^|f zRwtKB^b4LW?ZLOne3d4)P@K`hvH5Xu_{ZkhU`T!Wkj&KcUq9J(V&BBox)Lr9b2tT? zOM**w+ClnJbwGMHvuEKHWc%_!>)iP}8M5G z;pJjBU-aUh+FQ#y)E+EOsqR{H$q!WXCsvbF2l5wlFQ`j^x(sF*`5Ov?>(T%paQ9p8 z$p6;u&w~0F!Qj!bSQiZTJia^V9~^cZA490{Taa93K2`RsFC4xE%Y}So`**p|OvVa? zRp(@|hbAZ3XWbXlF~vCzdD?{{bd+;e4kS{bQVrV;XfvgLzUsWU_6uh#)At6Cy#Vm% zb_brZ^aTISlYkxZohatzPk6pf6P{;r}Zy)mix z#O;Er7?nZ}DNemEf~)V77^{c)im}UsB0)H*2MD`hF`0kbHF-K)s3rddE+Pb!tD6GD z#8peOUD8obaNh?Qf((|sknef2W(AqDW()P341b+`Ud`$cXeif5F)LyubTOXWLb>+# zs4pGo?SYupczZbI$c6i|GZ*eldtA6pNnAWwx&(N*c*v3xxmalNS}vY)vj7}p+mJ2A zP}sgsG7NGNcCS!<6`VsH`_lJm)+J0=Cx#zm&_}ys_SPldz)7GKQawm6ov04w9B%x|}m2 z_$T_=_gdL(W1m)SFZ$c#d&rC+fG?3mOasXQuW=zE^xF@e^28oF?M1gd$vc?4y8cAX z{BhNbM}j)5AAGhiL>;gnzPI#);3*ltWV5$4|8W{&XUhN@D!Fud82?)=Zqw7Bxu-{O zjvx6$E}4rDet@phdBdM=f7vxK)xGkGYry$mf!0?x$MY{$o%DmP)*r`V3}4?~^gi_o zIJVdG(HnKyUi2mQ#nkXyM$`(YEk_=#5e-+jc1qIGq7=tdw8Zj>jBc>}ajzrE-;2IzplQqW$sl^aNvPg7-}twqo$ z2RwXMMC}u);;5Zh4B8IjPphMc!xu-*CI|f7ntJRpt9v&$eq!+T`Vp?+q!D!D=*7$R z_VOKixn=NZy@maQgXo1%rJ&!hB1YyY#PcqwKEL{c>ISasTaj?{j>%Uj>#EZAUhY&A{CK*6Gh-UQ00Gk_2=LgYm#2u-n_|YWU{FC;sr2ES(JB>IiLn(esoA8~382mQlqtwnUvIN5Oab7jT)HCc=jQ#INzKz3caB zUSGeLF|%k!P&@j&brkCdhp%3(g-zI^g2{d2D(Z{2p^ge-SpA%v4mY zTV|DmVZ7)k-Xt9XTn9(;Fqp(2jB_KEb29Q$=U+AQbLm~!D)DM8vAnt6n>QWuLd&-) zhx|CPt;!%Ip?y#cy)cUZvD{_cd$W?X6<^9HE|!#4ZfW4d!Jh{2KnZ08i2<*34gJ^@ z`$$( zn!}r{N!I;4{_eK={_&tHzUgVNxsh7&{9)9{jc+Ts>98-C=XuS~Qi!qiyhWpO=Jxq= zc`vrFtM&Mbx=`%lJ1Blu0h?>u{6rsZ;yFK`tMmJ`@s;%4&;6la#NTV9#mUd{JA4V6 zZ3zjGBrC=ed3#iDm@Jnh+ZaNmX+%Z!;wtsid*shHNanUgXI8Zr^wZZCS} zVH1#)-#;g@EZ9n{D$5_k+f&+3mf>$8O>9i`X{j0fZw`z4cj}uokl5%wRx^0}cnY?s z4yC|PdBNEkA^BZ_#UF*-|Hvy8u_oZ1hBkmb1;RgSj&-X z=@>2b20yFvNoz97~z;y#+MeOgTmxv|Rh9)gnOmU?y5uNm z=FNHEN*F-n|Hs~Yz*kXp{o}K@_j{87>D;}B7D&Pky`)luv>*xs0s#ap2%(7;E=>fa zSRa)ZEUAwQ(nKKylp+d50kJ{=#UL002toq6zwgmvK7wP9C(ka!fLy_}qSXU@MV$E)9NXWw#b_SslT)&JQ53l&^f#d;kQyV6#F~I zsrxKxydD|&a!TG-5q=oM?iKk5{((6cvqx`CnY)`xqNUry@tVb{mW2cVP(yfz{5N;6 zAtkbO;lOJPyWU8ddsCbip0e<4@qE~Zz%xwWoihJ-`@E@0uZ6!+8DJk%?9^DJ|AYtd z)=$jylNWKy=_U8C7W>g^>Ms`VoQ>gluQ>a9*RDN9`@H^mzj!I94~wUaK6PrmoX3&x zkjINaop7Qpzw@`a_#_|`s0IJM#?<~bzq&#AK z9xd$7AcXwf-a;wLvk&22@jb3~(clee|BJH&;ImvRP}0N7_w#wLvv_U!+9-sSPe@N+ zh!b=7dC%6b^p^57B@=m9Ao5BPkoR7i;rk~`-+x9ngoDN5_3($f{H-yH@JuNVwpe2Q z5!u6AKNlg-VaqzStTc*HKNG&^ex&>Nzw-7i79^vGzHHBXJ}X z=Wf*|pB880yZiiKtQ{@|oIZWAb|5&Eis|?~)mIBf zDA-=D6+p*ct(kM)`n@vo;PV%2Z(=PLQYCh;_5wKiR6e&a&O*&~Zk|+M7q0++mgG+P z`y=}AaD})28j7i4dCHjNILO^-9y+FWr>T?Jf^oB-3^J?iuN0ui&Bo2{*mvl3B6giAl3N;b}0-@KteFyb@BN zZ_1c1c!6E^EnV;c52)|}U3Bp8DUC&FJSj__`Lx+N|H}G;q$k)ApVl<_BV=u)`XwZM zu?@FEW;~^gJB=8LxAfpV?pehT4|EAyBE?F z@7pKx^IpQvWJ;P|OYRkdh@(e~7%`T+PexwaX z56}sPl;QLIG=5Y6-FOT2{N?n(TEDM~v4(+eZld()hW>#=*KJ{ebZyn|B|raIT<`n* z_t5i~BC8;Z9`yXRK6&%XMD4b#nVg@lc4;b3GeT>Bn!OjD#iOTm+%>)U)}vFY^2zVg z{k6Zu@%wkc_p~9gil<69?`7{^sJyd0J|c(TMBxjN-UyEkA1J+d7L%DE+!*Pd6o(|} z6S=dOP=HiUst1y)>$RjC3qlEwockN{V!fA;-i16b?vm3REjIQy#h77bUX&7SaZ;l{ z3wIVfdFpk821})UB7gtz#&{kKqvL@(_Z^yy3^240vJvf_fd}f>YfSIBKEij4XOn6U zJmT$J1|WCd>!%ctsLJAT?|wYe-N*u;qck83V9p5`EN6kQr=P}6`k=0(JRqehJ*4&+BjCcCC5|3@i@nW9P>H8&v7-!r5tB+oXBwm$9^0WIL2^n z$kE2JVlBTv$KxCia?I!WKF8G@mvWrRaU#bN9Q$!h;26WPAx9g>iVS{#j>kD3 z8Na4P?o5H_{X?o|2jqhWJUw+Buk!ah!|@=;e2(vPT+MMQ$C(^QaO}r1fnyBEh7u~z zPt9Wp8u7i!cnQ45-ejt#fz-yaPbGQlQ=joPMV^s%yE}%f?enO+C5xmaAE#is{xsNN zm}-Eagt4qr>LT_j+8aFFCBEQ2*;7zipGDLb#JQ+5_X+Xll1yNgTd zK~S8NQx;-0Yfp+$j0ha()_qKmD0LHx$?P1&GYWu!7~I4SmUyUvmV$a@@%2^t@v?E5 z>@lyhJXf}VhvYCMVbiMl#n|O(Z%>{kJ*6~}Uturg8pziVP&kN#jpX6><1+$Ml0j@W&rVzvf~wZS%*4B(!p}Jcj8*GF1x+WJ$eVDLYSTQUq#tAoe0ZoLM91>r9TX zNvM3Uvrn(Sa!Z;;J;INoK?fhC2XjpST0-LKP|nN${_mqwHoVPad&QoU#L?3qp77wq z6UTKju=I(CrS~sAdt5?+4qQ);z9Qk%7q|5^%37dJF5heu= z3?vi-+eldWj`tnvSN!c^930Zav2|fDtdg=eWEu?rawY=Pt{#!(= zNZ4K7T}`-By-h6wH^FB)`i;%RX43gcF;YUDVo5}B{GzUk^4s`)-*rn%L!}@|5>>vt z=PNlsNdIT?@Id4gDV#(+=pbRriw@;Jtee1k^bnZz9&{qKcc{QpL!SYa4k$Bqc;wdk zg3!x2b0gHNoDT^N5m>_zC#ZDPn}sx67jz{^{-2=|SRq_x{2GDvhpYULaGl{Q|7!)- z6i)eHgk4sy!YTh*R7{k>+)*Py>2M>3?}k&(C%a#B3v88p4XAYFMCS;sWll>_Iy`um zl|Ijt>L;+r;gsu(8Vc+hobo@(Fw7vZ5rz?O+csS*JDxUo*pXX(lUDv*48f1^5ov z;FSOFoPl`rkvSvz`)4%x4Nq6n;2NlO7@~2t+mOKD&y&47TVMyXzXPQs72j%>&-Xfs zwEDv*z1(=#AwJKWihIB1bAM0h4&;=Au)R_VLWF@9lrdXiJ*lK2{@}-g1(qH>4U`T~ z?yg*c{g8VCln%kx3CjhOY{NjMBjbg41h)L0J)qKod-e3WA7f6u`z*GGrvUB+j+}gy zw!GL9Z_u(Fl#ari7j6m+m9$s>pXQq664>TOhZ@QMw?kXz0Gv{a&Ri6j7fy-K9Au@O zUSY4`K7x&1fK$%Dq27wJpYP14wh+f({V%_a_~Df6rHep*Q=LPu)yB4r6<9{>$Dmb@ zjDRcH_xD=BP0;kb9eLHypHg2yF7rfPM!C5j<>jU4Rvez31IQtTIp2XE&pE+!%dPV) ztUbP9=dfO&eZ%@IU$ZhmQSMcIv4Xtcap%&!>oxAr9BYt@ML89SIW2b<|GJfzv$Vho z$Pp6*(?O>MPUX4d)_HgIc#IKGicSZe6+Mf_i>n-N))8rz%zA*1V&iJ0-xjt&U8s6E zU^3F@`{3_+Zo3u5{GjLX9fMpkt_Ey{d!IC+a&aQ98P8m+|(sJvxC+3Y9 zfvt$S49Z#_<+_628cNaCCk!a3trI^Jz$rDLhS{0M4qGK1uk0ANRW-II`M#4|4BL zIEtU@19CoZ$Jjc7J-8kYo@0UI;e5|~0)IlDE($ygS{itr_+YPZF7y|e9~@m108HS# z^8D-Y|2OVe3cOzJyoeO-2uIf^0?GN_zrZB0GjQa04tSOGO8l%?V4uO!`MtnzIA3`l z?L{}KaIj_W@f&ZU!zkR@?)y}We2<$K%J>R#$a?UH)d+>i1_san0 zR6Ea*o}oNVoj`IFmvVUgkSE~CuMl{G^Ofi83v3e{o&N~<6X%ulg~(6EJWrJZuXA2G z&-7ME5O)3DpndiILDLM6AdQv7gYWSX|0V^%&#Qf(0<=FD;phr4kld}S(eBn0*j_mD zD+GSW`O5f_o~wA8t^tzsy}sZSo*7PxAL!-0a(xDB*GpW^^8(5FULU5jiI;-yVskN+ zwGd-O)Dq&upvqxnR)iPXd^VQZL_5wj^O2qz`w8z;q`V*Qz3#lg)?->iIbyYgP@@ge zJ`cJ`3$s>isdgFofbzg{=`O(yksHO}7NG9n&Y(%b-GVi&dvJd#mp-pqh#Zk0@{{P) z9T$sm|5M`6py`=YB-vjK838FIE#w){Ss@ESmxsIoni29ID5j!>sk-Lc8TKdbD)x5Z zJ1GB;h?8)AWJL1_I^SPC($8T_^V=ABuWnP|SaF@cWx<;6y3C;g`489ekguP5&NY6m2zJ3SN>MZeSwkm7ujE%5z^{rtbkx;`}TiIpuk3y%wHH=-n=bqi`8O z_BC=_A>;&Lceua0z6f>2C0-~00ldQba-f&~rVt(WhU3cQE&>=Nz__gD94 zke=G1)}nAp!239#1{}}%+uc7M66Z2Fy8b0#7Uy$-n>l~G>oZVaZ{cQj0N75tc zg~Cy&G~mOWp9q}9`P*Hefm$pJj;_xI9^*Vo!asBVcGoks2Sa$9Fbp^x4nrAi1aKtx zzuom7^jSZIqx%;CFLAyM=;i!vuZKMLBA3bD0IuYG4)6oc-|l+$t-uz4FW-L!a1H0z z0zcsV?XEAvH)iShUI-)r|6Cx)#h%BzEaLoaBBaq*}c02%_10 zsJ|C!bwSVgJ48QGV1Y5^?w?g-Dy-gIHr6}#FRo9rbwdxj!1fvFrr<63;vWZZ$}L~M zId^NWnthVHEtkGb-G}?4zDRvY?m?}nt zR>~M-K`Rv&BTB7Q-7(Z`zvTA+_N|ytd}jsL8%}zD;1JG_0FLCmPv|;XC6yFZ+MlFq z?NnV6P?_;P(lX@sOI%ueJLnshc;&tdX1v0$XJ1umXRBYQ{I7n0aRt9`=I!rq;}P)T zNmdss_o;d|Lf!!#t{omq=OG@@_#j9+^A`zCWvFF zOqn#LlQ?GlByr5B2@^(-8uO?)dhEDKQ^r>ML35|pLo*NZ-vUib$c&z_=h5P{2!A5H zFDr`nMnhK2TmvaErlO@X3REMCweB)E#sS@4(9&3MEP6q$Dq7(ywjSIXvaWX$%I{?F zsya*YZs7|-jfVDl=WF2$oy(j8d)>JPG~4+B=r-qROGI#%c?1AoKe1zjHYG48lMYzHWdZj8LxG&(vOdN0)v ztJ;e3L#qYUW_5u2Tf2ewu%>|CZS4 zK*P)1q3774JPtIWJgHp8lFPe-b}R1z+OxdB+^^&h@tLqkP_ezj=b-zAFF`K~mq9(w z1t_!2o&NyMaAtvSaDIxieA#&!e9c3tcPr2FdRxiA1-w60G_CgbpvLvQ-BHSchhHzY zm-|K4pFp{<5|7gVOM6fbpY#B~j{mQ`9(@cg?`If*COB$~%)qwXzdf)6=i`8haLRqy zHuRX`=>F-zXE^TxKF|5(z&AL*61ax*(lZnZOb18T8-PKa4+Xk79|0sY3>2mbusP@H z`kLdrTGVK&${MXRYPZ|fXdZuoJqJf|KM!04NAJHFxP<#J1Fquy8ek6R>Hh3d)H85& zeJXGW=Z67DaefSN2IrpzF5^61o^L#63}y-D!RBC=Z$1XP%W~8bz`n60SRZ18t%s~n zuoKp{p@XazF~)tDqosSbchaFX-ZfsjBV!2|Hy;c~?d>|q``h85#?3y!IM&#&&wrbrDMx;L zx=21W92Nhx*Q?4;N;y)>Va7vpIh<{q1Ic@y?Kzu@EwC*D|AOsp(06TF@X5Aq2QN$K zdu;pQ^SSK+d5_pI9I18yFJ;`MOg~=NN${ z#e}(k6x{9%cRGu$XoXb1zhX>w8q|q&J#dU8PcFijP%lMh_SBFZVpIW6Rr@?HDUZ>TlwR)9N zuQjMm*jx2Z4k#o_hmxLYyxmvo&oo62SL#JRzU5g_hg?wYrq$zD@&l_@Pb>9i#%ekL zh!bkBua)T2!corP=()!OAL9HYz@?mD4t$aGF9Toad4|sEfMt`sn3Jx5iAUUrl5GbnR(id&m#>PG+nFP?0gH-=_54YKxov=2+)KYMF0+ z4fIXxde9BlPe8X@3#@9k)4B)ze$;YWR%kr}{u}GJRu%isdIJ0z>jltD);~b&JS-NB zQrIjGP=8A^(B_topz)S&pgk-!Cx5r4FX(-iL7?|rhJvP9#(<8qOah%@nFU(sp@HNE zMxI zy~Sp-x7oXR%Vk=#?ip4g>6~0LUN&O3%Xrarm0dR{+Ipyd2+R<78`g?DL`YlNH*S7* z-@_Fe?+PeMokT7Di}%cJ>uTlQlpcm6FJ}C5KpKBYdq|!yNIJh?`d^grg@oe6GP2nm zYbKH8qtu_uI7MYUs?^iUc*m=9y{XirELN^RMOE$fsK+=L=`i2;0_Za1a?qEJ*`T?` zb)X*_H)9p`7UNd%+l-%q?ltZM{la(<^qBESqn7<-JYj@PU_1?dz8lYg(R zgjU)$jD^O`n5)VLAX> zXeu%}*h$lA@MlaHL9d&yGUK{o+_TXofc*xn-AkFX(>nA<(bA$3VaL{s`)O+!QcBrxMs{cy@*yIUe;uR6b)5 z0Uw5=&`$&BaDF~;G3RrE?{OXqK~?l?Zrwv*X2e4A&`e(}9cP{a z>U#`Fn(u+E{xBYHFK{0mg?K##GWes&N2yF|pQ$}w1yPFHWNMG;UE+M&;%;Zr+QC(g z{nnk7dO<0_m2wX+w~Cxqz{fom`B~)UJ1tu-zeS6ymg70*xp<@b=0%`O&C5YwGQSG? zhWSm-M>yBcr+-*@tR=HP$-f&ld z*3YnH{;55bb0#N{rRV;X`xv{N`$sOuv%KlmN`a!yS#$hJkwc4kIw|Rr_P$)tDdWUS z`Y|q_7ThbJ7ffoDXsyWvYBxDR15C|8o15IAtxT;!+nU;eb})4WjWZ>fR4mcd1$<9a z3TSUrU(f-jAtnPGYI*?t2-7(5<4uo)XK-`}N-r%?$9W4dfb;c%ft(Kpw&i>WU?LMwMq#hWC|m zC8eBI$`QPuCUW_rAcxBtynd>zw-ot9kw=vC+vIwyAWy~&{+|gdwOXUmYIQoj-e52q zO(wJ1VzF9nHoM*7@bmM>5rCr}j`}zn;0VMKgd-S72tPvk5yp>jel)ChIIA61b7PHv zk!S(iuxD9kd%i8+zOunceGhS_*nuq-jS)|)%+OMCvWJ^aZ>nN@n*Q3<#m+Xp0DfNN z>yhY1Mg9PKGcqEkiKcN(WDI8Y+N0A21HiF%PWp59bo zuQc_x03S0e1~t4}jT*t=7D3(agqyYli8s4f=!6~>J*t#QIW3#vsaoTSI@x#F;_NGs z|3buuBJ{7sXORn+hz8_BQ$!?k;X_TQA?NLGdIq_$xM>Oaxsm@s{`)rad(az^cOe%x zifM*i*d@l`-XoaY4&=gm?hw#$cjC>v4g^-Tt!T*FS9C@G>sc`bd2v|9aPS6iXK!mZ z%sbMn!N}w2DrtGw5$GSnQTh%74(9wK;9}0_19x!#67VwTn;b>o9*(XZ3LM7yrNCvJ z-v#`X^H+g?a=tnGx-H=7+7ZB!oPP=UGUq7;={y}u{U&v~@V}TBD@Rl5G|PBS4w=+N zo`LneM$5WzoV+4;z4pJ^S^EF8`Cb(<#gC^*y33Atj}+7Yj4AzsXyPX6DaYlZK+m3c zW-g(d?3{Gzxd`B*;ZJlQgPS~B}flH=|yPeO2>v z?5nXT{jDO=7fWf?A9O&gk)WPd3qjYn+5zg$Zkw&-f|{?8uAYkaf4X`G=u_%vL1&=` z(z55&&#P5zp?VQ~UQjOuzf8Rr{M+i2prvXrXp`{f;picSj{+ShNlH7ynKM}sob^#Z z1v$e(MQ3-=6lY)1ROevOAWAhRh)qMVBHWX<-iaiA?%W?(m8}Q$D|G^>c+`ew${t)1qcU#=9$P0czRBSvx>?W$gj|Gs_F= z&Kn9z?}5ArLC58d2W90t$RRFoCogJ7?@-X1hXwN=omm1~CahK)4ebnr43`ZWQ$16V zX}hDXUlE%B1^x^Dm-?^v*91fc#0Inp=oB!t-UIcFSa*>frVlrTcMeYo9~b^`__FYq z!Y4Hv`N+^mM?ZSskyS_DJM!_7ZAU&kqB~muX!y}iM-z{BKRV!O#`pT7yNVhYX-dSB zh>~U{txGzTbSgs@)rIn{utr(?+cphKM3@G|DpaWHq3uG_%wfcPK#!;miv>g`iK6TKtJ-& z2QBd5g^-{6?*_lm{~OS6{i)3R2h;}*3K$AHTq+};fH%NZ0o@mU5VSD*8_;i~kAn(%I(!Rbo(I=x zw$ckH^J?&F&TGJHIIjh-<-88Oj`Mo(dd?fb8#r$SZ{)lQyovK>@H8?(_l4y?IGMMC zr;!Ts$GkP1%)^KTU*}PQH6C6#@^`TqW@U+Nq4sWVL!A?{@e#-}QB2UQ^cc_4w>KoP zj)r)Hh9wvtG&T{&8t-=eqU-G#9T2D_0SgSSWgYp(8;%hs8nOpDC?+j1%KU^bo~fpfNFVF_?vSd*8V^&6a7;L<@%% zl^XWAv@Y^s_94_iEpzteyum)t8J_zJ8zHR#Db2l@E3k8U_f(9(XHdmpwEcqDhBtS5 zM|r#8&E!MYk4w`%j5jOQT?Z}De~vdBXPksu_KfiY=s43P^cK&UE`W|RPcnlyUjUWI z*X8loan?yz_)8=DskR|DTL zqn+wlVAr4tEGy)Ru&;zC!=A;n+`jKB_g5!Hw~Q&MGJet7y~;hBt#;pVkAPN^Jc@Dc zozt4@@7#O`t#jtA%&EMW8<^!pt(Ttld{z*9U~nrTeft&V_Q`ott*cqw|P5^Ft@YCDo|9bNw7;E7QB-+WyB+lT75M}XSrh)}EB zXf&E`KKE?F;+P*sUlUN9QJ;hQ8SBx*IELPZ$gZk&I)_THGa&zW6Ys&u%TwYrsBspF zFMtNQl3dMLva1`~=H9O5uI}tb*DK)Pbgcl*aAmsG?0wfkm&gvez6O89^#}MXF7I6^ zsS&OS+%aMWdJZciHh}*z;w0#n#ycCE*tN#q#?YT`QqXjOu(RoBO)+lX^cT?5re07M zIVTb$81}!XZeJ%Z3O&&Ki8qfV9=EJI;5zk9qAAe1@Z-?$ z5;Nv>`14%P9}*qEwJI7o{-?x3-4_qdf9gbHW~XnMc4twdt?<=`u{%yCe*IKpPxVKq z634qT$~NSkPFyU-_j6*KRj)UhGw+wgFQ4zZls$4L@m_tuZ=Ahb_;KOgoo&U&hI|RwkTJe9h40u|)0fi4(3Q z=Do4+_r$4>jNY*4`I5x@bcYrfb-$1(CqgO5DoG;!d5MBh2&6 zFPc}FSD8OITP=Q;NK1E1FUw%d63dI09Lq+_Zp-JE6P8ko#%i_tS-V?%S?5_lv~IH= zwH~w1wavFJw!LG^wC%R-wWZi!w{N!}uR^sHr)gfxy!Pes<%#88%Da~jtQcQ0z2aAI&6JUSeAle`u8$mtmQGn~ z;=8Vl)^H}mk!(Th)xP7r@7luDdXVDbq%c5bO_=ZcKi{=xv=*i#9EC5!Jn~sSmrQz* z)M}Ey@BBvw)$j>6P+41&xs z7>>f+2T4F#v)&WwlLANn`9NijXEM^GD^HW%z&)I&HGaq8{1Bej_;r9ZI|O`3U}re$ zGsgi*ck40my^(HxcpC1ik3K#e-RpDUG0rD9z#I%5`Ntvs5_lSRfn3%Vj{H{uS8_fX zY1xIR=bz}!DE10`*MeL@TDC_Tl7!U(*bR&5B&K@aVF=+41W{ArMsX2KEA zO3+t_BVG%1a^3~Ji}Q_uP2s5L9|>&5{o4TBbG{?6GaRK!7hq2~8f54N?92W80sF(z zy#@g9;rzY8!EhA+5a1{{if1(NLC%i{J_1MAP5@5f{50SU&OZr!hVwIl9yp3;HgF;L zUkqHq`L)2eIsYy&hx2*BPdHzjK7orxVg4}&BWxD*^Aa)sH&eYpZG^o3sTO{^P@SDs zmD^2vd=dRM{dq|5ZbN$(E7@Qe0qI=_>3yi}ie{M2V1JKU?f%HGcRP9`caGwc{J93t zvhxjoN1kaL*bcenNZ>c1-v_FKnlW{d7Uj+mWCZON+#M2awvU8AoJ;tL$Uk*S_{lMS zV$7^xO#c|9ty|?ryC+#hu~y3b7@eph^Y6`ho$br{JO}+aN#;M1Tbk>~F6N%gD`Tk@ z_n>XOmrMQ@^o4BR5cGmVy-xJCMtL8UMt(=ILiH5&SL(0Tqz_!G4rLeBf2h^$in@X5 z3Tq{{5g~z#qeK@QE$Np%i_vLlPKrxG3&kHnPl~@_yzq>8PSmpV;syAqTzZ$5nOp&o z66(8xT^bhR3J2fF)eL;3D+;ngw5z2{#oVq~__ua-0F84cf_8WH0)5r>2IxxH8ql?_ zw?N-Hgp zCyzFEKSCMf2_$w(A`bet7QaE?>4D@ zS1w>gE>`%9(Kxw>D6fc+N9O*O-XbcfS`QkhxF(HIluBb0MP7|L57ilsO%!-FrUST2 zYs>@Hn2|*=>WMV*YOKppjcTQ{8q0L}>*O%j8Msy}h16Ihz)NEpUahesV$d1KQ*2Ur zy}kwUrvBt_f{j{SwGdPq@2Jp8;q_8@of(S2%pir=8ybU`#zAiAr7#8^285Yj3S-og zVy;07BaM$-Gsy9nV(BXBtlkt!;f(>H(zr>f!ARDR3{n`g9LBnq!dO?3zZ6m$RVgx9 zCc(!jg*VIhwe5tD1+@x7O5x4Y-KCM203Ztd*VuKXkDuvhE`jWq%p2Ut+ z#q?8^6@n9^(9O zK=n~M{)WKzoWBS7DCeI8uHpR0z{8v`0qXfY!(G76oF4?7#Q8+ z3{=)C-A>l*2w9V!jbzS$OcdBa6gXeGa{*+|MO^MgF!|8@4gJ?6SeF1t_iO{~%lU_Z zvpK&KxEhYi$r|9ha8wUu0&_T@3;ZV>#q&OJ6Xy$oXF0DydC}X-&i0sVV zg;CfzBz6Q)vD?!F?RqL4rQKFYvme6|-x~d>HgLpG1s1?jZMz@%CHHTOa?y^Lk+VR> z77on;(p(=NkJUA&*QFkG*5D|8cL6_zqkHYBk9)xppV$C09NY}>TYzNWVH)_3kfJ;B zerypWj5a7U_rjlSj*)$jd+1t}moDhbqIzN7fZgFJ&VK+)A3^^RFLM$22ORmkQ7+o? zGU97LfOH}iUC774D{E=)L@$!o&&JAj z97T?f!~B1d9DlL)l=NIoC*$*Ns^X3Y^8Mbh%E*f3>(d^W{}*ENeHJw1=q)60;w)|sm@ zB=x4QNYE(WbL$JfKc7io41T$*30kY9Mt6gzHIhejr?x=9wZ*HTueXr(RH%nrD%%2d zYtYZtxed?~F>;-i_V9^wCxXg4I2rD8_b}*uE{}bMy%@VD7PUw0X;4v`i=Wl%P%F%e zw)VD$e_wB3WGqQr=Xpt6$DQ3V8#8D*hjP|Jb0^{E=>ds19Ti_Ozlvb=j@`VEOryvT zRXhsntq?GNtn!NJp>>9K541)k8xP7HOOZF3os@L-i@hZ{d)<4pvM*O#j@1j0G2tk? zPX|81`9;9RoIi>0UJgfLL{w&qy{-EpFDdrao&-M&j>7ob`SP_R(*XHFv6V%(R<^@Y z_yS;Uc2|Zwccb~>j)f$@m-JUf$Z}d z9QhZ0joDH-;?oWz{o#lwTWg2lh@X5Or4o+#_kbU8em!s_=Y`(|77a&XS_0!akLAS7 z0!RL}9{9#8{+)egVY2f_@^CTQYLbZ);Y38>tD8&qM@crG41ZcZaFu@t(nkMRWaLkf zzT0>j`^w8nNNZ)RbsG5TaP%HD?z)fjH17HX9ND4x38>gtqw&{xI0|1~cJ9yHd0%@q zd*HVdZGJhzlRWJ^2HO?wKE>l}!_{{@mh9A?f((#_K#6Gkd%#inH-XC7>}v47GPPnG zfb18MTutG9$85=Hy z#__-TL+Ts+Z~mw~f2fU}jU8BmF$wiUFJm9je#ZWwbEI`u`9`YSl{Hgef5a{&Uk8Saw}dnW~|rPftqD! zl~o&G!snP-ury~Xi`oJ`D{0;39Lrqr`IZ8#!rLjW(c8<{=zRfyd5xY*vV4(XO|qiI za%&fTzz??0v9@4yu_m++)`WfqpKqnrpqH$d!6(=g(K5`kw{ZNEMLA*}TGqkQ*`a1} zjzNwPHrO%RVP<0-527|3?|2M!u45kP0>=xW%N%dw>L^w-dM_FL$A@Fk&_LN)AiXrr)L);O#MYMwS>Z9z{$ z)@s1lc|~M5%7z@3?a3a(W@m5AZp}7je+r*_B%9H5b2jG0vQ0T3=V;j{Ir%wiwj-w? z2O2)P?p!=`ZX3|PxliZDuxE46u-J1 z+MEe?HNHrIy*?}`Hn0bR53xHz+t@q6C*Qss{9bO&<`DR!k}SGfvOBz7(kpwcK~nHx z)p5!0>Zs61kV?~JJLfLJh2EP>2WoH`LF>61VC+87)exihPFDnc8bjJtu^3k;7v^_d zce}8|qDz)J7r8pK#jaOfCiYsLvjsFy@Ifte1DD~?BJ>exyCa;ScSSS`1QaK;i%2q0{n>cQIH|J!;w7;vSmC6 zE)M(#;3hZ%A?_^?@9W~Z6ULK4Za<)J(p>cfXSTi4qONKD8j4i-oT%)frsJ9|3@I%1ty;C5_Y6I5&U4_{cz;}4`55w zWv}B;0+4J#O79Nb2uI_3i!k=0*hL^)2(Q4=wXXrMU`%r){#*qPn1r4c);`_?RBSqq z{R%pTa1`Em90q^uJgTn$*n(&H2s(e)g!QU^YLp1|V9?>}(V)}R(?Oqt_BHja=V~h0 zJk0`(eh$&TsO`XB(yoO>{euO^X({Os1{s1O)t@$0fW{g78vWP+X{Fa><2GY&cEA{Cn#B5=wt*fn-88vb zg|s3#4(mz%*a352iwA4o4_N$IXKNfdKW2%RJwywkZo-E|?cY<6 zY1Nvn9r;=KA^Psp3bvcl3byUe9ZnT1xb4cD3sDzQH@1vv?k*QvNGqe?kSxRBa8Jsd zI{lfOdY@2*Z)w_sD$kE=CUz3`k`&z`TcZsA3YQoY1iFXW+&9a4V_4UF&ACGh`4{U_$} zuB*%7e^cEOGQnatVHFUYpfQAwHop6rr20rrnPnQwyLl@*hPypc5#^vSGeL1$(?2f8?G3Fz{ymq1_3$_35K zdLP~0by*uhH)rL;XIIu|pu4kZMTETOfwG~zGGh}v!J4xoz9z$qTqp=F@r_#x?Lj*U zDWJWD8K6%IGeKuzwH{<=VLtfhg%^--UJ_mv)Mypng#T(`E%>*EcX2jbcu&y6$o@uz z*(_{@|8`*q{CC5y8d4B;)nJnVcGY0ZQ1}u4KM5B=FTutd%Dr$MSCtD+;vYtT4V zqDsw@R9(RLRP_f&Axsi2>t8Vi1$>LJjFRTFS_l4>&eDXQs+ z;aRLORI@p%x$s}0dI9t$*rqbG)vE65EH)WyZd7cB`f23XXQVYZ|5Sf~{I_0x5VR25 zE~M*n4D>tf&ViMI>dWA-sLPR4-I|feqoXvlkuMi(c7X2E90WZC3zX;|XrDv-Alm`F zDcJ#(cm9wqQ$88~Y`BJ6zlKwpy5XzHqbnM&ZKz{!W9&{{lg?k98rIJ>&()6a#4{>tY?OwL zi<*GkHZ|&T7d{_`6-LK@+%E)?8Nt1WDQh>VIM_=+huqXOC3X>!A5) zk}qjan&zX4e+zgg^U`;s`|+>LQIk%{zdJ`wIwXqjhwnUdbv+Pe&bhiCh%*0NT@OT= zi>|H*qRdNI*8?H_4@$#>Kt*5pfAv`9G^tCE)i;Z}SOn6qF>4Ci96e_h){A#S4^-A= zO=1tQFh;Ayb!;=+f-pOv-?~G!Ks{7@SQw@qiPmVLwm{pSeX8Aqy)`aN7E+Bm6Z$3= zofR}x*HEWsPTeEA-t1A`WAIVduhG6Hf9kJ;%Da#$8nEd`GrMVg-1H>o9%fh;2=gp| zSks06wxKpN8)nnkbC_1rjIC#%Z=cKxeJmK??dXsGMY$s_V65u)8nfR9evcjplQd@a zK_>J`WsO;RKf8>OO!PRir5!wmhNhvfA@6(FIqa79th}??;jji`S9PE$ibm!V<#qBM zM9duV)$(1z_Y?5(r#So(JW>;M0Q<_mg!>*G3for*kHNQ z9@7APP)smrXiP)+IAfYaJGTWEOCiN#I)cWShzv?rUgn$J*QJq(a3oujzdabDC-|#sgsA6WTLa38-Rr z=mVR%KCtMG^M-M)U|*~DO8&$+AT+IITLzuMCo*|no9s7@#LRXWY*?&ijgV9Gpzk5) zRhr{g^s$HXyt)WEHXk__^2C2bj_qV`0!th5=p)a!7utKXuWp-rS-pu3CaP9QyHTF2 zq4llg;lGo69b&v%vF|{6wL8zNWZ%J8)7l{EF3Wq?JOpbXf2D0)TMn1=t*l!vTZr&O zUacot330pQG14f{v-|3t|L5{;!p(uT&b(7D9aHg8g_b=c&A$sgmzt1EEnYilh&L3} z$!Fx_!6$l$A+HXHy$DFmf3X8Y-Tfo#8Fi3)M!jy^Gg>0NgdWhV!W-zzyeX^@s`rm_ z5lqK|kUjI!SHgG<`bX+2{i9O!kDz1E`$yC>YNP6iUQBKMqnF|Ts_FyK zn*EyP>VK-k*n4--ueqUiYud2b+xBkKq+Oco>fNyL78uJ>*7Uzg<9bpb=e35b8bX%3 zgFcRIpTM_|Q_~9fI&QIGi{%_Yo@m-U5r}`-W%DO#Y zot3}4hELIVp*4JpzKieP26e6B^VNf4&}mR?`Pa3!?_brG@s;iCS_|l_Pvg5T@V`{I z1_hxJYk}1kankx`c`iU+<@^?AJ|RO&8h8K1{3k|CvF;i36q1c7S?})7EJa;aPiD%@ zDeYXRlU7TcbvDS=HD^(Mt-qET%P}K=(UfkU#V$)W4`*5WTMOBf)QuSz)%3hXnk0J~l`P2Rg zwC}^<0r*phkyJSH#|>B$KA#r}e30{x04H#ME^r>_UjXKC{(WFE=h@fjWx+}J0 zvLA=|fi2)D9yjo1&SwDM=lpu$4$kic9^(90z$=`0T*UjrQ9QJ7*FQOb2B^9u`#XT0 zIiCbf;e0A^0O!+yV>!<*W9|ly;>3gwJ9<^-i-6}jUkWVaJf?ftR5%Ln0e;K*BH%gB zvuo)6!ciCpuqWqJfkQZ-23)}T<-qqi&#vQIIJ(vfoXq*Dz{ffN6!0z1zYEOe{QJQD zoId~@SSH7l2BiIoD9&`?RL(yEe2ViP;2h2`2foUATp}HhL&j0|c^8nc>$0V4$2q!B zBh0!g>n0T42J-LVK<^BW`09ENif#k*!}o-vFinB2I8XWxr0YQb9w6yF5KlS}r0YOD z={dB4BfdQ_0gl$fod6bde_vZ}q}x!JK0_azqj+wo&!FfqbghT?f}`-H%Ro8`#FL(a z@4mUDmq7kM0+l^R%jhGK-UIRdC&~H`q#xp||3GU}UV@`At5Kh_ zBIu(;pdJG1;K<(q4C4Hq?4i^1Eb97;SVx7-6btMHNAaWrALcyONeelTQ&o=tFWQ&? zscp*6Z~pzg4`|=ZztbMkKF;g#p{QM=J(Ve+QQJf|5O%?z_En&E3PV}{1Mt4ZWk?*!N7u9FoQ6-nzjUw;!S#37W3^MmLTflWaAg)cp*fUv0L z$gi?X!j}khO4=Wx%yffVV96SPLKRE%Nkp?>2NAr2IV;X$sRMF{L1t0&w0JOKQ0$YRL6F3lbO5jw`&5{=M{GjL1 z0_OyY!84eP>q3(jbYMs*TF(2V-PA{gOasqCHJEv68`>4r6FLWUZDvt;_u|SH(8uZp7Iw zxgUY%Ll-)lov)$`{aYUTs^tyK@h){9B5b0qg|5g`nDw8h9;6+jrCsXxYH7zo+HX(? z3o;FLR=oq7etq@p^#%Ge{Y`ze!EG337-4wCkOAxHhYc?nX>XI4jkKT1R%rTtW~7or zyO!iBJEs)T>@y2fB@_S5Qnlt_W#NH{{Z}kn8@n?jqX zu=heCe#h{my%!jEP>RQ%N%vu|B-$y7EQQR#emkUjNM+zSO28THr^B#2CGAZ~J5$cV z4o`1dX;-H`*8TV$g3dnK5RrCoLLN`F4YSS0ZfXVCKaDIu(%xwdyPi(Q-loh!`)3|- z6gv9)r^0S&n*S32<*-xw5iFDL^RFL3mPpACDOn*UoiEak zF8K4{!r<>QRuUT05ZaBiU~O`F$QvOUm}Mj@{4BJ0XezYQJ)xIFZ-llF>kO-teX-vx zX`gKm+Z$FGhINwR^_`?|MmBdz%PbXoIv$Kje*jmUO??acURc92UU-LexeMQEk(q>fMTT)?vX=X2R*P&} z^O%HLMo;$c?1QkCS(H5xbBoX9cwj5@cuu?Aq};x_WGnLl=sy0Cdo`CVxRU)=vfe6U z^f?_GkYq)Z_MM?!XK2qE+9{Uyi6xu6G#(v}5sky;i8smqKUx1L+y4w}Dn-oVq?7G` z>`+7d)6niTv^NdyOjCpvlQfP|r7&XrL0KQ?JC^M$clwTB(Ab4CW^od8BFef+T6;zN zrchb;-TUfx;}PS&lEc$@gt9-@os38Lt|_Fk2xSZ+4ZO0xkRAE|hu%qT`X4mr>$`5U za;;?Pe`Y+zcg>{({g%4MR=hYz`2k9=e`}oOf92Pz@+++dz2iOPr2S9kY8FDP^2NW9 za;wX?Cw%1F-*mr2%JtK`^-dOBMeF`DB;K3)28Li3DCyo;m3X5ek$&F>|Na#V`YP_zVh&XYfpBl#y*cV<>8*TevpCJ+crW%-XzJx`=otWYp!dU z?6a=HS5Dr86@p{2Lh#>_kcUgF9{eF$keuA6nw-3muZA!O#V`vb4Cso~A~SccCNrm1 zSusJo`pEJ#t((B8n6z#}k)la{J_9*}5S#4_c}7KcoIop(f~Lj6)5PY)k>)*Ej%s7~WH*7TIR(%yvrk#{twC z_0da~>kJi2d7LTPq-7-iYhit%w+X9n`3_*db;cZo+-BN|FuQo2L9;RkOs7nU%~WOz zWaZMzApt!C=n9&XQD=0K>I~`^7@1N}6qpmRu83J$GmRy|(upNnl2A9)+&5LJ6}DNh zV%W0BqGw-9`)w+9fPk_t*8#~=9ncr$zCQZF`>o^I*Sy5X+J><}eAD1&psj*C2J4x;QYtVc2xUDqWKc+Ftdko7J}qQ4))0>k znE-xjm0E$;QL#`p%6*&Ac34l`KC~0~xX>=4y2=`2HhkU+eHS!a>TTyqz3nrh#Rz{k z^mnW+z999yqrzIF=8)H5DfP!+?;lpxVkhk)R$Gm7YhSTRnbR`0YdtSrx*#62cXf08rD2MO0_)l?u4peM+(VP;^C6PbPC(&HeTJW@PRru?<#z-bt|&R(;-y=9?%UnsXw% zVZ_s16WI?Vp5~eq`(f4Rn#i6Qg`st>waqoPMmeo*u8C}q(R>r_^>h!=cixF?l2M%1 z=bcDnnf(9mypv+vjOLtZjcr%NxfrOdwXHtyL^jaqzP|HLWD~76TWEJOAGK;5#`EAP zPFkn?@7hc2@-^}m9L2-FM-2u?d}rVU&QAxjsKJ`8-SZR?`!w3dR^0k*B#M7 zrQWGt*KEeQwuo~JFrSC_t!rAMPKkx1`_eo%*$kxl&FXX6r{Pa?Sw91P>mY&GJHB;L z68xx6X@c-|)hQiNcO=5meaYTnH_lT%Lv>6L{5JqU;{G24cfirQ_yQo=C8X!1`sYK; zZC{}9K6BsOz;B16_N4N?^dEQ~C)IT!STTz5zVqo+$8~_C zczoy6tJiUZc$gtT-}>$~?oay|D|Me+l1St@JAjXeqxh)~r220l_?LlM+&>q%j`JS^ zH*kIva5LxcbY9-Kex$mv7w+{U@KrdtZF;nBmel-4s@F$EXK}(Gn zL2nqfCL=6w-|6noL-_v9Q%v>PRMRxbiFdMd{C3Rs1h690Nrb-+1Kbv-yMz76<1zQ+ zgwEPX^!(DyqcHk28GCj6|C@Vt7n-$}woGTSpm%7qI6(a^%|M%Dt_Z1Wx!)4XhG4!3 zDQ_7C>aonXU>8Hn3*eVpmV>@z$p+1}YyjPa*(B7Pm`QT6BFjne7crlNTGwiUylk^N zK>e+qK;y0btbXiX>mcwWt)oDn!fX@L4f9RdX~?=Bbc6L1P?~#Evz?fKLakzRWBjO< ztu<(ptvl#!+Z@n&w&!fHt6*CM{sr6HpzqqU;FE1z2mV9b22h%#LOiz5z|$-h@|8Ww zp2U*vJ<%WSg*hu&46wfd${fwndL}wj9kAlzxDWhL$8gY5j;`XwW;FN>^djcW@Z^COv>CQQKTt6+ z7Bnev0BBm^1kj0r>7bs#4WK^;ib36&E2s!+pfsnZW9u=mhB$*8 zqD@J~oZA0k?@Pd=ERw%pAjc$JK~WG?21P|g2&X6>;RfZ11VlMxhGdeABs0TILclY4 zq9}N+w=!M`c%SRJ##>icU5^#7bwpjS-PKiBQC#_c)m1e!>AW+^Zod8hpYQpc=Xs~P ztGoKFuCA``eqWqIz^)o%VqXms96M_W?XxRp0&kx=_|NS-0_)dF*iplI%f4~^-_iF@ z{6F0H6Z|{7?+w{LWA|D3pTBzp{y%T_PSWpz`Dyo2{m15v?oZ!^pxrU#^&fqkFt7N~ z;?d4H?4d!6EWRG^7`>tRC%9=pEzijtHgp)YmSJo0AIJV0M*XlI_;*I$KLh_Wzgmy~ zc+YqTXIJdD!A^J65BPU(?hKn+(T0BbAGF~B{D(HgHw<^K-|z|C>#ze??QFlL11xKu zHs<0#e`6v3yKLMG|0Nq^_&AY#5O{njt$@uqS zzYhGo=^p$iHhql$Pd9yq|DQJff`4c8z|BZ)^LYGE+SnyD zWAmMG-@W;MxF6X3(B@9g!^B4I420MV=osQTA%={h4rdzin zly(Hs%!@heG7ozBTM&o$i70~l6n=-XL!EG^CKQw^UxXGQasD?kR65IZA#zZ zqIb?ZSdWm6q!ACj&y>EUO6wE7(?fO=`gXE@H<`YfO!ko}c*JPu@eA3SOUDTbTQ@H>#)^#0U19=cPsL!LNf_APietpQ5$ z(EC)I@NC25Kq}KaRrNkn1nzZsNUtYd-GP2h=VuSWLpBP%tK@_G3OxGFu>^E)`nCbx zS)y~F^d8nL_Xhm{vAhp5?mYAA-f5kAEvXzgYY;X;$Al%vWXmqNAGvdgq!X~G;PKB!9(X! z>2W^m;5?1zIXr*DL*FPk3Qs8>`WBqN>o$1?;BQD554{s&0G|Eu(6`CxyJYlC!K2?C ztAM)-4}BAiz6VC%{nBrK(OFlzi=p>0^bQ8yzo2^-dL}^614RD8_bmPbNOvrX@X-AV zy<1TVH{GkyI~8=Fg7*3K{)FD0(0dblXF~5w=v@iDC!u#F^nQfiji7rGbpJu`KIpv% zz4M^=9eh9WT?f7Apm!V^fv5WodVUnEc;`=edyj+Lq$7XZ(J8^R$AcXmFP`gP#vNol zi7yep4fvk`#deipa>SR1_(yjQ=qH~WNJ6rc&@U6DwKe_P3&}1*zyE2d_0sa&`J)BR zFB8=GnZjGc+xfN@-wtnY{@UMmJJ{vYd@@11Ty6E+`DCKQ2sb|)YI&5-4$p+wcy_%t zJ%vi23EJ^AU4=@gejEO8*WVuZ+8zqEe3_uu|38JAuF`9JwH5xSbeg_hPfbsu#>)il zcy|9PosCz2TkEg!ZP?!Ye;xlUSL}YU+g1C~hITx=e>A?0SHGRF#*uv!)+?M}mH|w@1Gs2If0bz0w)QaEO3gzsRC)g zg?>jeaIm#ve)trI`OOeGQ{XItcm=fi(baMK(Rl#+(Owb#=$l{kqjyo$?-&NoJb}jw zJWk+zfeQpK6u3y>@d6hM^a=C}Tq3YsV1>Ydz)FEt0;>fE1)d;qslXb6wF2p#-Si7H za3TVi35*Ji39J)XFL1d)`d%9SRxof@3OrHZNdiw6c#6PN1)e6bQQ#_prwcqo;F$u? z5_q=2a|Esyc&@_^80g1pZFo;{uxmJ|XZ)flmp1THrGRpB4C= zz~=?NAn^ABUljO~z&{9lS>P)IUlsUAfv*XCUEmu6-xT;Kfo}=iBJgd2?+AQX;Clk! z7x-s^9|%kc{7~RlfgcI{Sl}lD|03{Hfu9NdT;LZ1zZCdafqxVDcY$9C{952Y1b!p% zTY=vR{HMV01^ys#o4_9h{v>d_z@G*FOW-d8cL;=m0CP3()6@5R_Z9a*z(d4+DBxk@ zJ_3;J=^a3?Hg$5VUER1lvdx^_YBwi0+06UlAvf90OT;}MaGJQuj!y6CH|YRgEABWT z+0_Y8w)I!UU7Ex8b(22q=44AJx7yPUUTo>)R(m?R)uwLZc-XE^ZnCWx;^_!?XTVW- z2!9YD+1$xZc6YM5cY?bFaIWBefXl_*2uL=0ik|@dM%>>4l5L*wP66J)i-+7^K(f)3 zTkZ4Y{vB|S3*IS&EsGxDdjRf?hvMu4*h}2I0g?@$@MNz)S=^0)*NB_!_s@y@1;CHR zt#*8(E$Pg*e3K5`WX~seBOuxI$*p#LBw$3yWC z0i?Z&UEy8`c&^~D1*E--y@7uMkoGJnKJ8n0@Q}L@aEQ1|0Hb(@10MsVy$ll{dlh+j z$n60v5O-(5VsTFfqyZQeFx*lzCb5Dz)Ore;jarJe1B7K-$M3H|=GdfQM*n z0Hflr18flYO2CuFeJWrh9->_Zc&^~r0Mecar9=B7H;Oy+`=xo0fa2>u0l9UrfZV!Y zKyKYLF!5#Iz_<~%y5sJi_?zo~I&}LE^8?EKmOp;@4X1s0$HVh%{B3iKz1w$P6SeSP zjCy&|k>`$o<$Mc&;Ml<2&37%m>J|(C!K6Nib$X@GU!Jh=zkl|s=X(7Tx%E8@KkL0m zfA{m<4LiQK@Lm42blg`L9rL>$KQ}M`p+BE?;qCoiy z+`jI%W!L&G{EKV;zIVf3*L-o3g|97JQF76=m+rXC!q0p8fki8KI`s6rE&M^pY@adu z(xN9{wD7a8m^vo>#bp=%#lk;#$m#pMcvjt8_&`^)e);b5$1nfGD`&_1TKG?H8`SHH z!jWH%vhZu~88v8o)tGxqE&SLco_+0uH=aLpsfB;|53l#xZ{qvUoMquJdtskPUS7ZS zlDLIG^X4U|45+#7-G?pw_wOw3dTiybH@<1%_x;CHf4*ef)^ESE@DDcb*u3%f`|mIO zrFnhV9C>n0?&2lq>|^1}ik}$i+wtfNM_Bl|`#yZp`De|%Y`%qm?9}}u0~&t(pw7at zd+?J7*FW~bhIJPH(o^bIzp{AKwoMlPm8&-KBo@U{@{oXm_ zoBPlCSGk4%vQLkTD?Fb(da8wgX?1D1;H5{;ztX}F4(zt|*j~T=d?4nAVlp6N)q@7*Q;g_B;sylISuk1YJ+q?d=E zx~|N^S2uos>rG=`d$ZQUPulCMUimNMEYQ!cxOKg ze`Lc~S6z8y?G58C{8>v+x$mSE1>Y>R@D07+UwQqsP<*eW}vIe^A!{ zq;Kc%dc`UWe@Jn^%ip^{zV&Jgf7!hsEiC)v+Z!LS@P|&k<)*h!zIyws7QSfWO}9*+ zzWfo|LQntoxu)cSuh!nW1}}YW_8WZWybVQP^?TK8;rAbQ(dVDsRCM)N3x92wYp&mZ zb`JpY<&dyRPG9Sgsy???At{O)eo{nNs~-SEM~p~o-#vU^Um>N-pR{`J*kJDzjr zAPe6-R?5fU2l6{7LVMZl+z);JVW$bF3!ZTP1k4usV$~BJaSOBKAMu~)_%Z(f+VSsr zCwin)9aj0vJ2l|{MDradx37;~kI)-C{S*H`bXo;#zIbu zT$@j-FZ=bRI^JDGrv$IU|23Vj!09r@y|Q8=%K_eSBa!S0Xa-|c+6vj-%`9*@H^ z;Ow~z&Oq{a4C&2MuoK|jQm}I1i*S%&{T7?=!kq$lqkJ2IzFpY-+X(a>L;W_wyKpDW z*9`UB2tNa}%zK)*n{y%FN`>`h|NQ>BP9eUNkooIh^!)@_dgSY0)8L-j|BU{loHOw~ zMA#F}m%xe#A3qqDkij40|4V#v4i*D^Ede!3`&t5@-VYzxcwi4_)qiw`f9R0h;y-oB zD;`#SfHS;!R58}3s&4Vo$BZF{QtTMrMwW_>Y@Y{k?AKcMEI_x2(qh8oX%>r_k}PEzCIoSI_ysvMI5thx4I1?f;in zPWvz4+}3mc?<1z3^ZyQRdfQj#bN$o=cS2iwX1C-*({4giAb+1fL?1pW!2y%dn{ zk5C-CL-L`x^)AUC!2hr9tGx05$2%yS{>1J7zxC}?y+cDha1ab-bdQGIZQ9HF9DIa_ z;?q4Ex+il#Y%3iAf8FDPa*lS`gooPaRzSKFL~gwYW888VhTQ+|9#`U+mn%$i8UDNr zUo+wH`3x^Sj$vd0!xfMI$%J1RW&dh{jRIE)j0p4zEEPCaV2MDlz#@SjfsVj!%Q&B} z1%4)Qi@*m3-X(COz$*ks1Xc_52|QL{slcfMO9U1R94OEu&=L4`gv<4rz=Xgp0$&xl z`o}+;_A1)U(0S^0Zr`gI`ZhE4E)&1NxWI%!Z&bns#swx6ALH=2z=T4{$6F`#0^KEu;&Uo*IlFkN!f{zPKsDG2hZ;*Hby;@H3CltRz;$JQC6$(BfFfMrS zN{J^hE-)d`d!mF3ObA_E{Q|uwNj!lGf!>n^FEB1JA<%n@gbR!dObGOz%HiH?B%b)= z;!gGOSr(ez=Yzp-U1T>y=#R|U|e8A@sfXBU_#5YUg!iS1bX9w7Z}%g(tdG;5}r^f z_{0rNpSX!(e1rG}`b4h81$u9h`~-Th7dr7LR1PU#U_zkxBB2+U5W2Yf1$sAfJg=0` zEB)&em=Nf_MAB0zba8E}6y{XN<1+_ahf!^A(Wg8jY3|8WERcNhOYx3hnQ_>bS8{dwZ|NqL=+w43<7 zmcQ8Y7g_$VYo$FOdV|~JOV%%Z>)4<8oc-Pl+232*%P0Qb#UB@cqxd)W=Jup>Vf54X zjA;Ab#`*T;;N-m{@j4}UaQIGwP^jU)%ebEg2|}TUH{H$jBL$&Q!{fqFIf78A;U#N1 zeeF+$8t!f4@G}LWP{R`saQJ*dDAaJLGnemSR>~cP8lD)$;ZI7qLJfEB=kUV>p)eWV zio^3D^HZj1O&hrU-6VoS=QhTB1h3HA_Q!>I9#?kyacNiGHdkoJ%S69I_(z&W)-)}s zoy6^2P93ib8=qo4ok69aLiVNpedlvLzD)dUXOzjGq5tAnaJ#C!P-rLPJ&WTh ze=D@(W%OsrKhehVQ`gS+f3ThHe{wtX_swScRwWFqFfpI;dX7w?&WqZf&$qMunfQN8 zJI7CB8^=$k{Nrt;Z<2V*p9-~oT7xC+PGAMhY9dasR1bq1}JJcIKa{{Ss{) zzZvPT=6dOTpwMp5xacSO5}?qIm(ibL{D0O){!4tvnTh7NT2`g-oPX>hd zN~Tx+OksxTEzkp{oZ3DLeeG=jF>S1Wrv6LZ%k@%yRH0pg#KRnKpadvPT+euwHwtZf z-~Akq_Mzyf(7TTDI-e`#SgC*SEgVnFr!W%#d@=$B$ll2$fyQH7Ogz$yVhYC4X+J6TA_qDP7nd;xzM*7D8 zlKv5IYyLMe|ImG0`YB{z>;H)i`bSe6^FKBN|Fxd8#K_Qpagj?pA1SocO-MOaFHva6 z%jnOLzjH3PtIlT%?R31Cb3Elwg?7A*{tWqh+n9giI?kW&@zPHr`&$3^iv3CFErppV zZ9O*LPX2HFFWFBL?UjEU+t#n=w{!j|`GWIVDh;mCd71G81h24#9+826s@thQm9&xn zGRe;s?aY658|i)R)L*aoFUc=oJKN8DA@{%bx55_VGDH8z+sMC}`fpk0Qr#S|v`Fn*!n6}GkhjqPlIUpw3X!8W#EruL7wGyixS>z|N(bbX`n`F57ydr|xN zH_^uRW1^k>m#O`m+Sq=X=zVRhfBao;C!a93!jc5zbze`R&Ho80r|LZl?RXjendC3=2dtiR_B=09DpDzy10Q~URBC;h;-(r@B&s@+Ioi*jZ{WEelbcD7$*8|mY_ z@;rZ^luuzvKgI_IudqcmGL+wYEw_uVR}{9DeoR~2KSTag+ekn4%r?q7DYz{MZ~8CE zpLiSlFBAWJFXnow-m1`U|6`?{v|bABc$xgw?WAAPPWsjDq|elTP3>&|E2La1w-nm# z*!Udx(|8F`*z^PA4;Q?`7V~HZ{!g^C{Ju8wPst5jPI^xr{S%zul-)ASXl<&f2RTRY{4SL&&Jp)hNvX@Q@$v;5B4+|DXr6sBd7 z-6{J&`$#N>B~Ng=dTvjlJ&v}uv;0l%)PFqx=KRs3F*5L9NgMgsyP4amyCkg8*GBqG z<1gOE@gL7H{#(>D1LD4R@_+Akw!g2P_G52*`9DMdH@3I`+DISIQ2#_5*Kd!uv;Q*j zUsD^)pNT%vM*4y6{Lp2I-eiNDG$8B7%0;zyPyFM?qGk@Q| z*?xBZD>Ag7??EoVp2tvV(>LbxI{6|AP?&HSKT+@sZF=Vd&L>v_6nf=cf{tf}HoZ^Q z*?R6nVZ4p&Z{NQuf9(7l+ez>JhU>pf5?1JYi}8a6uh8CK^$DN#ka!C1a;o1RKhCq9 z&k+(qVdAfhKS=NjH64YkGw@Gb_)^EaLZwsayPNarEeM4gZbR)~#d}5mDt{={a2wj| zlX!;yyP}QqJCT9^?0nU4=iihee~sTN%%DGQY-9Po4{`eskc<@io^wK!vrf+(hr~64{891=xAO^-fkNj-#_N2n&~E=kJI{|bed?aSFGK$J z_%2z`^ePt>YJ0T?w`AzQ*61|+vv$(I*UtX?xSjOd+FAa1hW2mD(0|%)3LEd_{!{yg zLJhZ}Ek8=yIsOyv)Snu^;_`>3pb8t`X1vbZ3RS)&>m_n>H$f;&hFcE1{!Kq|KBq`L zh29+2zorOYp~`1Xw&Z4}SNW{4MCMhMH#SuIrn{J4?}sY%$iBFiS78hJ@fgz|APFk; z-o$v7I|^+%oOqw(`GsC#)4v!$UhoRF9$J3yrSAD?xDB-)ivMdn$KS#X^xkv0Tv}d* znywD%ZDL@t?Ya z!}pbhyKH3t5b1T2L zeiGmIms{cE&gJ-CiEsP+Tj3on|A2j&UgyPo4rIT|nOO(0U-gP(EdNr=FU3fLo>uzF z0C7BO^~bKooS*jRI4k|H_i0|<9^zO27-;2p@qQd%$NlA&{~F7GtVRE+6&~N8(^Gk} z!Sdf``4?H`-_!E%WBKp3+QVz{bHFN3kyT!=<^SADub0(cdt3g;t@KZ~>a*JN|NRJV zk4*daM;_ihJmKN{(&g5;{7mp$2XlPozmElftd;Ly1^=v-&ewwX9L(u-5kC7)@CQs{ zyxJqS3H~~(oIeYGgw;Pz0hd2w^-GT67h3sx1YctHOBcbvW{tNZ!5?CcgPwvvVl!&cC+6k%$v0vpwjwR0?oXg=o zC7~^2*suL^!F2X(`Fs|Aj@1s!Ek5XSB-3ko&OVI&%8xzAvS0O*lP0iV`~M}YJe#fY za-qc+w_EM@fYt5~TjS?^YkYj<w<$2Qb7h3+Ot@IOCc~@KE z6Rh#}d#gRdR{SlNf4W6~y%k<<<$udmuCI>YQ?2+3YdpSa$?tNDex4;y-muDlg_ZyA z*7$tc%FiA@>Q_DX%DuV#>R0%b;8pKd{Hg&QU-cCYKQjZq+TwqW|BdjG>NT3)4;k=v z136#STQq*2Ex)CLT{GZclku+evyRi(hH(0-w<&(7VT@OOT+6$A2K?3$9AD=*jeqh% zjMsTX>#;fmK5+=gSGlk82Uzoi&RfGX;P)%xbabB7_y<^WS@GT}9AEia+i6$^{EH&* zb^g=%LuPV1dY@3+bzBDg>N1Y6^Q^|-?^wp`dttO64$XjHwUFcM{G{<``x&q6bH#73 zWW3tlwcmEGa>xJe1jehKLgUwj8L#iD(SANV1OC}4$Jh5NY5Wz-8L#_@+W(hkz!#s$ z@%7%A#@})(6ZT9(`v7Gt^QeW)#uI4oZm3%-*;=-ul2dvYTu)*^q#WP z`|v8J*YWYZRlZBD{(aQqzrs4E*Z$dP#s8C)-WfrTul@0{72es>H;=x8R(<|4pYzxGd{?VJpC8}6{NG#UIp1oJz6+YC_YbT7lLH*C^Ztugf4*bMv%RhG z)mC}yW&Y9j{+mUAk;UKRL?6-mb`<{7dAZIi-^q>XK zr~1w!E5Ac5c|XIFKZC6P&bQY4-&y7T$m*XxtoYk4{y$(cmrv*YYp-Yj0WuyZS^c}| zCJsMP!r!#yv)7W3`&j%n-I5m_t^T=s2B)v``Zz0oVlId4{53`N6xD04vfArL(HB*I z^8GvuPyLv3;ci90#obxoTXNKhy2uUn-6?-9RI=?L!Us6|Eh$m zfA|d_9C)P~bAbHABJXRJZ|vBi;r{~=AJiLnsj{{X-*6mXUy1(-e$=ef#F`rXPl`tIKP5n+(?axr zRA43krw7m&oo2-FiGWTs0}c3}RZpR%el%C7QsbUIY&6`nM-0b*nMq-e9~pL9T!n7y zv^YZl{tA?%lb?#-X^EfQOJZpHPD|?1g`LX%C^Y^FQ&EEs>{Jn>(gvt;It8L=F)BIz z*P~fGRh6U8ovH%H9U%PD0J^nPjltB;DucV$kIwE?8>1XU0rMXws_>E%=)aQk4c8NG z#FQsePPs>@XFElx^EySSLpnw3DEEkoxvY+IiTWv>sL3~K;zX(WJH-Oa%s-_W3sR1; z;ED7fL2uw6-GToR^dCh}b&AzdUa>m#J^m?|SUniLQ=MtOx*94^-AXFMazC|61MyX- z2Ffuf7N))jTp2=APEK907ULmjP>}jAXIw=Ex-(~dunt_DGd>(f7w1gyQ%~ni^w;?T zCe;LL0VW4)0tST8pE;9j{OIVM$@QUffN7y1C3aNMgiQ~`D6#3m5D`od69WLycWQHH z`fCX}Gq8dPO8s@!0J8%o%IrY-a`?&u0g73ME+r5=kqFA_{6tXJK%AIUhPnXESs9_Q zxu^7{T8ZZQPespL~rRl|-%6e0>sxb9qPF0vjc}`W7xH6~OAA=0bsYaVIpo#_20R)&F zEKc=XitYfY@zV&&sR>jXfLj^)uugbxm{|_ILN6DM~NJR zg>nd?HwlziLB`~S0wg^EOw)zT@W}}=dB_ZcoRHz+oNx`v#+-=XP%LMhD>gT>YOnu&JK?i4z!l zBMhBof>#(7e3)TVKwu@q_*|aH<6cQup!Wq1FKPUv$uH3)@NtIm>t8kalG_;iu45Rl zV>;h*hKb`DHdZtAs(&f_o2mrgz_4T`!$zrB+%Mr$Zr_RQcO<>0CG2nV30~qiN%~Gu z=q0=)#QwOzMs1hHf|qtq2wh_Z;~OR1C;53-Fy1HmC1T>Qk#qvDaJyA7Y%1q=a{|(S zl?+R&82TzCyjt)Ao0bXQ$sM!=!q)+W%oszk-bm=&aW%mz%&fWNSAx|5CsbDiA+iB6 zG7W?{B3KiSK^o>x3e`n1ALnA84pY3z!9WegBZ*D)lWW2xN^>XEga9}#R1sJ~Fg7y? zsh>NuzLwOQ+)^4sfMqqoa!R*sWvr6?$M|dN$v-bxLBczC9t0)^N$z5r3kYK3H4w6h z@O~=4fe#ke@|vE~eNy)Naw~W;V$s5ezwNAeAo~h><>&8x2-f8z}t_ zsciZyDtrU+Z1Rht+o``vBvU#BDV(Soy)1dB&DgGF{ozjuQBz8*rwSg z7mySpf`)Hn4FSVvbv#Swl72+(Ma7_Wma|>~SVOZh!Fo!!LF(7A1UfSLp+->u5gblX z#svgD)hiF&6OJOmyg>^V8MPp9kUJZn-uadA(bp|vcEQ1gCUlf%(k8uocpCoKo3lfxgXFyzx> z^DzklGd&z$S`YOqZ$_Z5nq*5JWPwrQ^JZ06qSt^aHPwQHbhdGXL1E1DfE?3I%5a#) zK%NmDBPdQ?G+04JB}WK~W8T~lb-QsiL`kR1Gjaqoao*yJKqd7+-eUA*5UiO8JuO^5 zlN^xF5Jg70D<{>Y37@eLlwKU-K{B`JYjKDl7e~ejDU%r%hFR#2BvV+Kt(4Yj% z!?m?xN~a=F9u1&B;i$pTCr5DkjCx1}av0H*7ZBdat0dV<>Cj+a0(qurArir^Mt!V&*68Z3Dc(}l)? zo}-|MW*RAHojETOhz3cr=S8C7avExR5GCk4a@5yNF}(~2w_!wvSKcx$GxR?rpz_dt zK~kULs0}Zt+@lRFWAad{aVCg_hpD@fA`oAB5F;VRKrWB~%)>&<%ourCWigM{RkMuC zs|yMv8DT+@F?V1|3`f0TY~x_{78nR(s!n|boIvGSUJ(nEnZP*8O38saB?zgSXR1X^ zvO?;WkLuI}XvX1RKI9@V;lz((mWM+aIO<=C?x0GFzaC@A=-~N-77Sy_1J{_OYZ2=m zz!7SUGCln1RQ!Pw*qG?apE;F`7M1YTt3moBa11!jI}7*z{U31<5nNOF-2fKw)y(W!ug zGC^8YK4cg#hRH<>OyK5VX24(~S9B#WR>{Q+)%;^n?O>S7Vk8;KMt0F`nD1YLWjJv` zzQ1C*KSVQCK6;6lVdUafQ$8w$dECT{R?*s*;$bPvF4#km4Hk;|OZ+vGiqV^BuE;ko zL(75=e3?(IfP#^W6daoQ^RZf+V3xP}<=iubBZ)W*axb4mqT%~|uode*`4Eaz(MSlb zfO-_5Aq^K;A`AhLkG2bjDyfzgLGT_~p2!t3`dEHNxD>(QU%1MnX7QRI2x%%*s2~Z< ze3AlYL6;BqD_&u01Q+!Jg_2yQc^xzcae+(3HFe$$a)DvZN;SW#+)O{fLGYH< z(Aqhlme5=`v}jexvnfh8eaT64JOm?d9+Fg6~I4_yMTNvu7> zC3<8&Exx(8YJy96B<7R&2~jI^>ikQE*JvPcf7FE|n3SmB;9_G;KIsDz z3YSqf@@cuxyhaMnB(hBAlgrGx`KVQg4y$(TI06pOpasK4_QE+v1Tvh2B>5^%>Sl5p zm5`j3w3Oor#>o*#dgJMU2;tZe)(ni3Mi^lu;hLHulP6LNloUD@Sc*5@SnQ0GMJSx1 z3YM#$$rxF6M!F+>RXVK($YSj!5NoU>_?HrN$&D1p7W@|V}D)Ta!oU)SjlvUT)kxh;wV4#t{4(A*& zQaRWb&Ut0yriIJvsAML6h<#Mv3?{?F7ih( z4Amp$$U|069+#dP*q`TMW+N<%VCrrXFXOo1$jK7YQ=6O?Jy-yj@*3AuYlH^v+IUC< zrcoeHvS!0cYi^bio=^Y{Y3#$s$p$?*SJX{5y0~$&(a!_ZhfEW2l6Ffw8y+mM%@84H zO+^{6M?KIqWD@c~i;ZG{V+Qj?{7@#j14NKh3N)f=F-T6DY{!w++C!3=1w5Q}tW8G=6aGF^Y5rUx* z^Dhj|kk4d8_F%6BtZcX>R$mdWGIOoRtozM`#Lg;OV0a+=F-U2W^}tR5&A|+5DE^{2 zcqk>Ft=VbJxNx$tAu?!$b;e5W0Zx{c9-0kErXd13Wds-}w>ml5QeYxb`%+wHdhp-Gunx6m3q(~^$pMXU}^ z!8q{m%Gz)}qR(th+HC6wJY33P;V9Uf?(Oky&K&DFAz8 z6=38-0g7p4DVmVn;qvij@@6-SrvlQXW9TyzCt8K#Eulqt0n}}bBzWOA%T~k1;#AM3Qz&<6UlPAAP60rxQpsiLmW&AkQ+jA5}P&)L@E+%tD6Q& z0b0jj!;21bL&i>`tsHTqlH!g{h1mmc)<>C_Y9SmV(+w{dgaXU`<@H8ognLCD@iw(# zh$KGC$^z32Nq17f6@=<*$5Y-^ez9D-HrZCB08e96DDYL7rK5({ z*p9FRMtwwEO4KaW4p4+iX`^|o04nbCWSS&5DPw~Vlde>aS+@Y4H77i$I!N)ssw9eO z>VZ2Ptw_2_H-se1fmxUq1S}E@RQOSRW5(oR&u+3ID4wT>0;Hf*TLG;hpeEA9cbr1VvEpJ8WaLRoCU}N7_mG?- z*@Tc0=^-PFRr(=hWO~S`bkFFdkVLf6W723wr$-yp+>@5+!K2e?4o;7@D5<6+X%h)7 zLj^U0lZ`9zu;qhnB!$MlLTe4k!ooqp@DS5}h6d9J8Fz+&YYPXd2SX?2!8k1>V@!Q{ zo%f_uC;{5X8N$+{aFC()FIfT&xR9(Z{wN!X3yqE~dr5`U$P5EZ18w>jl3^tp^j8F7 zJt>^wUqL!_z=SYUPlcf#AyCd?s$ru|AtZ9rmP4MTfrUIYV?)S+W^O3vuM80P!E+2O z8{sG`e1(hApFAI+K8Cj^@cxc?c=^p9(HaWb`a8oIvkI{^4f&@c6%3FZ;;_JHNmpnL#-Lf8m`Vo)@kAz)Tw z&one+6-`zYBE7(#xoY<-G}DRbB=7_vBSU77tB`HeJd40n3)#ujBRr(Uv3w}33YP_| zz_28+;0Y&Z#X`14kLQqJY|;wM$7b!eYv(DaT>8tjo-6`que01tF+tv0r~s<1pWXA}y>N@DbI^&mUsW7Kaj5}C*fNmaxO z(kO$4q+#=}2Zg{Bh?W}hE}nT1q!OY^8A6QyJ&w#>@Q^usy0Ngp17QwziMEysMNq(~ z2Tu&TSa~%|e0XSiG|`CpLMSXWg3)vojk;4?)TqTt4=q4Q(;>dbbbx&ko~sJk{>_4% zJmH2|#B8n=lGuqB&of6G*b~!eI1#};fz;~4b^aPzH5Qr~1r1NTZ%Gg7HteAV9fy!* zoefKcyu{$OTOk|GSzjrnxd*KpGrf)TPtYr|Q%I>bU?l>**_>u66bpg&G%HDZU zd8B>_#3W2AN5DFZiG+4cwp9ZVH1L2R1a4oE_jZ@_t4=`jNtnFF=ZQ$_FWp z?ZD2^$dQ~`M8Y>Q5X0FflOKF$IvBr_%{=hc1gIUShjDJNGc`95vvdiB1Oz1s|q`iQsu?4HOf;lwGKE8LJ=Q zO_*>xo5@hsyUzY{WBeihLjBC>RO!{37O6>FKxZSh$IA|1XCs_dnd%I43yLXHyt7f$ zVSuTRQp*QIF>E@B0O(9InHt|%qB|RNN1%e&vYko&#*D@acxOa}RjQmVkDYP$2{KZ1 zfG@RXN7#zG#W6q4xyX+NmZ=H((gjmz9w?K@HiIw(rKLjaiiv7{?+lAAjIJ^jb%s1D zPcl6DX!e4{$M_*1*>Pu^#b;*{NwmLath1d-458U$VN){l3BQ0&WDm7`4s5_|?`6fd zvj`lves)$h*NCa4&n(3{$5Q={G~r9Dku2viIbdGtY}&4>9@_`S&3v!{P*@ZjLZp`% zA0#S?7t>1Uk3dAHkj|pMvcOUwtJj@L@_|2jGq^K4nKTX(bVw7sA0e0!+3?hvgozPP zbllANSnxo?cQy)al0S`4#X3UCuIsGYaT*zSJHg|0!5B7{tDzY0@D7p@OJkJ03x=hM z2zZB&Vj)Cw2i_6GQz|9AB3z6-ftU19Rs>DtMbueCNc$tN$Pm^*yQH+qS!EPzvB9g@@ zV2G@ug|$d{Llra84}?Z2VN(*Mi%Aws9f{0F0+mu-W{4?{4bojyz)-P~6frnPBqe0W-qG@mZd51R+Uq0aoXh7rMWq`0A^sJK}K1GqU44*s?iMOjgciq zwaqRNmP@Hed6;#fabi~9#@5w^^&?%&b|E37ErO&VJ4|huy}OXcV=O1;L?XPT9EK|W z^)(n!@S3e7k%3)AVi-Y;P!bE;u*Qq!XRvkxZ5XD!rn`+7yVWbuP-a60v}9LN$=d}& z(9AQu`O-xN$CNPbTXg}`(Rm0OyVNOEbtv2A0i)M+p)m>0D@W5oICe1uFDbj&i=*Zh zX2nkf&-{G*@wrD(H1j(-&HWzB?>I%x;ul-tXtieHT`Yg2<#%!!q2*a+hdUh&M#G;k zYVPl5#gAM5L_Q;w{xi$(c$$azw&)?W%une%Sblxqi-vnG`hk{Tn$OYrMckZ8KSaFw zX}DBB8J@@0Px^aXekf4Q=%-uZc~*FdKGqNMtb;3#HR^$<7ZXVNOkx}4Qn25tP(2b5k4ke&cnYbU6Uk#N+;}q{7AFp`HI$w z><3PEY`W6nGnVDXp3s%FO*%FqdqQ1#yRfptXq=Ru97q{*6ogs2pXbh0I4zASKaEe4 z*~Awc$7IaFCS3eW%)Tk&i@)0PlVvkKJ}(VZ@k3!UsB}%vT|7;6g^YkES{IbrtLrqa zF6JFvZ|F)3DkJK6{_3hiRCX7;LRS@o1}G7q4$IM8Gc7i0)?}0?Wk;Ax=PkP$Jv6n^ z)0NMWRp?yPRVN5p26csb9p~_Ps)N`Azm((PsVL9z%c7Vgz)!1R2vZlPiyx1;o2qU97b2@Xz8<1SF9Q_BbQ?SGNQFB z*#OI6a+(>g3u6B3YIHLM8GQ+U=rvFT!{sx0>gk%)rPz4bjaF8}iVtEI$2bOJQJPNF z0SMw)KClSRq!^;gK-)28=q5Y~9fFvUVwgQ``lDqIrJ-CWYw;99&X=YT=HRCxQO2aR zCX}dhq8w>XLy!eDJ%%j6QZeN8ZWOUw zAb2&W;}i&5v|&AolND_C=tdSGVn1CZbgL>KUq!1yTFIG4CcS9}w63~RHV1a2?F+Mt z&}?ZO1}eveDkkv3@@}MmVZpT&vJ}D?+145}+GOO#M>Mm<)~&82{2>Q2d0Tb#`?6$qPrQYdK`or78v9W(?!xi zA{Y?v@8z{f3Nx5295`OsoyRZsc%o)0(E%78z`9ciO#1kQs7zt>$Z#6QsJQ8I*!-7{ zp_0-O-4qqr#Ow~HV|ekO!qimqm8KL%&(EeXp+MbYT70K4kfIQ*R4+QMGz3$LTEBrI z13oS9Csdo`XK5HP1s$j%3=gcSX0}Mj;BX5JJ%p(@)^=ZebKNmsVDwEh z&vsY)GGsd&-MSk~tXX#eL3?c7W$}S1K$B^(yRH&u^X@QVT9nX;IqE4i#Z)?*(;YiZ zDtTdGNb`co`4KW$tpgUpB zARF+>5n@n_l8nn54q~NHtke)n`CTR~gGy(3N(&m%0-glTA+ZiXDH(W&580+r5Q)>w zl#)V`&IHLqX(=JJPS)H~DC%U5B}Jee-HCL+1+fH0!zUO*J2podEv-3fIW6x}C5OP#Su@mhs7oZ3i*Wh{Yw|Ku28dBG(2C5&Ym_t{Aw^Z?sX3Cj*@}1&r*E<- zDs8JpP;~jxq)N%BjdjsXi+FlW8Iur;lvffOso`D(zQpl;GYgA71}L$=ghQe=7pYJ{ zm7;t%pa@c-S>qsi%zK!P)1^R<)YvBeHJsUlcAJ%}C$sVg90n;Ly&od{#5iHR#mr;P za59%R!2M2IkvwgfidPw2VHhBtmde7LzNvUsN$nto z1E(4FDxDT}1cT5uyHq@7tY*06DZaFLE_u*&!!vv;9#pcWcKLK6k)B7yB|RK6Gn=dF z@mN07s7vLcmS@5xXL~W`cQ81X8ju)bu%vj5BNdt$COsZW7hc|`;?cG}>|fYv?=h|h zVm|~0kZ5O`#Y_*=FI0C7H5rgSNaw)d_t#7#bp&%D%BKTsE^cg_(^!Nfxxt&$I=}WX zX7^IGXADP9X;eDSPBbGnXI_IG*&9K^}lDji9T zXeOt0WO6P_M<(ZnbYyZWAfzR&O*NY%NnYao%H%FMPnCS;091sTX}!YVPv)W(3$ zj@5;EEk+8tBv^M0HZJIlZVGA65|o)e^%RotdmTmZxJV++&=(oj94Tu*Ak$mD)K3>i zQ%K?}*=b5c>M|`oCd#=yRZdB>s+^Xs#<-OtPcFC8$!m@NXs*4cV#2I5Ei@IYv-FrW zkj)ffBW21+k@Jpz8J*!vLxK+`@eWK1NxeP+yUv)sc1n#>qmxgnf$F{!X5ZvgyL>cF zwusq$&Br^_JMmWi6s+YiuH@9+PMDu)dJR;}42Q;<9R`q_VMMi?j`>-1;GI&MDh{Zb z!;y}&b86yBYmuEXI=rWCAi>5>|OFHPNJFQ&U4aHYGrJ=3BF|ApttGj?rQ)LObiSb0Frz!DgMg ze+RUgjw+*6i$ybq<-x)eUr;Ri$rAM-czq3wF zMi!~ky0d8QSfN6142b%XG`~AN%@U2)cft!K4O78RPwC*H$#^KI4?E$TB`1 zon&xHL`s(?KhtAJQWCVMjy=;EP=$Q)=I<|YUBU@L-?UF?!%O?0oh{lN?lymYE(QTsQ5VuA_NZOp4-X$F3*7OKTCe?gs zq=z&g1iiE=wFkud={OQXJBVP$x`5eX7%?1s3=(1{7bW9H>qSEuhcD@PS-N?ZXf!5u z9mSp;ToI^%ttCSU_A?P8y$>~zPt2mp>4hDs5Vdub8C5eqB&plWa7@vdeoHRG(`ify zEd))CDI{rQXtuiVU_zip;b2$V)G>};S=K8#{m%(wu>>Q_N?G#tf0T6?EM~pk`C>eqzLFxQ7h(RU(BI%m`flW%vtwTP)e7V zl)U=1>IDDaU4;R~ptFOWQ5f0lQ`BHab_G3^RPwd=5rf zstd|i)-DN~naT7)DlC0MV=H(n%%0R7rw_GRrphBQ+0-q%Ha)OK=l0Q2pj9>1kqM(u zvK}#dIomYdOoVz!1=32UjW{*ixdvkE7cHz^wQwkYVP<5%*ABa-FIV=NVn zj3Dh%WJKR|<*Y?r`f^?5#RdC&d5v!LC@j+Z^4_m*X-ryCPS-msjM`11S{oYD*gK%3 zJYij|rb02-K)Gq&5~)x!-3}X_Ep){2Y@wq^W(tL{BY8?g8OjuyDIBM7O$0Ah3)a?S zTEYDYmYgJCv4hH?cwJ|P(A{ZxjvO=UAhPNRv!-da2!;hl16r^Yf)`~OrxdKTY0Sa> z?>M_BhiY;IbFl8?V*-`nbg`T+W_NcHvuS#<30|PwJS3$X(4eNoNu8#Cf-FeW=*4cd z2TGo%|F>YVJ?!B!r1{9-!zAyPJ0D5laO9q>dGn(Wd(tS+R)9T|ZMVNyCn28QMJ?=S zx+YuS_A_N$2WO7xPeyqJe+Z?lwivD)*yU70jrodF1%v_L)lbI9x zyY>O@4tVJx<>-;r3wxQo=s4Gc%n9w~o)C6x()u$klD3jLSvJ;d?&OacPTB39-WP7Q zLm{kK83&q4E7>D;khwSrkP<~Q9QSrDdqu3yt8bK{at+{aVeW+3*KxtqzgTH0S{f_4 z6`zM_vuK%{m7`HVYlFG6)d1HTR7J2j6RBIpRY2)sk@23CKH?&2(IT0nLEe+%?=^3B zrD*$bgESv55V6fvA4uALTnmSnq+6v+i?;-q*is?^hc(qh`R=RZV_@^FWN|c@Z5qf} zBBt7)Md9QKmqbK2m^AntmsguExt2(#Xy;4(haI7`V`BTJb3ryCxSup4r@#}`h~cS; zN-OR-`=uwTWpm|*{ahQoJk{LhuqHL7d(1#8ra1|kIpzw)Og~jXF$Z#uOk-*>lTE1V z5<<~I16wO7pHAXb-B>r~P3fw}z)Vs1=K{1CsAY!t%>;wE^YG4^{5~*T`rIjhoy}q}YR9W8-Z+M!=64J{D&d#c4&y zZDl&oIM%zQzS1k(2O><8O+C~#_1USiG&AT>2JRFQFsBTnu~0ZrIPN{7D0a=Gnb3mP zP_6?dGYLLEppnLUZRtMDHJ4Zoo>) zI$%y6KouIvtY=IK%GWfHq)kI^+&!`_nc+JXsWKY*-zG8`Ud&sOsEDj5xRc?<9TbH) zts-r-vPF{XmSlc?eN|HQDq^e`FG=w((AQ%SncGjU1?lx5=al`TH+wvu(at0M9oqYT zd<%z-9H;D1rY@QLhVg4!HvQkHF7D<=KeU7?nvTm(fB2lgf9OVEDfu^g-prc+eb4XF z=te*4V9q~&Uv~Q6LcH|_04zgaH0#m~x4-{XWQPIRNcc`xa| zR_NcmWAm|Y^bdED_WvY%{zw1imws;aKkdr&-m|mQ?|l0)54+Ll_F{TpEA;a}Ibxw3 z{cEEn|G#9<|MJ6*f5DCZ6dk{<(Dy6)Vu~C6J=*?1WY2#>{tJWL=)EJP{GVp0U%#r` zNH_Xop^x90ef)m<jtUn}&z&KUAf zH~PL~IscMNv)6zB?;g9)jlN(8)BCz-r~lKVr{uZOKQ8hs-irL`e*f)1xzRtlNb)~6 zd;UxBo3YJ}zLU^ue#xaw~cW;1Aj1y(~rBpuWSDMOaCQWq5tUerZ3&{ZxH^8 z`?BZ%=Zen`a-*NVkjtOAH9P&3m)3mhM!!|$x9^1P^xfV$+g1O0OZAu2veU17g^lveTDWzWcKqeWT2OCBMr~f5+RNyWQx&o+$0tBRlG#J^pv>y#VhTE?(p1uALANIfqH~LLd|9DMy`tu8~8tO)Wk<6doBeT{(DLt=oxb>w14p~%e;Mc-A%7CzW^ez7r}95? zqkl>C=aO!0*JN(QTh+y(u;b!<*Y*2U;a}g{?D?Ns8*#N?UZ?W!`t0@Z@m04fxAHfM z{Ap^1|32Jy-TiL#w~PEKxh#AB-g*CUo&UE;{!X9l^jADk;%Yx@lKT7J%uc`Qxp28# z`8Nsw#9PU~@i!}6_4oHmxuX-UjQ?9b_kZP{`c*Xo&SAn-gMPp_B)W0{i=|A5Xv zt&E>NmOlBN8~y7df4wuZ=U@2rws~&!Tb2L*nZ5m{f3ncke*BV@-|3jW{bm&Z*;Rks zD)fm~%Kx`5H!O22f6hVN{*Ak5um2%Wb{^tJzg74z(MtPY^70Gsy3wx@{*RxXJ^wS# zx_P)8{WH3Lotd5fg{Qr)_D^S|jNexJZ^RF0xz4}ar2W02?D^mO?W}-X`JWd3Bhd=| zz)yy|&L0Pjm+`Yl_Wb9)v;X66`F}0)*Y|yP{yibTdZQct2U31-Y4-e&dTin%H~OE1 z-nVCV`a$ESyV^gGlKLn9n4SLP0gGJa_W`p1=Jd->@9g3&b}PThIr){*ZVmJN<~8m$~Xce}&}lTb;fAf)Bgv zZ@Y;6De-2f@49x6H{II*0~!B`R_Kp9rtd3m^gZT@{Og}R|7#X5J(_k?d<`F9uDKlSaMJ^wA+-|pp>|9-On=?utDzb<~mqi*!K zi~ZZXE<68?8F=&zH~N^2U+=@&^S}S;=Umqh|C09iwUYn9mv3`je|lyAJl;zE@BQ?^ z?r!BjN6Y_N_WJkydBYKI^!p0`$6wA~|1D3ibsfK@(?tG+v(tavP<^>u{*^-C_(*p8 zvVtdF?T=SW{e82u)1UbH6|U>=gS7sw^xsM09T&Kjf2z}cFVu7oPY3Mn7#ks|9t+PZuAH1`H5EOM{W4f_58^^p^vvh|FE;uTDSZ!*8bl& zd;5+4peommev;_#iB|Hz;hLTgxzXRL_JdaRkLhopILVEEjEvvJvh3ym+x%Xx_S?!H zJbt_vW$(W)&!4!#E&orhXZl2k?D>0kTKhLQ`dA1eXX?rC7re<-16Tj{Odh0 zd-?b67W=}D{u`OUng(a5-+k5Fecb4Gi2RGUBL6@A>yfVd;}24QXGr$^$8@~mJh%Ms z75U}doSlCCnnOCf(U-{lpLjGo{Rz{nUFYAfdo%yUTPgpFAKxu<%YTbU=v(RkA>Up6 zcQ^WojNe2n^sm=E>$uUMsN?_Q?Bjo1-OxSV=+6{-Un}$*_kH^+H~K$!k@~knfBZ9R zE_0(lv=`GSTA}~PU-~`hM*pnvkN24D{onni3taX8w{`w(h5oRE{&l=t{=ZZGWnT9D zKiWFZb^Tu;=O?_EH1EIO+$>2u?7Rbi$nEIyNNYKcooqqnNJ7aG2f06O)yDdBYf)l(YZuHxZbojR{JN+)_e)g>!{S{M~ zyrdQSss|o$9Y3e;#q>?}+4CRxL1qm zMVmfy$rnT2=$Fd*AN%~NO&=X~$!>1+j-21I&yU*lkN)lcpWNu55&7rzZoYqE)87`o z<~ld}?s9%6VV(c6>5p6c{^4%)D}?{zld{hr1NT1lJvaI-QhxjVik<)513s;Gqu*D` zZ=e6L>G#eX^tc=SVS0Y(#O&oiIdahyH+s6i(1iKVK0jjTf9`WZ*ZKPnl^?HXAHNT; zc{J>ne}SCevd<6L`5)hO?M-gi&VX|76oI?D60q+~}uD{$6YU$)-=N-!RIJevb0LWk0d$ukZP#Yxy6R^4t4Q zHvPzbX1dEy$=}}pvFU5yInz~tnkxO*cuscy`{3{|B5w7+NaxRUv(qQ$kDTj9KUnk+ zd;h^MfA`;<Aod^I{%_M?wC%O-Zu!3>;mj6EVvr?I+(qdiwXs zU$`}8KUpaHt8gK}XuOZ+Ke|SIkn0s``C2S4EGKWJ<7>mXmR?yC_RDo=cS`djBHVcn z=%r_TJhv)!zuMdZPop$1y3mXJ^7RkgxX1V6E0UL>1x=Tp$M8MyTmsU-<#b^bi#qjncxI(x zqsMxy={p7*f2@~2EEqFhe*ev(u6bJxh8HFzIWzT=0?OQ-+&%tSZ?j~H{4m$t(;7JW zc8RfGyf?d)KAE6JA^NPp4p!(hc{_IQ-l0PWk99xa+;6T9rhMvf4R)?!^JaBHu&24psXu-LEdm zQhudBzvDhD^K){#S6lRSzq-+dK1T@!zJB3(!c7J5x*{3DlL>L#Dx68%`eOIq?4>p4oN@Pxm=K=#QN@;zsvmXJ9nzJ>K1 zrBm4D>37d{qu(t0t>&+GYNb=SrFXaAxY6gSech4#-*L}>=Q%H~aHH=n`i#aKerLje*VUuY9O<3irNmce0PRXus2SeIeynJB`vQeEF())Q$d+WzF-~d$vla za8>v2w7zPQ|6gtTpnLzH-|GyrFSelnr><|M{64wotobVZp-aO#ZuD1L^>4~RzrFGC zd2aM~S@e3(O!HNEL+y)uy3v0m_GRsVT_-4=!b?v1vdWEqo!Zx={;KCGo%YU8%gTPh z0ilkz{GdveMh~r{lQWAL*Wx zle^8Lr~CCKF7$T!cU*nMWhj51U4FV>-{?Yb&ma3W<-LUYqmw;<(Ea+RHqdvNF}5G* zJvKevPj9M$eQP+kf!5zH|1muWeS-4m+vPX+(`y~aD|U9Jx7#mp`-HFE+K=v+H@eW< z^$#A{=OnlC)BW-$7kWGYnjUmj|`{Kwz@{R3|Fbic5q4)cK+{O^w`&K`P2QhrsacogxBlCT{&P-iNw4Ks_}codb5Q<6?D1#Lho04v zUinbzLr;BlEA*e<+bsQ)&UZGt(A)K|eY@lHZuO`0g(c^-%-^p6q{Q)~-Re*01Djmv zZTbD<9cQhA{4TKNw>clUy7m0$9l3O8pZZ#o}X;zF`9Ng(VO#q=b{Xj{IvV;zJ)syZv98+>zY8XKbubR(Xvq_zq{Dwr}JSYYcOw$ zogowbP7M#f3HlcDpUxMRT%fsRo`2)rzWK_n|LA;CqYJ&wKZkvGi7GLRIJ~2=kh^7|z|A)Ho0I;G+`X6#oVZlH$jEIPsRxzNQ zIs_2}VF3f8;vhj(gcVRhQOAr4S4^m!4(cf==qd(O)KO6|vS;LsW5&E@42S=!*T3I; z^Lpp8Z#lp3|6N0E_0;rKS65f(@XY;mDFb9LmBXYH&Aq5ST=|jg`+K6k5+)^zXP<__wOIGBU!O$XD{1?vJZpSqK6TzSN?*)_BkAK*8*T>WN zUjY8x9}^YH{~Rkh&G!E2Rkfc=OaJ}4n1CVXpQE#l{Py=2J9*4+fyM{R?pFSh#lI5c zADh4Zz0JQA`X_#C%ggUl{rKST{%-PjZUOyYXfw2*5nze5?dtY?yx4KDfe$gsDE1846Ww? z&&1tE9(RG}Zw2Icz<+ID6VNWd>#lurwHu;wPF(!$2TA_a|KmHF5FA%KfytlR_}l9n zzcrilwd#lQk^dOvFZVY^Qw@KpmBDLZwrhN`=z;qVQTgruNmsIX;p^)?*q6WbvpOgVBRD^{?^9dh2Z4BWXt;ZB=z$k_%Auw1myl3@3i9dL%!70 zvL(mr`dll!|Gr{t!=Ljm0m^S}@{=!;;oLB4)>CQvpN#yq?Wb7&jOUD-Z@*Xg?`r4o zrP$xf^|1){SsVYj2_-kU{H5#n&B$-nKb4r*u;u?F|MMD8t1bTH_qkwiT9*bY6SoxJR}Aud?{R#QNt-$c_5HfhC{KpY~5Y^}gMPs(w~u{*C1;!2CP+mq*yA zYKdpF=X7P3en;=}Zpz=Y`nSigPj6=U+wGVAeO6F- zLJQx2UZ*#;eprR}t2x@2-ku4_{dW=SpW682GlzKRIiH=Ku}}1$-$wnm$${X{`)y#K zwec@ApE=!uPrPvNAu4|i`CkYB4`Kff_MZjo$^VkOo;+RUFN6H#-xcy__ZQ7Dc3PYK zF61U!y49UODgScIeg?AtkMpll9r?2zE*t*I=c=F8(9aU+=k=|P1a|#joBUSwx=5ICf_gUoc zx70uCPqyz|rv;lDhWA=`JH zpW*nF!%yi*1DYoMPuz`t2X6VSH*+W6b=&+pZCbJdSMKlmrc7q))v z?``?B3sGpmuAZ=8chye?^dDnLz#vb z9`>8_z+(EtIp?$9_|I4aY>H2&P{!=JF&u?J-QXBs=2-**L%IWo< z%kclqjNd7H2xD7HRph7kr`ebN-F;Um)wB z>Uh2(fc&-bx8LvgYw7)JpZ5I5Hd`3}cKNlxH=%NW{hT-BTh_DL0_9&}+5eaV_HTc0 zv2!XwmZ1J}B z9O`GdF=xB`!Oyaoe2H5JQ;rn*YwWLT+>f8s&v-bG$bD*jYh&^y9(ny2Woi8VLFUVP z7>Kz4GVNDr{oYG${M+<;KTPAlE6OkVbN4`L+ifw+T_3e%};gze+~W@VgASF zUswI#(|Nz+)A(->{ny}c-S=Lb{H?ZKetsJN#n3d2q_AM47$>o4_! zH2LpEM}+*@kEkpEVK4t9RX+AHIAQ4~Orqd}Re|G%B_V;p(Ut-)} z5uyLUe5=XXP{^0KNxj`4P2>MO+IQYh!E^R?k_`Am^@1Nw~0`2byoWDBRv?~F|r?ts%zn^mJ9_{LD{{)Wh z@5`8fragKXpW5H^J7SL>;A^jcUGIOX!GCv9Sl_Vys*S&E@!#WwN8hfm^OJCte*^Zf z&|XFBDgOhvRV+)BzYX}uY!9(MSeyI-HP!Q{QUy@ zIjExv$a<6WpSAG^`z+e;yy3@%yKzZ$(Q*2LA$4p-)_bDzaA#6xj&5URk8B#zWKeW{l7M2jAU8< z_Zj&)ekfM{Tej@bF-C&khgU3ja;){YjbMzns9Y9H$KXlya1GALZuTM$dh8snlOJ&QGnC{zveUjQd3p z9ivd5MY2IJTp(Xx+|-GxFbPg(f^GiuX21Q8@UO8yyj(OEuY-Re{KG}H^PfNVnfAiJ z#{TI!2gv^KPt5**ZIUVfB^(eXWKMc*`7ir%*J)||@9HV?x2Tsn-|?UDKlA$gI;ZLX z3E>}nO8IN*-yhuA@$X1VZv3|VGd}-OFVL=`pO(EOA0=P4UHwnwpL+GfN2c*VM)*s| z>ObaRecrFXrSX4zpzxQ?LUom2)0%YN_FGL#+rK}fzl2&{SpRNWV<+gQEB|OepVOf$)D3d+sRBlEp;^=`4UgO_UyH3{NEJ)c;loEc-@p=TmCA$B0~h^ zOZ@xqr(d1M|5~X()p7ma=Ksy6=Dq!we2J%je$l_u_%8$ha7fZVbM}9g*iWqe2+NT# zaqr!KI694grhb-hecb z{D-Y*yLVdu_Ehwr8;Dlaaa>_z-@0Fse2Jf#|8Yec|EGQBs;6b@XE6DaAD@Xcf#eVQ z63^*5@QXD5-&yjrzwcW8f6@!_ZxD~$SorV3_nd$1jP)VTk8?camTG#J@EIuv;3P@tdsxFPjwiY#(yc+&q9pXv*Q;0-gjHb->_-< zI{DB2^IILi*U(R2=qI}_G~U=Le-C^(_?I-Ta+0RXd?mi))TLjh@qY~KqnzL5Jdcmx z3ojvmS@B+%{(tHI#U*L{2V?xp`Ax1<@b|=v)z2nPdw!b6|9R+#<7yA<2-)8oUuy2F zQ2j{PF6*cl;_qH<&_0d-m*~HdKkYO7d*tL_Z_YaTFMZfgwa;HXBT|+>>?r$t<>X(B z{NDUize$sS8rIjzpYy2M-!mtFx1!#<^t0x%t@VER8uqyy<%{(J*Pr-%=h^&iTfI*E zX*_j--XB`Eea8Jm zF22zNw{4{Ta_81_eDLS~Fxp+Q{9o>Sc$+l-E&7Wr zkJg3%Vd-e#>51dh$HYE;_hdXM*@yAU}yqrEor%t={uBF+9aXM!7k12l;%_$}zf|lW zz46@rgP#x!HCG^JA=+h{>0@?UY9!Pvd_r_%}p>DafDgeS!J7H4B#< zA@c(@#_vzc{BXQMt`i==l0JU7@6Ta*qx@eSb<^)@_cuH%DFirpCW&Gm)oAT%M&whV2M#dD! zIZm6MMTU7fIc(qGE(*^|lV5-TIXXQT+Qm38M=?HIe*68cN1lFt8h`zL;b=xKG{=!S zN^$&;c|IPL#$U&$@uxX%agp-d`fqh}pP$n7zmL@ap&T~U^e63~`gf0$`L`nUfBvF# zmZizBzyIl-o0C6$qd5Dim-{5+J9zF!`wRaX_h)&1MSjeGCd=13GVTo%^R{og7DMat z)-&E*oF@OCI|-kLIcfYi)8((Zt~oNbVX$w$R(oL_UagE&5s{lgTI|l ziZN!==*gp|$i1rCF<3_v$9jYKnl_(ol{Wq^k@ahJ(OG6(qxYaC?ckIi<~{!|*@V7z zW5!E-Mw@+}NQ=KScGS8jn)t^H+hqJro11vz+nYHG?gsgGSx5@TOML&8ub!M1zqViM z*e=$!9W!6o{L6TWkNR$Lbcl=@p8f1kp{pKIs#Y?q(jLi16EOYSe@CEok=RS%`b zUxD$7cdpUD9sggI|4)DM@hfTZzmN6n82MkY9vvm^oIq{u7&X9j6OUfH@$|I#wSS?` zd)3rSCjOlMYwKs2uWSBgyu`l@nzB(^{6moc!7QWyLg|l2H+B@(H~p{8>iTC|{M!Ff z7yb{Yzt4yl%hTf5{);;HiwdS+`Z3yN<8+jn}ewD_+>`u#CU{pa$3W-}un|Bl}< zzdVv|W9B>Y7SlFsoEE>{|EJEKm2Q9ACMSI)+$u|+%*QEz7%%a0ckDMOE&hA-xGn@5%J|?e(qojsK~EuO5&V|EC>I{9!XQPiWV_(>9j(^Q3*f`gC7hUk`}-2kI1;h`k0zH$6tSy{e543=I&|n zpEBG;@8b8N?exDOx;?w_mlXO?ai>lMcy#NU)m4x3nNQf$oPGeO6+gp{aKGldmfEX zeEO-W2kU`E%n)4T;rO0(8#7+wt*-b*=MQSc|BBdCaJ zk@0iiLb360y}0knwD_Np_9vPsx{Qtf9sEu=^PSj7MAU!KOul)iQ0%$-i|N1P)y31( z;=f4i{|jaQt{VAYAbzcv)G_|LrhiyZ_mi~vGrv!MnaKMc(*JpS{&QbAKQS{=fA=K& zODDRceAL9z6Gly$lKq(;rSw*cyi6<|WPgz6gw&YE953HZ9X&p`5MhZsdenpoV<#tP zZYE3`ed5?LC-K5r^LW5CEM1r%kx?DibgVb|rtA#o9GSnaaewpEWm0}_H1m&IHPYUG zvBnLq)N@HUt#R^6{U@g;I(;6J`{wpB>4;jG_QR9);s)}MdYCB%IF@hOORw5|L|Xf$>nrL4tgGlcN-}=76Y&I@zY_R;i~Rn?39a*= z4Y!)^edqI{j@*j4wC(f0_7r>x_F@EhX5UY#80(*`TVr_Bc$K|!PHw?PbSO3 z#@2Z4qxOu!;DtNnKl6$464yWax6jhz-wNY*@0wiyEgAncVCXHb?Cryv&*<0XFOmTgk!Z*_mUI^OSS z>;FgOzX|0y#rEeJFL9UGKOU5p{+-c(=KXhe{O3cz?rYM27O01M&wEAkL!eUTleK>v zyk0*8l#}riPj9v1$h7#6*vow3CHn8l{GX`fS8?Gm1A&YulJ0)IPtw%Sc!{su@0uBD z@jnUs^ShY**ZzAl{(UwzoP$A49ECo3b_X|c6xw5c#~awhQJ`bI#I1IJWRtY`55f2$ z*uliF{e)!sKU(^U>ii#@X5b5iH&n+NDD_{OWBtwey*_`INdKkX8tjMS{tn$g?tbJr z^S?6vmk9gFX?DcAuUuUhgPg(W?{Qxa$XVoNea3UmACtFhBJ1m2aK8xS|40IQ6C`gp zH1&`7eR}^X+|lr1{4LqvTwuam(Pi_W^fn!T*YgqW?E6_7$^4!7u-re=+VF`A#V_rW zI>z6ii3y+GcVk``{Coe-O=qR8?^R0u8FCmpZhKH%lchZ3ec1u z;Ah@D^B|4iPQTXw{zkd@qax!!9rx!)3yahLGiTm-L7M)rmG&>1p6kDRn8#zfiS6{? zhWsz2e>?prUbMp=Y4KmK`M+Hv{YA$A%f}dRU|+bUZijM{K5WA^kE#Cc-(S-COIJc? z^ZzHKPkv8qr=R67#Jo4-A!hoCAKG_Rxukz@JO4k_`RlWC`j3i?|H@A=-Wge3{(tt( zH(RIa-;w!?=Bf1&)hGhoV>E$-6zJ?s9hYFWRFf?WJjk?}wN1M>f- z^z^g-C0_WQ54N^z!!qdf&9rnxFA7?)tue0?Z^Z%1^ zRo|xN|AyO3-%*%#LHrTor`-{Ae*xnk!218H;?m#z?A;DeOaFZ`f8ZqMFT6E|AG=w^ zoIfc=0#6lvL^6N#gp3EgmmO!C^n(?1UV(Zie*X0bK9l`pvKnk0=dbQLmK1|45kI?? z#7zp+Hycn7?;E)pAIJH8e$oB+q_p@SKFY)!qy8`b)$n1w#3vPqe+J4S+hN9M34>91q_+aUjg;p>in`L$2nnHK+*)mHldUAOcwRKDXArVf}Pfa;9E&h>q`s)~fQ`A512kVai zppBl?`)z8JzYDDROaH7}`j`HM{O`B!_>a2zcDrQ{i6TbX7E&gqe%`bnI>lwfMWPbYX_J7`_hyAqp zzZ#Jre`&q-OTXRzv)^a9JI#NCAt20Y5nx{Z;A1ica?e1c&k1~zZrDK zOU!Uz9DU3=Y4QJImA}eb;aqT^0H z@8*A(YWh9gAMD`%?7%92r47^5zbEv}^N&o|YOecUlVAQh{#$~8Tc6mWm8QQO>Fh;l{>plWpI!czj`{pgDStKY=eSJfKjR(cJhROE zHI#qUAJ;wnq0JnxUNh$$CK$N3br8l+y!Dxny_Yus_-q%$FkWQj*Kt;|9++d5^Xsp7ow|8&fXg%0qfor^$DOoR3h)^U8KT`WPDm6U@0<>f59{O6Jp(`!hOjo_t%_ zPq~PDT=&S`Y4Yj(mO7SOTRuG9zN9-9^WRBYnxAqJ@Aby~fobxE z{qXhqCVy@DR$zULaje`*^d;+GbhXLLa5kn~#20=yyF;3M(`9}p8Y}ZN&!@|GZF|Ex zc+hbaR`!tRdEy^hF9WSmpgo!6Rd}bfc^?g_=O`3%l#6)Ay+iIxldm4^$se39AJ?zk zn`Qk*;3KS8x}iJ^*!e$4>_MG3Pu>X>a+HhskI$BFmnNU~FVy+y*Vy^{pY}5h_nLnx z7xCd++_i0*d>!^P-^RD5>u-Rre@cGRMOg2OGEBs>tBiZ?EJjMDzDk~x_4x~PN0yZT*Rk6Hh4;!eA<6d=cC`C z{$%@cg_~c$yk*IACf3jNJ9XmF~PHc zYwHJO16m%+Mf}I8<6lV2-^)=x!b6PyjC{HMA>E}uNw!}h=Bb@E!daj()+ha6W&Kj% z1nAQer^X}YB5rZ(0oSL=_hm2hX*fAuzS{ObPJ+DQZ?eBa;Fu!iD}}@^u-B>a`MSP} zM`tPSuhsoEe&YPF+tSF%l1HrjZq#|1zm9V*=QU3={W6f9a*THz&?yJs6QkM5t^@pj z{P%C^`iDLKYId-+RTKG@v~>mK3niR7FZ0)NMqs^w=eR+3W^z6R_drk%z9)9!A>MTO zGiRuL{QbPgp+DUp6K`10;rzIdeKOP=ag2yO@ZF#DQf`T`zM`}^6~dOt3^IE$ zeN1}lLBKZ9pNsKoIR&>c$F0QkxBvRzN%`7CzI_jo`lTc%U$Aj;`m0U8c#g^ULh^M- zf{6R}JWcO+&enfrkZ-ui=WU#m&)wuNl5gI++qy}-=lsxdZ@JRo8{z- z{v!Dz%m)(<{i=#O~Q;M(1j^8E6Y-&$-J?c-Aig>lduY`V`2* zl$$tseQuvL{q@oLjZ<^;vvC3WbX=#-%lvhm{m~ELeOVwoeJncT=X+w(i8sA&y567C zE=2pw_D%1v^gqeT7vLUCw!6f(d~Dx1Z^?Miza5u&&QJTasKx_vAwej}P zwrtq2#{RhlqQ591pVOf@`M7T*w)Ssri2WSAZ_bv3^4N0!)_US?-|GJbSZ}0WZ8>cD`2Mtg23?VqkM;9YQhvi@bL$V@=Edo6ChRY~--KtpTSI@O zGk(4&w)Hx7#Dt9f+56)f%J@7wE2qD>NcoO$YuZ=d_rv#*{he-FTx~S z|7}^Ed>p^=Ts7lu_l>Dvc;45RgW*VLKHs;v=aMw}e$)QxIXU^FBIVU$Wz-D~#N>9F&Ls7UE9LmW)a3zckhP zp>uQkbKK(Oqa5yX6Q1$1|H5|9=L-une@X83^O`Tx@^@1yzutK{`NAUQdjs@T>(y!WyK z<$GK_5+5<~?k7|}&c8g$`Ip1R{u1)VMas8zQ^?oZq=)jcT?s~;_oew_WuK^5;=Ma9 zJT#fV?Emd7@;T?{@;B&Moc>r&?e%2ZCGCৄN`Rd~N?a}XUl+0i1ujyd1zl415 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

|@l~j0M?gpo>{0;Gd`ZaT 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

  • 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

    ZKG_B-iUa-nA||b!cD?;Sq0dLfTh0t?s$jp%+0z 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 L{QBn~;spLLn>`>& literal 0 HcmV?d00001 diff --git a/pkg/yara/config/config.go b/pkg/yara/config/config.go new file mode 100644 index 000000000..206841c43 --- /dev/null +++ b/pkg/yara/config/config.go @@ -0,0 +1,159 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "github.com/mitchellh/mapstructure" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "path/filepath" + "strings" + "time" +) + +const ( + enabled = "yara.enabled" + alertVia = "yara.alert-via" + alertTextTemplate = "yara.alert-template.text" + alertTitleTemplate = "yara.alert-template.title" + fastScanMode = "yara.fastscan" + scanTimeout = "yara.scan-timeout" + skipFiles = "yara.skip-files" + excludedProcesses = "yara.excluded-procs" + excludedFiles = "yara.excluded-files" +) + +// RulePath contains the rule path information. +type RulePath struct { + Path string `json:"path" yaml:"path" mapstructure:"path"` + Namespace string `json:"namespace" yaml:"namespace" mapstructure:"namespace"` +} + +// RuleString contains the in-place strings for the rule definition. +type RuleString struct { + String string `json:"string" yaml:"string" mapstructure:"string"` + Namespace string `json:"namespace" yaml:"namespace" mapstructure:"namespace"` +} + +// Rule contains rule-specific settings. +type Rule struct { + // Paths defines the location of the yara rules + Paths []RulePath `json:"yara.rule.paths" yaml:"yara.rule.paths" mapstructure:"paths"` + // Strings contains the raw rule definitions + Strings []RuleString `json:"yara.rule.strings" yaml:"yara.rule.strings" mapstructure:"strings"` +} + +// Config stores YARA watcher specific configuration. +type Config struct { + // Enabled indicates if YARA watcher is enabled. + Enabled bool `json:"yara.enabled" yaml:"yara.enabled"` + // Rule contains rule-specific settings. + Rule Rule `json:"yara.rule" yaml:"yara.rule" mapstructure:"rule"` + // AlertVia defines which alert sender is used to emit the alert on rule matches. + AlertVia string `json:"yara.alert-via" yaml:"yara.alert-via"` + // AlertTemplate defines the template that is used to render the text of the alert. + AlertTextTemplate string `json:"yara.alert-text-template" yaml:"yara.alert-text-template"` + // AlertTitle represents the template for the alert title + AlertTitleTemplate string `json:"yara.alert-title-template" yaml:"yara.alert-title-template"` + // FastScanMode avoids multiple matches of the same string when not necessary. + FastScanMode bool `json:"yara.fastscan" yaml:"yara.fastscan"` + // ScanTimeout sets the timeout for the scanner. If the timeout is reached, the scan operation is cancelled. + ScanTimeout time.Duration `json:"yara.scan-timeout" yaml:"yara.scan-timeout"` + // SkipFiles indicates whether file scanning is disabled + SkipFiles bool `json:"yara.skip-files" yaml:"yara.skip-files"` + // ExcludedProcesses contains the list of the process' image names that shouldn't be scanned + ExcludedProcesses []string `json:"yara.excluded-procs" yaml:"yara.excluded-procs"` + // ExcludedProcesses contains the list of the file names that shouldn't be scanned + ExcludedFiles []string `json:"yara.excluded-files" yaml:"yara.excluded-files"` +} + +// InitFromViper initializes Yara config from Viper. +func (c *Config) InitFromViper(v *viper.Viper) { + c.Enabled = v.GetBool(enabled) + c.AlertVia = v.GetString(alertVia) + c.AlertTextTemplate = v.GetString(alertTextTemplate) + c.AlertTitleTemplate = v.GetString(alertTitleTemplate) + c.FastScanMode = v.GetBool(fastScanMode) + c.ScanTimeout = v.GetDuration(scanTimeout) + c.SkipFiles = v.GetBool(skipFiles) + c.ExcludedFiles = v.GetStringSlice(excludedFiles) + c.ExcludedProcesses = v.GetStringSlice(excludedProcesses) + + all := v.AllSettings() + if _, ok := all["yara"]; !ok { + return + } + if _, ok := all["yara"].(map[string]interface{}); !ok { + return + } + + var r Rule + _ = decode(all["yara"].(map[string]interface{})["rule"], &r) + c.Rule = r +} + +// AddFlags registers persistent flags. +func AddFlags(flags *pflag.FlagSet) { + flags.Bool(enabled, false, "Specifies if Yara scanner is enabled") + flags.String(alertVia, "mail", "Defines which alert sender is used to emit the alert on rule matches") + flags.String(alertTextTemplate, "", "Defines the template that is used to render the text of the alert") + flags.String(alertTitleTemplate, "", "Defines the template that is used to render the title of the alert") + flags.Bool(fastScanMode, true, "Avoids multiple matches of the same string when not necessary") + flags.Duration(scanTimeout, time.Second*10, "Specifies the timeout for the scanner. If the timeout is reached, the scan operation is cancelled") + flags.Bool(skipFiles, true, "Indicates whether file scanning is disabled") + flags.StringSlice(excludedFiles, []string{}, "Contains the list of the comma-separated file names that shouldn't be scanned") + flags.StringSlice(excludedProcesses, []string{}, "Contains the list of the comma-separated process' image names that shouldn't be scanned") +} + +// ShouldSkipProcess determines whether the specified process name is rejected by the scanner. +func (c Config) ShouldSkipProcess(ps string) bool { + for _, proc := range c.ExcludedProcesses { + if strings.ToLower(proc) == strings.ToLower(ps) { + return true + } + } + return false +} + +// ShouldSkipFile determines whether the specified file name is rejected by the scanner. +func (c Config) ShouldSkipFile(file string) bool { + for _, f := range c.ExcludedFiles { + if strings.ToLower(f) == strings.ToLower(filepath.Base(file)) { + return true + } + } + return false +} + +func decode(input, output interface{}) error { + var decoderConfig = &mapstructure.DecoderConfig{ + Metadata: nil, + Result: output, + WeaklyTypedInput: true, + DecodeHook: mapstructure.ComposeDecodeHookFunc( + mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToSliceHookFunc(","), + ), + } + decoder, err := mapstructure.NewDecoder(decoderConfig) + if err != nil { + return err + } + return decoder.Decode(input) +} diff --git a/pkg/yara/scanner.go b/pkg/yara/scanner.go new file mode 100644 index 000000000..01a2105d2 --- /dev/null +++ b/pkg/yara/scanner.go @@ -0,0 +1,385 @@ +// +build yara + +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package yara + +import ( + "bytes" + "expvar" + "fmt" + "github.com/hillu/go-yara" + "github.com/rabbitstack/fibratus/pkg/alertsender" + "github.com/rabbitstack/fibratus/pkg/ps" + pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" + "github.com/rabbitstack/fibratus/pkg/util/multierror" + "github.com/rabbitstack/fibratus/pkg/yara/config" + log "github.com/sirupsen/logrus" + "html/template" + "os" + "path/filepath" + "time" +) + +const alertTitleTmpl = `{{if .PS }}YARA alert on process {{ .PS.Name }}{{ else }}YARA alert on file {{ .Filename }}{{ end }}` + +const alertTextTmpl = ` + {{ if .PS }} + Possible malicious process, {{ .PS.Name }} ({{ .PS.PID }}), detected at {{ .Timestamp }}. + + Rule matches + {{- with .Matches }} + {{ range . }} + Rule: {{ .Rule }} + Namespace: {{ .Namespace }} + Meta: {{ .Meta }} + Tags: {{ .Tags }} + {{ end }} + {{- end }} + + Process information + + Name: {{ .PS.Name }} + PID: {{ .PS.PID }} + PPID: {{ .PS.Ppid }} + Comm: {{ .PS.Comm }} + Cwd: {{ .PS.Cwd }} + SID: {{ .PS.SID }} + Session ID: {{ .PS.SessionID }} + {{ if .PS.Envs }} + Env: + {{- with .PS.Envs }} + {{- range $k, $v := . }} + {{ $k }}: {{ $v }} + {{- end }} + {{- end }} + {{ end }} + Threads: + {{- with .PS.Threads }} + {{- range . }} + {{ . }} + {{- end }} + {{- end }} + Modules: + {{- with .PS.Modules }} + {{- range . }} + {{ . }} + {{- end }} + {{- end }} + {{ if .PS.Handles }} + Handles: + {{- with .PS.Handles }} + {{- range . }} + {{ . }} + {{- end }} + {{- end }} + {{ end }} + + {{ if .PS.PE }} + Entrypoint: {{ .PS.PE.EntryPoint }} + Image base: {{ .PS.PE.ImageBase }} + Build date: {{ .PS.PE.LinkTime }} + + Number of symbols: {{ .PS.PE.NumberOfSymbols }} + Number of sections: {{ .PS.PE.NumberOfSections }} + + Sections: + {{- with .PS.PE.Sections }} + {{- range . }} + {{ . }} + {{- end }} + {{- end }} + {{ if .PS.PE.Symbols }} + Symbols: + {{- with .PS.PE.Symbols }} + {{- range . }} + {{ . }} + {{- end }} + {{- end }} + {{ end }} + {{ if .PS.PE.Imports }} + Imports: + {{- with .PS.PE.Imports }} + {{- range . }} + {{ . }} + {{- end }} + {{- end }} + {{ end }} + {{ if .PS.PE.VersionResources }} + Resources: + {{- with .PS.PE.VersionResources }} + {{- range $k, $v := . }} + {{ $k }}: {{ $v }} + {{- end }} + {{- end }} + {{ end }} + {{ end }} + + {{ else }} + + Possible malicious file, {{ .Filename }}, detected at {{ .Timestamp }}. + + Rule matches + {{ with .Matches }} + {{ range . }} + Rule: {{ .Rule }} + Namespace: {{ .Namespace }} + Meta: {{ .Meta }} + Tags: {{ .Tags }} + {{ end }} + {{ end }} + + {{ end }} +` + +var ( + // ruleMatches computes all the rule matches + ruleMatches = expvar.NewInt("yara.rule.matches") + // rulesInCompiler keeps the counter of the number of rules in the compiler + rulesInCompiler = expvar.NewInt("yara.rules.in.compiler") + // totalScans computes the number of process/file scans + totalScans = expvar.NewInt("yara.total.scans") +) + +type scanner struct { + c *yara.Compiler + s *yara.Scanner + config config.Config + + psnap ps.Snapshotter +} + +// AlertContext contains the process state or file name along with all the rule matches. +type AlertContext struct { + PS *pstypes.PS + Filename string + Matches []yara.MatchRule + Timestamp string +} + +const tsLayout = "02 Jan 2006 15:04:05 MST" + +// NewScanner creates a new YARA scanner. +func NewScanner(psnap ps.Snapshotter, config config.Config) (Scanner, error) { + c, err := yara.NewCompiler() + if err != nil { + return nil, fmt.Errorf("unable to create yara compiler: %v", err) + } + // add yara rules from file system paths by walking the dirs recursively + for _, dir := range config.Rule.Paths { + f, err := os.Stat(dir.Path) + if err != nil { + log.Warnf("cannot access %q rule path: %v", dir.Path, err) + continue + } + if !f.IsDir() { + continue + } + err = filepath.Walk(dir.Path, func(path string, fi os.FileInfo, err error) error { + if filepath.Ext(path) != ".yar" { + return nil + } + f, err := os.Open(path) + if err != nil { + log.Warnf("cannot open the rule %q: %v", path, err) + return nil + } + err = c.AddFile(f, dir.Namespace) + _ = f.Close() + if err != nil { + log.Warnf("couldn't add %s rule: %v", fi.Name(), err) + return nil + } + rulesInCompiler.Add(1) + + return nil + }) + if err != nil { + log.Warnf("couldn't walk %s path: %v", dir.Path, err) + } + } + + // add yara rules from config strings + for _, s := range config.Rule.Strings { + err := c.AddString(s.String, s.Namespace) + if err != nil { + log.Warnf("couldn't add %s rule string: %v", s.String, err) + continue + } + rulesInCompiler.Add(1) + } + + if len(c.Errors) > 0 { + return nil, parseCompilerErrors(c.Errors) + } + + r, err := c.GetRules() + if err != nil { + return nil, fmt.Errorf("couldn't compile yara rules: %v", err) + } + s, err := yara.NewScanner(r) + if err != nil { + return nil, fmt.Errorf("fail to create yara scanner: %v", err) + } + + // set scan flags + var flags yara.ScanFlags + if config.FastScanMode { + flags |= yara.ScanFlagsFastMode + } + s.SetFlags(flags) + s.SetTimeout(config.ScanTimeout) + + return &scanner{ + c: c, + s: s, + config: config, + psnap: psnap, + }, nil +} + +func parseCompilerErrors(errors []yara.CompilerMessage) error { + errs := make([]error, len(errors)) + for i, err := range errors { + errs[i] = fmt.Errorf("%s, filename: %s line: %d", err.Text, err.Filename, err.Line) + } + return multierror.Wrap(errs) +} + +func (s scanner) ScanProc(pid uint32) error { + proc := s.psnap.Find(pid) + if proc == nil { + return fmt.Errorf("cannot scan proc. pid %d does not exist in snapshotter", pid) + } + + if s.config.ShouldSkipProcess(proc.Name) { + return nil + } + + matches, err := s.s.ScanProc(int(pid)) + if err != nil { + return fmt.Errorf("yara scan failed on proc %s (%d): %v", proc.Name, pid, err) + } + totalScans.Add(1) + if len(matches) == 0 { + return nil + } + ruleMatches.Add(int64(len(matches))) + + ctx := AlertContext{ + PS: proc, + Matches: matches, + Timestamp: time.Now().Format(tsLayout), + } + return s.send(ctx) +} + +func (s scanner) ScanFile(filename string) error { + if s.config.SkipFiles || s.config.ShouldSkipFile(filename) { + return nil + } + matches, err := s.s.ScanFile(filename) + if err != nil { + return fmt.Errorf("yara scan failed on %s file: %v", filename, err) + } + totalScans.Add(1) + if len(matches) == 0 { + return nil + } + ruleMatches.Add(int64(len(matches))) + + ctx := AlertContext{ + Filename: filename, + Matches: matches, + Timestamp: time.Now().Format(tsLayout), + } + + return s.send(ctx) +} + +func (s scanner) send(ctx AlertContext) error { + if s.config.AlertTitleTemplate == "" { + s.config.AlertTitleTemplate = alertTitleTmpl + } + if s.config.AlertTextTemplate == "" { + s.config.AlertTextTemplate = alertTextTmpl + } + // build a new yara alert template from the config options + // or use a default template string. We'll feed the alertsender + // with the output of the parsed template. Template content is + // rendered by employing the Go templating engine. For more + // details see https://golang.org/pkg/text/template/ + title, err := executeTmpl(s.config.AlertTitleTemplate, ctx) + if err != nil { + return err + } + text, err := executeTmpl(s.config.AlertTextTemplate, ctx) + if err != nil { + return err + } + + // fetch the alert sender that is specified in the config + sender := alertsender.Find(alertsender.ToType(s.config.AlertVia)) + if sender == nil { + return fmt.Errorf("%q alert sender is not initialized", s.config.AlertVia) + } + + alert := alertsender.NewAlert( + title, + text, + tagsFromMatches(ctx.Matches), + alertsender.Normal, + ) + + log.Infof("emitting yara alert via %q sender: %s", s.config.AlertVia, alert) + + return sender.Send(alert) +} + +func executeTmpl(body string, ctx AlertContext) (string, error) { + var writer bytes.Buffer + + tmpl, err := template.New("yara").Parse(body) + if err != nil { + return "", fmt.Errorf("template syntax error: %v", err) + } + err = tmpl.Execute(&writer, ctx) + if err != nil { + return "", fmt.Errorf("couldn't execute template: %v", err) + } + + return writer.String(), nil +} + +func tagsFromMatches(matches []yara.MatchRule) []string { + tags := make([]string, 0) + for _, match := range matches { + tags = append(tags, match.Tags...) + } + return tags +} + +func (s scanner) Close() { + if s.c != nil { + s.c.Destroy() + } + if s.s != nil { + s.s.Destroy() + } +} diff --git a/pkg/yara/scanner_test.go b/pkg/yara/scanner_test.go new file mode 100644 index 000000000..bc51f899b --- /dev/null +++ b/pkg/yara/scanner_test.go @@ -0,0 +1,185 @@ +// +build yara + +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package yara + +import ( + "github.com/rabbitstack/fibratus/pkg/alertsender" + htypes "github.com/rabbitstack/fibratus/pkg/handle/types" + "github.com/rabbitstack/fibratus/pkg/kevent/kparams" + "github.com/rabbitstack/fibratus/pkg/pe" + "github.com/rabbitstack/fibratus/pkg/ps" + pstypes "github.com/rabbitstack/fibratus/pkg/ps/types" + "github.com/rabbitstack/fibratus/pkg/syscall/handle" + "github.com/rabbitstack/fibratus/pkg/syscall/process" + "github.com/rabbitstack/fibratus/pkg/yara/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "os" + "path/filepath" + "syscall" + "testing" + "time" +) + +var yaraAlert *alertsender.Alert + +type mockSender struct{} + +func (s *mockSender) Send(a alertsender.Alert) error { + yaraAlert = &a + return nil +} + +func makeSender(config alertsender.Config) (alertsender.Sender, error) { + return &mockSender{}, nil +} + +func init() { + alertsender.Register(alertsender.Noop, makeSender) +} + +func TestScan(t *testing.T) { + psnap := new(ps.SnapshotterMock) + require.NoError(t, alertsender.LoadAll([]alertsender.Config{{Type: alertsender.Noop}})) + + s, err := NewScanner(psnap, config.Config{ + Enabled: true, + AlertVia: "noop", + Rule: config.Rule{ + Paths: []config.RulePath{ + { + Namespace: "default", + Path: "_fixtures/rules", + }, + }, + }, + }) + require.NoError(t, err) + + var si syscall.StartupInfo + var pi syscall.ProcessInformation + + argv := syscall.StringToUTF16Ptr(filepath.Join(os.Getenv("windir"), "notepad.exe")) + + err = syscall.CreateProcess( + nil, + argv, + nil, + nil, + true, + 0, + nil, + nil, + &si, + &pi) + require.NoError(t, err) + defer syscall.TerminateProcess(pi.Process, uint32(257)) + + proc := &pstypes.PS{ + Name: "notepad.exe", + PID: pi.ProcessId, + Ppid: 2434, + Exe: `C:\Windows\notepad.exe`, + Comm: `C:\Windows\notepad.exe`, + SID: "archrabbit\\SYSTEM", + Cwd: `C:\Windows\`, + SessionID: 1, + Threads: map[uint32]pstypes.Thread{ + 3453: {Tid: 3453, Entrypoint: kparams.Hex("0x7ffe2557ff80"), IOPrio: 2, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + 3455: {Tid: 3455, Entrypoint: kparams.Hex("0x5efe2557ff80"), IOPrio: 3, PagePrio: 5, KstackBase: kparams.Hex("0xffffc307810d6000"), KstackLimit: kparams.Hex("0xffffc307810cf000"), UstackLimit: kparams.Hex("0x5260000"), UstackBase: kparams.Hex("0x525f000")}, + }, + Envs: map[string]string{"ProgramData": "C:\\ProgramData", "COMPUTRENAME": "archrabbit"}, + Modules: []pstypes.Module{ + {Name: "kernel32.dll", Size: 12354, Checksum: 23123343, BaseAddress: kparams.Hex("fff23fff"), DefaultBaseAddress: kparams.Hex("fff124fd")}, + {Name: "user32.dll", Size: 212354, Checksum: 33123343, BaseAddress: kparams.Hex("fef23fff"), DefaultBaseAddress: kparams.Hex("fff124fd")}, + }, + Handles: []htypes.Handle{ + {Num: handle.Handle(0xffffd105e9baaf70), + Name: `\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{b677c565-6ca5-45d3-b618-736b4e09b036}`, + Type: "Key", + Object: 777488883434455544, + Pid: uint32(1023), + }, + { + Num: handle.Handle(0xffffd105e9adaf70), + Name: `\RPC Control\OLEA61B27E13E028C4EA6C286932E80`, + Type: "ALPC Port", + Pid: uint32(1023), + MD: &htypes.AlpcPortInfo{ + Seqno: 1, + Context: 0x0, + Flags: 0x0, + }, + Object: 457488883434455544, + }, + { + Num: handle.Handle(0xeaffd105e9adaf30), + Name: `C:\Users\bunny`, + Type: "File", + Pid: uint32(1023), + MD: &htypes.FileInfo{ + IsDirectory: true, + }, + Object: 357488883434455544, + }, + }, + PE: &pe.PE{ + NumberOfSections: 2, + NumberOfSymbols: 10, + EntryPoint: "0x20110", + ImageBase: "0x140000000", + LinkTime: time.Now(), + Sections: []pe.Sec{ + {Name: ".text", Size: 132608, Entropy: 6.368381, Md5: "db23dce3911a42e987041d98abd4f7cd"}, + {Name: ".rdata", Size: 35840, Entropy: 5.996976, Md5: "ffa5c960b421ca9887e54966588e97e8"}, + }, + Symbols: []string{"SelectObject", "GetTextFaceW", "EnumFontsW", "TextOutW", "GetProcessHeap"}, + Imports: []string{"GDI32.dll", "USER32.dll", "msvcrt.dll", "api-ms-win-core-libraryloader-l1-2-0.dl"}, + VersionResources: map[string]string{"CompanyName": "Microsoft Corporation", "FileDescription": "Notepad", "FileVersion": "10.0.18362.693"}, + }, + } + psnap.On("Find", mock.Anything).Return(proc) + + for { + if process.IsAlive(handle.Handle(pi.Process)) { + break + } + time.Sleep(time.Millisecond * 100) + } + + // test attaching on pid + require.NoError(t, s.ScanProc(pi.ProcessId)) + require.NotNil(t, yaraAlert) + + assert.Equal(t, "YARA alert on process notepad.exe", yaraAlert.Title) + assert.NotEmpty(t, yaraAlert.Text) + assert.Contains(t, yaraAlert.Tags, "notepad") + + // test file scanning on DLL that merely contains + // the fmt.Println("Go Yara DLL Test") statement + require.NoError(t, s.ScanFile("_fixtures/yara-test.dll")) + require.NotNil(t, yaraAlert) + + assert.Equal(t, "YARA alert on file _fixtures/yara-test.dll", yaraAlert.Title) + assert.Contains(t, yaraAlert.Tags, "dll") + +} diff --git a/pkg/yara/scanner_unsupported.go b/pkg/yara/scanner_unsupported.go new file mode 100644 index 000000000..14cd8622b --- /dev/null +++ b/pkg/yara/scanner_unsupported.go @@ -0,0 +1,32 @@ +// +build !yara + +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package yara + +import ( + kerrors "github.com/rabbitstack/fibratus/pkg/errors" + "github.com/rabbitstack/fibratus/pkg/ps" + "github.com/rabbitstack/fibratus/pkg/yara/config" +) + +// NewScanner returns unsupported scanner error. +func NewScanner(psnap ps.Snapshotter, config config.Config) (Scanner, error) { + return nil, kerrors.ErrFeatureUnsupported("yara") +} diff --git a/pkg/yara/types.go b/pkg/yara/types.go new file mode 100644 index 000000000..ee425725a --- /dev/null +++ b/pkg/yara/types.go @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 by Nedim Sabic Sabic + * https://www.fibratus.io + * All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package yara + +// Scanner watches for certain kernel events such as process creation or image loading and +// triggers the scanning either of the target process or image file. If matches occur, an +// alert is emitted via specified alert sender. +type Scanner interface { + // ScanProc scans process memory. + ScanProc(pid uint32) error + // ScanFile scans the specified file in the file system. + ScanFile(filename string) error + // Close disposes any resources allocated by scanner. + Close() +} diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 92cea75ff..000000000 --- a/requirements.txt +++ /dev/null @@ -1,17 +0,0 @@ -cython==0.23.4 -docopt==0.6.2 -ptable==0.9.2 -logbook==0.12.5 -apscheduler==3.0.5 -pefile - -pika==0.10.0 -elasticsearch==5.0.0 - -anyconfig==0.6.0 -pyaml==15.8.2 -pykwalify==1.6.0 - -pytest==2.9.1 -pytest-cov==2.2.1 -codecov diff --git a/schema.yml b/schema.yml deleted file mode 100644 index 54844efb5..000000000 --- a/schema.yml +++ /dev/null @@ -1,132 +0,0 @@ -type: map -mapping: - - image_meta: - type: map - mapping: - enabled: - type: bool - required: True - imports: - type: bool - file_info: - type: bool - - skips: - type: map - required: True - mapping: - images: - type: seq - sequence: - - type: str - - output: - type: seq - required: True - sequence: - - type: map - mapping: - - console: - type: map - mapping: - format: - type: str - enum: ['pretty', 'json'] - - amqp: - type: map - mapping: - host: - type: str - port: - type: int - username: - type: str - password: - type: str - vhost: - type: str - exchange: - type: str - required: True - routingkey: - type: str - required: True - - smtp: - type: map - mapping: - host: - type: str - required: True - port: - type: int - password: - type: str - from: - type: str - pattern: .+@.+ - to: - type: seq - sequence: - - type: str - pattern: .+@.+ - - elasticsearch: - type: map - mapping: - hosts: - type: seq - required: True - sequence: - - type: str - required: True - index: - type: str - required: True - index_type: - type: str - enum: ['fixed', 'daily'] - daily_index_format: - type: str - document: - type: str - required: True - bulk: - type: bool - username: - type: str - password: - type: str - ssl: - type: bool - - fs: - type: map - mapping: - path: - type: str - required: True - mode: - type: str - enum: ['r', 'w', 'x', 'a', 'r+', 'w+'] - format: - type: str - enum: ['json'] - - binding: - type: seq - sequence: - - type: map - mapping: - - yara: - type: map - mapping: - path: - type: str - required: True - - diff --git a/setup.py b/setup.py deleted file mode 100644 index 7a57ed49c..000000000 --- a/setup.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -import re -import os -import shutil -from os.path import expanduser -from setuptools import setup, find_packages -from setuptools.extension import Extension -from Cython.Distutils import build_ext -try: - from pip._internal.req import parse_requirements -except ImportError: - from pip.req import parse_requirements - -from fibratus.version import VERSION - - -def copy_artifacts(): - home_dir = expanduser('~') - here = os.path.abspath(os.path.dirname(__file__)) - fibratus_dir = os.path.join(home_dir, '.fibratus') - if not os.path.exists(fibratus_dir): - os.mkdir(fibratus_dir) - shutil.copy(os.path.join(here, 'fibratus.yml'), fibratus_dir) - shutil.copy(os.path.join(here, 'schema.yml'), fibratus_dir) - shutil.copytree(os.path.join(here, 'filaments'), os.path.join(fibratus_dir, 'filaments')) - -kstreamc_ext = Extension('kstreamc', - ['kstream/kstreamc.pyx'], - libraries=["tdh", "advapi32", "ole32", "ws2_32"], - language='c++') - -install_reqs = parse_requirements('requirements.txt', session=False) -reqs = [str(ir.req) for ir in install_reqs if not re.match('pytest|codecov', str(ir.req))] - -copy_artifacts() - -setup( - name="fibratus", - version=VERSION, - author="Nedim Sabic (RabbitStack)", - author_email="bhnedo@hotmail.com", - description="Tool for exploration and tracing of the Windows kernel", - long_description="Fibratus is a tool which is able to capture the most of the Windows kernel activity - " - "process/thread creation and termination, file system I/O, registry, network activity, " - "DLL loading/unloading and much more. Fibratus has a very simple CLI which encapsulates " - "the machinery to start the kernel event stream collector, set kernel event filters or " - "run the lightweight Python modules called filaments. You can use filaments to extend " - "Fibratus with your own arsenal of tools.", - license="Apache", - keywords="windows kernel, tracing, system exploration, syscalls", - platforms=["Windows"], - url="https://github.com/rabbitstack/fibratus", - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Topic :: System', - 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4' - ], - ext_modules=[kstreamc_ext], - cmdclass={"build_ext": build_ext}, - packages=find_packages(), - install_requires=reqs, - entry_points={ - 'console_scripts': [ - 'fibratus=fibratus.cli:main', - ], - } -) diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index b4b9e2a2a..000000000 --- a/tests/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. \ No newline at end of file diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py deleted file mode 100644 index c41bcbd9f..000000000 --- a/tests/fixtures/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2015/2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. diff --git a/tests/fixtures/fibratus.yml b/tests/fixtures/fibratus.yml deleted file mode 100644 index 9290cad8f..000000000 --- a/tests/fixtures/fibratus.yml +++ /dev/null @@ -1,54 +0,0 @@ ---- -image_meta: - enabled: false - imports: false - file_info: false - -skips: - images: - - svchost.exe - - smss.exe - - services.exe - - taskmgr.exe - - dwm.exe - - vprot.exe - - lsass.exe - - sihost.exe - - system - -output: - - console: - format: json - - amqp: - host: 127.0.0.1 - port: 5672 - username: guest - password: guest - vhost: / - exchange: amq.direct - routingkey: fibratus - - smtp: - host: smtp.gmail.com - port: 587 - from: info@github.io - password: secret - to: - - fibratus@github.io - - netmutatus@github.io - - elasticsearch: - hosts: - - localhost:9200 - index: kernelstream - document: threads - bulk: True - username: elastic - password: changeme - ssl: True - - fs: - path: D:\\ - mode: a - format: json - -binding: - - yara: - path: D:\\yara-rules \ No newline at end of file diff --git a/tests/fixtures/filaments/__init__.py b/tests/fixtures/filaments/__init__.py deleted file mode 100644 index c41bcbd9f..000000000 --- a/tests/fixtures/filaments/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2015/2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. diff --git a/tests/fixtures/filaments/test_filament_no_on_next_kevent.py b/tests/fixtures/filaments/test_filament_no_on_next_kevent.py deleted file mode 100644 index 0b8057d82..000000000 --- a/tests/fixtures/filaments/test_filament_no_on_next_kevent.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2015/2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Test filament description -""" - - -def on_init(): - pass diff --git a/tests/fixtures/filaments/test_filament_nodoc.py b/tests/fixtures/filaments/test_filament_nodoc.py deleted file mode 100644 index 0d88913b2..000000000 --- a/tests/fixtures/filaments/test_filament_nodoc.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2015/2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -def on_init(): - pass - - -def on_next_kevent(kevent): - pass \ No newline at end of file diff --git a/tests/fixtures/filaments/test_filament_wrong_on_next_kevent.py b/tests/fixtures/filaments/test_filament_wrong_on_next_kevent.py deleted file mode 100644 index 1364e458a..000000000 --- a/tests/fixtures/filaments/test_filament_wrong_on_next_kevent.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2015/2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Test filament description -""" - - -def on_init(): - pass - - -def on_next_kevent(kevent, thread): - pass \ No newline at end of file diff --git a/tests/fixtures/schema.yml b/tests/fixtures/schema.yml deleted file mode 100644 index bdc93fdb8..000000000 --- a/tests/fixtures/schema.yml +++ /dev/null @@ -1,127 +0,0 @@ -type: map -mapping: - - image_meta: - type: map - mapping: - enabled: - type: bool - required: True - imports: - type: bool - file_info: - type: bool - - skips: - type: map - required: True - mapping: - images: - type: seq - sequence: - - type: str - - output: - type: seq - required: True - sequence: - - type: map - mapping: - - console: - type: map - mapping: - format: - type: str - enum: ['pretty', 'json'] - - amqp: - type: map - mapping: - host: - type: str - port: - type: int - username: - type: str - password: - type: str - vhost: - type: str - exchange: - type: str - required: True - routingkey: - type: str - required: True - - smtp: - type: map - mapping: - host: - type: str - required: True - port: - type: int - password: - type: str - from: - type: str - pattern: .+@.+ - to: - type: seq - sequence: - - type: str - pattern: .+@.+ - - elasticsearch: - type: map - mapping: - hosts: - type: seq - required: True - sequence: - - type: str - required: True - index: - type: str - required: True - document: - type: str - required: True - bulk: - type: bool - username: - type: str - password: - type: str - ssl: - type: bool - - fs: - type: map - mapping: - path: - type: str - required: True - mode: - type: str - enum: ['r', 'w', 'x', 'a', 'r+', 'w+'] - format: - type: str - enum: ['json'] - - binding: - type: seq - sequence: - - type: map - mapping: - - yara: - type: map - mapping: - path: - type: str - required: True - - diff --git a/tests/pytest.ini b/tests/pytest.ini deleted file mode 100644 index 59147a1ee..000000000 --- a/tests/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -[pytest] -python_files = *.py -addopts = -sv --cov-report html --cov-report xml --cov=fibratus \ No newline at end of file diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py deleted file mode 100644 index 23b74303e..000000000 --- a/tests/unit/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. \ No newline at end of file diff --git a/tests/unit/apidefs/__init__.py b/tests/unit/apidefs/__init__.py deleted file mode 100644 index c41bcbd9f..000000000 --- a/tests/unit/apidefs/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2015/2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. diff --git a/tests/unit/apidefs/declarer.py b/tests/unit/apidefs/declarer.py deleted file mode 100644 index 28d23ce51..000000000 --- a/tests/unit/apidefs/declarer.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from ctypes import c_uint -import os - -import fibratus.apidefs.declarer as declarer - - -class TestDeclarer(): - - def test_declare_function(self): - get_current_process_id = declarer.declare(declarer.KERNEL, 'GetCurrentProcessId', [], c_uint) - - assert callable(get_current_process_id) - assert get_current_process_id.restype == c_uint - assert get_current_process_id.argtypes is None - - # check the function result - pid = os.getpid() - assert get_current_process_id() == pid \ No newline at end of file diff --git a/tests/unit/binding/__init__.py b/tests/unit/binding/__init__.py deleted file mode 100644 index d7ce39087..000000000 --- a/tests/unit/binding/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright 2017 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. \ No newline at end of file diff --git a/tests/unit/binding/yar.py b/tests/unit/binding/yar.py deleted file mode 100644 index 65698239d..000000000 --- a/tests/unit/binding/yar.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright 2017 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from unittest.mock import Mock, patch, MagicMock - -from fibratus.kevent import KEvent -from logbook import Logger -import pytest -import os - -from fibratus.errors import BindingError -from fibratus.output.amqp import AmqpOutput -from fibratus.thread import ThreadInfo - - -@pytest.fixture(scope='module') -def outputs(): - return dict(amqp=Mock(spec_set=AmqpOutput)) - - -class TestYaraBinding(object): - - def test_init(self, outputs): - with patch.dict('sys.modules', **{ - 'yara': MagicMock(), - }): - from fibratus.binding.yar import YaraBinding - with patch('os.path.exists', return_value=True), \ - patch('os.path.isdir', return_value=True), \ - patch('glob.glob', return_value=['silent_banker.yar']), \ - patch('yara.compile') as yara_compile_mock: - YaraBinding(outputs, - Mock(spec_set=Logger), output='amqp', path='C:\\yara-rules') - yara_compile_mock.assert_called_with(os.path.join('C:\\yara-rules', 'silent_banker.yar')) - - def test_init_invalid_path(self, outputs): - with patch.dict('sys.modules', **{ - 'yara': None, - }): - from fibratus.binding.yar import YaraBinding - with patch('os.path.exists', return_value=False), \ - patch('os.path.isdir', return_value=False): - with pytest.raises(BindingError) as e: - YaraBinding(outputs, - Mock(spec_set=Logger), output='amqp', path='C:\\yara-rules-invalid') - assert 'C:\\yara-rules-invalid rules path does not exist' in str(e.value) - - def test_init_yara_python_not_installed(self, outputs): - with patch.dict('sys.modules', **{ - 'yara': None, - }): - from fibratus.binding.yar import YaraBinding - with pytest.raises(BindingError) as e: - YaraBinding(outputs, - Mock(spec_set=Logger), output='amqp', path='C:\\yara-rules') - assert 'yara-python package is not installed' in str(e.value) - - def test_run(self, outputs): - with patch.dict('sys.modules', **{ - 'yara': MagicMock(), - }): - from fibratus.binding.yar import YaraBinding - with patch('os.path.exists', return_value=True), \ - patch('os.path.isdir', return_value=True), \ - patch('glob.glob', return_value=['silent_banker.yar']), \ - patch('yara.compile'): - yara_binding = YaraBinding(outputs, - Mock(spec_set=Logger), output='amqp', path='C:\\yara-rules') - yara_binding.run(thread_info=Mock(spec_set=ThreadInfo), kevent=Mock(spec_set=KEvent)) - assert yara_binding._rules.match.called diff --git a/tests/unit/cli.py b/tests/unit/cli.py deleted file mode 100644 index c41bcbd9f..000000000 --- a/tests/unit/cli.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2015/2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. diff --git a/tests/unit/config.py b/tests/unit/config.py deleted file mode 100644 index dff1c938d..000000000 --- a/tests/unit/config.py +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from unittest.mock import patch -from fibratus.config import YamlConfig - -import os - -__CONFIG_PATH__ = os.path.join(os.path.dirname(__file__), '..', 'fixtures', 'fibratus.yml') -__SCHEMA_FILE__ = os.path.join(os.path.dirname(__file__), '..', 'fixtures', 'schema.yml') - - -class TestYamlConfig(): - - def test_load_yaml(self): - config = YamlConfig(__CONFIG_PATH__) - config.load(False) - assert config.yaml - - def test_load_validate(self): - config = YamlConfig(__CONFIG_PATH__) - config.default_schema_path = __SCHEMA_FILE__ - config.load() - - def test_load_yaml_not_found(self): - with patch('sys.exit') as sys_exit: - YamlConfig('C:\\fibratus.yml').load(False) - sys_exit.assert_called_once() - - def test_outputs(self): - config = YamlConfig(__CONFIG_PATH__) - config.load(False) - outputs = config.outputs - assert outputs - assert isinstance(outputs, list) - assert len(outputs) > 0 - - def test_enum_outputs(self): - config = YamlConfig(__CONFIG_PATH__) - config.load(False) - output_names = ['amqp', 'smtp', 'console', 'elasticsearch', 'fs'] - outputs = config.outputs - if outputs: - for output in outputs: - output_name = next(iter(list(output.keys())), None) - assert output_name in output_names - - def test_image_skips(self): - config = YamlConfig(__CONFIG_PATH__) - config.load(False) - image_skips = config.skips.images - assert image_skips - assert isinstance(image_skips, list) - assert 'smss.exe' in image_skips - - def test_bindings(self): - config = YamlConfig(__CONFIG_PATH__) - config.load(False) - bindings = config.bindings - binding_names = ['yara'] - assert bindings - for binding in bindings: - assert isinstance(binding, dict) - binding_name = next(iter(list(binding.keys())), None) - assert binding_name in binding_names \ No newline at end of file diff --git a/tests/unit/context_switch.py b/tests/unit/context_switch.py deleted file mode 100644 index b4e0fa14e..000000000 --- a/tests/unit/context_switch.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from unittest.mock import Mock, call - -from datetime import datetime -import pytest - -from fibratus.context_switch import ContextSwitchRegistry, ThreadState, WaitMode, WaitReason -from fibratus.kevent import KEvent -from fibratus.thread import ThreadRegistry, ThreadInfo -from fibratus.common import DotD as ddict - - -@pytest.fixture(scope='module') -def thread_registry_mock(): - thread_registry = Mock(spec_set=ThreadRegistry) - thread_info1 = ThreadInfo(456, int('0x1054', 16), 22, 'explorer.exe', 'C:\\Windows\\EXPLORER.exe') - thread_info2 = ThreadInfo(1, int('0x0', 16), 2, 'system.exe', 'C:\\Windows\\system.exe') - thread_registry.get_thread.side_effect = [thread_info1, thread_info2] - return thread_registry - - -@pytest.fixture(scope='module') -def thread_registry_empty_mock(): - return Mock(spec_set=ThreadRegistry) - - -@pytest.fixture(scope='module') -def kevent_mock(): - return Mock(spec_set=KEvent) - - -class TestContextSwitchRegistry(object): - - def test_next_cswitch(self, thread_registry_mock, kevent_mock): - - context_switch_registry = ContextSwitchRegistry(thread_registry_mock, kevent_mock) - ts = datetime.strptime("12:05:45.233", '%H:%M:%S.%f') - kcs1 = ddict({'old_thread_wait_ideal_processor': 2, 'previous_c_state': 1, 'old_thread_state': 2, - 'old_thread_priority': 0, 'reserved': 777748717, 'spare_byte': 0, - 'old_thread_wait_reason': 0, 'new_thread_wait_time': '0x0', 'old_thread_wait_mode': 0, - 'new_thread_priority': 15, 'new_thread_id': '0x1054', 'old_thread_id': '0x0'}) - new_thread_id = int(kcs1.new_thread_id, 16) - context_switch_registry.next_cswitch(1, ts, kcs1) - - thread_registry_mock.get_thread.assert_has_calls([call(new_thread_id), - call(int(kcs1.old_thread_id, 16))]) - - assert (1, new_thread_id,) in context_switch_registry.context_switches() - cs = context_switch_registry.context_switches()[(1, new_thread_id,)] - assert cs - - assert cs.timestamp is ts - assert cs.next_proc_name == "explorer.exe" - assert cs.next_thread_wait_time == 0 - assert cs.next_thread_prio == 15 - assert cs.prev_thread_prio == 0 - assert cs.prev_thread_state is ThreadState.RUNNING - assert cs.count == 1 - assert cs.prev_thread_wait_mode is WaitMode.KERNEL - assert cs.prev_thread_wait_reason is WaitReason.EXECUTIVE - - def test_next_cswitch_in_registry(self, thread_registry_empty_mock, kevent_mock): - context_switch_registry = ContextSwitchRegistry(thread_registry_empty_mock, kevent_mock) - - kcs1 = ddict({'old_thread_wait_ideal_processor': 3, 'previous_c_state': 0, 'old_thread_state': 5, - 'old_thread_priority': 8, 'reserved': 4294967294, 'spare_byte': 0, - 'old_thread_wait_reason': 8, 'new_thread_wait_time': '0x0', 'old_thread_wait_mode': 1, - 'new_thread_priority': 8, 'new_thread_id': '0x1fc8', 'old_thread_id': '0x2348'}) - kcs2 = ddict({'old_thread_wait_ideal_processor': 3, 'previous_c_state': 0, 'old_thread_state': 3, - 'old_thread_priority': 8, 'reserved': 4294967295, 'spare_byte': 0, - 'old_thread_wait_reason': 8, 'new_thread_wait_time': '0x0', 'old_thread_wait_mode': 1, - 'new_thread_priority': 5, 'new_thread_id': '0x1fc8', 'old_thread_id': '0x2348'}) - - context_switch_registry.next_cswitch(1, datetime.strptime("12:05:45.233", '%H:%M:%S.%f'), kcs1) - context_switch_registry.next_cswitch(1, datetime.strptime("12:05:45.234", '%H:%M:%S.%f'), kcs2) - - k = (1, int(kcs1.new_thread_id, 16)) - cs = context_switch_registry.context_switches()[k] - assert cs - assert cs.count == 2 - assert cs.next_thread_prio == 5 - assert cs.prev_thread_state is ThreadState.STANDBY - assert cs.timestamp == datetime.strptime("12:05:45.234", '%H:%M:%S.%f') - assert cs.prev_thread_wait_reason is WaitReason.FREE_PAGE diff --git a/tests/unit/controller.py b/tests/unit/controller.py deleted file mode 100644 index 4489ea4ab..000000000 --- a/tests/unit/controller.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2015/2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from fibratus.controller import KTraceController - - -def ktrace_controller(): - return KTraceController() - - -class TestKTraceController: - pass \ No newline at end of file diff --git a/tests/unit/dll.py b/tests/unit/dll.py deleted file mode 100644 index ae12b5977..000000000 --- a/tests/unit/dll.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2015/2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from unittest.mock import Mock -from fibratus.dll import DllRepository -from fibratus.kevent import KEvent - - -def dll_repo(): - return DllRepository(Mock(spec_set=KEvent)) - - -class TestDllRepository(): - pass \ No newline at end of file diff --git a/tests/unit/filament.py b/tests/unit/filament.py deleted file mode 100644 index 73f8d647a..000000000 --- a/tests/unit/filament.py +++ /dev/null @@ -1,174 +0,0 @@ -# Copyright 2015/2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -import os -from unittest.mock import Mock, patch - -import pytest -from apscheduler.schedulers.background import BackgroundScheduler - -import fibratus.filament as fil -from fibratus.errors import FilamentError -from fibratus.filament import Filament -from fibratus.output.amqp import AmqpOutput -from fibratus.term import AnsiTerm -from tests.fixtures.filaments import test_filament - - -@pytest.fixture(scope='module') -def ansi_term_mock(): - return Mock(spec_set=AnsiTerm) - - -@pytest.fixture() -def filament(ansi_term_mock): - fil.FILAMENTS_DIR = os.path.join(os.path.dirname(__file__), '..', 'fixtures\\filaments') - f = Filament() - f._ansi_term = ansi_term_mock - f.scheduler = Mock(spec_set=BackgroundScheduler) - return f - - -class TestFilament(object): - - def test_load_filament(self, filament): - with patch('os.listdir', return_value=['top_connections.py', 'test_filament.py']): - filament.load_filament('test_filament') - assert filament._filament_module - assert isinstance(filament._filament_module, type(test_filament)) - assert filament._filament_module.__doc__ - assert 'test_filament' in filament.name - - f = filament.filament_module - assert hasattr(f, 'set_filter') - assert hasattr(f, 'set_interval') - assert hasattr(f, 'columns') - assert hasattr(f, 'title') - assert hasattr(f, 'sort_by') - assert hasattr(f, 'limit') - assert hasattr(f, 'add_row') - assert hasattr(f, 'render_tabular') - - f.set_filter('CreateProcess', 'CreateThread') - assert 'CreateProcess' in filament._filters - assert 'CreateThread' in filament._filters - - f.set_interval(1) - assert filament._interval == 1 - - f.columns(['IP', 'Port', 'Process']) - assert filament._cols == ['IP', 'Port', 'Process'] - - f.limit(20) - assert filament._limit == 20 - - f.add_row(['192.168.1.3', '80', 'chrome.exe']) - - f.sort_by('Port') - assert 'Port' in filament._sort_by - assert filament._sort_desc - - f.title('Top outbound connections') - - def test_load_filament_not_found(self, filament): - with patch('os.listdir', return_value=['top_connections.py', 'test_filament.py']): - with pytest.raises(FilamentError): - filament.load_filament('filament_notfound') - - def test_load_filament_nodoc(self, filament): - with patch('os.listdir', return_value=['top_connections.py', 'test_filament_nodoc.py']): - with pytest.raises(FilamentError) as e: - filament.load_filament('test_filament_nodoc') - assert "Please provide a short description for the filament" in str(e.value) - - def test_load_filament_no_on_next_kevent_method(self, filament): - with patch('os.listdir', return_value=['top_connections.py', 'test_filament_no_on_next_kevent.py']): - with pytest.raises(FilamentError) as e: - filament.load_filament('test_filament_no_on_next_kevent') - assert 'Missing required on_next_kevent method on filament' in str(e.value) - - def test_load_filament_wrong_on_next_kevent(self, filament): - with patch('os.listdir', return_value=['test_filament_wrong_on_next_kevent.py']): - with pytest.raises(FilamentError) as e: - filament.load_filament('test_filament_wrong_on_next_kevent') - assert 'one argument' in str(e.value) - - def test_close(self, filament): - filament.close() - - def test_set_invalid_interval(self, filament): - with patch('os.listdir', return_value=['top_connections.py', 'test_filament_invalid_interval.py']): - with pytest.raises(FilamentError): - filament.load_filament('test_filament_invalid_interval') - - def test_add_row_invalid_type(self, filament): - with patch('os.listdir', return_value=['test_filament.py']): - filament.load_filament('test_filament') - f = filament.filament_module - with pytest.raises(FilamentError): - f.add_row({'ip': '192.168.4.31'}) - - def test_sort_by_no_columns(self, filament): - with patch('os.listdir', return_value=['test_filament.py']): - filament.load_filament('test_filament') - f = filament.filament_module - with pytest.raises(FilamentError) as e: - f.sort_by('Port') - assert 'Expected at least 1 column but 0 found' in str(e.value) - - def test_sort_by_column_not_found(self, filament): - with patch('os.listdir', return_value=['test_filament.py']): - filament.load_filament('test_filament') - f = filament.filament_module - f.columns(['IP', 'Port', 'Process']) - with pytest.raises(FilamentError) as e: - f.sort_by('File') - assert 'File column does not exist' in str(e.value) - - def test_limit_no_columns(self, filament): - with patch('os.listdir', return_value=['test_filament.py']): - filament.load_filament('test_filament') - f = filament.filament_module - with pytest.raises(FilamentError): - f.limit(20) - - def test_limit_non_integer(self, filament): - with patch('os.listdir', return_value=['test_filament.py']): - filament.load_filament('test_filament') - f = filament.filament_module - f.columns(['IP', 'Port', 'Process']) - with pytest.raises(FilamentError): - f.limit('20') - - def test_do_output_accessors(self, filament): - with patch('os.listdir', return_value=['test_filament.py']): - filament.load_filament('test_filament') - outputs = {'amqp': Mock(spec_set=AmqpOutput)} - filament.do_output_accessors(outputs) - assert getattr(filament.filament_module, 'amqp') - - def test_set_columns_not_list(self, filament): - with patch('os.listdir', return_value=['test_filament.py']): - filament.load_filament('test_filament') - with pytest.raises(FilamentError): - filament.filament_module.columns(('IP', 'Port')) - - def test_render_tabular(self, filament, ansi_term_mock): - with patch('os.listdir', return_value=['test_filament.py']): - filament.load_filament('test_filament') - filament.filament_module.columns(['IP', 'Port']) - filament.render_tabular() - ansi_term_mock.setup_console.assert_called_once() - ansi_term_mock.write_output.assert_called_once() diff --git a/tests/unit/fs.py b/tests/unit/fs.py deleted file mode 100644 index 1c461f5be..000000000 --- a/tests/unit/fs.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from unittest.mock import Mock - -import pytest - - -from fibratus.common import DotD as dd, NA -from fibratus.fs import FsIO, FileOps -from fibratus.handle import HandleInfo, HandleType -from fibratus.kevent import KEvent -from fibratus.kevent_types import CREATE_FILE, DELETE_FILE, WRITE_FILE, RENAME_FILE, SET_FILE_INFORMATION -from fibratus.thread import ThreadRegistry - - -@pytest.fixture(scope='module') -def kevent(): - return KEvent(Mock(spec_set=ThreadRegistry)) - - -@pytest.fixture(scope='module') -def fsio(kevent): - handles = [HandleInfo(3080, 18446738026482168384, HandleType.DIRECTORY, - "\\Device\\HarddiskVolume2\\Users\\Nedo\\AppData\\Local\\VirtualStore", 640), - HandleInfo(2010, 18446738023471035392, HandleType.FILE, - "\\Device\\HarddiskVolume2\\Windows\\system32\\rpcss.dll", 640)] - fsio = FsIO(kevent, handles) - fsio.file_pool[18446738026474426144] = '\\Device\\HarddiskVolume2\\fibratus.log' - return fsio - - -class TestFsIO(): - - def test_init_fsio(self, fsio): - assert len(fsio.file_handles) == 2 - - @pytest.mark.parametrize('expected_op, kfsio', - [(FileOps.SUPERSEDE, dd({"file_object": 18446738026482168384, "ttid": 1484, - "process_id": 859, - "create_options": 1223456, - "open_path": "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll", - "irp_ptr": 18446738026471032392, "share_access": 1, "file_attributes": 0})), - (FileOps.OPEN, dd({"file_object": 18446738026482168384, "ttid": 1484, "process_id": 859, - "create_options": 18874368, - "open_path": "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll", - "irp_ptr": 18446738026471032392, "share_access": 2, "file_attributes": 0})), - (FileOps.CREATE, dd({"file_object": 18446738026482168384, "ttid": 1484, "process_id": 859, - "create_options": 33554532, - "open_path": "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll", - "irp_ptr": 18446738026471032392, "share_access": 4, "file_attributes": 0})), - (FileOps.OPEN_IF, dd({"file_object": 18446738026482168384, "ttid": 1484, - "process_id": 859, - "create_options": 58651617, - "open_path": "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll", - "irp_ptr": 18446738026471032392, "share_access": 3, "file_attributes": 0})), - (FileOps.OVERWRITE, dd({"file_object": 18446738026482168384, "ttid": 1484, - "process_id": 859, - "create_options": 78874400, - "open_path": "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll", - "irp_ptr": 18446738026471032392, "share_access": 5, "file_attributes": 0})), - (FileOps.OVERWRITE_IF, dd({"file_object": 18446738026482168384, "ttid": 1484, - "process_id": 859, - "create_options": 83886112, - "open_path": "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll", - "irp_ptr": 18446738026471032392, "share_access": 6, "file_attributes": 0}))]) - def test_create_file_operation(self, expected_op, kfsio, fsio, kevent): - - fsio.parse_fsio(CREATE_FILE, kfsio) - - kparams = kevent.params - assert kparams.file == kfsio.open_path - assert kparams.tid == kfsio.ttid - assert kparams.pid == kfsio.process_id - assert kparams.operation == expected_op.name - - @pytest.mark.parametrize('expected_share_mask, kfsio', - [('r--', dd({"file_object": 18446738026482168384, "ttid": 1484, "create_options": 18874368, - "open_path": "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll", - "process_id": 859, - "irp_ptr": 18446738026471032392, "share_access": 1, "file_attributes": 0})), - ('-w-', dd({"file_object": 18446738026482168384, "ttid": 1484, "create_options": 18874368, - "open_path": "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll", - "process_id": 859, - "irp_ptr": 18446738026471032392, "share_access": 2, "file_attributes": 0})), - ('--d', dd({"file_object": 18446738026482168384, "ttid": 1484, "create_options": 18874368, - "open_path": "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll", - "process_id": 859, - "irp_ptr": 18446738026471032392, "share_access": 4, "file_attributes": 0})), - ('rw-', dd({"file_object": 18446738026482168384, "ttid": 1484, "create_options": 18874368, - "open_path": "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll", - "process_id": 859, - "irp_ptr": 18446738026471032392, "share_access": 3, "file_attributes": 0})), - ('r-d', dd({"file_object": 18446738026482168384, "ttid": 1484, "create_options": 18874368, - "open_path": "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll", - "process_id": 859, - "irp_ptr": 18446738026471032392, "share_access": 5, "file_attributes": 0})), - ('-wd', dd({"file_object": 18446738026482168384, "ttid": 1484, "create_options": 18874368, - "open_path": "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll", - "process_id": 859, - "irp_ptr": 18446738026471032392, "share_access": 6, "file_attributes": 0})), - ('rwd', dd({"file_object": 18446738026482168384, "ttid": 1484, "create_options": 18874368, - "open_path": "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll", - "process_id": 859, - "irp_ptr": 18446738026471032392, "share_access": 7, "file_attributes": 0})), - ('---', dd({"file_object": 18446738026482168384, "ttid": 1484, "create_options": 18874368, - "open_path": "\\Device\\HarddiskVolume2\\Windows\\system32\\kernel32.dll", - "process_id": 859, - "irp_ptr": 18446738026471032392, "share_access": -1, "file_attributes": 0}))]) - def test_create_file_share_mask(self, expected_share_mask, kfsio, fsio, kevent): - fsio.parse_fsio(CREATE_FILE, kfsio) - assert kevent.params.share_mask == expected_share_mask - - def test_delete_file(self, fsio, kevent): - kfsio = dd({"file_object": 18446738026474426144, "ttid": 1956, "process_id": 859, "irp_ptr": 18446738026471032392}) - fsio.parse_fsio(DELETE_FILE, kfsio) - assert kevent.params.tid == kfsio.ttid - assert kevent.params.file == '\\Device\\HarddiskVolume2\\fibratus.log' - - def test_write_file(self, fsio, kevent): - kfsio = dd({"file_object": 18446738026474426144, "process_id": 859, "io_flags": 0, "io_size": 8296, - "offset": 75279, "ttid": 1956}) - fsio.parse_fsio(WRITE_FILE, kfsio) - assert kevent.params.tid == kfsio.ttid - assert kevent.params.file == NA - assert kevent.params.io_size == kfsio.io_size / 1024 - - def test_rename_file(self, fsio, kevent): - kfsio = dd({"file_object": 18446738023471035392, "ttid": 1956, "process_id": 859, "irp_ptr": 18446738026471032392}) - fsio.parse_fsio(RENAME_FILE, kfsio) - assert kevent.params.tid == kfsio.ttid - assert kevent.params.file == '\\Device\\HarddiskVolume2\\Windows\\system32\\rpcss.dll' - - def test_set_file_information(self, fsio, kevent): - kfsio = dd( - {"file_object": 18446738023471035392, "ttid": 1956, "info_class": 20, "process_id": 859, - "irp_ptr": 18446738026471032392}) - fsio.parse_fsio(SET_FILE_INFORMATION, kfsio) - assert kevent.params.tid == kfsio.ttid - assert kevent.params.info_class == 20 - assert kevent.params.file == '\\Device\\HarddiskVolume2\\Windows\\system32\\rpcss.dll' diff --git a/tests/unit/handle.py b/tests/unit/handle.py deleted file mode 100644 index 2d56bf198..000000000 --- a/tests/unit/handle.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2015/2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os -from unittest.mock import patch, call - -import pytest - -from fibratus.apidefs.process import PROCESS_DUP_HANDLE -from fibratus.handle import HandleRepository, HandleType -from fibratus.common import DotD as dd - - -def raw_handles(): - handles = {} - handles[18446738026470543136] = dd(obj=18446738026470543136, handle=12, pid=3472, access_mask=1048608, obj_type_index=28) - handles[18446738026474344592] = dd(obj=18446738026474344592, handle=16, pid=3472, access_mask=2031617, obj_type_index=36) - handles[18446738026474569824] = dd(obj=18446738026474569824, handle=204, pid=920, access_mask=1048578, obj_type_index=17) - handles[18446738026469227424] = dd(obj=18446738026469227424, handle=116, pid=920, access_mask=2031619, obj_type_index=12) - handles[18446735964891181152] = dd(obj=18446735964891181152, handle=108, pid=1616, access_mask=983551, obj_type_index=5) - return handles - - -def query_handle_side_effects(): - return [dd(contents=dd(type_name=dd(buffer="FILE"))), dd(contents=dd(buffer="\Device\HarddiskVolume2"))] \ - * len(raw_handles()) - - -@pytest.fixture(scope='module') -def handle_repo(): - return HandleRepository() - - -class TestHandleRepository(): - - def test_init_handle_repository(self, handle_repo): - handle_types = handle_repo._handle_types - assert isinstance(handle_types, list) - assert HandleType.FILE.name in handle_types - - @patch('fibratus.handle.HandleRepository._enum_handles', return_value=raw_handles()) - @patch('fibratus.handle.open_process', side_effect=[None, 200, 301, 120, 343]) - @patch('fibratus.handle.duplicate_handle', side_effect=[1, 1, 1, 0]) - @patch('fibratus.handle.HandleRepository._query_handle', side_effect=query_handle_side_effects()) - @patch('fibratus.handle.close_handle') - @patch('fibratus.handle.get_current_process', return_value=os.getpid()) - @patch('fibratus.handle.cast', return_value=dd(value='FILE')) - @patch('fibratus.handle.byref', return_value=0x100) - def test_query_handles(self, byref_mock, cast_mock, get_current_process_mock, close_handle_mock, - query_handle_mock, duplicate_handle_mock, open_process_mock, - enum_handles_mock, handle_repo): - handles = handle_repo.query_handles() - assert enum_handles_mock.called - assert get_current_process_mock.called - - open_process_expected_calls = [call(PROCESS_DUP_HANDLE, False, 3472), call(PROCESS_DUP_HANDLE, False, 3472), - call(PROCESS_DUP_HANDLE, False, 920), - call(PROCESS_DUP_HANDLE, False, 920), - call(PROCESS_DUP_HANDLE, False, 1616)] - open_process_mock.assert_has_calls(open_process_expected_calls, any_order=True) - - assert duplicate_handle_mock.call_count == 4 - assert query_handle_mock.call_count == 6 - assert close_handle_mock.call_count == 7 - - assert len(handles) == 3 - assert len([h for h in handles if h.handle_type == HandleType.FILE]) > 0 \ No newline at end of file diff --git a/tests/unit/image_meta.py b/tests/unit/image_meta.py deleted file mode 100644 index c01dc3a51..000000000 --- a/tests/unit/image_meta.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from unittest import mock -import os -import pytest - -from fibratus.image_meta import ImageMetaRegistry - - -@pytest.fixture(scope='module') -def image_meta_registry(): - image_meta_registry = ImageMetaRegistry(imports=True, file_info=True) - return image_meta_registry - - -@pytest.fixture(scope='module') -def image_path(): - return '%s\\notepad.exe' % os.environ['WINDIR'] - - -class TestImageMetaRegistry(object): - - @pytest.mark.skip(reason="failing on appveyor") - def test_add_image_meta(self, image_meta_registry, image_path): - image_meta_registry.add_image_meta(image_path) - image_meta = image_meta_registry.get_image_meta(image_path) - assert image_meta - - assert image_meta.arch - assert image_meta.timestamp - assert image_meta.num_sections > 0 - - assert len(image_meta.sections) > 0 - section_names = [se['name'] for se in image_meta.sections] - - assert '.text' in section_names - - assert len(image_meta.imports) > 0 - assert 'KERNEL32.dll' in image_meta.imports - - assert 'Microsoft Corporation' in image_meta.org - assert image_meta.description - assert image_meta.version - assert 'Notepad' in image_meta.internal_name - assert image_meta.copyright diff --git a/tests/unit/kevent.py b/tests/unit/kevent.py deleted file mode 100644 index e835acd31..000000000 --- a/tests/unit/kevent.py +++ /dev/null @@ -1,113 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from unittest.mock import Mock, patch - -import pytest -from fibratus.apidefs.process import THREAD_QUERY_INFORMATION -from fibratus.common import DotD - -from fibratus.kevent import KEvent, KEvents, Category -from fibratus.thread import ThreadRegistry, ThreadInfo - - -@pytest.fixture(scope='module') -def thread_registry_mock(): - thread_registry = Mock(spec_set=ThreadRegistry) - thread_registry.get_thread.side_effect = [ThreadInfo(436, 1024, 23, 'svchost.exe', - 'C:\\Windows\\system32\\svchost.exe -k RPCSS'), - None, - None, - ThreadInfo(836, 564, 23, 'svchost.exe', - 'C:\\Windows\\system32\\svchost.exe -k RPCSS'), - None, - ThreadInfo(836, 564, 23, 'svchost.exe', - 'C:\\Windows\\system32\\svchost.exe -k RPCSS'), - None, None] - return thread_registry - - -@pytest.fixture() -def kevent(thread_registry_mock): - return KEvent(thread_registry_mock) - - -class TestKEvent(object): - - def test_get_thread_pid_not_none(self, kevent, thread_registry_mock): - kevent.pid = 436 - assert kevent.thread - thread_registry_mock.get_thread.assert_called_with(kevent.pid) - - def test_get_thread_pid_not_none_and_not_found_in_registry(self, kevent, thread_registry_mock): - kevent.pid = 436 - assert kevent.thread is None - thread_registry_mock.get_thread.assert_called_with(kevent.pid) - - def test_get_thread_pid_not_none_find_by_thread(self, kevent, thread_registry_mock): - kevent.pid = 436 - kevent.tid = 564 - assert kevent.thread - thread_registry_mock.get_thread.assert_called_with(kevent.tid) - - def test_get_thread_pid_none(self, kevent, thread_registry_mock): - kevent.tid = 564 - assert kevent.thread - thread_registry_mock.get_thread.assert_called_with(kevent.tid) - - @patch('fibratus.kevent.open_thread', return_value=25) - @patch('fibratus.kevent.get_process_id_of_thread') - @patch('fibratus.kevent.close_handle') - def test_get_thread_pid_none(self, close_handle_mock, get_process_id_of_thread_mock, open_thread_mock, - kevent): - kevent.tid = 245 - kevent.get_thread() - open_thread_mock.assert_called_with(THREAD_QUERY_INFORMATION, - False, kevent.tid) - get_process_id_of_thread_mock.assert_called_with(25) - close_handle_mock.assert_called_with(25) - - def test_kevents_all(self): - kevents = KEvents.all() - assert isinstance(kevents, list) - assert len(kevents) > 0 - - def test_kevents_meta_info(self): - kevents_meta_info = KEvents.meta_info() - assert isinstance(kevents_meta_info, dict) - cat, description = kevents_meta_info[KEvents.CREATE_PROCESS] - assert cat == Category.PROCESS - assert description - - def test_set_kevent_name(self, kevent): - kevent.name = KEvents.CREATE_PROCESS - assert kevent.name == KEvents.CREATE_PROCESS - assert kevent.category == Category.PROCESS.name - - def test_set_kevent_ts(self, kevent): - kevent.ts = '2016-11-29 12:31:48.210000' - assert kevent.ts.second == 48 - assert kevent.ts.minute == 31 - assert kevent.ts.hour == 12 - - def test_set_kevent_cpu_id(self, kevent): - kevent.cpuid = 2 - assert kevent.cpuid == 2 - - def test_set_kevent_params(self, kevent): - kevent.params = {'exe': 'svchost.exe', 'comm': 'C:\\Windows\\system32\\svchost.exe -k RPCSS'} - assert isinstance(kevent.params, DotD) - assert kevent.params.exe == 'svchost.exe' - diff --git a/tests/unit/kevent_types.py b/tests/unit/kevent_types.py deleted file mode 100644 index 28d11b1e8..000000000 --- a/tests/unit/kevent_types.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2015/2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import pytest -from fibratus.kevent_types import * - - -@pytest.mark.parametrize('name, expected_kevent', - [(KEvents.CREATE_PROCESS, CREATE_PROCESS), - (KEvents.TERMINATE_PROCESS, TERMINATE_PROCESS), - (KEvents.CREATE_THREAD, CREATE_THREAD), - (KEvents.TERMINATE_THREAD, TERMINATE_THREAD), - (KEvents.REG_SET_VALUE, REG_SET_VALUE), - (KEvents.REG_QUERY_KEY, REG_QUERY_KEY), - (KEvents.REG_OPEN_KEY, REG_OPEN_KEY), - (KEvents.REG_QUERY_VALUE, REG_QUERY_VALUE), - (KEvents.REG_DELETE_KEY, REG_DELETE_KEY), - (KEvents.REG_DELETE_VALUE, REG_DELETE_VALUE), - (KEvents.REG_CREATE_KEY, REG_CREATE_KEY), - (KEvents.SEND, [SEND_SOCKET_UDPV4, SEND_SOCKET_TCPV4])]) -def test_kname_to_tuple(name, expected_kevent): - assert kname_to_tuple(name) == expected_kevent - - -def test_kname_to_tuple_unknown_kevent(): - with pytest.raises(UnknownKeventTypeError): - kname_to_tuple('Exec') - - -def test_ktuple_to_name(): - pass \ No newline at end of file diff --git a/tests/unit/output/__init__.py b/tests/unit/output/__init__.py deleted file mode 100644 index b4b9e2a2a..000000000 --- a/tests/unit/output/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. \ No newline at end of file diff --git a/tests/unit/output/aggregator.py b/tests/unit/output/aggregator.py deleted file mode 100644 index d50763fc9..000000000 --- a/tests/unit/output/aggregator.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from unittest.mock import Mock - -import pytest - -from fibratus.kevent import KEvent -from fibratus.output.aggregator import OutputAggregator -from fibratus.output.amqp import AmqpOutput -from fibratus.output.console import ConsoleOutput - - -@pytest.fixture(scope='module') -def outputs(): - return dict(amqp=Mock(spec_set=AmqpOutput), console=Mock(spec_set=ConsoleOutput)) - - -@pytest.fixture(scope='module') -def output_aggregator(outputs): - return OutputAggregator(outputs) - - -@pytest.fixture(scope='module') -def kevent(): - kevent_mock = Mock(spec_set=KEvent) - kevent_mock.get_thread.return_value = (343, 'svchost.exe') - return kevent_mock - - -class TestOutputAggregator(object): - - def test_aggregate(self, output_aggregator, kevent, outputs): - output_aggregator.aggregate(kevent) - for _, output in outputs.items(): - assert output.emit.call_count == 1 - diff --git a/tests/unit/output/amqp.py b/tests/unit/output/amqp.py deleted file mode 100644 index 8868dab1a..000000000 --- a/tests/unit/output/amqp.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -import json -from unittest.mock import patch - -import pika -import pytest - -from fibratus.errors import InvalidPayloadError -from fibratus.output.amqp import AmqpOutput - - -@pytest.fixture(scope='module') -def amqp_adapter(): - config = { - 'username': 'fibratus', - 'host': '127.0.0.1', - 'port': 5672, - 'vhost': '/', - 'exchange': 'test', - 'routingkey': 'fibratus' - } - return AmqpOutput(**config) - - -class TestAmqpOutput(object): - - def test_init(self, amqp_adapter): - assert 'fibratus' in amqp_adapter.username - assert 'guest' in amqp_adapter._password - assert '127.0.0.1' in amqp_adapter.host - assert amqp_adapter.port == 5672 - assert '/' in amqp_adapter.vhost - assert 'test' in amqp_adapter.exchange - assert 'fibratus' in amqp_adapter.routingkey - assert amqp_adapter.delivery_mode == 1 - - @patch('pika.BlockingConnection', spec_set=pika.BlockingConnection) - def test_emit(self, connection_mock, amqp_adapter): - body = {'kevent_type': 'CreateProcess'} - amqp_adapter.emit(body) - connection_mock.channel.assert_called_once() - amqp_adapter._channel.basic_publish.assert_called_with('test', 'fibratus', - json.dumps(body), - amqp_adapter._basic_props) - - @patch('pika.BlockingConnection', spec_set=pika.BlockingConnection) - def test_emit_invalid_payload(self, connection_mock, amqp_adapter): - body = ['CrateProcess', 'TerminateProcess'] - with pytest.raises(InvalidPayloadError) as e: - connection_mock.channel.assert_called_once() - amqp_adapter.emit(body) - assert "invalid payload for AMQP message. dict expected but found" == str(e.value) - amqp_adapter._channel.basic_publish.assert_not_called() - - @patch('pika.BlockingConnection', spec_set=pika.BlockingConnection) - def test_emit_override_exchange_and_rk(self, connection_mock, amqp_adapter): - body = {'kevent_type': 'CreateProcess'} - amqp_adapter.emit(body, exchange='test.override', routingkey='fibratus.override') - amqp_adapter._channel.basic_publish.assert_called_with('test.override', 'fibratus.override', - json.dumps(body), - amqp_adapter._basic_props) - - @pytest.mark.parametrize('body', [{'kevent_type': 'CreateProcess'}, {'kevent_type': 'TerminateProcess'}, - {'kevent_type': 'WriteFile'}, {'kevent_type': 'Recv'}]) - @patch('pika.BlockingConnection', spec_set=pika.BlockingConnection) - def test_emit_multiple(self, connection_mock, body, amqp_adapter): - amqp_adapter.emit(body, exchange='test.override', routingkey='fibratus.override') - connection_mock.channel.assert_called_once() - amqp_adapter._channel.basic_publish.assert_called_with('test.override', 'fibratus.override', - json.dumps(body), - amqp_adapter._basic_props) diff --git a/tests/unit/output/elasticsearch.py b/tests/unit/output/elasticsearch.py deleted file mode 100644 index a774cb542..000000000 --- a/tests/unit/output/elasticsearch.py +++ /dev/null @@ -1,137 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from unittest.mock import patch -from datetime import datetime - -import time -import elasticsearch -import pytest -from unittest.mock import Mock - -from fibratus.errors import InvalidPayloadError -from fibratus.output.elasticsearch import ElasticsearchOutput - - -@pytest.fixture(scope='module') -def elasticsearch_adapter(): - config = { - 'hosts': [ - 'localhost:9200', - 'rabbitstack:9200' - ], - 'index': 'kernelstream', - 'document': 'threads', - } - return ElasticsearchOutput(**config) - - -@pytest.fixture(scope='module') -def elasticsearch_bulk_adapter(): - config = { - 'hosts': [ - 'localhost:9200', - 'rabbitstack:9200' - ], - 'index': 'kernelstream', - 'document': 'threads', - 'bulk': True, - 'username': 'elastic', - 'password': 'changeme' - } - return ElasticsearchOutput(**config) - - -@pytest.fixture(scope='module') -def elasticsearch_adapter_daily_index(): - config = { - 'hosts': [ - 'localhost:9200', - 'rabbitstack:9200' - ], - 'index': 'kernelstream', - 'document': 'threads', - 'index_type': 'daily' - } - return ElasticsearchOutput(**config) - - -mock_time = Mock() -mock_time.return_value = time.mktime(datetime(2017, 12, 16).timetuple()) - - -class TestElasticsearchOutput(object): - - def test_init(self, elasticsearch_adapter): - assert isinstance(elasticsearch_adapter.hosts, list) - assert len(elasticsearch_adapter.hosts) > 0 - assert {'host': 'localhost', 'port': 9200} in elasticsearch_adapter.hosts - assert elasticsearch_adapter.index_name == 'kernelstream' - assert elasticsearch_adapter.document_type == 'threads' - assert not elasticsearch_adapter.bulk - - def test_emit(self, elasticsearch_adapter): - body = {'kevent_type': 'CreateProcess', 'params': {'name': 'smss.exe'}} - assert elasticsearch_adapter._elasticsearch is None - with patch('elasticsearch.Elasticsearch', spec_set=elasticsearch.Elasticsearch) as es_client_mock: - elasticsearch_adapter.emit(body) - es_client_mock.assert_called_with([{'host': 'localhost', 'port': 9200}, - {'host': 'rabbitstack', 'port': 9200}], use_ssl=False) - elasticsearch_adapter._elasticsearch.index.assert_called_with('kernelstream', 'threads', body=body) - - @patch('time.time', mock_time) - def test_emit_daily_index(self, elasticsearch_adapter_daily_index): - body = {'kevent_type': 'CreateProcess', 'params': {'name': 'smss.exe'}} - assert elasticsearch_adapter_daily_index._elasticsearch is None - assert elasticsearch_adapter_daily_index.index_type == 'daily' - with patch('elasticsearch.Elasticsearch', spec_set=elasticsearch.Elasticsearch) as es_client_mock: - elasticsearch_adapter_daily_index.emit(body) - es_client_mock.assert_called_with([{'host': 'localhost', 'port': 9200}, - {'host': 'rabbitstack', 'port': 9200}], use_ssl=False) - #elasticsearch_adapter_daily_index._elasticsearch.index.assert_called_with('kernelstream-2017.12.16', 'threads', body=body) - - @patch('elasticsearch.Elasticsearch', spec_set=elasticsearch.Elasticsearch) - def test_emit_invalid_payload(self, es_client_mock, elasticsearch_adapter): - body = ['CreateProcess', 'TerminateProcess'] - with pytest.raises(InvalidPayloadError) as e: - elasticsearch_adapter.emit(body) - assert "invalid payload for document. dict expected but found" == str(e.value) - assert es_client_mock.index.assert_not_called() - - @patch('elasticsearch.Elasticsearch', spec_set=elasticsearch.Elasticsearch) - @patch('elasticsearch.helpers.bulk') - def test_emit_bulk(self, es_bulk_mock, es_client_mock, elasticsearch_bulk_adapter): - body = [{'kevent_type': 'CreateProcess', 'params': {'name': 'smss.exe'}}, - {'kevent_type': 'TerminateProcess', 'params': {'name': 'smss.exe'}}] - elasticsearch_bulk_adapter.emit(body) - es_client_mock.assert_called_with([{'host': 'localhost', 'port': 9200}, - {'host': 'rabbitstack', 'port': 9200}], use_ssl=False, - http_auth=('elastic', 'changeme',)) - expected_body = [{'_index': 'kernelstream', '_type': 'threads', '_source': - {'kevent_type': 'CreateProcess', 'params': {'name': 'smss.exe'}}}, - {'_index': 'kernelstream', '_type': 'threads', '_source': - {'kevent_type': 'TerminateProcess', 'params': {'name': 'smss.exe'}}}] - es_bulk_mock.assert_called_once_with(elasticsearch_bulk_adapter._elasticsearch, expected_body) - - @patch('elasticsearch.Elasticsearch', spec_set=elasticsearch.Elasticsearch) - @patch('elasticsearch.helpers.bulk') - def test_emit_bulk_invalid_payload(self, es_bulk_mock, es_client_mock, elasticsearch_bulk_adapter): - body = ({'kevent_type': 'CreateProcess', 'params': {'name': 'smss.exe'}}, - {'kevent_type': 'TerminateProcess', 'params': {'name': 'smss.exe'}},) - with pytest.raises(InvalidPayloadError) as e: - elasticsearch_bulk_adapter.emit(body) - assert "invalid payload for bulk indexing. list expected but found" == str(e.value) - assert es_bulk_mock.assert_not_called() - - diff --git a/tests/unit/output/fs.py b/tests/unit/output/fs.py deleted file mode 100644 index dfb59462f..000000000 --- a/tests/unit/output/fs.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2017 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from io import TextIOBase - -from fibratus.output.fs import FsOutput -from unittest.mock import patch, Mock -import time -import os -import json - - -class TestFsOutput(object): - - def test_init(self): - with patch('io.open', return_value=Mock(spec_set=TextIOBase)) as stream_mock: - fs_output = FsOutput(path='C:\\', mode='a', format='json') - filename = os.path.join(fs_output.path, '%s.fibra' % time.strftime('%x').replace('/', '-')) - stream_mock.assert_called_with(filename, 'a') - assert 'C:\\' in fs_output.path - assert 'a' in fs_output.mode - assert 'json' in fs_output.format - - def test_emit(self): - with patch('io.open', return_value=Mock(spec_set=TextIOBase)): - fs_output = FsOutput(path='C:\\', mode='a', format='json') - body = {'kevent_type': 'CreateProcess'} - fs_output.emit(body) - fs_output.stream.write.assert_called_with(json.dumps(body) + '\n') \ No newline at end of file diff --git a/tests/unit/output/smtp.py b/tests/unit/output/smtp.py deleted file mode 100644 index 6b3342788..000000000 --- a/tests/unit/output/smtp.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -import smtplib -from unittest.mock import patch, Mock - -import pytest -from logbook import Logger - -from fibratus.output.smtp import SmtpOutput - - -@pytest.fixture(scope='module') -def smtp_adapter(): - config = { - 'host': 'smtp.gmail.com', - 'from': 'fibratus@fibratus.io', - 'password': 'secret', - 'to': ['fibratus@fibratus.io', 'netmutatus@netmutatus.io'] - } - return SmtpOutput(**config) - - -class TestSmtpOutput(object): - - def test_init(self, smtp_adapter): - assert 'smtp.gmail.com' in smtp_adapter.host - assert 'fibratus@fibratus.io' in smtp_adapter.sender - assert set(['fibratus@fibratus.io', 'netmutatus@netmutatus.io']) == set(smtp_adapter.to) - assert smtp_adapter.port == 587 - - def test_emit(self, smtp_adapter): - body = 'Anomalous network activity detected from notepad.exe process' - with patch('smtplib.SMTP'): - smtp_adapter.emit(body, subject='Anomalous network activity detected') - assert smtp_adapter._smtp.ehlo.call_count == 2 - smtp_adapter._smtp.starttls.assert_called_once() - smtp_adapter._smtp.login.assert_called_with('fibratus@fibratus.io', 'secret') - message = 'From: fibratus@fibratus.io' \ - 'To: fibratus@fibratus.io, netmutatus@netmutatus.io' \ - 'Subject: Anomalous network activity detected' \ - 'Anomalous network activity detected from notepad.exe process' - - smtp_adapter._smtp.login.sendmail('fibratus@fibratus.io', ['fibratus@fibratus.io', - 'netmutatus@netmutatus.io'], - message) - smtp_adapter._smtp.quit.assert_called_once() - - def test_emit_invalid_credentials(self, smtp_adapter): - body = 'Anomalous network activity detected from notpead.exe process' - smtp_adapter.logger = Mock(spec_set=Logger) - with patch('smtplib.SMTP'): - smtp_adapter._smtp.login.side_effect = smtplib.SMTPAuthenticationError(534, 'Invalid smtp credentials') - smtp_adapter.emit(body, subject='Anomalous network activity detected') - smtp_adapter.logger.error.assert_called_with('Invalid SMTP credentials for ' - 'fibratus@fibratus.io account') - smtp_adapter._smtp.quit.assert_called_once() diff --git a/tests/unit/registry.py b/tests/unit/registry.py deleted file mode 100644 index 823efa8e0..000000000 --- a/tests/unit/registry.py +++ /dev/null @@ -1,199 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from unittest.mock import Mock, patch - -import pytest - -from fibratus.apidefs.registry import HKEY_LOCAL_MACHINE, HKEY_USERS -from fibratus.common import DotD as dd, NA -from fibratus.handle import HandleInfo, HandleType -from fibratus.kevent import KEvent -from fibratus.kevent_types import * -from fibratus.registry import HiveParser, Kcb -from fibratus.thread import ThreadRegistry, ThreadInfo - - -@pytest.fixture(scope='module') -def kevent_mock(): - return Mock(spec_set=KEvent) - - -@pytest.fixture(scope='module') -def thread_registry_mock(): - thread_registry = Mock(spec_set=ThreadRegistry) - thread_info = ThreadInfo(896, 2916, 22, 'explorer.exe', 'C:\\Windows\\EXPLORER.exe') - thread_info.handles.append(HandleInfo(836, 18446735964859105184, HandleType.KEY, - "\\REGISTRY\\USER\\S-1-5-21-2945379629-2233710143-2353048178-1000_CLASSES\\Local Settings" - "\\Software\Microsoft\\Windows\\Shell\\Bags\\59\\Shell\\{5C4F28B5-F869-4E84-8E60-F11DB97C5CC7}", - 896)) - thread_registry.get_thread.return_value = thread_info - return thread_registry - - -@pytest.fixture(scope='module') -def hive_parser(kevent_mock, thread_registry_mock): - kcb1 = dd({"index": 0, "process_id": 1224, "status": 0, "key_handle": 18446735964840821928, - "key_name": "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\MountPoints2\\" - "{4a33e644-94a8-11e5-a0c5-806e6f6e6963}\\", - "thread_id": 1484, "initial_time": 24218562806}) - kcb2 = dd({'initial_time': 0, 'index': 0, 'thread_id': 620, 'status': 0, 'key_handle': 18446735964896987168, - 'key_name': '\\REGISTRY\\MACHINE\\SYSTEM\\ControlSet001\\services\\HDAudBus', 'process_id': 3820}) - kcb3 = dd({"index": 0, "process_id": 896, "status": 0, "key_handle": 18446735964812642928, - "key_name": "\\REGISTRY\\MACHINE\\SOFTWARE\\Classes\\CLSID\\{D2D588B5-D081-11D0-99E0-00C04FC2F8EC}" - "\\InprocServer32", "thread_id": 2916, "initial_time": 0}) - hive_parser = HiveParser(kevent_mock, thread_registry_mock) - hive_parser.add_kcb(kcb1) - hive_parser.add_kcb(kcb2) - hive_parser.add_kcb(kcb3) - return hive_parser - - -class TestHiveParser(): - - def test_add_kcb(self, hive_parser): - kcb = dd({'initial_time': 0, 'index': 0, 'thread_id': 620, - 'status': 0, 'key_handle': 18446735964879434920, - 'key_name': '\\REGISTRY\\MACHINE\\SOFTWARE\\Microsoft\\WBEM\\WDM', 'process_id': 3820}) - hive_parser.add_kcb(kcb) - assert kcb.key_handle in hive_parser.kcblocks - - kcblock = hive_parser.kcblocks[kcb.key_handle] - assert isinstance(kcblock, Kcb) - assert kcblock.key == kcb.key_name - - def test_remove_kcb(self, hive_parser): - kcb = dd({'initial_time': 0, 'index': 0, 'thread_id': 620, - 'status': 0, 'key_handle': 18446735964879434920, - 'key_name': '\\REGISTRY\\MACHINE\\SOFTWARE\\Microsoft\\WBEM\\WDM', 'process_id': 3820}) - hive_parser.remove_kcb(kcb.key_handle) - assert kcb.key_handle not in hive_parser.kcblocks - - @pytest.mark.parametrize('kevent_type', [REG_OPEN_KEY, REG_CREATE_KEY, REG_QUERY_KEY, REG_DELETE_KEY]) - def test_parse_hive_key_kevent_full_node_name(self, kevent_type, hive_parser, kevent_mock): - regkevt = dd({"index": 0, "process_id": 1224, "status": 3221225524, "key_handle": 0, - "key_name": "\\Registry\\Machine\\Software\\Classes\\Applications\\Explorer.exe" - "\\Drives\\C\\DefaultIcon", "thread_id": 2164, "initial_time": 24218563385}) - hive_parser.parse_hive(kevent_type, regkevt) - kparams = kevent_mock.params - - assert kparams['hive'] == 'REGISTRY_MACHINE_SOFTWARE' - assert kparams['key'] == 'SOFTWARE\\Classes\\Applications\\Explorer.exe\\Drives\\C\\DefaultIcon' - assert kparams['status'] == regkevt.status - assert kparams['pid'] == regkevt.process_id - assert kparams['tid'] == regkevt.thread_id - - @pytest.mark.parametrize('kevent_type', [REG_OPEN_KEY, REG_CREATE_KEY, REG_QUERY_KEY, REG_DELETE_KEY]) - def test_parse_hive_key_kevent_kcb_lookup_match(self, kevent_type, hive_parser, kevent_mock): - regkevt = dd({"index": 2, "process_id": 896, "status": 0, "key_handle": 18446735964812642928, - "key_name": "ThreadingModel", "thread_id": 2916, "initial_time": 24219715376}) - hive_parser.parse_hive(kevent_type, regkevt) - kparams = kevent_mock.params - - assert kparams['hive'] == 'REGISTRY_MACHINE_SOFTWARE' - assert kparams['key'] == 'SOFTWARE\\Classes\\CLSID\\{D2D588B5-D081-11D0-99E0-00C04FC2F8EC}' \ - '\\InprocServer32\\ThreadingModel' - assert kparams['status'] == regkevt.status - assert kparams['pid'] == regkevt.process_id - assert kparams['tid'] == regkevt.thread_id - - def test_parse_hive_key_handles_lookup(self, hive_parser, kevent_mock, thread_registry_mock): - regkevt = dd({"index": 0, "process_id": 896, "status": 3221225524, - "key_handle": 18446735964819421216, - "key_name": "Bags\\59\\Shell\\{5C4F28B5-F869-4E84-8E60-F11DB97C5CC7}", - "thread_id": 2916, "initial_time": 24219715717}) - hive_parser.parse_hive(REG_OPEN_KEY, regkevt) - - thread_registry_mock.get_thread.assert_called_with(regkevt.process_id) - - kcb = hive_parser.kcblocks[regkevt.key_handle] - assert kcb and isinstance(kcb, Kcb) - - assert kcb.key == "\\REGISTRY\\USER\\S-1-5-21-2945379629-2233710143-2353048178-1000_CLASSES\\Local Settings" \ - "\\Software\Microsoft\\Windows\\Shell\\Bags\\59" \ - "\\Shell\\{5C4F28B5-F869-4E84-8E60-F11DB97C5CC7}" - assert kevent_mock.params['hive'] == 'REGISTRY_USER_S-1-5-21-2945379629-2233710143-2353048178-1000_CLASSES' - assert kevent_mock.params['key'] == "S-1-5-21-2945379629-2233710143-2353048178-1000_CLASSES\\Local Settings" \ - "\\Software\Microsoft\\Windows\\Shell\\Bags\\59" \ - "\\Shell\\{5C4F28B5-F869-4E84-8E60-F11DB97C5CC7}" - - def test_parse_hive_set_value_kevent_full_node_name(self, hive_parser, kevent_mock): - regkevt = dd({"index": 0, "process_id": 1224, "status": 3221225524, "key_handle": 0, - "key_name": "\\Registry\\Machine\\Software\\Classes\\Applications\\Explorer.exe" - "\\Drives\\C\\DefaultIcon", "thread_id": 2164, "initial_time": 24218563385}) - with patch('fibratus.registry.HiveParser._query_value', return_value=('open', 'REG_SZ')) \ - as query_value_mock: - hive_parser.parse_hive(REG_SET_VALUE, regkevt) - kparams = kevent_mock.params - query_value_mock.assert_called_with(HKEY_LOCAL_MACHINE, - 'SOFTWARE\\Classes\\Applications\\Explorer.exe\\Drives\\C', - 'DefaultIcon') - - assert kparams['hive'] == 'REGISTRY_MACHINE_SOFTWARE' - assert kparams['key'] == 'SOFTWARE\\Classes\\Applications\\Explorer.exe\\Drives\\C\\DefaultIcon' - assert kparams['status'] == regkevt.status - assert kparams['pid'] == regkevt.process_id - assert kparams['tid'] == regkevt.thread_id - assert kparams['value_type'] == 'REG_SZ' - assert kparams['value'] == 'open' - - def test_parse_hive_set_value_kevent_full_node_name(self, hive_parser, kevent_mock): - regkevt = dd({"index": 0, "process_id": 1224, "status": 3221225524, "key_handle": 0, - "key_name": "\\REGISTRY\\USER\\S-1-5-21-2945379629-2233710143-2353048178-1000_CLASSES" - "\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion\\TrayNotify\\IconStreams", - "thread_id": 2164, "initial_time": 24218563385}) - with patch('fibratus.registry.HiveParser._query_value', return_value=('0x12', 'REG_DWORD')) \ - as query_value_mock: - hive_parser.parse_hive(REG_QUERY_VALUE, regkevt) - kparams = kevent_mock.params - query_value_mock.assert_called_with(HKEY_USERS, - "S-1-5-21-2945379629-2233710143-2353048178-1000_CLASSES" - "\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion" - "\\TrayNotify", - 'IconStreams') - assert kparams['hive'] == 'REGISTRY_USER_S-1-5-21-2945379629-2233710143-2353048178-1000_CLASSES' - assert kparams['key'] == "S-1-5-21-2945379629-2233710143-2353048178-1000_CLASSES\\Local Settings" \ - "\\Software\\Microsoft\\Windows\\CurrentVersion\\TrayNotify\\IconStreams" - assert kparams['status'] == regkevt.status - assert kparams['pid'] == regkevt.process_id - assert kparams['tid'] == regkevt.thread_id - assert kparams['value_type'] == 'REG_DWORD' - assert kparams['value'] == '0x12' - - def test_parse_hive_delete_value(self, hive_parser, kevent_mock): - regkevt = dd({"index": 0, "process_id": 1224, "status": 3221225524, "key_handle": 0, - "key_name": "\\Registry\\Machine\\Software\\Classes\\Applications\\Explorer.exe" - "\\Drives\\C\\DefaultIcon", "thread_id": 2164, "initial_time": 24218563385}) - - hive_parser.parse_hive(REG_DELETE_VALUE, regkevt) - - kparams = kevent_mock.params - - assert kparams['hive'] == 'REGISTRY_MACHINE_SOFTWARE' - assert kparams['key'] == 'SOFTWARE\\Classes\\Applications\\Explorer.exe\\Drives\\C\\DefaultIcon' - assert kparams['status'] == regkevt.status - assert kparams['pid'] == regkevt.process_id - assert kparams['tid'] == regkevt.thread_id - - def test_parse_hive_na(self, hive_parser, kevent_mock): - regkevt = dd({"index": 2, "process_id": 896, "status": 0, "key_handle": 19446735964812642920, - "key_name": "ThreadingModel", "thread_id": 2916, "initial_time": 24219715376}) - hive_parser.parse_hive(REG_OPEN_KEY, regkevt) - kparams = kevent_mock.params - - assert kparams['hive'] == NA - assert kparams['key'] == '..\\ThreadingModel' - - diff --git a/tests/unit/tcpip/__init__.py b/tests/unit/tcpip/__init__.py deleted file mode 100644 index c41bcbd9f..000000000 --- a/tests/unit/tcpip/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2015/2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. diff --git a/tests/unit/tcpip/tcpip.py b/tests/unit/tcpip/tcpip.py deleted file mode 100644 index 89a6474af..000000000 --- a/tests/unit/tcpip/tcpip.py +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# http://rabbitstack.github.io - -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from unittest.mock import Mock - -import pytest - -from fibratus.common import DotD as dd -from fibratus.kevent import KEvent -from fibratus.kevent_types import SEND_SOCKET_TCPV4, SEND_SOCKET_UDPV4, RECV_SOCKET_TCPV4, RECV_SOCKET_UDPV4, \ - ACCEPT_SOCKET_TCPV4, CONNECT_SOCKET_TCPV4, DISCONNECT_SOCKET_TCPV4, RECONNECT_SOCKET_TCPV4 -from fibratus.tcpip.tcpip import TcpIpParser - - -@pytest.fixture(scope='module') -def kevent_mock(): - return Mock(spec_set=KEvent) - - -@pytest.fixture(scope='module') -def tcpip_parser(kevent_mock): - return TcpIpParser(kevent_mock) - - -class TestTcpIpParser(): - - @pytest.mark.parametrize('ktcpip, l4_proto, l5_proto, kevent_type', [ - (dd({"saddr": "10.0.2.15", "sport": 49279, "daddr": "216.58.211.238", - "dport": 443, "pid": 1848, "connid": 0, "size": 63, "seqnum": 0}), 'TCP', 'https', SEND_SOCKET_TCPV4), - (dd({"saddr": "10.0.2.15", "sport": 49279, "daddr": "216.58.211.238", - "dport": 53, "pid": 1848, "connid": 0, "size": 63, "seqnum": 0}), 'UDP', 'domain', SEND_SOCKET_UDPV4)]) - def test_parse_send(self, ktcpip, l4_proto, l5_proto, - kevent_type, tcpip_parser, - kevent_mock): - - tcpip_parser.parse_tcpip(kevent_type, ktcpip) - kparams = kevent_mock.params - - assert kparams['pid'] == ktcpip.pid - assert kparams['ip_src'] == ktcpip.saddr - assert kparams['ip_dst'] == ktcpip.daddr - assert kparams['sport'] == ktcpip.sport - assert kparams['dport'] == ktcpip.dport - assert kparams['packet_size'] == ktcpip.size - assert kparams['l4_proto'] == l4_proto - assert kparams['protocol'] == l5_proto - - @pytest.mark.parametrize('ktcpip, l4_proto, l5_proto, kevent_type', [ - (dd({'seqnum': 0, 'connid': 0, 'sport': 49720, 'daddr': '91.226.88.5', - 'saddr': '10.0.2.15', 'size': 266, 'pid': 1380, 'dport': 443}), 'TCP', 'https', RECV_SOCKET_TCPV4), - (dd({"saddr": "10.0.2.15", "sport": 49279, "daddr": "216.58.211.238", - "dport": 53, "pid": 1848, "connid": 0, "size": 63, "seqnum": 0}), 'UDP', 'domain', RECV_SOCKET_UDPV4)]) - def test_parse_recv(self, ktcpip, l4_proto, l5_proto, - kevent_type, tcpip_parser, - kevent_mock): - - tcpip_parser.parse_tcpip(kevent_type, ktcpip) - kparams = kevent_mock.params - - assert kparams['pid'] == ktcpip.pid - assert kparams['ip_src'] == ktcpip.saddr - assert kparams['ip_dst'] == ktcpip.daddr - assert kparams['sport'] == ktcpip.sport - assert kparams['dport'] == ktcpip.dport - assert kparams['packet_size'] == ktcpip.size - assert kparams['l4_proto'] == l4_proto - assert kparams['protocol'] == l5_proto - - def test_parse_recv_dport_na(self, tcpip_parser, kevent_mock): - ktcpip = dd({'seqnum': 0, 'connid': 0, 'sport': 25, 'daddr': '91.226.88.5', - 'saddr': '10.0.2.15', 'size': 266, 'pid': 1380, 'dport': 51234}) - tcpip_parser.parse_tcpip(RECV_SOCKET_TCPV4, ktcpip) - kparams = kevent_mock.params - assert kparams['protocol'] == 'smtp' - - def test_parse_accept(self, tcpip_parser, kevent_mock): - ktcpip = dd({'connid': 0, 'sndwinscale': 0, 'rcvwinscale': 0, 'saddr': '10.0.2.15', - 'sport': 22, 'rcvwin': 64240, 'tsopt': 0, 'pid': 1380, 'seqnum': 0, - 'daddr': '216.58.211.206', 'size': 0, 'mss': 1460, 'dport': 49804, 'wsopt': 0, 'sackopt': 0}) - - tcpip_parser.parse_tcpip(ACCEPT_SOCKET_TCPV4, ktcpip) - kparams = kevent_mock.params - - assert kparams['pid'] == ktcpip.pid - assert kparams['ip_src'] == ktcpip.saddr - assert kparams['ip_dst'] == ktcpip.daddr - assert kparams['sport'] == ktcpip.sport - assert kparams['dport'] == ktcpip.dport - assert kparams['rwin'] == ktcpip.rcvwin - assert kparams['protocol'] == 'ssh' - - def test_parse_connect(self, tcpip_parser, kevent_mock): - ktcpip = dd({'connid': 0, 'sndwinscale': 0, 'rcvwinscale': 0, 'saddr': '10.0.2.15', - 'sport': 49804, 'rcvwin': 64240, 'tsopt': 0, 'pid': 1380, 'seqnum': 0, - 'daddr': '216.58.211.206', 'size': 0, 'mss': 1460, 'dport': 443, 'wsopt': 0, 'sackopt': 0}) - - tcpip_parser.parse_tcpip(CONNECT_SOCKET_TCPV4, ktcpip) - kparams = kevent_mock.params - - assert kparams['pid'] == ktcpip.pid - assert kparams['ip_src'] == ktcpip.saddr - assert kparams['ip_dst'] == ktcpip.daddr - assert kparams['sport'] == ktcpip.sport - assert kparams['dport'] == ktcpip.dport - assert kparams['rwin'] == ktcpip.rcvwin - assert kparams['protocol'] == 'https' - - def test_parse_disconnect(self, tcpip_parser, kevent_mock): - ktcpip = dd({'connid': 0, 'saddr': '10.0.2.15', - 'sport': 49804, 'pid': 1380, - 'daddr': '216.58.211.206', 'dport': 443,}) - - tcpip_parser.parse_tcpip(DISCONNECT_SOCKET_TCPV4, ktcpip) - kparams = kevent_mock.params - - assert kparams['pid'] == ktcpip.pid - assert kparams['ip_src'] == ktcpip.saddr - assert kparams['ip_dst'] == ktcpip.daddr - assert kparams['sport'] == ktcpip.sport - assert kparams['dport'] == ktcpip.dport - - def test_parse_reconnect(self, tcpip_parser, kevent_mock): - ktcpip = dd({'connid': 0, 'saddr': '10.0.2.15', - 'sport': 49804, 'pid': 1380, - 'daddr': '216.58.211.206', 'dport': 443,}) - - tcpip_parser.parse_tcpip(RECONNECT_SOCKET_TCPV4, ktcpip) - kparams = kevent_mock.params - - assert kparams['pid'] == ktcpip.pid - assert kparams['ip_src'] == ktcpip.saddr - assert kparams['ip_dst'] == ktcpip.daddr - assert kparams['sport'] == ktcpip.sport - assert kparams['dport'] == ktcpip.dport - - - - - diff --git a/tests/unit/term.py b/tests/unit/term.py deleted file mode 100644 index ad9d981d2..000000000 --- a/tests/unit/term.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright 2016 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from unittest.mock import patch - -import pytest - -from fibratus.apidefs.sys import STD_OUTPUT_HANDLE, GENERIC_READ, GENERIC_WRITE, \ - FILE_SHARE_READ, CONSOLE_TEXTMODE_BUFFER, FILE_SHARE_WRITE, INVALID_HANDLE_VALUE, COORD, SMALL_RECT, CHAR_INFO, \ - CONSOLE_SCREEN_BUFFER_INFO -from fibratus.errors import TermInitializationError -from fibratus.term import AnsiTerm - - -class TestAnsiTerm(object): - - @patch('fibratus.term.get_std_handle', return_value=1) - @patch('fibratus.term.get_console_screen_buffer_info') - @patch('fibratus.term.get_console_cursor_info') - @patch('fibratus.term.create_console_screen_buffer', return_value=2) - @patch('fibratus.term.set_console_cursor_info') - @patch('fibratus.term.set_console_active_screen_buffer') - def test_setup_console(self, set_console_active_screen_buffer_mock, - set_console_cursor_info_mock, - create_console_screen_buffer_mock, - get_console_cursor_info_mock, - get_console_screen_buffer_info_mock, - get_std_handle_mock): - - with patch('fibratus.term.byref', side_effect=[1, 2, 3]): - ansi_term = AnsiTerm() - ansi_term.setup_console() - - get_std_handle_mock.assert_called_with(STD_OUTPUT_HANDLE) - get_console_screen_buffer_info_mock.assert_called_with(1, 1) - get_console_cursor_info_mock.assert_called_with(1, 2) - - create_console_screen_buffer_mock.assert_called_with(GENERIC_READ | GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_WRITE, - None, - CONSOLE_TEXTMODE_BUFFER, - None) - - set_console_cursor_info_mock.assert_called_with(2, 3) - set_console_active_screen_buffer_mock.assert_called_with(2) - - @patch('fibratus.term.get_std_handle', return_value=INVALID_HANDLE_VALUE) - def test_setup_console_invalid_std_console(self, get_std_handle_mock): - - ansi_term = AnsiTerm() - with pytest.raises(TermInitializationError): - ansi_term.setup_console() - get_std_handle_mock.assert_called_with(STD_OUTPUT_HANDLE) - - @patch('fibratus.term.get_std_handle', return_value=1) - @patch('fibratus.term.get_console_screen_buffer_info') - @patch('fibratus.term.get_console_cursor_info') - @patch('fibratus.term.create_console_screen_buffer', return_value=INVALID_HANDLE_VALUE) - def test_setup_console_invalid_frame_buffer(self, create_console_screen_buffer_mock, - get_console_cursor_info_mock, - get_console_screen_buffer_info_mock, - get_std_handle_mock): - - ansi_term = AnsiTerm() - with pytest.raises(TermInitializationError): - ansi_term.setup_console() - create_console_screen_buffer_mock.assert_called_with(GENERIC_READ | GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_WRITE, - None, - CONSOLE_TEXTMODE_BUFFER, - None) - - @patch('fibratus.term.get_std_handle', return_value=1) - @patch('fibratus.term.get_console_screen_buffer_info') - @patch('fibratus.term.get_console_cursor_info') - @patch('fibratus.term.create_console_screen_buffer', return_value=2) - @patch('fibratus.term.set_console_cursor_info') - @patch('fibratus.term.set_console_active_screen_buffer') - def test_restore_console(self, set_console_active_screen_buffer_mock, - set_console_cursor_info_mock, - create_console_screen_buffer_mock, - get_console_cursor_info_mock, - get_console_screen_buffer_info_mock, - get_std_handle_mock): - - ansi_term = AnsiTerm() - ansi_term.setup_console() - with patch('fibratus.term.byref', return_value=2): - ansi_term.restore_console() - set_console_active_screen_buffer_mock.assert_called_with(1) - set_console_cursor_info_mock.assert_called_with(1, 2) - - @patch('fibratus.term.get_std_handle', return_value=1) - @patch('fibratus.term.get_console_screen_buffer_info') - @patch('fibratus.term.get_console_cursor_info') - @patch('fibratus.term.create_console_screen_buffer', return_value=2) - @patch('fibratus.term.set_console_cursor_info') - @patch('fibratus.term.set_console_active_screen_buffer') - @patch('fibratus.term.write_console_output') - def test_write_output(self, write_console_output_mock, - set_console_active_screen_buffer_mock, - set_console_cursor_info_mock, - create_console_screen_buffer_mock, - get_console_cursor_info_mock, - get_console_screen_buffer_info_mock, - get_std_handle_mock): - ansi_term = AnsiTerm() - - buffer_info = CONSOLE_SCREEN_BUFFER_INFO() - buffer_info.size.x = 200 - buffer_info.size.y = 300 - - with patch.object(CONSOLE_SCREEN_BUFFER_INFO, '__new__', return_value=buffer_info): - ansi_term.setup_console() - ansi_term.write_output('Top inbound packets\n') - write_console_output_mock.assert_called_once() - - diff --git a/tests/unit/thread.py b/tests/unit/thread.py deleted file mode 100644 index ccbc2d47a..000000000 --- a/tests/unit/thread.py +++ /dev/null @@ -1,250 +0,0 @@ -# Copyright 2015 by Nedim Sabic (RabbitStack) -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from unittest.mock import patch, Mock - -import pytest -import os - -from fibratus.apidefs.cdefs import ERROR_ACCESS_DENIED -from fibratus.apidefs.process import PROCESS_QUERY_INFORMATION, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_VM_READ -from fibratus.handle import HandleRepository, HandleInfo -from fibratus.image_meta import ImageMetaRegistry -from fibratus.kevent_types import CREATE_PROCESS, CREATE_THREAD, ENUM_PROCESS, ENUM_THREAD, TERMINATE_THREAD, \ - TERMINATE_PROCESS -from fibratus.common import DotD as dd -from fibratus.thread import ThreadRegistry, ThreadInfo - - -@pytest.fixture(scope='module') -def handle_repo_mock(): - handle_repo = Mock(spec_set=HandleRepository) - handle_repo.query_handles.return_value = [HandleInfo(20, 18446738026501927904, 'FILE', - 'C:\\Windows\\System32\\kernel32.dll', - 0x2d8)] - return handle_repo - - -@pytest.fixture(scope='module') -def image_meta_registry_mock(): - imeta_meta_registry_mock = Mock(spec_set=ImageMetaRegistry) - imeta_meta_registry_mock.get_image_meta.return_value = None - return imeta_meta_registry_mock - - -@pytest.fixture(scope='module') -def thread_registry(handle_repo_mock, image_meta_registry_mock): - p1 = {"session_id": 0, "command_line": "C:\\Windows\\system32\\services.exe", "process_id": "0x1e4", - "unique_process_key": 18446738026492816176, "exit_status": 259, - "parent_id": "0x17c", - "image_file_name": "services.exe", - "directory_table_base": 4299976704, - "user_sid": None} - t1 = {"user_stack_base": 14483456, "io_priority": 2, "teb_base": 8796092874752, "process_id": "0x1e4", - "stack_limit": 18446735827441688576, "t_thread_id": "0x238", "base_priority": 9, - "win32_start_addr": 2009573488, "page_priority": 5, - "stack_base": 18446735827441713152, "user_stack_limit": 14450688, - "thread_flags": 0, "affinity": 15, "sub_process_tag": "0x0"} - t2 = {"user_stack_base": 16580608, "io_priority": 2, "teb_base": 8796092669952, "process_id": "0x1e4", - "stack_limit": 18446735827442425856, "t_thread_id": "0x254", "base_priority": 9, - "win32_start_addr": 8791752742020, "page_priority": 5, - "stack_base": 18446735827442450432, "user_stack_limit": 16547840, - "thread_flags": 0, "affinity": 15, "sub_process_tag": "0x0"} - - thread_registry = ThreadRegistry(handle_repo_mock, [], image_meta_registry_mock) - thread_registry.add_thread(ENUM_PROCESS, dd(p1)) - thread_registry.add_thread(ENUM_THREAD, dd(t1)) - thread_registry.add_thread(ENUM_THREAD, dd(t2)) - - return thread_registry - - -class TestThreadRegistry: - - def test_init_thread_registry(self, thread_registry): - - assert len(thread_registry.threads) == 3 - proc = thread_registry.threads[int('0x1e4', 16)] - assert proc - assert isinstance(proc, ThreadInfo) - assert proc.child_count == 2 - - def test_enum_process(self, thread_registry, handle_repo_mock): - - kti = dd({"session_id": 0, "command_line": "C:\\Windows\\system32\\svchost.exe -k RPCSS", - "process_id": "0x2d8", - "unique_process_key": 18446738026496154416, - "exit_status": 259, "user_sid": None, "parent_id": "0x1e4", - "image_file_name": "svchost.exe", "directory_table_base": 3716534272}) - thread_registry.add_thread(ENUM_PROCESS, kti) - process_id = int(kti.process_id, 16) - - handle_repo_mock.query_handles.assert_not_called() - - t = thread_registry.get_thread(process_id) - assert t - - assert t.pid == process_id - assert t.ppid == int(kti.parent_id, 16) - assert t.name == kti.image_file_name - assert t.exe == 'C:\\Windows\\system32\\svchost.exe' - assert t.comm == kti.command_line - assert len(t.args) > 0 - assert ['-k', 'RPCSS'] == t.args - assert '-k' in t.args - assert 'RPCSS' in t.args - - def test_create_process(self, thread_registry, handle_repo_mock): - - kti = dd({"session_id": 4294967295, "command_line": "\\SystemRoot\\System32\\smss.exe", - "process_id": "0xfc", "unique_process_key": 18446738026484345648, "exit_status": 259, - "user_sid": None, "parent_id": "0x4", "image_file_name": "smss.exe", - "directory_table_base": 4508921856}) - thread_registry.add_thread(CREATE_PROCESS, kti) - process_id = int(kti.process_id, 16) - - t = thread_registry.get_thread(process_id) - sys_root = os.path.expandvars("%SystemRoot%") - assert t - assert t.pid == process_id - assert t.ppid == int(kti.parent_id, 16) - assert t.name == kti.image_file_name - assert t.exe == '%s\\System32\\smss.exe' % sys_root - assert t.comm == kti.command_line - assert len(t.args) == 0 - - def test_create_thread(self, thread_registry): - - kti = dd({"user_stack_base": 18874368, "io_priority": 2, "teb_base": 8796092882944, - "process_id": "0x1e4", "stack_limit": 18446735827462836224, - "t_thread_id": "0x57c", "base_priority": 9, "win32_start_addr": 2009592544, - "page_priority": 5, "stack_base": 18446735827462860800, - "user_stack_limit": 18841600, "thread_flags": 0, - "affinity": 15, "sub_process_tag": "0x0"}) - thread_registry.add_thread(CREATE_THREAD, kti) - thread_id = int(kti.t_thread_id, 16) - t = thread_registry.get_thread(thread_id) - assert t - assert t.tid == thread_id - assert t.pid == t.ppid == int(kti.process_id, 16) - assert t.kstack_base == hex(kti.stack_base) - assert t.ustack_base == hex(kti.user_stack_base) - assert t.base_priority == kti.base_priority - assert t.io_priority == kti.io_priority - assert t.child_count == 0 - - @patch('fibratus.thread.open_process', return_value=13) - @patch('fibratus.thread.close_handle', return_value=None) - def test_create_thread_registry_proc_lookup_failed(self, close_handle_mock, open_process_mock, - thread_registry, handle_repo_mock): - - kti = dd({"user_stack_base": 10289152, "io_priority": 2, "teb_base": 8796092874752, - "process_id": "0x330", "stack_limit": 18446735827443150848, "t_thread_id": "0x338", - "base_priority": 8, "win32_start_addr": 2009573488, - "page_priority": 5, "stack_base": 18446735827443175424, "user_stack_limit": 10256384, - "thread_flags": 0, "affinity": 15, "sub_process_tag": "0x0"}) - thread_id = int(kti.t_thread_id, 16) - process_id = int(kti.process_id, 16) - - with patch('fibratus.thread.ThreadRegistry._query_process_info', return_value=dd(name='Dwm.exe', - comm='C:\\Windows\\system32\\Dwm.exe', - parent_pid=0x748)) as query_process_info_mock: - thread_registry.add_thread(CREATE_THREAD, kti) - open_process_mock.assert_called_with(PROCESS_QUERY_INFORMATION | - PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ, - False, process_id) - query_process_info_mock.assert_called_with(13) - close_handle_mock.assert_called_with(13) - - handle_repo_mock.query_handles.assert_called_with(process_id) - - proc = thread_registry.get_thread(process_id) - assert proc - assert proc.name == 'dwm.exe' - t = thread_registry.get_thread(thread_id) - assert t - assert t.name == 'dwm.exe' - - - @patch('fibratus.thread.open_process', side_effect=[None, 29]) - @patch('fibratus.thread.close_handle', return_value=None) - @patch('fibratus.thread.get_last_error', return_value=ERROR_ACCESS_DENIED) - def test_create_thread_registry_proc_lookup_failed_invalid_handle(self, get_last_error, close_handle_mock, - open_process_mock, - thread_registry, handle_repo_mock): - - kti = dd({"user_stack_base": 1835008, "io_priority": 2, "teb_base": 8796092878848, - "process_id": "0x4c8", "stack_limit": 18446735827446923264, "t_thread_id": "0x4dc", - "base_priority": 8, "win32_start_addr": 4286166856, "page_priority": 5, - "stack_base": 18446735827446964224, - "user_stack_limit": 1744896, "thread_flags": 0, "affinity": 15, "sub_process_tag": "0x0"}) - thread_id = int(kti.t_thread_id, 16) - process_id = int(kti.process_id, 16) - - with patch('fibratus.thread.ThreadRegistry._query_process_info', return_value=dd(name='explorer.exe', - comm='C:\\Windows\\Explorer.EXE', - parent_pid=0x4c8)) as query_process_info_mock: - thread_registry.add_thread(CREATE_THREAD, kti) - assert get_last_error.call_count == 1 - open_process_mock.assert_called_with(PROCESS_QUERY_LIMITED_INFORMATION, - False, process_id) - - query_process_info_mock.assert_called_with(29, False) - close_handle_mock.assert_called_with(29) - - handle_repo_mock.query_handles.assert_called_with(process_id) - - proc = thread_registry.get_thread(process_id) - assert proc - assert proc.name == 'explorer.exe' - t = thread_registry.get_thread(thread_id) - assert t - assert t.name == 'explorer.exe' - assert t.comm == 'C:\\Windows\\Explorer.EXE' - - def test_terminate_thread(self, thread_registry): - - kti = dd({"user_stack_base": 18874368, "io_priority": 2, "teb_base": 8796092882944, - "process_id": "0x1e4", "stack_limit": 18446735827462836224, - "t_thread_id": "0x57c", "base_priority": 9, "win32_start_addr": 2009592544, - "page_priority": 5, "stack_base": 18446735827462860800, - "user_stack_limit": 18841600, "thread_flags": 0, - "affinity": 15, "sub_process_tag": "0x0"}) - - thread_id = int(kti.t_thread_id, 16) - process_id = int(kti.process_id, 16) - proc = thread_registry.get_thread(process_id) - - child_count = proc.child_count - t = thread_registry.get_thread(thread_id) - assert t - thread_registry.remove_thread(TERMINATE_THREAD, kti) - - t = thread_registry.get_thread(thread_id) - assert t is None - assert proc.child_count == child_count - 1 - - def test_terminate_process(self, thread_registry): - kti = dd({"session_id": 0, "command_line": "C:\\Windows\\system32\\services.exe", "process_id": "0x1e4", - "unique_process_key": 18446738026492816176, "exit_status": 259, - "parent_id": "0x17c", - "image_file_name": "services.exe", - "directory_table_base": 4299976704, - "user_sid": None}) - process_id = int(kti.process_id, 16) - proc = thread_registry.get_thread(process_id) - assert proc - thread_registry.remove_thread(TERMINATE_PROCESS, kti) - proc = thread_registry.get_thread(process_id) - assert proc is None \ No newline at end of file

  • S=o5eqfJ~*X-FwL&eqB!P&(N>! zHcks0u99mbpA*>YM%;uQUdefAnf;WzjT8;(;bHl6#(o!%zb=h<30BCZMhx?NN05Y$fCYZU_e43>R}O*Cpig4iMGu40 zvwP`*Vsveb2c);V0n>urLA6MCKS>{~DyWWzWllNQ-RAHikB5Rj95@p2_-u2(_ttB@ zv&W~mjbWBjJ4;DgT+U-7SU#fIxh1!Fky+0DJKg8)!Tr2)`5g2=0PXg4y{14918M=8 zyytmm=W6;mKG0KsPntP>%>4SiE2XJ&k{5y% zgIp+8+7Q(B81d=3DmFjTSTED7Z8=H4&eq>*==nt2uTzu|jOfg-;!fIXkCV;Io$}lQ zzCAr^JLp$|Hvo4$w zqKyB5C_@6gSPhv)rcOtnN(!$cMRMSj>-4a!Oz7h8XotV~cck^N7SJaGX8~Tnx)=1r zz!QK>o*%_Q8TR$iAcxOk8U3n@-$UCQrRJ zo#}Qp3G@tL9^mDx3G^!9BtWJ|+6|XCc&Zh z@eFbg#6jJks1k>|kA)!^IQ1NwPYg36hOrp!O$5d@S)_tUg*qW;Txo{yvt6BkB76ib z!Z)?69DctCFDaMbqIdmjGH_e9k!~?U9~iny7O%5lB5wTvQ3z~fE=L)dC)Rszg!E^L9p*snScTa zuuA#VeQrLsfgU|mPdv_d_3tSMQ+=dSc1%Y{Q@h;U zKB2w6wY{RheNyPa&HQC{9VeUlK5m~*A@-^CI&LacHk!()F+3UZTRAX0_;u_}CVu{R zLW7|s=5c*O{vhSkv5Hrq*7*jL#dJAVs6$1k5-rMo&}-dli=jW{kQXrAD#{u@81n#n z3_TRBv5eM|$C;Isql2RS&Z)=E$lEYCGk1Z07)|c@q}T@!U?AkJP>C_CFT% z6ks~w@i7nd5+I(TKY4sK$%39^FYkJ#omfYXT&pWn)+$$$QY;Pikavekc{dOCWL%Z0 z_C%*Vw&EE{`wDc7GqB$kpI-*Ru}swSGp;#+KGe3Nak*3LE*n#_l>6Xl+~D3nbRV8& z-%3%+Ko0>%0A4$SeB#*W>|eTNB@~t%+0c_L1A^ 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�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