From 0da6783a455775772d660f9197cfda62dac4e8d0 Mon Sep 17 00:00:00 2001 From: Stefan Date: Mon, 6 Apr 2020 18:48:34 +0200 Subject: [PATCH] Import from old repository --- .gitignore | 21 + .travis.yml | 14 + ACKNOWLEDGEMENTS | 138 + AUTHORS | 27 + LICENSE | 202 ++ MANIFEST.in | 10 + README | 31 + README.md | 35 + config/dpkg/debian/changelog | 23 + config/dpkg/debian/compat | 1 + config/dpkg/debian/control | 16 + config/dpkg/debian/copyright | 27 + config/dpkg/debian/python-plaso.docs | 4 + config/dpkg/debian/rules | 45 + config/licenses/LICENSE.PyYAML | 20 + config/licenses/LICENSE.bencode | 143 + config/licenses/LICENSE.binplist | 13 + config/licenses/LICENSE.construct | 21 + config/licenses/LICENSE.dateutil-parser | 29 + config/licenses/LICENSE.dfvfs | 202 ++ config/licenses/LICENSE.dpkt | 28 + config/licenses/LICENSE.ipython | 85 + config/licenses/LICENSE.libbde | 166 + config/licenses/LICENSE.libesedb | 166 + config/licenses/LICENSE.libevt | 166 + config/licenses/LICENSE.libevtx | 166 + config/licenses/LICENSE.libewf | 166 + config/licenses/LICENSE.libfwsi | 166 + config/licenses/LICENSE.liblnk | 166 + config/licenses/LICENSE.libmsiecf | 166 + config/licenses/LICENSE.libolecf | 166 + config/licenses/LICENSE.libqcow | 166 + config/licenses/LICENSE.libregf | 166 + config/licenses/LICENSE.libsmdev | 166 + config/licenses/LICENSE.libsmraw | 166 + config/licenses/LICENSE.libvhdi | 166 + config/licenses/LICENSE.libvmdk | 166 + config/licenses/LICENSE.libvshadow | 166 + config/licenses/LICENSE.libyaml | 19 + config/licenses/LICENSE.protobuf | 33 + config/licenses/LICENSE.psutil | 27 + config/licenses/LICENSE.pyelasticsearch | 27 + config/licenses/LICENSE.pyinstaller | 364 +++ config/licenses/LICENSE.pyparsing | 18 + config/licenses/LICENSE.pysqlite | 19 + config/licenses/LICENSE.python | 945 ++++++ config/licenses/LICENSE.pytsk | 13 + config/licenses/LICENSE.pytz | 19 + config/licenses/LICENSE.pywin32 | 30 + config/licenses/LICENSE.six | 18 + config/licenses/LICENSE.sleuthkit.IBM | 221 ++ config/licenses/LICENSE.sleuthkit.cpl1.0 | 213 ++ config/licenses/LICENSE.talloc | 166 + config/logo.jpg | Bin 0 -> 2680 bytes config/macosx/Readme.txt | 14 + config/macosx/install.sh.in | 56 + config/macosx/make_dist.sh | 74 + config/windows/make.bat | 16 + config/windows/make_check.bat | 12 + config/windows/make_dist.bat | 22 + extra/README | 1 + extra/plaso_kibana_example.json | 329 ++ extra/tag_macosx.txt | 21 + extra/tag_windows.txt | 94 + plaso/__init__.py | 30 + plaso/analysis/__init__.py | 83 + plaso/analysis/browser_search.py | 257 ++ plaso/analysis/browser_search_test.py | 74 + plaso/analysis/chrome_extension.py | 201 ++ plaso/analysis/chrome_extension_test.py | 196 ++ plaso/analysis/context.py | 168 + plaso/analysis/context_test.py | 134 + plaso/analysis/interface.py | 139 + plaso/analysis/test_lib.py | 171 + plaso/analysis/windows_services.py | 267 ++ plaso/analysis/windows_services_test.py | 192 ++ plaso/artifacts/__init__.py | 17 + plaso/artifacts/knowledge_base.py | 137 + plaso/classifier/__init__.py | 16 + plaso/classifier/classifier.py | 184 ++ plaso/classifier/classifier_test.py | 72 + plaso/classifier/classify.py | 78 + plaso/classifier/patterns.py | 308 ++ plaso/classifier/range_list.py | 156 + plaso/classifier/range_list_test.py | 113 + plaso/classifier/scan_tree.py | 744 +++++ plaso/classifier/scan_tree_test.py | 74 + plaso/classifier/scanner.py | 749 +++++ plaso/classifier/scanner_test.py | 119 + plaso/classifier/specification.py | 156 + plaso/classifier/specification_test.py | 46 + plaso/classifier/test_lib.py | 113 + plaso/engine/__init__.py | 17 + plaso/engine/classifier.py | 202 ++ plaso/engine/collector.py | 421 +++ plaso/engine/collector_test.py | 354 +++ plaso/engine/engine.py | 319 ++ plaso/engine/queue.py | 204 ++ plaso/engine/single_process.py | 366 +++ plaso/engine/single_process_test.py | 133 + plaso/engine/test_lib.py | 71 + plaso/engine/utils.py | 75 + plaso/engine/worker.py | 352 +++ plaso/events/__init__.py | 17 + plaso/events/plist_event.py | 92 + plaso/events/shell_item_events.py | 50 + plaso/events/text_events.py | 48 + plaso/events/time_events.py | 157 + plaso/events/windows_events.py | 95 + plaso/filters/__init__.py | 56 + plaso/filters/dynamic_filter.py | 162 + plaso/filters/dynamic_filter_test.py | 85 + plaso/filters/eventfilter.py | 40 + plaso/filters/eventfilter_test.py | 43 + plaso/filters/filterlist.py | 109 + plaso/filters/filterlist_test.py | 98 + plaso/filters/test_helper.py | 50 + plaso/formatters/__init__.py | 86 + plaso/formatters/android_app_usage.py | 33 + plaso/formatters/android_calls.py | 37 + plaso/formatters/android_sms.py | 37 + plaso/formatters/appcompatcache.py | 36 + plaso/formatters/appusage.py | 33 + plaso/formatters/asl.py | 47 + plaso/formatters/bencode_parser.py | 49 + plaso/formatters/bsm.py | 54 + plaso/formatters/chrome.py | 61 + plaso/formatters/chrome_cache.py | 32 + plaso/formatters/chrome_cookies.py | 40 + plaso/formatters/chrome_extension_activity.py | 47 + plaso/formatters/cups_ipp.py | 42 + plaso/formatters/filestat.py | 66 + plaso/formatters/firefox.py | 136 + plaso/formatters/firefox_cache.py | 39 + plaso/formatters/firefox_cookies.py | 40 + plaso/formatters/ganalytics.py | 70 + plaso/formatters/gdrive.py | 55 + plaso/formatters/hachoir.py | 57 + plaso/formatters/iis.py | 59 + plaso/formatters/interface.py | 244 ++ plaso/formatters/ipod.py | 37 + plaso/formatters/java_idx.py | 34 + plaso/formatters/ls_quarantine.py | 36 + plaso/formatters/mac_appfirewall.py | 39 + plaso/formatters/mac_document_versions.py | 38 + plaso/formatters/mac_keychain.py | 53 + plaso/formatters/mac_securityd.py | 39 + plaso/formatters/mac_wifi.py | 38 + plaso/formatters/mackeeper_cache.py | 35 + plaso/formatters/mactime.py | 32 + plaso/formatters/manager.py | 140 + plaso/formatters/manager_test.py | 163 + plaso/formatters/mcafeeav.py | 34 + plaso/formatters/msie_webcache.py | 99 + plaso/formatters/msiecf.py | 65 + plaso/formatters/olecf.py | 149 + plaso/formatters/opera.py | 47 + plaso/formatters/oxml.py | 67 + plaso/formatters/pcap.py | 50 + plaso/formatters/plist.py | 36 + plaso/formatters/pls_recall.py | 33 + plaso/formatters/popcontest.py | 55 + plaso/formatters/recycler.py | 82 + plaso/formatters/rubanetra.py | 422 +++ plaso/formatters/safari.py | 33 + plaso/formatters/selinux.py | 34 + plaso/formatters/shell_items.py | 41 + plaso/formatters/skydrivelog.py | 36 + plaso/formatters/skydrivelogerr.py | 37 + plaso/formatters/skype.py | 88 + plaso/formatters/symantec.py | 197 ++ plaso/formatters/syslog.py | 33 + plaso/formatters/task_scheduler.py | 36 + plaso/formatters/text.py | 30 + plaso/formatters/utmp.py | 41 + plaso/formatters/utmpx.py | 36 + plaso/formatters/windows.py | 38 + plaso/formatters/winevt.py | 113 + plaso/formatters/winevtx.py | 41 + plaso/formatters/winfirewall.py | 63 + plaso/formatters/winjob.py | 36 + plaso/formatters/winlnk.py | 101 + plaso/formatters/winprefetch.py | 76 + plaso/formatters/winreg.py | 81 + plaso/formatters/winregservice.py | 58 + plaso/formatters/xchatlog.py | 31 + plaso/formatters/xchatscrollback.py | 33 + plaso/formatters/zeitgeist.py | 31 + plaso/frontend/__init__.py | 16 + plaso/frontend/frontend.py | 1693 ++++++++++ plaso/frontend/frontend_test.py | 279 ++ plaso/frontend/image_export.py | 700 +++++ plaso/frontend/image_export_test.py | 237 ++ plaso/frontend/log2timeline.py | 454 +++ plaso/frontend/log2timeline_test.py | 75 + plaso/frontend/pinfo.py | 266 ++ plaso/frontend/pinfo_test.py | 65 + plaso/frontend/plasm.py | 832 +++++ plaso/frontend/plasm_test.py | 195 ++ plaso/frontend/pprof.py | 364 +++ plaso/frontend/preg.py | 2161 +++++++++++++ plaso/frontend/preg_test.py | 353 +++ plaso/frontend/presets.py | 72 + plaso/frontend/pshell.py | 498 +++ plaso/frontend/psort.py | 764 +++++ plaso/frontend/psort_test.py | 197 ++ plaso/frontend/test_lib.py | 68 + plaso/frontend/utils.py | 212 ++ plaso/lib/__init__.py | 17 + plaso/lib/binary.py | 280 ++ plaso/lib/binary_test.py | 206 ++ plaso/lib/bufferlib.py | 77 + plaso/lib/bufferlib_test.py | 60 + plaso/lib/errors.py | 113 + plaso/lib/event.py | 478 +++ plaso/lib/event_test.py | 324 ++ plaso/lib/eventdata.py | 65 + plaso/lib/filter_interface.py | 94 + plaso/lib/lexer.py | 514 +++ plaso/lib/limit.py | 20 + plaso/lib/objectfilter.py | 925 ++++++ plaso/lib/objectfilter_test.py | 519 +++ plaso/lib/output.py | 394 +++ plaso/lib/output_test.py | 193 ++ plaso/lib/pfilter.py | 455 +++ plaso/lib/pfilter_test.py | 238 ++ plaso/lib/proxy.py | 130 + plaso/lib/putils.py | 58 + plaso/lib/registry.py | 80 + plaso/lib/storage.py | 1564 +++++++++ plaso/lib/storage_test.py | 340 ++ plaso/lib/timelib.py | 635 ++++ plaso/lib/timelib_test.py | 531 ++++ plaso/lib/utils.py | 199 ++ plaso/lib/utils_test.py | 48 + plaso/multi_processing/__init__.py | 17 + plaso/multi_processing/foreman.py | 332 ++ plaso/multi_processing/multi_process.py | 700 +++++ plaso/multi_processing/multi_process_test.py | 52 + plaso/multi_processing/process_info.py | 259 ++ plaso/multi_processing/rpc_proxy.py | 134 + plaso/output/__init__.py | 34 + plaso/output/dynamic.py | 300 ++ plaso/output/dynamic_test.py | 131 + plaso/output/elastic.py | 235 ++ plaso/output/helper.py | 109 + plaso/output/json_out.py | 40 + plaso/output/json_out_test.py | 90 + plaso/output/l2t_csv.py | 144 + plaso/output/l2t_csv_test.py | 95 + plaso/output/l2t_tln.py | 122 + plaso/output/l2t_tln_test.py | 86 + plaso/output/mysql_4n6.py | 402 +++ plaso/output/pstorage.py | 62 + plaso/output/pstorage_test.py | 93 + plaso/output/rawpy.py | 41 + plaso/output/sqlite_4n6.py | 316 ++ plaso/output/tln.py | 110 + plaso/output/tln_test.py | 85 + plaso/parsers/__init__.py | 73 + plaso/parsers/android_app_usage.py | 126 + plaso/parsers/android_app_usage_test.py | 85 + plaso/parsers/asl.py | 412 +++ plaso/parsers/asl_test.py | 96 + plaso/parsers/bencode_parser.py | 121 + plaso/parsers/bencode_parser_test.py | 143 + plaso/parsers/bencode_plugins/__init__.py | 20 + plaso/parsers/bencode_plugins/interface.py | 205 ++ plaso/parsers/bencode_plugins/test_lib.py | 24 + plaso/parsers/bencode_plugins/transmission.py | 102 + plaso/parsers/bencode_plugins/utorrent.py | 137 + plaso/parsers/bsm.py | 1145 +++++++ plaso/parsers/bsm_test.py | 197 ++ plaso/parsers/chrome_cache.py | 441 +++ plaso/parsers/chrome_cache_test.py | 60 + plaso/parsers/context.py | 289 ++ plaso/parsers/cookie_plugins/__init__.py | 19 + plaso/parsers/cookie_plugins/ganalytics.py | 221 ++ .../parsers/cookie_plugins/ganalytics_test.py | 139 + plaso/parsers/cookie_plugins/interface.py | 115 + plaso/parsers/cookie_plugins/test_lib.py | 24 + plaso/parsers/cups_ipp.py | 345 ++ plaso/parsers/cups_ipp_test.py | 98 + plaso/parsers/custom_destinations.py | 212 ++ plaso/parsers/custom_destinations_test.py | 116 + plaso/parsers/esedb.py | 98 + plaso/parsers/esedb_plugins/__init__.py | 20 + plaso/parsers/esedb_plugins/interface.py | 303 ++ plaso/parsers/esedb_plugins/msie_webcache.py | 366 +++ .../esedb_plugins/msie_webcache_test.py | 71 + plaso/parsers/esedb_plugins/test_lib.py | 76 + plaso/parsers/filestat.py | 129 + plaso/parsers/filestat_test.py | 153 + plaso/parsers/firefox_cache.py | 246 ++ plaso/parsers/firefox_cache_test.py | 188 ++ plaso/parsers/hachoir.py | 172 + plaso/parsers/iis.py | 234 ++ plaso/parsers/iis_test.py | 96 + plaso/parsers/interface.py | 251 ++ plaso/parsers/java_idx.py | 236 ++ plaso/parsers/java_idx_test.py | 124 + plaso/parsers/mac_appfirewall.py | 257 ++ plaso/parsers/mac_appfirewall_test.py | 118 + plaso/parsers/mac_keychain.py | 507 +++ plaso/parsers/mac_keychain_test.py | 107 + plaso/parsers/mac_securityd.py | 276 ++ plaso/parsers/mac_securityd_test.py | 159 + plaso/parsers/mac_wifi.py | 280 ++ plaso/parsers/mac_wifi_test.py | 134 + plaso/parsers/mactime.py | 153 + plaso/parsers/mactime_test.py | 105 + plaso/parsers/manager.py | 205 ++ plaso/parsers/manager_test.py | 152 + plaso/parsers/mcafeeav.py | 141 + plaso/parsers/mcafeeav_test.py | 79 + plaso/parsers/msiecf.py | 233 ++ plaso/parsers/msiecf_test.py | 113 + plaso/parsers/olecf.py | 109 + plaso/parsers/olecf_plugins/__init__.py | 22 + .../olecf_plugins/automatic_destinations.py | 204 ++ .../automatic_destinations_test.py | 101 + plaso/parsers/olecf_plugins/default.py | 162 + plaso/parsers/olecf_plugins/default_test.py | 75 + plaso/parsers/olecf_plugins/interface.py | 150 + plaso/parsers/olecf_plugins/summary.py | 433 +++ plaso/parsers/olecf_plugins/summary_test.py | 129 + plaso/parsers/olecf_plugins/test_lib.py | 84 + plaso/parsers/opera.py | 326 ++ plaso/parsers/opera_test.py | 118 + plaso/parsers/oxml.py | 183 ++ plaso/parsers/oxml_test.py | 96 + plaso/parsers/pcap.py | 843 +++++ plaso/parsers/pcap_test.py | 117 + plaso/parsers/plist.py | 159 + plaso/parsers/plist_plugins/__init__.py | 30 + plaso/parsers/plist_plugins/airport.py | 63 + plaso/parsers/plist_plugins/airport_test.py | 69 + plaso/parsers/plist_plugins/appleaccount.py | 111 + .../plist_plugins/appleaccount_test.py | 79 + plaso/parsers/plist_plugins/bluetooth.py | 99 + plaso/parsers/plist_plugins/bluetooth_test.py | 85 + plaso/parsers/plist_plugins/default.py | 86 + plaso/parsers/plist_plugins/default_test.py | 110 + .../parsers/plist_plugins/install_history.py | 67 + .../plist_plugins/install_history_test.py | 77 + plaso/parsers/plist_plugins/interface.py | 323 ++ plaso/parsers/plist_plugins/interface_test.py | 135 + plaso/parsers/plist_plugins/ipod.py | 91 + plaso/parsers/plist_plugins/ipod_test.py | 77 + plaso/parsers/plist_plugins/macuser.py | 172 + plaso/parsers/plist_plugins/macuser_test.py | 73 + plaso/parsers/plist_plugins/safari.py | 97 + plaso/parsers/plist_plugins/safari_test.py | 65 + plaso/parsers/plist_plugins/softwareupdate.py | 83 + .../plist_plugins/softwareupdate_test.py | 65 + plaso/parsers/plist_plugins/spotlight.py | 67 + plaso/parsers/plist_plugins/spotlight_test.py | 69 + .../parsers/plist_plugins/spotlight_volume.py | 60 + .../plist_plugins/spotlight_volume_test.py | 67 + plaso/parsers/plist_plugins/test_lib.py | 87 + plaso/parsers/plist_plugins/timemachine.py | 86 + .../parsers/plist_plugins/timemachine_test.py | 72 + plaso/parsers/plist_test.py | 74 + plaso/parsers/pls_recall.py | 173 + plaso/parsers/pls_recall_test.py | 78 + plaso/parsers/plugins.py | 118 + plaso/parsers/popcontest.py | 275 ++ plaso/parsers/popcontest_test.py | 145 + plaso/parsers/recycler.py | 222 ++ plaso/parsers/recycler_test.py | 111 + plaso/parsers/rubanetra.py | 754 +++++ plaso/parsers/selinux.py | 175 ++ plaso/parsers/selinux_test.py | 96 + plaso/parsers/shared/__init__.py | 17 + plaso/parsers/shared/shell_items.py | 188 ++ plaso/parsers/skydrivelog.py | 209 ++ plaso/parsers/skydrivelog_test.py | 85 + plaso/parsers/skydrivelogerr.py | 257 ++ plaso/parsers/skydrivelogerr_test.py | 103 + plaso/parsers/sqlite.py | 279 ++ plaso/parsers/sqlite_plugins/__init__.py | 32 + plaso/parsers/sqlite_plugins/android_calls.py | 111 + .../sqlite_plugins/android_calls_test.py | 91 + plaso/parsers/sqlite_plugins/android_sms.py | 100 + .../sqlite_plugins/android_sms_test.py | 73 + plaso/parsers/sqlite_plugins/appusage.py | 108 + plaso/parsers/sqlite_plugins/appusage_test.py | 69 + plaso/parsers/sqlite_plugins/chrome.py | 337 ++ .../parsers/sqlite_plugins/chrome_cookies.py | 166 + .../sqlite_plugins/chrome_cookies_test.py | 135 + .../chrome_extension_activity.py | 91 + .../chrome_extension_activity_test.py | 76 + plaso/parsers/sqlite_plugins/chrome_test.py | 102 + plaso/parsers/sqlite_plugins/firefox.py | 476 +++ .../parsers/sqlite_plugins/firefox_cookies.py | 163 + .../sqlite_plugins/firefox_cookies_test.py | 107 + plaso/parsers/sqlite_plugins/firefox_test.py | 277 ++ plaso/parsers/sqlite_plugins/gdrive.py | 268 ++ plaso/parsers/sqlite_plugins/gdrive_test.py | 104 + plaso/parsers/sqlite_plugins/interface.py | 121 + plaso/parsers/sqlite_plugins/ls_quarantine.py | 90 + .../sqlite_plugins/ls_quarantine_test.py | 90 + .../sqlite_plugins/mac_document_versions.py | 114 + .../mac_document_versions_test.py | 74 + .../parsers/sqlite_plugins/mackeeper_cache.py | 229 ++ .../sqlite_plugins/mackeeper_cache_test.py | 68 + plaso/parsers/sqlite_plugins/skype.py | 492 +++ plaso/parsers/sqlite_plugins/skype_test.py | 158 + plaso/parsers/sqlite_plugins/test_lib.py | 63 + plaso/parsers/sqlite_plugins/zeitgeist.py | 84 + .../parsers/sqlite_plugins/zeitgeist_test.py | 61 + plaso/parsers/sqlite_test.py | 63 + plaso/parsers/symantec.py | 153 + plaso/parsers/symantec_test.py | 93 + plaso/parsers/syslog.py | 209 ++ plaso/parsers/syslog_test.py | 76 + plaso/parsers/test_lib.py | 234 ++ plaso/parsers/text_parser.py | 1099 +++++++ plaso/parsers/text_parser_test.py | 181 ++ plaso/parsers/utmp.py | 273 ++ plaso/parsers/utmp_test.py | 136 + plaso/parsers/utmpx.py | 207 ++ plaso/parsers/utmpx_test.py | 94 + plaso/parsers/winevt.py | 192 ++ plaso/parsers/winevt_test.py | 120 + plaso/parsers/winevtx.py | 165 + plaso/parsers/winevtx_test.py | 138 + plaso/parsers/winfirewall.py | 197 ++ plaso/parsers/winfirewall_test.py | 83 + plaso/parsers/winjob.py | 270 ++ plaso/parsers/winjob_test.py | 101 + plaso/parsers/winlnk.py | 158 + plaso/parsers/winlnk_test.py | 170 + plaso/parsers/winprefetch.py | 504 +++ plaso/parsers/winprefetch_test.py | 378 +++ plaso/parsers/winreg.py | 336 ++ plaso/parsers/winreg_plugins/__init__.py | 42 + .../parsers/winreg_plugins/appcompatcache.py | 624 ++++ .../winreg_plugins/appcompatcache_test.py | 73 + plaso/parsers/winreg_plugins/bagmru.py | 206 ++ plaso/parsers/winreg_plugins/bagmru_test.py | 99 + plaso/parsers/winreg_plugins/ccleaner.py | 87 + plaso/parsers/winreg_plugins/ccleaner_test.py | 83 + plaso/parsers/winreg_plugins/default.py | 120 + plaso/parsers/winreg_plugins/default_test.py | 79 + plaso/parsers/winreg_plugins/interface.py | 263 ++ plaso/parsers/winreg_plugins/lfu.py | 126 + plaso/parsers/winreg_plugins/lfu_test.py | 155 + plaso/parsers/winreg_plugins/mountpoints.py | 88 + .../winreg_plugins/mountpoints_test.py | 72 + plaso/parsers/winreg_plugins/mrulist.py | 308 ++ plaso/parsers/winreg_plugins/mrulist_test.py | 171 + plaso/parsers/winreg_plugins/mrulistex.py | 528 ++++ .../parsers/winreg_plugins/mrulistex_test.py | 303 ++ plaso/parsers/winreg_plugins/msie_zones.py | 292 ++ .../parsers/winreg_plugins/msie_zones_test.py | 384 +++ plaso/parsers/winreg_plugins/officemru.py | 116 + .../parsers/winreg_plugins/officemru_test.py | 76 + plaso/parsers/winreg_plugins/outlook.py | 97 + plaso/parsers/winreg_plugins/outlook_test.py | 103 + plaso/parsers/winreg_plugins/run.py | 90 + plaso/parsers/winreg_plugins/run_test.py | 179 ++ plaso/parsers/winreg_plugins/sam_users.py | 191 ++ .../parsers/winreg_plugins/sam_users_test.py | 80 + plaso/parsers/winreg_plugins/services.py | 98 + plaso/parsers/winreg_plugins/services_test.py | 170 + plaso/parsers/winreg_plugins/shutdown.py | 78 + plaso/parsers/winreg_plugins/shutdown_test.py | 79 + .../parsers/winreg_plugins/task_scheduler.py | 169 + .../winreg_plugins/task_scheduler_test.py | 87 + .../parsers/winreg_plugins/terminal_server.py | 127 + .../winreg_plugins/terminal_server_test.py | 130 + plaso/parsers/winreg_plugins/test_lib.py | 106 + plaso/parsers/winreg_plugins/typedurls.py | 84 + .../parsers/winreg_plugins/typedurls_test.py | 112 + plaso/parsers/winreg_plugins/usb.py | 87 + plaso/parsers/winreg_plugins/usb_test.py | 80 + plaso/parsers/winreg_plugins/usbstor.py | 133 + plaso/parsers/winreg_plugins/usbstor_test.py | 84 + plaso/parsers/winreg_plugins/userassist.py | 208 ++ .../parsers/winreg_plugins/userassist_test.py | 112 + plaso/parsers/winreg_plugins/winrar.py | 87 + plaso/parsers/winreg_plugins/winrar_test.py | 83 + plaso/parsers/winreg_plugins/winver.py | 102 + plaso/parsers/winreg_plugins/winver_test.py | 85 + plaso/parsers/winreg_test.py | 106 + plaso/parsers/xchatlog.py | 264 ++ plaso/parsers/xchatlog_test.py | 101 + plaso/parsers/xchatscrollback.py | 212 ++ plaso/parsers/xchatscrollback_test.py | 87 + plaso/preprocessors/__init__.py | 22 + plaso/preprocessors/interface.py | 223 ++ plaso/preprocessors/linux.py | 118 + plaso/preprocessors/linux_test.py | 100 + plaso/preprocessors/macosx.py | 390 +++ plaso/preprocessors/macosx_test.py | 221 ++ plaso/preprocessors/manager.py | 138 + plaso/preprocessors/manager_test.py | 71 + plaso/preprocessors/test_lib.py | 91 + plaso/preprocessors/windows.py | 556 ++++ plaso/preprocessors/windows_test.py | 265 ++ plaso/proto/__init__.py | 16 + plaso/proto/plaso_storage.proto | 367 +++ plaso/proto/plaso_storage_pb2.py | 1041 ++++++ plaso/serializer/__init__.py | 17 + plaso/serializer/interface.py | 181 ++ plaso/serializer/json_serializer.py | 232 ++ plaso/serializer/json_serializer_test.py | 126 + plaso/serializer/protobuf_serializer.py | 737 +++++ plaso/serializer/protobuf_serializer_test.py | 211 ++ plaso/unix/__init__.py | 16 + plaso/unix/bsmtoken.py | 810 +++++ plaso/winnt/__init__.py | 16 + plaso/winnt/environ_expand.py | 52 + plaso/winnt/human_readable_service_enums.py | 44 + plaso/winnt/known_folder_ids.py | 270 ++ plaso/winnt/shell_folder_ids.py | 204 ++ plaso/winreg/__init__.py | 17 + plaso/winreg/cache.py | 142 + plaso/winreg/cache_test.py | 49 + plaso/winreg/interface.py | 227 ++ plaso/winreg/path_expander.py | 81 + plaso/winreg/test_lib.py | 220 ++ plaso/winreg/utils.py | 44 + plaso/winreg/winpyregf.py | 384 +++ plaso/winreg/winpyregf_test.py | 61 + plaso/winreg/winregistry.py | 149 + plaso/winreg/winregistry_test.py | 51 + plasov1.2.0-rubanetra0.0.6-distribution.zip | Bin 0 -> 40799744 bytes run_tests.py | 28 + setup.cfg | 37 + setup.py | 123 + test_data/$II3DF3L.zip | Bin 0 -> 544 bytes .../1b4dd67f29cb1962.automaticDestinations-ms | Bin 0 -> 20480 bytes .../5afe4de1b92fc382.customDestinations-ms | Bin 0 -> 17261 bytes test_data/AccessProtectionLog.txt | 14 + test_data/CMD.EXE-087B4001.pf | Bin 0 -> 11986 bytes test_data/Document.doc | Bin 0 -> 59392 bytes test_data/Document.docx | Bin 0 -> 14202 bytes test_data/Extension Activity | Bin 0 -> 28672 bytes test_data/History | Bin 0 -> 79872 bytes test_data/History.plist | Bin 0 -> 3258 bytes test_data/INFO2 | Bin 0 -> 3220 bytes test_data/InstallHistory.plist | 127 + test_data/NTUSER-CCLEANER.DAT | Bin 0 -> 524288 bytes test_data/NTUSER-RunTests.DAT | Bin 0 -> 262144 bytes test_data/NTUSER-WIN7.DAT | Bin 0 -> 1310720 bytes test_data/NTUSER.DAT | Bin 0 -> 786432 bytes test_data/NeroInfoTool.lnk | Bin 0 -> 2716 bytes test_data/PING.EXE-B29F6629.pf | Bin 0 -> 11216 bytes test_data/PLSRecall_Test.dat | Bin 0 -> 8250 bytes test_data/SAM | Bin 0 -> 262144 bytes test_data/SOFTWARE | Bin 0 -> 37748736 bytes test_data/SOFTWARE-RunTests | Bin 0 -> 54263808 bytes test_data/SYSTEM | Bin 0 -> 15990784 bytes test_data/Symantec.Log | 8 + test_data/SysEvent.Evt | Bin 0 -> 2031616 bytes test_data/System.evtx | Bin 0 -> 1118208 bytes test_data/TASKHOST.EXE-3AE259FC.pf | Bin 0 -> 28766 bytes test_data/VolumeConfiguration.plist | 58 + test_data/WUAUCLT.EXE-830BCC14.pf | Bin 0 -> 158422 bytes test_data/WebCacheV01.dat | Bin 0 -> 33619968 bytes test_data/Windows.edb | Bin 0 -> 42008576 bytes test_data/__init__.py | 16 + test_data/activity.sqlite | Bin 0 -> 59392 bytes test_data/appfirewall.log | 47 + test_data/apple.bsm | Bin 0 -> 6566 bytes test_data/applesystemlog.asl | Bin 0 -> 1144 bytes test_data/application_usage.sqlite | Bin 0 -> 12288 bytes test_data/bencode_transmission | Bin 0 -> 757 bytes test_data/bencode_utorrent | Bin 0 -> 2076 bytes test_data/chrome_cache/data_0 | Bin 0 -> 45056 bytes test_data/chrome_cache/data_1 | Bin 0 -> 270336 bytes test_data/chrome_cache/data_2 | Bin 0 -> 1056768 bytes test_data/chrome_cache/data_3 | Bin 0 -> 4202496 bytes test_data/chrome_cache/f_000001 | Bin 0 -> 29182 bytes test_data/chrome_cache/f_000002 | Bin 0 -> 24524 bytes test_data/chrome_cache/f_000003 | Bin 0 -> 60554 bytes test_data/chrome_cache/f_000004 | Bin 0 -> 21704 bytes test_data/chrome_cache/f_000005 | Bin 0 -> 20544 bytes test_data/chrome_cache/f_000006 | Bin 0 -> 21744 bytes test_data/chrome_cache/f_000007 | Bin 0 -> 21272 bytes test_data/chrome_cache/f_000008 | Bin 0 -> 40032 bytes test_data/chrome_cache/f_000009 | Bin 0 -> 19073 bytes test_data/chrome_cache/f_00000a | Bin 0 -> 28652 bytes test_data/chrome_cache/f_00000b | Bin 0 -> 49222 bytes test_data/chrome_cache/f_00000c | Bin 0 -> 57048 bytes test_data/chrome_cache/f_00000d | Bin 0 -> 73316 bytes test_data/chrome_cache/f_00000e | Bin 0 -> 26457 bytes test_data/chrome_cache/f_00000f | Bin 0 -> 262262 bytes test_data/chrome_cache/f_000010 | Bin 0 -> 25960 bytes test_data/chrome_cache/f_000011 | Bin 0 -> 49267 bytes test_data/chrome_cache/f_000012 | Bin 0 -> 42803 bytes test_data/chrome_cache/f_000014 | Bin 0 -> 53441 bytes test_data/chrome_cache/f_000015 | Bin 0 -> 48239 bytes test_data/chrome_cache/f_000016 | Bin 0 -> 47454 bytes test_data/chrome_cache/f_000017 | Bin 0 -> 22632 bytes test_data/chrome_cache/f_000018 | Bin 0 -> 88370 bytes test_data/chrome_cache/f_000019 | Bin 0 -> 58953 bytes test_data/chrome_cache/f_00001a | Bin 0 -> 29364 bytes test_data/chrome_cache/f_00001b | Bin 0 -> 133782 bytes test_data/chrome_cache/f_00001c | Bin 0 -> 73457 bytes test_data/chrome_cache/f_00001d | Bin 0 -> 27568 bytes test_data/chrome_cache/f_00001e | Bin 0 -> 38170 bytes test_data/chrome_cache/f_00001f | Bin 0 -> 86823 bytes test_data/chrome_cache/f_000020 | Bin 0 -> 41548 bytes test_data/chrome_cache/f_000021 | Bin 0 -> 635870 bytes test_data/chrome_cache/f_000022 | Bin 0 -> 28730 bytes test_data/chrome_cache/f_000023 | Bin 0 -> 59153 bytes test_data/chrome_cache/f_000024 | Bin 0 -> 18991 bytes test_data/chrome_cache/f_000025 | Bin 0 -> 155989 bytes test_data/chrome_cache/f_000026 | Bin 0 -> 36701 bytes test_data/chrome_cache/f_000027 | Bin 0 -> 155990 bytes test_data/chrome_cache/f_000028 | 32 + test_data/chrome_cache/f_000029 | Bin 0 -> 33598 bytes test_data/chrome_cache/f_00002a | Bin 0 -> 27668 bytes test_data/chrome_cache/f_00002b | Bin 0 -> 46297 bytes test_data/chrome_cache/f_00002c | Bin 0 -> 37022 bytes test_data/chrome_cache/f_00002d | Bin 0 -> 90257 bytes test_data/chrome_cache/f_00002e | Bin 0 -> 122175 bytes test_data/chrome_cache/f_00002f | Bin 0 -> 25223 bytes test_data/chrome_cache/f_000030 | Bin 0 -> 121905 bytes test_data/chrome_cache/f_000031 | Bin 0 -> 35677 bytes test_data/chrome_cache/f_000032 | Bin 0 -> 373977 bytes test_data/chrome_cache/f_000033 | Bin 0 -> 91536 bytes test_data/chrome_cache/f_000034 | Bin 0 -> 18783 bytes test_data/chrome_cache/f_000035 | Bin 0 -> 141526 bytes test_data/chrome_cache/f_000036 | Bin 0 -> 36903 bytes test_data/chrome_cache/f_000037 | Bin 0 -> 103958 bytes test_data/chrome_cache/f_000038 | Bin 0 -> 66348 bytes test_data/chrome_cache/f_000039 | Bin 0 -> 450266 bytes test_data/chrome_cache/f_00003a | Bin 0 -> 635870 bytes test_data/chrome_cache/f_00003b | Bin 0 -> 67846 bytes test_data/chrome_cache/f_00003c | Bin 0 -> 295222 bytes test_data/chrome_cache/f_00003d | Bin 0 -> 37053 bytes test_data/chrome_cache/f_00003e | Bin 0 -> 38917 bytes test_data/chrome_cache/f_00003f | Bin 0 -> 342417 bytes test_data/chrome_cache/f_000040 | Bin 0 -> 213317 bytes test_data/chrome_cache/f_000041 | Bin 0 -> 51024 bytes test_data/chrome_cache/f_000042 | Bin 0 -> 412402 bytes test_data/chrome_cache/f_000043 | Bin 0 -> 477554 bytes test_data/chrome_cache/f_000044 | Bin 0 -> 53150 bytes test_data/chrome_cache/f_000045 | Bin 0 -> 72263 bytes test_data/chrome_cache/f_000046 | Bin 0 -> 110438 bytes test_data/chrome_cache/f_000047 | Bin 0 -> 130747 bytes test_data/chrome_cache/f_000048 | Bin 0 -> 30761 bytes test_data/chrome_cache/f_000049 | Bin 0 -> 71630 bytes test_data/chrome_cache/f_00004a | Bin 0 -> 1785682 bytes test_data/chrome_cache/f_00004b | Bin 0 -> 225418 bytes test_data/chrome_cache/f_00004c | Bin 0 -> 348855 bytes test_data/chrome_cache/f_00004d | Bin 0 -> 19379 bytes test_data/chrome_cache/index | Bin 0 -> 262512 bytes .../apdfllckaahabafndbhieahigkjlhalf | 433 +++ .../blpcfgokakmgnkcojhhkbfbldkacnbeo | 444 +++ .../hmjkmjkepdijhoojdojkdfohbdgmmhki | 446 +++ .../icppfcnhkcmnfdhfhphakoifcfokfdhg | 429 +++ .../pjkljhegncpnkpknbcohdijeoejaedia | 426 +++ test_data/com.apple.HIToolbox.plist | Bin 0 -> 287 bytes test_data/com.apple.SoftwareUpdate.plist | Bin 0 -> 666 bytes test_data/com.apple.TimeMachine.plist | Bin 0 -> 1664 bytes test_data/com.apple.airport.preferences.plist | 397 +++ ...ABC0ABC1-ABC0-ABC0-ABC0-ABC0ABC1ABC2.plist | Bin 0 -> 764 bytes test_data/com.apple.iPod.plist | 111 + test_data/com.apple.spotlight.plist | Bin 0 -> 1078 bytes test_data/contacts2.db | Bin 0 -> 33792 bytes test_data/cookies.db | Bin 0 -> 169984 bytes test_data/document_versions.sql | Bin 0 -> 61440 bytes test_data/downloads.sqlite | Bin 0 -> 2048 bytes test_data/empty_file | 0 test_data/example.lnk | Bin 0 -> 1244 bytes test_data/firefox_cache/firefox28/E8D65m01 | Bin 0 -> 4194304 bytes test_data/firefox_cache/firefox28/_CACHE_001_ | Bin 0 -> 4194304 bytes test_data/firefox_cache/firefox28/_CACHE_002_ | Bin 0 -> 4194304 bytes test_data/firefox_cache/firefox28/_CACHE_003_ | Bin 0 -> 4194304 bytes test_data/firefox_cache/firefox3/_CACHE_001_ | Bin 0 -> 25193 bytes test_data/firefox_cache/firefox3/_CACHE_002_ | Bin 0 -> 79596 bytes test_data/firefox_cache/firefox3/_CACHE_003_ | Bin 0 -> 111861 bytes test_data/firefox_cache/invalid_file | 1 + test_data/firefox_cookies.sqlite | Bin 0 -> 524288 bytes test_data/firewall.log | 19 + test_data/global_history.dat | 148 + test_data/iis.log | 15 + test_data/image-split.E01 | Bin 0 -> 1018520 bytes test_data/image-split.E02 | Bin 0 -> 460533 bytes test_data/image.E01 | Bin 0 -> 4469 bytes test_data/image.qcow2 | Bin 0 -> 328704 bytes test_data/image.vhd | Bin 0 -> 2100224 bytes test_data/image.vmdk | Bin 0 -> 131072 bytes test_data/index.dat | Bin 0 -> 32768 bytes test_data/java.idx | Bin 0 -> 675 bytes test_data/java_602.idx | Bin 0 -> 455 bytes test_data/login.keychain | Bin 0 -> 26764 bytes test_data/mac_cups_ipp | Bin 0 -> 2047 bytes test_data/mackeeper_cache.db | Bin 0 -> 1015808 bytes test_data/mactime.body | 17 + test_data/mmssms.db | Bin 0 -> 102400 bytes test_data/nobody.plist | Bin 0 -> 270 bytes test_data/openbsm.bsm | Bin 0 -> 1792 bytes test_data/places.sqlite | Bin 0 -> 163840 bytes test_data/places_new.sqlite | Bin 0 -> 10485760 bytes test_data/plist_binary | Bin 0 -> 4344 bytes test_data/popcontest1.log | 19 + test_data/psort_test.out | Bin 0 -> 7218 bytes test_data/quarantine.db | Bin 0 -> 24576 bytes test_data/security.log | 9 + test_data/selinux.log | 10 + test_data/skydrive.log | 22 + test_data/skydriveerr-unicode.log | 28 + test_data/skydriveerr.log | 28 + test_data/skype_main.db | Bin 0 -> 344064 bytes test_data/snapshot.db | Bin 0 -> 23552 bytes test_data/syslog | 16 + test_data/syslog.bz2 | Bin 0 -> 608 bytes test_data/syslog.gz | Bin 0 -> 540 bytes test_data/syslog.tar | Bin 0 -> 10240 bytes test_data/syslog.tgz | Bin 0 -> 639 bytes test_data/syslog.zip | Bin 0 -> 900 bytes test_data/syslog_copy | 15 + test_data/syslog_image.dd | Bin 0 -> 102400 bytes test_data/test.pcap | Bin 0 -> 1276502 bytes test_data/testdir/filter2.txt | 1 + test_data/testdir/filter_1.txt | 1 + test_data/testdir/filter_3.txt | 1 + test_data/text_parser/test1.txt | 3 + test_data/text_parser/test2.txt | 3 + test_data/tsk_volume_system.raw | Bin 0 -> 1474560 bytes test_data/typed_history.xml | 11 + test_data/usage-history.xml | 59 + test_data/user.plist | Bin 0 -> 1451 bytes test_data/utmp | Bin 0 -> 5376 bytes test_data/utmpx_mac | Bin 0 -> 4396 bytes test_data/vsstest.qcow2 | Bin 0 -> 751104 bytes test_data/wifi.log | 10 + test_data/wintask.job | Bin 0 -> 896 bytes test_data/wtmp.1 | Bin 0 -> 1537 bytes test_data/xchat.log | 22 + test_data/xchatscrollback.log | 11 + test_data/ímynd.dd | Bin 0 -> 102400 bytes tools/README.tools | 8 + tools/__init__.py | 17 + tools/plaso_extract_search_history.py | 242 ++ tools/plaso_process_info.py | 254 ++ utils/build_dependencies.py | 2789 +++++++++++++++++ utils/check_dependencies.py | 387 +++ utils/common.sh | 84 + utils/compile_proto.sh | 24 + utils/create_authors.py | 81 + utils/dependencies.ini | 164 + utils/download_patch_set.py | 115 + utils/doxygen.conf | 1781 +++++++++++ utils/example_vimrc | 51 + utils/git-cl | 871 +++++ utils/prep_dist.sh | 45 + utils/pylintrc | 289 ++ utils/pylintrc-1.1.0 | 288 ++ utils/review.sh | 165 + utils/run_linter.sh | 39 + utils/run_tests.sh | 89 + utils/submit.sh | 213 ++ utils/update.sh | 129 + utils/update_dependencies.py | 656 ++++ utils/upload.py | 2645 ++++++++++++++++ 762 files changed, 103065 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 ACKNOWLEDGEMENTS create mode 100644 AUTHORS create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README create mode 100644 README.md create mode 100644 config/dpkg/debian/changelog create mode 100644 config/dpkg/debian/compat create mode 100644 config/dpkg/debian/control create mode 100644 config/dpkg/debian/copyright create mode 100644 config/dpkg/debian/python-plaso.docs create mode 100755 config/dpkg/debian/rules create mode 100644 config/licenses/LICENSE.PyYAML create mode 100644 config/licenses/LICENSE.bencode create mode 100644 config/licenses/LICENSE.binplist create mode 100644 config/licenses/LICENSE.construct create mode 100644 config/licenses/LICENSE.dateutil-parser create mode 100644 config/licenses/LICENSE.dfvfs create mode 100644 config/licenses/LICENSE.dpkt create mode 100644 config/licenses/LICENSE.ipython create mode 100644 config/licenses/LICENSE.libbde create mode 100644 config/licenses/LICENSE.libesedb create mode 100644 config/licenses/LICENSE.libevt create mode 100644 config/licenses/LICENSE.libevtx create mode 100644 config/licenses/LICENSE.libewf create mode 100644 config/licenses/LICENSE.libfwsi create mode 100644 config/licenses/LICENSE.liblnk create mode 100644 config/licenses/LICENSE.libmsiecf create mode 100644 config/licenses/LICENSE.libolecf create mode 100644 config/licenses/LICENSE.libqcow create mode 100644 config/licenses/LICENSE.libregf create mode 100644 config/licenses/LICENSE.libsmdev create mode 100644 config/licenses/LICENSE.libsmraw create mode 100644 config/licenses/LICENSE.libvhdi create mode 100644 config/licenses/LICENSE.libvmdk create mode 100644 config/licenses/LICENSE.libvshadow create mode 100644 config/licenses/LICENSE.libyaml create mode 100644 config/licenses/LICENSE.protobuf create mode 100644 config/licenses/LICENSE.psutil create mode 100644 config/licenses/LICENSE.pyelasticsearch create mode 100644 config/licenses/LICENSE.pyinstaller create mode 100644 config/licenses/LICENSE.pyparsing create mode 100644 config/licenses/LICENSE.pysqlite create mode 100644 config/licenses/LICENSE.python create mode 100644 config/licenses/LICENSE.pytsk create mode 100644 config/licenses/LICENSE.pytz create mode 100644 config/licenses/LICENSE.pywin32 create mode 100644 config/licenses/LICENSE.six create mode 100644 config/licenses/LICENSE.sleuthkit.IBM create mode 100644 config/licenses/LICENSE.sleuthkit.cpl1.0 create mode 100644 config/licenses/LICENSE.talloc create mode 100644 config/logo.jpg create mode 100644 config/macosx/Readme.txt create mode 100755 config/macosx/install.sh.in create mode 100755 config/macosx/make_dist.sh create mode 100644 config/windows/make.bat create mode 100644 config/windows/make_check.bat create mode 100644 config/windows/make_dist.bat create mode 100644 extra/README create mode 100644 extra/plaso_kibana_example.json create mode 100755 extra/tag_macosx.txt create mode 100755 extra/tag_windows.txt create mode 100644 plaso/__init__.py create mode 100644 plaso/analysis/__init__.py create mode 100644 plaso/analysis/browser_search.py create mode 100644 plaso/analysis/browser_search_test.py create mode 100644 plaso/analysis/chrome_extension.py create mode 100644 plaso/analysis/chrome_extension_test.py create mode 100644 plaso/analysis/context.py create mode 100644 plaso/analysis/context_test.py create mode 100644 plaso/analysis/interface.py create mode 100644 plaso/analysis/test_lib.py create mode 100644 plaso/analysis/windows_services.py create mode 100644 plaso/analysis/windows_services_test.py create mode 100644 plaso/artifacts/__init__.py create mode 100644 plaso/artifacts/knowledge_base.py create mode 100644 plaso/classifier/__init__.py create mode 100644 plaso/classifier/classifier.py create mode 100644 plaso/classifier/classifier_test.py create mode 100644 plaso/classifier/classify.py create mode 100644 plaso/classifier/patterns.py create mode 100644 plaso/classifier/range_list.py create mode 100644 plaso/classifier/range_list_test.py create mode 100644 plaso/classifier/scan_tree.py create mode 100644 plaso/classifier/scan_tree_test.py create mode 100644 plaso/classifier/scanner.py create mode 100644 plaso/classifier/scanner_test.py create mode 100644 plaso/classifier/specification.py create mode 100644 plaso/classifier/specification_test.py create mode 100644 plaso/classifier/test_lib.py create mode 100644 plaso/engine/__init__.py create mode 100644 plaso/engine/classifier.py create mode 100644 plaso/engine/collector.py create mode 100644 plaso/engine/collector_test.py create mode 100644 plaso/engine/engine.py create mode 100644 plaso/engine/queue.py create mode 100644 plaso/engine/single_process.py create mode 100644 plaso/engine/single_process_test.py create mode 100644 plaso/engine/test_lib.py create mode 100644 plaso/engine/utils.py create mode 100644 plaso/engine/worker.py create mode 100644 plaso/events/__init__.py create mode 100644 plaso/events/plist_event.py create mode 100644 plaso/events/shell_item_events.py create mode 100644 plaso/events/text_events.py create mode 100644 plaso/events/time_events.py create mode 100644 plaso/events/windows_events.py create mode 100644 plaso/filters/__init__.py create mode 100644 plaso/filters/dynamic_filter.py create mode 100644 plaso/filters/dynamic_filter_test.py create mode 100644 plaso/filters/eventfilter.py create mode 100644 plaso/filters/eventfilter_test.py create mode 100644 plaso/filters/filterlist.py create mode 100644 plaso/filters/filterlist_test.py create mode 100644 plaso/filters/test_helper.py create mode 100644 plaso/formatters/__init__.py create mode 100644 plaso/formatters/android_app_usage.py create mode 100644 plaso/formatters/android_calls.py create mode 100644 plaso/formatters/android_sms.py create mode 100644 plaso/formatters/appcompatcache.py create mode 100644 plaso/formatters/appusage.py create mode 100644 plaso/formatters/asl.py create mode 100644 plaso/formatters/bencode_parser.py create mode 100644 plaso/formatters/bsm.py create mode 100644 plaso/formatters/chrome.py create mode 100644 plaso/formatters/chrome_cache.py create mode 100644 plaso/formatters/chrome_cookies.py create mode 100644 plaso/formatters/chrome_extension_activity.py create mode 100644 plaso/formatters/cups_ipp.py create mode 100644 plaso/formatters/filestat.py create mode 100644 plaso/formatters/firefox.py create mode 100644 plaso/formatters/firefox_cache.py create mode 100644 plaso/formatters/firefox_cookies.py create mode 100644 plaso/formatters/ganalytics.py create mode 100644 plaso/formatters/gdrive.py create mode 100644 plaso/formatters/hachoir.py create mode 100644 plaso/formatters/iis.py create mode 100644 plaso/formatters/interface.py create mode 100644 plaso/formatters/ipod.py create mode 100644 plaso/formatters/java_idx.py create mode 100644 plaso/formatters/ls_quarantine.py create mode 100644 plaso/formatters/mac_appfirewall.py create mode 100644 plaso/formatters/mac_document_versions.py create mode 100644 plaso/formatters/mac_keychain.py create mode 100644 plaso/formatters/mac_securityd.py create mode 100644 plaso/formatters/mac_wifi.py create mode 100644 plaso/formatters/mackeeper_cache.py create mode 100644 plaso/formatters/mactime.py create mode 100644 plaso/formatters/manager.py create mode 100644 plaso/formatters/manager_test.py create mode 100644 plaso/formatters/mcafeeav.py create mode 100644 plaso/formatters/msie_webcache.py create mode 100644 plaso/formatters/msiecf.py create mode 100644 plaso/formatters/olecf.py create mode 100644 plaso/formatters/opera.py create mode 100644 plaso/formatters/oxml.py create mode 100644 plaso/formatters/pcap.py create mode 100644 plaso/formatters/plist.py create mode 100644 plaso/formatters/pls_recall.py create mode 100644 plaso/formatters/popcontest.py create mode 100644 plaso/formatters/recycler.py create mode 100755 plaso/formatters/rubanetra.py create mode 100644 plaso/formatters/safari.py create mode 100644 plaso/formatters/selinux.py create mode 100644 plaso/formatters/shell_items.py create mode 100644 plaso/formatters/skydrivelog.py create mode 100644 plaso/formatters/skydrivelogerr.py create mode 100644 plaso/formatters/skype.py create mode 100644 plaso/formatters/symantec.py create mode 100644 plaso/formatters/syslog.py create mode 100644 plaso/formatters/task_scheduler.py create mode 100644 plaso/formatters/text.py create mode 100644 plaso/formatters/utmp.py create mode 100644 plaso/formatters/utmpx.py create mode 100644 plaso/formatters/windows.py create mode 100644 plaso/formatters/winevt.py create mode 100644 plaso/formatters/winevtx.py create mode 100644 plaso/formatters/winfirewall.py create mode 100644 plaso/formatters/winjob.py create mode 100644 plaso/formatters/winlnk.py create mode 100644 plaso/formatters/winprefetch.py create mode 100644 plaso/formatters/winreg.py create mode 100644 plaso/formatters/winregservice.py create mode 100644 plaso/formatters/xchatlog.py create mode 100644 plaso/formatters/xchatscrollback.py create mode 100644 plaso/formatters/zeitgeist.py create mode 100755 plaso/frontend/__init__.py create mode 100755 plaso/frontend/frontend.py create mode 100644 plaso/frontend/frontend_test.py create mode 100755 plaso/frontend/image_export.py create mode 100644 plaso/frontend/image_export_test.py create mode 100755 plaso/frontend/log2timeline.py create mode 100644 plaso/frontend/log2timeline_test.py create mode 100755 plaso/frontend/pinfo.py create mode 100644 plaso/frontend/pinfo_test.py create mode 100755 plaso/frontend/plasm.py create mode 100644 plaso/frontend/plasm_test.py create mode 100755 plaso/frontend/pprof.py create mode 100755 plaso/frontend/preg.py create mode 100644 plaso/frontend/preg_test.py create mode 100644 plaso/frontend/presets.py create mode 100755 plaso/frontend/pshell.py create mode 100755 plaso/frontend/psort.py create mode 100644 plaso/frontend/psort_test.py create mode 100644 plaso/frontend/test_lib.py create mode 100644 plaso/frontend/utils.py create mode 100644 plaso/lib/__init__.py create mode 100644 plaso/lib/binary.py create mode 100644 plaso/lib/binary_test.py create mode 100644 plaso/lib/bufferlib.py create mode 100644 plaso/lib/bufferlib_test.py create mode 100644 plaso/lib/errors.py create mode 100644 plaso/lib/event.py create mode 100644 plaso/lib/event_test.py create mode 100644 plaso/lib/eventdata.py create mode 100644 plaso/lib/filter_interface.py create mode 100644 plaso/lib/lexer.py create mode 100644 plaso/lib/limit.py create mode 100644 plaso/lib/objectfilter.py create mode 100644 plaso/lib/objectfilter_test.py create mode 100644 plaso/lib/output.py create mode 100644 plaso/lib/output_test.py create mode 100644 plaso/lib/pfilter.py create mode 100644 plaso/lib/pfilter_test.py create mode 100644 plaso/lib/proxy.py create mode 100644 plaso/lib/putils.py create mode 100644 plaso/lib/registry.py create mode 100644 plaso/lib/storage.py create mode 100644 plaso/lib/storage_test.py create mode 100644 plaso/lib/timelib.py create mode 100644 plaso/lib/timelib_test.py create mode 100644 plaso/lib/utils.py create mode 100644 plaso/lib/utils_test.py create mode 100644 plaso/multi_processing/__init__.py create mode 100644 plaso/multi_processing/foreman.py create mode 100644 plaso/multi_processing/multi_process.py create mode 100644 plaso/multi_processing/multi_process_test.py create mode 100644 plaso/multi_processing/process_info.py create mode 100644 plaso/multi_processing/rpc_proxy.py create mode 100644 plaso/output/__init__.py create mode 100644 plaso/output/dynamic.py create mode 100644 plaso/output/dynamic_test.py create mode 100644 plaso/output/elastic.py create mode 100644 plaso/output/helper.py create mode 100644 plaso/output/json_out.py create mode 100644 plaso/output/json_out_test.py create mode 100644 plaso/output/l2t_csv.py create mode 100644 plaso/output/l2t_csv_test.py create mode 100644 plaso/output/l2t_tln.py create mode 100644 plaso/output/l2t_tln_test.py create mode 100644 plaso/output/mysql_4n6.py create mode 100644 plaso/output/pstorage.py create mode 100644 plaso/output/pstorage_test.py create mode 100644 plaso/output/rawpy.py create mode 100644 plaso/output/sqlite_4n6.py create mode 100644 plaso/output/tln.py create mode 100644 plaso/output/tln_test.py create mode 100644 plaso/parsers/__init__.py create mode 100644 plaso/parsers/android_app_usage.py create mode 100644 plaso/parsers/android_app_usage_test.py create mode 100644 plaso/parsers/asl.py create mode 100644 plaso/parsers/asl_test.py create mode 100644 plaso/parsers/bencode_parser.py create mode 100644 plaso/parsers/bencode_parser_test.py create mode 100644 plaso/parsers/bencode_plugins/__init__.py create mode 100644 plaso/parsers/bencode_plugins/interface.py create mode 100644 plaso/parsers/bencode_plugins/test_lib.py create mode 100644 plaso/parsers/bencode_plugins/transmission.py create mode 100644 plaso/parsers/bencode_plugins/utorrent.py create mode 100644 plaso/parsers/bsm.py create mode 100644 plaso/parsers/bsm_test.py create mode 100644 plaso/parsers/chrome_cache.py create mode 100644 plaso/parsers/chrome_cache_test.py create mode 100644 plaso/parsers/context.py create mode 100644 plaso/parsers/cookie_plugins/__init__.py create mode 100644 plaso/parsers/cookie_plugins/ganalytics.py create mode 100644 plaso/parsers/cookie_plugins/ganalytics_test.py create mode 100644 plaso/parsers/cookie_plugins/interface.py create mode 100644 plaso/parsers/cookie_plugins/test_lib.py create mode 100644 plaso/parsers/cups_ipp.py create mode 100644 plaso/parsers/cups_ipp_test.py create mode 100644 plaso/parsers/custom_destinations.py create mode 100644 plaso/parsers/custom_destinations_test.py create mode 100644 plaso/parsers/esedb.py create mode 100644 plaso/parsers/esedb_plugins/__init__.py create mode 100644 plaso/parsers/esedb_plugins/interface.py create mode 100644 plaso/parsers/esedb_plugins/msie_webcache.py create mode 100644 plaso/parsers/esedb_plugins/msie_webcache_test.py create mode 100644 plaso/parsers/esedb_plugins/test_lib.py create mode 100644 plaso/parsers/filestat.py create mode 100644 plaso/parsers/filestat_test.py create mode 100644 plaso/parsers/firefox_cache.py create mode 100644 plaso/parsers/firefox_cache_test.py create mode 100644 plaso/parsers/hachoir.py create mode 100644 plaso/parsers/iis.py create mode 100644 plaso/parsers/iis_test.py create mode 100644 plaso/parsers/interface.py create mode 100644 plaso/parsers/java_idx.py create mode 100644 plaso/parsers/java_idx_test.py create mode 100644 plaso/parsers/mac_appfirewall.py create mode 100644 plaso/parsers/mac_appfirewall_test.py create mode 100644 plaso/parsers/mac_keychain.py create mode 100644 plaso/parsers/mac_keychain_test.py create mode 100644 plaso/parsers/mac_securityd.py create mode 100644 plaso/parsers/mac_securityd_test.py create mode 100644 plaso/parsers/mac_wifi.py create mode 100644 plaso/parsers/mac_wifi_test.py create mode 100644 plaso/parsers/mactime.py create mode 100644 plaso/parsers/mactime_test.py create mode 100644 plaso/parsers/manager.py create mode 100644 plaso/parsers/manager_test.py create mode 100644 plaso/parsers/mcafeeav.py create mode 100644 plaso/parsers/mcafeeav_test.py create mode 100644 plaso/parsers/msiecf.py create mode 100644 plaso/parsers/msiecf_test.py create mode 100644 plaso/parsers/olecf.py create mode 100644 plaso/parsers/olecf_plugins/__init__.py create mode 100644 plaso/parsers/olecf_plugins/automatic_destinations.py create mode 100644 plaso/parsers/olecf_plugins/automatic_destinations_test.py create mode 100644 plaso/parsers/olecf_plugins/default.py create mode 100644 plaso/parsers/olecf_plugins/default_test.py create mode 100644 plaso/parsers/olecf_plugins/interface.py create mode 100644 plaso/parsers/olecf_plugins/summary.py create mode 100644 plaso/parsers/olecf_plugins/summary_test.py create mode 100644 plaso/parsers/olecf_plugins/test_lib.py create mode 100644 plaso/parsers/opera.py create mode 100644 plaso/parsers/opera_test.py create mode 100644 plaso/parsers/oxml.py create mode 100644 plaso/parsers/oxml_test.py create mode 100644 plaso/parsers/pcap.py create mode 100644 plaso/parsers/pcap_test.py create mode 100644 plaso/parsers/plist.py create mode 100644 plaso/parsers/plist_plugins/__init__.py create mode 100644 plaso/parsers/plist_plugins/airport.py create mode 100644 plaso/parsers/plist_plugins/airport_test.py create mode 100644 plaso/parsers/plist_plugins/appleaccount.py create mode 100644 plaso/parsers/plist_plugins/appleaccount_test.py create mode 100644 plaso/parsers/plist_plugins/bluetooth.py create mode 100644 plaso/parsers/plist_plugins/bluetooth_test.py create mode 100644 plaso/parsers/plist_plugins/default.py create mode 100644 plaso/parsers/plist_plugins/default_test.py create mode 100644 plaso/parsers/plist_plugins/install_history.py create mode 100644 plaso/parsers/plist_plugins/install_history_test.py create mode 100644 plaso/parsers/plist_plugins/interface.py create mode 100644 plaso/parsers/plist_plugins/interface_test.py create mode 100644 plaso/parsers/plist_plugins/ipod.py create mode 100644 plaso/parsers/plist_plugins/ipod_test.py create mode 100644 plaso/parsers/plist_plugins/macuser.py create mode 100644 plaso/parsers/plist_plugins/macuser_test.py create mode 100644 plaso/parsers/plist_plugins/safari.py create mode 100644 plaso/parsers/plist_plugins/safari_test.py create mode 100644 plaso/parsers/plist_plugins/softwareupdate.py create mode 100644 plaso/parsers/plist_plugins/softwareupdate_test.py create mode 100644 plaso/parsers/plist_plugins/spotlight.py create mode 100644 plaso/parsers/plist_plugins/spotlight_test.py create mode 100644 plaso/parsers/plist_plugins/spotlight_volume.py create mode 100644 plaso/parsers/plist_plugins/spotlight_volume_test.py create mode 100644 plaso/parsers/plist_plugins/test_lib.py create mode 100644 plaso/parsers/plist_plugins/timemachine.py create mode 100644 plaso/parsers/plist_plugins/timemachine_test.py create mode 100644 plaso/parsers/plist_test.py create mode 100644 plaso/parsers/pls_recall.py create mode 100644 plaso/parsers/pls_recall_test.py create mode 100644 plaso/parsers/plugins.py create mode 100644 plaso/parsers/popcontest.py create mode 100644 plaso/parsers/popcontest_test.py create mode 100644 plaso/parsers/recycler.py create mode 100644 plaso/parsers/recycler_test.py create mode 100755 plaso/parsers/rubanetra.py create mode 100644 plaso/parsers/selinux.py create mode 100644 plaso/parsers/selinux_test.py create mode 100644 plaso/parsers/shared/__init__.py create mode 100644 plaso/parsers/shared/shell_items.py create mode 100644 plaso/parsers/skydrivelog.py create mode 100644 plaso/parsers/skydrivelog_test.py create mode 100644 plaso/parsers/skydrivelogerr.py create mode 100644 plaso/parsers/skydrivelogerr_test.py create mode 100644 plaso/parsers/sqlite.py create mode 100644 plaso/parsers/sqlite_plugins/__init__.py create mode 100644 plaso/parsers/sqlite_plugins/android_calls.py create mode 100644 plaso/parsers/sqlite_plugins/android_calls_test.py create mode 100644 plaso/parsers/sqlite_plugins/android_sms.py create mode 100644 plaso/parsers/sqlite_plugins/android_sms_test.py create mode 100644 plaso/parsers/sqlite_plugins/appusage.py create mode 100644 plaso/parsers/sqlite_plugins/appusage_test.py create mode 100644 plaso/parsers/sqlite_plugins/chrome.py create mode 100644 plaso/parsers/sqlite_plugins/chrome_cookies.py create mode 100644 plaso/parsers/sqlite_plugins/chrome_cookies_test.py create mode 100644 plaso/parsers/sqlite_plugins/chrome_extension_activity.py create mode 100644 plaso/parsers/sqlite_plugins/chrome_extension_activity_test.py create mode 100644 plaso/parsers/sqlite_plugins/chrome_test.py create mode 100644 plaso/parsers/sqlite_plugins/firefox.py create mode 100644 plaso/parsers/sqlite_plugins/firefox_cookies.py create mode 100644 plaso/parsers/sqlite_plugins/firefox_cookies_test.py create mode 100644 plaso/parsers/sqlite_plugins/firefox_test.py create mode 100644 plaso/parsers/sqlite_plugins/gdrive.py create mode 100644 plaso/parsers/sqlite_plugins/gdrive_test.py create mode 100644 plaso/parsers/sqlite_plugins/interface.py create mode 100644 plaso/parsers/sqlite_plugins/ls_quarantine.py create mode 100644 plaso/parsers/sqlite_plugins/ls_quarantine_test.py create mode 100644 plaso/parsers/sqlite_plugins/mac_document_versions.py create mode 100644 plaso/parsers/sqlite_plugins/mac_document_versions_test.py create mode 100644 plaso/parsers/sqlite_plugins/mackeeper_cache.py create mode 100644 plaso/parsers/sqlite_plugins/mackeeper_cache_test.py create mode 100644 plaso/parsers/sqlite_plugins/skype.py create mode 100644 plaso/parsers/sqlite_plugins/skype_test.py create mode 100644 plaso/parsers/sqlite_plugins/test_lib.py create mode 100644 plaso/parsers/sqlite_plugins/zeitgeist.py create mode 100644 plaso/parsers/sqlite_plugins/zeitgeist_test.py create mode 100644 plaso/parsers/sqlite_test.py create mode 100644 plaso/parsers/symantec.py create mode 100644 plaso/parsers/symantec_test.py create mode 100644 plaso/parsers/syslog.py create mode 100644 plaso/parsers/syslog_test.py create mode 100644 plaso/parsers/test_lib.py create mode 100644 plaso/parsers/text_parser.py create mode 100644 plaso/parsers/text_parser_test.py create mode 100644 plaso/parsers/utmp.py create mode 100644 plaso/parsers/utmp_test.py create mode 100644 plaso/parsers/utmpx.py create mode 100644 plaso/parsers/utmpx_test.py create mode 100644 plaso/parsers/winevt.py create mode 100644 plaso/parsers/winevt_test.py create mode 100644 plaso/parsers/winevtx.py create mode 100644 plaso/parsers/winevtx_test.py create mode 100644 plaso/parsers/winfirewall.py create mode 100644 plaso/parsers/winfirewall_test.py create mode 100644 plaso/parsers/winjob.py create mode 100644 plaso/parsers/winjob_test.py create mode 100644 plaso/parsers/winlnk.py create mode 100644 plaso/parsers/winlnk_test.py create mode 100644 plaso/parsers/winprefetch.py create mode 100644 plaso/parsers/winprefetch_test.py create mode 100644 plaso/parsers/winreg.py create mode 100644 plaso/parsers/winreg_plugins/__init__.py create mode 100644 plaso/parsers/winreg_plugins/appcompatcache.py create mode 100644 plaso/parsers/winreg_plugins/appcompatcache_test.py create mode 100644 plaso/parsers/winreg_plugins/bagmru.py create mode 100644 plaso/parsers/winreg_plugins/bagmru_test.py create mode 100644 plaso/parsers/winreg_plugins/ccleaner.py create mode 100644 plaso/parsers/winreg_plugins/ccleaner_test.py create mode 100644 plaso/parsers/winreg_plugins/default.py create mode 100644 plaso/parsers/winreg_plugins/default_test.py create mode 100644 plaso/parsers/winreg_plugins/interface.py create mode 100644 plaso/parsers/winreg_plugins/lfu.py create mode 100644 plaso/parsers/winreg_plugins/lfu_test.py create mode 100644 plaso/parsers/winreg_plugins/mountpoints.py create mode 100644 plaso/parsers/winreg_plugins/mountpoints_test.py create mode 100644 plaso/parsers/winreg_plugins/mrulist.py create mode 100644 plaso/parsers/winreg_plugins/mrulist_test.py create mode 100644 plaso/parsers/winreg_plugins/mrulistex.py create mode 100644 plaso/parsers/winreg_plugins/mrulistex_test.py create mode 100644 plaso/parsers/winreg_plugins/msie_zones.py create mode 100644 plaso/parsers/winreg_plugins/msie_zones_test.py create mode 100644 plaso/parsers/winreg_plugins/officemru.py create mode 100644 plaso/parsers/winreg_plugins/officemru_test.py create mode 100644 plaso/parsers/winreg_plugins/outlook.py create mode 100644 plaso/parsers/winreg_plugins/outlook_test.py create mode 100644 plaso/parsers/winreg_plugins/run.py create mode 100644 plaso/parsers/winreg_plugins/run_test.py create mode 100644 plaso/parsers/winreg_plugins/sam_users.py create mode 100644 plaso/parsers/winreg_plugins/sam_users_test.py create mode 100644 plaso/parsers/winreg_plugins/services.py create mode 100644 plaso/parsers/winreg_plugins/services_test.py create mode 100644 plaso/parsers/winreg_plugins/shutdown.py create mode 100644 plaso/parsers/winreg_plugins/shutdown_test.py create mode 100644 plaso/parsers/winreg_plugins/task_scheduler.py create mode 100644 plaso/parsers/winreg_plugins/task_scheduler_test.py create mode 100644 plaso/parsers/winreg_plugins/terminal_server.py create mode 100644 plaso/parsers/winreg_plugins/terminal_server_test.py create mode 100644 plaso/parsers/winreg_plugins/test_lib.py create mode 100644 plaso/parsers/winreg_plugins/typedurls.py create mode 100644 plaso/parsers/winreg_plugins/typedurls_test.py create mode 100644 plaso/parsers/winreg_plugins/usb.py create mode 100644 plaso/parsers/winreg_plugins/usb_test.py create mode 100644 plaso/parsers/winreg_plugins/usbstor.py create mode 100644 plaso/parsers/winreg_plugins/usbstor_test.py create mode 100644 plaso/parsers/winreg_plugins/userassist.py create mode 100644 plaso/parsers/winreg_plugins/userassist_test.py create mode 100644 plaso/parsers/winreg_plugins/winrar.py create mode 100644 plaso/parsers/winreg_plugins/winrar_test.py create mode 100644 plaso/parsers/winreg_plugins/winver.py create mode 100644 plaso/parsers/winreg_plugins/winver_test.py create mode 100644 plaso/parsers/winreg_test.py create mode 100644 plaso/parsers/xchatlog.py create mode 100644 plaso/parsers/xchatlog_test.py create mode 100644 plaso/parsers/xchatscrollback.py create mode 100644 plaso/parsers/xchatscrollback_test.py create mode 100644 plaso/preprocessors/__init__.py create mode 100644 plaso/preprocessors/interface.py create mode 100644 plaso/preprocessors/linux.py create mode 100644 plaso/preprocessors/linux_test.py create mode 100644 plaso/preprocessors/macosx.py create mode 100644 plaso/preprocessors/macosx_test.py create mode 100644 plaso/preprocessors/manager.py create mode 100644 plaso/preprocessors/manager_test.py create mode 100644 plaso/preprocessors/test_lib.py create mode 100644 plaso/preprocessors/windows.py create mode 100644 plaso/preprocessors/windows_test.py create mode 100644 plaso/proto/__init__.py create mode 100644 plaso/proto/plaso_storage.proto create mode 100644 plaso/proto/plaso_storage_pb2.py create mode 100644 plaso/serializer/__init__.py create mode 100644 plaso/serializer/interface.py create mode 100644 plaso/serializer/json_serializer.py create mode 100644 plaso/serializer/json_serializer_test.py create mode 100644 plaso/serializer/protobuf_serializer.py create mode 100644 plaso/serializer/protobuf_serializer_test.py create mode 100644 plaso/unix/__init__.py create mode 100644 plaso/unix/bsmtoken.py create mode 100644 plaso/winnt/__init__.py create mode 100644 plaso/winnt/environ_expand.py create mode 100644 plaso/winnt/human_readable_service_enums.py create mode 100644 plaso/winnt/known_folder_ids.py create mode 100644 plaso/winnt/shell_folder_ids.py create mode 100644 plaso/winreg/__init__.py create mode 100644 plaso/winreg/cache.py create mode 100644 plaso/winreg/cache_test.py create mode 100644 plaso/winreg/interface.py create mode 100644 plaso/winreg/path_expander.py create mode 100644 plaso/winreg/test_lib.py create mode 100644 plaso/winreg/utils.py create mode 100644 plaso/winreg/winpyregf.py create mode 100644 plaso/winreg/winpyregf_test.py create mode 100644 plaso/winreg/winregistry.py create mode 100644 plaso/winreg/winregistry_test.py create mode 100755 plasov1.2.0-rubanetra0.0.6-distribution.zip create mode 100755 run_tests.py create mode 100644 setup.cfg create mode 100755 setup.py create mode 100644 test_data/$II3DF3L.zip create mode 100644 test_data/1b4dd67f29cb1962.automaticDestinations-ms create mode 100644 test_data/5afe4de1b92fc382.customDestinations-ms create mode 100644 test_data/AccessProtectionLog.txt create mode 100755 test_data/CMD.EXE-087B4001.pf create mode 100644 test_data/Document.doc create mode 100644 test_data/Document.docx create mode 100644 test_data/Extension Activity create mode 100644 test_data/History create mode 100644 test_data/History.plist create mode 100644 test_data/INFO2 create mode 100644 test_data/InstallHistory.plist create mode 100755 test_data/NTUSER-CCLEANER.DAT create mode 100644 test_data/NTUSER-RunTests.DAT create mode 100644 test_data/NTUSER-WIN7.DAT create mode 100644 test_data/NTUSER.DAT create mode 100755 test_data/NeroInfoTool.lnk create mode 100644 test_data/PING.EXE-B29F6629.pf create mode 100644 test_data/PLSRecall_Test.dat create mode 100644 test_data/SAM create mode 100644 test_data/SOFTWARE create mode 100644 test_data/SOFTWARE-RunTests create mode 100644 test_data/SYSTEM create mode 100644 test_data/Symantec.Log create mode 100644 test_data/SysEvent.Evt create mode 100644 test_data/System.evtx create mode 100755 test_data/TASKHOST.EXE-3AE259FC.pf create mode 100644 test_data/VolumeConfiguration.plist create mode 100644 test_data/WUAUCLT.EXE-830BCC14.pf create mode 100755 test_data/WebCacheV01.dat create mode 100755 test_data/Windows.edb create mode 100644 test_data/__init__.py create mode 100644 test_data/activity.sqlite create mode 100644 test_data/appfirewall.log create mode 100644 test_data/apple.bsm create mode 100644 test_data/applesystemlog.asl create mode 100644 test_data/application_usage.sqlite create mode 100644 test_data/bencode_transmission create mode 100755 test_data/bencode_utorrent create mode 100644 test_data/chrome_cache/data_0 create mode 100644 test_data/chrome_cache/data_1 create mode 100644 test_data/chrome_cache/data_2 create mode 100644 test_data/chrome_cache/data_3 create mode 100644 test_data/chrome_cache/f_000001 create mode 100644 test_data/chrome_cache/f_000002 create mode 100644 test_data/chrome_cache/f_000003 create mode 100644 test_data/chrome_cache/f_000004 create mode 100644 test_data/chrome_cache/f_000005 create mode 100644 test_data/chrome_cache/f_000006 create mode 100644 test_data/chrome_cache/f_000007 create mode 100644 test_data/chrome_cache/f_000008 create mode 100644 test_data/chrome_cache/f_000009 create mode 100644 test_data/chrome_cache/f_00000a create mode 100644 test_data/chrome_cache/f_00000b create mode 100644 test_data/chrome_cache/f_00000c create mode 100644 test_data/chrome_cache/f_00000d create mode 100644 test_data/chrome_cache/f_00000e create mode 100644 test_data/chrome_cache/f_00000f create mode 100644 test_data/chrome_cache/f_000010 create mode 100644 test_data/chrome_cache/f_000011 create mode 100644 test_data/chrome_cache/f_000012 create mode 100644 test_data/chrome_cache/f_000014 create mode 100644 test_data/chrome_cache/f_000015 create mode 100644 test_data/chrome_cache/f_000016 create mode 100644 test_data/chrome_cache/f_000017 create mode 100644 test_data/chrome_cache/f_000018 create mode 100644 test_data/chrome_cache/f_000019 create mode 100644 test_data/chrome_cache/f_00001a create mode 100644 test_data/chrome_cache/f_00001b create mode 100644 test_data/chrome_cache/f_00001c create mode 100644 test_data/chrome_cache/f_00001d create mode 100644 test_data/chrome_cache/f_00001e create mode 100644 test_data/chrome_cache/f_00001f create mode 100644 test_data/chrome_cache/f_000020 create mode 100644 test_data/chrome_cache/f_000021 create mode 100644 test_data/chrome_cache/f_000022 create mode 100644 test_data/chrome_cache/f_000023 create mode 100644 test_data/chrome_cache/f_000024 create mode 100644 test_data/chrome_cache/f_000025 create mode 100644 test_data/chrome_cache/f_000026 create mode 100644 test_data/chrome_cache/f_000027 create mode 100644 test_data/chrome_cache/f_000028 create mode 100644 test_data/chrome_cache/f_000029 create mode 100644 test_data/chrome_cache/f_00002a create mode 100644 test_data/chrome_cache/f_00002b create mode 100644 test_data/chrome_cache/f_00002c create mode 100644 test_data/chrome_cache/f_00002d create mode 100644 test_data/chrome_cache/f_00002e create mode 100644 test_data/chrome_cache/f_00002f create mode 100644 test_data/chrome_cache/f_000030 create mode 100644 test_data/chrome_cache/f_000031 create mode 100644 test_data/chrome_cache/f_000032 create mode 100644 test_data/chrome_cache/f_000033 create mode 100644 test_data/chrome_cache/f_000034 create mode 100644 test_data/chrome_cache/f_000035 create mode 100644 test_data/chrome_cache/f_000036 create mode 100644 test_data/chrome_cache/f_000037 create mode 100644 test_data/chrome_cache/f_000038 create mode 100644 test_data/chrome_cache/f_000039 create mode 100644 test_data/chrome_cache/f_00003a create mode 100644 test_data/chrome_cache/f_00003b create mode 100644 test_data/chrome_cache/f_00003c create mode 100644 test_data/chrome_cache/f_00003d create mode 100644 test_data/chrome_cache/f_00003e create mode 100644 test_data/chrome_cache/f_00003f create mode 100644 test_data/chrome_cache/f_000040 create mode 100644 test_data/chrome_cache/f_000041 create mode 100644 test_data/chrome_cache/f_000042 create mode 100644 test_data/chrome_cache/f_000043 create mode 100644 test_data/chrome_cache/f_000044 create mode 100644 test_data/chrome_cache/f_000045 create mode 100644 test_data/chrome_cache/f_000046 create mode 100644 test_data/chrome_cache/f_000047 create mode 100644 test_data/chrome_cache/f_000048 create mode 100644 test_data/chrome_cache/f_000049 create mode 100644 test_data/chrome_cache/f_00004a create mode 100644 test_data/chrome_cache/f_00004b create mode 100644 test_data/chrome_cache/f_00004c create mode 100644 test_data/chrome_cache/f_00004d create mode 100644 test_data/chrome_cache/index create mode 100644 test_data/chrome_extensions/apdfllckaahabafndbhieahigkjlhalf create mode 100644 test_data/chrome_extensions/blpcfgokakmgnkcojhhkbfbldkacnbeo create mode 100644 test_data/chrome_extensions/hmjkmjkepdijhoojdojkdfohbdgmmhki create mode 100644 test_data/chrome_extensions/icppfcnhkcmnfdhfhphakoifcfokfdhg create mode 100644 test_data/chrome_extensions/pjkljhegncpnkpknbcohdijeoejaedia create mode 100644 test_data/com.apple.HIToolbox.plist create mode 100644 test_data/com.apple.SoftwareUpdate.plist create mode 100644 test_data/com.apple.TimeMachine.plist create mode 100644 test_data/com.apple.airport.preferences.plist create mode 100644 test_data/com.apple.coreservices.appleidauthenticationinfo.ABC0ABC1-ABC0-ABC0-ABC0-ABC0ABC1ABC2.plist create mode 100644 test_data/com.apple.iPod.plist create mode 100644 test_data/com.apple.spotlight.plist create mode 100644 test_data/contacts2.db create mode 100644 test_data/cookies.db create mode 100644 test_data/document_versions.sql create mode 100644 test_data/downloads.sqlite create mode 100644 test_data/empty_file create mode 100644 test_data/example.lnk create mode 100644 test_data/firefox_cache/firefox28/E8D65m01 create mode 100644 test_data/firefox_cache/firefox28/_CACHE_001_ create mode 100644 test_data/firefox_cache/firefox28/_CACHE_002_ create mode 100644 test_data/firefox_cache/firefox28/_CACHE_003_ create mode 100644 test_data/firefox_cache/firefox3/_CACHE_001_ create mode 100644 test_data/firefox_cache/firefox3/_CACHE_002_ create mode 100644 test_data/firefox_cache/firefox3/_CACHE_003_ create mode 100644 test_data/firefox_cache/invalid_file create mode 100644 test_data/firefox_cookies.sqlite create mode 100644 test_data/firewall.log create mode 100644 test_data/global_history.dat create mode 100644 test_data/iis.log create mode 100644 test_data/image-split.E01 create mode 100644 test_data/image-split.E02 create mode 100644 test_data/image.E01 create mode 100644 test_data/image.qcow2 create mode 100644 test_data/image.vhd create mode 100644 test_data/image.vmdk create mode 100644 test_data/index.dat create mode 100644 test_data/java.idx create mode 100644 test_data/java_602.idx create mode 100644 test_data/login.keychain create mode 100644 test_data/mac_cups_ipp create mode 100644 test_data/mackeeper_cache.db create mode 100644 test_data/mactime.body create mode 100755 test_data/mmssms.db create mode 100644 test_data/nobody.plist create mode 100644 test_data/openbsm.bsm create mode 100644 test_data/places.sqlite create mode 100644 test_data/places_new.sqlite create mode 100644 test_data/plist_binary create mode 100644 test_data/popcontest1.log create mode 100644 test_data/psort_test.out create mode 100644 test_data/quarantine.db create mode 100644 test_data/security.log create mode 100644 test_data/selinux.log create mode 100755 test_data/skydrive.log create mode 100755 test_data/skydriveerr-unicode.log create mode 100755 test_data/skydriveerr.log create mode 100644 test_data/skype_main.db create mode 100644 test_data/snapshot.db create mode 100644 test_data/syslog create mode 100644 test_data/syslog.bz2 create mode 100644 test_data/syslog.gz create mode 100644 test_data/syslog.tar create mode 100644 test_data/syslog.tgz create mode 100644 test_data/syslog.zip create mode 100644 test_data/syslog_copy create mode 100644 test_data/syslog_image.dd create mode 100644 test_data/test.pcap create mode 100644 test_data/testdir/filter2.txt create mode 100644 test_data/testdir/filter_1.txt create mode 100644 test_data/testdir/filter_3.txt create mode 100644 test_data/text_parser/test1.txt create mode 100644 test_data/text_parser/test2.txt create mode 100644 test_data/tsk_volume_system.raw create mode 100644 test_data/typed_history.xml create mode 100644 test_data/usage-history.xml create mode 100644 test_data/user.plist create mode 100644 test_data/utmp create mode 100644 test_data/utmpx_mac create mode 100644 test_data/vsstest.qcow2 create mode 100644 test_data/wifi.log create mode 100644 test_data/wintask.job create mode 100644 test_data/wtmp.1 create mode 100755 test_data/xchat.log create mode 100644 test_data/xchatscrollback.log create mode 100644 test_data/ímynd.dd create mode 100644 tools/README.tools create mode 100755 tools/__init__.py create mode 100755 tools/plaso_extract_search_history.py create mode 100644 tools/plaso_process_info.py create mode 100755 utils/build_dependencies.py create mode 100755 utils/check_dependencies.py create mode 100755 utils/common.sh create mode 100755 utils/compile_proto.sh create mode 100644 utils/create_authors.py create mode 100644 utils/dependencies.ini create mode 100644 utils/download_patch_set.py create mode 100644 utils/doxygen.conf create mode 100644 utils/example_vimrc create mode 100755 utils/git-cl create mode 100755 utils/prep_dist.sh create mode 100644 utils/pylintrc create mode 100644 utils/pylintrc-1.1.0 create mode 100755 utils/review.sh create mode 100755 utils/run_linter.sh create mode 100755 utils/run_tests.sh create mode 100755 utils/submit.sh create mode 100755 utils/update.sh create mode 100755 utils/update_dependencies.py create mode 100644 utils/upload.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..758bb51 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Ignore back-up files. +*~ + +# Ignore compiled Python files. +*.pyc +*.pyo + +# Don't include build related files. +/dependencies/ +/dist/ +/build/ + +# And don't care about the 'egg'. +/plaso.egg-info + +# Test files +.coverage +tests-coverage.txt + +# And don't care about the temporary code review file if it exists. +._code_review_number diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4736328 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: python +python: + - "2.7" +before_install: + - sudo add-apt-repository ppa:kristinn-l/plaso-dev -y + - sudo apt-get update -q + - sudo apt-get install binplist libbde-python libesedb-python libevt-python libevtx-python libewf-python libfwsi-python liblnk-python libmsiecf-python libolecf-python libqcow-python libregf-python libsmdev-python libsmraw-python libvhdi-python libvmdk-python libvshadow-python ipython python-bencode python-construct python-dateutil python-dfvfs python-dpkt python-hachoir-core python-hachoir-metadata python-hachoir-parser python-protobuf python-psutil python-pyparsing python-six python-yaml python-tz pytsk3 + - sudo pip install coveralls + - sudo pip install ipython --upgrade +script: + - ./run_tests.py + - coverage run --source=plaso --omit="*_test*,*__init__*,*test_lib*" ./run_tests.py +after_success: + - coveralls diff --git a/ACKNOWLEDGEMENTS b/ACKNOWLEDGEMENTS new file mode 100644 index 0000000..021a81b --- /dev/null +++ b/ACKNOWLEDGEMENTS @@ -0,0 +1,138 @@ +Acknowledgements: plaso + +Copyright 2012 The Plaso Project Authors. +Please see the AUTHORS file for details on individual authors. + +Plaso is a Python rewrite of the log2timeline Perl version. + +Plaso is developed and maintained by: +* Kristinn Gudjonsson +* Eric Mak +* Joachim Metz + +Plaso depends on various other projects. So thanks to the authors +and others involved with these projects: +* Python and modules +* libyaml +* iPython +* PyInstaller +* the SleuthKit +* pytsk +* Hachoir (not included in binary release) + +Thanks to contributors (alphabetically based on last name): +* Brian Baskin + * Parsers + * BEncode + * Java IDX parser +* Johan Berggren + * SQLite plugins + * Zeitgeist activity database +* Petter Bjelland + * Parsers + * Firefox Cache +* Ashley Holtz + * Parsers + * IIS + * Adobe ColdFusion +* Dominique Kilman + * Parsers + * PCAP +* Marc Leavitt + * Parsers + * PL-SQL recall (PLSRecall.dat) +* Preston Miller + *Windows Registry Plugins + * SAM Users + * Shutdown + * USB +* Joaquin Moreno Garijo + * Parsers + * ASL + * BSM + * Cups IPP + * Mac AppFirewall + * Mac KeyChain + * Mac Securityd + * mac_wifi.log + * utmp + * utmpx + * SQLite plugins + * Skype + * Plist plugins + * Airport + * Apple Account + * Install History + * Mac User + * Software Update + * Spotlight + * TimeMachine +* David Nides (@davnads) + * Output modules + * 4n6time SQLite, with thanks to Eric Wong for assistance + * 4n6time MySQL + * Parsers + * Hachoir (meta data) + * OLECF + * OMXL + * Symantec AV Log + * timelib StringToDatetime function + * SQLite plugins + * Google Drive + * Windows Registry plugins + * Office MRU + * Outlook + * Terminal Server Client (RDP) + * Typed Paths + * Typed URLs + * USBStor + * Win7 UserAssist + * WinRar +* Patrik Nisen + * For providing input for parsing the DestList stream for the automatic + destinations OLECF plugin +* Francesco Picasso + * Parsers + * PopContest + * SELinux + * SkyDriveLog + * SkyDriveLogErr + * XChatLog + * XChatScrollBack +* Jordi Sanchez + * For providing: + * binplist + * object filter +* Elizabeth Schweinsberg + * Parsers + * McAfee AV Access Protection Log + * Windows Registry plugins + * MSIE zones +* Marc Séguin + * Windows Registry plugins + * CCleaner +* Keith Wall + * SQLite plugins + * Android calls database + * Android sms database + * updates to the timezone transformation + +Test data: + +Copied with permission from the GRR project: https://github.com/google/grr +* History +* index.dat +* places.sqlite + +Copied with permission granted by Jerome Marty. +* WUAUCLT.EXE-830BCC14.pf + +Copied with permission granted by Rob Lee. +Copyright SANS Institute - Digital Forensics and Incident Response. +* 1b4dd67f29cb1962.automaticDestinations-ms +* 5afe4de1b92fc382.customDestinations-ms +* example.lnk +* SysEvent.Evt +* System.evtx +* Ntuser.dat (multiple instances) +* Windows.edb diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..668de65 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,27 @@ +# Names should be added to this file with this pattern: +# +# For individuals: +# Name (email address) +# +# For organizations: +# Organization (fnmatch pattern) +# +# See python fnmatch module documentation for more information. + +Google Inc. (*@google.com) +Kristinn Gudjonsson (kiddi@kiddaland.net) +Joachim Metz (joachim.metz@gmail.com) +Brian Baskin (brian@thebaskins.com) +David Nides (david.nides@gmail.com) +Dominique Kilman (lexistar97@gmail.com) +Elizabeth Schweinsberg (beth@bethlogic.net) +Eric Mak (ericmak@gmail.com) +Francesco Picasso (francesco.picasso@gmail.com) +Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk) +Keith Wall (kwallster@gmail.com) +Marc Seguin (segumarc@gmail.com) +Oliver Jensen (ojensen5115@gmail.com) +Petter Bjelland (petter.bjelland@gmail.com) +Ashley Holtz (ashley.a.holtz@gmail.com) +Stefan Swerk (stefan_rubanetra@swerk.priv.at) +Preston Miller (preston.miller@dpmforensics.com) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..14e6a3a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,10 @@ +include ACKNOWLEDGEMENTS AUTHORS LICENSE README +include run_tests.py +include utils/check_dependencies.py +exclude .gitignore +exclude *.pyc +recursive-include config * +recursive-include extra * +recursive-include plaso *.proto +recursive-exclude plaso *.pyc +recursive-include test_data * diff --git a/README b/README new file mode 100644 index 0000000..09c037a --- /dev/null +++ b/README @@ -0,0 +1,31 @@ +plaso (Plaso Langar Að Safna Öllu) - super timeline all the things + +In short, plaso is a Python-based backend engine for the tool log2timeline. + +A longer version: + +log2timeline is a tool designed to extract timestamps from various files found +on a typical computer system(s) and aggregate them. + +The initial purpose of plaso was to collect all timestamped events of interest +on a computer system and have them aggregated in a single place for computer +forensic analysis (aka Super Timeline). + +However plaso has become a framework that supports: +* adding new parsers or parsing plug-ins; +* adding new analysis plug-ins; +* writing one-off scripts to automate repetitive tasks in computer forensic + analysis or equivalent. + +And is moving to support: +* adding new general purpose parses/plugins that may not have timestamps + associated to them; +* adding more analysis context; +* tagging events; +* allowing more targeted approach to the collection/parsing. + +Also see: +* log2timeline: http://plaso.kiddaland.net/usage/log2timeline/ +* Project documentation: http://plaso.kiddaland.net/ +* Downloads: https://googledrive.com/host/0B30H7z4S52FleW5vUHBnblJfcjg/ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e993510 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# plaso (Plaso Langar Að Safna Öllu) # +*super timeline all the things* + +Various statistics for the tool: + +Code Coverage: [![Coverage +Status](https://img.shields.io/coveralls/log2timeline/plaso.svg)](https://coveralls.io/r/log2timeline/plaso?branch=master) + +Build Status: [![Build +Status](https://travis-ci.org/log2timeline/plaso.svg?branch=master)](https://travis-ci.org/log2timeline/plaso) + +In short, plaso is a Python-based backend engine for the tool [log2timeline] (http://plaso.kiddaland.net "Plaso home of the super timeline"). + +## A longer version ## + +log2timeline is a tool designed to extract timestamps from various files found on a typical computer system(s) and aggregate them. + +The initial purpose of plaso was to collect all timestamped events of interest on a computer system and have them aggregated in a single place for computer forensic analysis (aka Super Timeline). + +However plaso has become a framework that supports: +* adding new parsers or parsing plug-ins; +* adding new analysis plug-ins; +* writing one-off scripts to automate repetitive tasks in computer forensic analysis or equivalent. + +And is moving to support: +* adding new general purpose parses/plugins that may not have timestamps associated to them; +* adding more analysis context; +* tagging events; +* allowing more targeted approach to the collection/parsing. + +Also see: +* [log2timeline] (http://plaso.kiddaland.net/usage/log2timeline/ "Usage for log2timeline") +* [Project documentation] (http://plaso.kiddaland.net/ "Tool's main documentation site") +* [Downloads] (https://googledrive.com/host/0B30H7z4S52FleW5vUHBnblJfcjg/ "Download the latest version") + diff --git a/config/dpkg/debian/changelog b/config/dpkg/debian/changelog new file mode 100644 index 0000000..4c93d0c --- /dev/null +++ b/config/dpkg/debian/changelog @@ -0,0 +1,23 @@ +python-plaso (1.1.0-1) unstable; urgency=low + + * Version 1.1.0 development release. + + -- Log2Timeline Sat, 14 Dec 2013 12:15:00 +0100 + +python-plaso (1.0.2-2) unstable; urgency=low + + * Version 1.0.2 alpha release. + + -- Log2Timeline Mon, 28 Oct 2013 12:20:23 -0700 + +python-plaso (1.0.2-1) unstable; urgency=low + + * Version 1.0.2 RC1 release. + + -- Log2Timeline Sat, 19 Oct 2013 09:15:00 +0200 + +python-plaso (1.0-1) unstable; urgency=low + + * Initial release. + + -- Log2Timeline Sat, 8 Dec 2012 09:15:00 +0200 diff --git a/config/dpkg/debian/compat b/config/dpkg/debian/compat new file mode 100644 index 0000000..7f8f011 --- /dev/null +++ b/config/dpkg/debian/compat @@ -0,0 +1 @@ +7 diff --git a/config/dpkg/debian/control b/config/dpkg/debian/control new file mode 100644 index 0000000..89130f1 --- /dev/null +++ b/config/dpkg/debian/control @@ -0,0 +1,16 @@ +Source: python-plaso +Section: unknown +Priority: extra +Maintainer: Log2Timeline +Build-Depends: debhelper (>= 7.0.0), python, python-setuptools +Standards-Version: 3.9.2 +Homepage: https://github.com/log2timeline/plaso/ + +Package: python-plaso +Architecture: all +Depends: binplist, libprotobuf7 | libprotobuf8, libyaml-0-2, libbde-python, libesedb-python, libevt-python, libevtx-python, libewf-python, libfwsi-python, liblnk-python, libmsiecf-python, libolecf-python, libqcow-python, libregf-python, libtsk, libsmdev-python, libsmraw-python, libvhdi-python, libvmdk-python, libvshadow-python, ipython, python-bencode, python-construct, python-dateutil, python-dfvfs, python-dpkt, python-hachoir-core, python-hachoir-metadata, python-hachoir-parser, python-protobuf, python-psutil, python-pyparsing, python-six, python-yaml, python-tz, pytsk3, ${shlibs:Depends}, ${misc:Depends} +Recommends: elasticsearch, libesedb-tools, libbde-tools, libevt-tools, libevtx-tools, libewf-tools, liblnk-tools, libmsiecf-tools, libolecf-tools, libqcow-tools, libregf-tools, libsmdev-tools, libsmraw-tools, libvhdi-tools, libvmdk-tools, libvshadow-tools, libtsk-dev, pyelasticsearch, sleuthkit +Description: Plaso Log2Timeline + Log2Timeline is a framework to create super timelines. + It is a framework to parse various files and collect time-based + digital artifacts that can be used in computer forensics. diff --git a/config/dpkg/debian/copyright b/config/dpkg/debian/copyright new file mode 100644 index 0000000..9da6618 --- /dev/null +++ b/config/dpkg/debian/copyright @@ -0,0 +1,27 @@ +Format: http://dep.debian.net/deps/dep5 +Upstream-Name: plaso +Source: https://github.com/log2timeline/plaso/ + +Files: * +Copyright: 2012 The Plaso Project Authors. +License: Apache-2.0 + +Files: debian/* +Copyright: 2012 The Plaso Project Authors. +License: Apache-2.0 + +License: Apache-2.0 + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + . + http://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + . + On Debian systems, the complete text of the Apache version 2.0 license + can be found in "/usr/share/common-licenses/Apache-2.0". diff --git a/config/dpkg/debian/python-plaso.docs b/config/dpkg/debian/python-plaso.docs new file mode 100644 index 0000000..7aab282 --- /dev/null +++ b/config/dpkg/debian/python-plaso.docs @@ -0,0 +1,4 @@ +ACKNOWLEDGEMENTS +AUTHORS +LICENSE +README diff --git a/config/dpkg/debian/rules b/config/dpkg/debian/rules new file mode 100755 index 0000000..e7fd152 --- /dev/null +++ b/config/dpkg/debian/rules @@ -0,0 +1,45 @@ +#!/usr/bin/make -f +# debian/rules that uses debhelper >= 7. + +# Uncomment this to turn on verbose mode. +#export DH_VERBOSE=1 + +# This has to be exported to make some magic below work. +export DH_OPTIONS + + +%: + dh $@ + +override_dh_auto_clean: + +override_dh_auto_test: + +override_dh_installmenu: + +override_dh_installmime: + +override_dh_installmodules: + +override_dh_installlogcheck: + +override_dh_installlogrotate: + +override_dh_installpam: + +override_dh_installppp: + +override_dh_installudev: + +override_dh_installwm: + +override_dh_installxfonts: + +override_dh_gconf: + +override_dh_icons: + +override_dh_perl: + +override_dh_pysupport: + diff --git a/config/licenses/LICENSE.PyYAML b/config/licenses/LICENSE.PyYAML new file mode 100644 index 0000000..312c1a1 --- /dev/null +++ b/config/licenses/LICENSE.PyYAML @@ -0,0 +1,20 @@ +Copyright (c) 2006 Kirill Simonov + +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 furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/config/licenses/LICENSE.bencode b/config/licenses/LICENSE.bencode new file mode 100644 index 0000000..4b7a674 --- /dev/null +++ b/config/licenses/LICENSE.bencode @@ -0,0 +1,143 @@ +BitTorrent Open Source License + +Version 1.1 + +This BitTorrent Open Source License (the "License") applies to the BitTorrent client and related software products as well as any updates or maintenance releases of that software ("BitTorrent Products") that are distributed by BitTorrent, Inc. ("Licensor"). Any BitTorrent Product licensed pursuant to this License is a Licensed Product. Licensed Product, in its entirety, is protected by U.S. copyright law. This License identifies the terms under which you may use, copy, distribute or modify Licensed Product. + +Preamble + +This Preamble is intended to describe, in plain English, the nature and scope of this License. However, this Preamble is not a part of this license. The legal effect of this License is dependent only upon the terms of the License and not this Preamble. + +This License complies with the Open Source Definition and is derived from the Jabber Open Source License 1.0 (the "JOSL"), which has been approved by Open Source Initiative. Sections 4(c) and 4(f)(iii) from the JOSL have been deleted. + +This License provides that: + +1. You may use or give away the Licensed Product, alone or as a component of an aggregate software distribution containing programs from several different sources. No royalty or other fee is required. + +2. Both Source Code and executable versions of the Licensed Product, including Modifications made by previous Contributors, are available for your use. (The terms "Licensed Product," "Modifications," "Contributors" and "Source Code" are defined in the License.) + +3. You are allowed to make Modifications to the Licensed Product, and you can create Derivative Works from it. (The term "Derivative Works" is defined in the License.) + +4. By accepting the Licensed Product under the provisions of this License, you agree that any Modifications you make to the Licensed Product and then distribute are governed by the provisions of this License. In particular, you must make the Source Code of your Modifications available to others free of charge and without a royalty. + +5. You may sell, accept donations or otherwise receive compensation for executable versions of a Licensed Product, without paying a royalty or other fee to the Licensor or any Contributor, provided that such executable versions contain your or another Contributor?s material Modifications. For the avoidance of doubt, to the extent your executable version of a Licensed Product does not contain your or another Contributor?s material Modifications, you may not sell, accept donations or otherwise receive compensation for such executable. + +You may use the Licensed Product for any purpose, but the Licensor is not providing you any warranty whatsoever, nor is the Licensor accepting any liability in the event that the Licensed Product doesn't work properly or causes you any injury or damages. + +6. If you sublicense the Licensed Product or Derivative Works, you may charge fees for warranty or support, or for accepting indemnity or liability obligations to your customers. You cannot charge for, sell, accept donations or otherwise receive compensation for the Source Code. + +7. If you assert any patent claims against the Licensor relating to the Licensed Product, or if you breach any terms of the License, your rights to the Licensed Product under this License automatically terminate. + +You may use this License to distribute your own Derivative Works, in which case the provisions of this License will apply to your Derivative Works just as they do to the original Licensed Product. + +Alternatively, you may distribute your Derivative Works under any other OSI-approved Open Source license, or under a proprietary license of your choice. If you use any license other than this License, however, you must continue to fulfill the requirements of this License (including the provisions relating to publishing the Source Code) for those portions of your Derivative Works that consist of the Licensed Product, including the files containing Modifications. + +New versions of this License may be published from time to time in connection with new versions of a Licensed Product or otherwise. You may choose to continue to use the license terms in this version of the License for the Licensed Product that was originally licensed hereunder, however, the new versions of this License will at all times apply to new versions of the Licensed Product released by Licensor after the release of the new version of this License. Only the Licensor has the right to change the License terms as they apply to the Licensed Product. + +This License relies on precise definitions for certain terms. Those terms are defined when they are first used, and the definitions are repeated for your convenience in a Glossary at the end of the License. + +License Terms + +1. Grant of License From Licensor. Subject to the terms and conditions of this License, Licensor hereby grants you a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims, to do the following: + +a. Use, reproduce, modify, display, perform, sublicense and distribute any Modifications created by a Contributor or portions thereof, in both Source Code or as an executable program, either on an unmodified basis or as part of Derivative Works. + +b. Under claims of patents now or hereafter owned or controlled by Contributor, to make, use, sell, offer for sale, have made, and/or otherwise dispose of Modifications or portions thereof, but solely to the extent that any such claim is necessary to enable you to make, use, sell, offer for sale, have made, and/or otherwise dispose of Modifications or portions thereof or Derivative Works thereof. + +2. Grant of License to Modifications From Contributor. "Modifications" means any additions to or deletions from the substance or structure of (i) a file containing a Licensed Product, or (ii) any new file that contains any part of a Licensed Product. Hereinafter in this License, the term "Licensed Product" shall include all previous Modifications that you receive from any Contributor. Subject to the terms and conditions of this License, By application of the provisions in Section 4(a) below, each person or entity who created or contributed to the creation of, and distributed, a Modification (a "Contributor") hereby grants you a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims, to do the following: + +a. Use, reproduce, modify, display, perform, sublicense and distribute any Modifications created by such Contributor or portions thereof, in both Source Code or as an executable program, either on an unmodified basis or as part of Derivative Works. + +b. Under claims of patents now or hereafter owned or controlled by Contributor, to make, use, sell, offer for sale, have made, and/or otherwise dispose of Modifications or portions thereof, but solely to the extent that any such claim is necessary to enable you to make, use, sell, offer for sale, have made, and/or otherwise dispose of Modifications or portions thereof or Derivative Works thereof. + +3. Exclusions From License Grant. Nothing in this License shall be deemed to grant any rights to trademarks, copyrights, patents, trade secrets or any other intellectual property of Licensor or any Contributor except as expressly stated herein. No patent license is granted separate from the Licensed Product, for code that you delete from the Licensed Product, or for combinations of the Licensed Product with other software or hardware. No right is granted to the trademarks of Licensor or any Contributor even if such marks are included in the Licensed Product. Nothing in this License shall be interpreted to prohibit Licensor from licensing under different terms from this License any code that Licensor otherwise would have a right to license. As an express condition for your use of the Licensed Product, you hereby agree that you will not, without the prior written consent of Licensor, use any trademarks, copyrights, patents, trade secrets or any other intellectual property of Licensor or any Contributor except as expressly stated herein. For the avoidance of doubt and without limiting the foregoing, you hereby agree that you will not use or display any trademark of Licensor or any Contributor in any domain name, directory filepath, advertisement, link or other reference to you in any manner or in any media. + +4. Your Obligations Regarding Distribution. + +a. Application of This License to Your Modifications. As an express condition for your use of the Licensed Product, you hereby agree that any Modifications that you create or to which you contribute, and which you distribute, are governed by the terms of this License including, without limitation, Section 2. Any Modifications that you create or to which you contribute may be distributed only under the terms of this License or a future version of this License released under Section 7. You must include a copy of this License with every copy of the Modifications you distribute. You agree not to offer or impose any terms on any Source Code or executable version of the Licensed Product or Modifications that alter or restrict the applicable version of this License or the recipients' rights hereunder. However, you may include an additional document offering the additional rights described in Section 4(d). + +b. Availability of Source Code. You must make available, without charge, under the terms of this License, the Source Code of the Licensed Product and any Modifications that you distribute, either on the same media as you distribute any executable or other form of the Licensed Product, or via a mechanism generally accepted in the software development community for the electronic transfer of data (an "Electronic Distribution Mechanism"). The Source Code for any version of Licensed Product or Modifications that you distribute must remain available for as long as any executable or other form of the Licensed Product is distributed by you. You are responsible for ensuring that the Source Code version remains available even if the Electronic Distribution Mechanism is maintained by a third party. + +c. Intellectual Property Matters. + + i. Third Party Claims. If you have knowledge that a license to a third party's intellectual property right is required to exercise the rights granted by this License, you must include a text file with the Source Code distribution titled "LEGAL" that describes the claim and the party making the claim in sufficient detail that a recipient will know whom to contact. If you obtain such knowledge after you make any Modifications available as described in Section 4(b), you shall promptly modify the LEGAL file in all copies you make available thereafter and shall take other steps (such as notifying appropriate mailing lists or newsgroups) reasonably calculated to inform those who received the Licensed Product from you that new knowledge has been obtained. + + ii. Contributor APIs. If your Modifications include an application programming interface ("API") and you have knowledge of patent licenses that are reasonably necessary to implement that API, you must also include this information in the LEGAL file. + + iii. Representations. You represent that, except as disclosed pursuant to 4(c)(i) above, you believe that any Modifications you distribute are your original creations and that you have sufficient rights to grant the rights conveyed by this License. + +d. Required Notices. You must duplicate this License in any documentation you provide along with the Source Code of any Modifications you create or to which you contribute, and which you distribute, wherever you describe recipients' rights relating to Licensed Product. You must duplicate the notice contained in Exhibit A (the "Notice") in each file of the Source Code of any copy you distribute of the Licensed Product. If you created a Modification, you may add your name as a Contributor to the Notice. If it is not possible to put the Notice in a particular Source Code file due to its structure, then you must include such Notice in a location (such as a relevant directory file) where a user would be likely to look for such a notice. You may choose to offer, and charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Licensed Product. However, you may do so only on your own behalf, and not on behalf of the Licensor or any Contributor. You must make it clear that any such warranty, support, indemnity or liability obligation is offered by you alone, and you hereby agree to indemnify the Licensor and every Contributor for any liability incurred by the Licensor or such Contributor as a result of warranty, support, indemnity or liability terms you offer. + +e. Distribution of Executable Versions. You may distribute Licensed Product as an executable program under a license of your choice that may contain terms different from this License provided (i) you have satisfied the requirements of Sections 4(a) through 4(d) for that distribution, (ii) you include a conspicuous notice in the executable version, related documentation and collateral materials stating that the Source Code version of the +Licensed Product is available under the terms of this License, including a description of how and where you have fulfilled the obligations of Section 4(b), and (iii) you make it clear that any terms that differ from this License are offered by you alone, not by Licensor or any Contributor. You hereby agree to indemnify the Licensor and every Contributor for any liability incurred by Licensor or such Contributor as a result of any terms you offer. + +f. Distribution of Derivative Works. You may create Derivative Works (e.g., combinations of some or all of the Licensed Product with other code) and distribute the Derivative Works as products under any other license you select, with the proviso that the requirements of this License are fulfilled for those portions of the Derivative Works that consist of the Licensed Product or any Modifications thereto. + +g. Compensation for Distribution of Executable Versions of Licensed Products, Modifications or Derivative Works. Notwithstanding any provision of this License to the contrary, by distributing, selling, licensing, sublicensing or otherwise making available any Licensed Product, or Modification or Derivative Work thereof, you and Licensor hereby acknowledge and agree that you may sell, license or sublicense for a fee, accept donations or otherwise receive compensation for executable versions of a Licensed Product, without paying a royalty or other fee to the Licensor or any other Contributor, provided that such executable versions (i) contain your or another Contributor?s material Modifications, or (ii) are otherwise material Derivative Works. For purposes of this License, an executable version of the Licensed Product will be deemed to contain a material Modification, or will otherwise be deemed a material Derivative Work, if (a) the Licensed Product is modified with your own or a third party?s software programs or other code, and/or the Licensed Product is combined with a number of your own or a third party?s software programs or code, respectively, and (b) such software programs or code add or contribute material value, functionality or features to the License Product. For the avoidance of doubt, to the extent your executable version of a Licensed Product does not contain your or another Contributor?s material Modifications or is otherwise not a material Derivative Work, in each case as contemplated herein, you may not sell, license or sublicense for a fee, accept donations or otherwise receive compensation for such executable. Additionally, without limitation of the foregoing and notwithstanding any provision of this License to the contrary, you cannot charge for, sell, license or sublicense for a fee, accept donations or otherwise receive compensation for the Source Code. + +5. Inability to Comply Due to Statute or Regulation. If it is impossible for you to comply with any of the terms of this License with respect to some or all of the Licensed Product due to statute, judicial order, or regulation, then you must (i) comply with the terms of this License to the maximum extent possible, (ii) cite the statute or regulation that prohibits you from adhering to the License, and (iii) describe the limitations and the code they affect. Such description must be included in the LEGAL file described in Section 4(d), and must be included with all distributions of the Source Code. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill at computer programming to be able to understand it. + +6. Application of This License. This License applies to code to which Licensor or Contributor has attached the Notice in Exhibit A, which is incorporated herein by this reference. + +7. Versions of This License. + +a. New Versions. Licensor may publish from time to time revised and/or new versions of the License. + +b. Effect of New Versions. Once Licensed Product has been published under a particular version of the License, you may always continue to use it under the terms of that version, provided that any such license be in full force and effect at the time, and has not been revoked or otherwise terminated. You may also choose to use such Licensed Product under the terms of any subsequent version (but not any prior version) of the License published by Licensor. No one other than Licensor has the right to modify the terms applicable to Licensed Product created under this License. + +c. Derivative Works of this License. If you create or use a modified version of this License, which you may do only in order to apply it to software that is not already a Licensed Product under this License, you must rename your license so that it is not confusingly similar to this License, and must make it clear that your license contains terms that differ from this License. In so naming your license, you may not use any trademark of Licensor or any Contributor. + +8. Disclaimer of Warranty. LICENSED PRODUCT IS PROVIDED UNDER THIS LICENSE ON AN AS IS BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE LICENSED PRODUCT IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LICENSED PRODUCT IS WITH YOU. SHOULD LICENSED PRODUCT PROVE DEFECTIVE IN ANY RESPECT, YOU (AND NOT THE LICENSOR OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS +DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF LICENSED PRODUCT IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. + +9. Termination. + +a. Automatic Termination Upon Breach. This license and the rights granted hereunder will terminate automatically if you fail to comply with the terms herein and fail to cure such breach within ten (10) days of being notified of the breach by the Licensor. For purposes of this provision, proof of delivery via email to the address listed in the ?WHOIS? database of the registrar for any website through which you distribute or market any Licensed Product, or to any alternate email address which you designate in writing to the Licensor, shall constitute sufficient notification. All sublicenses to the Licensed Product that are properly granted shall survive any termination of this license so long as they continue to complye with the terms of this License. Provisions that, by their nature, must remain in effect beyond the termination of this License, shall survive. + +b. Termination Upon Assertion of Patent Infringement. If you initiate litigation by asserting a patent infringement claim (excluding declaratory judgment actions) against Licensor or a Contributor (Licensor or Contributor against whom you file such an action is referred to herein as Respondent) alleging that Licensed Product directly or indirectly infringes any patent, then any and all rights granted by such Respondent to you under Sections 1 or 2 of this License shall terminate prospectively upon sixty (60) days notice from Respondent (the "Notice Period") unless within that Notice Period you either agree in writing (i) to pay Respondent a mutually agreeable reasonably royalty for your past or future use of Licensed Product made by such Respondent, or (ii) withdraw your litigation claim with respect to Licensed Product against such Respondent. If within said Notice Period a reasonable royalty and payment arrangement are not mutually agreed upon in writing by the parties or the litigation claim is not withdrawn, the rights granted by Licensor to you under Sections 1 and 2 automatically terminate at the expiration of said Notice Period. + +c. Reasonable Value of This License. If you assert a patent infringement claim against Respondent alleging that Licensed Product directly or indirectly infringes any patent where such claim is resolved (such as by license or settlement) prior to the initiation of patent infringement litigation, then the reasonable value of the licenses granted by said Respondent under Sections 1 and 2 shall be taken into account in determining the amount or value of any payment or license. + +d. No Retroactive Effect of Termination. In the event of termination under Sections 9(a) or 9(b) above, all end user license agreements (excluding licenses to distributors and resellers) that have been validly granted by you or any distributor hereunder prior to termination shall survive termination. + +10. Limitation of Liability. UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL THE LICENSOR, ANY CONTRIBUTOR, OR ANY DISTRIBUTOR OF LICENSED PRODUCT, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTYS NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. + +11. Responsibility for Claims. As between Licensor and Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License. You agree to work with Licensor and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability. + +12. U.S. Government End Users. The Licensed Product is a commercial item, as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of commercial computer software and commercial computer software documentation, as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Licensed Product with only those rights set forth herein. + +13. Miscellaneous. This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by California law provisions (except to the extent applicable law, if any, provides otherwise), excluding its conflict-of-law provisions. You expressly agree that in any litigation relating to this license the losing party shall be responsible for costs including, without limitation, court costs and reasonable attorneys fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation that provides that the language of a contract shall be construed against the drafter shall not apply to this License. + +14. Definition of You in This License. You throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License or a future version of this License issued under Section 7. For legal entities, you includes any entity that controls, is controlled by, is under common control with, or affiliated with, you. For purposes of this definition, control means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. You are responsible for advising any affiliated entity of the terms of this License, and that any rights or privileges derived from or obtained by way of this License are subject to the restrictions outlined herein. + +15. Glossary. All defined terms in this License that are used in more than one Section of this License are repeated here, in alphabetical order, for the convenience of the reader. The Section of this License in which each defined term is first used is shown in parentheses. + +Contributor: Each person or entity who created or contributed to the creation of, and distributed, a Modification. (See Section 2) + +Derivative Works: That term as used in this License is defined under U.S. copyright law. (See Section 1(b)) + +License: This BitTorrent Open Source License. (See first paragraph of License) + +Licensed Product: Any BitTorrent Product licensed pursuant to this License. The term "Licensed Product" includes all previous Modifications from any Contributor that you receive. (See first paragraph of License and Section 2) + +Licensor: BitTorrent, Inc. (See first paragraph of License) + +Modifications: Any additions to or deletions from the substance or structure of (i) a file containing Licensed Product, or (ii) any new file that contains any part of Licensed Product. (See Section 2) + +Notice: The notice contained in Exhibit A. (See Section 4(e)) + +Source Code: The preferred form for making modifications to the Licensed Product, including all modules contained therein, plus any associated interface definition files, scripts used to control compilation and installation of an executable program, or a list of differential comparisons against the Source Code of the Licensed Product. (See Section 1(a)) + +You: This term is defined in Section 14 of this License. + + +EXHIBIT A + +The Notice below must appear in each file of the Source Code of any copy you distribute of the Licensed Product or any hereto. Contributors to any Modifications may add their own copyright notices to identify their own contributions. + +License: + +The contents of this file are subject to the BitTorrent Open Source License Version 1.0 (the License). You may not copy or use this file, in either source code or executable form, except in compliance with the License. You may obtain a copy of the License at http://www.bittorrent.com/license/. + +Software distributed under the License is distributed on an AS IS basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License. + diff --git a/config/licenses/LICENSE.binplist b/config/licenses/LICENSE.binplist new file mode 100644 index 0000000..f60dd92 --- /dev/null +++ b/config/licenses/LICENSE.binplist @@ -0,0 +1,13 @@ +Copyright 2013 Google Inc. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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/config/licenses/LICENSE.construct b/config/licenses/LICENSE.construct new file mode 100644 index 0000000..a3c7898 --- /dev/null +++ b/config/licenses/LICENSE.construct @@ -0,0 +1,21 @@ +Copyright (C) 2006-2013 + Tomer Filiba (tomerfiliba@gmail.com) + Corbin Simpson (MostAwesomeDude@gmail.com) + +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 +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/config/licenses/LICENSE.dateutil-parser b/config/licenses/LICENSE.dateutil-parser new file mode 100644 index 0000000..f08c6ad --- /dev/null +++ b/config/licenses/LICENSE.dateutil-parser @@ -0,0 +1,29 @@ +dateutil - Extensions to the standard python 2.3+ datetime module. + +Copyright (c) 2003-2011 - Gustavo Niemeyer + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/config/licenses/LICENSE.dfvfs b/config/licenses/LICENSE.dfvfs new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/config/licenses/LICENSE.dfvfs @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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/config/licenses/LICENSE.dpkt b/config/licenses/LICENSE.dpkt new file mode 100644 index 0000000..99d1437 --- /dev/null +++ b/config/licenses/LICENSE.dpkt @@ -0,0 +1,28 @@ + + Copyright (c) 2004 Dug Song + All rights reserved, all wrongs reversed. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. The names of the authors and copyright holders may not be used to + endorse or promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL + THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/config/licenses/LICENSE.ipython b/config/licenses/LICENSE.ipython new file mode 100644 index 0000000..64205fe --- /dev/null +++ b/config/licenses/LICENSE.ipython @@ -0,0 +1,85 @@ +============================= + The IPython licensing terms +============================= + +IPython is licensed under the terms of the Modified BSD License (also known as +New or Revised BSD), as follows: + +Copyright (c) 2008-2010, IPython Development Team +Copyright (c) 2001-2007, Fernando Perez. +Copyright (c) 2001, Janko Hauser +Copyright (c) 2001, Nathaniel Gray + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the IPython Development Team nor the names of its +contributors may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +About the IPython Development Team +---------------------------------- + +Fernando Perez began IPython in 2001 based on code from Janko Hauser + and Nathaniel Gray . Fernando is still +the project lead. + +The IPython Development Team is the set of all contributors to the IPython +project. This includes all of the IPython subprojects. A full list with +details is kept in the documentation directory, in the file +``about/credits.txt``. + +The core team that coordinates development on GitHub can be found here: +http://github.com/ipython. As of late 2010, it consists of: + +* Brian E. Granger +* Jonathan March +* Evan Patterson +* Fernando Perez +* Min Ragan-Kelley +* Robert Kern + + +Our Copyright Policy +-------------------- + +IPython uses a shared copyright model. Each contributor maintains copyright +over their contributions to IPython. But, it is important to note that these +contributions are typically only changes to the repositories. Thus, the IPython +source code, in its entirety is not the copyright of any single person or +institution. Instead, it is the collective copyright of the entire IPython +Development Team. If individual contributors want to maintain a record of what +changes/contributions they have specific copyright on, they should indicate +their copyright in the commit message of the change, when they commit the +change to one of the IPython repositories. + +With this in mind, the following banner should be used in any source code file +to indicate the copyright and license terms: + +#----------------------------------------------------------------------------- +# Copyright (c) 2010, IPython Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file COPYING.txt, distributed with this software. +#----------------------------------------------------------------------------- diff --git a/config/licenses/LICENSE.libbde b/config/licenses/LICENSE.libbde new file mode 100644 index 0000000..3f7b8b1 --- /dev/null +++ b/config/licenses/LICENSE.libbde @@ -0,0 +1,166 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/config/licenses/LICENSE.libesedb b/config/licenses/LICENSE.libesedb new file mode 100644 index 0000000..3f7b8b1 --- /dev/null +++ b/config/licenses/LICENSE.libesedb @@ -0,0 +1,166 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/config/licenses/LICENSE.libevt b/config/licenses/LICENSE.libevt new file mode 100644 index 0000000..3f7b8b1 --- /dev/null +++ b/config/licenses/LICENSE.libevt @@ -0,0 +1,166 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/config/licenses/LICENSE.libevtx b/config/licenses/LICENSE.libevtx new file mode 100644 index 0000000..3f7b8b1 --- /dev/null +++ b/config/licenses/LICENSE.libevtx @@ -0,0 +1,166 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/config/licenses/LICENSE.libewf b/config/licenses/LICENSE.libewf new file mode 100644 index 0000000..3f7b8b1 --- /dev/null +++ b/config/licenses/LICENSE.libewf @@ -0,0 +1,166 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/config/licenses/LICENSE.libfwsi b/config/licenses/LICENSE.libfwsi new file mode 100644 index 0000000..3f7b8b1 --- /dev/null +++ b/config/licenses/LICENSE.libfwsi @@ -0,0 +1,166 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/config/licenses/LICENSE.liblnk b/config/licenses/LICENSE.liblnk new file mode 100644 index 0000000..3f7b8b1 --- /dev/null +++ b/config/licenses/LICENSE.liblnk @@ -0,0 +1,166 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/config/licenses/LICENSE.libmsiecf b/config/licenses/LICENSE.libmsiecf new file mode 100644 index 0000000..3f7b8b1 --- /dev/null +++ b/config/licenses/LICENSE.libmsiecf @@ -0,0 +1,166 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/config/licenses/LICENSE.libolecf b/config/licenses/LICENSE.libolecf new file mode 100644 index 0000000..3f7b8b1 --- /dev/null +++ b/config/licenses/LICENSE.libolecf @@ -0,0 +1,166 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/config/licenses/LICENSE.libqcow b/config/licenses/LICENSE.libqcow new file mode 100644 index 0000000..3f7b8b1 --- /dev/null +++ b/config/licenses/LICENSE.libqcow @@ -0,0 +1,166 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/config/licenses/LICENSE.libregf b/config/licenses/LICENSE.libregf new file mode 100644 index 0000000..3f7b8b1 --- /dev/null +++ b/config/licenses/LICENSE.libregf @@ -0,0 +1,166 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/config/licenses/LICENSE.libsmdev b/config/licenses/LICENSE.libsmdev new file mode 100644 index 0000000..3f7b8b1 --- /dev/null +++ b/config/licenses/LICENSE.libsmdev @@ -0,0 +1,166 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/config/licenses/LICENSE.libsmraw b/config/licenses/LICENSE.libsmraw new file mode 100644 index 0000000..3f7b8b1 --- /dev/null +++ b/config/licenses/LICENSE.libsmraw @@ -0,0 +1,166 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/config/licenses/LICENSE.libvhdi b/config/licenses/LICENSE.libvhdi new file mode 100644 index 0000000..3f7b8b1 --- /dev/null +++ b/config/licenses/LICENSE.libvhdi @@ -0,0 +1,166 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/config/licenses/LICENSE.libvmdk b/config/licenses/LICENSE.libvmdk new file mode 100644 index 0000000..3f7b8b1 --- /dev/null +++ b/config/licenses/LICENSE.libvmdk @@ -0,0 +1,166 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/config/licenses/LICENSE.libvshadow b/config/licenses/LICENSE.libvshadow new file mode 100644 index 0000000..3f7b8b1 --- /dev/null +++ b/config/licenses/LICENSE.libvshadow @@ -0,0 +1,166 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/config/licenses/LICENSE.libyaml b/config/licenses/LICENSE.libyaml new file mode 100644 index 0000000..050ced2 --- /dev/null +++ b/config/licenses/LICENSE.libyaml @@ -0,0 +1,19 @@ +Copyright (c) 2006 Kirill Simonov + +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 furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/config/licenses/LICENSE.protobuf b/config/licenses/LICENSE.protobuf new file mode 100644 index 0000000..5c2b52f --- /dev/null +++ b/config/licenses/LICENSE.protobuf @@ -0,0 +1,33 @@ +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. +* Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Code generated by the Protocol Buffer compiler is owned by the owner +of the input file used when generating it. This code is not +standalone and requires a support library to be linked with it. This +support library is itself covered by the above license. diff --git a/config/licenses/LICENSE.psutil b/config/licenses/LICENSE.psutil new file mode 100644 index 0000000..e91b135 --- /dev/null +++ b/config/licenses/LICENSE.psutil @@ -0,0 +1,27 @@ +psutil is distributed under BSD license reproduced below. + +Copyright (c) 2009, Jay Loden, Dave Daeschler, Giampaolo Rodola' +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the psutil authors nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/config/licenses/LICENSE.pyelasticsearch b/config/licenses/LICENSE.pyelasticsearch new file mode 100644 index 0000000..11b538e --- /dev/null +++ b/config/licenses/LICENSE.pyelasticsearch @@ -0,0 +1,27 @@ +Copyright (c) 2010 Robert Eanes and contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of pyelasticsearch nor the names of its contributors may be used + to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/config/licenses/LICENSE.pyinstaller b/config/licenses/LICENSE.pyinstaller new file mode 100644 index 0000000..5e36454 --- /dev/null +++ b/config/licenses/LICENSE.pyinstaller @@ -0,0 +1,364 @@ +================================ + The PyInstaller licensing terms +================================ + + +Copyright (c) 2010-2013, PyInstaller Development Team +Copyright (c) 2005-2009, Giovanni Bajo +Based on previous work under copyright (c) 2002 McMillan Enterprises, Inc. + + +PyInstaller is licensed under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + + +Bootloader Exception +-------------------- + +In addition to the permissions in the GNU General Public License, the +authors give you unlimited permission to link or embed compiled bootloader +and related files into combinations with other programs, and to distribute +those combinations without any restriction coming from the use of those +files. (The General Public License restrictions do apply in other respects; +for example, they cover modification of the files, and distribution when +not linked into a combine executable.) + + +Bootloader and Related Files +---------------------------- + +Bootloader and related files are files which are embedded within the +final executable. This includes files in directories: + +./bootloader/ +./PyInstaller/loader + + +About the PyInstaller Development Team +-------------------------------------- + +The PyInstaller Development Team is the set of contributors +to the PyInstaller project. A full list with details is kept +in the documentation directory, in the file +``doc/credits.txt``. + +The core team that coordinates development on GitHub can be found here: +https://github.com/pyinstaller/pyinstaller. As of 2013, it consists of: + +* Giovanni Bajo +* Hartmut Goebel +* Martin Zibricky + + +Our Copyright Policy +-------------------- + +PyInstaller uses a shared copyright model. Each contributor maintains copyright +over their contributions to PyInstaller. But, it is important to note that these +contributions are typically only changes to the repositories. Thus, +the PyInstaller source code, in its entirety is not the copyright of any single +person or institution. Instead, it is the collective copyright of the entire +PyInstaller Development Team. If individual contributors want to maintain +a record of what changes/contributions they have specific copyright on, they +should indicate their copyright in the commit message of the change, when they +commit the change to the PyInstaller repository. + +With this in mind, the following banner should be used in any source code file +to indicate the copyright and license terms: + + +#----------------------------------------------------------------------------- +# Copyright (c) 2013, PyInstaller Development Team. +# +# Distributed under the terms of the GNU General Public License with exception +# for distributing bootloader. +# +# The full license is in the file COPYING.txt, distributed with this software. +#----------------------------------------------------------------------------- + + + +GNU General Public License +-------------------------- + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/config/licenses/LICENSE.pyparsing b/config/licenses/LICENSE.pyparsing new file mode 100644 index 0000000..bbc959e --- /dev/null +++ b/config/licenses/LICENSE.pyparsing @@ -0,0 +1,18 @@ +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 furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/config/licenses/LICENSE.pysqlite b/config/licenses/LICENSE.pysqlite new file mode 100644 index 0000000..793691b --- /dev/null +++ b/config/licenses/LICENSE.pysqlite @@ -0,0 +1,19 @@ +Copyright (c) 2004-2013 Gerhard Häring + +This software is provided 'as-is', without any express or implied warranty. In +no event will the authors be held liable for any damages arising from the use +of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it freely, +subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software in + a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source distribution. diff --git a/config/licenses/LICENSE.python b/config/licenses/LICENSE.python new file mode 100644 index 0000000..7b519a9 --- /dev/null +++ b/config/licenses/LICENSE.python @@ -0,0 +1,945 @@ +.. highlightlang:: none + +.. _history-and-license: + +******************* +History and License +******************* + + +History of the software +======================= + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see http://www.cwi.nl/) in the Netherlands as a +successor of a language called ABC. Guido remains Python's principal author, +although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for National +Research Initiatives (CNRI, see http://www.cnri.reston.va.us/) in Reston, +Virginia where he released several versions of the software. + +In May 2000, Guido and the Python core development team moved to BeOpen.com to +form the BeOpen PythonLabs team. In October of the same year, the PythonLabs +team moved to Digital Creations (now Zope Corporation; see +http://www.zope.com/). In 2001, the Python Software Foundation (PSF, see +http://www.python.org/psf/) was formed, a non-profit organization created +specifically to own Python-related Intellectual Property. Zope Corporation is a +sponsoring member of the PSF. + +All Python releases are Open Source (see http://www.opensource.org/ for the Open +Source Definition). Historically, most, but not all, Python releases have also +been GPL-compatible; the table below summarizes the various releases. + ++----------------+--------------+-----------+------------+-----------------+ +| Release | Derived from | Year | Owner | GPL compatible? | ++================+==============+===========+============+=================+ +| 0.9.0 thru 1.2 | n/a | 1991-1995 | CWI | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 1.3 thru 1.5.2 | 1.2 | 1995-1999 | CNRI | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 1.6 | 1.5.2 | 2000 | CNRI | no | ++----------------+--------------+-----------+------------+-----------------+ +| 2.0 | 1.6 | 2000 | BeOpen.com | no | ++----------------+--------------+-----------+------------+-----------------+ +| 1.6.1 | 1.6 | 2001 | CNRI | no | ++----------------+--------------+-----------+------------+-----------------+ +| 2.1 | 2.0+1.6.1 | 2001 | PSF | no | ++----------------+--------------+-----------+------------+-----------------+ +| 2.0.1 | 2.0+1.6.1 | 2001 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.1.1 | 2.1+2.0.1 | 2001 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.2 | 2.1.1 | 2001 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.1.2 | 2.1.1 | 2002 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.1.3 | 2.1.2 | 2002 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.2.1 | 2.2 | 2002 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.2.2 | 2.2.1 | 2002 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.2.3 | 2.2.2 | 2002-2003 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.3 | 2.2.2 | 2002-2003 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.3.1 | 2.3 | 2002-2003 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.3.2 | 2.3.1 | 2003 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.3.3 | 2.3.2 | 2003 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.3.4 | 2.3.3 | 2004 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.3.5 | 2.3.4 | 2005 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.4 | 2.3 | 2004 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.4.1 | 2.4 | 2005 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.4.2 | 2.4.1 | 2005 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.4.3 | 2.4.2 | 2006 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.4.4 | 2.4.3 | 2006 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.5 | 2.4 | 2006 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.5.1 | 2.5 | 2007 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.5.2 | 2.5.1 | 2008 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.5.3 | 2.5.2 | 2008 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.6 | 2.5 | 2008 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.6.1 | 2.6 | 2008 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.6.2 | 2.6.1 | 2009 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.6.3 | 2.6.2 | 2009 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.6.4 | 2.6.3 | 2010 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ +| 2.7 | 2.6 | 2010 | PSF | yes | ++----------------+--------------+-----------+------------+-----------------+ + +.. note:: + + GPL-compatible doesn't mean that we're distributing Python under the GPL. All + Python licenses, unlike the GPL, let you distribute a modified version without + making your changes open source. The GPL-compatible licenses make it possible to + combine Python with other software that is released under the GPL; the others + don't. + +Thanks to the many outside volunteers who have worked under Guido's direction to +make these releases possible. + + +Terms and conditions for accessing or otherwise using Python +============================================================ + + +.. centered:: PSF LICENSE AGREEMENT FOR PYTHON |release| + +#. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and + the Individual or Organization ("Licensee") accessing and otherwise using Python + |release| software in source or binary form and its associated documentation. + +#. Subject to the terms and conditions of this License Agreement, PSF hereby + grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, + analyze, test, perform and/or display publicly, prepare derivative works, + distribute, and otherwise use Python |release| alone or in any derivative + version, provided, however, that PSF's License Agreement and PSF's notice of + copyright, i.e., "Copyright © 2001-2013 Python Software Foundation; All Rights + Reserved" are retained in Python |release| alone or in any derivative version + prepared by Licensee. + +#. In the event Licensee prepares a derivative work that is based on or + incorporates Python |release| or any part thereof, and wants to make the + derivative work available to others as provided herein, then Licensee hereby + agrees to include in any such work a brief summary of the changes made to Python + |release|. + +#. PSF is making Python |release| available to Licensee on an "AS IS" basis. + PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF + EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR + WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE + USE OF PYTHON |release| WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + +#. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON |release| + FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF + MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON |release|, OR ANY DERIVATIVE + THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +#. This License Agreement will automatically terminate upon a material breach of + its terms and conditions. + +#. Nothing in this License Agreement shall be deemed to create any relationship + of agency, partnership, or joint venture between PSF and Licensee. This License + Agreement does not grant permission to use PSF trademarks or trade name in a + trademark sense to endorse or promote products or services of Licensee, or any + third party. + +#. By copying, installing or otherwise using Python |release|, Licensee agrees + to be bound by the terms and conditions of this License Agreement. + + +.. centered:: BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 + + +.. centered:: BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +#. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an office at + 160 Saratoga Avenue, Santa Clara, CA 95051, and the Individual or Organization + ("Licensee") accessing and otherwise using this software in source or binary + form and its associated documentation ("the Software"). + +#. Subject to the terms and conditions of this BeOpen Python License Agreement, + BeOpen hereby grants Licensee a non-exclusive, royalty-free, world-wide license + to reproduce, analyze, test, perform and/or display publicly, prepare derivative + works, distribute, and otherwise use the Software alone or in any derivative + version, provided, however, that the BeOpen Python License is retained in the + Software, alone or in any derivative version prepared by Licensee. + +#. BeOpen is making the Software available to Licensee on an "AS IS" basis. + BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF + EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND DISCLAIMS ANY REPRESENTATION OR + WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE + USE OF THE SOFTWARE WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + +#. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE SOFTWARE FOR + ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF USING, + MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY DERIVATIVE THEREOF, EVEN IF + ADVISED OF THE POSSIBILITY THEREOF. + +#. This License Agreement will automatically terminate upon a material breach of + its terms and conditions. + +#. This License Agreement shall be governed by and interpreted in all respects + by the law of the State of California, excluding conflict of law provisions. + Nothing in this License Agreement shall be deemed to create any relationship of + agency, partnership, or joint venture between BeOpen and Licensee. This License + Agreement does not grant permission to use BeOpen trademarks or trade names in a + trademark sense to endorse or promote products or services of Licensee, or any + third party. As an exception, the "BeOpen Python" logos available at + http://www.pythonlabs.com/logos.html may be used according to the permissions + granted on that web page. + +#. By copying, installing or otherwise using the software, Licensee agrees to be + bound by the terms and conditions of this License Agreement. + + +.. centered:: CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 + +#. This LICENSE AGREEMENT is between the Corporation for National Research + Initiatives, having an office at 1895 Preston White Drive, Reston, VA 20191 + ("CNRI"), and the Individual or Organization ("Licensee") accessing and + otherwise using Python 1.6.1 software in source or binary form and its + associated documentation. + +#. Subject to the terms and conditions of this License Agreement, CNRI hereby + grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, + analyze, test, perform and/or display publicly, prepare derivative works, + distribute, and otherwise use Python 1.6.1 alone or in any derivative version, + provided, however, that CNRI's License Agreement and CNRI's notice of copyright, + i.e., "Copyright © 1995-2001 Corporation for National Research Initiatives; All + Rights Reserved" are retained in Python 1.6.1 alone or in any derivative version + prepared by Licensee. Alternately, in lieu of CNRI's License Agreement, + Licensee may substitute the following text (omitting the quotes): "Python 1.6.1 + is made available subject to the terms and conditions in CNRI's License + Agreement. This Agreement together with Python 1.6.1 may be located on the + Internet using the following unique, persistent identifier (known as a handle): + 1895.22/1013. This Agreement may also be obtained from a proxy server on the + Internet using the following URL: http://hdl.handle.net/1895.22/1013." + +#. In the event Licensee prepares a derivative work that is based on or + incorporates Python 1.6.1 or any part thereof, and wants to make the derivative + work available to others as provided herein, then Licensee hereby agrees to + include in any such work a brief summary of the changes made to Python 1.6.1. + +#. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" basis. CNRI + MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, + BUT NOT LIMITATION, CNRI MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY + OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF + PYTHON 1.6.1 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. + +#. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 1.6.1 FOR + ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF + MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, OR ANY DERIVATIVE + THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +#. This License Agreement will automatically terminate upon a material breach of + its terms and conditions. + +#. This License Agreement shall be governed by the federal intellectual property + law of the United States, including without limitation the federal copyright + law, and, to the extent such U.S. federal law does not apply, by the law of the + Commonwealth of Virginia, excluding Virginia's conflict of law provisions. + Notwithstanding the foregoing, with regard to derivative works based on Python + 1.6.1 that incorporate non-separable material that was previously distributed + under the GNU General Public License (GPL), the law of the Commonwealth of + Virginia shall govern this License Agreement only as to issues arising under or + with respect to Paragraphs 4, 5, and 7 of this License Agreement. Nothing in + this License Agreement shall be deemed to create any relationship of agency, + partnership, or joint venture between CNRI and Licensee. This License Agreement + does not grant permission to use CNRI trademarks or trade name in a trademark + sense to endorse or promote products or services of Licensee, or any third + party. + +#. By clicking on the "ACCEPT" button where indicated, or by copying, installing + or otherwise using Python 1.6.1, Licensee agrees to be bound by the terms and + conditions of this License Agreement. + + +.. centered:: ACCEPT + + +.. centered:: CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 + +Copyright © 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, The +Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, provided that +the above copyright notice appear in all copies and that both that copyright +notice and this permission notice appear in supporting documentation, and that +the name of Stichting Mathematisch Centrum or CWI not be used in advertising or +publicity pertaining to distribution of the software without specific, written +prior permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE FOR ANY SPECIAL, INDIRECT +OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. + + +Licenses and Acknowledgements for Incorporated Software +======================================================= + +This section is an incomplete, but growing list of licenses and acknowledgements +for third-party software incorporated in the Python distribution. + + +Mersenne Twister +---------------- + +The :mod:`_random` module includes code based on a download from +http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/MT2002/emt19937ar.html. The following are +the verbatim comments from the original code:: + + A C-program for MT19937, with initialization improved 2002/1/26. + Coded by Takuji Nishimura and Makoto Matsumoto. + + Before using, initialize the state by using init_genrand(seed) + or init_by_array(init_key, key_length). + + Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura, + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. The names of its contributors may not be used to endorse or promote + products derived from this software without specific prior written + permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + Any feedback is very welcome. + http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html + email: m-mat @ math.sci.hiroshima-u.ac.jp (remove space) + + +Sockets +------- + +The :mod:`socket` module uses the functions, :func:`getaddrinfo`, and +:func:`getnameinfo`, which are coded in separate source files from the WIDE +Project, http://www.wide.ad.jp/. :: + + Copyright (C) 1995, 1996, 1997, and 1998 WIDE Project. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the project nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND + GAI_ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE + FOR GAI_ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON GAI_ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN GAI_ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + + +Floating point exception control +-------------------------------- + +The source for the :mod:`fpectl` module includes the following notice:: + + --------------------------------------------------------------------- + / Copyright (c) 1996. \ + | The Regents of the University of California. | + | All rights reserved. | + | | + | Permission to use, copy, modify, and distribute this software for | + | any purpose without fee is hereby granted, provided that this en- | + | tire notice is included in all copies of any software which is or | + | includes a copy or modification of this software and in all | + | copies of the supporting documentation for such software. | + | | + | This work was produced at the University of California, Lawrence | + | Livermore National Laboratory under contract no. W-7405-ENG-48 | + | between the U.S. Department of Energy and The Regents of the | + | University of California for the operation of UC LLNL. | + | | + | DISCLAIMER | + | | + | This software was prepared as an account of work sponsored by an | + | agency of the United States Government. Neither the United States | + | Government nor the University of California nor any of their em- | + | ployees, makes any warranty, express or implied, or assumes any | + | liability or responsibility for the accuracy, completeness, or | + | usefulness of any information, apparatus, product, or process | + | disclosed, or represents that its use would not infringe | + | privately-owned rights. Reference herein to any specific commer- | + | cial products, process, or service by trade name, trademark, | + | manufacturer, or otherwise, does not necessarily constitute or | + | imply its endorsement, recommendation, or favoring by the United | + | States Government or the University of California. The views and | + | opinions of authors expressed herein do not necessarily state or | + | reflect those of the United States Government or the University | + | of California, and shall not be used for advertising or product | + \ endorsement purposes. / + --------------------------------------------------------------------- + + +MD5 message digest algorithm +---------------------------- + +The source code for the :mod:`md5` module contains the following notice:: + + Copyright (C) 1999, 2002 Aladdin Enterprises. All rights reserved. + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + L. Peter Deutsch + ghost@aladdin.com + + Independent implementation of MD5 (RFC 1321). + + This code implements the MD5 Algorithm defined in RFC 1321, whose + text is available at + http://www.ietf.org/rfc/rfc1321.txt + The code is derived from the text of the RFC, including the test suite + (section A.5) but excluding the rest of Appendix A. It does not include + any code or documentation that is identified in the RFC as being + copyrighted. + + The original and principal author of md5.h is L. Peter Deutsch + . Other authors are noted in the change history + that follows (in reverse chronological order): + + 2002-04-13 lpd Removed support for non-ANSI compilers; removed + references to Ghostscript; clarified derivation from RFC 1321; + now handles byte order either statically or dynamically. + 1999-11-04 lpd Edited comments slightly for automatic TOC extraction. + 1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5); + added conditionalization for C++ compilation from Martin + Purschke . + 1999-05-03 lpd Original version. + + +Asynchronous socket services +---------------------------- + +The :mod:`asynchat` and :mod:`asyncore` modules contain the following notice:: + + Copyright 1996 by Sam Rushing + + All Rights Reserved + + Permission to use, copy, modify, and distribute this software and + its documentation for any purpose and without fee is hereby + granted, provided that the above copyright notice appear in all + copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of Sam + Rushing not be used in advertising or publicity pertaining to + distribution of the software without specific, written prior + permission. + + SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, + INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN + NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR + CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS + OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, + NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +Cookie management +----------------- + +The :mod:`Cookie` module contains the following notice:: + + Copyright 2000 by Timothy O'Malley + + All Rights Reserved + + Permission to use, copy, modify, and distribute this software + and its documentation for any purpose and without fee is hereby + granted, provided that the above copyright notice appear in all + copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + Timothy O'Malley not be used in advertising or publicity + pertaining to distribution of the software without specific, written + prior permission. + + Timothy O'Malley DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS, IN NO EVENT SHALL Timothy O'Malley BE LIABLE FOR + ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, + WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS + ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + + +Execution tracing +----------------- + +The :mod:`trace` module contains the following notice:: + + portions copyright 2001, Autonomous Zones Industries, Inc., all rights... + err... reserved and offered to the public under the terms of the + Python 2.2 license. + Author: Zooko O'Whielacronx + http://zooko.com/ + mailto:zooko@zooko.com + + Copyright 2000, Mojam Media, Inc., all rights reserved. + Author: Skip Montanaro + + Copyright 1999, Bioreason, Inc., all rights reserved. + Author: Andrew Dalke + + Copyright 1995-1997, Automatrix, Inc., all rights reserved. + Author: Skip Montanaro + + Copyright 1991-1995, Stichting Mathematisch Centrum, all rights reserved. + + + Permission to use, copy, modify, and distribute this Python software and + its associated documentation for any purpose without fee is hereby + granted, provided that the above copyright notice appears in all copies, + and that both that copyright notice and this permission notice appear in + supporting documentation, and that the name of neither Automatrix, + Bioreason or Mojam Media be used in advertising or publicity pertaining to + distribution of the software without specific, written prior permission. + + +UUencode and UUdecode functions +------------------------------- + +The :mod:`uu` module contains the following notice:: + + Copyright 1994 by Lance Ellinghouse + Cathedral City, California Republic, United States of America. + All Rights Reserved + Permission to use, copy, modify, and distribute this software and its + documentation for any purpose and without fee is hereby granted, + provided that the above copyright notice appear in all copies and that + both that copyright notice and this permission notice appear in + supporting documentation, and that the name of Lance Ellinghouse + not be used in advertising or publicity pertaining to distribution + of the software without specific, written prior permission. + LANCE ELLINGHOUSE DISCLAIMS ALL WARRANTIES WITH REGARD TO + THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL LANCE ELLINGHOUSE CENTRUM BE LIABLE + FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT + OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + Modified by Jack Jansen, CWI, July 1995: + - Use binascii module to do the actual line-by-line conversion + between ascii and binary. This results in a 1000-fold speedup. The C + version is still 5 times faster, though. + - Arguments more compliant with Python standard + + +XML Remote Procedure Calls +-------------------------- + +The :mod:`xmlrpclib` module contains the following notice:: + + The XML-RPC client interface is + + Copyright (c) 1999-2002 by Secret Labs AB + Copyright (c) 1999-2002 by Fredrik Lundh + + By obtaining, using, and/or copying this software and/or its + associated documentation, you agree that you have read, understood, + and will comply with the following terms and conditions: + + Permission to use, copy, modify, and distribute this software and + its associated documentation for any purpose and without fee is + hereby granted, provided that the above copyright notice appears in + all copies, and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + Secret Labs AB or the author not be used in advertising or publicity + pertaining to distribution of the software without specific, written + prior permission. + + SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD + TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- + ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR + BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY + DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, + WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS + ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE + OF THIS SOFTWARE. + + +test_epoll +---------- + +The :mod:`test_epoll` contains the following notice:: + + Copyright (c) 2001-2006 Twisted Matrix Laboratories. + + 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 furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Select kqueue +------------- + +The :mod:`select` and contains the following notice for the kqueue interface:: + + Copyright (c) 2000 Doug White, 2006 James Knight, 2007 Christian Heimes + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + + +strtod and dtoa +--------------- + +The file :file:`Python/dtoa.c`, which supplies C functions dtoa and +strtod for conversion of C doubles to and from strings, is derived +from the file of the same name by David M. Gay, currently available +from http://www.netlib.org/fp/. The original file, as retrieved on +March 16, 2009, contains the following copyright and licensing +notice:: + + /**************************************************************** + * + * The author of this software is David M. Gay. + * + * Copyright (c) 1991, 2000, 2001 by Lucent Technologies. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose without fee is hereby granted, provided that this entire notice + * is included in all copies of any software which is or includes a copy + * or modification of this software and in all copies of the supporting + * documentation for such software. + * + * THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED + * WARRANTY. IN PARTICULAR, NEITHER THE AUTHOR NOR LUCENT MAKES ANY + * REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY + * OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE. + * + ***************************************************************/ + + +OpenSSL +------- + +The modules :mod:`hashlib`, :mod:`posix`, :mod:`ssl`, :mod:`crypt` use +the OpenSSL library for added performance if made available by the +operating system. Additionally, the Windows installers for Python +include a copy of the OpenSSL libraries, so we include a copy of the +OpenSSL license here:: + + + LICENSE ISSUES + ============== + + The OpenSSL toolkit stays under a dual license, i.e. both the conditions of + the OpenSSL License and the original SSLeay license apply to the toolkit. + See below for the actual license texts. Actually both licenses are BSD-style + Open Source licenses. In case of any license issues related to OpenSSL + please contact openssl-core@openssl.org. + + OpenSSL License + --------------- + + /* ==================================================================== + * Copyright (c) 1998-2008 The OpenSSL Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. All advertising materials mentioning features or use of this + * software must display the following acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + * + * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please contact + * openssl-core@openssl.org. + * + * 5. Products derived from this software may not be called "OpenSSL" + * nor may "OpenSSL" appear in their names without prior written + * permission of the OpenSSL Project. + * + * 6. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit (http://www.openssl.org/)" + * + * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * This product includes cryptographic software written by Eric Young + * (eay@cryptsoft.com). This product includes software written by Tim + * Hudson (tjh@cryptsoft.com). + * + */ + + Original SSLeay License + ----------------------- + + /* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) + * All rights reserved. + * + * This package is an SSL implementation written + * by Eric Young (eay@cryptsoft.com). + * The implementation was written so as to conform with Netscapes SSL. + * + * This library is free for commercial and non-commercial use as long as + * the following conditions are aheared to. The following conditions + * apply to all code found in this distribution, be it the RC4, RSA, + * lhash, DES, etc., code; not just the SSL code. The SSL documentation + * included with this distribution is covered by the same copyright terms + * except that the holder is Tim Hudson (tjh@cryptsoft.com). + * + * Copyright remains Eric Young's, and as such any Copyright notices in + * the code are not to be removed. + * If this package is used in a product, Eric Young should be given attribution + * as the author of the parts of the library used. + * This can be in the form of a textual message at program startup or + * in documentation (online or textual) provided with the package. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * "This product includes cryptographic software written by + * Eric Young (eay@cryptsoft.com)" + * The word 'cryptographic' can be left out if the rouines from the library + * being used are not cryptographic related :-). + * 4. If you include any Windows specific code (or a derivative thereof) from + * the apps directory (application code) you must include an acknowledgement: + * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + * + * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * The licence and distribution terms for any publically available version or + * derivative of this code cannot be changed. i.e. this code cannot simply be + * copied and put under another distribution licence + * [including the GNU Public Licence.] + */ + + +expat +----- + +The :mod:`pyexpat` extension is built using an included copy of the expat +sources unless the build is configured ``--with-system-expat``:: + + Copyright (c) 1998, 1999, 2000 Thai Open Source Software Center Ltd + and Clark Cooper + + 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 furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +libffi +------ + +The :mod:`_ctypes` extension is built using an included copy of the libffi +sources unless the build is configured ``--with-system-libffi``:: + + Copyright (c) 1996-2008 Red Hat, Inc and others. + + 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 furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + +zlib +---- + +The :mod:`zlib` extension is built using an included copy of the zlib +sources if the zlib version found on the system is too old to be +used for the build:: + + Copyright (C) 1995-2010 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + diff --git a/config/licenses/LICENSE.pytsk b/config/licenses/LICENSE.pytsk new file mode 100644 index 0000000..1d151a1 --- /dev/null +++ b/config/licenses/LICENSE.pytsk @@ -0,0 +1,13 @@ +Copyright 2010 Michael Cohen + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT 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/config/licenses/LICENSE.pytz b/config/licenses/LICENSE.pytz new file mode 100644 index 0000000..5e12fcc --- /dev/null +++ b/config/licenses/LICENSE.pytz @@ -0,0 +1,19 @@ +Copyright (c) 2003-2009 Stuart Bishop + +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 furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/config/licenses/LICENSE.pywin32 b/config/licenses/LICENSE.pywin32 new file mode 100644 index 0000000..fa340d7 --- /dev/null +++ b/config/licenses/LICENSE.pywin32 @@ -0,0 +1,30 @@ +Unless stated in the specfic source file, this work is +Copyright (c) 1994-2008, Mark Hammond +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in +the documentation and/or other materials provided with the distribution. + +Neither name of Mark Hammond nor the name of contributors may be used +to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS +IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/config/licenses/LICENSE.six b/config/licenses/LICENSE.six new file mode 100644 index 0000000..d76e024 --- /dev/null +++ b/config/licenses/LICENSE.six @@ -0,0 +1,18 @@ +Copyright (c) 2010-2014 Benjamin Peterson + +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 furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/config/licenses/LICENSE.sleuthkit.IBM b/config/licenses/LICENSE.sleuthkit.IBM new file mode 100644 index 0000000..26f289d --- /dev/null +++ b/config/licenses/LICENSE.sleuthkit.IBM @@ -0,0 +1,221 @@ +IBM PUBLIC LICENSE VERSION 1.0 - CORONER TOOLKIT UTILITIES + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS IBM PUBLIC +LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE +PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + a) in the case of International Business Machines Corporation ("IBM"), + the Original Program, and + b) in the case of each Contributor, + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate + from and are distributed by that particular Contributor. + A Contribution 'originates' from a Contributor if it was added + to the Program by such Contributor itself or anyone acting on + such Contributor's behalf. + Contributions do not include additions to the Program which: + (i) are separate modules of software distributed in conjunction + with the Program under their own license agreement, and + (ii) are not derivative works of the Program. + +"Contributor" means IBM and any other entity that distributes the Program. + +"Licensed Patents " mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Original Program" means the original version of the software accompanying +this Agreement as released by IBM, including source code, object code +and documentation, if any. + +"Program" means the Original Program and Contributions. + +"Recipient" means anyone who receives the Program under this Agreement, +including all Contributors. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare derivative works of, publicly display, + publicly perform, distribute and sublicense the Contribution of such + Contributor, if any, and such derivative works, in source code and + object code form. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in source code and object code form. This patent license + shall apply to the combination of the Contribution and the Program + if, at the time the Contribution is added by the Contributor, such + addition of the Contribution causes such combination to be covered + by the Licensed Patents. The patent license shall not apply to any + other combinations which include the Contribution. No hardware per + se is licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the rights + and licenses granted hereunder, each Recipient hereby assumes sole + responsibility to secure any other intellectual property rights + needed, if any. For example, if a third party patent license + is required to allow Recipient to distribute the Program, it is + Recipient's responsibility to acquire that license before distributing + the Program. + + d) Each Contributor represents that to its knowledge it has sufficient + copyright rights in its Contribution, if any, to grant the copyright + license set forth in this Agreement. + +3. REQUIREMENTS + +A Contributor may choose to distribute the Program in object code form +under its own license agreement, provided that: + a) it complies with the terms and conditions of this Agreement; and + b) its license agreement: + i) effectively disclaims on behalf of all Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + ii) effectively excludes on behalf of all Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + iii) states that any provisions which differ from this Agreement + are offered by that Contributor alone and not by any other + party; and + iv) states that source code for the Program is available from + such Contributor, and informs licensees how to obtain it in a + reasonable manner on or through a medium customarily used for + software exchange. + +When the Program is made available in source code form: + a) it must be made available under this Agreement; and + b) a copy of this Agreement must be included with each copy of the + Program. + +Each Contributor must include the following in a conspicuous location +in the Program: + + Copyright (c) 1997,1998,1999, International Business Machines + Corporation and others. All Rights Reserved. + +In addition, each Contributor must identify itself as the originator of +its Contribution, if any, in a manner that reasonably allows subsequent +Recipients to identify the originator of the Contribution. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, the +Contributor who includes the Program in a commercial product offering +should do so in a manner which does not create potential liability for +other Contributors. Therefore, if a Contributor includes the Program in +a commercial product offering, such Contributor ("Commercial Contributor") +hereby agrees to defend and indemnify every other Contributor +("Indemnified Contributor") against any losses, damages and costs +(collectively "Losses") arising from claims, lawsuits and other legal +actions brought by a third party against the Indemnified Contributor to +the extent caused by the acts or omissions of such Commercial Contributor +in connection with its distribution of the Program in a commercial +product offering. The obligations in this section do not apply to any +claims or Losses relating to any actual or alleged intellectual property +infringement. In order to qualify, an Indemnified Contributor must: + a) promptly notify the Commercial Contributor in writing of such claim, +and + b) allow the Commercial Contributor to control, and cooperate with + the Commercial Contributor in, the defense and any related + settlement negotiations. The Indemnified Contributor may + participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay those +damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED +ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER +EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR +CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A +PARTICULAR PURPOSE. Each Recipient is solely responsible for determining +the appropriateness of using and distributing the Program and assumes +all risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs or +equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR +ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING +WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION +OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further action +by the parties hereto, such provision shall be reformed to the minimum +extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against a Contributor with +respect to a patent applicable to software (including a cross-claim or +counterclaim in a lawsuit), then any patent licenses granted by that +Contributor to such Recipient under this Agreement shall terminate +as of the date such litigation is filed. In addition, If Recipient +institutes patent litigation against any entity (including a cross-claim +or counterclaim in a lawsuit) alleging that the Program itself (excluding +combinations of the Program with other software or hardware) infringes +such Recipient's patent(s), then such Recipient's rights granted under +Section 2(b) shall terminate as of the date such litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails +to comply with any of the material terms or conditions of this Agreement +and does not cure such failure in a reasonable period of time after +becoming aware of such noncompliance. If all Recipient's rights under +this Agreement terminate, Recipient agrees to cease use and distribution +of the Program as soon as reasonably practicable. However, Recipient's +obligations under this Agreement and any licenses granted by Recipient +relating to the Program shall continue and survive. + +IBM may publish new versions (including revisions) of this Agreement +from time to time. Each new version of the Agreement will be given a +distinguishing version number. The Program (including Contributions) +may always be distributed subject to the version of the Agreement under +which it was received. In addition, after a new version of the Agreement +is published, Contributor may elect to distribute the Program (including +its Contributions) under the new version. No one other than IBM has the +right to modify this Agreement. Except as expressly stated in Sections +2(a) and 2(b) above, Recipient receives no rights or licenses to the +intellectual property of any Contributor under this Agreement, whether +expressly, by implication, estoppel or otherwise. All rights in the +Program not expressly granted under this Agreement are reserved. + +This Agreement is governed by the laws of the State of New York and the +intellectual property laws of the United States of America. No party to +this Agreement will bring a legal action under this Agreement more than +one year after the cause of action arose. Each party waives its rights +to a jury trial in any resulting litigation. diff --git a/config/licenses/LICENSE.sleuthkit.cpl1.0 b/config/licenses/LICENSE.sleuthkit.cpl1.0 new file mode 100644 index 0000000..c9990a7 --- /dev/null +++ b/config/licenses/LICENSE.sleuthkit.cpl1.0 @@ -0,0 +1,213 @@ +Common Public License Version 1.0 + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS COMMON PUBLIC +LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM +CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial code and +documentation distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + + i) changes to the Program, and + + ii) additions to the Program; + + where such changes and/or additions to the Program originate from and are +distributed by that particular Contributor. A Contribution 'originates' from a +Contributor if it was added to the Program by such Contributor itself or anyone +acting on such Contributor's behalf. Contributions do not include additions to +the Program which: (i) are separate modules of software distributed in +conjunction with the Program under their own license agreement, and (ii) are not +derivative works of the Program. + +"Contributor" means any person or entity that distributes the Program. + +"Licensed Patents " mean patent claims licensable by a Contributor which are +necessarily infringed by the use or sale of its Contribution alone or when +combined with the Program. + +"Program" means the Contributions distributed in accordance with this Agreement. + +"Recipient" means anyone who receives the Program under this Agreement, +including all Contributors. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby grants +Recipient a non-exclusive, worldwide, royalty-free copyright license to +reproduce, prepare derivative works of, publicly display, publicly perform, +distribute and sublicense the Contribution of such Contributor, if any, and such +derivative works, in source code and object code form. + + b) Subject to the terms of this Agreement, each Contributor hereby grants +Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed +Patents to make, use, sell, offer to sell, import and otherwise transfer the +Contribution of such Contributor, if any, in source code and object code form. +This patent license shall apply to the combination of the Contribution and the +Program if, at the time the Contribution is added by the Contributor, such +addition of the Contribution causes such combination to be covered by the +Licensed Patents. The patent license shall not apply to any other combinations +which include the Contribution. No hardware per se is licensed hereunder. + + c) Recipient understands that although each Contributor grants the licenses +to its Contributions set forth herein, no assurances are provided by any +Contributor that the Program does not infringe the patent or other intellectual +property rights of any other entity. Each Contributor disclaims any liability to +Recipient for claims brought by any other entity based on infringement of +intellectual property rights or otherwise. As a condition to exercising the +rights and licenses granted hereunder, each Recipient hereby assumes sole +responsibility to secure any other intellectual property rights needed, if any. +For example, if a third party patent license is required to allow Recipient to +distribute the Program, it is Recipient's responsibility to acquire that license +before distributing the Program. + + d) Each Contributor represents that to its knowledge it has sufficient +copyright rights in its Contribution, if any, to grant the copyright license set +forth in this Agreement. + +3. REQUIREMENTS + +A Contributor may choose to distribute the Program in object code form under its +own license agreement, provided that: + + a) it complies with the terms and conditions of this Agreement; and + + b) its license agreement: + + i) effectively disclaims on behalf of all Contributors all warranties and +conditions, express and implied, including warranties or conditions of title and +non-infringement, and implied warranties or conditions of merchantability and +fitness for a particular purpose; + + ii) effectively excludes on behalf of all Contributors all liability for +damages, including direct, indirect, special, incidental and consequential +damages, such as lost profits; + + iii) states that any provisions which differ from this Agreement are offered +by that Contributor alone and not by any other party; and + + iv) states that source code for the Program is available from such +Contributor, and informs licensees how to obtain it in a reasonable manner on or +through a medium customarily used for software exchange. + +When the Program is made available in source code form: + + a) it must be made available under this Agreement; and + + b) a copy of this Agreement must be included with each copy of the Program. + +Contributors may not remove or alter any copyright notices contained within the +Program. + +Each Contributor must identify itself as the originator of its Contribution, if +any, in a manner that reasonably allows subsequent Recipients to identify the +originator of the Contribution. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities with +respect to end users, business partners and the like. While this license is +intended to facilitate the commercial use of the Program, the Contributor who +includes the Program in a commercial product offering should do so in a manner +which does not create potential liability for other Contributors. Therefore, if +a Contributor includes the Program in a commercial product offering, such +Contributor ("Commercial Contributor") hereby agrees to defend and indemnify +every other Contributor ("Indemnified Contributor") against any losses, damages +and costs (collectively "Losses") arising from claims, lawsuits and other legal +actions brought by a third party against the Indemnified Contributor to the +extent caused by the acts or omissions of such Commercial Contributor in +connection with its distribution of the Program in a commercial product +offering. The obligations in this section do not apply to any claims or Losses +relating to any actual or alleged intellectual property infringement. In order +to qualify, an Indemnified Contributor must: a) promptly notify the Commercial +Contributor in writing of such claim, and b) allow the Commercial Contributor to +control, and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may participate in +any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial product +offering, Product X. That Contributor is then a Commercial Contributor. If that +Commercial Contributor then makes performance claims, or offers warranties +related to Product X, those performance claims and warranties are such +Commercial Contributor's responsibility alone. Under this section, the +Commercial Contributor would have to defend claims against the other +Contributors related to those performance claims and warranties, and if a court +requires any other Contributor to pay any damages as a result, the Commercial +Contributor must pay those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, +NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each +Recipient is solely responsible for determining the appropriateness of using and +distributing the Program and assumes all risks associated with its exercise of +rights under this Agreement, including but not limited to the risks and costs of +program errors, compliance with applicable laws, damage to or loss of data, +programs or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY +CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS +GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under applicable +law, it shall not affect the validity or enforceability of the remainder of the +terms of this Agreement, and without further action by the parties hereto, such +provision shall be reformed to the minimum extent necessary to make such +provision valid and enforceable. + +If Recipient institutes patent litigation against a Contributor with respect to +a patent applicable to software (including a cross-claim or counterclaim in a +lawsuit), then any patent licenses granted by that Contributor to such Recipient +under this Agreement shall terminate as of the date such litigation is filed. In +addition, if Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the Program +itself (excluding combinations of the Program with other software or hardware) +infringes such Recipient's patent(s), then such Recipient's rights granted under +Section 2(b) shall terminate as of the date such litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails to +comply with any of the material terms or conditions of this Agreement and does +not cure such failure in a reasonable period of time after becoming aware of +such noncompliance. If all Recipient's rights under this Agreement terminate, +Recipient agrees to cease use and distribution of the Program as soon as +reasonably practicable. However, Recipient's obligations under this Agreement +and any licenses granted by Recipient relating to the Program shall continue and +survive. + +Everyone is permitted to copy and distribute copies of this Agreement, but in +order to avoid inconsistency the Agreement is copyrighted and may only be +modified in the following manner. The Agreement Steward reserves the right to +publish new versions (including revisions) of this Agreement from time to time. +No one other than the Agreement Steward has the right to modify this Agreement. +IBM is the initial Agreement Steward. IBM may assign the responsibility to serve +as the Agreement Steward to a suitable separate entity. Each new version of the +Agreement will be given a distinguishing version number. The Program (including +Contributions) may always be distributed subject to the version of the Agreement +under which it was received. In addition, after a new version of the Agreement +is published, Contributor may elect to distribute the Program (including its +Contributions) under the new version. Except as expressly stated in Sections +2(a) and 2(b) above, Recipient receives no rights or licenses to the +intellectual property of any Contributor under this Agreement, whether +expressly, by implication, estoppel or otherwise. All rights in the Program not +expressly granted under this Agreement are reserved. + +This Agreement is governed by the laws of the State of New York and the +intellectual property laws of the United States of America. No party to this +Agreement will bring a legal action under this Agreement more than one year +after the cause of action arose. Each party waives its rights to a jury trial in +any resulting litigation. diff --git a/config/licenses/LICENSE.talloc b/config/licenses/LICENSE.talloc new file mode 100644 index 0000000..3f7b8b1 --- /dev/null +++ b/config/licenses/LICENSE.talloc @@ -0,0 +1,166 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + diff --git a/config/logo.jpg b/config/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..903c84928440077610f9134c831bb831a4639fc8 GIT binary patch literal 2680 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 literal 0 HcmV?d00001 diff --git a/config/macosx/Readme.txt b/config/macosx/Readme.txt new file mode 100644 index 0000000..c5daceb --- /dev/null +++ b/config/macosx/Readme.txt @@ -0,0 +1,14 @@ +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +| PLASO INSTALLER README +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +This is a simple installer for plaso. + +Simply run the install.sh script as root (sudo ./install.sh) and all shall +be installed and work as it should. + +What the installer does is to install all dependencies to plaso as well as +plaso and dfvfs as separate packages. + +More documentation: http://plaso.kiddaland.net + +Questions/comments/thoughts? send them to log2timeline-discuss@googlegroups.com diff --git a/config/macosx/install.sh.in b/config/macosx/install.sh.in new file mode 100755 index 0000000..af7e17b --- /dev/null +++ b/config/macosx/install.sh.in @@ -0,0 +1,56 @@ +#!/bin/bash +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This is a simple installer script for the Mac OS X platform. + +EXIT_SUCCESS=0; +EXIT_FAILURE=1; + +echo "===============================================================" +echo " PLASO INSTALLER" +echo "===============================================================" + +if test "$USER" != "root"; +then + echo "This script requires root privileges. Running: sudo."; + sudo ls > /dev/null + + if test $? -ne 0; + then + echo "Do you have root privileges?"; + + exit ${EXIT_FAILURE}; + fi +fi + +VOLUME_NAME="/Volumes/@VOLUMENAME@"; + +if ! test -d ${VOLUME_NAME}; +then + echo "Unable to find installation directory: ${VOLUME_NAME}"; + + exit ${EXIT_FAILURE}; +fi + +echo "Installing packages."; + +find ${VOLUME_NAME} -name "*.pkg" -exec sudo installer -target / -pkg {} \; + +echo "Done."; + +exit ${EXIT_SUCCESS}; + diff --git a/config/macosx/make_dist.sh b/config/macosx/make_dist.sh new file mode 100755 index 0000000..16842f5 --- /dev/null +++ b/config/macosx/make_dist.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Script to make a plaso Mac OS X distribution package. + +EXIT_SUCCESS=0; +EXIT_FAILURE=1; + +if ! test -d dependencies; +then + echo "Missing dependencies directory."; + + exit ${EXIT_FAILURE}; +fi + +if ! test -d config; +then + echo "Missing config directory."; + + exit ${EXIT_FAILURE}; +fi + +MACOSX_VERSION=`sw_vers -productVersion | awk -F '.' '{print $1 "." $2}'`; +PLASO_VERSION=`grep -e '^__version' plaso/__init__.py | sed -e "s/^[^=]*= '\([^']*\)'/\1/g"`; + +if ! test -z $1; +then + PLASO_VERSION="${PLASO_VERSION}-$1"; +fi + +if ! test -f ../python-plaso-${PLASO_VERSION}.pkg; +then + echo "Missing plaso package: ../python-plaso-${PLASO_VERSION}.pkg file."; + + exit ${EXIT_FAILURE}; +fi + +DISTDIR="plaso-${PLASO_VERSION}"; + +if test -d "${DISTDIR}"; +then + echo "Distribution directory: ${DISTDIR} already exists."; + + exit ${EXIT_FAILURE}; +fi + +mkdir "${DISTDIR}"; +cp -r config/licenses "${DISTDIR}"; +cp config/macosx/Readme.txt "${DISTDIR}"; + +sed "s/@VOLUMENAME@/${DISTDIR}/" config/macosx/install.sh.in > "${DISTDIR}/install.sh"; + +mkdir "${DISTDIR}/packages"; +cp dependencies/*.pkg "${DISTDIR}/packages"; +cp ../python-plaso-${PLASO_VERSION}.pkg "${DISTDIR}/packages"; + +hdiutil create ../plaso-${PLASO_VERSION}_macosx-${MACOSX_VERSION}.dmg -srcfolder "${DISTDIR}/" -fs HFS+; + +exit ${EXIT_SUCCESS}; + diff --git a/config/windows/make.bat b/config/windows/make.bat new file mode 100644 index 0000000..46f33db --- /dev/null +++ b/config/windows/make.bat @@ -0,0 +1,16 @@ +@echo off +del /q /s build dist 2> NUL +rmdir /q /s build dist 2> NUL + +set PYTHONPATH=. + +C:\Python27\python.exe ..\pyinstaller\pyinstaller.py --onedir plaso\frontend\image_export.py +C:\Python27\python.exe ..\pyinstaller\pyinstaller.py --onedir plaso\frontend\log2timeline.py +C:\Python27\python.exe ..\pyinstaller\pyinstaller.py --onedir plaso\frontend\pinfo.py +C:\Python27\python.exe ..\pyinstaller\pyinstaller.py --onedir plaso\frontend\plasm.py +C:\Python27\python.exe ..\pyinstaller\pyinstaller.py --onedir plaso\frontend\pprof.py +C:\Python27\python.exe ..\pyinstaller\pyinstaller.py --onedir plaso\frontend\preg.py +C:\Python27\python.exe ..\pyinstaller\pyinstaller.py --onedir plaso\frontend\pshell.py +C:\Python27\python.exe ..\pyinstaller\pyinstaller.py --onedir plaso\frontend\psort.py + +set PYTHONPATH= diff --git a/config/windows/make_check.bat b/config/windows/make_check.bat new file mode 100644 index 0000000..77d893b --- /dev/null +++ b/config/windows/make_check.bat @@ -0,0 +1,12 @@ +@echo off +@rem Script to make sure the executables run after make_dist.bat. + +dist\plaso\image_export.exe -h +dist\plaso\log2timeline.exe --info +dist\plaso\pinfo.exe -v test_data\psort_test.out +dist\plaso\plasm.exe -h +dist\plaso\pprof.exe +dist\plaso\preg.exe -h +dist\plaso\psort.exe +dist\plaso\pshell.exe + diff --git a/config/windows/make_dist.bat b/config/windows/make_dist.bat new file mode 100644 index 0000000..8866c23 --- /dev/null +++ b/config/windows/make_dist.bat @@ -0,0 +1,22 @@ +@echo off +del /q /s dist\plaso 2> NUL + +rmdir /q /s dist\plaso 2> NUL + +mkdir dist\plaso +mkdir dist\plaso\licenses + +xcopy /q /y ACKNOWLEDGEMENTS dist\plaso +xcopy /q /y AUTHORS dist\plaso +xcopy /q /y LICENSE dist\plaso +xcopy /q /y README dist\plaso +xcopy /q /y config\licenses\* dist\plaso\licenses + +xcopy /q /y /s dist\image_export\* dist\plaso +xcopy /q /y /s dist\log2timeline\* dist\plaso +xcopy /q /y /s dist\pinfo\* dist\plaso +xcopy /q /y /s dist\plasm\* dist\plaso +xcopy /q /y /s dist\pprof\* dist\plaso +xcopy /q /y /s dist\preg\* dist\plaso +xcopy /q /y /s dist\pshell\* dist\plaso +xcopy /q /y /s dist\psort\* dist\plaso diff --git a/extra/README b/extra/README new file mode 100644 index 0000000..e0932c1 --- /dev/null +++ b/extra/README @@ -0,0 +1 @@ +This folder will contain additional files that contain filter criteria, tagging files, etc. diff --git a/extra/plaso_kibana_example.json b/extra/plaso_kibana_example.json new file mode 100644 index 0000000..ad1f8a9 --- /dev/null +++ b/extra/plaso_kibana_example.json @@ -0,0 +1,329 @@ +{ + "title": "Plaso", + "services": { + "query": { + "idQueue": [ + 1, + 2, + 3, + 4 + ], + "list": { + "0": { + "query": "*", + "alias": "", + "color": "#7EB26D", + "id": 0, + "pin": false, + "type": "lucene" + } + }, + "ids": [ + 0 + ] + }, + "filter": { + "idQueue": [ + 0, + 1, + 2 + ], + "list": {}, + "ids": [] + } + }, + "rows": [ + { + "title": "Histogram", + "height": "200px", + "editable": true, + "collapse": false, + "collapsable": true, + "panels": [ + { + "span": 12, + "editable": true, + "type": "histogram", + "loadingEditor": false, + "mode": "count", + "time_field": "datetime", + "queries": { + "mode": "all", + "ids": [ + 0 + ] + }, + "value_field": null, + "auto_int": true, + "resolution": 100, + "interval": "1y", + "intervals": [ + "auto", + "1s", + "1m", + "5m", + "10m", + "30m", + "1h", + "3h", + "12h", + "1d", + "1w", + "1M", + "1y" + ], + "fill": 0, + "linewidth": 3, + "timezone": "browser", + "spyable": true, + "zoomlinks": true, + "bars": true, + "stack": true, + "points": false, + "lines": false, + "legend": true, + "x-axis": true, + "y-axis": true, + "percentage": false, + "interactive": true, + "options": true, + "tooltip": { + "value_type": "cumulative", + "query_as_alias": false + }, + "title": "Histogram" + } + ], + "notice": false + }, + { + "title": "Graph", + "height": "250px", + "editable": true, + "collapse": false, + "collapsable": true, + "panels": [ + { + "error": false, + "span": 4, + "editable": true, + "type": "terms", + "loadingEditor": false, + "queries": { + "mode": "selected", + "ids": [ + 0 + ] + }, + "field": "source_short", + "exclude": [], + "missing": true, + "other": true, + "size": 10, + "order": "count", + "style": { + "font-size": "10pt" + }, + "donut": false, + "tilt": false, + "labels": true, + "arrangement": "horizontal", + "chart": "bar", + "counter_pos": "below", + "spyable": true, + "title": "Source Distribution" + }, + { + "error": false, + "span": 4, + "editable": true, + "type": "terms", + "loadingEditor": false, + "queries": { + "mode": "selected", + "ids": [] + }, + "field": "parser", + "exclude": [], + "missing": true, + "other": true, + "size": 10, + "order": "count", + "style": { + "font-size": "10pt" + }, + "donut": false, + "tilt": false, + "labels": true, + "arrangement": "horizontal", + "chart": "table", + "counter_pos": "above", + "spyable": true, + "title": "Parser Count" + }, + { + "error": false, + "span": 4, + "editable": true, + "type": "terms", + "loadingEditor": false, + "queries": { + "mode": "selected", + "ids": [] + }, + "field": "hostname", + "exclude": [], + "missing": true, + "other": true, + "size": 10, + "order": "count", + "style": { + "font-size": "10pt" + }, + "donut": false, + "tilt": false, + "labels": true, + "arrangement": "horizontal", + "chart": "bar", + "counter_pos": "above", + "spyable": true, + "title": "Hosts" + } + ], + "notice": false + }, + { + "title": "Events", + "height": "650px", + "editable": true, + "collapse": false, + "collapsable": true, + "panels": [ + { + "error": false, + "span": 12, + "editable": true, + "group": [ + "default" + ], + "type": "table", + "size": 100, + "pages": 5, + "offset": 0, + "sort": [ + "datetime", + "desc" + ], + "style": { + "font-size": "9pt" + }, + "overflow": "min-height", + "fields": [ + "datetime", + "timestamp_desc", + "hostname", + "username", + "source_short", + "source_long", + "message", + "tag", + "display_name" + ], + "highlight": [], + "sortable": true, + "header": true, + "paging": true, + "spyable": true, + "queries": { + "mode": "all", + "ids": [ + 0 + ] + }, + "field_list": true, + "status": "Stable", + "trimFactor": 300, + "normTimes": true, + "title": "Documents", + "all_fields": false + } + ], + "notice": false + } + ], + "editable": true, + "index": { + "interval": "none", + "pattern": "[logstash-]YYYY.MM.DD", + "default": "_all" + }, + "style": "light", + "failover": false, + "panel_hints": true, + "loader": { + "save_gist": false, + "save_elasticsearch": true, + "save_local": true, + "save_default": true, + "save_temp": true, + "save_temp_ttl_enable": true, + "save_temp_ttl": "30d", + "load_gist": true, + "load_elasticsearch": true, + "load_elasticsearch_size": 20, + "load_local": true, + "hide": false + }, + "pulldowns": [ + { + "type": "query", + "collapse": false, + "notice": false, + "query": "*", + "pinned": true, + "history": [], + "remember": 10, + "enable": true + }, + { + "type": "filtering", + "collapse": true, + "notice": false, + "enable": true + } + ], + "nav": [ + { + "type": "timepicker", + "collapse": false, + "notice": false, + "status": "Stable", + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ], + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "timefield": "@timestamp", + "enable": true + } + ], + "refresh": false +} \ No newline at end of file diff --git a/extra/tag_macosx.txt b/extra/tag_macosx.txt new file mode 100755 index 0000000..49dda0b --- /dev/null +++ b/extra/tag_macosx.txt @@ -0,0 +1,21 @@ +Application Execution + data_type is 'macosx:application_usage' + data_type is 'syslog:line' AND body contains 'COMMAND=/bin/launchctl' + +Application Install + data_type is 'plist:key' AND plugin is 'plist_install_history' + +AutoRun + data_type is 'fs:stat' AND filename contains 'LaunchAgents/' AND timestamp_desc is 'HFS_DETECT crtime' AND filename contains '.plist' + +File Downloaded + data_type is 'chrome:history:file_downloaded' + timestamp_desc is 'File Downloaded' + data_type is 'macosx:lsquarantine' + +Device Connected + data_type is 'ipod:device:entry' + data_type is 'plist:key' and plugin is 'plist_airport' + +Document Printed + (data_type is 'metadata:hachoir' OR data_type is 'metadata:OLECF') AND timestamp_desc contains 'Printed' diff --git a/extra/tag_windows.txt b/extra/tag_windows.txt new file mode 100755 index 0000000..8820591 --- /dev/null +++ b/extra/tag_windows.txt @@ -0,0 +1,94 @@ +Application Execution + data_type is 'windows:prefetch' + data_type is 'windows:lnk:link' and filename contains 'Recent' and (local_path contains '.exe' or network_path contains '.exe' or relative_path contains '.exe') + data_type is 'windows:registry:key_value' AND (plugin contains 'userassist' or plugin contains 'mru') AND regvalue.__all__ contains '.exe' + data_type is 'windows:evtx:record' and strings contains 'user mode service' and strings contains 'demand start' + data_type is 'fs:stat' and filename contains 'Windows/Tasks/At' + data_type is 'windows:tasks:job' + data_type is 'windows:evt:record' and source_name is 'Security' and event_identifier is 592 + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-Security-Auditing' and event_identifier is 4688 + data_type is 'windows:registry:appcompatcache' + +Application Installed + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-Application-Experience' and event_identifier is 903 + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-Application-Experience' and event_identifier is 904 + +Application Updated + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-Application-Experience' and event_identifier is 905 + +Application Removed + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-Application-Experience' and event_identifier is 907 + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-Application-Experience' and event_identifier is 908 + +Document Opened + data_type is 'windows:registry:key_value' AND plugin contains 'mru' AND regvalue.__all__ not contains '.exe' AND timestamp > 0 + +Failed Login + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-Security-Auditing' and event_identifier is 4625 + +Logon + data_type is 'windows:evt:record' and source_name is 'Security' and event_identifier is 540 + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-Security-Auditing' and event_identifier is 4624 + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-TerminalServices-LocalSessionManager' and event_identifer is 21 + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-TerminalServices-LocalSessionManager' and event_identifer is 1101 + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-Winlogon' and event_identifier is 7001 + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-TerminalServices-RemoteConnectionManager' and event_identifier is 1147 + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-TerminalServices-RemoteConnectionManager' and event_identifier is 1149 + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-User Profiles Service' and event_identifier is 2 + +Logoff + data_type is 'windows:evt:record' and source_name is 'Security' and event_identifier is 538 + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-Security-Auditing' and event_identifier is 4634 + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-Winlogon' and event_identifier is 7002 + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-TerminalServices-LocalSessionManager' and event_identifer is 23 + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-TerminalServices-LocalSessionManager' and event_identifer is 1103 + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-User Profiles Service' and event_identifier is 4 + +Disconnect + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-TerminalServices-LocalSessionManager' and event_identifer is 24 + +Reconnect + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-TerminalServices-LocalSessionManager' and event_identifer is 25 + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-TerminalServices-LocalSessionManager' and event_identifer is 1105 + +Shell Start + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-TerminalServices-LocalSessionManager' and event_identifer is 22 + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-TerminalServices-LocalSessionManager' and event_identifer is 1102 + +Task Scheduled + data_type is 'windows:evt:record' and source_name is 'Security' and event_identifier is 602 + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-Security-Auditing' and event_identifier is 4698 + +Job Success + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-TaskScheduler' and event_identifier is 102 + +Action Success + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-TaskScheduler' and event_identifier is 201 + +Name Resolution Timeout + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-DNS-Client' and event_identifier is 1014 + +Time Change + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-Kernel-General' and event_identifier is 1 + +Shutdown + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-Kernel-General' and event_identifier is 13 + +System Start + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-Kernel-General' and event_identifier is 13 + +System Sleep + data_type is 'windows:evtx:record' and source_name is 'Microsoft-Windows-Kernel-Power' and event_identifier is 42 + +AutoRun + data_type is 'windows:registry:key_value' and plugin contains 'Run' + +File Downloaded + data_type is 'chrome:history:file_downloaded' + timestamp_desc is 'File Downloaded' + +Document Printed + (data_type is 'metadata:hachoir' OR data_type is 'olecf:summary_info') AND timestamp_desc contains 'Printed' + +Startup Application + data_type is 'windows:registry:key_value' AND (plugin contains 'run' or plugin contains 'lfu') AND (regvalue.__all__ contains '.exe' OR regvalue.__all__ contains '.dll') diff --git a/plaso/__init__.py b/plaso/__init__.py new file mode 100644 index 0000000..93bd326 --- /dev/null +++ b/plaso/__init__.py @@ -0,0 +1,30 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__version__ = '1.2.0' + +VERSION_DEV = False +VERSION_DATE = '20141220' + + +def GetVersion(): + """Returns version information for plaso.""" + if not VERSION_DEV: + return __version__ + + return u'{0:s}_{1:s}'.format(__version__, VERSION_DATE) diff --git a/plaso/analysis/__init__.py b/plaso/analysis/__init__.py new file mode 100644 index 0000000..a4e6c8e --- /dev/null +++ b/plaso/analysis/__init__.py @@ -0,0 +1,83 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 statements for analysis plugins and common methods.""" + +from plaso.analysis import interface +from plaso.lib import errors + +# Import statements of analysis plugins. +from plaso.analysis import browser_search +from plaso.analysis import chrome_extension +from plaso.analysis import windows_services + + +# TODO: move these functions to a manager class. And add a test for this +# function. +def ListAllPluginNames(show_all=True): + """Return a list of all available plugin names and it's doc string.""" + results = [] + for cls_obj in interface.AnalysisPlugin.classes.itervalues(): + doc_string, _, _ = cls_obj.__doc__.partition('\n') + + obj = cls_obj(None) + if not show_all and cls_obj.ENABLE_IN_EXTRACTION: + results.append((obj.plugin_name, doc_string, obj.plugin_type)) + elif show_all: + results.append((obj.plugin_name, doc_string, obj.plugin_type)) + + return sorted(results) + + +def LoadPlugins(plugin_names, incoming_queues, options=None): + """Yield analysis plugins for a given list of plugin names. + + Given a list of plugin names this method finds the analysis + plugins, initializes them and returns a generator. + + Args: + plugin_names: A list of plugin names that should be loaded up. This + should be a list of strings. + incoming_queues: A list of queues (QueueInterface object) that the plugin + uses to read in incoming events to analyse. + options: Optional command line arguments (instance of + argparse.Namespace). The default is None. + + Yields: + Analysis plugin objects (instances of AnalysisPlugin). + + Raises: + errors.BadConfigOption: If plugins_names does not contain a list of + strings. + """ + try: + plugin_names_lower = [word.lower() for word in plugin_names] + except AttributeError: + raise errors.BadConfigOption(u'Plugin names should be a list of strings.') + + for plugin_object in interface.AnalysisPlugin.classes.itervalues(): + plugin_name = plugin_object.NAME.lower() + + if plugin_name in plugin_names_lower: + queue_index = plugin_names_lower.index(plugin_name) + + try: + incoming_queue = incoming_queues[queue_index] + except (TypeError, IndexError): + incoming_queue = None + + yield plugin_object(incoming_queue, options) diff --git a/plaso/analysis/browser_search.py b/plaso/analysis/browser_search.py new file mode 100644 index 0000000..e9c88db --- /dev/null +++ b/plaso/analysis/browser_search.py @@ -0,0 +1,257 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""A plugin that extracts browser history from events.""" + +import collections +import logging +import urllib + +from plaso import filters +from plaso.analysis import interface +from plaso.formatters import manager as formatters_manager +from plaso.lib import event + + +# Create a lightweight object that is used to store timeline based information +# about each search term. +SEARCH_OBJECT = collections.namedtuple( + 'SEARCH_OBJECT', 'time source engine search_term') + + +def ScrubLine(line): + """Scrub the line of most obvious HTML codes. + + An attempt at taking a line and swapping all instances + of %XX which represent a character in hex with it's + unicode character. + + Args: + line: The string that we are about to "fix". + + Returns: + String that has it's %XX hex codes swapped for text. + """ + if not line: + return '' + + try: + return unicode(urllib.unquote(str(line)), 'utf-8') + except UnicodeDecodeError: + logging.warning(u'Unable to decode line: {0:s}'.format(line)) + + return line + + +class FilterClass(object): + """A class that contains all the parser functions.""" + + @classmethod + def _GetBetweenQEqualsAndAmbersand(cls, string): + """Return back string that is defined 'q=' and '&'.""" + if 'q=' not in string: + return string + _, _, line = string.partition('q=') + before_and, _, _ = line.partition('&') + if not before_and: + return line + return before_and.split()[0] + + @classmethod + def _SearchAndQInLine(cls, string): + """Return a bool indicating if the words q= and search appear in string.""" + return 'search' in string and 'q=' in string + + @classmethod + def GoogleSearch(cls, url): + """Return back the extracted string.""" + if not cls._SearchAndQInLine(url): + return + + line = cls._GetBetweenQEqualsAndAmbersand(url) + if not line: + return + + return line.replace('+', ' ') + + @classmethod + def YouTube(cls, url): + """Return back the extracted string.""" + return cls.GenericSearch(url) + + @classmethod + def BingSearch(cls, url): + """Return back the extracted string.""" + return cls.GenericSearch(url) + + @classmethod + def GenericSearch(cls, url): + """Return back the extracted string from a generic search engine.""" + if not cls._SearchAndQInLine(url): + return + + return cls._GetBetweenQEqualsAndAmbersand(url).replace('+', ' ') + + @classmethod + def Yandex(cls, url): + """Return back the results from Yandex search engine.""" + if 'text=' not in url: + return + _, _, line = url.partition('text=') + before_and, _, _ = line.partition('&') + if not before_and: + return + yandex_search_url = before_and.split()[0] + + return yandex_search_url.replace('+', ' ') + + @classmethod + def DuckDuckGo(cls, url): + """Return back the extracted string.""" + if not 'q=' in url: + return + return cls._GetBetweenQEqualsAndAmbersand(url).replace('+', ' ') + + @classmethod + def Gmail(cls, url): + """Return back the extracted string.""" + if 'search/' not in url: + return + + _, _, line = url.partition('search/') + first, _, _ = line.partition('/') + second, _, _ = first.partition('?compose') + + return second.replace('+', ' ') + + +class AnalyzeBrowserSearchPlugin(interface.AnalysisPlugin): + """Analyze browser search entries from events.""" + + NAME = 'browser_search' + + # Indicate that we do not want to run this plugin during regular extraction. + ENABLE_IN_EXTRACTION = False + + # Here we define filters and callback methods for all hits on each filter. + FILTERS = ( + (('url iregexp "(www.|encrypted.|/)google." and url contains "search"'), + 'GoogleSearch'), + ('url contains "youtube.com"', 'YouTube'), + (('source is "WEBHIST" and url contains "bing.com" and url contains ' + '"search"'), 'BingSearch'), + ('url contains "mail.google.com"', 'Gmail'), + (('source is "WEBHIST" and url contains "yandex.com" and url contains ' + '"yandsearch"'), 'Yandex'), + ('url contains "duckduckgo.com"', 'DuckDuckGo') + ) + + # We need to implement the interface for analysis plugins, but we don't use + # command line options here, so disable checking for unused args. + # pylint: disable=unused-argument + def __init__(self, incoming_queue, options=None): + """Initializes the browser search analysis plugin. + + Args: + incoming_queue: A queue that is used to listen to incoming events. + options: Optional command line arguments (instance of + argparse.Namespace). The default is None. + """ + super(AnalyzeBrowserSearchPlugin, self).__init__(incoming_queue) + self._filter_dict = {} + self._counter = collections.Counter() + + # Store a list of search terms in a timeline format. + # The format is key = timestamp, value = (source, engine, search term). + self._search_term_timeline = [] + + for filter_str, call_back in self.FILTERS: + filter_obj = filters.GetFilter(filter_str) + call_back_obj = getattr(FilterClass, call_back, None) + if filter_obj and call_back_obj: + self._filter_dict[filter_obj] = (call_back, call_back_obj) + + # pylint: enable=unused-argument + + def CompileReport(self): + """Compiles a report of the analysis. + + Returns: + The analysis report (instance of AnalysisReport). + """ + report = event.AnalysisReport() + + results = {} + for key, count in self._counter.iteritems(): + search_engine, _, search_term = key.partition(':') + results.setdefault(search_engine, {}) + results[search_engine][search_term] = count + report.report_dict = results + report.report_array = self._search_term_timeline + + lines_of_text = [] + for search_engine, terms in sorted(results.items()): + lines_of_text.append(u' == ENGINE: {0:s} =='.format(search_engine)) + + for search_term, count in sorted( + terms.iteritems(), key=lambda x: (x[1], x[0]), reverse=True): + lines_of_text.append(u'{0:d} {1:s}'.format(count, search_term)) + + # An empty string is added to have SetText create an empty line. + lines_of_text.append(u'') + + report.SetText(lines_of_text) + + return report + + def ExamineEvent( + self, unused_analysis_context, event_object, **unused_kwargs): + """Analyzes an event object. + + Args: + analysis_context: An analysis context object + (instance of AnalysisContext). + event_object: An event object (instance of EventObject). + """ + # This event requires an URL attribute. + url_attribute = getattr(event_object, 'url', None) + + if not url_attribute: + return + + # TODO: refactor this the source should be used in formatting only. + # Check if we are dealing with a web history event. + source, _ = formatters_manager.EventFormatterManager.GetSourceStrings( + event_object) + + if source != 'WEBHIST': + return + + for filter_obj, call_backs in self._filter_dict.items(): + call_back_name, call_back_object = call_backs + if filter_obj.Match(event_object): + returned_line = ScrubLine(call_back_object(url_attribute)) + if not returned_line: + continue + self._counter[u'{0:s}:{1:s}'.format(call_back_name, returned_line)] += 1 + + # Add the timeline format for each search term. + self._search_term_timeline.append(SEARCH_OBJECT( + getattr(event_object, 'timestamp', 0), + getattr(event_object, 'plugin', getattr( + event_object, 'parser', u'N/A')), + call_back_name, returned_line)) diff --git a/plaso/analysis/browser_search_test.py b/plaso/analysis/browser_search_test.py new file mode 100644 index 0000000..0c013ac --- /dev/null +++ b/plaso/analysis/browser_search_test.py @@ -0,0 +1,74 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the browser search analysis plugin.""" + +import unittest + +from plaso.analysis import browser_search +from plaso.analysis import test_lib +# pylint: disable=unused-import +from plaso.formatters import chrome as chrome_formatter +from plaso.lib import event +from plaso.parsers import sqlite +from plaso.parsers.sqlite_plugins import chrome + + +class BrowserSearchAnalysisTest(test_lib.AnalysisPluginTestCase): + """Tests for the browser search analysis plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = sqlite.SQLiteParser() + + def testAnalyzeFile(self): + """Read a storage file that contains URL data and analyze it.""" + knowledge_base = self._SetUpKnowledgeBase() + + test_file = self._GetTestFilePath(['History']) + event_queue = self._ParseFile(self._parser, test_file, knowledge_base) + + analysis_plugin = browser_search.AnalyzeBrowserSearchPlugin(event_queue) + analysis_report_queue_consumer = self._RunAnalysisPlugin( + analysis_plugin, knowledge_base) + analysis_reports = self._GetAnalysisReportsFromQueue( + analysis_report_queue_consumer) + + self.assertEquals(len(analysis_reports), 1) + + analysis_report = analysis_reports[0] + + # Due to the behavior of the join one additional empty string at the end + # is needed to create the last empty line. + expected_text = u'\n'.join([ + u' == ENGINE: GoogleSearch ==', + u'1 really really funny cats', + u'1 java plugin', + u'1 funnycats.exe', + u'1 funny cats', + u'', + u'']) + + self.assertEquals(analysis_report.text, expected_text) + self.assertEquals(analysis_report.plugin_name, 'browser_search') + + expected_keys = set([u'GoogleSearch']) + self.assertEquals(set(analysis_report.report_dict.keys()), expected_keys) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/analysis/chrome_extension.py b/plaso/analysis/chrome_extension.py new file mode 100644 index 0000000..43da663 --- /dev/null +++ b/plaso/analysis/chrome_extension.py @@ -0,0 +1,201 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""A plugin that gather extension ID's from Chrome history browser.""" + +import logging +import re +import urllib2 + +from plaso.analysis import interface +from plaso.lib import event + + +class AnalyzeChromeExtensionPlugin(interface.AnalysisPlugin): + """Convert Chrome extension ID's into names, requires Internet connection.""" + + NAME = 'chrome_extension' + + # Indicate that we can run this plugin during regular extraction. + ENABLE_IN_EXTRACTION = True + + _TITLE_RE = re.compile('([^<]+)') + _WEB_STORE_URL = u'https://chrome.google.com/webstore/detail/{xid}?hl=en-US' + + # We need to implement the interface for analysis plugins, but we don't use + # command line options here, so disable checking for unused args. + # pylint: disable=unused-argument + def __init__(self, incoming_queue, options=None): + """Initializes the Chrome extension analysis plugin. + + Args: + incoming_queue: A queue that is used to listen to incoming events. + options: Optional command line arguments (instance of + argparse.Namespace). The default is None. + """ + super(AnalyzeChromeExtensionPlugin, self).__init__(incoming_queue) + + self._results = {} + self.plugin_type = self.TYPE_REPORT + + # TODO: see if these can be moved to arguments passed to ExamineEvent + # or some kind of state object. + self._sep = None + self._user_paths = None + + # Saved list of already looked up extensions. + self._extensions = {} + + # pylint: enable=unused-argument + + def _GetChromeWebStorePage(self, extension_id): + """Retrieves the page for the extension from the Chrome store website. + + Args: + extension_id: string containing the extension identifier. + """ + web_store_url = self._WEB_STORE_URL.format(xid=extension_id) + try: + response = urllib2.urlopen(web_store_url) + + except urllib2.HTTPError as exception: + logging.warning(( + u'[{0:s}] unable to retrieve URL: {1:s} with error: {2:s}').format( + self.NAME, web_store_url, exception)) + return + + except urllib2.URLError as exception: + logging.warning(( + u'[{0:s}] invalid URL: {1:s} with error: {2:s}').format( + self.NAME, web_store_url, exception)) + return + + return response + + def _GetTitleFromChromeWebStore(self, extension_id): + """Retrieves the name of the extension from the Chrome store website. + + Args: + extension_id: string containing the extension identifier. + """ + # Check if we have already looked this extension up. + if extension_id in self._extensions: + return self._extensions.get(extension_id) + + response = self._GetChromeWebStorePage(extension_id) + if not response: + logging.warning( + u'[{0:s}] no data returned for extension identifier: {1:s}'.format( + self.NAME, extension_id)) + return + + first_line = response.readline() + match = self._TITLE_RE.search(first_line) + if match: + title = match.group(1) + if title.startswith(u'Chrome Web Store - '): + name = title[19:] + elif title.endswith(u'- Chrome Web Store'): + name = title[:-19] + + self._extensions[extension_id] = name + return name + + self._extensions[extension_id] = u'Not Found' + + def CompileReport(self): + """Compiles a report of the analysis. + + Returns: + The analysis report (instance of AnalysisReport). + """ + report = event.AnalysisReport() + + report.report_dict = self._results + + lines_of_text = [] + for user, extensions in sorted(self._results.iteritems()): + lines_of_text.append(u' == USER: {0:s} =='.format(user)) + for extension, extension_id in sorted(extensions): + lines_of_text.append(u' {0:s} [{1:s}]'.format(extension, extension_id)) + + # An empty string is added to have SetText create an empty line. + lines_of_text.append(u'') + + report.SetText(lines_of_text) + + return report + + def ExamineEvent(self, analysis_context, event_object, **unused_kwargs): + """Analyzes an event object. + + Args: + analysis_context: An analysis context object + (instance of AnalysisContext). + event_object: An event object (instance of EventObject). + """ + # Only interested in filesystem events. + if event_object.data_type != 'fs:stat': + return + + filename = getattr(event_object, 'filename', None) + if not filename: + return + + # Determine if we have a Chrome extension ID. + if u'chrome' not in filename.lower(): + return + + if not self._sep: + self._sep = analysis_context.GetPathSegmentSeparator(filename) + + if not self._user_paths: + self._user_paths = analysis_context.GetUserPaths(analysis_context.users) + + if u'{0:s}Extensions{0:s}'.format(self._sep) not in filename: + return + + # Now we have extension ID's, let's check if we've got the + # folder, nothing else. + paths = filename.split(self._sep) + if paths[-2] != u'Extensions': + return + + extension_id = paths[-1] + if extension_id == u'Temp': + return + + # Get the user and ID. + user = analysis_context.GetUsernameFromPath( + self._user_paths, filename, self._sep) + + # We still want this information in here, so that we can + # manually deduce the username. + if not user: + if len(filename) > 25: + user = u'Not found ({0:s}...)'.format(filename[0:25]) + else: + user = u'Not found ({0:s})'.format(filename) + + extension = self._GetTitleFromChromeWebStore(extension_id) + if not extension: + extension = extension_id + + self._results.setdefault(user, []) + extension_string = extension.decode('utf-8', 'ignore') + if (extension_string, extension_id) not in self._results[user]: + self._results[user].append((extension_string, extension_id)) diff --git a/plaso/analysis/chrome_extension_test.py b/plaso/analysis/chrome_extension_test.py new file mode 100644 index 0000000..baff358 --- /dev/null +++ b/plaso/analysis/chrome_extension_test.py @@ -0,0 +1,196 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the chrome extension analysis plugin.""" + +import os +import unittest + +from plaso.analysis import chrome_extension +from plaso.analysis import test_lib +from plaso.engine import queue +from plaso.engine import single_process +from plaso.lib import event + +# We are accessing quite a lot of protected members in this test file. +# Suppressing that message test file wide. +# pylint: disable=protected-access + + +class AnalyzeChromeExtensionTestPlugin( + chrome_extension.AnalyzeChromeExtensionPlugin): + """Chrome extension analysis plugin used for testing.""" + + NAME = 'chrome_extension_test' + + _TEST_DATA_PATH = os.path.join( + os.getcwd(), u'test_data', u'chrome_extensions') + + def _GetChromeWebStorePage(self, extension_id): + """Retrieves the page for the extension from the Chrome store test data. + + Args: + extension_id: string containing the extension identifier. + """ + chrome_web_store_file = os.path.join(self._TEST_DATA_PATH, extension_id) + if not os.path.exists(chrome_web_store_file): + return + + return open(chrome_web_store_file, 'rb') + + +class ChromeExtensionTest(test_lib.AnalysisPluginTestCase): + """Tests for the chrome extension analysis plugin.""" + + # Few config options here. + MAC_PATHS = [ + '/Users/dude/Libary/Application Data/Google/Chrome/Default/Extensions', + ('/Users/dude/Libary/Application Data/Google/Chrome/Default/Extensions/' + 'apdfllckaahabafndbhieahigkjlhalf'), + '/private/var/log/system.log', + '/Users/frank/Library/Application Data/Google/Chrome/Default', + '/Users/hans/Library/Application Data/Google/Chrome/Default', + ('/Users/frank/Library/Application Data/Google/Chrome/Default/' + 'Extensions/pjkljhegncpnkpknbcohdijeoejaedia'), + '/Users/frank/Library/Application Data/Google/Chrome/Default/Extensions',] + + WIN_PATHS = [ + 'C:\\Users\\Dude\\SomeFolder\\Chrome\\Default\\Extensions', + ('C:\\Users\\Dude\\SomeNoneStandardFolder\\Chrome\\Default\\Extensions\\' + 'hmjkmjkepdijhoojdojkdfohbdgmmhki'), + ('\\Users\\frank\\AppData\\Local\\Google\\Chrome\\Extensions\\' + 'blpcfgokakmgnkcojhhkbfbldkacnbeo'), + '\\Users\\frank\\AppData\\Local\\Google\\Chrome\\Extensions', + ('\\Users\\frank\\AppData\\Local\\Google\\Chrome\\Extensions\\' + 'icppfcnhkcmnfdhfhphakoifcfokfdhg'), + 'C:\\Windows\\System32', + '\\Stuff/with path separator\\Folder'] + + MAC_USERS = [ + {u'name': u'root', u'path': u'/var/root', u'sid': u'0'}, + {u'name': u'frank', u'path': u'/Users/frank', u'sid': u'4052'}, + {u'name': u'hans', u'path': u'/Users/hans', u'sid': u'4352'}, + {u'name': u'dude', u'path': u'/Users/dude', u'sid': u'1123'}] + + WIN_USERS = [ + {u'name': u'dude', u'path': u'C:\\Users\\dude', u'sid': u'S-1'}, + {u'name': u'frank', u'path': u'C:\\Users\\frank', u'sid': u'S-2'}] + + def _CreateTestEventObject(self, path): + """Create a test event object with a particular path.""" + event_object = event.EventObject() + event_object.data_type = 'fs:stat' + event_object.timestamp = 12345 + event_object.timestamp_desc = u'Some stuff' + event_object.filename = path + + return event_object + + def testMacAnalyzerPlugin(self): + """Test the plugin against mock events.""" + knowledge_base = self._SetUpKnowledgeBase(knowledge_base_values={ + 'users': self.MAC_USERS}) + + event_queue = single_process.SingleProcessQueue() + + # Fill the incoming queue with events. + test_queue_producer = queue.ItemQueueProducer(event_queue) + test_queue_producer.ProduceItems([ + self._CreateTestEventObject(path) for path in self.MAC_PATHS]) + test_queue_producer.SignalEndOfInput() + + # Initialize plugin. + analysis_plugin = AnalyzeChromeExtensionTestPlugin(event_queue) + + # Run the analysis plugin. + analysis_report_queue_consumer = self._RunAnalysisPlugin( + analysis_plugin, knowledge_base) + analysis_reports = self._GetAnalysisReportsFromQueue( + analysis_report_queue_consumer) + + self.assertEquals(len(analysis_reports), 1) + + analysis_report = analysis_reports[0] + + self.assertEquals(analysis_plugin._sep, u'/') + + # Due to the behavior of the join one additional empty string at the end + # is needed to create the last empty line. + expected_text = u'\n'.join([ + u' == USER: dude ==', + u' Google Drive [apdfllckaahabafndbhieahigkjlhalf]', + u'', + u' == USER: frank ==', + u' Gmail [pjkljhegncpnkpknbcohdijeoejaedia]', + u'', + u'']) + + self.assertEquals(analysis_report.text, expected_text) + self.assertEquals(analysis_report.plugin_name, 'chrome_extension_test') + + expected_keys = set([u'frank', u'dude']) + self.assertEquals(set(analysis_report.report_dict.keys()), expected_keys) + + def testWinAnalyzePlugin(self): + """Test the plugin against mock events.""" + knowledge_base = self._SetUpKnowledgeBase(knowledge_base_values={ + 'users': self.WIN_USERS}) + + event_queue = single_process.SingleProcessQueue() + + # Fill the incoming queue with events. + test_queue_producer = queue.ItemQueueProducer(event_queue) + test_queue_producer.ProduceItems([ + self._CreateTestEventObject(path) for path in self.WIN_PATHS]) + test_queue_producer.SignalEndOfInput() + + # Initialize plugin. + analysis_plugin = AnalyzeChromeExtensionTestPlugin(event_queue) + + # Run the analysis plugin. + analysis_report_queue_consumer = self._RunAnalysisPlugin( + analysis_plugin, knowledge_base) + analysis_reports = self._GetAnalysisReportsFromQueue( + analysis_report_queue_consumer) + + self.assertEquals(len(analysis_reports), 1) + + analysis_report = analysis_reports[0] + + self.assertEquals(analysis_plugin._sep, u'\\') + + # Due to the behavior of the join one additional empty string at the end + # is needed to create the last empty line. + expected_text = u'\n'.join([ + u' == USER: dude ==', + u' Google Keep - notes and lists [hmjkmjkepdijhoojdojkdfohbdgmmhki]', + u'', + u' == USER: frank ==', + u' Google Play Music [icppfcnhkcmnfdhfhphakoifcfokfdhg]', + u' YouTube [blpcfgokakmgnkcojhhkbfbldkacnbeo]', + u'', + u'']) + + self.assertEquals(analysis_report.text, expected_text) + self.assertEquals(analysis_report.plugin_name, 'chrome_extension_test') + + expected_keys = set([u'frank', u'dude']) + self.assertEquals(set(analysis_report.report_dict.keys()), expected_keys) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/analysis/context.py b/plaso/analysis/context.py new file mode 100644 index 0000000..237e584 --- /dev/null +++ b/plaso/analysis/context.py @@ -0,0 +1,168 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The analysis context object.""" + + +class AnalysisContext(object): + """Class that implements the analysis context.""" + + def __init__(self, analysis_report_queue_producer, knowledge_base): + """Initializes a analysis plugin context object. + + Args: + analysis_report_queue_producer: the analysis report queue producer + (instance of ItemQueueProducer). + knowledge_base: A knowledge base object (instance of KnowledgeBase), + which contains information from the source data needed + for analysis. + """ + super(AnalysisContext, self).__init__() + self._analysis_report_queue_producer = analysis_report_queue_producer + self._knowledge_base = knowledge_base + + self.number_of_produced_analysis_reports = 0 + + @property + def users(self): + """The list of users.""" + return self._knowledge_base.users + + def GetPathSegmentSeparator(self, path): + """Given a path give back the path separator as a best guess. + + Args: + path: the path. + + Returns: + The path segment separator. + """ + if path.startswith(u'\\') or path[1:].startswith(u':\\'): + return u'\\' + + if path.startswith(u'/'): + return u'/' + + if u'/' and u'\\' in path: + # Let's count slashes and guess which one is the right one. + forward_count = len(path.split(u'/')) + backward_count = len(path.split(u'\\')) + + if forward_count > backward_count: + return u'/' + else: + return u'\\' + + # Now we are sure there is only one type of separators yet + # the path does not start with one. + if u'/' in path: + return u'/' + else: + return u'\\' + + def GetUsernameFromPath(self, user_paths, file_path, path_segment_separator): + """Return a username based on preprocessing and the path. + + During preprocessing the tool will gather file paths to where each user + profile is stored, and which user it belongs to. This function takes in + a path to a file and compares it to a list of all discovered usernames + and paths to their profiles in the system. If it finds that the file path + belongs to a user profile it will return the username that the profile + belongs to. + + Args: + user_paths: A dictionary object containing the paths per username. + file_path: The full path to the file being analyzed. + path_segment_separator: String containing the path segment separator. + + Returns: + If possible the responsible username behind the file. Otherwise None. + """ + if not user_paths: + return + + if path_segment_separator != u'/': + use_path = file_path.replace(path_segment_separator, u'/') + else: + use_path = file_path + + if use_path[1:].startswith(u':/'): + use_path = use_path[2:] + + use_path = use_path.lower() + + for user, path in user_paths.iteritems(): + if use_path.startswith(path): + return user + + def GetUserPaths(self, users): + """Retrieves the user paths. + + Args: + users: a list of users. + + Returns: + A dictionary object containing the paths per username or None if no users. + """ + if not users: + return + + user_paths = {} + + user_separator = None + for user in users: + name = user.get('name') + path = user.get('path') + + if not path or not name: + continue + + if not user_separator: + user_separator = self.GetPathSegmentSeparator(path) + + if user_separator != u'/': + path = path.replace(user_separator, u'/').replace(u'//', u'/') + + if path[1:].startswith(u':/'): + path = path[2:] + + name = name.lower() + user_paths[name] = path.lower() + + return user_paths + + def ProcessAnalysisReport(self, analysis_report, plugin_name=None): + """Processes an analysis report before it is emitted to the queue. + + Args: + analysis_report: the analysis report object (instance of AnalysisReport). + plugin_name: Optional name of the plugin. The default is None. + """ + if not getattr(analysis_report, 'plugin_name', None) and plugin_name: + analysis_report.plugin_name = plugin_name + + def ProduceAnalysisReport(self, analysis_report, plugin_name=None): + """Produces an analysis report onto the queue. + + Args: + analysis_report: the analysis report object (instance of AnalysisReport). + plugin_name: Optional name of the plugin. The default is None. + """ + self.ProcessAnalysisReport(analysis_report, plugin_name=plugin_name) + + self._analysis_report_queue_producer.ProduceItem(analysis_report) + self.number_of_produced_analysis_reports += 1 diff --git a/plaso/analysis/context_test.py b/plaso/analysis/context_test.py new file mode 100644 index 0000000..9c67978 --- /dev/null +++ b/plaso/analysis/context_test.py @@ -0,0 +1,134 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the analysis context.""" + +import unittest + +from plaso.analysis import context +from plaso.analysis import test_lib +from plaso.engine import queue +from plaso.engine import single_process + + +class AnalysisContextTest(test_lib.AnalysisPluginTestCase): + """Tests for the analysis context.""" + + MAC_PATHS = [ + '/Users/dude/Library/Application Data/Google/Chrome/Default/Extensions', + ('/Users/dude/Library/Application Data/Google/Chrome/Default/Extensions/' + 'apdfllckaahabafndbhieahigkjlhalf'), + '/private/var/log/system.log', + '/Users/frank/Library/Application Data/Google/Chrome/Default', + '/Users/hans/Library/Application Data/Google/Chrome/Default', + ('/Users/frank/Library/Application Data/Google/Chrome/Default/' + 'Extensions/pjkljhegncpnkpknbcohdijeoejaedia'), + '/Users/frank/Library/Application Data/Google/Chrome/Default/Extensions',] + + WIN_PATHS = [ + 'C:\\Users\\Dude\\SomeFolder\\Chrome\\Default\\Extensions', + ('C:\\Users\\Dude\\SomeNoneStandardFolder\\Chrome\\Default\\Extensions\\' + 'hmjkmjkepdijhoojdojkdfohbdgmmhki'), + ('\\Users\\frank\\AppData\\Local\\Google\\Chrome\\Extensions\\' + 'blpcfgokakmgnkcojhhkbfbldkacnbeo'), + '\\Users\\frank\\AppData\\Local\\Google\\Chrome\\Extensions', + ('\\Users\\frank\\AppData\\Local\\Google\\Chrome\\Extensions\\' + 'icppfcnhkcmnfdhfhphakoifcfokfdhg'), + 'C:\\Windows\\System32', + '\\Stuff/with path separator\\Folder'] + + MAC_USERS = [ + {u'name': u'root', u'path': u'/var/root', u'sid': u'0'}, + {u'name': u'frank', u'path': u'/Users/frank', u'sid': u'4052'}, + {u'name': u'hans', u'path': u'/Users/hans', u'sid': u'4352'}, + {u'name': u'dude', u'path': u'/Users/dude', u'sid': u'1123'}] + + WIN_USERS = [ + {u'name': u'dude', u'path': u'C:\\Users\\dude', u'sid': u'S-1'}, + {u'name': u'frank', u'path': u'C:\\Users\\frank', u'sid': u'S-2'}] + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + knowledge_base = self._SetUpKnowledgeBase() + + analysis_report_queue = single_process.SingleProcessQueue() + analysis_report_queue_producer = queue.ItemQueueProducer( + analysis_report_queue) + + self._analysis_context = context.AnalysisContext( + analysis_report_queue_producer, knowledge_base) + + def testGetPathSegmentSeparator(self): + """Tests the GetPathSegmentSeparator function.""" + for path in self.MAC_PATHS: + path_segment_separator = self._analysis_context.GetPathSegmentSeparator( + path) + self.assertEquals(path_segment_separator, u'/') + + for path in self.WIN_PATHS: + path_segment_separator = self._analysis_context.GetPathSegmentSeparator( + path) + self.assertEquals(path_segment_separator, u'\\') + + def testGetUserPaths(self): + """Tests the GetUserPaths function.""" + user_paths = self._analysis_context.GetUserPaths(self.MAC_USERS) + self.assertEquals( + set(user_paths.keys()), set([u'frank', u'dude', u'hans', u'root'])) + self.assertEquals(user_paths[u'frank'], u'/users/frank') + self.assertEquals(user_paths[u'dude'], u'/users/dude') + self.assertEquals(user_paths[u'hans'], u'/users/hans') + self.assertEquals(user_paths[u'root'], u'/var/root') + + user_paths = self._analysis_context.GetUserPaths(self.WIN_USERS) + self.assertEquals(set(user_paths.keys()), set([u'frank', u'dude'])) + self.assertEquals(user_paths[u'frank'], u'/users/frank') + self.assertEquals(user_paths[u'dude'], u'/users/dude') + + def testGetUsernameFromPath(self): + """Tests the GetUsernameFromPath function.""" + user_paths = self._analysis_context.GetUserPaths(self.MAC_USERS) + + username = self._analysis_context.GetUsernameFromPath( + user_paths, self.MAC_PATHS[0], u'/') + self.assertEquals(username, u'dude') + + username = self._analysis_context.GetUsernameFromPath( + user_paths, self.MAC_PATHS[4], u'/') + self.assertEquals(username, u'hans') + + username = self._analysis_context.GetUsernameFromPath( + user_paths, self.WIN_PATHS[0], u'/') + self.assertEquals(username, None) + + user_paths = self._analysis_context.GetUserPaths(self.WIN_USERS) + + username = self._analysis_context.GetUsernameFromPath( + user_paths, self.WIN_PATHS[0], u'\\') + self.assertEquals(username, u'dude') + + username = self._analysis_context.GetUsernameFromPath( + user_paths, self.WIN_PATHS[2], u'\\') + self.assertEquals(username, u'frank') + + username = self._analysis_context.GetUsernameFromPath( + user_paths, self.MAC_PATHS[2], u'\\') + self.assertEquals(username, None) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/analysis/interface.py b/plaso/analysis/interface.py new file mode 100644 index 0000000..1b447b5 --- /dev/null +++ b/plaso/analysis/interface.py @@ -0,0 +1,139 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains basic interface for analysis plugins.""" + +import abc + +from plaso.engine import queue +from plaso.lib import registry +from plaso.lib import timelib + + +class AnalysisPlugin(queue.EventObjectQueueConsumer): + """Analysis plugin gets a copy of each read event for analysis.""" + + __metaclass__ = registry.MetaclassRegistry + __abstract = True + + # The URLS should contain a list of URLs with additional information about + # this analysis plugin. + URLS = [] + + # The name of the plugin. This is the name that is matched against when + # loading plugins, so it is important that this name is short, concise and + # explains the nature of the plugin easily. It also needs to be unique. + NAME = 'Plugin' + + # A flag indicating whether or not this plugin should be run during extraction + # phase or reserved entirely for post processing stage. + # Typically this would mean that the plugin is perhaps too computationally + # heavy to be run during event extraction and should rather be run during + # post-processing. + # Since most plugins should perhaps rather be run during post-processing + # this is set to False by default and needs to be overwritten if the plugin + # should be able to run during the extraction phase. + ENABLE_IN_EXTRACTION = False + + # All the possible report types. + TYPE_ANOMALY = 1 # Plugin that is inspecting events for anomalies. + TYPE_STATISTICS = 2 # Statistical calculations. + TYPE_ANNOTATION = 3 # Inspecting events with the primary purpose of + # annotating or tagging them. + TYPE_REPORT = 4 # Inspecting events to provide a summary information. + + # Optional arguments to be added to the argument parser. + # An example would be: + # ARGUMENTS = [('--myparameter', { + # 'action': 'store', + # 'help': 'This is my parameter help', + # 'dest': 'myparameter', + # 'default': '', + # 'type': 'unicode'})] + # + # Where all arguments into the dict object have a direct translation + # into the argparse parser. + ARGUMENTS = [] + + # We need to implement the interface for analysis plugins, but we don't use + # command line options here, so disable checking for unused args. + # pylint: disable=unused-argument + def __init__(self, incoming_queue, options=None): + """Initializes an analysis plugin. + + Args: + incoming_queue: A queue that is used to listen to incoming events. + options: Optional command line arguments (instance of + argparse.Namespace). The default is None. + """ + super(AnalysisPlugin, self).__init__(incoming_queue) + self.plugin_type = self.TYPE_REPORT + + # pylint: enable=unused-argument + def _ConsumeEventObject(self, event_object, analysis_context=None, **kwargs): + """Consumes an event object callback for ConsumeEventObjects. + + Args: + event_object: An event object (instance of EventObject). + analysis_context: Optional analysis context object (instance of + AnalysisContext). The default is None. + """ + self.ExamineEvent(analysis_context, event_object, **kwargs) + + @property + def plugin_name(self): + """Return the name of the plugin.""" + return self.NAME + + @abc.abstractmethod + def CompileReport(self): + """Compiles a report of the analysis. + + After the plugin has received every copy of an event to + analyze this function will be called so that the report + can be assembled. + + Returns: + The analysis report (instance of AnalysisReport). + """ + + @abc.abstractmethod + def ExamineEvent(self, analysis_context, event_object, **kwargs): + """Analyzes an event object. + + Args: + analysis_context: An analysis context object (instance of + AnalysisContext). + event_object: An event object (instance of EventObject). + """ + + def RunPlugin(self, analysis_context): + """For each item in the queue send the read event to analysis. + + Args: + analysis_context: An analysis context object (instance of + AnalysisContext). + """ + self.ConsumeEventObjects(analysis_context=analysis_context) + + analysis_report = self.CompileReport() + + if analysis_report: + # TODO: move this into the plugins? + analysis_report.time_compiled = timelib.Timestamp.GetNow() + analysis_context.ProduceAnalysisReport( + analysis_report, plugin_name=self.plugin_name) diff --git a/plaso/analysis/test_lib.py b/plaso/analysis/test_lib.py new file mode 100644 index 0000000..1bb30a6 --- /dev/null +++ b/plaso/analysis/test_lib.py @@ -0,0 +1,171 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Analysis plugin related functions and classes for testing.""" + +import os +import unittest + +from dfvfs.lib import definitions +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver import resolver as path_spec_resolver + +from plaso.analysis import context +from plaso.artifacts import knowledge_base +from plaso.engine import queue +from plaso.engine import single_process +from plaso.lib import event +from plaso.parsers import context as parsers_context + + +class TestAnalysisReportQueueConsumer(queue.ItemQueueConsumer): + """Class that implements a test analysis report queue consumer.""" + + def __init__(self, queue_object): + """Initializes the queue consumer. + + Args: + queue_object: the queue object (instance of Queue). + """ + super(TestAnalysisReportQueueConsumer, self).__init__(queue_object) + self.analysis_reports = [] + + def _ConsumeItem(self, analysis_report): + """Consumes an item callback for ConsumeItems. + + Args: + analysis_report: the analysis report (instance of AnalysisReport). + """ + self.analysis_reports.append(analysis_report) + + @property + def number_of_analysis_reports(self): + """The number of analysis reports.""" + return len(self.analysis_reports) + + +class AnalysisPluginTestCase(unittest.TestCase): + """The unit test case for an analysis plugin.""" + + _TEST_DATA_PATH = os.path.join(os.getcwd(), 'test_data') + + # Show full diff results, part of TestCase so does not follow our naming + # conventions. + maxDiff = None + + def _GetAnalysisReportsFromQueue(self, analysis_report_queue_consumer): + """Retrieves the analysis reports from the queue consumer. + + Args: + analysis_report_queue_consumer: the analysis report queue consumer + object (instance of + TestAnalysisReportQueueConsumer). + + Returns: + A list of analysis reports (instances of AnalysisReport). + """ + analysis_report_queue_consumer.ConsumeItems() + + analysis_reports = [] + for analysis_report in analysis_report_queue_consumer.analysis_reports: + self.assertIsInstance(analysis_report, event.AnalysisReport) + analysis_reports.append(analysis_report) + + return analysis_reports + + def _GetTestFilePath(self, path_segments): + """Retrieves the path of a test file relative to the test data directory. + + Args: + path_segments: the path segments inside the test data directory. + + Returns: + A path of the test file. + """ + # Note that we need to pass the individual path segments to os.path.join + # and not a list. + return os.path.join(self._TEST_DATA_PATH, *path_segments) + + def _ParseFile(self, parser_object, path, knowledge_base_object): + """Parses a file using the parser object. + + Args: + parser_object: the parser object. + path: the path of the file to parse. + knowledge_base_object: the knowledge base object (instance of + KnowledgeBase). + + Returns: + An event object queue object (instance of Queue). + """ + event_queue = single_process.SingleProcessQueue() + event_queue_producer = queue.ItemQueueProducer(event_queue) + + parse_error_queue = single_process.SingleProcessQueue() + + parser_context = parsers_context.ParserContext( + event_queue_producer, parse_error_queue, knowledge_base_object) + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=path) + file_entry = path_spec_resolver.Resolver.OpenFileEntry(path_spec) + + parser_object.Parse(parser_context, file_entry) + event_queue.SignalEndOfInput() + + return event_queue + + def _RunAnalysisPlugin(self, analysis_plugin, knowledge_base_object): + """Analyzes an event object queue using the plugin object. + + Args: + analysis_plugin: the analysis plugin object (instance of AnalysisPlugin). + knowledge_base_object: the knowledge base object (instance of + KnowledgeBase). + + Returns: + An event object queue object (instance of Queue). + """ + analysis_report_queue = single_process.SingleProcessQueue() + analysis_report_queue_consumer = TestAnalysisReportQueueConsumer( + analysis_report_queue) + analysis_report_queue_producer = queue.ItemQueueProducer( + analysis_report_queue) + + analysis_context = context.AnalysisContext( + analysis_report_queue_producer, knowledge_base_object) + + analysis_plugin.RunPlugin(analysis_context) + analysis_report_queue.SignalEndOfInput() + + return analysis_report_queue_consumer + + def _SetUpKnowledgeBase(self, knowledge_base_values=None): + """Sets up a knowledge base. + + Args: + knowledge_base_values: optional dict containing the knowledge base + values. The default is None. + + Returns: + An knowledge base object (instance of KnowledgeBase). + """ + knowledge_base_object = knowledge_base.KnowledgeBase() + if knowledge_base_values: + for identifier, value in knowledge_base_values.iteritems(): + knowledge_base_object.SetValue(identifier, value) + + return knowledge_base_object diff --git a/plaso/analysis/windows_services.py b/plaso/analysis/windows_services.py new file mode 100644 index 0000000..aa42d90 --- /dev/null +++ b/plaso/analysis/windows_services.py @@ -0,0 +1,267 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""A plugin to enable quick triage of Windows Services.""" + +from plaso.analysis import interface +from plaso.lib import event +from plaso.winnt import human_readable_service_enums + +# Moving this import to the bottom due to complaints from certain versions of +# linters. +import yaml + + +class WindowsService(yaml.YAMLObject): + """Class to represent a Windows Service.""" + # This is used for comparison operations and defines attributes that should + # not be used during evaluation of whether two services are the same. + COMPARE_EXCLUDE = frozenset(['sources']) + + KEY_PATH_SEPARATOR = u'\\' + + # YAML attributes + yaml_tag = u'!WindowsService' + yaml_loader = yaml.SafeLoader + yaml_dumper = yaml.SafeDumper + + + def __init__(self, name, service_type, image_path, start_type, object_name, + source, service_dll=None): + """Initializes a new Windows service object. + + Args: + name: The name of the service + service_type: The value of the Type value of the service key. + image_path: The value of the ImagePath value of the service key. + start_type: The value of the Start value of the service key. + object_name: The value of the ObjectName value of the service key. + source: A tuple of (pathspec, Registry key) describing where the + service was found + service_dll: Optional string value of the ServiceDll value in the + service's Parameters subkey. The default is None. + + Raises: + TypeError: If a tuple with two elements is not passed as the 'source' + argument. + """ + self.name = name + self.service_type = service_type + self.image_path = image_path + self.start_type = start_type + self.service_dll = service_dll + self.object_name = object_name + if isinstance(source, tuple): + if len(source) != 2: + raise TypeError(u'Source arguments must be tuple of length 2.') + # A service may be found in multiple Control Sets or Registry hives, + # hence the list. + self.sources = [source] + else: + raise TypeError(u'Source argument must be a tuple.') + self.anomalies = [] + + @classmethod + def FromEvent(cls, service_event): + """Creates a Service object from an plaso event. + + Args: + service_event: The event object (instance of EventObject) to create a new + Service object from. + + """ + _, _, name = service_event.keyname.rpartition( + WindowsService.KEY_PATH_SEPARATOR) + service_type = service_event.regvalue.get('Type') + image_path = service_event.regvalue.get('ImagePath') + start_type = service_event.regvalue.get('Start') + service_dll = service_event.regvalue.get('ServiceDll', u'') + object_name = service_event.regvalue.get('ObjectName', u'') + if service_event.pathspec: + source = (service_event.pathspec.location, service_event.keyname) + else: + source = (u'Unknown', u'Unknown') + return cls( + name=name, service_type=service_type, image_path=image_path, + start_type=start_type, object_name=object_name, + source=source, service_dll=service_dll) + + def HumanReadableType(self): + """Return a human readable string describing the type value.""" + return human_readable_service_enums.SERVICE_ENUMS['Type'].get( + self.service_type, u'{0:d}'.format(self.service_type)) + + def HumanReadableStartType(self): + """Return a human readable string describing the start_type value.""" + return human_readable_service_enums.SERVICE_ENUMS['Start'].get( + self.start_type, u'{0:d}'.format(self.start_type)) + + def __eq__(self, other_service): + """Custom equality method so that we match near-duplicates. + + Compares two service objects together and evaluates if they are + the same or close enough to be considered to represent the same service. + + For two service objects to be considered the same they need to + have the the same set of attributes and same values for all their + attributes, other than those enumerated as reserved in the + COMPARE_EXCLUDE constant. + + Args: + other_service: The service (instance of WindowsService) we are testing + for equality. + + Returns: + A boolean value to indicate whether the services are equal. + + """ + if not isinstance(other_service, WindowsService): + return False + + attributes = set(self.__dict__.keys()) + other_attributes = set(self.__dict__.keys()) + + if attributes != other_attributes: + return False + + # We compare the values for all attributes, other than those specifically + # enumerated as not relevant for equality comparisons. + for attribute in attributes.difference(self.COMPARE_EXCLUDE): + if getattr(self, attribute, None) != getattr( + other_service, attribute, None): + return False + + return True + + +class WindowsServiceCollection(object): + """Class to hold and de-duplicate Windows Services.""" + + def __init__(self): + """Initialize a collection that holds Windows Service.""" + self._services = [] + + def AddService(self, new_service): + """Add a new service to the list of ones we know about. + + Args: + new_service: The service (instance of WindowsService) to add. + """ + for service in self._services: + if new_service == service: + # If this service is the same as one we already know about, we + # just want to add where it came from. + service.sources.append(new_service.sources[0]) + return + # We only add a new object to our list if we don't have + # an identical one already. + self._services.append(new_service) + + @property + def services(self): + """Get the services in this collection.""" + return self._services + + +class AnalyzeWindowsServicesPlugin(interface.AnalysisPlugin): + """Provides a single list of for Windows services found in the Registry.""" + + NAME = 'windows_services' + + # Indicate that we can run this plugin during regular extraction. + ENABLE_IN_EXTRACTION = True + + ARGUMENTS = [ + ('--windows-services-output', { + 'dest': 'windows-services-output', + 'type': unicode, + 'help': 'Specify how the results should be displayed. Options are ' + 'text and yaml.', + 'action': 'store', + 'default': u'text', + 'choices': [u'text', u'yaml']}),] + + def __init__(self, incoming_queue, options=None): + """Initializes the Windows Services plugin + + Args: + incoming_queue: A queue to read events from. + options: Optional command line arguments (instance of + argparse.Namespace). The default is None. + """ + super(AnalyzeWindowsServicesPlugin, self).__init__(incoming_queue) + self._service_collection = WindowsServiceCollection() + self.plugin_type = interface.AnalysisPlugin.TYPE_REPORT + self._output_mode = getattr(options, 'windows-services-output', u'text') + + def ExamineEvent(self, analysis_context, event_object, **kwargs): + """Analyzes an event_object and creates Windows Services as required. + + At present, this method only handles events extracted from the Registry. + + Args: + analysis_context: The context object analysis plugins. + event_object: The event object (instance of EventObject) to examine. + """ + # TODO: Handle event log entries here also (ie, event id 4697). + if getattr(event_object, 'data_type', None) != 'windows:registry:service': + return + else: + # Create and store the service. + service = WindowsService.FromEvent(event_object) + self._service_collection.AddService(service) + + def _FormatServiceText(self, service): + """Produces a human readable multi-line string representing the service. + + Args: + service: The service (instance of WindowsService) to format. + """ + string_segments = [ + service.name, + u'\tImage Path = {0:s}'.format(service.image_path), + u'\tService Type = {0:s}'.format(service.HumanReadableType()), + u'\tStart Type = {0:s}'.format(service.HumanReadableStartType()), + u'\tService Dll = {0:s}'.format(service.service_dll), + u'\tObject Name = {0:s}'.format(service.object_name), + u'\tSources:'] + for source in service.sources: + string_segments.append(u'\t\t{0:s}:{1:s}'.format(source[0], source[1])) + return u'\n'.join(string_segments) + + def CompileReport(self): + """Compiles a report of the analysis. + + Returns: + The analysis report (instance of AnalysisReport). + """ + report = event.AnalysisReport() + + if self._output_mode == 'yaml': + lines_of_text = [] + lines_of_text.append( + yaml.safe_dump_all(self._service_collection.services)) + else: + lines_of_text = ['Listing Windows Services'] + for service in self._service_collection.services: + lines_of_text.append(self._FormatServiceText(service)) + # Separate services with a blank line. + lines_of_text.append(u'') + + report.SetText(lines_of_text) + + return report diff --git a/plaso/analysis/windows_services_test.py b/plaso/analysis/windows_services_test.py new file mode 100644 index 0000000..1ee923f --- /dev/null +++ b/plaso/analysis/windows_services_test.py @@ -0,0 +1,192 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the windows services analysis plugin.""" + +import argparse +import unittest + +from dfvfs.path import fake_path_spec + +from plaso.analysis import test_lib +from plaso.analysis import windows_services +from plaso.engine import queue +from plaso.engine import single_process +from plaso.events import windows_events +from plaso.parsers import winreg + + +class WindowsServicesTest(test_lib.AnalysisPluginTestCase): + """Tests for the Windows Services analysis plugin.""" + + SERVICE_EVENTS = [ + {u'path': u'\\ControlSet001\\services\\TestbDriver', + u'text_dict': {u'ImagePath': u'C:\\Dell\\testdriver.sys', u'Type': 2, + u'Start': 2, u'ObjectName': u''}, + u'timestamp': 1346145829002031}, + # This is almost the same, but different timestamp and source, so that + # we can test the service de-duplication. + {u'path': u'\\ControlSet003\\services\\TestbDriver', + u'text_dict': {u'ImagePath': u'C:\\Dell\\testdriver.sys', u'Type': 2, + u'Start': 2, u'ObjectName': u''}, + u'timestamp': 1346145839002031}, + ] + + def _CreateAnalysisPlugin(self, input_queue, output_mode): + """Create an analysis plugin to test with. + + Args: + input_queue: A queue the plugin will read events from. + output_mode: The output format the plugin will use. + Valid options are 'text' and 'yaml'. + + Returns: + An instance of AnalyzeWindowsServicesPlugin. + """ + argument_parser = argparse.ArgumentParser() + plugin_args = windows_services.AnalyzeWindowsServicesPlugin.ARGUMENTS + for parameter, config in plugin_args: + argument_parser.add_argument(parameter, **config) + arguments = ['--windows-services-output', output_mode] + options = argument_parser.parse_args(arguments) + analysis_plugin = windows_services.AnalyzeWindowsServicesPlugin( + input_queue, options) + return analysis_plugin + + + def _CreateTestEventObject(self, service_event): + """Create a test event object with a particular path. + + Args: + service_event: A hash containing attributes of an event to add to the + queue. + + Returns: + An EventObject representing the service to be created. + """ + test_pathspec = fake_path_spec.FakePathSpec( + location=u'C:\\WINDOWS\\system32\\SYSTEM') + event_object = windows_events.WindowsRegistryServiceEvent( + service_event[u'timestamp'], service_event[u'path'], + service_event[u'text_dict']) + event_object.pathspec = test_pathspec + return event_object + + def testSyntheticKeysText(self): + """Test the plugin against mock events.""" + event_queue = single_process.SingleProcessQueue() + + # Fill the incoming queue with events. + test_queue_producer = queue.ItemQueueProducer(event_queue) + events = [self._CreateTestEventObject(service_event) + for service_event + in self.SERVICE_EVENTS] + test_queue_producer.ProduceItems(events) + test_queue_producer.SignalEndOfInput() + + # Initialize plugin. + analysis_plugin = self._CreateAnalysisPlugin(event_queue, u'text') + + # Run the analysis plugin. + knowledge_base = self._SetUpKnowledgeBase() + analysis_report_queue_consumer = self._RunAnalysisPlugin( + analysis_plugin, knowledge_base) + analysis_reports = self._GetAnalysisReportsFromQueue( + analysis_report_queue_consumer) + + self.assertEquals(len(analysis_reports), 1) + + analysis_report = analysis_reports[0] + + expected_text = ( + u'Listing Windows Services\n' + u'TestbDriver\n' + u'\tImage Path = C:\\Dell\\testdriver.sys\n' + u'\tService Type = File System Driver (0x2)\n' + u'\tStart Type = Auto Start (2)\n' + u'\tService Dll = \n' + u'\tObject Name = \n' + u'\tSources:\n' + u'\t\tC:\\WINDOWS\\system32\\SYSTEM:' + u'\\ControlSet001\\services\\TestbDriver\n' + u'\t\tC:\\WINDOWS\\system32\\SYSTEM:' + u'\\ControlSet003\\services\\TestbDriver\n\n') + + self.assertEquals(expected_text, analysis_report.text) + self.assertEquals(analysis_report.plugin_name, 'windows_services') + + def testRealEvents(self): + """Test the plugin with text output against real events from the parser.""" + parser = winreg.WinRegistryParser() + # We could remove the non-Services plugins, but testing shows that the + # performance gain is negligible. + + knowledge_base = self._SetUpKnowledgeBase() + test_path = self._GetTestFilePath(['SYSTEM']) + event_queue = self._ParseFile(parser, test_path, knowledge_base) + + # Run the analysis plugin. + analysis_plugin = self._CreateAnalysisPlugin(event_queue, u'text') + analysis_report_queue_consumer = self._RunAnalysisPlugin( + analysis_plugin, knowledge_base) + analysis_reports = self._GetAnalysisReportsFromQueue( + analysis_report_queue_consumer) + + report = analysis_reports[0] + text = report.text + + # We'll check that a few strings are in the report, like they're supposed + # to be, rather than checking for the exact content of the string, + # as that's dependent on the full path to the test files. + test_strings = [u'1394ohci', u'WwanSvc', u'Sources:', u'ControlSet001', + u'ControlSet002'] + for string in test_strings: + self.assertTrue(string in text) + + def testRealEventsYAML(self): + """Test the plugin with YAML output against real events from the parser.""" + parser = winreg.WinRegistryParser() + # We could remove the non-Services plugins, but testing shows that the + # performance gain is negligible. + + knowledge_base = self._SetUpKnowledgeBase() + test_path = self._GetTestFilePath(['SYSTEM']) + event_queue = self._ParseFile(parser, test_path, knowledge_base) + + # Run the analysis plugin. + analysis_plugin = self._CreateAnalysisPlugin(event_queue, 'yaml') + analysis_report_queue_consumer = self._RunAnalysisPlugin( + analysis_plugin, knowledge_base) + analysis_reports = self._GetAnalysisReportsFromQueue( + analysis_report_queue_consumer) + + report = analysis_reports[0] + text = report.text + + # We'll check that a few strings are in the report, like they're supposed + # to be, rather than checking for the exact content of the string, + # as that's dependent on the full path to the test files. + test_strings = [windows_services.WindowsService.yaml_tag, u'1394ohci', + u'WwanSvc', u'ControlSet001', u'ControlSet002'] + + for string in test_strings: + self.assertTrue(string in text, u'{0:s} not found in report text'.format( + string)) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/artifacts/__init__.py b/plaso/artifacts/__init__.py new file mode 100644 index 0000000..f462564 --- /dev/null +++ b/plaso/artifacts/__init__.py @@ -0,0 +1,17 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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/plaso/artifacts/knowledge_base.py b/plaso/artifacts/knowledge_base.py new file mode 100644 index 0000000..59a91a6 --- /dev/null +++ b/plaso/artifacts/knowledge_base.py @@ -0,0 +1,137 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The artifact knowledge base object. + +The knowledge base is filled by user provided input and the pre-processing +phase. It is intended to provide successive phases, like the parsing and +analysis phases, with essential information like e.g. the timezone and +codepage of the source data. +""" + +from plaso.lib import event + +import pytz + + +class KnowledgeBase(object): + """Class that implements the artifact knowledge base.""" + + def __init__(self): + """Initialize the knowledge base object.""" + super(KnowledgeBase, self).__init__() + + # TODO: the first versions of the knowledge base will wrap the pre-process + # object, but this should be replaced by an artifact style knowledge base + # or artifact cache. + self._pre_obj = event.PreprocessObject() + + self._default_codepage = u'cp1252' + self._default_timezone = pytz.timezone('UTC') + + @property + def pre_obj(self): + """The pre-process object.""" + return self._pre_obj + + @property + def codepage(self): + """The codepage.""" + return getattr(self._pre_obj, 'codepage', self._default_codepage) + + @property + def hostname(self): + """The hostname.""" + return getattr(self._pre_obj, 'hostname', u'') + + @property + def platform(self): + """The platform.""" + return getattr(self._pre_obj, 'guessed_os', u'') + + @platform.setter + def platform(self, value): + """The platform.""" + return setattr(self._pre_obj, 'guessed_os', value) + + @property + def timezone(self): + """The timezone object.""" + return getattr(self._pre_obj, 'zone', self._default_timezone) + + @property + def users(self): + """The list of users.""" + return getattr(self._pre_obj, 'users', []) + + @property + def year(self): + """The year.""" + return getattr(self._pre_obj, 'year', 0) + + def GetUsernameByIdentifier(self, identifier): + """Retrieves the username based on an identifier. + + Args: + identifier: the identifier, either a UID or SID. + + Returns: + The username or - if not available. + """ + if not identifier: + return u'-' + + return self._pre_obj.GetUsernameById(identifier) + + def GetValue(self, identifier, default_value=None): + """Retrieves a value by identifier. + + Args: + identifier: the value identifier. + default_value: optional default value. The default is None. + + Returns: + The value or None if not available. + """ + return getattr(self._pre_obj, identifier, default_value) + + def SetDefaultCodepage(self, codepage): + """Sets the default codepage. + + Args: + codepage: the default codepage. + """ + # TODO: check if value is sane. + self._default_codepage = codepage + + def SetDefaultTimezone(self, timezone): + """Sets the default timezone. + + Args: + timezone: the default timezone. + """ + # TODO: check if value is sane. + self._default_timezone = timezone + + def SetValue(self, identifier, value): + """Sets a value by identifier. + + Args: + identifier: the value identifier. + value: the value. + """ + setattr(self._pre_obj, identifier, value) diff --git a/plaso/classifier/__init__.py b/plaso/classifier/__init__.py new file mode 100644 index 0000000..ae78399 --- /dev/null +++ b/plaso/classifier/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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/plaso/classifier/classifier.py b/plaso/classifier/classifier.py new file mode 100644 index 0000000..649f31d --- /dev/null +++ b/plaso/classifier/classifier.py @@ -0,0 +1,184 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the format classifier classes. + +Plaso is a tool that extracts events from files on a file system. +For this it either reads files from a mounted file system or from an image. +It uses an exhaustive approach to determine parse events from a file, meaning +that it passes the file first to parser A and if that fails it continues with +parser B. + +The classifier is designed to be able to more quickly determine the format of +a file and limit the number of parsers part of the exhaustive approach. + +The current version of the classifier uses signatures to identify file formats. +Some signatures must always be defined at a specific offset, this is referred to +as an offset-bound signature or bound for short. Other signatures are commonly +found at a specific offset but not necessarily. The last form of signatures is +unbound, meaning that they don't have a fixed or common location where they can +be found. + +A specification is a collection of signatures with additional metadata that +defines a specific file format. These specifications are grouped into a store +for ease of use, e.g. so that they can be read from a configuration file all +at once. + +The classifier requires a scanner to analyze the data in a file. The scanner +uses the specifications in a store to scan for the signatures or a certain +format. + +The classifier allows for multiple methods of scanning a file: +* full: the entire file is scanned. This is the default scanning method. +* head-tail: only the beginning (head) and the end (tail) of the file is + scanned. This approach is more efficient for larger files. + The buffer size is used as the size of the data that is scanned. + Smaller files are scanned entirely. + +The classifier returns zero or more classifications which point to a format +specification and the scan results for the signatures defined by +the specification. +""" + +import logging + + +class Classification(object): + """This class represents a format classification. + + The format classification consists of a format specification and + scan results. + """ + + def __init__(self, specification, scan_matches): + """Initializes the classification. + + Args: + specification: the format specification (instance of Specification). + scan_matches: the list of scan matches (instances of _ScanMatch). + + Raises: + TypeError: if the specification is not of type Specification. + """ + self._specification = specification + self.scan_matches = scan_matches + + @property + def identifier(self): + """The classification type.""" + return self._specification.identifier + + @property + def magic_types(self): + """The magic types or an empty list if none.""" + return self._specification.magic_types + + @property + def mime_types(self): + """The mime type or an empty list if none.""" + return self._specification.mime_types + + +class Classifier(object): + """Class for classifying formats in raw data. + + The classifier is initialized with one or more specifications. + After which it can be used to classify data in files or file-like objects. + + The actual scanning of the data is done by the scanner, these are separate + to allow for the scanner to easily be replaced for a more efficient + alternative if necessary. + + For an example of how the classifier is to be used see: classify.py. + """ + BUFFER_SIZE = 16 * 1024 * 1024 + + def __init__(self, scanner): + """Initializes the classifier and sets up the scanning related structures. + + Args: + scanner: an instance of the signature scanner. + """ + self._scanner = scanner + + def _GetClassifications(self, scan_results): + """Retrieves the classifications based on the scan results. + + Multiple scan results are combined into a single classification. + + Args: + scan_results: a list containing instances of _ScanResult. + + Returns: + a list of instances of Classification. + """ + classifications = {} + + for scan_result in scan_results: + for scan_match in scan_result.scan_matches: + logging.debug( + u'scan match at offset: 0x{0:08x} specification: {1:s}'.format( + scan_match.total_data_offset, scan_result.identifier)) + + if scan_result.identifier not in classifications: + classifications[scan_result.identifier] = Classification( + scan_result.specification, scan_result.scan_matches) + + return classifications.values() + + def ClassifyBuffer(self, data, data_size): + """Classifies the data in a buffer, assumes all necessary data is available. + + Args: + data: a buffer containing raw data. + data_size: the size of the raw data in the buffer. + + Returns: + a list of classifications or an empty list. + """ + scan_state = self._scanner.StartScan() + self._scanner.ScanBuffer(scan_state, data, data_size) + self._scanner.StopScan(scan_state) + + return self._GetClassifications(scan_state.GetResults()) + + def ClassifyFileObject(self, file_object): + """Classifies the data in a file-like object. + + Args: + file_object: a file-like object. + + Returns: + a list of classifier classifications or an empty list. + """ + scan_results = self._scanner.ScanFileObject(file_object) + + return self._GetClassifications(scan_results) + + def ClassifyFile(self, filename): + """Classifies the data in a file. + + Args: + filename: the name of the file. + + Returns: + a list of classifier classifications or an empty list. + """ + classifications = [] + with open(filename, 'rb') as file_object: + classifications = self.ClassifyFileObject(file_object) + return classifications diff --git a/plaso/classifier/classifier_test.py b/plaso/classifier/classifier_test.py new file mode 100644 index 0000000..0575836 --- /dev/null +++ b/plaso/classifier/classifier_test.py @@ -0,0 +1,72 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains tests for the format classifier classes.""" + +import os +import unittest + +from plaso.classifier import classifier +from plaso.classifier import scanner +from plaso.classifier import test_lib + + +class ClassifierTest(unittest.TestCase): + """Class to test Classifier.""" + + def setUp(self): + """Function to test the initialize function.""" + self._store = test_lib.CreateSpecificationStore() + + self._test_file1 = os.path.join('test_data', 'NTUSER.DAT') + self._test_file2 = os.path.join('test_data', 'syslog.zip') + + def testClassifyFileWithScanner(self): + """Function to test the classify file function.""" + test_scanner = scanner.Scanner(self._store) + + test_classifier = classifier.Classifier(test_scanner) + classifications = test_classifier.ClassifyFile(self._test_file1) + self.assertEqual(len(classifications), 1) + + # TODO: assert the contents of the classification. + + test_classifier = classifier.Classifier(test_scanner) + classifications = test_classifier.ClassifyFile(self._test_file2) + self.assertEqual(len(classifications), 1) + + # TODO: assert the contents of the classification. + + def testClassifyFileWithOffsetBoundScanner(self): + """Function to test the classify file function.""" + test_scanner = scanner.OffsetBoundScanner(self._store) + + test_classifier = classifier.Classifier(test_scanner) + classifications = test_classifier.ClassifyFile(self._test_file1) + self.assertEqual(len(classifications), 1) + + # TODO: assert the contents of the classification. + + test_classifier = classifier.Classifier(test_scanner) + classifications = test_classifier.ClassifyFile(self._test_file2) + self.assertEqual(len(classifications), 1) + + # TODO: assert the contents of the classification. + + +if __name__ == "__main__": + unittest.main() diff --git a/plaso/classifier/classify.py b/plaso/classifier/classify.py new file mode 100644 index 0000000..f9789b9 --- /dev/null +++ b/plaso/classifier/classify.py @@ -0,0 +1,78 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a small classify test program.""" + +import argparse +import glob +import logging + +from plaso.classifier import classifier +from plaso.classifier import scanner +from plaso.classifier import test_lib + + +def Main(): + args_parser = argparse.ArgumentParser( + description='Classify test program.') + + args_parser.add_argument( + '-t', '--type', type='choice', metavar='TYPE', action='store', + dest='scanner_type', choices=['scan-tree', 'scan_tree'], + default='scan-tree', help='The scanner type') + + args_parser.add_argument( + '-v', '--verbose', action='store_true', dest='verbose', default=False, + help='Print verbose output') + + args_parser.add_argument( + 'filenames', nargs='+', action='store', metavar='FILENAMES', + default=None, help='The input filename(s) to classify.') + + options = args_parser.parse_args() + + if options.verbose: + logging.basicConfig(level=logging.DEBUG) + + files_to_classify = [] + for input_glob in options.filenames: + files_to_classify += glob.glob(input_glob) + + store = test_lib.CreateSpecificationStore() + + if options.scanner_type not in ['scan-tree', 'scan_tree']: + print u'Unsupported scanner type defaulting to: scan-tree' + + scan = scanner.Scanner(store) + classify = classifier.Classifier(scan) + + for input_filename in files_to_classify: + classifications = classify.ClassifyFile(input_filename) + + print u'File: {0:s}'.format(input_filename) + if not classifications: + print u'No classifications found.' + else: + print u'Classifications:' + for classification in classifications: + print u'\tformat: {0:s}'.format(classification.identifier) + + print u'' + + +if __name__ == '__main__': + Main() diff --git a/plaso/classifier/patterns.py b/plaso/classifier/patterns.py new file mode 100644 index 0000000..63f0aca --- /dev/null +++ b/plaso/classifier/patterns.py @@ -0,0 +1,308 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The patterns classes used by the scan tree-based format scanner.""" + + +class _ByteValuePatterns(object): + """Class that implements a mapping between byte value and patterns. + + The byte value patterns are used in the scan tree-based format scanner + to map a byte value to one or more patterns. + """ + + def __init__(self, byte_value): + """Initializes the pattern table (entry) byte value. + + Args: + byte_value: the byte value that maps the patterns in the table. + """ + super(_ByteValuePatterns, self).__init__() + self.byte_value = byte_value + self.patterns = {} + + def __unicode__(self): + """Retrieves a string representation of the byte value patterns.""" + return u'0x{0:02x} {1!s}'.format(ord(self.byte_value), self.patterns) + + def AddPattern(self, pattern): + """Adds a pattern. + + Args: + pattern: the pattern (instance of Pattern). + + Raises: + ValueError: if the table entry already contains a pattern + with the same identifier. + """ + if pattern.identifier in self.patterns: + raise ValueError(u'Pattern {0:s} is already defined.'.format( + pattern.identifier)) + + self.patterns[pattern.identifier] = pattern + + def ToDebugString(self, indentation_level=1): + """Converts the byte value pattern into a debug string.""" + indentation = u' ' * indentation_level + + header = u'{0:s}byte value: 0x{1:02x}\n'.format( + indentation, ord(self.byte_value)) + + entries = u''.join([u'{0:s} patterns: {1:s}\n'.format( + indentation, identifier) for identifier in self.patterns]) + + return u''.join([header, entries, u'\n']) + + +class _SkipTable(object): + """Class that implements a skip table. + + The skip table is used in the scan tree-based format scanner to determine + the skip value for the Boyer–Moore–Horspool search. + """ + + def __init__(self, skip_pattern_length): + """Initializes the skip table. + + Args: + skip_pattern_length: the (maximum) skip pattern length. + """ + super(_SkipTable, self).__init__() + self._skip_value_per_byte_value = {} + self.skip_pattern_length = skip_pattern_length + + def __getitem__(self, key): + """Retrieves a specific skip value. + + Args: + key: the byte value within the skip table. + + Returns: + the skip value for the key or the maximim skip value + if no corresponding key was found. + """ + if key in self._skip_value_per_byte_value: + return self._skip_value_per_byte_value[key] + return self.skip_pattern_length + + def SetSkipValue(self, byte_value, skip_value): + """Sets a skip value. + + Args: + byte_value: the corresponding byte value. + skip_value: the number of bytes to skip. + + Raises: + ValueError: if byte value or skip value is out of bounds. + """ + if byte_value < 0 or byte_value > 255: + raise ValueError(u'Invalid byte value, value out of bounds.') + + if skip_value < 0 or skip_value >= self.skip_pattern_length: + raise ValueError(u'Invalid skip value, value out of bounds.') + + if (not byte_value in self._skip_value_per_byte_value or + self._skip_value_per_byte_value[byte_value] > skip_value): + self._skip_value_per_byte_value[byte_value] = skip_value + + def ToDebugString(self): + """Converts the skip table into a debug string.""" + header = u'Byte value\tSkip value\n' + + entries = u''.join([u'0x{0:02x}\t{1:d}\n'.format( + byte_value, self._skip_value_per_byte_value[byte_value]) + for byte_value in self._skip_value_per_byte_value]) + + default = u'Default\t{0:d}\n'.format(self.skip_pattern_length) + + return u''.join([header, entries, default, u'\n']) + + +class Pattern(object): + """Class that implements a pattern.""" + + def __init__(self, signature_index, signature, specification): + """Initializes the pattern. + + Args: + signature_index: the index of the signature within the specification. + signature: the signature (instance of Signature). + specification: the specification (instance of Specification) that + contains the signature. + """ + super(Pattern, self).__init__() + self._signature_index = signature_index + self.signature = signature + self.specification = specification + + def __unicode__(self): + """Retrieves a string representation.""" + return self.identifier + + @property + def expression(self): + """The signature expression.""" + return self.signature.expression + + @property + def identifier(self): + """The identifier.""" + # Using _ here because some scanner implementation are limited to what + # characters can be used in the identifiers. + return u'{0:s}_{1:d}'.format( + self.specification.identifier, self._signature_index) + + @property + def offset(self): + """The signature offset.""" + return self.signature.offset + + @property + def is_bound(self): + """Boolean value to indicate the signature is bound to an offset.""" + return self.signature.is_bound + + +class PatternTable(object): + """Class that implements a pattern table. + + The pattern table is used in the the scan tree-based format scanner + to construct a scan tree. It contains either unbound patterns or + patterns bound to a specific offset. + """ + + def __init__(self, patterns, ignore_list, is_bound=None): + """Initializes and builds the patterns table from patterns. + + Args: + patterns: a list of the patterns. + ignore_list: a list of pattern offsets to ignore. + is_bound: optional boolean value to indicate if the signatures are bound + to offsets. The default is None, which means the value should + be ignored and both bound and unbound patterns are considered + unbound. + + Raises: + ValueError: if a signature pattern is too small to be useful (< 4). + """ + super(PatternTable, self).__init__() + self._byte_values_per_offset = {} + self.largest_pattern_length = 0 + self.largest_pattern_offset = 0 + self.patterns = [] + self.smallest_pattern_length = 0 + self.smallest_pattern_offset = 0 + + for pattern in patterns: + if is_bound is not None and pattern.signature.is_bound != is_bound: + continue + + pattern_length = len(pattern.expression) + + if pattern_length < 4: + raise ValueError(u'Pattern too small to be useful.') + + self.smallest_pattern_length = min( + self.smallest_pattern_length, pattern_length) + self.largest_pattern_length = max( + self.largest_pattern_length, pattern_length) + + self.patterns.append(pattern) + + self._AddPattern(pattern, ignore_list, is_bound) + + def _AddPattern(self, pattern, ignore_list, is_bound): + """Adds the byte values per offset in the pattern to the table. + + Args: + pattern: the pattern (instance of Pattern). + ignore_list: a list of pattern offsets to ignore. + is_bound: boolean value to indicate if the signatures are bound + to offsets. A value of None indicates that the value should + be ignored and both bound and unbound patterns are considered + unbound. + """ + pattern_offset = pattern.offset if is_bound else 0 + + self.smallest_pattern_offset = min( + self.smallest_pattern_offset, pattern_offset) + self.largest_pattern_offset = max( + self.largest_pattern_offset, pattern_offset) + + for byte_value in pattern.expression: + if pattern_offset not in self._byte_values_per_offset: + self._byte_values_per_offset[pattern_offset] = {} + + if pattern_offset not in ignore_list: + byte_values = self._byte_values_per_offset[pattern_offset] + + if byte_value not in byte_values: + byte_values[byte_value] = _ByteValuePatterns(byte_value) + + byte_value_patterns = byte_values[byte_value] + + byte_value_patterns.AddPattern(pattern) + + pattern_offset += 1 + + @property + def offsets(self): + """The offsets.""" + return self._byte_values_per_offset.keys() + + def GetByteValues(self, pattern_offset): + """Returns the bytes values for a specific pattern offset.""" + return self._byte_values_per_offset[pattern_offset] + + def GetSkipTable(self): + """Retrieves the skip table for the patterns in the table. + + Returns: + The skip table (instance of SkipTable). + """ + skip_table = _SkipTable(self.smallest_pattern_length) + + for pattern in self.patterns: + if pattern.expression: + skip_value = self.smallest_pattern_length + + for expression_index in range(0, self.smallest_pattern_length): + skip_value -= 1 + skip_table.SetSkipValue( + ord(pattern.expression[expression_index]), skip_value) + + return skip_table + + def ToDebugString(self): + """Converts the pattern table into a debug string.""" + header = u'Pattern offset\tByte value(s)\n' + entries = u'' + + for pattern_offset in self._byte_values_per_offset: + entries += u'{0:d}'.format(pattern_offset) + + byte_values = self._byte_values_per_offset[pattern_offset] + + for byte_value in byte_values: + identifiers = u', '.join( + [identifier for identifier in byte_values[byte_value].patterns]) + + entries += u'\t0x{0:02x} ({1:s})'.format(ord(byte_value), identifiers) + + entries += u'\n' + + return u''.join([header, entries, u'\n']) diff --git a/plaso/classifier/range_list.py b/plaso/classifier/range_list.py new file mode 100644 index 0000000..2545ca7 --- /dev/null +++ b/plaso/classifier/range_list.py @@ -0,0 +1,156 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The range list data type.""" + + +class Range(object): + """Class that implements a range object.""" + + def __init__(self, range_offset, range_size): + """Initializes the range object. + + Args: + range_offset: the range offset. + range_size: the range size. + + Raises: + ValueError: if the range offset or range size is not valid. + """ + if range_offset < 0: + raise ValueError(u'Invalid range offset value.') + + if range_size < 0: + raise ValueError(u'Invalid range size value.') + + super(Range, self).__init__() + self.start_offset = range_offset + self.size = range_size + self.end_offset = range_offset + range_size + + +class RangeList(object): + """Class that implements a range list object.""" + + def __init__(self): + """Initializes the range list object.""" + super(RangeList, self).__init__() + self.ranges = [] + + @property + def number_of_ranges(self): + """The number of ranges.""" + return len(self.ranges) + + def GetSpanningRange(self): + """Retrieves the range spanning the entire range list.""" + if self.number_of_ranges == 0: + return + + first_range = self.ranges[0] + last_range = self.ranges[-1] + range_size = last_range.end_offset - first_range.start_offset + + return Range(first_range.start_offset, range_size) + + def Insert(self, range_offset, range_size): + """Inserts the range defined by the offset and size in the list. + + Note that overlapping ranges will be merged. + + Args: + range_offset: the range offset. + range_size: the range size. + + Raises: + RuntimeError: if the range cannot be inserted. + ValueError: if the range offset or range size is not valid. + """ + if range_offset < 0: + raise ValueError(u'Invalid range offset value.') + + if range_size < 0: + raise ValueError(u'Invalid range size value.') + + insert_index = None + merge_index = None + + number_of_range_objects = len(self.ranges) + + range_end_offset = range_offset + range_size + + if number_of_range_objects == 0: + insert_index = 0 + + else: + range_object_index = 0 + + for range_object in self.ranges: + # Ignore negative ranges. + if range_object.start_offset < 0: + range_object_index += 1 + continue + + # Insert the range before an existing one. + if range_end_offset < range_object.start_offset: + insert_index = range_object_index + break + + # Ignore the range since the existing one overlaps it. + if (range_offset >= range_object.start_offset and + range_end_offset <= range_object.end_offset): + break + + # Merge the range since it overlaps the existing one at the end. + if (range_offset >= range_object.start_offset and + range_offset <= range_object.end_offset): + merge_index = range_object_index + break + + # Merge the range since it overlaps the existing one at the start. + if (range_end_offset >= range_object.start_offset and + range_end_offset <= range_object.end_offset): + merge_index = range_object_index + break + + # Merge the range since it overlaps the existing one. + if (range_offset <= range_object.start_offset and + range_end_offset >= range_object.end_offset): + merge_index = range_object_index + break + + range_object_index += 1 + + # Insert the range after the last one. + if range_object_index >= number_of_range_objects: + insert_index = number_of_range_objects + + if insert_index is not None and merge_index is not None: + raise RuntimeError( + u'Unable to insert the range both insert and merge specified.') + + if insert_index is not None: + self.ranges.insert(insert_index, Range(range_offset, range_size)) + + elif merge_index is not None: + range_object = self.ranges[merge_index] + if range_offset < range_object.start_offset: + range_object.size += range_object.start_offset - range_offset + range_object.start_offset = range_offset + if range_end_offset > range_object.end_offset: + range_object.size += range_end_offset - range_object.end_offset + range_object.end_offset = range_end_offset diff --git a/plaso/classifier/range_list_test.py b/plaso/classifier/range_list_test.py new file mode 100644 index 0000000..2e77a36 --- /dev/null +++ b/plaso/classifier/range_list_test.py @@ -0,0 +1,113 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the range list.""" + +import unittest + +from plaso.classifier import range_list + + +class RangeListTest(unittest.TestCase): + """Class to test the range list.""" + + def testInsertPositiveRanges(self): + """Function to test the insert function using positive ranges.""" + range_list_object = range_list.RangeList() + + # Test non-overlapping range. + range_list_object.Insert(500, 100) + self.assertEquals(range_list_object.number_of_ranges, 1) + + range_object = range_list_object.ranges[0] + self.assertEquals(range_object.start_offset, 500) + self.assertEquals(range_object.end_offset, 600) + self.assertEquals(range_object.size, 100) + + # Test non-overlapping range. + range_list_object.Insert(2000, 100) + self.assertEquals(range_list_object.number_of_ranges, 2) + + range_object = range_list_object.ranges[1] + self.assertEquals(range_object.start_offset, 2000) + self.assertEquals(range_object.end_offset, 2100) + self.assertEquals(range_object.size, 100) + + # Test range that overlaps with an existing range at the start. + range_list_object.Insert(1950, 100) + self.assertEquals(range_list_object.number_of_ranges, 2) + + range_object = range_list_object.ranges[1] + self.assertEquals(range_object.start_offset, 1950) + self.assertEquals(range_object.end_offset, 2100) + self.assertEquals(range_object.size, 150) + + # Test range that overlaps with an existing range at the end. + range_list_object.Insert(2050, 100) + self.assertEquals(range_list_object.number_of_ranges, 2) + + range_object = range_list_object.ranges[1] + self.assertEquals(range_object.start_offset, 1950) + self.assertEquals(range_object.end_offset, 2150) + self.assertEquals(range_object.size, 200) + + # Test non-overlapping range. + range_list_object.Insert(1000, 100) + self.assertEquals(range_list_object.number_of_ranges, 3) + + range_object = range_list_object.ranges[1] + self.assertEquals(range_object.start_offset, 1000) + self.assertEquals(range_object.end_offset, 1100) + self.assertEquals(range_object.size, 100) + + # Test range that aligns with an existing range at the end. + range_list_object.Insert(1100, 100) + self.assertEquals(range_list_object.number_of_ranges, 3) + + range_object = range_list_object.ranges[1] + self.assertEquals(range_object.start_offset, 1000) + self.assertEquals(range_object.end_offset, 1200) + self.assertEquals(range_object.size, 200) + + # Test range that aligns with an existing range at the start. + range_list_object.Insert(900, 100) + self.assertEquals(range_list_object.number_of_ranges, 3) + + range_object = range_list_object.ranges[1] + self.assertEquals(range_object.start_offset, 900) + self.assertEquals(range_object.end_offset, 1200) + self.assertEquals(range_object.size, 300) + + # Test non-overlapping range. + range_list_object.Insert(0, 100) + self.assertEquals(range_list_object.number_of_ranges, 4) + + range_object = range_list_object.ranges[0] + self.assertEquals(range_object.start_offset, 0) + self.assertEquals(range_object.end_offset, 100) + self.assertEquals(range_object.size, 100) + + # Test invalid ranges. + with self.assertRaises(ValueError): + range_list_object.Insert(-1, 100) + + with self.assertRaises(ValueError): + range_list_object.Insert(3000, -100) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/classifier/scan_tree.py b/plaso/classifier/scan_tree.py new file mode 100644 index 0000000..c7b8039 --- /dev/null +++ b/plaso/classifier/scan_tree.py @@ -0,0 +1,744 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The scan tree classes used by the scan tree-based format scanner.""" + +import logging + +from plaso.classifier import patterns +from plaso.classifier import range_list + + +class _PatternWeights(object): + """Class that implements pattern weights.""" + + def __init__(self): + """Initializes the pattern weights.""" + super(_PatternWeights, self).__init__() + self._offsets_per_weight = {} + self._weight_per_offset = {} + + def AddOffset(self, pattern_offset): + """Adds a pattern offset and sets its weight to 0. + + Args: + pattern_offset: the pattern offset to add to the pattern weights. + + Raises: + ValueError: if the pattern weights already contains the pattern offset. + """ + if pattern_offset in self._weight_per_offset: + raise ValueError(u'Pattern offset already set.') + + self._weight_per_offset[pattern_offset] = 0 + + def AddWeight(self, pattern_offset, weight): + """Adds a weight for a specific pattern offset. + + Args: + pattern_offset: the pattern offset to add to the pattern weights. + weight: the corresponding weight to add. + + Raises: + ValueError: if the pattern weights does not contain the pattern offset. + """ + if pattern_offset not in self._weight_per_offset: + raise ValueError(u'Pattern offset not set.') + + self._weight_per_offset[pattern_offset] += weight + + if weight not in self._offsets_per_weight: + self._offsets_per_weight[weight] = [] + + self._offsets_per_weight[weight].append(pattern_offset) + + def GetLargestWeight(self): + """Retrieves the largest weight or 0 if none.""" + if self._offsets_per_weight: + return max(self._offsets_per_weight) + + return 0 + + def GetOffsetsForWeight(self, weight): + """Retrieves the list of offsets for a specific weight.""" + return self._offsets_per_weight[weight] + + def GetWeightForOffset(self, pattern_offset): + """Retrieves the weight for a specific pattern offset.""" + return self._weight_per_offset[pattern_offset] + + def ToDebugString(self): + """Converts the pattern weights into a debug string.""" + header1 = u'Pattern offset\tWeight\n' + + entries1 = u''.join([u'{0:d}\t{1:d}\n'.format( + pattern_offset, self._weight_per_offset[pattern_offset]) + for pattern_offset in self._weight_per_offset]) + + header2 = u'Weight\tPattern offset(s)\n' + + entries2 = u''.join([u'{0:d}\t{1!s}\n'.format( + weight, self._offsets_per_weight[weight]) + for weight in self._offsets_per_weight]) + + return u''.join([header1, entries1, u'\n', header2, entries2, u'\n']) + + def SetWeight(self, pattern_offset, weight): + """Sets a weight for a specific pattern offset. + + Args: + pattern_offset: the pattern offset to set in the pattern weights. + weight: the corresponding weight to set. + + Raises: + ValueError: if the pattern weights does not contain the pattern offset. + """ + if pattern_offset not in self._weight_per_offset: + raise ValueError(u'Pattern offset not set.') + + self._weight_per_offset[pattern_offset] = weight + + if weight not in self._offsets_per_weight: + self._offsets_per_weight[weight] = [] + + self._offsets_per_weight[weight].append(pattern_offset) + + +class ScanTree(object): + """Class that implements a scan tree.""" + + _COMMON_BYTE_VALUES = frozenset( + '\x00\x01\xff\t\n\r 0123456789' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 'abcdefghijklmnopqrstuvwxyz') + + # The offset must be positive, negative offsets are ignored. + OFFSET_MODE_POSITIVE = 1 + # The offset must be negative, positive offsets are ignored. + OFFSET_MODE_NEGATIVE = 2 + # The offset must be positive, an error is raised for negative offsets. + OFFSET_MODE_POSITIVE_STRICT = 3 + # The offset must be negative, an error is raised for positive offsets. + OFFSET_MODE_NEGATIVE_STRICT = 4 + + def __init__( + self, specification_store, is_bound, + offset_mode=OFFSET_MODE_POSITIVE_STRICT): + """Initializes and builds the scan tree. + + Args: + specification_store: the specification store (instance of + SpecificationStore) that contains the format + specifications. + is_bound: boolean value to indicate if the signatures are bound + to offsets. A value of None indicates that the value should + be ignored and both bound and unbound patterns are considered + unbound. + offset_mode: optional value to indicate how the signature offsets should + be handled. The default is that the offset must be positive + and an error is raised for negative offsets. + """ + super(ScanTree, self).__init__() + self.largest_length = 0 + self.pattern_list = [] + self.range_list = range_list.RangeList() + self.root_node = None + self.skip_table = None + + # First determine all the patterns from the specification store. + self._BuildPatterns(specification_store, is_bound, offset_mode=offset_mode) + + # Next create the scan tree starting with the root node. + ignore_list = [] + pattern_table = patterns.PatternTable( + self.pattern_list, ignore_list, is_bound) + + if pattern_table.patterns: + self.root_node = self._BuildScanTreeNode( + pattern_table, ignore_list, is_bound) + + logging.debug(u'Scan tree:\n{0:s}'.format( + self.root_node.ToDebugString())) + + # At the end the skip table is determined to provide for the + # Boyer–Moore–Horspool skip value. + self.skip_table = pattern_table.GetSkipTable() + + logging.debug(u'Skip table:\n{0:s}'.format( + self.skip_table.ToDebugString())) + + self.largest_length = pattern_table.largest_pattern_length + + def _BuildPatterns( + self, specification_store, is_bound, + offset_mode=OFFSET_MODE_POSITIVE_STRICT): + """Builds the list of patterns. + + Args: + specification_store: the specification store (instance of + SpecificationStore) that contains the format + specifications. + is_bound: boolean value to indicate if the signatures are bound + to offsets. A value of None indicates that the value should + be ignored and both bound and unbound patterns are considered + unbound. + offset_mode: optional value to indicate how the signature offsets should + be handled. The default is that the offset must be positive + and an error is raised for negative offsets. + + Raises: + ValueError: if a signature offset invalid according to specified offset + mode or a signature pattern is too small to be useful (< 4). + """ + self.pattern_list = [] + + for specification in specification_store.specifications: + signature_index = 0 + + for signature in specification.signatures: + if signature.expression: + signature_offset = signature.offset if is_bound else 0 + signature_pattern_length = len(signature.expression) + + # Make sure signature offset is numeric. + try: + signature_offset = int(signature_offset) + except (TypeError, ValueError): + signature_offset = 0 + + if signature_offset < 0: + if offset_mode == self.OFFSET_MODE_POSITIVE: + continue + elif offset_mode == self.OFFSET_MODE_POSITIVE_STRICT: + raise ValueError(u'Signature offset less than 0.') + + # The range list does not allow offsets to be negative and thus + # the signature offset is turned into a positive equivalent. + signature_offset *= -1 + + # The signature size is substracted to make sure the spanning + # range will align with the original negative offset values. + signature_offset -= signature_pattern_length + + elif signature_offset > 0: + if offset_mode == self.OFFSET_MODE_NEGATIVE: + continue + elif offset_mode == self.OFFSET_MODE_NEGATIVE_STRICT: + raise ValueError(u'Signature offset greater than 0.') + + if signature_pattern_length < 4: + raise ValueError(u'Signature pattern smaller than 4.') + + pattern = patterns.Pattern( + signature_index, signature, specification) + self.pattern_list.append(pattern) + self.range_list.Insert(signature_offset, signature_pattern_length) + + signature_index += 1 + + def _BuildScanTreeNode(self, pattern_table, ignore_list, is_bound): + """Builds a scan tree node. + + Args: + pattern_table: a pattern table (instance of PatternTable). + ignore_list: a list of pattern offsets to ignore + is_bound: boolean value to indicate if the signatures are bound + to offsets. A value of None indicates that the value should + be ignored and both bound and unbound patterns are considered + unbound. + + Raises: + ValueError: if number of byte value patterns value out of bounds. + + Returns: + A scan tree node (instance of ScanTreeNode). + """ + # Make a copy of the lists because the function is going to alter them + # and the changes must remain in scope of the function. + pattern_list = list(pattern_table.patterns) + ignore_list = list(ignore_list) + + similarity_weights = _PatternWeights() + occurrence_weights = _PatternWeights() + value_weights = _PatternWeights() + + for pattern_offset in pattern_table.offsets: + similarity_weights.AddOffset(pattern_offset) + occurrence_weights.AddOffset(pattern_offset) + value_weights.AddOffset(pattern_offset) + + byte_values = pattern_table.GetByteValues(pattern_offset) + number_of_byte_values = len(byte_values) + + if number_of_byte_values > 1: + occurrence_weights.SetWeight(pattern_offset, number_of_byte_values) + + for byte_value in byte_values: + byte_value_patterns = byte_values[byte_value] + byte_value_weight = len(byte_value_patterns.patterns) + + if byte_value_weight > 1: + similarity_weights.AddWeight(pattern_offset, byte_value_weight) + + if byte_value_weight not in self._COMMON_BYTE_VALUES: + value_weights.AddWeight(pattern_offset, 1) + + logging.debug(u'Pattern table:\n{0:s}'.format( + pattern_table.ToDebugString())) + logging.debug(u'Similarity weights:\n{0:s}'.format( + similarity_weights.ToDebugString())) + logging.debug(u'Occurrence weights:\n{0:s}'.format( + occurrence_weights.ToDebugString())) + logging.debug(u'Value weights:\n{0:s}'.format( + value_weights.ToDebugString())) + + pattern_offset = self._GetMostSignificantPatternOffset( + pattern_list, similarity_weights, occurrence_weights, value_weights) + + ignore_list.append(pattern_offset) + + # For the scan tree negative offsets are adjusted so that + # the smallest pattern offset is 0. + scan_tree_pattern_offset = pattern_offset + if scan_tree_pattern_offset < 0: + scan_tree_pattern_offset -= pattern_table.smallest_pattern_offset + + scan_tree_node = ScanTreeNode(scan_tree_pattern_offset) + + byte_values = pattern_table.GetByteValues(pattern_offset) + + for byte_value in byte_values: + byte_value_patterns = byte_values[byte_value] + + logging.debug(u'{0:s}'.format(byte_value_patterns.ToDebugString())) + + number_of_byte_value_patterns = len(byte_value_patterns.patterns) + + if number_of_byte_value_patterns <= 0: + raise ValueError( + u'Invalid number of byte value patterns value out of bounds.') + + elif number_of_byte_value_patterns == 1: + for identifier in byte_value_patterns.patterns: + logging.debug( + u'Adding pattern: {0:s} for byte value: 0x{1:02x}.'.format( + identifier, ord(byte_value))) + + scan_tree_node.AddByteValue( + byte_value, byte_value_patterns.patterns[identifier]) + + else: + pattern_table = patterns.PatternTable( + byte_value_patterns.patterns.itervalues(), ignore_list, is_bound) + + scan_sub_node = self._BuildScanTreeNode( + pattern_table, ignore_list, is_bound) + + logging.debug( + u'Adding scan node for byte value: 0x{0:02x}\n{1:s}'.format( + ord(byte_value), scan_sub_node.ToDebugString())) + + scan_tree_node.AddByteValue(ord(byte_value), scan_sub_node) + + for identifier in byte_value_patterns.patterns: + logging.debug(u'Removing pattern: {0:s} from:\n{1:s}'.format( + identifier, self._PatternsToDebugString(pattern_list))) + + pattern_list.remove(byte_value_patterns.patterns[identifier]) + + logging.debug(u'Remaining patterns:\n{0:s}'.format( + self._PatternsToDebugString(pattern_list))) + + number_of_patterns = len(pattern_list) + + if number_of_patterns == 1: + logging.debug(u'Setting pattern: {0:s} for default value'.format( + pattern_list[0].identifier)) + + scan_tree_node.SetDefaultValue(pattern_list[0]) + + elif number_of_patterns > 1: + pattern_table = patterns.PatternTable(pattern_list, ignore_list, is_bound) + + scan_sub_node = self._BuildScanTreeNode( + pattern_table, ignore_list, is_bound) + + logging.debug(u'Setting scan node for default value:\n{0:s}'.format( + scan_sub_node.ToDebugString())) + + scan_tree_node.SetDefaultValue(scan_sub_node) + + return scan_tree_node + + def _GetMostSignificantPatternOffset( + self, pattern_list, similarity_weights, occurrence_weights, + value_weights): + """Returns the most significant pattern offset. + + Args: + pattern_list: a list of patterns + similarity_weights: the similarity (pattern) weights. + occurrence_weights: the occurrence (pattern) weights. + value_weights: the value (pattern) weights. + + Raises: + ValueError: when pattern is an empty list. + + Returns: + a pattern offset. + """ + if not pattern_list: + raise ValueError(u'Missing pattern list.') + + pattern_offset = None + number_of_patterns = len(pattern_list) + + if number_of_patterns == 1: + pattern_offset = self._GetPatternOffsetForValueWeights( + value_weights) + + elif number_of_patterns == 2: + pattern_offset = self._GetPatternOffsetForOccurrenceWeights( + occurrence_weights, value_weights) + + elif number_of_patterns > 2: + pattern_offset = self._GetPatternOffsetForSimilarityWeights( + similarity_weights, occurrence_weights, value_weights) + + logging.debug(u'Largest weight offset: {0:d}'.format(pattern_offset)) + + return pattern_offset + + def _GetPatternOffsetForOccurrenceWeights( + self, occurrence_weights, value_weights): + """Returns the most significant pattern offset based on the value weights. + + Args: + occurrence_weights: the occurrence (pattern) weights. + value_weights: the value (pattern) weights. + + Returns: + a pattern offset. + """ + debug_string = "" + pattern_offset = None + + largest_weight = occurrence_weights.GetLargestWeight() + logging.debug(u'Largest occurrence weight: {0:d}'.format(largest_weight)) + + if largest_weight > 0: + occurrence_weight_offsets = occurrence_weights.GetOffsetsForWeight( + largest_weight) + number_of_occurrence_offsets = len(occurrence_weight_offsets) + else: + number_of_occurrence_offsets = 0 + + if number_of_occurrence_offsets == 0: + pattern_offset = self._GetPatternOffsetForValueWeights( + value_weights) + + elif number_of_occurrence_offsets == 1: + pattern_offset = occurrence_weight_offsets[0] + + else: + largest_weight = 0 + largest_value_weight = 0 + + for occurrence_offset in occurrence_weight_offsets: + value_weight = value_weights.GetWeightForOffset( + occurrence_offset) + + debug_string = ( + u'Occurrence offset: {0:d} value weight: {1:d}').format( + occurrence_offset, value_weight) + + if not pattern_offset or largest_weight < value_weight: + largest_weight = value_weight + pattern_offset = occurrence_offset + + debug_string += u' largest value weight: {0:d}'.format( + largest_value_weight) + + logging.debug(u'{0:s}'.format(debug_string)) + + return pattern_offset + + def _GetPatternOffsetForSimilarityWeights( + self, similarity_weights, occurrence_weights, value_weights): + """Returns the most significant pattern offset. + + Args: + similarity_weights: the similarity (pattern) weights. + occurrence_weights: the occurrence (pattern) weights. + value_weights: the value (pattern) weights. + + Returns: + a pattern offset. + """ + debug_string = "" + pattern_offset = None + + largest_weight = similarity_weights.GetLargestWeight() + logging.debug(u'Largest similarity weight: {0:d}'.format(largest_weight)) + + if largest_weight > 0: + similarity_weight_offsets = similarity_weights.GetOffsetsForWeight( + largest_weight) + number_of_similarity_offsets = len(similarity_weight_offsets) + else: + number_of_similarity_offsets = 0 + + if number_of_similarity_offsets == 0: + pattern_offset = self._GetPatternOffsetForOccurrenceWeights( + occurrence_weights, value_weights) + + elif number_of_similarity_offsets == 1: + pattern_offset = similarity_weight_offsets[0] + + else: + largest_weight = 0 + largest_value_weight = 0 + + for similarity_offset in similarity_weight_offsets: + occurrence_weight = occurrence_weights.GetWeightForOffset( + similarity_offset) + + debug_string = ( + u'Similarity offset: {0:d} occurrence weight: {1:d}').format( + similarity_offset, occurrence_weight) + + if largest_weight > 0 and largest_weight == occurrence_weight: + value_weight = value_weights.GetWeightForOffset( + similarity_offset) + + debug_string += u' value weight: {0:d}'.format(value_weight) + + if largest_value_weight < value_weight: + largest_weight = 0 + + if not pattern_offset or largest_weight < occurrence_weight: + largest_weight = occurrence_weight + pattern_offset = similarity_offset + + largest_value_weight = value_weights.GetWeightForOffset( + similarity_offset) + + debug_string += u' largest value weight: {0:d}'.format( + largest_value_weight) + + logging.debug(u'{0:s}'.format(debug_string)) + + return pattern_offset + + def _GetPatternOffsetForValueWeights( + self, value_weights): + """Returns the most significant pattern offset based on the value weights. + + Args: + value_weights: the value (pattern) weights. + + Raises: + RuntimeError: no value weight offset were found. + + Returns: + a pattern offset. + """ + largest_weight = value_weights.GetLargestWeight() + logging.debug(u'Largest value weight: {0:d}'.format(largest_weight)) + + if largest_weight > 0: + value_weight_offsets = value_weights.GetOffsetsForWeight(largest_weight) + number_of_value_offsets = len(value_weight_offsets) + else: + number_of_value_offsets = 0 + + if number_of_value_offsets == 0: + raise RuntimeError(u'No value weight offsets found.') + + return value_weight_offsets[0] + + def _PatternsToDebugString(self, pattern_list): + """Converts the list of patterns into a debug string.""" + entries = u', '.join([u'{0:s}'.format(pattern) for pattern in pattern_list]) + + return u''.join([u'[', entries, u']']) + + +class ScanTreeNode(object): + """Class that implements a scan tree node.""" + + def __init__(self, pattern_offset): + """Initializes the scan tree node. + + Args: + pattern_offset: the offset in the pattern to which the node + applies. + """ + super(ScanTreeNode, self).__init__() + self._byte_values = {} + self.default_value = None + self.parent = None + self.pattern_offset = pattern_offset + + def AddByteValue(self, byte_value, scan_object): + """Adds a byte value. + + Args: + byte_value: the corresponding byte value. + scan_object: the scan object, either a scan sub node or a pattern. + + Raises: + ValueError: if byte value is out of bounds or if the node already + contains a scan object for the byte value. + """ + if isinstance(byte_value, str): + byte_value = ord(byte_value) + + if byte_value < 0 or byte_value > 255: + raise ValueError(u'Invalid byte value, value out of bounds.') + + if byte_value in self._byte_values: + raise ValueError(u'Byte value already set.') + + if isinstance(scan_object, ScanTreeNode): + scan_object.parent = self + + self._byte_values[byte_value] = scan_object + + def CompareByteValue( + self, data, data_offset, data_size, total_data_offset, + total_data_size=None): + """Scans a buffer using the bounded scan tree. + + This function will return partial matches on the ata block block + boundary as long as the total data size has not been reached. + + Args: + data: a buffer containing raw data. + data_offset: the offset in the raw data in the buffer. + data_size: the size of the raw data in the buffer. + total_data_offset: the offset of the data relative to the start of + the total data scanned. + total_data_size: optional value to indicate the total data size. + The default is None. + + Returns: + the resulting scan object which is either a ScanTreeNode or Pattern + or None. + + Raises: + RuntimeError: if the data offset, total data offset, total data size + or pattern offset value is out of bounds. + """ + found_match = False + scan_tree_byte_value = 0 + + if data_offset < 0 or data_offset >= data_size: + raise RuntimeError(u'Invalid data offset, value out of bounds.') + + if total_data_size is not None and total_data_size < 0: + raise RuntimeError(u'Invalid total data size, value out of bounds.') + + if total_data_offset < 0 or ( + total_data_size is not None and total_data_offset >= total_data_size): + raise RuntimeError(u'Invalid total data offset, value out of bounds.') + + if (total_data_size is not None and + total_data_offset + data_size >= total_data_size): + match_on_boundary = True + else: + match_on_boundary = False + + data_offset += self.pattern_offset + + if not match_on_boundary and data_offset >= data_size: + raise RuntimeError(u'Invalid pattern offset value, out of bounds.') + + if data_offset < data_size: + data_byte_value = ord(data[data_offset]) + + for scan_tree_byte_value in self._byte_values: + if data_byte_value == scan_tree_byte_value: + found_match = True + break + + if found_match: + scan_object = self._byte_values[scan_tree_byte_value] + + logging.debug( + u'Scan tree node match at data offset: 0x{0:08x}.'.format(data_offset) + ) + + else: + scan_object = self.default_value + + if not scan_object: + scan_object = self.parent + while scan_object and not scan_object.default_value: + scan_object = scan_object.parent + + if scan_object: + scan_object = scan_object.default_value + + return scan_object + + def SetDefaultValue(self, scan_object): + """Sets the default (non-match) value. + + Args: + scan_object: the scan object, either a scan sub node or a pattern. + + Raises: + ValueError: if the default value is already set. + """ + if self.default_value: + raise ValueError(u'Default value already set.') + + self.default_value = scan_object + + def ToDebugString(self, indentation_level=1): + """Converts the scan tree node into a debug string.""" + indentation = u' ' * indentation_level + + header = u'{0:s}pattern offset: {1:d}\n'.format( + indentation, self.pattern_offset) + + entries = u'' + + for byte_value in self._byte_values: + entries += u'{0:s}byte value: 0x{1:02x}\n'.format(indentation, byte_value) + + if isinstance(self._byte_values[byte_value], ScanTreeNode): + entries += u'{0:s}scan tree node:\n'.format(indentation) + entries += self._byte_values[byte_value].ToDebugString( + indentation_level + 1) + + elif isinstance(self._byte_values[byte_value], patterns.Pattern): + entries += u'{0:s}pattern: {1:s}\n'.format( + indentation, self._byte_values[byte_value].identifier) + + default = u'{0:s}default value:\n'.format(indentation) + + if isinstance(self.default_value, ScanTreeNode): + default += u'{0:s}scan tree node:\n'.format(indentation) + default += self.default_value.ToDebugString(indentation_level + 1) + + elif isinstance(self.default_value, patterns.Pattern): + default += u'{0:s}pattern: {1:s}\n'.format( + indentation, self.default_value.identifier) + + return u''.join([header, entries, default, u'\n']) diff --git a/plaso/classifier/scan_tree_test.py b/plaso/classifier/scan_tree_test.py new file mode 100644 index 0000000..e3227b5 --- /dev/null +++ b/plaso/classifier/scan_tree_test.py @@ -0,0 +1,74 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains tests for the scan tree classes.""" + +import unittest + +from plaso.classifier import patterns +from plaso.classifier import scan_tree +from plaso.classifier import specification + + +class ScanTreeNodeTest(unittest.TestCase): + """Class to test the scan tree node.""" + + def testAddByteValueWithPattern(self): + """Function to test the add byte value with pattern function.""" + scan_node = scan_tree.ScanTreeNode(0) + + format_regf = specification.Specification('REGF') + format_regf.AddNewSignature('regf', offset=0) + + format_esedb = specification.Specification('ESEDB') + format_esedb.AddNewSignature('\xef\xcd\xab\x89', offset=4) + + signature_esedb = specification.Signature('\xef\xcd\xab\x89', offset=4) + signature_regf = specification.Signature('regf', offset=0) + + pattern_regf = patterns.Pattern(0, signature_regf, format_regf) + pattern_esedb = patterns.Pattern(0, signature_esedb, format_esedb) + + scan_node.AddByteValue('r', pattern_regf) + scan_node.AddByteValue('\xef', pattern_esedb) + + self.assertRaises( + ValueError, scan_node.AddByteValue, 'r', pattern_regf) + self.assertRaises( + ValueError, scan_node.AddByteValue, -1, pattern_regf) + self.assertRaises( + ValueError, scan_node.AddByteValue, 256, pattern_regf) + + def testAddByteValueWithScanNode(self): + """Function to test the add byte value with scan node function.""" + scan_node = scan_tree.ScanTreeNode(0) + scan_sub_node_0x41 = scan_tree.ScanTreeNode(1) + scan_sub_node_0x80 = scan_tree.ScanTreeNode(1) + + scan_node.AddByteValue(0x41, scan_sub_node_0x41) + scan_node.AddByteValue(0x80, scan_sub_node_0x80) + + self.assertRaises( + ValueError, scan_node.AddByteValue, 0x80, scan_sub_node_0x80) + self.assertRaises( + ValueError, scan_node.AddByteValue, -1, scan_sub_node_0x80) + self.assertRaises( + ValueError, scan_node.AddByteValue, 256, scan_sub_node_0x80) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/classifier/scanner.py b/plaso/classifier/scanner.py new file mode 100644 index 0000000..b18582d --- /dev/null +++ b/plaso/classifier/scanner.py @@ -0,0 +1,749 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the classes for a scan tree-based format scanner.""" + +import logging +import os + +from plaso.classifier import patterns +from plaso.classifier import range_list +from plaso.classifier import scan_tree + + +class _ScanMatch(object): + """Class that implements a scan match.""" + + def __init__(self, total_data_offset, pattern): + """Initializes the scan result. + + Args: + total_data_offset: the offset of the resulting match relative + to the start of the total data scanned. + pattern: the pattern matched. + """ + super(_ScanMatch, self).__init__() + self.total_data_offset = total_data_offset + self.pattern = pattern + + @property + def specification(self): + """The specification.""" + return self.pattern.specification + + +class _ScanResult(object): + """Class that implements a scan result.""" + + def __init__(self, specification): + """Initializes the scan result. + + Args: + scan_tree_node: the corresponding scan tree node or None. + """ + super(_ScanResult, self).__init__() + self.specification = specification + self.scan_matches = [] + + @property + def identifier(self): + """The specification identifier.""" + return self.specification.identifier + + +class ScanState(object): + """Class that implements a scan state.""" + + # The state definitions. + _SCAN_STATE_START = 1 + _SCAN_STATE_SCANNING = 2 + _SCAN_STATE_STOP = 3 + + def __init__(self, scan_tree_node, total_data_size=None): + """Initializes the scan state. + + Args: + scan_tree_node: the corresponding scan tree node or None. + total_data_size: optional value to indicate the total data size. + The default is None. + """ + super(ScanState, self).__init__() + self._matches = [] + self.remaining_data = None + self.remaining_data_size = 0 + self.scan_tree_node = scan_tree_node + self.state = self._SCAN_STATE_START + self.total_data_offset = 0 + self.total_data_size = total_data_size + + def AddMatch(self, total_data_offset, pattern): + """Adds a result to the state to scanning. + + Args: + total_data_offset: the offset of the resulting match relative + to the start total data scanned. + pattern: the pattern matched. + + Raises: + RuntimeError: when a unsupported state is encountered. + """ + if (self.state != self._SCAN_STATE_START and + self.state != self._SCAN_STATE_SCANNING): + raise RuntimeError(u'Unsupported scan state.') + + self._matches.append(_ScanMatch(total_data_offset, pattern)) + + def GetMatches(self): + """Retrieves a list containing the results. + + Returns: + A list of scan matches (instances of _ScanMatch). + + Raises: + RuntimeError: when a unsupported state is encountered. + """ + if self.state != self._SCAN_STATE_STOP: + raise RuntimeError(u'Unsupported scan state.') + + return self._matches + + def Reset(self, scan_tree_node): + """Resets the state to start. + + This function will clear the remaining data. + + Args: + scan_tree_node: the corresponding scan tree node or None. + + Raises: + RuntimeError: when a unsupported state is encountered. + """ + if self.state != self._SCAN_STATE_STOP: + raise RuntimeError(u'Unsupported scan state.') + + self.remaining_data = None + self.remaining_data_size = 0 + self.scan_tree_node = scan_tree_node + self.state = self._SCAN_STATE_START + + def Scanning(self, scan_tree_node, total_data_offset): + """Sets the state to scanning. + + Args: + scan_tree_node: the active scan tree node. + total_data_offset: the offset of the resulting match relative + to the start of the total data scanned. + + Raises: + RuntimeError: when a unsupported state is encountered. + """ + if (self.state != self._SCAN_STATE_START and + self.state != self._SCAN_STATE_SCANNING): + raise RuntimeError(u'Unsupported scan state.') + + self.scan_tree_node = scan_tree_node + self.state = self._SCAN_STATE_SCANNING + self.total_data_offset = total_data_offset + + def Stop(self): + """Sets the state to stop. + + Raises: + RuntimeError: when a unsupported state is encountered. + """ + if (self.state != self._SCAN_STATE_START and + self.state != self._SCAN_STATE_SCANNING): + raise RuntimeError(u'Unsupported scan state.') + + self.scan_tree_node = None + self.state = self._SCAN_STATE_STOP + + +class ScanTreeScannerBase(object): + """Class that implements a scan tree-based scanner base.""" + + def __init__(self, specification_store): + """Initializes the scanner. + + Args: + specification_store: the specification store (instance of + SpecificationStore) that contains the format + specifications. + """ + super(ScanTreeScannerBase, self).__init__() + self._scan_tree = None + self._specification_store = specification_store + + def _ScanBufferScanState( + self, scan_tree_object, scan_state, data, data_size, total_data_offset, + total_data_size=None): + """Scans a buffer using the scan tree. + + This function implements a Boyer–Moore–Horspool equivalent approach + in combination with the scan tree. + + Args: + scan_tree_object: the scan tree (instance of ScanTree). + scan_state: the scan state (instance of ScanState). + data: a buffer containing raw data. + data_size: the size of the raw data in the buffer. + total_data_offset: the offset of the data relative to the start of + the total data scanned. + total_data_size: optional value to indicate the total data size. + The default is None. + + Raises: + RuntimeError: if the total data offset, total data size or the last + pattern offset value is out of bounds + """ + if total_data_size is not None and total_data_size < 0: + raise RuntimeError(u'Invalid total data size, value out of bounds.') + + if total_data_offset < 0 or ( + total_data_size is not None and total_data_offset >= total_data_size): + raise RuntimeError(u'Invalid total data offset, value out of bounds.') + + data_offset = 0 + scan_tree_node = scan_state.scan_tree_node + + if scan_state.remaining_data: + # str.join() should be more efficient then concatenation by +. + data = ''.join([scan_state.remaining_data, data]) + data_size += scan_state.remaining_data_size + + scan_state.remaining_data = None + scan_state.remaining_data_size = 0 + + if (total_data_size is not None and + total_data_offset + data_size >= total_data_size): + match_on_boundary = True + else: + match_on_boundary = False + + while data_offset < data_size: + if (not match_on_boundary and + data_offset + scan_tree_object.largest_length >= data_size): + break + + found_match = False + scan_done = False + + while not scan_done: + scan_object = scan_tree_node.CompareByteValue( + data, data_offset, data_size, total_data_offset, + total_data_size=total_data_size) + + if isinstance(scan_object, scan_tree.ScanTreeNode): + scan_tree_node = scan_object + else: + scan_done = True + + if isinstance(scan_object, patterns.Pattern): + pattern_length = len(scan_object.signature.expression) + data_last_offset = data_offset + pattern_length + + if cmp(scan_object.signature.expression, + data[data_offset:data_last_offset]) == 0: + + if (not scan_object.signature.is_bound or + scan_object.signature.offset == data_offset): + found_match = True + + logging.debug( + u'Signature match at data offset: 0x{0:08x}.'.format( + data_offset)) + + scan_state.AddMatch(total_data_offset + data_offset, scan_object) + + if found_match: + skip_value = len(scan_object.signature.expression) + scan_tree_node = scan_tree_object.root_node + else: + last_pattern_offset = ( + scan_tree_object.skip_table.skip_pattern_length - 1) + + if data_offset + last_pattern_offset >= data_size: + raise RuntimeError( + u'Invalid last pattern offset, value out of bounds.') + skip_value = 0 + + while last_pattern_offset >= 0 and not skip_value: + last_data_offset = data_offset + last_pattern_offset + byte_value = ord(data[last_data_offset]) + skip_value = scan_tree_object.skip_table[byte_value] + last_pattern_offset -= 1 + + if not skip_value: + skip_value = 1 + + scan_tree_node = scan_tree_object.root_node + + data_offset += skip_value + + if not match_on_boundary and data_offset < data_size: + scan_state.remaining_data = data[data_offset:data_size] + scan_state.remaining_data_size = data_size - data_offset + + scan_state.Scanning(scan_tree_node, total_data_offset + data_offset) + + def _ScanBufferScanStateFinal(self, scan_tree_object, scan_state): + """Scans the remaining data in the scan state using the scan tree. + + Args: + scan_tree_object: the scan tree (instance of ScanTree). + scan_state: the scan state (instance of ScanState). + """ + if scan_state.remaining_data: + data = scan_state.remaining_data + data_size = scan_state.remaining_data_size + + scan_state.remaining_data = None + scan_state.remaining_data_size = 0 + + # Setting the total data size will make boundary matches are returned + # in this scanning pass. + total_data_size = scan_state.total_data_size + if total_data_size is None: + total_data_size = scan_state.total_data_offset + data_size + + self._ScanBufferScanState( + scan_tree_object, scan_state, data, data_size, + scan_state.total_data_offset, total_data_size=total_data_size) + + scan_state.Stop() + + def GetScanResults(self, scan_state): + """Retrieves the scan results. + + Args: + scan_state: the scan state (instance of ScanState). + + Return: + A list of scan results (instances of _ScanResult). + """ + scan_results = {} + + for scan_match in scan_state.GetMatches(): + specification = scan_match.specification + identifier = specification.identifier + + logging.debug( + u'Scan match at offset: 0x{0:08x} specification: {1:s}'.format( + scan_match.total_data_offset, identifier)) + + if identifier not in scan_results: + scan_results[identifier] = _ScanResult(specification) + + scan_results[identifier].scan_matches.append(scan_match) + + return scan_results.values() + + +class Scanner(ScanTreeScannerBase): + """Class that implements a scan tree-based scanner.""" + + _READ_BUFFER_SIZE = 512 + + def __init__(self, specification_store): + """Initializes the scanner. + + Args: + specification_store: the specification store (instance of + SpecificationStore) that contains the format + specifications. + """ + super(Scanner, self).__init__(specification_store) + + def ScanBuffer(self, scan_state, data, data_size): + """Scans a buffer. + + Args: + scan_state: the scan state (instance of ScanState). + data: a buffer containing raw data. + data_size: the size of the raw data in the buffer. + """ + self._ScanBufferScanState( + self._scan_tree, scan_state, data, data_size, + scan_state.total_data_offset, + total_data_size=scan_state.total_data_size) + + def ScanFileObject(self, file_object): + """Scans a file-like object. + + Args: + file_object: a file-like object. + + Returns: + A list of scan results (instances of ScanResult). + """ + file_offset = 0 + + if hasattr(file_object, 'get_size'): + file_size = file_object.get_size() + else: + file_object.seek(0, os.SEEK_END) + file_size = file_object.tell() + + scan_state = self.StartScan(total_data_size=file_size) + + file_object.seek(file_offset, os.SEEK_SET) + + while file_offset < file_size: + data = file_object.read(self._READ_BUFFER_SIZE) + data_size = len(data) + + if data_size == 0: + break + + self._ScanBufferScanState( + self._scan_tree, scan_state, data, data_size, file_offset, + total_data_size=file_size) + + file_offset += data_size + + self.StopScan(scan_state) + + return self.GetScanResults(scan_state) + + def StartScan(self, total_data_size=None): + """Starts a scan. + + The function sets up the scanning related structures if necessary. + + Args: + total_data_size: optional value to indicate the total data size. + The default is None. + Returns: + A scan state (instance of ScanState). + + Raises: + RuntimeError: when total data size is invalid. + """ + if total_data_size is not None and total_data_size < 0: + raise RuntimeError(u'Invalid total data size.') + + if self._scan_tree is None: + self._scan_tree = scan_tree.ScanTree( + self._specification_store, None) + + return ScanState(self._scan_tree.root_node, total_data_size=total_data_size) + + def StopScan(self, scan_state): + """Stops a scan. + + Args: + scan_state: the scan state (instance of ScanState). + """ + self._ScanBufferScanStateFinal(self._scan_tree, scan_state) + + +class OffsetBoundScanner(ScanTreeScannerBase): + """Class that implements an offset-bound scan tree-based scanner.""" + + _READ_BUFFER_SIZE = 512 + + def __init__(self, specification_store): + """Initializes the scanner. + + Args: + specification_store: the specification store (instance of + SpecificationStore) that contains the format + specifications. + """ + super(OffsetBoundScanner, self).__init__(specification_store) + self._footer_scan_tree = None + self._footer_spanning_range = None + self._header_scan_tree = None + self._header_spanning_range = None + + def _GetFooterRange(self, total_data_size): + """Retrieves the read buffer aligned footer range. + + Args: + total_data_size: optional value to indicate the total data size. + The default is None. + Returns: + A range (instance of Range). + """ + # The actual footer range is in reverse since the spanning footer range + # is based on positive offsets, where 0 is the end of file. + if self._footer_spanning_range.end_offset < total_data_size: + footer_range_start_offset = ( + total_data_size - self._footer_spanning_range.end_offset) + else: + footer_range_start_offset = 0 + + # Calculate the lower bound modulus of the footer range start offset + # in increments of the read buffer size. + footer_range_start_offset /= self._READ_BUFFER_SIZE + footer_range_start_offset *= self._READ_BUFFER_SIZE + + # Calculate the upper bound modulus of the footer range size + # in increments of the read buffer size. + footer_range_size = self._footer_spanning_range.size + remainder = footer_range_size % self._READ_BUFFER_SIZE + footer_range_size /= self._READ_BUFFER_SIZE + + if remainder > 0: + footer_range_size += 1 + + footer_range_size *= self._READ_BUFFER_SIZE + + return range_list.Range(footer_range_start_offset, footer_range_size) + + def _GetHeaderRange(self): + """Retrieves the read buffer aligned header range. + + Returns: + A range (instance of Range). + """ + # Calculate the lower bound modulus of the header range start offset + # in increments of the read buffer size. + header_range_start_offset = self._header_spanning_range.start_offset + header_range_start_offset /= self._READ_BUFFER_SIZE + header_range_start_offset *= self._READ_BUFFER_SIZE + + # Calculate the upper bound modulus of the header range size + # in increments of the read buffer size. + header_range_size = self._header_spanning_range.size + remainder = header_range_size % self._READ_BUFFER_SIZE + header_range_size /= self._READ_BUFFER_SIZE + + if remainder > 0: + header_range_size += 1 + + header_range_size *= self._READ_BUFFER_SIZE + + return range_list.Range(header_range_start_offset, header_range_size) + + def _ScanBufferScanState( + self, scan_tree_object, scan_state, data, data_size, total_data_offset, + total_data_size=None): + """Scans a buffer using the scan tree. + + This function implements a Boyer–Moore–Horspool equivalent approach + in combination with the scan tree. + + Args: + scan_tree_object: the scan tree (instance of ScanTree). + scan_state: the scan state (instance of ScanState). + data: a buffer containing raw data. + data_size: the size of the raw data in the buffer. + total_data_offset: the offset of the data relative to the start of + the total data scanned. + total_data_size: optional value to indicate the total data size. + The default is None. + """ + scan_done = False + scan_tree_node = scan_tree_object.root_node + + while not scan_done: + data_offset = 0 + + scan_object = scan_tree_node.CompareByteValue( + data, data_offset, data_size, total_data_offset, + total_data_size=total_data_size) + + if isinstance(scan_object, scan_tree.ScanTreeNode): + scan_tree_node = scan_object + else: + scan_done = True + + if isinstance(scan_object, patterns.Pattern): + pattern_length = len(scan_object.signature.expression) + pattern_start_offset = scan_object.signature.offset + pattern_end_offset = pattern_start_offset + pattern_length + + if cmp(scan_object.signature.expression, + data[pattern_start_offset:pattern_end_offset]) == 0: + scan_state.AddMatch( + total_data_offset + scan_object.signature.offset, scan_object) + + logging.debug( + u'Signature match at data offset: 0x{0:08x}.'.format(data_offset)) + + # TODO: implement. + # def ScanBuffer(self, scan_state, data, data_size): + # """Scans a buffer. + + # Args: + # scan_state: the scan state (instance of ScanState). + # data: a buffer containing raw data. + # data_size: the size of the raw data in the buffer. + # """ + # # TODO: fix footer scanning logic. + # # need to know the file size here for the footers. + + # # TODO: check for clashing ranges? + + # header_range = self._GetHeaderRange() + # footer_range = self._GetFooterRange(scan_state.total_data_size) + + # if self._scan_tree == self._header_scan_tree: + # if (scan_state.total_data_offset >= header_range.start_offset and + # scan_state.total_data_offset < header_range.end_offset): + # self._ScanBufferScanState( + # self._scan_tree, scan_state, data, data_size, + # scan_state.total_data_offset, + # total_data_size=scan_state.total_data_size) + + # elif scan_state.total_data_offset > header_range.end_offset: + # # TODO: implement. + # pass + + # if self._scan_tree == self._footer_scan_tree: + # if (scan_state.total_data_offset >= footer_range.start_offset and + # scan_state.total_data_offset < footer_range.end_offset): + # self._ScanBufferScanState( + # self._scan_tree, scan_state, data, data_size, + # scan_state.total_data_offset, + # total_data_size=scan_state.total_data_size) + + def ScanFileObject(self, file_object): + """Scans a file-like object. + + Args: + file_object: a file-like object. + + Returns: + A scan state (instance of ScanState). + """ + # TODO: add support for fixed size block-based reads. + + if hasattr(file_object, 'get_size'): + file_size = file_object.get_size() + else: + file_object.seek(0, os.SEEK_END) + file_size = file_object.tell() + + file_offset = 0 + scan_state = self.StartScan(total_data_size=file_size) + + if self._header_scan_tree.root_node is not None: + header_range = self._GetHeaderRange() + + # TODO: optimize the read by supporting fixed size block-based reads. + # if file_offset < header_range.start_offset: + # file_offset = header_range.start_offset + + file_object.seek(file_offset, os.SEEK_SET) + + # TODO: optimize the read by supporting fixed size block-based reads. + # data = file_object.read(header_range.size) + data = file_object.read(header_range.end_offset) + data_size = len(data) + + if data_size > 0: + self._ScanBufferScanState( + self._scan_tree, scan_state, data, data_size, file_offset, + total_data_size=file_size) + + file_offset += data_size + + if self._footer_scan_tree.root_node is not None: + self.StopScan(scan_state) + + self._scan_tree = self._footer_scan_tree + scan_state.Reset(self._scan_tree.root_node) + + if self._footer_scan_tree.root_node is not None: + footer_range = self._GetFooterRange(file_size) + + # Note that the offset in the footer scan tree start with 0. Make sure + # the data offset of the data being scanned is aligned with the offset + # in the scan tree. + if footer_range.start_offset < self._footer_spanning_range.end_offset: + data_offset = ( + self._footer_spanning_range.end_offset - footer_range.start_offset) + else: + data_offset = 0 + + if file_offset < footer_range.start_offset: + file_offset = footer_range.start_offset + + file_object.seek(file_offset, os.SEEK_SET) + + data = file_object.read(self._READ_BUFFER_SIZE) + data_size = len(data) + + if data_size > 0: + self._ScanBufferScanState( + self._scan_tree, scan_state, data[data_offset:], + data_size - data_offset, file_offset + data_offset, + total_data_size=file_size) + + self.StopScan(scan_state) + + return self.GetScanResults(scan_state) + + def StartScan(self, total_data_size=None): + """Starts a scan. + + The function sets up the scanning related structures if necessary. + + Args: + total_data_size: optional value to indicate the total data size. + The default is None. + Returns: + A list of scan results (instances of ScanResult). + + Raises: + RuntimeError: when total data size is invalid. + """ + if total_data_size is None or total_data_size < 0: + raise RuntimeError(u'Invalid total data size.') + + if self._header_scan_tree is None: + self._header_scan_tree = scan_tree.ScanTree( + self._specification_store, True, + offset_mode=scan_tree.ScanTree.OFFSET_MODE_POSITIVE) + + if self._header_spanning_range is None: + spanning_range = self._header_scan_tree.range_list.GetSpanningRange() + self._header_spanning_range = spanning_range + + if self._footer_scan_tree is None: + self._footer_scan_tree = scan_tree.ScanTree( + self._specification_store, True, + offset_mode=scan_tree.ScanTree.OFFSET_MODE_NEGATIVE) + + if self._footer_spanning_range is None: + spanning_range = self._footer_scan_tree.range_list.GetSpanningRange() + self._footer_spanning_range = spanning_range + + if self._header_scan_tree.root_node is not None: + self._scan_tree = self._header_scan_tree + elif self._footer_scan_tree.root_node is not None: + self._scan_tree = self._footer_scan_tree + else: + self._scan_tree = None + + if self._scan_tree is not None: + root_node = self._scan_tree.root_node + else: + root_node = None + + return ScanState(root_node, total_data_size=total_data_size) + + def StopScan(self, scan_state): + """Stops a scan. + + Args: + scan_state: the scan state (instance of ScanState). + """ + self._ScanBufferScanStateFinal(self._scan_tree, scan_state) + self._scan_tree = None diff --git a/plaso/classifier/scanner_test.py b/plaso/classifier/scanner_test.py new file mode 100644 index 0000000..32098f5 --- /dev/null +++ b/plaso/classifier/scanner_test.py @@ -0,0 +1,119 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains tests for the format scanner classes.""" + +import unittest + +from plaso.classifier import scanner +from plaso.classifier import test_lib + + +class ScannerTest(unittest.TestCase): + """Class to test the scanner.""" + + def testInitialize(self): + """Function to test the initialize function.""" + store = test_lib.CreateSpecificationStore() + + # Signature for LNK + data1 = ('\x4c\x00\x00\x00\x01\x14\x02\x00\x00\x00\x00\x00\xc0\x00\x00\x00' + '\x00\x00\x00\x46') + + # Signature for REGF + data2 = 'regf' + + # Random data + data3 = '\x01\xfa\xe0\xbe\x99\x8e\xdb\x70\xea\xcc\x6b\xae\x2f\xf5\xa2\xe4' + + # Boundary scan test + data4a = ('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00PK') + data4b = ('\x07\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00Z') + + # Large buffer test + data5_size = 1024 * 1024 + data5 = '\x00' * (data5_size - 4) + data5 += 'PK\x07\x08' + + test_scanner = scanner.Scanner(store) + + total_data_size = len(data1) + scan_state = test_scanner.StartScan(total_data_size=total_data_size) + test_scanner.ScanBuffer(scan_state, data1, len(data1)) + test_scanner.StopScan(scan_state) + + self.assertEqual(len(scan_state.GetMatches()), 1) + + scan_state = test_scanner.StartScan(total_data_size=None) + test_scanner.ScanBuffer(scan_state, data1, len(data1)) + test_scanner.StopScan(scan_state) + + self.assertEqual(len(scan_state.GetMatches()), 1) + + total_data_size = len(data2) + scan_state = test_scanner.StartScan(total_data_size=total_data_size) + test_scanner.ScanBuffer(scan_state, data2, len(data2)) + test_scanner.StopScan(scan_state) + + self.assertEqual(len(scan_state.GetMatches()), 1) + + scan_state = test_scanner.StartScan(total_data_size=None) + test_scanner.ScanBuffer(scan_state, data2, len(data2)) + test_scanner.StopScan(scan_state) + + self.assertEqual(len(scan_state.GetMatches()), 1) + + total_data_size = len(data3) + scan_state = test_scanner.StartScan(total_data_size=total_data_size) + test_scanner.ScanBuffer(scan_state, data3, len(data3)) + test_scanner.StopScan(scan_state) + + self.assertEqual(len(scan_state.GetMatches()), 0) + + scan_state = test_scanner.StartScan(total_data_size=None) + test_scanner.ScanBuffer(scan_state, data3, len(data3)) + test_scanner.StopScan(scan_state) + + self.assertEqual(len(scan_state.GetMatches()), 0) + + total_data_size = len(data4a) + len(data4b) + scan_state = test_scanner.StartScan(total_data_size=total_data_size) + test_scanner.ScanBuffer(scan_state, data4a, len(data4a)) + test_scanner.ScanBuffer(scan_state, data4b, len(data4b)) + test_scanner.StopScan(scan_state) + + self.assertEqual(len(scan_state.GetMatches()), 1) + + scan_state = test_scanner.StartScan(total_data_size=None) + test_scanner.ScanBuffer(scan_state, data4a, len(data4a)) + test_scanner.ScanBuffer(scan_state, data4b, len(data4b)) + test_scanner.StopScan(scan_state) + + self.assertEqual(len(scan_state.GetMatches()), 1) + + total_data_size = len(data5) + scan_state = test_scanner.StartScan(total_data_size=total_data_size) + test_scanner.ScanBuffer(scan_state, data5, len(data5)) + test_scanner.StopScan(scan_state) + + self.assertEqual(len(scan_state.GetMatches()), 1) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/classifier/specification.py b/plaso/classifier/specification.py new file mode 100644 index 0000000..2e37fbe --- /dev/null +++ b/plaso/classifier/specification.py @@ -0,0 +1,156 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The format specification classes.""" + + +class Signature(object): + """Class that defines a signature of a format specification. + + The signature consists of a byte string expression, an optional + offset relative to the start of the data, and a value to indidate + if the expression is bound to the offset. + """ + def __init__(self, expression, offset=None, is_bound=False): + """Initializes the signature. + + Args: + expression: string containing the expression of the signature. + The expression consists of a byte string at the moment + regular expression (regexp) are not supported. + offset: the offset of the signature or None by default. None is used + to indicate the signature has no offset. A positive offset + is relative from the start of the data a negative offset + is relative from the end of the data. + is_bound: boolean value to indicate the signature must be bound to + the offset or False by default. + """ + self.expression = expression + self.offset = offset + self.is_bound = is_bound + + +class Specification(object): + """Class that contains a format specification.""" + + def __init__(self, identifier): + """Initializes the specification. + + Args: + identifier: string containing a unique name for the format. + """ + self.identifier = identifier + self.mime_types = [] + self.signatures = [] + self.universal_type_identifiers = [] + + def AddMimeType(self, mime_type): + """Adds a MIME type.""" + self.mime_types.append(mime_type) + + def AddNewSignature(self, expression, offset=None, is_bound=False): + """Adds a signature. + + Args: + expression: string containing the expression of the signature. + offset: the offset of the signature or None by default. None is used + to indicate the signature has no offset. A positive offset + is relative from the start of the data a negative offset + is relative from the end of the data. + is_bound: boolean value to indicate the signature must be bound to + the offset or False by default. + """ + self.signatures.append( + Signature(expression, offset=offset, is_bound=is_bound)) + + def AddUniversalTypeIdentifier(self, universal_type_identifiers): + """Adds a Universal Type Identifier (UTI).""" + self.universal_type_identifiers.append(universal_type_identifiers) + + +class SpecificationStore(object): + """Class that servers as a store for specifications.""" + + def __init__(self): + """Initializes the specification store.""" + self._format_specifications = {} + + @property + def specifications(self): + """A specifications iterator object.""" + return self._format_specifications.itervalues() + + def AddNewSpecification(self, identifier): + """Adds a new specification. + + Args: + identifier: a string containing the format identifier, + which should be unique for the store. + + Returns: + a instance of Specification. + + Raises: + ValueError: if the store already contains a specification with + the same identifier. + """ + if identifier in self._format_specifications: + raise ValueError("specification {0:s} is already defined in " + "store.".format(identifier)) + + self._format_specifications[identifier] = Specification(identifier) + + return self._format_specifications[identifier] + + def AddSpecification(self, specification): + """Adds a specification. + + Args: + specification: the specification (instance of Specification). + + Raises: + KeyError: if the store already contains a specification with + the same identifier. + """ + if specification.identifier in self._format_specifications: + raise KeyError( + u'Specification {0:s} is already defined in store.'.format( + specification.identifier)) + + self._format_specifications[specification.identifier] = specification + + def ReadFromFileObject(self, unused_file_object): + """Reads the specification store from a file-like object. + + Args: + unused_file_object: A file-like object. + + Raises: + RuntimeError: because functionality is not implemented yet. + """ + # TODO: implement this function. + raise RuntimeError(u'Function not implemented.') + + def ReadFromFile(self, filename): + """Reads the specification store from a file. + + Args: + filename: The name of the file. + """ + file_object = open(filename, 'r') + self.ReadFromFileObject(file_object) + file_object.close() diff --git a/plaso/classifier/specification_test.py b/plaso/classifier/specification_test.py new file mode 100644 index 0000000..6e578bb --- /dev/null +++ b/plaso/classifier/specification_test.py @@ -0,0 +1,46 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the format specification classes.""" + +import unittest + +from plaso.classifier import specification + + +class SpecificationStoreTest(unittest.TestCase): + """Class to test the specification store.""" + + def testAddSpecification(self): + """Function to test the add specification function.""" + store = specification.SpecificationStore() + + format_regf = specification.Specification('REGF') + format_regf.AddNewSignature('regf', offset=0) + + format_esedb = specification.Specification('ESEDB') + format_esedb.AddNewSignature('\xef\xcd\xab\x89', offset=4) + + store.AddSpecification(format_regf) + store.AddSpecification(format_esedb) + + with self.assertRaises(KeyError): + store.AddSpecification(format_regf) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/classifier/test_lib.py b/plaso/classifier/test_lib.py new file mode 100644 index 0000000..d0fc964 --- /dev/null +++ b/plaso/classifier/test_lib.py @@ -0,0 +1,113 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Shared test cases.""" + +from plaso.classifier import specification + + +def CreateSpecificationStore(): + """Creates a format specification store for testing purposes. + + Returns: + A format specification store (instance of SpecificationStore). + """ + store = specification.SpecificationStore() + + test_specification = store.AddNewSpecification('7zip') + test_specification.AddMimeType('application/x-7z-compressed') + test_specification.AddUniversalTypeIdentifier('org.7-zip.7-zip-archive') + test_specification.AddNewSignature('7z\xbc\xaf\x27\x1c', offset=0) + + test_specification = store.AddNewSpecification('esedb') + test_specification.AddNewSignature( + '\xef\xcd\xab\x89', offset=4, is_bound=True) + + test_specification = store.AddNewSpecification('evt') + test_specification.AddNewSignature( + '\x30\x00\x00\x00LfLe\x01\x00\x00\x00\x01\x00\x00\x00', offset=0, + is_bound=True) + + test_specification = store.AddNewSpecification('evtx') + test_specification.AddNewSignature('ElfFile\x00', offset=0, is_bound=True) + + test_specification = store.AddNewSpecification('ewf') + test_specification.AddNewSignature( + 'EVF\x09\x0d\x0a\xff\x00', offset=0, is_bound=True) + + test_specification = specification.Specification('ewf_logical') + test_specification.AddNewSignature( + 'LVF\x09\x0d\x0a\xff\x00', offset=0, is_bound=True) + + test_specification = store.AddNewSpecification('lnk') + test_specification.AddNewSignature( + '\x4c\x00\x00\x00\x01\x14\x02\x00\x00\x00\x00\x00\xc0\x00\x00\x00' + '\x00\x00\x00\x46', offset=0) + + test_specification = store.AddNewSpecification('msiecf_index_dat') + test_specification.AddNewSignature( + 'Client UrlCache MMF Ver ', offset=0, is_bound=True) + + test_specification = store.AddNewSpecification('nk2') + test_specification.AddNewSignature( + '\x0d\xf0\xad\xba\xa0\x00\x00\x00\x01\x00\x00\x00', offset=0, + is_bound=True) + + test_specification = store.AddNewSpecification('olecf') + test_specification.AddNewSignature( + '\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1', offset=0, is_bound=True) + test_specification.AddNewSignature( + '\x0e\x11\xfc\x0d\xd0\xcf\x11\x0e', offset=0, is_bound=True) + + test_specification = store.AddNewSpecification('pff') + test_specification.AddNewSignature('!BDN', offset=0, is_bound=True) + + test_specification = store.AddNewSpecification('qcow') + test_specification.AddNewSignature('QFI\xfb', offset=0, is_bound=True) + + test_specification = store.AddNewSpecification('rar') + test_specification.AddMimeType('application/x-rar-compressed') + test_specification.AddUniversalTypeIdentifier('com.rarlab.rar-archive') + test_specification.AddNewSignature( + 'Rar!\x1a\x07\x00', offset=0, is_bound=True) + + test_specification = store.AddNewSpecification('regf') + test_specification.AddNewSignature('regf', offset=0, is_bound=True) + + test_specification = store.AddNewSpecification('thumbache_db_cache') + test_specification.AddNewSignature('CMMM', offset=0, is_bound=True) + + test_specification = store.AddNewSpecification('thumbache_db_index') + test_specification.AddNewSignature('IMMM', offset=0, is_bound=True) + + test_specification = store.AddNewSpecification('zip') + test_specification.AddMimeType('application/zip') + test_specification.AddUniversalTypeIdentifier('com.pkware.zip-archive') + # WinZip 8 signature. + test_specification.AddNewSignature('PK00', offset=0, is_bound=True) + test_specification.AddNewSignature('PK\x01\x02') + test_specification.AddNewSignature('PK\x03\x04', offset=0) + test_specification.AddNewSignature('PK\x05\x05') + # Will be at offset 0 when the archive is empty. + test_specification.AddNewSignature('PK\x05\x06', offset=-22, is_bound=True) + test_specification.AddNewSignature('PK\x06\x06') + test_specification.AddNewSignature('PK\x06\x07') + test_specification.AddNewSignature('PK\x06\x08') + # Will be at offset 0 when this is spanned archive. + test_specification.AddNewSignature('PK\x07\x08') + + return store diff --git a/plaso/engine/__init__.py b/plaso/engine/__init__.py new file mode 100644 index 0000000..f462564 --- /dev/null +++ b/plaso/engine/__init__.py @@ -0,0 +1,17 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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/plaso/engine/classifier.py b/plaso/engine/classifier.py new file mode 100644 index 0000000..301bb36 --- /dev/null +++ b/plaso/engine/classifier.py @@ -0,0 +1,202 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The file format classifier.""" + +# TODO: rewrite most of the classifier in C and integrate with the code in: +# plaso/classifier + +import gzip +import logging +import os +import tarfile +import zipfile +import zlib + +from dfvfs.lib import definitions +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver import resolver as path_spec_resolver + +from plaso.lib import errors + + +class Classifier(object): + """Class that defines the file format classifier.""" + + _MAGIC_VALUES = { + 'ZIP': {'length': 4, 'offset': 0, 'values': ['P', 'K', '\x03', '\x04']}, + 'TAR': {'length': 5, 'offset': 257, 'values': ['u', 's', 't', 'a', 'r']}, + 'GZ': {'length': 2, 'offset': 0, 'values': ['\x1f', '\x8b']}, + } + + # TODO: Remove this logic when the classifier is ready. + # This is only used temporary until files can be classified. + magic_max_length = 0 + + # Defines the maximum depth into a file (for SmartOpenFiles). + MAX_FILE_DEPTH = 3 + + @classmethod + def _SmartOpenFile(cls, file_entry): + """Return a generator for all pathspec protobufs extracted from a file. + + If the file is compressed then extract all members and include + them into the processing queue. + + Args: + file_entry: The file entry object. + + Yields: + A path specification (instance of dfvfs.PathSpec) of embedded file + entries. + """ + file_object = file_entry.GetFileObject() + + # TODO: Remove when classifier gets deployed. Then we + # call the classifier here and use that for definition (and + # then we forward the classifier definition in the pathspec + # protobuf. + file_object.seek(0, os.SEEK_SET) + + if not cls.magic_max_length: + for magic_value in cls._MAGIC_VALUES.values(): + cls.magic_max_length = max( + cls.magic_max_length, + magic_value['length'] + magic_value['offset']) + + header = file_object.read(cls.magic_max_length) + + file_classification = '' + # Go over each and every magic value defined and compare + # each read byte (according to original offset and current one) + # If all match, then we have a particular file format and we + # can move on. + for m_value, m_dict in cls._MAGIC_VALUES.items(): + length = m_dict['length'] + m_dict['offset'] + if len(header) < length: + continue + + offset = m_dict['offset'] + magic = m_dict['values'] + + if header[offset:offset + len(magic)] == ''.join(magic): + file_classification = m_value + break + + # TODO: refactor the file type specific code into sub functions. + if file_classification == 'ZIP': + try: + file_object.seek(0, os.SEEK_SET) + zip_file = zipfile.ZipFile(file_object, 'r') + + # TODO: Make this is a more "sane" check, and perhaps + # not entirely skip the file if it has this particular + # ending, but for now, this both slows the tool down + # considerably and makes it also more unstable. + _, _, filename_extension = file_entry.name.rpartition(u'.') + + if filename_extension in [u'.jar', u'.sym', u'.xpi']: + file_object.close() + logging.debug( + u'Unsupported ZIP sub type: {0:s} detected in file: {1:s}'.format( + filename_extension, file_entry.path_spec.comparable)) + return + + for info in zip_file.infolist(): + if info.file_size > 0: + logging.debug( + u'Including: {0:s} from ZIP into process queue.'.format( + info.filename)) + + yield path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_ZIP, location=info.filename, + parent=file_entry.path_spec) + + except zipfile.BadZipfile: + pass + + elif file_classification == 'GZ': + try: + type_indicator = file_entry.path_spec.type_indicator + if type_indicator == definitions.TYPE_INDICATOR_GZIP: + raise errors.SameFileType + + file_object.seek(0, os.SEEK_SET) + gzip_file = gzip.GzipFile(fileobj=file_object, mode='rb') + _ = gzip_file.read(4) + gzip_file.close() + + logging.debug(( + u'Including: {0:s} as GZIP compressed stream into process ' + u'queue.').format(file_entry.name)) + + yield path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_GZIP, parent=file_entry.path_spec) + + except (IOError, zlib.error, errors.SameFileType): + pass + + # TODO: Add BZ2 support. + elif file_classification == 'TAR': + try: + file_object.seek(0, os.SEEK_SET) + tar_file = tarfile.open(fileobj=file_object, mode='r') + + for name_info in tar_file.getmembers(): + if not name_info.isfile(): + continue + + name = name_info.path + logging.debug( + u'Including: {0:s} from TAR into process queue.'.format(name)) + + yield path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_TAR, location=name, + parent=file_entry.path_spec) + + except tarfile.ReadError: + pass + + file_object.close() + + @classmethod + def SmartOpenFiles(cls, file_entry, depth=0): + """Generate a list of all available PathSpecs extracted from a file. + + Args: + file_entry: A file entry object. + depth: Incrementing number that defines the current depth into + a file (file inside a ZIP file is depth 1, file inside a tar.gz + would be of depth 2). + + Yields: + A file entry object (instance of dfvfs.FileEntry). + """ + if depth >= cls.MAX_FILE_DEPTH: + return + + for path_spec in cls._SmartOpenFile(file_entry): + sub_file_entry = path_spec_resolver.Resolver.OpenFileEntry(path_spec) + if sub_file_entry is None: + logging.debug( + u'Unable to open file: {0:s}'.format(path_spec.comparable)) + continue + yield sub_file_entry + + depth += 1 + for sub_file_entry in cls.SmartOpenFiles(sub_file_entry, depth=depth): + yield sub_file_entry diff --git a/plaso/engine/collector.py b/plaso/engine/collector.py new file mode 100644 index 0000000..e4c7af6 --- /dev/null +++ b/plaso/engine/collector.py @@ -0,0 +1,421 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Generic collector that supports both file system and image files.""" + +import hashlib +import logging +import os + +from dfvfs.helpers import file_system_searcher +from dfvfs.lib import definitions as dfvfs_definitions +from dfvfs.lib import errors as dfvfs_errors +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver import resolver as path_spec_resolver + +from plaso.engine import queue +from plaso.lib import errors + + +class Collector(queue.ItemQueueProducer): + """Class that implements a collector object.""" + + def __init__( + self, process_queue, source_path, source_path_spec, + resolver_context=None): + """Initializes the collector object. + + The collector discovers all the files that need to be processed by + the workers. Once a file is discovered it is added to the process queue + as a path specification (instance of dfvfs.PathSpec). + + Args: + process_queue: The process queue (instance of Queue). This queue contains + the file entries that need to be processed. + source_path: Path of the source file or directory. + source_path_spec: The source path specification (instance of + dfvfs.PathSpec) as determined by the file system + scanner. The default is None. + resolver_context: Optional resolver context (instance of dfvfs.Context). + The default is None. + """ + super(Collector, self).__init__(process_queue) + self._filter_find_specs = None + self._fs_collector = FileSystemCollector(process_queue) + self._resolver_context = resolver_context + # TODO: remove the need to pass source_path + self._source_path = os.path.abspath(source_path) + self._source_path_spec = source_path_spec + self._vss_stores = None + + def __enter__(self): + """Enters a with statement.""" + return self + + def __exit__(self, unused_type, unused_value, unused_traceback): + """Exits a with statement.""" + return + + def _ProcessImage(self, volume_path_spec, find_specs=None): + """Processes a volume within a storage media image. + + Args: + volume_path_spec: The path specification of the volume containing + the file system. + find_specs: Optional list of find specifications (instances of + dfvfs.FindSpec). The default is None. + """ + if find_specs: + logging.debug(u'Collecting from image file: {0:s} with filter'.format( + self._source_path)) + else: + logging.debug(u'Collecting from image file: {0:s}'.format( + self._source_path)) + + path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_TSK, location=u'/', + parent=volume_path_spec) + + try: + file_system = path_spec_resolver.Resolver.OpenFileSystem( + path_spec, resolver_context=self._resolver_context) + except IOError as exception: + logging.error( + u'Unable to open file system with error: {0:s}'.format(exception)) + return + + try: + self._fs_collector.Collect( + file_system, path_spec, find_specs=find_specs) + except (dfvfs_errors.AccessError, dfvfs_errors.BackEndError) as exception: + logging.warning(u'{0:s}'.format(exception)) + + if find_specs: + logging.debug(u'Collection from image with filter FAILED.') + else: + logging.debug(u'Collection from image FAILED.') + return + + if self._abort: + return + + if self._vss_stores: + self._ProcessVSS(volume_path_spec, find_specs=find_specs) + + if find_specs: + logging.debug(u'Collection from image with filter COMPLETED.') + else: + logging.debug(u'Collection from image COMPLETED.') + + def _ProcessVSS(self, volume_path_spec, find_specs=None): + """Processes a VSS volume within a storage media image. + + Args: + volume_path_spec: The path specification of the volume containing + the file system. + find_specs: Optional list of find specifications (instances of + dfvfs.FindSpec). The default is None. + """ + logging.info(u'Processing VSS.') + + vss_path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_VSHADOW, location=u'/', + parent=volume_path_spec) + + vss_file_entry = path_spec_resolver.Resolver.OpenFileEntry( + vss_path_spec, resolver_context=self._resolver_context) + + number_of_vss = vss_file_entry.number_of_sub_file_entries + + # In plaso 1 represents the first store index in dfvfs and pyvshadow 0 + # represents the first store index so 1 is subtracted. + vss_store_range = [store_nr - 1 for store_nr in self._vss_stores] + + for store_index in vss_store_range: + if self._abort: + return + + if find_specs: + logging.info(( + u'Collecting from VSS volume: {0:d} out of: {1:d} ' + u'with filter').format(store_index + 1, number_of_vss)) + else: + logging.info(u'Collecting from VSS volume: {0:d} out of: {1:d}'.format( + store_index + 1, number_of_vss)) + + vss_path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_VSHADOW, store_index=store_index, + parent=volume_path_spec) + path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_TSK, location=u'/', + parent=vss_path_spec) + + file_system = path_spec_resolver.Resolver.OpenFileSystem( + path_spec, resolver_context=self._resolver_context) + + try: + self._fs_collector.Collect( + file_system, path_spec, find_specs=find_specs) + except (dfvfs_errors.AccessError, dfvfs_errors.BackEndError) as exception: + logging.warning(u'{0:s}'.format(exception)) + + if find_specs: + logging.debug( + u'Collection from VSS store: {0:d} with filter FAILED.'.format( + store_index + 1)) + else: + logging.debug(u'Collection from VSS store: {0:d} FAILED.'.format( + store_index + 1)) + return + + if find_specs: + logging.debug( + u'Collection from VSS store: {0:d} with filter COMPLETED.'.format( + store_index + 1)) + else: + logging.debug(u'Collection from VSS store: {0:d} COMPLETED.'.format( + store_index + 1)) + + def Collect(self): + """Collects files from the source.""" + source_file_entry = path_spec_resolver.Resolver.OpenFileEntry( + self._source_path_spec, resolver_context=self._resolver_context) + + if not source_file_entry: + logging.warning(u'No files to collect.') + self.SignalEndOfInput() + return + + if (not source_file_entry.IsDirectory() and + not source_file_entry.IsFile() and + not source_file_entry.IsDevice()): + raise errors.CollectorError( + u'Source path: {0:s} not a device, file or directory.'.format( + self._source_path)) + + type_indicator = self._source_path_spec.type_indicator + if type_indicator == dfvfs_definitions.TYPE_INDICATOR_OS: + if source_file_entry.IsFile(): + self.ProduceItem(self._source_path_spec) + + else: + file_system = path_spec_resolver.Resolver.OpenFileSystem( + self._source_path_spec, resolver_context=self._resolver_context) + + try: + self._fs_collector.Collect( + file_system, self._source_path_spec, + find_specs=self._filter_find_specs) + except (dfvfs_errors.AccessError, + dfvfs_errors.BackEndError) as exception: + logging.warning(u'{0:s}'.format(exception)) + + else: + self._ProcessImage( + self._source_path_spec.parent, find_specs=self._filter_find_specs) + + self.SignalEndOfInput() + + def SetCollectDirectoryMetadata(self, collect_directory_metadata): + """Sets the collect directory metadata flag. + + Args: + collect_directory_metadata: Boolean value to indicate to collect + directory metadata. + """ + self._fs_collector.SetCollectDirectoryMetadata(collect_directory_metadata) + + def SetFilter(self, filter_find_specs): + """Sets the collection filter find specifications. + + Args: + filter_find_specs: List of filter find specifications (instances of + dfvfs.FindSpec). + """ + self._filter_find_specs = filter_find_specs + + def SetVssInformation(self, vss_stores): + """Sets the Volume Shadow Snapshots (VSS) information. + + This function will enable VSS collection. + + Args: + vss_stores: The range of VSS stores to include in the collection, + where 1 represents the first store. + """ + self._vss_stores = vss_stores + + def SignalAbort(self): + """Signals the producer to abort.""" + super(Collector, self).SignalAbort() + self._fs_collector.SignalAbort() + + +class FileSystemCollector(queue.ItemQueueProducer): + """Class that implements a file system collector object.""" + + def __init__(self, process_queue): + """Initializes the collector object. + + The collector discovers all the files that need to be processed by + the workers. Once a file is discovered it is added to the process queue + as a path specification (instance of dfvfs.PathSpec). + + Args: + process_queue: The process queue (instance of Queue). This queue contains + the file entries that need to be processed. + """ + super(FileSystemCollector, self).__init__(process_queue) + self._collect_directory_metadata = True + self._duplicate_file_check = False + self._hashlist = {} + + self.number_of_file_entries = 0 + + def __enter__(self): + """Enters a with statement.""" + return self + + def __exit__(self, unused_type, unused_value, unused_traceback): + """Exits a with statement.""" + return + + def _CalculateNTFSTimeHash(self, file_entry): + """Return a hash value calculated from a NTFS file's metadata. + + Args: + file_entry: The file entry (instance of TSKFileEntry). + + Returns: + A hash value (string) that can be used to determine if a file's timestamp + value has changed. + """ + stat_object = file_entry.GetStat() + ret_hash = hashlib.md5() + + ret_hash.update('atime:{0:d}.{1:d}'.format( + getattr(stat_object, 'atime', 0), + getattr(stat_object, 'atime_nano', 0))) + + ret_hash.update('crtime:{0:d}.{1:d}'.format( + getattr(stat_object, 'crtime', 0), + getattr(stat_object, 'crtime_nano', 0))) + + ret_hash.update('mtime:{0:d}.{1:d}'.format( + getattr(stat_object, 'mtime', 0), + getattr(stat_object, 'mtime_nano', 0))) + + ret_hash.update('ctime:{0:d}.{1:d}'.format( + getattr(stat_object, 'ctime', 0), + getattr(stat_object, 'ctime_nano', 0))) + + return ret_hash.hexdigest() + + def _ProcessDirectory(self, file_entry): + """Processes a directory and extract its metadata if necessary.""" + # Need to do a breadth-first search otherwise we'll hit the Python + # maximum recursion depth. + sub_directories = [] + + for sub_file_entry in file_entry.sub_file_entries: + if self._abort: + return + + try: + if not sub_file_entry.IsAllocated() or sub_file_entry.IsLink(): + continue + except dfvfs_errors.BackEndError as exception: + logging.warning( + u'Unable to process file: {0:s} with error: {1:s}'.format( + sub_file_entry.path_spec.comparable.replace( + u'\n', u';'), exception)) + continue + + # For TSK-based file entries only, ignore the virtual /$OrphanFiles + # directory. + if sub_file_entry.type_indicator == dfvfs_definitions.TYPE_INDICATOR_TSK: + if file_entry.IsRoot() and sub_file_entry.name == u'$OrphanFiles': + continue + + if sub_file_entry.IsDirectory(): + # This check is here to improve performance by not producing + # path specifications that don't get processed. + if self._collect_directory_metadata: + self.ProduceItem(sub_file_entry.path_spec) + self.number_of_file_entries += 1 + + sub_directories.append(sub_file_entry) + + elif sub_file_entry.IsFile(): + # If we are dealing with a VSS we want to calculate a hash + # value based on available timestamps and compare that to previously + # calculated hash values, and only include the file into the queue if + # the hash does not match. + if self._duplicate_file_check: + hash_value = self._CalculateNTFSTimeHash(sub_file_entry) + + inode = getattr(sub_file_entry.path_spec, 'inode', 0) + if inode in self._hashlist: + if hash_value in self._hashlist[inode]: + continue + + self._hashlist.setdefault(inode, []).append(hash_value) + + self.ProduceItem(sub_file_entry.path_spec) + self.number_of_file_entries += 1 + + for sub_file_entry in sub_directories: + if self._abort: + return + + try: + self._ProcessDirectory(sub_file_entry) + except (dfvfs_errors.AccessError, dfvfs_errors.BackEndError) as exception: + logging.warning(u'{0:s}'.format(exception)) + + def Collect(self, file_system, path_spec, find_specs=None): + """Collects files from the file system. + + Args: + file_system: The file system (instance of dfvfs.FileSystem). + path_spec: The path specification (instance of dfvfs.PathSpec). + find_specs: Optional list of find specifications (instances of + dfvfs.FindSpec). The default is None. + """ + if find_specs: + searcher = file_system_searcher.FileSystemSearcher(file_system, path_spec) + + for path_spec in searcher.Find(find_specs=find_specs): + if self._abort: + return + + self.ProduceItem(path_spec) + self.number_of_file_entries += 1 + + else: + file_entry = file_system.GetFileEntryByPathSpec(path_spec) + + self._ProcessDirectory(file_entry) + + def SetCollectDirectoryMetadata(self, collect_directory_metadata): + """Sets the collect directory metadata flag. + + Args: + collect_directory_metadata: Boolean value to indicate to collect + directory metadata. + """ + self._collect_directory_metadata = collect_directory_metadata diff --git a/plaso/engine/collector_test.py b/plaso/engine/collector_test.py new file mode 100644 index 0000000..08c14c8 --- /dev/null +++ b/plaso/engine/collector_test.py @@ -0,0 +1,354 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The unit tests for the generic collector object.""" + +import logging +import os +import shutil +import tempfile +import unittest + +from dfvfs.helpers import file_system_searcher +from dfvfs.lib import definitions as dfvfs_definitions +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver import context +from dfvfs.resolver import resolver as path_spec_resolver + +from plaso.engine import collector +from plaso.engine import queue +from plaso.engine import single_process +from plaso.engine import utils as engine_utils + + +class TempDirectory(object): + """A self cleaning temporary directory.""" + + def __init__(self): + """Initializes the temporary directory.""" + super(TempDirectory, self).__init__() + self.name = u'' + + def __enter__(self): + """Make this work with the 'with' statement.""" + self.name = tempfile.mkdtemp() + return self.name + + def __exit__(self, unused_type, unused_value, unused_traceback): + """Make this work with the 'with' statement.""" + shutil.rmtree(self.name, True) + + +class TestCollectorQueueConsumer(queue.ItemQueueConsumer): + """Class that implements a test collector queue consumer.""" + + def __init__(self, queue_object): + """Initializes the queue consumer. + + Args: + queue_object: the queue object (instance of Queue). + """ + super(TestCollectorQueueConsumer, self).__init__(queue_object) + self.path_specs = [] + + def _ConsumeItem(self, path_spec): + """Consumes an item callback for ConsumeItems. + + Args: + path_spec: a path specification (instance of dfvfs.PathSpec). + """ + self.path_specs.append(path_spec) + + @property + def number_of_path_specs(self): + """The number of path specifications.""" + return len(self.path_specs) + + def GetFilePaths(self): + """Retrieves a list of file paths from the path specifications.""" + file_paths = [] + for path_spec in self.path_specs: + location = getattr(path_spec, 'location', None) + if location is not None: + file_paths.append(location) + return file_paths + + +class CollectorTestCase(unittest.TestCase): + """The collector test case.""" + + _TEST_DATA_PATH = os.path.join(os.getcwd(), u'test_data') + + # Show full diff results, part of TestCase so does not follow our naming + # conventions. + maxDiff = None + + def _GetTestFilePath(self, path_segments): + """Retrieves the path of a test file relative to the test data directory. + + Args: + path_segments: the path segments inside the test data directory. + + Returns: + A path of the test file. + """ + # Note that we need to pass the individual path segments to os.path.join + # and not a list. + return os.path.join(self._TEST_DATA_PATH, *path_segments) + + +class CollectorTest(CollectorTestCase): + """Tests for the collector.""" + + def testFileSystemCollection(self): + """Test collection on the file system.""" + test_files = [ + self._GetTestFilePath([u'syslog.tgz']), + self._GetTestFilePath([u'syslog.zip']), + self._GetTestFilePath([u'syslog.bz2']), + self._GetTestFilePath([u'wtmp.1'])] + + with TempDirectory() as dirname: + for a_file in test_files: + shutil.copy(a_file, dirname) + + path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_OS, location=dirname) + + test_collection_queue = single_process.SingleProcessQueue() + resolver_context = context.Context() + test_collector = collector.Collector( + test_collection_queue, dirname, path_spec, + resolver_context=resolver_context) + test_collector.Collect() + + test_collector_queue_consumer = TestCollectorQueueConsumer( + test_collection_queue) + test_collector_queue_consumer.ConsumeItems() + + self.assertEquals(test_collector_queue_consumer.number_of_path_specs, 4) + + def testFileSystemWithFilterCollection(self): + """Test collection on the file system with a filter.""" + dirname = u'.' + path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_OS, location=dirname) + + filter_name = '' + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + filter_name = temp_file.name + temp_file.write('/test_data/testdir/filter_.+.txt\n') + temp_file.write('/test_data/.+evtx\n') + temp_file.write('/AUTHORS\n') + temp_file.write('/does_not_exist/some_file_[0-9]+txt\n') + + test_collection_queue = single_process.SingleProcessQueue() + resolver_context = context.Context() + test_collector = collector.Collector( + test_collection_queue, dirname, path_spec, + resolver_context=resolver_context) + + find_specs = engine_utils.BuildFindSpecsFromFile(filter_name) + test_collector.SetFilter(find_specs) + + test_collector.Collect() + + test_collector_queue_consumer = TestCollectorQueueConsumer( + test_collection_queue) + test_collector_queue_consumer.ConsumeItems() + + try: + os.remove(filter_name) + except (OSError, IOError) as exception: + logging.warning(( + u'Unable to remove temporary file: {0:s} with error: {1:s}').format( + filter_name, exception)) + + # Two files with test_data/testdir/filter_*.txt, AUTHORS + # and test_data/System.evtx. + self.assertEquals(test_collector_queue_consumer.number_of_path_specs, 4) + + paths = test_collector_queue_consumer.GetFilePaths() + + current_directory = os.getcwd() + + expected_path = os.path.join( + current_directory, u'test_data', u'testdir', u'filter_1.txt') + self.assertTrue(expected_path in paths) + + expected_path = os.path.join( + current_directory, u'test_data', u'testdir', u'filter_2.txt') + self.assertFalse(expected_path in paths) + + expected_path = os.path.join( + current_directory, u'test_data', u'testdir', u'filter_3.txt') + self.assertTrue(expected_path in paths) + + expected_path = os.path.join( + current_directory, u'AUTHORS') + self.assertTrue(expected_path in paths) + + def testImageCollection(self): + """Test collection on a storage media image file. + + This images has two files: + + logs/hidden.zip + + logs/sys.tgz + + The hidden.zip file contains one file, syslog, which is the + same for sys.tgz. + + The end results should therefore be: + + logs/hidden.zip (unchanged) + + logs/hidden.zip:syslog (the text file extracted out) + + logs/sys.tgz (unchanged) + + logs/sys.tgz (read as a GZIP file, so not compressed) + + logs/sys.tgz:syslog.gz (A GZIP file from the TAR container) + + logs/sys.tgz:syslog.gz:syslog (the extracted syslog file) + + This means that the collection script should collect 6 files in total. + """ + test_file = self._GetTestFilePath([u'syslog_image.dd']) + + volume_path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_OS, location=test_file) + path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_TSK, location=u'/', + parent=volume_path_spec) + + test_collection_queue = single_process.SingleProcessQueue() + resolver_context = context.Context() + test_collector = collector.Collector( + test_collection_queue, test_file, path_spec, + resolver_context=resolver_context) + test_collector.Collect() + + test_collector_queue_consumer = TestCollectorQueueConsumer( + test_collection_queue) + test_collector_queue_consumer.ConsumeItems() + + self.assertEquals(test_collector_queue_consumer.number_of_path_specs, 3) + + def testImageWithFilterCollection(self): + """Test collection on a storage media image file with a filter.""" + test_file = self._GetTestFilePath([u'ímynd.dd']) + + volume_path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_OS, location=test_file) + path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_TSK, location=u'/', + parent=volume_path_spec) + + filter_name = '' + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + filter_name = temp_file.name + temp_file.write('/a_directory/.+zip\n') + temp_file.write('/a_directory/another.+\n') + temp_file.write('/passwords.txt\n') + + test_collection_queue = single_process.SingleProcessQueue() + resolver_context = context.Context() + test_collector = collector.Collector( + test_collection_queue, test_file, path_spec, + resolver_context=resolver_context) + + find_specs = engine_utils.BuildFindSpecsFromFile(filter_name) + test_collector.SetFilter(find_specs) + + test_collector.Collect() + + test_collector_queue_consumer = TestCollectorQueueConsumer( + test_collection_queue) + test_collector_queue_consumer.ConsumeItems() + + try: + os.remove(filter_name) + except (OSError, IOError) as exception: + logging.warning(( + u'Unable to remove temporary file: {0:s} with error: {1:s}').format( + filter_name, exception)) + + self.assertEquals(test_collector_queue_consumer.number_of_path_specs, 2) + + paths = test_collector_queue_consumer.GetFilePaths() + + # path_specs[0] + # type: TSK + # file_path: '/a_directory/another_file' + # container_path: 'test_data/ímynd.dd' + # image_offset: 0 + self.assertEquals(paths[0], u'/a_directory/another_file') + + # path_specs[1] + # type: TSK + # file_path: '/passwords.txt' + # container_path: 'test_data/ímynd.dd' + # image_offset: 0 + self.assertEquals(paths[1], u'/passwords.txt') + + +class BuildFindSpecsFromFileTest(unittest.TestCase): + """Tests for the BuildFindSpecsFromFile function.""" + + def testBuildFindSpecsFromFile(self): + """Tests the BuildFindSpecsFromFile function.""" + filter_name = '' + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + filter_name = temp_file.name + # 2 hits. + temp_file.write('/test_data/testdir/filter_.+.txt\n') + # A single hit. + temp_file.write('/test_data/.+evtx\n') + # A single hit. + temp_file.write('/AUTHORS\n') + temp_file.write('/does_not_exist/some_file_[0-9]+txt\n') + # This should not compile properly, missing file information. + temp_file.write('failing/\n') + # This should not fail during initial loading, but fail later on. + temp_file.write('bad re (no close on that parenthesis/file\n') + + find_specs = engine_utils.BuildFindSpecsFromFile(filter_name) + + try: + os.remove(filter_name) + except (OSError, IOError) as exception: + logging.warning( + u'Unable to remove temporary file: {0:s} with error: {1:s}'.format( + filter_name, exception)) + + self.assertEquals(len(find_specs), 4) + + dirname = u'.' + path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_OS, location=dirname) + file_system = path_spec_resolver.Resolver.OpenFileSystem(path_spec) + searcher = file_system_searcher.FileSystemSearcher( + file_system, path_spec) + + path_spec_generator = searcher.Find(find_specs=find_specs) + self.assertNotEquals(path_spec_generator, None) + + path_specs = list(path_spec_generator) + # One evtx, one AUTHORS, two filter_*.txt files, total 4 files. + self.assertEquals(len(path_specs), 4) + + with self.assertRaises(IOError): + _ = engine_utils.BuildFindSpecsFromFile('thisfiledoesnotexist') + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/engine/engine.py b/plaso/engine/engine.py new file mode 100644 index 0000000..64ec829 --- /dev/null +++ b/plaso/engine/engine.py @@ -0,0 +1,319 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The processing engine.""" + +import abc +import logging + +from dfvfs.helpers import file_system_searcher +from dfvfs.lib import definitions as dfvfs_definitions +from dfvfs.resolver import resolver as path_spec_resolver + +from plaso.artifacts import knowledge_base +from plaso.engine import collector +from plaso.engine import queue +from plaso.lib import errors +from plaso.preprocessors import interface as preprocess_interface +from plaso.preprocessors import manager as preprocess_manager + + +class BaseEngine(object): + """Class that defines the processing engine base.""" + + def __init__(self, collection_queue, storage_queue, parse_error_queue): + """Initialize the engine object. + + Args: + collection_queue: the collection queue object (instance of Queue). + storage_queue: the storage queue object (instance of Queue). + parse_error_queue: the parser error queue object (instance of Queue). + """ + self._collection_queue = collection_queue + self._enable_debug_output = False + self._enable_profiling = False + self._event_queue_producer = queue.ItemQueueProducer(storage_queue) + self._filter_object = None + self._mount_path = None + self._open_files = False + self._parse_error_queue = parse_error_queue + self._parse_error_queue_producer = queue.ItemQueueProducer( + parse_error_queue) + self._profiling_sample_rate = 1000 + self._source = None + self._source_path_spec = None + self._source_file_entry = None + self._text_prepend = None + + self.knowledge_base = knowledge_base.KnowledgeBase() + self.storage_queue = storage_queue + + def CreateCollector( + self, include_directory_stat, vss_stores=None, filter_find_specs=None, + resolver_context=None): + """Creates a collector object. + + The collector discovers all the files that need to be processed by + the workers. Once a file is discovered it is added to the process queue + as a path specification (instance of dfvfs.PathSpec). + + Args: + include_directory_stat: Boolean value to indicate whether directory + stat information should be collected. + vss_stores: Optional list of VSS stores to include in the collection, + where 1 represents the first store. Set to None if no + VSS stores should be processed. The default is None. + filter_find_specs: Optional list of filter find specifications (instances + of dfvfs.FindSpec). The default is None. + resolver_context: Optional resolver context (instance of dfvfs.Context). + The default is None. Note that every thread or process + must have its own resolver context. + + Returns: + A collector object (instance of Collector). + + Raises: + RuntimeError: if source path specification is not set. + """ + if not self._source_path_spec: + raise RuntimeError(u'Missing source.') + + collector_object = collector.Collector( + self._collection_queue, self._source, self._source_path_spec, + resolver_context=resolver_context) + + collector_object.SetCollectDirectoryMetadata(include_directory_stat) + + if vss_stores: + collector_object.SetVssInformation(vss_stores) + + if filter_find_specs: + collector_object.SetFilter(filter_find_specs) + + return collector_object + + @abc.abstractmethod + def CreateExtractionWorker(self, worker_number): + """Creates an extraction worker object. + + Args: + worker_number: A number that identifies the worker. + + Returns: + An extraction worker (instance of worker.ExtractionWorker). + """ + + def GetSourceFileSystemSearcher(self, resolver_context=None): + """Retrieves the file system searcher of the source. + + Args: + resolver_context: Optional resolver context (instance of dfvfs.Context). + The default is None. Note that every thread or process + must have its own resolver context. + + Returns: + The file system searcher object (instance of dfvfs.FileSystemSearcher). + + Raises: + RuntimeError: if source path specification is not set. + """ + if not self._source_path_spec: + raise RuntimeError(u'Missing source.') + + file_system = path_spec_resolver.Resolver.OpenFileSystem( + self._source_path_spec, resolver_context=resolver_context) + + type_indicator = self._source_path_spec.type_indicator + if type_indicator == dfvfs_definitions.TYPE_INDICATOR_OS: + mount_point = self._source_path_spec + else: + mount_point = self._source_path_spec.parent + + return file_system_searcher.FileSystemSearcher(file_system, mount_point) + + def PreprocessSource(self, platform, resolver_context=None): + """Preprocesses the source and fills the preprocessing object. + + Args: + platform: string that indicates the platform (operating system). + resolver_context: Optional resolver context (instance of dfvfs.Context). + The default is None. Note that every thread or process + must have its own resolver context. + """ + searcher = self.GetSourceFileSystemSearcher( + resolver_context=resolver_context) + if not platform: + platform = preprocess_interface.GuessOS(searcher) + self.knowledge_base.platform = platform + + preprocess_manager.PreprocessPluginsManager.RunPlugins( + platform, searcher, self.knowledge_base) + + def SetEnableDebugOutput(self, enable_debug_output): + """Enables or disables debug output. + + Args: + enable_debug_output: boolean value to indicate if the debug output + should be enabled. + """ + self._enable_debug_output = enable_debug_output + + def SetEnableProfiling(self, enable_profiling, profiling_sample_rate=1000): + """Enables or disables profiling. + + Args: + enable_debug_output: boolean value to indicate if the profiling + should be enabled. + profiling_sample_rate: optional integer indicating the profiling sample + rate. The value contains the number of files + processed. The default value is 1000. + """ + self._enable_profiling = enable_profiling + self._profiling_sample_rate = profiling_sample_rate + + def SetFilterObject(self, filter_object): + """Sets the filter object. + + Args: + filter_object: the filter object (instance of objectfilter.Filter). + """ + self._filter_object = filter_object + + def SetMountPath(self, mount_path): + """Sets the mount path. + + Args: + mount_path: string containing the mount path. + """ + self._mount_path = mount_path + + # TODO: rename this mode. + def SetOpenFiles(self, open_files): + """Sets the open files mode. + + Args: + open_files: boolean value to indicate if the worker should scan for + file entries inside files. + """ + self._open_files = open_files + + def SetSource(self, source_path_spec, resolver_context=None): + """Sets the source. + + Args: + source_path_spec: The source path specification (instance of + dfvfs.PathSpec) as determined by the file system + scanner. The default is None. + resolver_context: Optional resolver context (instance of dfvfs.Context). + The default is None. Note that every thread or process + must have its own resolver context. + + Raises: + BadConfigOption: if source cannot be set. + """ + path_spec = source_path_spec + while path_spec.parent: + path_spec = path_spec.parent + + # Note that source should be used for output purposes only. + self._source = getattr(path_spec, 'location', u'') + self._source_path_spec = source_path_spec + + self._source_file_entry = path_spec_resolver.Resolver.OpenFileEntry( + self._source_path_spec, resolver_context=resolver_context) + + if not self._source_file_entry: + raise errors.BadConfigOption( + u'No such device, file or directory: {0:s}.'.format(self._source)) + + if (not self._source_file_entry.IsDirectory() and + not self._source_file_entry.IsFile() and + not self._source_file_entry.IsDevice()): + raise errors.CollectorError( + u'Source path: {0:s} not a device, file or directory.'.format( + self._source)) + + if self._source_path_spec.type_indicator in [ + dfvfs_definitions.TYPE_INDICATOR_OS, + dfvfs_definitions.TYPE_INDICATOR_FAKE]: + + if self._source_file_entry.IsFile(): + logging.debug(u'Starting a collection on a single file.') + # No need for multiple workers when parsing a single file. + + elif not self._source_file_entry.IsDirectory(): + raise errors.BadConfigOption( + u'Source: {0:s} has to be a file or directory.'.format( + self._source)) + + # TODO: remove this functionality. + def SetTextPrepend(self, text_prepend): + """Sets the text prepend. + + Args: + text_prepend: string that contains the text to prepend to every + event object. + """ + self._text_prepend = text_prepend + + def SignalAbort(self): + """Signals the engine to abort.""" + logging.warning(u'Signalled abort.') + self._event_queue_producer.SignalEndOfInput() + self._parse_error_queue_producer.SignalEndOfInput() + + def SignalEndOfInputStorageQueue(self): + """Signals the storage queue no input remains.""" + self._event_queue_producer.SignalEndOfInput() + self._parse_error_queue_producer.SignalEndOfInput() + + def SourceIsDirectory(self): + """Determines if the source is a directory. + + Raises: + RuntimeError: if source path specification is not set. + """ + if not self._source_file_entry: + raise RuntimeError(u'Missing source.') + + return (not self.SourceIsStorageMediaImage() and + self._source_file_entry.IsDirectory()) + + def SourceIsFile(self): + """Determines if the source is a file. + + Raises: + RuntimeError: if source path specification is not set. + """ + if not self._source_file_entry: + raise RuntimeError(u'Missing source.') + + return (not self.SourceIsStorageMediaImage() and + self._source_file_entry.IsFile()) + + def SourceIsStorageMediaImage(self): + """Determines if the source is storage media image file or device. + + Raises: + RuntimeError: if source path specification is not set. + """ + if not self._source_path_spec: + raise RuntimeError(u'Missing source.') + + return self._source_path_spec.type_indicator not in [ + dfvfs_definitions.TYPE_INDICATOR_OS, + dfvfs_definitions.TYPE_INDICATOR_FAKE] diff --git a/plaso/engine/queue.py b/plaso/engine/queue.py new file mode 100644 index 0000000..bd6d7d8 --- /dev/null +++ b/plaso/engine/queue.py @@ -0,0 +1,204 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Queue management implementation for Plaso. + +This file contains an implementation of a queue used by plaso for +queue management. + +The queue has been abstracted in order to provide support for different +implementations of the queueing mechanism, to support multi processing and +scalability. +""" + +import abc + +from plaso.lib import errors + + +class QueueEndOfInput(object): + """Class that implements a queue end of input.""" + + +class Queue(object): + """Class that implements the queue interface.""" + + @abc.abstractmethod + def __len__(self): + """Returns the estimated current number of items in the queue.""" + + @abc.abstractmethod + def IsEmpty(self): + """Determines if the queue is empty.""" + + @abc.abstractmethod + def PushItem(self, item): + """Pushes an item onto the queue.""" + + @abc.abstractmethod + def PopItem(self): + """Pops an item off the queue.""" + + def SignalEndOfInput(self): + """Signals the queue no input remains.""" + self.PushItem(QueueEndOfInput()) + + +class QueueConsumer(object): + """Class that implements the queue consumer interface. + + The consumer subscribes to updates on the queue. + """ + + def __init__(self, queue_object): + """Initializes the queue consumer. + + Args: + queue_object: the queue object (instance of Queue). + """ + super(QueueConsumer, self).__init__() + self._abort = False + self._queue = queue_object + + def SignalAbort(self): + """Signals the consumer to abort.""" + self._abort = True + + +class QueueProducer(object): + """Class that implements the queue producer interface. + + The producer generates updates on the queue. + """ + + def __init__(self, queue_object): + """Initializes the queue producer. + + Args: + queue_object: the queue object (instance of Queue). + """ + super(QueueProducer, self).__init__() + self._abort = False + self._queue = queue_object + + def SignalAbort(self): + """Signals the producer to abort.""" + self._abort = True + + def SignalEndOfInput(self): + """Signals the queue no input remains.""" + self._queue.SignalEndOfInput() + + +class EventObjectQueueConsumer(QueueConsumer): + """Class that implements the event object queue consumer. + + The consumer subscribes to updates on the queue. + """ + + @abc.abstractmethod + def _ConsumeEventObject(self, event_object, **kwargs): + """Consumes an event object callback for ConsumeEventObjects.""" + + def ConsumeEventObjects(self, **kwargs): + """Consumes the event object that are pushed on the queue. + + This function will issue a callback to _ConsumeEventObject for every + event object (instance of EventObject) consumed from the queue. + + Args: + kwargs: keyword arguments to pass to the _ConsumeEventObject callback. + """ + while not self._abort: + try: + item = self._queue.PopItem() + except errors.QueueEmpty: + break + + if isinstance(item, QueueEndOfInput): + # Push the item back onto the queue to make sure all + # queue consumers are stopped. + self._queue.PushItem(item) + break + + self._ConsumeEventObject(item, **kwargs) + + self._abort = False + + +class ItemQueueConsumer(QueueConsumer): + """Class that implements an item queue consumer. + + The consumer subscribes to updates on the queue. + """ + + @abc.abstractmethod + def _ConsumeItem(self, item): + """Consumes an item callback for ConsumeItems. + + Args: + item: the item object. + """ + + def ConsumeItems(self): + """Consumes the items that are pushed on the queue.""" + while not self._abort: + try: + item = self._queue.PopItem() + except errors.QueueEmpty: + break + + if isinstance(item, QueueEndOfInput): + # Push the item back onto the queue to make sure all + # queue consumers are stopped. + self._queue.PushItem(item) + break + + self._ConsumeItem(item) + + self._abort = False + + +class ItemQueueProducer(QueueProducer): + """Class that implements an item queue producer. + + The producer generates updates on the queue. + """ + + def _FlushQueue(self): + """Flushes the queue callback for the QueueFull exception.""" + return + + def ProduceItem(self, item): + """Produces an item onto the queue. + + Args: + item: the item object. + """ + try: + self._queue.PushItem(item) + except errors.QueueFull: + self._FlushQueue() + + def ProduceItems(self, items): + """Produces items onto the queue. + + Args: + items: a list or generator of item objects. + """ + for item in items: + self.ProduceItem(item) diff --git a/plaso/engine/single_process.py b/plaso/engine/single_process.py new file mode 100644 index 0000000..bde1c38 --- /dev/null +++ b/plaso/engine/single_process.py @@ -0,0 +1,366 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The single process processing engine.""" + +import collections +import logging +import pdb + +from plaso.engine import collector +from plaso.engine import engine +from plaso.engine import queue +from plaso.engine import worker +from plaso.lib import errors +from plaso.parsers import context as parsers_context + + +class SingleProcessCollector(collector.Collector): + """Class that implements a single process collector object.""" + + def __init__( + self, process_queue, source_path, source_path_spec, + resolver_context=None): + """Initializes the collector object. + + The collector discovers all the files that need to be processed by + the workers. Once a file is discovered it is added to the process queue + as a path specification (instance of dfvfs.PathSpec). + + Args: + process_queue: The process queue (instance of Queue). This queue contains + the file entries that need to be processed. + source_path: Path of the source file or directory. + source_path_spec: The source path specification (instance of + dfvfs.PathSpec) as determined by the file system + scanner. The default is None. + resolver_context: Optional resolver context (instance of dfvfs.Context). + The default is None. + """ + super(SingleProcessCollector, self).__init__( + process_queue, source_path, source_path_spec, + resolver_context=resolver_context) + + self._extraction_worker = None + self._fs_collector = SingleProcessFileSystemCollector(process_queue) + + def _FlushQueue(self): + """Flushes the queue callback for the QueueFull exception.""" + while not self._queue.IsEmpty(): + logging.debug(u'Extraction worker started.') + self._extraction_worker.Run() + logging.debug(u'Extraction worker stopped.') + + def SetExtractionWorker(self, extraction_worker): + """Sets the extraction worker. + + Args: + extraction_worker: the extraction worker object (instance of + EventExtractionWorker). + """ + self._extraction_worker = extraction_worker + + self._fs_collector.SetExtractionWorker(extraction_worker) + + +class SingleProcessEngine(engine.BaseEngine): + """Class that defines the single process engine.""" + + def __init__(self, maximum_number_of_queued_items=0): + """Initialize the single process engine object. + + Args: + maximum_number_of_queued_items: The maximum number of queued items. + The default is 0, which represents + no limit. + """ + collection_queue = SingleProcessQueue( + maximum_number_of_queued_items=maximum_number_of_queued_items) + storage_queue = SingleProcessQueue( + maximum_number_of_queued_items=maximum_number_of_queued_items) + parse_error_queue = SingleProcessQueue( + maximum_number_of_queued_items=maximum_number_of_queued_items) + + super(SingleProcessEngine, self).__init__( + collection_queue, storage_queue, parse_error_queue) + + self._event_queue_producer = SingleProcessItemQueueProducer(storage_queue) + self._parse_error_queue_producer = SingleProcessItemQueueProducer( + parse_error_queue) + + def CreateCollector( + self, include_directory_stat, vss_stores=None, filter_find_specs=None, + resolver_context=None): + """Creates a collector object. + + The collector discovers all the files that need to be processed by + the workers. Once a file is discovered it is added to the process queue + as a path specification (instance of dfvfs.PathSpec). + + Args: + include_directory_stat: Boolean value to indicate whether directory + stat information should be collected. + vss_stores: Optional list of VSS stores to include in the collection, + where 1 represents the first store. Set to None if no + VSS stores should be processed. The default is None. + filter_find_specs: Optional list of filter find specifications (instances + of dfvfs.FindSpec). The default is None. + resolver_context: Optional resolver context (instance of dfvfs.Context). + The default is None. Note that every thread or process + must have its own resolver context. + + Returns: + A collector object (instance of Collector). + + Raises: + RuntimeError: if source path specification is not set. + """ + if not self._source_path_spec: + raise RuntimeError(u'Missing source.') + + collector_object = SingleProcessCollector( + self._collection_queue, self._source, self._source_path_spec, + resolver_context=resolver_context) + + collector_object.SetCollectDirectoryMetadata(include_directory_stat) + + if vss_stores: + collector_object.SetVssInformation(vss_stores) + + if filter_find_specs: + collector_object.SetFilter(filter_find_specs) + + return collector_object + + def CreateExtractionWorker(self, worker_number): + """Creates an extraction worker object. + + Args: + worker_number: A number that identifies the worker. + + Returns: + An extraction worker (instance of worker.ExtractionWorker). + """ + parser_context = parsers_context.ParserContext( + self._event_queue_producer, self._parse_error_queue_producer, + self.knowledge_base) + + extraction_worker = SingleProcessEventExtractionWorker( + worker_number, self._collection_queue, self._event_queue_producer, + self._parse_error_queue_producer, parser_context) + + extraction_worker.SetEnableDebugOutput(self._enable_debug_output) + + # TODO: move profiler in separate object. + extraction_worker.SetEnableProfiling( + self._enable_profiling, + profiling_sample_rate=self._profiling_sample_rate) + + if self._open_files: + extraction_worker.SetOpenFiles(self._open_files) + + if self._filter_object: + extraction_worker.SetFilterObject(self._filter_object) + + if self._mount_path: + extraction_worker.SetMountPath(self._mount_path) + + if self._text_prepend: + extraction_worker.SetTextPrepend(self._text_prepend) + + return extraction_worker + + def ProcessSource( + self, collector_object, storage_writer, parser_filter_string=None): + """Processes the source and extracts event objects. + + Args: + collector_object: A collector object (instance of Collector). + storage_writer: A storage writer object (instance of BaseStorageWriter). + parser_filter_string: Optional parser filter string. The default is None. + """ + extraction_worker = self.CreateExtractionWorker(0) + + extraction_worker.InitalizeParserObjects( + parser_filter_string=parser_filter_string) + + # Set the extraction worker and storage writer values so that they + # can be accessed if the QueueFull exception is raised. This is + # needed in single process mode to prevent the queue consuming too + # much memory. + collector_object.SetExtractionWorker(extraction_worker) + self._event_queue_producer.SetStorageWriter(storage_writer) + self._parse_error_queue_producer.SetStorageWriter(storage_writer) + + logging.debug(u'Processing started.') + + logging.debug(u'Collection started.') + collector_object.Collect() + logging.debug(u'Collection stopped.') + + logging.debug(u'Extraction worker started.') + extraction_worker.Run() + logging.debug(u'Extraction worker stopped.') + + self._event_queue_producer.SignalEndOfInput() + + logging.debug(u'Storage writer started.') + storage_writer.WriteEventObjects() + logging.debug(u'Storage writer stopped.') + + # Reset the extraction worker and storage writer values to return + # the objects in their original state. This will prevent access + # to the extraction worker outside this function and allow it + # to be garbage collected. + self._event_queue_producer.SetStorageWriter(None) + self._parse_error_queue_producer.SetStorageWriter(None) + collector_object.SetExtractionWorker(None) + + logging.debug(u'Processing completed.') + + +class SingleProcessEventExtractionWorker(worker.BaseEventExtractionWorker): + """Class that defines the single process event extraction worker.""" + + def _DebugParseFileEntry(self): + """Callback for debugging file entry parsing failures.""" + pdb.post_mortem() + + +class SingleProcessFileSystemCollector(collector.FileSystemCollector): + """Class that implements a single process file system collector object.""" + + def __init__(self, process_queue): + """Initializes the collector object. + + The collector discovers all the files that need to be processed by + the workers. Once a file is discovered it is added to the process queue + as a path specification (instance of dfvfs.PathSpec). + + Args: + process_queue: The process queue (instance of Queue). This queue contains + the file entries that need to be processed. + """ + super(SingleProcessFileSystemCollector, self).__init__(process_queue) + + self._extraction_worker = None + + def _FlushQueue(self): + """Flushes the queue callback for the QueueFull exception.""" + while not self._queue.IsEmpty(): + logging.debug(u'Extraction worker started.') + self._extraction_worker.Run() + logging.debug(u'Extraction worker stopped.') + + def SetExtractionWorker(self, extraction_worker): + """Sets the extraction worker. + + Args: + extraction_worker: the extraction worker object (instance of + EventExtractionWorker). + """ + self._extraction_worker = extraction_worker + + +class SingleProcessItemQueueProducer(queue.ItemQueueProducer): + """Class that implements a single process item queue producer.""" + + def __init__(self, queue_object): + """Initializes the queue producer. + + Args: + queue_object: the queue object (instance of Queue). + """ + super(SingleProcessItemQueueProducer, self).__init__(queue_object) + + self._storage_writer = None + + def _FlushQueue(self): + """Flushes the queue callback for the QueueFull exception.""" + logging.debug(u'Storage writer started.') + self._storage_writer.WriteEventObjects() + logging.debug(u'Storage writer stopped.') + + def SetStorageWriter(self, storage_writer): + """Sets the storage writer. + + Args: + storage_writer: the storage writer object (instance of + BaseStorageWriter). + """ + self._storage_writer = storage_writer + + +class SingleProcessQueue(queue.Queue): + """Single process queue.""" + + def __init__(self, maximum_number_of_queued_items=0): + """Initializes a single process queue object. + + Args: + maximum_number_of_queued_items: The maximum number of queued items. + The default is 0, which represents + no limit. + """ + super(SingleProcessQueue, self).__init__() + + # The Queue interface defines the maximum number of queued items to be + # 0 if unlimited as does the multi processing queue, but deque uses + # None to indicate no limit. + if maximum_number_of_queued_items == 0: + maximum_number_of_queued_items = None + + # maxlen contains the maximum number of items allowed to be queued, + # where None represents unlimited. + self._queue = collections.deque( + maxlen=maximum_number_of_queued_items) + + def __len__(self): + """Returns the estimated current number of items in the queue.""" + return len(self._queue) + + def IsEmpty(self): + """Determines if the queue is empty.""" + return len(self._queue) == 0 + + def PushItem(self, item): + """Pushes an item onto the queue. + + Raises: + QueueFull: when the queue is full. + """ + number_of_items = len(self._queue) + + # Deque will drop the first item in the queue when maxlen is exceeded. + if not self._queue.maxlen or number_of_items < self._queue.maxlen: + self._queue.append(item) + number_of_items += 1 + + if self._queue.maxlen and number_of_items == self._queue.maxlen: + raise errors.QueueFull + + def PopItem(self): + """Pops an item off the queue. + + Raises: + QueueEmpty: when the queue is empty. + """ + try: + # Using popleft to have FIFO behavior. + return self._queue.popleft() + except IndexError: + raise errors.QueueEmpty diff --git a/plaso/engine/single_process_test.py b/plaso/engine/single_process_test.py new file mode 100644 index 0000000..da3f57c --- /dev/null +++ b/plaso/engine/single_process_test.py @@ -0,0 +1,133 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests the single process processing engine.""" + +import os +import unittest + +from dfvfs.lib import definitions as dfvfs_definitions +from dfvfs.helpers import file_system_searcher +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver import context + +from plaso.engine import single_process +from plaso.engine import test_lib +from plaso.lib import errors + + +class SingleProcessQueueTest(unittest.TestCase): + """Tests the single process queue.""" + + _ITEMS = frozenset(['item1', 'item2', 'item3', 'item4']) + + def testPushPopItem(self): + """Tests the PushItem and PopItem functions.""" + test_queue = single_process.SingleProcessQueue() + + for item in self._ITEMS: + test_queue.PushItem(item) + + self.assertEquals(len(test_queue), len(self._ITEMS)) + + test_queue.SignalEndOfInput() + test_queue_consumer = test_lib.TestQueueConsumer(test_queue) + test_queue_consumer.ConsumeItems() + + expected_number_of_items = len(self._ITEMS) + self.assertEquals( + test_queue_consumer.number_of_items, expected_number_of_items) + + def testQueueEmpty(self): + """Tests the queue raises the QueueEmpty exception.""" + test_queue = single_process.SingleProcessQueue() + + with self.assertRaises(errors.QueueEmpty): + test_queue.PopItem() + + def testQueueFull(self): + """Tests the queue raises the QueueFull exception.""" + test_queue = single_process.SingleProcessQueue( + maximum_number_of_queued_items=5) + + for item in self._ITEMS: + test_queue.PushItem(item) + + with self.assertRaises(errors.QueueFull): + test_queue.PushItem('item5') + + with self.assertRaises(errors.QueueFull): + test_queue.PushItem('item6') + + test_queue_consumer = test_lib.TestQueueConsumer(test_queue) + test_queue_consumer.ConsumeItems() + + expected_number_of_items = len(self._ITEMS) + self.assertEquals( + test_queue_consumer.number_of_items, expected_number_of_items + 1) + + +class SingleProcessEngineTest(unittest.TestCase): + """Tests for the engine object.""" + + _TEST_DATA_PATH = os.path.join(os.getcwd(), u'test_data') + + def testEngine(self): + """Test the engine functionality.""" + resolver_context = context.Context() + test_engine = single_process.SingleProcessEngine( + maximum_number_of_queued_items=25000) + + self.assertNotEquals(test_engine, None) + + source_path = os.path.join(self._TEST_DATA_PATH, u'ímynd.dd') + os_path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_OS, location=source_path) + source_path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_TSK, location=u'/', + parent=os_path_spec) + + test_engine.SetSource(source_path_spec, resolver_context=resolver_context) + + self.assertFalse(test_engine.SourceIsDirectory()) + self.assertFalse(test_engine.SourceIsFile()) + self.assertTrue(test_engine.SourceIsStorageMediaImage()) + + test_searcher = test_engine.GetSourceFileSystemSearcher( + resolver_context=resolver_context) + self.assertNotEquals(test_searcher, None) + self.assertIsInstance( + test_searcher, file_system_searcher.FileSystemSearcher) + + test_engine.PreprocessSource('Windows') + + test_collector = test_engine.CreateCollector( + False, vss_stores=None, filter_find_specs=None, + resolver_context=resolver_context) + self.assertNotEquals(test_collector, None) + self.assertIsInstance( + test_collector, single_process.SingleProcessCollector) + + test_extraction_worker = test_engine.CreateExtractionWorker(0) + self.assertNotEquals(test_extraction_worker, None) + self.assertIsInstance( + test_extraction_worker, + single_process.SingleProcessEventExtractionWorker) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/engine/test_lib.py b/plaso/engine/test_lib.py new file mode 100644 index 0000000..26035ea --- /dev/null +++ b/plaso/engine/test_lib.py @@ -0,0 +1,71 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Engine related functions and classes for testing.""" + +import os +import unittest + +from plaso.engine import queue + + +class TestQueueConsumer(queue.ItemQueueConsumer): + """Class that implements the test queue consumer. + + The queue consumer subscribes to updates on the queue. + """ + + def __init__(self, test_queue): + """Initializes the queue consumer. + + Args: + test_queue: the test queue (instance of Queue). + """ + super(TestQueueConsumer, self).__init__(test_queue) + self.items = [] + + def _ConsumeItem(self, item): + """Consumes an item callback for ConsumeItems.""" + self.items.append(item) + + @property + def number_of_items(self): + """The number of items.""" + return len(self.items) + + +class EngineTestCase(unittest.TestCase): + """The unit test case for a front-end.""" + + _TEST_DATA_PATH = os.path.join(os.getcwd(), 'test_data') + + # Show full diff results, part of TestCase so does not follow our naming + # conventions. + maxDiff = None + + def _GetTestFilePath(self, path_segments): + """Retrieves the path of a test file relative to the test data directory. + + Args: + path_segments: the path segments inside the test data directory. + + Returns: + A path of the test file. + """ + # Note that we need to pass the individual path segments to os.path.join + # and not a list. + return os.path.join(self._TEST_DATA_PATH, *path_segments) diff --git a/plaso/engine/utils.py b/plaso/engine/utils.py new file mode 100644 index 0000000..1d60a55 --- /dev/null +++ b/plaso/engine/utils.py @@ -0,0 +1,75 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Engine utility functions.""" + +import logging + +from dfvfs.helpers import file_system_searcher + +from plaso.winreg import path_expander + + +def BuildFindSpecsFromFile(filter_file_path, pre_obj=None): + """Returns a list of find specification from a filter file. + + Args: + filter_file_path: A path to a file that contains find specifications. + pre_obj: A preprocessing object (instance of PreprocessObject). This is + optional but when provided takes care of expanding each segment. + """ + find_specs = [] + + if pre_obj: + expander = path_expander.WinRegistryKeyPathExpander() + + with open(filter_file_path, 'rb') as file_object: + for line in file_object: + line = line.strip() + if line.startswith(u'#'): + continue + + if pre_obj: + try: + line = expander.ExpandPath(line, pre_obj=pre_obj) + except KeyError as exception: + logging.error(( + u'Unable to use collection filter line: {0:s} with error: ' + u'{1:s}').format(line, exception)) + continue + + if not line.startswith(u'/'): + logging.warning(( + u'The filter string must be defined as an abolute path: ' + u'{0:s}').format(line)) + continue + + _, _, file_path = line.rstrip().rpartition(u'/') + if not file_path: + logging.warning( + u'Unable to parse the filter string: {0:s}'.format(line)) + continue + + # Convert the filter paths into a list of path segments and strip + # the root path segment. + path_segments = line.split(u'/') + path_segments.pop(0) + + find_specs.append(file_system_searcher.FindSpec( + location_regex=path_segments, case_sensitive=False)) + + return find_specs diff --git a/plaso/engine/worker.py b/plaso/engine/worker.py new file mode 100644 index 0000000..8eae3a1 --- /dev/null +++ b/plaso/engine/worker.py @@ -0,0 +1,352 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The event extraction worker.""" + +import logging +import os + +from dfvfs.resolver import context +from dfvfs.resolver import resolver as path_spec_resolver + +try: + from guppy import hpy +except ImportError: + hpy = None + +from plaso.engine import classifier +from plaso.engine import queue +from plaso.lib import errors +from plaso.parsers import manager as parsers_manager + + +class BaseEventExtractionWorker(queue.ItemQueueConsumer): + """Class that defines the event extraction worker base. + + This class is designed to watch a queue for path specifications of files + and directories (file entries) for which events need to be extracted. + + The event extraction worker needs to determine if a parser suitable + for parsing a particular file is available. All extracted event objects + are pushed on a storage queue for further processing. + """ + + def __init__( + self, identifier, process_queue, event_queue_producer, + parse_error_queue_producer, parser_context): + """Initializes the event extraction worker object. + + Args: + identifier: The identifier, usually an incrementing integer. + process_queue: The process queue (instance of Queue). This queue contains + the file entries that need to be processed. + event_queue_producer: The event object queue producer (instance of + ItemQueueProducer). + parse_error_queue_producer: The parse error queue producer (instance of + ItemQueueProducer). + parser_context: A parser context object (instance of ParserContext). + """ + super(BaseEventExtractionWorker, self).__init__(process_queue) + self._enable_debug_output = False + self._identifier = identifier + self._open_files = False + self._parser_context = parser_context + self._filestat_parser_object = None + self._parser_objects = None + + # We need a resolver context per process to prevent multi processing + # issues with file objects stored in images. + self._resolver_context = context.Context() + self._event_queue_producer = event_queue_producer + self._parse_error_queue_producer = parse_error_queue_producer + + # Attributes that contain the current status of the worker. + self._current_working_file = u'' + self._is_running = False + + # Attributes for profiling. + self._enable_profiling = False + self._heapy = None + self._profiling_sample = 0 + self._profiling_sample_rate = 1000 + self._profiling_sample_file = u'{0!s}.hpy'.format(self._identifier) + + def _ConsumeItem(self, path_spec): + """Consumes an item callback for ConsumeItems. + + Args: + path_spec: a path specification (instance of dfvfs.PathSpec). + """ + file_entry = path_spec_resolver.Resolver.OpenFileEntry( + path_spec, resolver_context=self._resolver_context) + + if file_entry is None: + logging.warning(u'Unable to open file entry: {0:s}'.format( + path_spec.comparable)) + return + + try: + self.ParseFileEntry(file_entry) + except IOError as exception: + logging.warning(u'Unable to parse file: {0:s} with error: {1:s}'.format( + path_spec.comparable, exception)) + + def _DebugParseFileEntry(self): + """Callback for debugging file entry parsing failures.""" + return + + def _ParseFileEntryWithParser(self, parser_object, file_entry): + """Parses a file entry with a specific parser. + + Args: + parser_object: A parser object (instance of BaseParser). + file_entry: A file entry object (instance of dfvfs.FileEntry). + + Raises: + QueueFull: If a queue is full. + """ + try: + parser_object.Parse(self._parser_context, file_entry) + + except errors.UnableToParseFile as exception: + logging.debug(u'Not a {0:s} file ({1:s}) - {2:s}'.format( + parser_object.NAME, file_entry.name, exception)) + + except errors.QueueFull: + raise + + except IOError as exception: + logging.debug( + u'[{0:s}] Unable to parse: {1:s} with error: {2:s}'.format( + parser_object.NAME, file_entry.path_spec.comparable, + exception)) + + # Casting a wide net, catching all exceptions. Done to keep the worker + # running, despite the parser hitting errors, so the worker doesn't die + # if a single file is corrupted or there is a bug in a parser. + except Exception as exception: + logging.warning( + u'[{0:s}] Unable to process file: {1:s} with error: {2:s}.'.format( + parser_object.NAME, file_entry.path_spec.comparable, + exception)) + logging.debug( + u'The path specification that caused the error: {0:s}'.format( + file_entry.path_spec.comparable)) + logging.exception(exception) + + if self._enable_debug_output: + self._DebugParseFileEntry() + + def _ProfilingStart(self): + """Starts the profiling.""" + self._heapy.setrelheap() + self._profiling_sample = 0 + + try: + os.remove(self._profiling_sample_file) + except OSError: + pass + + def _ProfilingStop(self): + """Stops the profiling.""" + self._ProfilingWriteSample() + + def _ProfilingUpdate(self): + """Updates the profiling.""" + self._profiling_sample += 1 + + if self._profiling_sample >= self._profiling_sample_rate: + self._ProfilingWriteSample() + self._profiling_sample = 0 + + def _ProfilingWriteSample(self): + """Writes a profiling sample to the sample file.""" + heap = self._heapy.heap() + heap.dump(self._profiling_sample_file) + + def GetStatus(self): + """Returns a status dictionary.""" + return { + 'is_running': self._is_running, + 'identifier': u'Worker_{0:d}'.format(self._identifier), + 'current_file': self._current_working_file, + 'counter': self._parser_context.number_of_events} + + def InitalizeParserObjects(self, parser_filter_string=None): + """Initializes the parser objects. + + The parser_filter_string is a simple comma separated value string that + denotes a list of parser names to include and/or exclude. Each entry + can have the value of: + + Exact match of a list of parsers, or a preset (see + plaso/frontend/presets.py for a full list of available presets). + + A name of a single parser (case insensitive), eg. msiecfparser. + + A glob name for a single parser, eg: '*msie*' (case insensitive). + + Args: + parser_filter_string: Optional parser filter string. The default is None. + """ + self._parser_objects = parsers_manager.ParsersManager.GetParserObjects( + parser_filter_string=parser_filter_string) + + for parser_object in self._parser_objects: + if parser_object.NAME == 'filestat': + self._filestat_parser_object = parser_object + break + + def ParseFileEntry(self, file_entry): + """Parses a file entry. + + Args: + file_entry: A file entry object (instance of dfvfs.FileEntry). + """ + logging.debug(u'[ParseFileEntry] Parsing: {0:s}'.format( + file_entry.path_spec.comparable)) + + self._current_working_file = getattr( + file_entry.path_spec, u'location', file_entry.name) + + if file_entry.IsDirectory() and self._filestat_parser_object: + self._ParseFileEntryWithParser(self._filestat_parser_object, file_entry) + + elif file_entry.IsFile(): + # TODO: Not go through all parsers, just the ones + # that the classifier classifies the file as. + + for parser_object in self._parser_objects: + logging.debug(u'Trying to parse: {0:s} with parser: {1:s}'.format( + file_entry.name, parser_object.NAME)) + + self._ParseFileEntryWithParser(parser_object, file_entry) + + logging.debug(u'[ParseFileEntry] Done parsing: {0:s}'.format( + file_entry.path_spec.comparable)) + + if self._enable_profiling: + self._ProfilingUpdate() + + if self._open_files: + try: + for sub_file_entry in classifier.Classifier.SmartOpenFiles(file_entry): + if self._abort: + break + + self.ParseFileEntry(sub_file_entry) + + except IOError as exception: + logging.warning( + u'Unable to parse file: {0:s} with error: {1:s}'.format( + file_entry.path_spec.comparable, exception)) + + def Run(self): + """Extracts event objects from file entries.""" + self._parser_context.ResetCounters() + + if self._enable_profiling: + self._ProfilingStart() + + self._is_running = True + + logging.info( + u'Worker {0:d} (PID: {1:d}) started monitoring process queue.'.format( + self._identifier, os.getpid())) + + self.ConsumeItems() + + logging.info( + u'Worker {0:d} (PID: {1:d}) stopped monitoring process queue.'.format( + self._identifier, os.getpid())) + + self._current_working_file = u'' + + self._is_running = False + + if self._enable_profiling: + self._ProfilingStop() + + self._resolver_context.Empty() + + def SetEnableDebugOutput(self, enable_debug_output): + """Enables or disables debug output. + + Args: + enable_debug_output: boolean value to indicate if the debug output + should be enabled. + """ + self._enable_debug_output = enable_debug_output + + def SetEnableProfiling(self, enable_profiling, profiling_sample_rate=1000): + """Enables or disables profiling. + + Args: + enable_debug_output: boolean value to indicate if the profiling + should be enabled. + profiling_sample_rate: optional integer indicating the profiling sample + rate. The value contains the number of files + processed. The default value is 1000. + """ + if hpy: + self._enable_profiling = enable_profiling + self._profiling_sample_rate = profiling_sample_rate + + if self._enable_profiling and not self._heapy: + self._heapy = hpy() + + def SetFilterObject(self, filter_object): + """Sets the filter object. + + Args: + filter_object: the filter object (instance of objectfilter.Filter). + """ + self._parser_context.SetFilterObject(filter_object) + + def SetMountPath(self, mount_path): + """Sets the mount path. + + Args: + mount_path: string containing the mount path. + """ + self._parser_context.SetMountPath(mount_path) + + # TODO: rename this mode. + def SetOpenFiles(self, open_files): + """Sets the open files mode. + + Args: + open_files: boolean value to indicate if the worker should scan for + file entries inside files. + """ + self._open_files = open_files + + def SetTextPrepend(self, text_prepend): + """Sets the text prepend. + + Args: + text_prepend: string that contains the text to prepend to every + event object. + """ + self._parser_context.SetTextPrepend(text_prepend) + + def SignalAbort(self): + """Signals the worker to abort.""" + super(BaseEventExtractionWorker, self).SignalAbort() + self._parser_context.SignalAbort() + + @classmethod + def SupportsProfiling(cls): + """Returns a boolean value to indicate if profiling is supported.""" + return hpy is not None diff --git a/plaso/events/__init__.py b/plaso/events/__init__.py new file mode 100644 index 0000000..f4a69a4 --- /dev/null +++ b/plaso/events/__init__.py @@ -0,0 +1,17 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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/plaso/events/plist_event.py b/plaso/events/plist_event.py new file mode 100644 index 0000000..3641f3a --- /dev/null +++ b/plaso/events/plist_event.py @@ -0,0 +1,92 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file is the template for Plist events.""" + +from plaso.events import time_events +from plaso.lib import eventdata + + +class PlistEvent(time_events.PythonDatetimeEvent): + """Convenience class for a plist events.""" + + DATA_TYPE = 'plist:key' + + def __init__(self, root, key, timestamp, desc=None, host=None, user=None): + """Template for creating a Plist EventObject for returning data to Plaso. + + All events extracted from files get passed around Plaso internally as an + EventObject. PlistEvent is an EventObject with attributes specifically + relevant to data extracted from a Plist file. The attribute DATA_TYPE + 'plist:key' allows the formatter used during output to identify + the appropriate formatter for converting these attributes to output. + + Args: + root: A string representing the path from the root to this key. + key: A string representing the name of key. + timestamp: The date object (instance of datetime.datetime). + desc: An optional string intended for the user describing the event. + host: An optional host name if one is available within the log file. + user: An optional user name if one is available within the log file. + """ + super(PlistEvent, self).__init__( + timestamp, eventdata.EventTimestamp.WRITTEN_TIME) + + self.root = root + self.key = key + if desc: + self.desc = desc + if host: + self.hostname = host + if user: + self.username = user + + +class PlistTimeEvent(time_events.TimestampEvent): + """Convenience class for a plist event that does not use datetime objects.""" + + DATA_TYPE = 'plist:key' + + def __init__(self, root, key, timestamp, desc=None, host=None, user=None): + """Template for creating a Plist EventObject for returning data to Plaso. + + All events extracted from files get passed around Plaso internally as an + EventObject. PlistEvent is an EventObject with attributes specifically + relevant to data extracted from a Plist file. The attribute DATA_TYPE + 'plist:key' allows the formatter used during output to identify + the appropriate formatter for converting these attributes to output. + + Args: + root: A string representing the path from the root to this key. + key: A string representing the name of key. + timestamp: The timestamp time value. The timestamp contains the + number of microseconds since Jan 1, 1970 00:00:00 UTC. + desc: An optional string intended for the user describing the event. + host: An optional host name if one is available within the log file. + user: An optional user name if one is available within the log file. + """ + super(PlistTimeEvent, self).__init__( + timestamp, eventdata.EventTimestamp.WRITTEN_TIME) + + self.root = root + self.key = key + if desc: + self.desc = desc + if host: + self.hostname = host + if user: + self.username = user diff --git a/plaso/events/shell_item_events.py b/plaso/events/shell_item_events.py new file mode 100644 index 0000000..82de6b3 --- /dev/null +++ b/plaso/events/shell_item_events.py @@ -0,0 +1,50 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the shell item specific event object classes.""" + +from plaso.events import time_events + + +class ShellItemFileEntryEvent(time_events.FatDateTimeEvent): + """Convenience class for a shell item file entry event.""" + + DATA_TYPE = 'windows:shell_item:file_entry' + + def __init__( + self, fat_date_time, usage, name, long_name, localized_name, + file_reference, origin): + """Initializes an event object. + + Args: + fat_date_time: The FAT date time value. + usage: The description of the usage of the time value. + name: A string containing the name of the file entry shell item. + long_name: A string containing the long name of the file entry shell item. + localized_name: A string containing the localized name of the file entry + shell item. + file_reference: A string containing the NTFS file reference + (MTF entry - sequence number). + origin: A string containing the origin of the event (event source). + """ + super(ShellItemFileEntryEvent, self).__init__(fat_date_time, usage) + + self.name = name + self.long_name = long_name + self.localized_name = localized_name + self.file_reference = file_reference + self.origin = origin diff --git a/plaso/events/text_events.py b/plaso/events/text_events.py new file mode 100644 index 0000000..f40b7eb --- /dev/null +++ b/plaso/events/text_events.py @@ -0,0 +1,48 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the text format specific event object classes.""" + +from plaso.events import time_events +from plaso.lib import eventdata + + +class TextEvent(time_events.TimestampEvent): + """Convenience class for a text format-based event.""" + + DATA_TYPE = 'text:entry' + + def __init__(self, timestamp, offset, attributes): + """Initializes a text event object. + + Args: + timestamp: The timestamp time value. The timestamp contains the + number of microseconds since Jan 1, 1970 00:00:00 UTC. + offset: The offset of the attributes. + attributes: A dict that contains the events attributes. + """ + super(TextEvent, self).__init__( + timestamp, eventdata.EventTimestamp.WRITTEN_TIME) + + self.offset = offset + + for name, value in attributes.iteritems(): + # TODO: Revisit this constraints and see if we can implement + # it using a more sane solution. + if isinstance(value, basestring) and not value: + continue + setattr(self, name, value) diff --git a/plaso/events/time_events.py b/plaso/events/time_events.py new file mode 100644 index 0000000..dddb39c --- /dev/null +++ b/plaso/events/time_events.py @@ -0,0 +1,157 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the time-based event object classes.""" + +from plaso.lib import event +from plaso.lib import timelib + + +class TimestampEvent(event.EventObject): + """Convenience class for a timestamp-based event.""" + + def __init__(self, timestamp, usage, data_type=None): + """Initializes an event object. + + Args: + timestamp: The timestamp value. + usage: The description of the usage of the time value. + data_type: Optional event data type. If not set data_type is + derived from the DATA_TYPE attribute. + """ + super(TimestampEvent, self).__init__() + self.timestamp = timestamp + self.timestamp_desc = usage + + if data_type: + self.data_type = data_type + + +class CocoaTimeEvent(TimestampEvent): + """Convenience class for a Cocoa time-based event.""" + + def __init__(self, cocoa_time, usage, data_type=None): + """Initializes an event object. + + Args: + cocoa_time: The Cocoa time value. + usage: The description of the usage of the time value. + data_type: Optional event data type. If not set data_type is + derived from the DATA_TYPE attribute. + """ + super(CocoaTimeEvent, self).__init__( + timelib.Timestamp.FromCocoaTime(cocoa_time), usage, + data_type=data_type) + + +class FatDateTimeEvent(TimestampEvent): + """Convenience class for a FAT date time-based event.""" + + def __init__(self, fat_date_time, usage, data_type=None): + """Initializes an event object. + + Args: + fat_date_time: The FAT date time value. + usage: The description of the usage of the time value. + data_type: Optional event data type. If not set data_type is + derived from the DATA_TYPE attribute. + """ + super(FatDateTimeEvent, self).__init__( + timelib.Timestamp.FromFatDateTime(fat_date_time), usage, + data_type=data_type) + + +class FiletimeEvent(TimestampEvent): + """Convenience class for a FILETIME timestamp-based event.""" + + def __init__(self, filetime, usage, data_type=None): + """Initializes an event object. + + Args: + filetime: The FILETIME timestamp value. + usage: The description of the usage of the time value. + data_type: Optional event data type. If not set data_type is + derived from the DATA_TYPE attribute. + """ + super(FiletimeEvent, self).__init__( + timelib.Timestamp.FromFiletime(filetime), usage, data_type=data_type) + + +class JavaTimeEvent(TimestampEvent): + """Convenience class for a Java time-based event.""" + + def __init__(self, java_time, usage, data_type=None): + """Initializes an event object. + + Args: + java_time: The Java time value. + usage: The description of the usage of the time value. + data_type: Optional event data type. If not set data_type is + derived from the DATA_TYPE attribute. + """ + super(JavaTimeEvent, self).__init__( + timelib.Timestamp.FromJavaTime(java_time), usage, data_type=data_type) + + +class PosixTimeEvent(TimestampEvent): + """Convenience class for a POSIX time-based event.""" + + def __init__(self, posix_time, usage, data_type=None): + """Initializes an event object. + + Args: + posix_time: The POSIX time value. + usage: The description of the usage of the time value. + data_type: Optional event data type. If not set data_type is + derived from the DATA_TYPE attribute. + """ + super(PosixTimeEvent, self).__init__( + timelib.Timestamp.FromPosixTime(posix_time), usage, data_type=data_type) + + +class PythonDatetimeEvent(TimestampEvent): + """Convenience class for a Python DateTime time-based event.""" + + def __init__(self, datetime_time, usage, data_type=None): + """Initializes an event object. + + Args: + datetime_time: The datetime object (instance of datetime.datetime). + usage: The description of the usage of the time value. + data_type: Optional event data type. If not set data_type is + derived from the DATA_TYPE attribute. + """ + super(PythonDatetimeEvent, self).__init__( + timelib.Timestamp.FromPythonDatetime(datetime_time), usage, + data_type=data_type) + + +class WebKitTimeEvent(TimestampEvent): + """Convenience class for a WebKit time-based event.""" + + def __init__(self, webkit_time, usage, data_type=None): + """Initializes an event object. + + Args: + webkit_time: The WebKit time value. + usage: The description of the usage of the time value. + data_type: Optional event data type. If not set data_type is + derived from the DATA_TYPE attribute. + """ + super(WebKitTimeEvent, self).__init__( + timelib.Timestamp.FromWebKitTime(webkit_time), usage, + data_type=data_type) diff --git a/plaso/events/windows_events.py b/plaso/events/windows_events.py new file mode 100644 index 0000000..77cc397 --- /dev/null +++ b/plaso/events/windows_events.py @@ -0,0 +1,95 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the Windows specific event object classes.""" + +from plaso.events import time_events +from plaso.lib import eventdata + + +class WindowsVolumeCreationEvent(time_events.FiletimeEvent): + """Convenience class for a Windows volume creation event.""" + + DATA_TYPE = 'windows:volume:creation' + + def __init__(self, filetime, device_path, serial_number, origin): + """Initializes an event object. + + Args: + filetime: The FILETIME timestamp value. + device_path: A string containing the volume device path. + serial_number: A string containing the volume serial number. + origin: A string containing the origin of the event (event source). + """ + super(WindowsVolumeCreationEvent, self).__init__( + filetime, eventdata.EventTimestamp.CREATION_TIME) + + self.device_path = device_path + self.serial_number = serial_number + self.origin = origin + + +class WindowsRegistryEvent(time_events.TimestampEvent): + """Convenience class for a Windows Registry-based event.""" + + DATA_TYPE = 'windows:registry:key_value' + + def __init__( + self, timestamp, key_name, value_dict, usage=None, offset=None, + registry_type=None, urls=None, source_append=None): + """Initializes a Windows registry event. + + Args: + timestamp: The timestamp time value. The timestamp contains the + number of microseconds since Jan 1, 1970 00:00:00 UTC. + key_name: The name of the Registry key being parsed. + value_dict: The interpreted value of the key, stored as a dictionary. + usage: Optional description of the usage of the time value. + The default is None. + offset: Optional (data) offset of the Registry key or value. + The default is None. + registry_type: Optional Registry type string. The default is None. + urls: Optional list of URLs. The default is None. + source_append: Optional string to append to the source_long of the event. + The default is None. + """ + if usage is None: + usage = eventdata.EventTimestamp.WRITTEN_TIME + + super(WindowsRegistryEvent, self).__init__(timestamp, usage) + + if key_name: + self.keyname = key_name + + self.regvalue = value_dict + + if offset or type(offset) in [int, long]: + self.offset = offset + + if registry_type: + self.registry_type = registry_type + + if urls: + self.url = u' - '.join(urls) + + if source_append: + self.source_append = source_append + + +class WindowsRegistryServiceEvent(WindowsRegistryEvent): + """Convenience class for service entries retrieved from the registry.""" + DATA_TYPE = 'windows:registry:service' diff --git a/plaso/filters/__init__.py b/plaso/filters/__init__.py new file mode 100644 index 0000000..e2e5b40 --- /dev/null +++ b/plaso/filters/__init__.py @@ -0,0 +1,56 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains an import statement for each filter.""" +import logging + +from plaso.filters import dynamic_filter +from plaso.filters import eventfilter +from plaso.filters import filterlist + +from plaso.lib import filter_interface +from plaso.lib import errors + + +def ListFilters(): + """Generate a list of all available filters.""" + filters = [] + for cl in filter_interface.FilterObject.classes: + filters.append(filter_interface.FilterObject.classes[cl]()) + + return filters + + +def GetFilter(filter_string): + """Returns the first filter that matches the filter string. + + Args: + filter_string: A filter string for any of the available filters. + + Returns: + The first FilterObject found matching the filter string. If no FilterObject + is available for this filter string None is returned. + """ + if not filter_string: + return + + for filter_obj in ListFilters(): + try: + filter_obj.CompileFilter(filter_string) + return filter_obj + except errors.WrongPlugin: + logging.debug(u'Filterstring [{}] is not a filter: {}'.format( + filter_string, filter_obj.filter_name)) diff --git a/plaso/filters/dynamic_filter.py b/plaso/filters/dynamic_filter.py new file mode 100644 index 0000000..972aa2e --- /dev/null +++ b/plaso/filters/dynamic_filter.py @@ -0,0 +1,162 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains definition for a selective fields EventObjectFilter.""" +from plaso.lib import errors +from plaso.lib import lexer +from plaso.filters import eventfilter + + +class SelectiveLexer(lexer.Lexer): + """A simple selective filter lexer implementation.""" + + tokens = [ + lexer.Token('INITIAL', r'SELECT', '', 'FIELDS'), + lexer.Token('FIELDS', r'(.+) WHERE ', 'SetFields', 'FILTER'), + lexer.Token('FIELDS', r'(.+) LIMIT', 'SetFields', 'LIMIT_END'), + lexer.Token('FIELDS', r'(.+) SEPARATED BY', 'SetFields', 'SEPARATE'), + lexer.Token('FIELDS', r'(.+)$', 'SetFields', 'END'), + lexer.Token('FILTER', r'(.+) SEPARATED BY', 'SetFilter', 'SEPARATE'), + lexer.Token('FILTER', r'(.+) LIMIT', 'SetFilter', 'LIMIT_END'), + lexer.Token('FILTER', r'(.+)$', 'SetFilter', 'END'), + lexer.Token('SEPARATE', r' ', '', ''), # Ignore white space here. + lexer.Token('SEPARATE', r'LIMIT', '', 'LIMIT_END'), + lexer.Token( + 'SEPARATE', r'[\'"]([^ \'"]+)[\'"] LIMIT', 'SetSeparator', + 'LIMIT_END'), + lexer.Token( + 'SEPARATE', r'[\'"]([^ \'"]+)[\'"]$', 'SetSeparator', 'END'), + lexer.Token( + 'SEPARATE', r'(.+)$', 'SetSeparator', 'END'), + lexer.Token( + 'LIMIT_END', r'SEPARATED BY [\'"]([^\'"]+)[\'"]', 'SetSeparator', ''), + lexer.Token('LIMIT_END', r'(.+) SEPARATED BY', 'SetLimit', 'SEPARATE'), + lexer.Token('LIMIT_END', r'(.+)$', 'SetLimit', 'END')] + + def __init__(self, data=''): + """Initialize the lexer.""" + self.fields = [] + self.limit = 0 + self.lex_filter = None + self.separator = u',' + super(SelectiveLexer, self).__init__(data) + + def SetFilter(self, match, **_): + """Set the filter query.""" + filter_match = match.group(1) + if 'LIMIT' in filter_match: + # This only occurs in the case where we have "LIMIT X SEPARATED BY". + self.lex_filter, _, push_back = filter_match.rpartition('LIMIT') + self.PushBack('LIMIT {} SEPARATED BY '.format(push_back)) + else: + self.lex_filter = filter_match + + def SetSeparator(self, match, **_): + """Set the separator of the output, only uses the first char.""" + separator = match.group(1) + if separator: + self.separator = separator[0] + + def SetLimit(self, match, **_): + """Set the row limit.""" + try: + limit = int(match.group(1)) + except ValueError: + self.Error('Invalid limit value, should be int [{}] = {}'.format( + type(match.group(1)), match.group(1))) + limit = 0 + + self.limit = limit + + def SetFields(self, match, **_): + """Set the selective fields.""" + text = match.group(1).lower() + field_text, _, _ = text.partition(' from ') + + use_field_text = field_text.replace(' ', '') + if ',' in use_field_text: + self.fields = use_field_text.split(',') + else: + self.fields = [use_field_text] + + +class DynamicFilter(eventfilter.EventObjectFilter): + """A twist to the EventObjectFilter allowing output fields to be selected. + + This filter is essentially the same as the EventObjectFilter except it wraps + it in a selection of which fields should be included by an output module that + has support for selective fields. That is to say the filter: + + SELECT field_a, field_b WHERE attribute contains 'text' + + Will use the EventObjectFilter "attribute contains 'text'" and at the same + time indicate to the appropriate output module that the user wants only the + fields field_a and field_b to be used in the output. + """ + + @property + def fields(self): + """Set the fields property.""" + return self._fields + + @property + def limit(self): + """Return the limit of row counts.""" + return self._limit + + @property + def separator(self): + """Return the separator value.""" + return self._separator + + def __init__(self): + """Initialize the selective EventObjectFilter.""" + super(DynamicFilter, self).__init__() + self._fields = [] + self._limit = 0 + self._separator = u',' + + def CompileFilter(self, filter_string): + """Compile the filter string into a EventObjectFilter matcher.""" + lex = SelectiveLexer(filter_string) + + _ = lex.NextToken() + if lex.error: + raise errors.WrongPlugin('Malformed filter string.') + + _ = lex.NextToken() + if lex.error: + raise errors.WrongPlugin('No fields defined.') + + if lex.state is not 'END': + while lex.state is not 'END': + _ = lex.NextToken() + if lex.error: + raise errors.WrongPlugin('No filter defined for DynamicFilter.') + + if lex.state != 'END': + raise errors.WrongPlugin( + 'Malformed DynamicFilter, end state not reached.') + + self._fields = lex.fields + self._limit = lex.limit + self._separator = unicode(lex.separator) + + if lex.lex_filter: + super(DynamicFilter, self).CompileFilter(lex.lex_filter) + else: + self.matcher = None + diff --git a/plaso/filters/dynamic_filter_test.py b/plaso/filters/dynamic_filter_test.py new file mode 100644 index 0000000..d4c722e --- /dev/null +++ b/plaso/filters/dynamic_filter_test.py @@ -0,0 +1,85 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the DynamicFilter filter.""" +import unittest + +from plaso.filters import dynamic_filter +from plaso.filters import test_helper + + +class DynamicFilterTest(test_helper.FilterTestHelper): + """Tests for the DynamicFilter filter.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self.test_filter = dynamic_filter.DynamicFilter() + + def testFilterFail(self): + """Run few tests that should not be a proper filter.""" + self.TestFail('/tmp/file_that_most_likely_does_not_exist') + self.TestFail('some random stuff that is destined to fail') + self.TestFail('some_stuff is "random" and other_stuff ') + self.TestFail('some_stuff is "random" and other_stuff is not "random"') + self.TestFail('SELECT stuff FROM machine WHERE conditions are met') + self.TestFail('SELECT field_a, field_b WHERE ') + self.TestFail('SELECT field_a, field_b SEPARATED BY') + self.TestFail('SELECT field_a, SEPARATED BY field_b WHERE ') + self.TestFail('SELECT field_a, field_b LIMIT WHERE') + + def testFilterApprove(self): + self.TestTrue('SELECT stuff FROM machine WHERE some_stuff is "random"') + self.TestTrue('SELECT field_a, field_b, field_c') + self.TestTrue('SELECT field_a, field_b, field_c SEPARATED BY "%"') + self.TestTrue('SELECT field_a, field_b, field_c LIMIT 10') + self.TestTrue('SELECT field_a, field_b, field_c LIMIT 10 SEPARATED BY "|"') + self.TestTrue('SELECT field_a, field_b, field_c SEPARATED BY "|" LIMIT 10') + self.TestTrue('SELECT field_a, field_b, field_c WHERE date > "2012"') + self.TestTrue( + 'SELECT field_a, field_b, field_c WHERE date > "2012" LIMIT 100') + self.TestTrue(( + 'SELECT field_a, field_b, field_c WHERE date > "2012" SEPARATED BY "@"' + ' LIMIT 100')) + self.TestTrue(( + 'SELECT parser, date, time WHERE some_stuff is "random" and ' + 'date < "2021-02-14 14:51:23"')) + + def testFilterFields(self): + query = 'SELECT stuff FROM machine WHERE some_stuff is "random"' + self.test_filter.CompileFilter(query) + self.assertEquals(['stuff'], self.test_filter.fields) + + query = 'SELECT stuff, a, b, date FROM machine WHERE some_stuff is "random"' + self.test_filter.CompileFilter(query) + self.assertEquals(['stuff', 'a', 'b', 'date'], self.test_filter.fields) + + query = 'SELECT date, message, zone, hostname WHERE some_stuff is "random"' + self.test_filter.CompileFilter(query) + self.assertEquals(['date', 'message', 'zone', 'hostname'], + self.test_filter.fields) + + query = 'SELECT hlutir' + self.test_filter.CompileFilter(query) + self.assertEquals(['hlutir'], self.test_filter.fields) + + query = 'SELECT hlutir LIMIT 10' + self.test_filter.CompileFilter(query) + self.assertEquals(['hlutir'], self.test_filter.fields) + self.assertEquals(10, self.test_filter.limit) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/filters/eventfilter.py b/plaso/filters/eventfilter.py new file mode 100644 index 0000000..6a4e760 --- /dev/null +++ b/plaso/filters/eventfilter.py @@ -0,0 +1,40 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains definition for a simple filter.""" +from plaso.lib import errors +from plaso.lib import filter_interface +from plaso.lib import pfilter + + +class EventObjectFilter(filter_interface.FilterObject): + """A simple filter using the objectfilter library.""" + + def CompileFilter(self, filter_string): + """Compile the filter string into a filter matcher.""" + self.matcher = pfilter.GetMatcher(filter_string, True) + if not self.matcher: + raise errors.WrongPlugin('Malformed filter string.') + + def Match(self, event_object): + """Evaluate an EventObject against a filter.""" + if not self.matcher: + return True + + self._decision = self.matcher.Matches(event_object) + + return self._decision + diff --git a/plaso/filters/eventfilter_test.py b/plaso/filters/eventfilter_test.py new file mode 100644 index 0000000..4740f2f --- /dev/null +++ b/plaso/filters/eventfilter_test.py @@ -0,0 +1,43 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the EventObjectFilter filter.""" +import unittest + +from plaso.filters import test_helper +from plaso.filters import eventfilter + + +class EventObjectFilterTest(test_helper.FilterTestHelper): + """Tests for the EventObjectFilter filter.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self.test_filter = eventfilter.EventObjectFilter() + + def testFilterFail(self): + """Run few tests that should not be a proper filter.""" + self.TestFail('SELECT stuff FROM machine WHERE conditions are met') + self.TestFail('/tmp/file_that_most_likely_does_not_exist') + self.TestFail('some random stuff that is destined to fail') + self.TestFail('some_stuff is "random" and other_stuff ') + + def testFilterApprove(self): + self.TestTrue('some_stuff is "random" and other_stuff is not "random"') + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/filters/filterlist.py b/plaso/filters/filterlist.py new file mode 100644 index 0000000..0b933f4 --- /dev/null +++ b/plaso/filters/filterlist.py @@ -0,0 +1,109 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains definition for a list of ObjectFilter.""" +import os +import yaml +import logging + +from plaso.lib import errors +from plaso.lib import filter_interface +from plaso.lib import pfilter + + +def IncludeKeyword(loader, node): + """A constructor for the include keyword in YAML.""" + filename = loader.construct_scalar(node) + if os.path.isfile(filename): + with open(filename, 'rb') as fh: + try: + data = yaml.safe_load(fh) + except yaml.ParserError as exception: + logging.error(u'Unable to load rule file with error: {0:s}'.format( + exception)) + return None + return data + + +class ObjectFilterList(filter_interface.FilterObject): + """A series of Pfilter filters along with metadata.""" + + def CompileFilter(self, filter_string): + """Compile a set of ObjectFilters defined in an YAML file.""" + if not os.path.isfile(filter_string): + raise errors.WrongPlugin(( + 'ObjectFilterList requires an YAML file to be passed on, this filter ' + 'string is not a file.')) + + yaml.add_constructor('!include', IncludeKeyword, + Loader=yaml.loader.SafeLoader) + results = None + + with open(filter_string, 'rb') as fh: + try: + results = yaml.safe_load(fh) + except (yaml.scanner.ScannerError, IOError) as exception: + raise errors.WrongPlugin( + u'Unable to parse YAML file with error: {0:s}.'.format(exception)) + + self.filters = [] + if type(results) is dict: + self._ParseEntry(results) + elif type(results) is list: + for result in results: + if type(result) is not dict: + raise errors.WrongPlugin( + u'Wrong format of YAML file, entry not a dict ({})'.format( + type(result))) + self._ParseEntry(result) + else: + raise errors.WrongPlugin( + u'Wrong format of YAML file, entry not a dict ({})'.format( + type(result))) + + def _ParseEntry(self, entry): + """Parse a single YAML filter entry.""" + # A single file with a list of filters to parse. + for name, meta in entry.items(): + if 'filter' not in meta: + raise errors.WrongPlugin( + u'Entry inside {} does not contain a filter statement.'.format( + name)) + + matcher = pfilter.GetMatcher(meta.get('filter'), True) + if not matcher: + raise errors.WrongPlugin( + u'Filter entry [{0:s}] malformed for rule: <{1:s}>'.format( + meta.get('filter'), name)) + + self.filters.append((name, matcher, meta)) + + def Match(self, event_object): + """Evaluate an EventObject against a pfilter.""" + if not self.filters: + return True + + for name, matcher, meta in self.filters: + self._decision = matcher.Matches(event_object) + if self._decision: + self._reason = u'[{}] {} {}'.format( + name, meta.get('description', 'N/A'), u' - '.join( + meta.get('urls', []))) + return True + + return False + + diff --git a/plaso/filters/filterlist_test.py b/plaso/filters/filterlist_test.py new file mode 100644 index 0000000..773d60e --- /dev/null +++ b/plaso/filters/filterlist_test.py @@ -0,0 +1,98 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the PFilters filter.""" + +import os +import logging +import tempfile +import unittest + +from plaso.filters import filterlist +from plaso.filters import test_helper + + +class ObjectFilterTest(test_helper.FilterTestHelper): + """Tests for the ObjectFilterList filter.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self.test_filter = filterlist.ObjectFilterList() + + def testFilterFail(self): + """Run few tests that should not be a proper filter.""" + self.TestFail('SELECT stuff FROM machine WHERE conditions are met') + self.TestFail('/tmp/file_that_most_likely_does_not_exist') + self.TestFail('some random stuff that is destined to fail') + self.TestFail('some_stuff is "random" and other_stuff ') + self.TestFail('some_stuff is "random" and other_stuff is not "random"') + + def CreateFileAndTest(self, content): + """Creates a file and then runs the test.""" + name = '' + with tempfile.NamedTemporaryFile(delete=False) as file_object: + name = file_object.name + file_object.write(content) + + self.TestTrue(name) + + try: + os.remove(name) + except (OSError, IOError) as exception: + logging.warning( + u'Unable to remove temporary file: {0:s} with error: {1:s}'.format( + name, exception)) + + def testFilterApprove(self): + one_rule = u'\n'.join([ + u'Again_Dude:', + u' description: Heavy artillery caught on fire', + u' case_nr: 62345', + u' analysts: [anonymous]', + u' urls: [cnn.com,microsoft.com]', + u' filter: message contains "dude where is my car"']) + + self.CreateFileAndTest(one_rule) + + collection = u'\n'.join([ + u'Rule_Dude:', + u' description: This is the very case I talk about, a lot', + u' case_nr: 1235', + u' analysts: [dude, jack, horn]', + u' urls: [mbl.is,visir.is]', + (u' filter: date > "2012-01-01 10:54:13" and parser not contains ' + u'"evtx"'), + u'', + u'Again_Dude:', + u' description: Heavy artillery caught on fire', + u' case_nr: 62345', + u' analysts: [smith, perry, john]', + u' urls: [cnn.com,microsoft.com]', + u' filter: message contains "dude where is my car"', + u'', + u'Third_Rule_Of_Thumb:', + u' description: Another ticket for another day.', + u' case_nr: 234', + u' analysts: [joe]', + u' urls: [mbl.is,symantec.com/whereevillies,virustotal.com/myhash]', + u' filter: evil_bit is 1']) + + self.CreateFileAndTest(collection) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/filters/test_helper.py b/plaso/filters/test_helper.py new file mode 100644 index 0000000..c68e9cd --- /dev/null +++ b/plaso/filters/test_helper.py @@ -0,0 +1,50 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains helper function and classes for filters.""" +import unittest + +from plaso.lib import errors + + +class FilterTestHelper(unittest.TestCase): + """A simple class that provides helper functions for testing filters.""" + + def setUp(self): + """This should be overwritten.""" + self.test_filter = None + + def TestTrue(self, query): + """A quick test that should return a valid filter.""" + if not self.test_filter: + self.assertTrue(False) + + try: + self.test_filter.CompileFilter(query) + # And a success. + self.assertTrue(True) + except errors.WrongPlugin: + # Let the test fail. + self.assertTrue(False) + + def TestFail(self, query): + """A quick failure test with a filter.""" + if not self.test_filter: + self.assertTrue(False) + + with self.assertRaises(errors.WrongPlugin): + self.test_filter.CompileFilter(query) + diff --git a/plaso/formatters/__init__.py b/plaso/formatters/__init__.py new file mode 100644 index 0000000..588e3f1 --- /dev/null +++ b/plaso/formatters/__init__.py @@ -0,0 +1,86 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains an import statement for each formatter.""" + +from plaso.formatters import android_app_usage +from plaso.formatters import android_calls +from plaso.formatters import android_sms +from plaso.formatters import appcompatcache +from plaso.formatters import appusage +from plaso.formatters import asl +from plaso.formatters import bencode_parser +from plaso.formatters import bsm +from plaso.formatters import chrome +from plaso.formatters import chrome_cache +from plaso.formatters import chrome_cookies +from plaso.formatters import chrome_extension_activity +from plaso.formatters import cups_ipp +from plaso.formatters import filestat +from plaso.formatters import firefox +from plaso.formatters import firefox_cache +from plaso.formatters import firefox_cookies +from plaso.formatters import ganalytics +from plaso.formatters import gdrive +from plaso.formatters import hachoir +from plaso.formatters import iis +from plaso.formatters import ipod +from plaso.formatters import java_idx +from plaso.formatters import ls_quarantine +from plaso.formatters import mac_appfirewall +from plaso.formatters import mac_document_versions +from plaso.formatters import mac_keychain +from plaso.formatters import mac_securityd +from plaso.formatters import mac_wifi +from plaso.formatters import mackeeper_cache +from plaso.formatters import mactime +from plaso.formatters import mcafeeav +from plaso.formatters import msie_webcache +from plaso.formatters import msiecf +from plaso.formatters import olecf +from plaso.formatters import opera +from plaso.formatters import oxml +from plaso.formatters import pcap +from plaso.formatters import plist +from plaso.formatters import popcontest +from plaso.formatters import pls_recall +from plaso.formatters import recycler +from plaso.formatters import rubanetra +from plaso.formatters import safari +from plaso.formatters import selinux +from plaso.formatters import shell_items +from plaso.formatters import skydrivelog +from plaso.formatters import skydrivelogerr +from plaso.formatters import skype +from plaso.formatters import symantec +from plaso.formatters import syslog +from plaso.formatters import task_scheduler +from plaso.formatters import text +from plaso.formatters import utmp +from plaso.formatters import utmpx +from plaso.formatters import windows +from plaso.formatters import winevt +from plaso.formatters import winevtx +from plaso.formatters import winfirewall +from plaso.formatters import winjob +from plaso.formatters import winlnk +from plaso.formatters import winprefetch +from plaso.formatters import winreg +from plaso.formatters import winregservice +from plaso.formatters import xchatlog +from plaso.formatters import xchatscrollback +from plaso.formatters import zeitgeist diff --git a/plaso/formatters/android_app_usage.py b/plaso/formatters/android_app_usage.py new file mode 100644 index 0000000..14fd6a1 --- /dev/null +++ b/plaso/formatters/android_app_usage.py @@ -0,0 +1,33 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for Android Application Usage.""" + +from plaso.formatters import interface + + +class AndroidApplicationFormatter(interface.ConditionalEventFormatter): + """Formatter for an Application Last Resumed event.""" + + DATA_TYPE = 'android:event:last_resume_time' + + FORMAT_STRING_PIECES = [ + u'Package: {package}', + u'Component: {component}'] + + SOURCE_LONG = 'Android App Usage' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/android_calls.py b/plaso/formatters/android_calls.py new file mode 100644 index 0000000..fefdbf0 --- /dev/null +++ b/plaso/formatters/android_calls.py @@ -0,0 +1,37 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for Android contacts2.db database events.""" + +from plaso.formatters import interface + + +class AndroidCallFormatter(interface.ConditionalEventFormatter): + """Formatter for Android call history events.""" + + DATA_TYPE = 'android:event:call' + + FORMAT_STRING_PIECES = [ + u'{call_type}', + u'Number: {number}', + u'Name: {name}', + u'Duration: {duration} seconds'] + + FORMAT_STRING_SHORT_PIECES = [u'{call_type} Call'] + + SOURCE_LONG = 'Android Call History' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/android_sms.py b/plaso/formatters/android_sms.py new file mode 100644 index 0000000..729bd5d --- /dev/null +++ b/plaso/formatters/android_sms.py @@ -0,0 +1,37 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for Android mmssms.db database events.""" + +from plaso.formatters import interface + + +class AndroidSmsFormatter(interface.ConditionalEventFormatter): + """Formatter for Android sms events.""" + + DATA_TYPE = 'android:messaging:sms' + + FORMAT_STRING_PIECES = [ + u'Type: {sms_type}', + u'Address: {address}', + u'Status: {sms_read}', + u'Message: {body}'] + + FORMAT_STRING_SHORT_PIECES = [u'{body}'] + + SOURCE_LONG = 'Android SMS messages' + SOURCE_SHORT = 'SMS' diff --git a/plaso/formatters/appcompatcache.py b/plaso/formatters/appcompatcache.py new file mode 100644 index 0000000..9bcde57 --- /dev/null +++ b/plaso/formatters/appcompatcache.py @@ -0,0 +1,36 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for the AppCompatCache entries inside the Windows Registry.""" + +from plaso.formatters import interface + + +class AppCompatCacheFormatter(interface.ConditionalEventFormatter): + """Formatter for an AppCompatCache Windows Registry entry.""" + + DATA_TYPE = 'windows:registry:appcompatcache' + + FORMAT_STRING_PIECES = [ + u'[{keyname}]', + u'Cached entry: {entry_index}', + u'Path: {path}'] + + FORMAT_STRING_SHORT_PIECES = [u'Path: {path}'] + + SOURCE_LONG = 'AppCompatCache Registry Entry' + SOURCE_SHORT = 'REG' diff --git a/plaso/formatters/appusage.py b/plaso/formatters/appusage.py new file mode 100644 index 0000000..a36ee73 --- /dev/null +++ b/plaso/formatters/appusage.py @@ -0,0 +1,33 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a formatter for the Mac OS X application usage.""" + +from plaso.formatters import interface + + +class ApplicationUsageFormatter(interface.EventFormatter): + """Define the formatting for Application Usage information.""" + + DATA_TYPE = 'macosx:application_usage' + + FORMAT_STRING = (u'{application} v.{app_version} (bundle: {bundle_id}).' + ' Launched: {count} time(s)') + FORMAT_STRING_SHORT = u'{application} ({count} time(s))' + + SOURCE_LONG = 'Application Usage' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/asl.py b/plaso/formatters/asl.py new file mode 100644 index 0000000..4840c22 --- /dev/null +++ b/plaso/formatters/asl.py @@ -0,0 +1,47 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for the Apple System Log binary files.""" + +from plaso.formatters import interface + + +class AslFormatter(interface.ConditionalEventFormatter): + """Formatter for an ASL log entry.""" + + DATA_TYPE = 'mac:asl:event' + + FORMAT_STRING_PIECES = [ + u'MessageID: {message_id}', + u'Level: {level}', + u'User ID: {user_sid}', + u'Group ID: {group_id}', + u'Read User: {read_uid}', + u'Read Group: {read_gid}', + u'Host: {computer_name}', + u'Sender: {sender}', + u'Facility: {facility}', + u'Message: {message}', + u'{extra_information}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'Host: {host}', + u'Sender: {sender}', + u'Facility: {facility}'] + + SOURCE_LONG = 'ASL entry' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/bencode_parser.py b/plaso/formatters/bencode_parser.py new file mode 100644 index 0000000..bdf2625 --- /dev/null +++ b/plaso/formatters/bencode_parser.py @@ -0,0 +1,49 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for bencode parser events.""" + +from plaso.formatters import interface + + +class uTorrentFormatter(interface.ConditionalEventFormatter): + """Formatter for a BitTorrent uTorrent active torrents.""" + + DATA_TYPE = 'p2p:bittorrent:utorrent' + + SOURCE_LONG = 'uTorrent Active Torrents' + SOURCE_SHORT = 'TORRENT' + + FORMAT_STRING_SEPARATOR = u'; ' + + FORMAT_STRING_PIECES = [u'Torrent {caption}', + u'Saved to {path}', + u'Minutes seeded: {seedtime}'] + + +class TransmissionFormatter(interface.ConditionalEventFormatter): + """Formatter for a Transmission active torrents.""" + + DATA_TYPE = 'p2p:bittorrent:transmission' + + SOURCE_LONG = 'Transmission Active Torrents' + SOURCE_SHORT = 'TORRENT' + + FORMAT_STRING_SEPARATOR = u'; ' + + FORMAT_STRING_PIECES = [u'Saved to {destination}', + u'Minutes seeded: {seedtime}'] diff --git a/plaso/formatters/bsm.py b/plaso/formatters/bsm.py new file mode 100644 index 0000000..ba82667 --- /dev/null +++ b/plaso/formatters/bsm.py @@ -0,0 +1,54 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for Basic Security Module binary files.""" + +from plaso.formatters import interface + + +class MacBSMFormatter(interface.ConditionalEventFormatter): + """Formatter for an BSM log entry.""" + + DATA_TYPE = 'mac:bsm:event' + + FORMAT_STRING_PIECES = [ + u'Type: {event_type}', + u'Return: {return_value}', + u'Information: {extra_tokens}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'Type: {event_type}', + u'Return: {return_value}'] + + SOURCE_LONG = 'BSM entry' + SOURCE_SHORT = 'LOG' + + +class BSMFormatter(interface.ConditionalEventFormatter): + """Formatter for an BSM log entry.""" + + DATA_TYPE = 'bsm:event' + + FORMAT_STRING_PIECES = [ + u'Type: {event_type}', + u'Information: {extra_tokens}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'Type: {event_type}'] + + SOURCE_LONG = 'BSM entry' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/chrome.py b/plaso/formatters/chrome.py new file mode 100644 index 0000000..3a9ef70 --- /dev/null +++ b/plaso/formatters/chrome.py @@ -0,0 +1,61 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a formatter for the Google Chrome history.""" + +from plaso.formatters import interface + + +class ChromePageVisitedFormatter(interface.ConditionalEventFormatter): + """The event formatter for page visited data in Chrome History.""" + + DATA_TYPE = 'chrome:history:page_visited' + + FORMAT_STRING_PIECES = [ + u'{url}', + u'({title})', + u'[count: {typed_count}]', + u'Host: {host}', + u'Visit from: {from_visit}', + u'Visit Source: [{visit_source}]', + u'{extra}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'{url}', + u'({title})'] + + SOURCE_LONG = 'Chrome History' + SOURCE_SHORT = 'WEBHIST' + + +class ChromeFileDownloadFormatter(interface.ConditionalEventFormatter): + """The event formatter for file downloaded data in Chrome History.""" + + DATA_TYPE = 'chrome:history:file_downloaded' + + FORMAT_STRING_PIECES = [ + u'{url}', + u'({full_path}).', + u'Received: {received_bytes} bytes', + u'out of: {total_bytes} bytes.'] + + FORMAT_STRING_SHORT_PIECES = [ + u'{full_path} downloaded', + u'({received_bytes} bytes)'] + + SOURCE_LONG = 'Chrome History' + SOURCE_SHORT = 'WEBHIST' diff --git a/plaso/formatters/chrome_cache.py b/plaso/formatters/chrome_cache.py new file mode 100644 index 0000000..32669b2 --- /dev/null +++ b/plaso/formatters/chrome_cache.py @@ -0,0 +1,32 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for Chrome Cache files based-events.""" + +from plaso.formatters import interface + + +class ChromeCacheEntryEventFormatter(interface.ConditionalEventFormatter): + """Class contains the Chrome Cache Entry event formatter.""" + + DATA_TYPE = 'chrome:cache:entry' + + FORMAT_STRING_PIECES = [ + u'Original URL: {original_url}'] + + SOURCE_LONG = 'Chrome Cache' + SOURCE_SHORT = 'WEBHIST' diff --git a/plaso/formatters/chrome_cookies.py b/plaso/formatters/chrome_cookies.py new file mode 100644 index 0000000..c9ac405 --- /dev/null +++ b/plaso/formatters/chrome_cookies.py @@ -0,0 +1,40 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a formatter for the Google Chrome cookie.""" + +from plaso.formatters import interface + + +class ChromeCookieFormatter(interface.ConditionalEventFormatter): + """The event formatter for cookie data in Chrome Cookies database.""" + + DATA_TYPE = 'chrome:cookie:entry' + + FORMAT_STRING_PIECES = [ + u'{url}', + u'({cookie_name})', + u'Flags:', + u'[HTTP only] = {httponly}', + u'[Persistent] = {persistent}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'{host}', + u'({cookie_name})'] + + SOURCE_LONG = 'Chrome Cookies' + SOURCE_SHORT = 'WEBHIST' diff --git a/plaso/formatters/chrome_extension_activity.py b/plaso/formatters/chrome_extension_activity.py new file mode 100644 index 0000000..6a2dab0 --- /dev/null +++ b/plaso/formatters/chrome_extension_activity.py @@ -0,0 +1,47 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for the Google extension activity database events.""" + +from plaso.formatters import interface + + +class ChromeExtensionActivityEventFormatter( + interface.ConditionalEventFormatter): + """The event formatter for Chrome extension activity log entries.""" + + DATA_TYPE = 'chrome:extension_activity:activity_log' + + FORMAT_STRING_PIECES = [ + u'Chrome extension: {extension_id}', + u'Action type: {action_type}', + u'Activity identifier: {activity_id}', + u'Page URL: {page_url}', + u'Page title: {page_title}', + u'API name: {api_name}', + u'Args: {args}', + u'Other: {other}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'{extension_id}', + u'{api_name}', + u'{args}'] + + SOURCE_LONG = 'Chrome Extension Activity' + SOURCE_SHORT = 'WEBHIST' + + # TODO: add action_type string representation. diff --git a/plaso/formatters/cups_ipp.py b/plaso/formatters/cups_ipp.py new file mode 100644 index 0000000..707f143 --- /dev/null +++ b/plaso/formatters/cups_ipp.py @@ -0,0 +1,42 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for CUPS IPP file.""" + +from plaso.formatters import interface + + +class CupsIppFormatter(interface.ConditionalEventFormatter): + """Formatter for CUPS IPP file.""" + + DATA_TYPE = 'cups:ipp:event' + + FORMAT_STRING_PIECES = [ + u'Status: {status}', + u'User: {user}', + u'Owner: {owner}', + u'Job Name: {job_name}', + u'Application: {application}', + u'Document type: {type_doc}', + u'Printer: {printer_id}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'Status: {status}', + u'Job Name: {job_name}'] + + SOURCE_LONG = 'CUPS IPP Log' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/filestat.py b/plaso/formatters/filestat.py new file mode 100644 index 0000000..c95e2fa --- /dev/null +++ b/plaso/formatters/filestat.py @@ -0,0 +1,66 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a formatter for the Stat object of a PFile.""" + +from plaso.lib import errors +from plaso.formatters import interface + + +class PfileStatFormatter(interface.ConditionalEventFormatter): + """Define the formatting for PFileStat.""" + + DATA_TYPE = 'fs:stat' + + FORMAT_STRING_PIECES = [u'{display_name}', + u'({unallocated})'] + + FORMAT_STRING_SHORT_PIECES = [u'{filename}'] + + SOURCE_SHORT = 'FILE' + + def GetSources(self, event_object): + """Return a list of source short and long messages.""" + if self.DATA_TYPE != event_object.data_type: + raise errors.WrongFormatter('Unsupported data type: {0:s}.'.format( + event_object.data_type)) + + self.source_string = u'{0:s} {1:s}'.format( + getattr(event_object, 'fs_type', u'Unknown FS'), + getattr(event_object, 'timestamp_desc', u'Time')) + + return super(PfileStatFormatter, self).GetSources(event_object) + + def GetMessages(self, event_object): + """Returns a list of messages extracted from an event object. + + Args: + event_object: The event object (EventObject) containing the event + specific data. + + Returns: + A list that contains both the longer and shorter version of the message + string. + """ + if self.DATA_TYPE != event_object.data_type: + raise errors.WrongFormatter(u'Unsupported data type: {0:s}.'.format( + event_object.data_type)) + + if not getattr(event_object, 'allocated', True): + event_object.unallocated = u'unallocated' + + return super(PfileStatFormatter, self).GetMessages(event_object) diff --git a/plaso/formatters/firefox.py b/plaso/formatters/firefox.py new file mode 100644 index 0000000..0809aeb --- /dev/null +++ b/plaso/formatters/firefox.py @@ -0,0 +1,136 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a formatter for the Mozilla Firefox history.""" + +from plaso.lib import errors +from plaso.formatters import interface + + +class FirefoxBookmarkAnnotationFormatter(interface.ConditionalEventFormatter): + """Formatter for a Firefox places.sqlite bookmark annotation.""" + + DATA_TYPE = 'firefox:places:bookmark_annotation' + + FORMAT_STRING_PIECES = [ + u'Bookmark Annotation: [{content}]', + u'to bookmark [{title}]', + u'({url})'] + + FORMAT_STRING_SHORT_PIECES = [u'Bookmark Annotation: {title}'] + + SOURCE_LONG = 'Firefox History' + SOURCE_SHORT = 'WEBHIST' + + +class FirefoxBookmarkFolderFormatter(interface.EventFormatter): + """Formatter for a Firefox places.sqlite bookmark folder.""" + + DATA_TYPE = 'firefox:places:bookmark_folder' + + FORMAT_STRING = u'{title}' + + SOURCE_LONG = 'Firefox History' + SOURCE_SHORT = 'WEBHIST' + + +class FirefoxBookmarkFormatter(interface.ConditionalEventFormatter): + """Formatter for a Firefox places.sqlite URL bookmark.""" + + DATA_TYPE = 'firefox:places:bookmark' + + FORMAT_STRING_PIECES = [ + u'Bookmark {type}', + u'{title}', + u'({url})', + u'[{places_title}]', + u'visit count {visit_count}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'Bookmarked {title}', + u'({url})'] + + SOURCE_LONG = 'Firefox History' + SOURCE_SHORT = 'WEBHIST' + + +class FirefoxPageVisitFormatter(interface.ConditionalEventFormatter): + """Formatter for a Firefox places.sqlite page visited.""" + + DATA_TYPE = 'firefox:places:page_visited' + + # Transitions defined in the source file: + # src/toolkit/components/places/nsINavHistoryService.idl + # Also contains further explanation into what each of these settings mean. + _URL_TRANSITIONS = { + 1: 'LINK', + 2: 'TYPED', + 3: 'BOOKMARK', + 4: 'EMBED', + 5: 'REDIRECT_PERMANENT', + 6: 'REDIRECT_TEMPORARY', + 7: 'DOWNLOAD', + 8: 'FRAMED_LINK', + } + _URL_TRANSITIONS.setdefault('UNKOWN') + + # TODO: Make extra conditional formatting. + FORMAT_STRING_PIECES = [ + u'{url}', + u'({title})', + u'[count: {visit_count}]', + u'Host: {host}', + u'{extra_string}'] + + FORMAT_STRING_SHORT_PIECES = [u'URL: {url}'] + + SOURCE_LONG = 'Firefox History' + SOURCE_SHORT = 'WEBHIST' + + def GetMessages(self, event_object): + """Return the message strings.""" + if self.DATA_TYPE != event_object.data_type: + raise errors.WrongFormatter(u'Unsupported data type: {0:s}.'.format( + event_object.data_type)) + + transition = self._URL_TRANSITIONS.get( + getattr(event_object, 'visit_type', 0), None) + + if transition: + transition_str = u'Transition: {0!s}'.format(transition) + + if hasattr(event_object, 'extra'): + if transition: + event_object.extra.append(transition_str) + event_object.extra_string = u' '.join(event_object.extra) + elif transition: + event_object.extra_string = transition_str + + return super(FirefoxPageVisitFormatter, self).GetMessages(event_object) + + +class FirefoxDowloadFormatter(interface.EventFormatter): + """Formatter for a Firefox downloads.sqlite download.""" + + DATA_TYPE = 'firefox:downloads:download' + + FORMAT_STRING = (u'{url} ({full_path}). Received: {received_bytes} bytes ' + u'out of: {total_bytes} bytes.') + FORMAT_STRING_SHORT = u'{full_path} downloaded ({received_bytes} bytes)' + + SOURCE_LONG = 'Firefox History' + SOURCE_SHORT = 'WEBHIST' diff --git a/plaso/formatters/firefox_cache.py b/plaso/formatters/firefox_cache.py new file mode 100644 index 0000000..468c294 --- /dev/null +++ b/plaso/formatters/firefox_cache.py @@ -0,0 +1,39 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for Firefox cache records.""" + +from plaso.formatters import interface + +class FirefoxCacheFormatter(interface.ConditionalEventFormatter): + """Formatter for Firefox cache record.""" + + DATA_TYPE = 'firefox:cache:record' + + FORMAT_STRING_PIECES = [ + u'Fetched {fetch_count} time(s)', + u'[{response_code}]', + u'{request_method}', + u'"{url}"'] + + FORMAT_STRING_SHORT_PIECES = [ + u'[{response_code}]', + u'{request_method}', + u'"{url}"'] + + SOURCE_LONG = 'Firefox Cache' + SOURCE_SHORT = 'WEBHIST' diff --git a/plaso/formatters/firefox_cookies.py b/plaso/formatters/firefox_cookies.py new file mode 100644 index 0000000..1034ff1 --- /dev/null +++ b/plaso/formatters/firefox_cookies.py @@ -0,0 +1,40 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a formatter for the Firefox cookie.""" + +from plaso.formatters import interface + + +class FirefoxCookieFormatter(interface.ConditionalEventFormatter): + """The event formatter for cookie data in Firefox Cookies database.""" + + DATA_TYPE = 'firefox:cookie:entry' + + FORMAT_STRING_PIECES = [ + u'{url}', + u'({cookie_name})', + u'Flags:', + u'[HTTP only]: {httponly}', + u'(GA analysis: {ga_data})'] + + FORMAT_STRING_SHORT_PIECES = [ + u'{host}', + u'({cookie_name})'] + + SOURCE_LONG = 'Firefox Cookies' + SOURCE_SHORT = 'WEBHIST' diff --git a/plaso/formatters/ganalytics.py b/plaso/formatters/ganalytics.py new file mode 100644 index 0000000..8339a30 --- /dev/null +++ b/plaso/formatters/ganalytics.py @@ -0,0 +1,70 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a formatter for the Google Analytics cookie.""" + +from plaso.formatters import interface + + +class AnalyticsUtmaCookieFormatter(interface.ConditionalEventFormatter): + """The event formatter for UTMA Google Analytics cookie.""" + + DATA_TYPE = 'cookie:google:analytics:utma' + + FORMAT_STRING_PIECES = [ + u'{url}', + u'({cookie_name})', + u'Sessions: {sessions}', + u'Domain Hash: {domain_hash}', + u'Visitor ID: {domain_hash}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'{url}', + u'({cookie_name})'] + + SOURCE_LONG = 'Google Analytics Cookies' + SOURCE_SHORT = 'WEBHIST' + + +class AnalyticsUtmbCookieFormatter(AnalyticsUtmaCookieFormatter): + """The event formatter for UTMB Google Analytics cookie.""" + + DATA_TYPE = 'cookie:google:analytics:utmb' + + FORMAT_STRING_PIECES = [ + u'{url}', + u'({cookie_name})', + u'Pages Viewed: {pages_viewed}', + u'Domain Hash: {domain_hash}'] + + +class AnalyticsUtmzCookieFormatter(AnalyticsUtmaCookieFormatter): + """The event formatter for UTMZ Google Analytics cookie.""" + + DATA_TYPE = 'cookie:google:analytics:utmz' + + FORMAT_STRING_PIECES = [ + u'{url}', + u'({cookie_name})', + u'Sessions: {sessions}', + u'Domain Hash: {domain_hash}', + u'Sources: {sources}', + u'Last source used to access: {utmcsr}', + u'Ad campaign information: {utmccn}', + u'Last type of visit: {utmcmd}', + u'Keywords used to find site: {utmctr}', + u'Path to the page of referring link: {utmcct}'] diff --git a/plaso/formatters/gdrive.py b/plaso/formatters/gdrive.py new file mode 100644 index 0000000..ad6d99b --- /dev/null +++ b/plaso/formatters/gdrive.py @@ -0,0 +1,55 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a formatter for the Google Drive snaphots.""" + +from plaso.formatters import interface + + +__author__ = 'David Nides (david.nides@gmail.com)' + + +class GDriveCloudEntryFormatter(interface.ConditionalEventFormatter): + """Formatter for Google Drive snapshot cloud entry.""" + + DATA_TYPE = 'gdrive:snapshot:cloud_entry' + + FORMAT_STRING_PIECES = [ + u'File Path: {path}', + u'[{shared}]', + u'Size: {size}', + u'URL: {url}', + u'Type: {document_type}'] + FORMAT_STRING_SHORT_PIECES = [u'{path}'] + + SOURCE_LONG = 'Google Drive (cloud entry)' + SOURCE_SHORT = 'LOG' + + +class GDriveLocalEntryFormatter(interface.ConditionalEventFormatter): + """Formatter for Google Drive snapshot local entry.""" + + DATA_TYPE = 'gdrive:snapshot:local_entry' + + FORMAT_STRING_PIECES = [ + u'File Path: {path}', + u'Size: {size}'] + + FORMAT_STRING_SHORT_PIECES = [u'{path}'] + + SOURCE_LONG = 'Google Drive (local entry)' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/hachoir.py b/plaso/formatters/hachoir.py new file mode 100644 index 0000000..d120089 --- /dev/null +++ b/plaso/formatters/hachoir.py @@ -0,0 +1,57 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for Hachoir events.""" + +from plaso.lib import errors +from plaso.formatters import interface + + +__author__ = 'David Nides (david.nides@gmail.com)' + + +class HachoirFormatter(interface.EventFormatter): + """Formatter for Hachoir based events.""" + + DATA_TYPE = 'metadata:hachoir' + FORMAT_STRING = u'{data}' + + SOURCE_LONG = 'Hachoir Metadata' + SOURCE_SHORT = 'META' + + def GetMessages(self, event_object): + """Returns a list of messages extracted from an event object. + + Args: + event_object: The event object (EventObject) containing the event + specific data. + + Returns: + A list that contains both the longer and shorter version of the message + string. + """ + if self.DATA_TYPE != event_object.data_type: + raise errors.WrongFormatter(u'Unsupported data type: {0:s}.'.format( + event_object.data_type)) + + string_parts = [] + for key, value in sorted(event_object.metadata.items()): + string_parts.append(u'{0:s}: {1:s}'.format(key, value)) + + event_object.data = u' '.join(string_parts) + + return super(HachoirFormatter, self).GetMessages(event_object) diff --git a/plaso/formatters/iis.py b/plaso/formatters/iis.py new file mode 100644 index 0000000..2f9228d --- /dev/null +++ b/plaso/formatters/iis.py @@ -0,0 +1,59 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for Windows IIS log files.""" + +from plaso.formatters import interface + + +__author__ = 'Ashley Holtz (ashley.a.holtz@gmail.com)' + + +class WinIISFormatter(interface.ConditionalEventFormatter): + """A formatter for Windows IIS log entries.""" + + DATA_TYPE = 'iis:log:line' + + FORMAT_STRING_PIECES = [ + u'{http_method}', + u'{requested_uri_stem}', + u'[', + u'{source_ip}', + u'>', + u'{dest_ip}', + u':', + u'{dest_port}', + u']', + u'Http Status: {http_status}', + u'Bytes Sent: {sent_bytes}', + u'Bytes Received: {received_bytes}', + u'User Agent: {user_agent}', + u'Protocol Version: {protocol_version}',] + + FORMAT_STRING_SHORT_PIECES = [ + u'{http_method}', + u'{requested_uri_stem}', + u'[', + u'{source_ip}', + u'>', + u'{dest_ip}', + u':', + u'{dest_port}', + u']',] + + SOURCE_LONG = 'IIS Log' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/interface.py b/plaso/formatters/interface.py new file mode 100644 index 0000000..9c0b0d8 --- /dev/null +++ b/plaso/formatters/interface.py @@ -0,0 +1,244 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the event formatters interface classes.""" + +import re + +from plaso.lib import errors +from plaso.lib import registry + + +class EventFormatter(object): + """Base class to format event type specific data using a format string. + + Define the (long) format string and the short format string by defining + FORMAT_STRING and FORMAT_STRING_SHORT. The syntax of the format strings + is similar to that of format() where the place holder for a certain + event object attribute is defined as {attribute_name}. + """ + __metaclass__ = registry.MetaclassRegistry + __abstract = True + + # The data type is a unique identifier for the event data. The current + # approach is to define it as human readable string in the format + # root:branch: ... :leaf, e.g. a page visited entry inside a Chrome History + # database is defined as: chrome:history:page_visited. + DATA_TYPE = u'internal' + + # The format string. + FORMAT_STRING = u'' + FORMAT_STRING_SHORT = u'' + + # The source short and long strings. + SOURCE_SHORT = u'LOG' + SOURCE_LONG = u'' + + def __init__(self): + """Set up the formatter and determine if this is the right formatter.""" + # Forcing the format string to be unicode to make sure we don't + # try to format it as an ASCII string. + self.format_string = unicode(self.FORMAT_STRING) + self.format_string_short = unicode(self.FORMAT_STRING_SHORT) + self.source_string = unicode(self.SOURCE_LONG) + self.source_string_short = unicode(self.SOURCE_SHORT) + + def GetMessages(self, event_object): + """Return a list of messages extracted from an event object. + + The l2t_csv and other formats are dependent on a message field, + referred to as description_long and description_short in l2t_csv. + + Plaso does not store this field explicitly, it only contains a format + string and the appropriate attributes. + + This method takes the format string and converts that back into a + formatted string that can be used for display. + + Args: + event_object: The event object (EventObject) containing the event + specific data. + + Returns: + A list that contains both the longer and shorter version of the message + string. + + Raises: + WrongFormatter: if the event object cannot be formatted by the formatter. + """ + if self.DATA_TYPE != event_object.data_type: + raise errors.WrongFormatter(u'Unsupported data type: {0:s}.'.format( + event_object.data_type)) + + event_values = event_object.GetValues() + + try: + msg = self.format_string.format(**event_values) + except KeyError as exception: + msgs = [] + msgs.append(u'Format error: [{0:s}] for: <{1:s}>'.format( + exception, self.format_string)) + for attr, value in event_object.GetValues().iteritems(): + msgs.append(u'{0}: {1}'.format(attr, value)) + + msg = u' '.join(msgs) + + # Strip carriage return and linefeed form the message strings. + # Using replace function here because it is faster + # than re.sub() or string.strip(). + msg = msg.replace('\r', u'').replace('\n', u'') + + if not self.format_string_short: + msg_short = msg + else: + try: + msg_short = self.format_string_short.format(**event_values) + # Using replace function here because it is faster + # than re.sub() or string.strip(). + msg_short = msg_short.replace('\r', u'').replace('\n', u'') + except KeyError: + msg_short = u'Unable to format short message string: {0:s}'.format( + self.format_string_short) + + # Truncate the short message string if necessary. + if len(msg_short) > 80: + msg_short = u'{0:s}...'.format(msg_short[0:77]) + + return msg, msg_short + + def GetSources(self, event_object): + """Return a list containing source short and long.""" + if self.DATA_TYPE != event_object.data_type: + raise errors.WrongFormatter('Unsupported data type: {0:s}.'.format( + event_object.data_type)) + + return self.source_string_short, self.source_string + + +class ConditionalEventFormatter(EventFormatter): + """Base class to conditionally format event data using format string pieces. + + Define the (long) format string and the short format string by defining + FORMAT_STRING_PIECES and FORMAT_STRING_SHORT_PIECES. The syntax of the + format strings pieces is similar to of the event formatter + (EventFormatter). Every format string piece should contain a single + attribute name or none. + + FORMAT_STRING_SEPARATOR is used to control the string which the separate + string pieces should be joined. It contains a space by default. + """ + __abstract = True + + # The format string pieces. + FORMAT_STRING_PIECES = [u''] + FORMAT_STRING_SHORT_PIECES = [u''] + + # The separator used to join the string pieces. + FORMAT_STRING_SEPARATOR = u' ' + + def __init__(self): + """Initializes the conditional formatter. + + A map is build of the string pieces and their corresponding attribute + name to optimize conditional string formatting. + + Raises: + RuntimeError: when an invalid format string piece is encountered. + """ + super(ConditionalEventFormatter, self).__init__() + + # The format string can be defined as: + # {name}, {name:format}, {name!conversion}, {name!conversion:format} + regexp = re.compile('{[a-z][a-zA-Z0-9_]*[!]?[^:}]*[:]?[^}]*}') + regexp_name = re.compile('[a-z][a-zA-Z0-9_]*') + + # The format string pieces map is a list containing the attribute name + # per format string piece. E.g. ["Description: {description}"] would be + # mapped to: [0] = "description". If the string piece does not contain + # an attribute name it is treated as text that does not needs formatting. + self._format_string_pieces_map = [] + for format_string_piece in self.FORMAT_STRING_PIECES: + result = regexp.findall(format_string_piece) + if not result: + # The text format string piece is stored as an empty map entry to + # keep the index in the map equal to the format string pieces. + self._format_string_pieces_map.append('') + elif len(result) == 1: + # Extract the attribute name. + attribute_name = regexp_name.findall(result[0])[0] + self._format_string_pieces_map.append(attribute_name) + else: + raise RuntimeError(( + u'Invalid format string piece: [{0:s}] contains more than 1 ' + u'attribute name.').format(format_string_piece)) + + self._format_string_short_pieces_map = [] + for format_string_piece in self.FORMAT_STRING_SHORT_PIECES: + result = regexp.findall(format_string_piece) + if not result: + # The text format string piece is stored as an empty map entry to + # keep the index in the map equal to the format string pieces. + self._format_string_short_pieces_map.append('') + elif len(result) == 1: + # Extract the attribute name. + attribute_name = regexp_name.findall(result[0])[0] + self._format_string_short_pieces_map.append(attribute_name) + else: + raise RuntimeError(( + u'Invalid short format string piece: [{0:s}] contains more ' + u'than 1 attribute name.').format(format_string_piece)) + + def GetMessages(self, event_object): + """Returns a list of messages extracted from an event object. + + Args: + event_object: The event object (EventObject) containing the event + specific data. + + Returns: + A list that contains both the longer and shorter version of the message + string. + """ + if self.DATA_TYPE != event_object.data_type: + raise errors.WrongFormatter(u'Unsupported data type: {0:s}.'.format( + event_object.data_type)) + + # Using getattr here to make sure the attribute is not set to None. + # if A.b = None, hasattr(A, b) is True but getattr(A, b, None) is False. + string_pieces = [] + for map_index, attribute_name in enumerate(self._format_string_pieces_map): + if not attribute_name or hasattr(event_object, attribute_name): + if attribute_name: + attribute = getattr(event_object, attribute_name, None) + # If an attribute is an int, yet has zero value we want to include + # that in the format string, since that is still potentially valid + # information. Otherwise we would like to skip it. + if type(attribute) not in (bool, int, long, float) and not attribute: + continue + string_pieces.append(self.FORMAT_STRING_PIECES[map_index]) + self.format_string = unicode( + self.FORMAT_STRING_SEPARATOR.join(string_pieces)) + + string_pieces = [] + for map_index, attribute_name in enumerate( + self._format_string_short_pieces_map): + if not attribute_name or getattr(event_object, attribute_name, None): + string_pieces.append(self.FORMAT_STRING_SHORT_PIECES[map_index]) + self.format_string_short = unicode( + self.FORMAT_STRING_SEPARATOR.join(string_pieces)) + + return super(ConditionalEventFormatter, self).GetMessages(event_object) diff --git a/plaso/formatters/ipod.py b/plaso/formatters/ipod.py new file mode 100644 index 0000000..ac9a663 --- /dev/null +++ b/plaso/formatters/ipod.py @@ -0,0 +1,37 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for the iPod device events.""" + +from plaso.formatters import interface + + +class IPodDeviceFormatter(interface.ConditionalEventFormatter): + """Formatter for iPod device events.""" + + DATA_TYPE = 'ipod:device:entry' + + FORMAT_STRING_PIECES = [ + u'Device ID: {device_id}', + u'Type: {device_class}', + u'[{family_id}]', + u'Connected {use_count} times', + u'Serial nr: {serial_number}', + u'IMEI [{imei}]'] + + SOURCE_LONG = 'iPod Connections' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/java_idx.py b/plaso/formatters/java_idx.py new file mode 100644 index 0000000..55cc94f --- /dev/null +++ b/plaso/formatters/java_idx.py @@ -0,0 +1,34 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for Java Cache IDX events.""" + +from plaso.formatters import interface + + +class JavaIDXFormatter(interface.ConditionalEventFormatter): + """Formatter for a Java Cache IDX download item.""" + + DATA_TYPE = 'java:download:idx' + + SOURCE_LONG = 'Java Cache IDX' + SOURCE_SHORT = 'JAVA_IDX' + + FORMAT_STRING_PIECES = [ + u'IDX Version: {idx_version}', + u'Host IP address: ({ip_address})', + u'Download URL: {url}'] diff --git a/plaso/formatters/ls_quarantine.py b/plaso/formatters/ls_quarantine.py new file mode 100644 index 0000000..59a8f3a --- /dev/null +++ b/plaso/formatters/ls_quarantine.py @@ -0,0 +1,36 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for the Mac OS X launch services quarantine events.""" + +from plaso.formatters import interface + + +class LSQuarantineFormatter(interface.ConditionalEventFormatter): + """Formatter for a LS Quarantine history event.""" + + DATA_TYPE = 'macosx:lsquarantine' + + FORMAT_STRING_PIECES = [ + u'[{agent}]', + u'Downloaded: {url}', + u'<{data}>'] + + FORMAT_STRING_SHORT_PIECES = [u'{url}'] + + SOURCE_LONG = 'LS Quarantine Event' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/mac_appfirewall.py b/plaso/formatters/mac_appfirewall.py new file mode 100644 index 0000000..66b4a77 --- /dev/null +++ b/plaso/formatters/mac_appfirewall.py @@ -0,0 +1,39 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for the Mac appfirewall.log file.""" + +from plaso.formatters import interface + +class MacAppFirewallLogFormatter(interface.ConditionalEventFormatter): + """Formatter for Mac appfirewall.log file.""" + + DATA_TYPE = 'mac:asl:appfirewall:line' + + FORMAT_STRING_PIECES = [ + u'Computer: {computer_name}', + u'Agent: {agent}', + u'Status: {status}', + u'Process name: {process_name}', + u'Log: {action}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'Process name: {process_name}', + u'Status: {status}'] + + SOURCE_LONG = 'Mac AppFirewall Log' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/mac_document_versions.py b/plaso/formatters/mac_document_versions.py new file mode 100644 index 0000000..9317f74 --- /dev/null +++ b/plaso/formatters/mac_document_versions.py @@ -0,0 +1,38 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a formatter for the Mac OS X Document Versions files.""" + +from plaso.formatters import interface + + +class MacDocumentVersionsFormatter(interface.ConditionalEventFormatter): + """The event formatter for page visited data in Document Versions.""" + + DATA_TYPE = 'mac:document_versions:file' + + FORMAT_STRING_PIECES = [ + u'Version of [{name}]', + u'({path})', + u'stored in {version_path}', + u'by {user_sid}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'Stored a document version of [{name}]'] + + SOURCE_LONG = 'Document Versions' + SOURCE_SHORT = 'HISTORY' diff --git a/plaso/formatters/mac_keychain.py b/plaso/formatters/mac_keychain.py new file mode 100644 index 0000000..ee70616 --- /dev/null +++ b/plaso/formatters/mac_keychain.py @@ -0,0 +1,53 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for the Keychain password database file.""" + +from plaso.formatters import interface + + +class KeychainApplicationRecordFormatter(interface.ConditionalEventFormatter): + """Formatter for keychain application record event.""" + + DATA_TYPE = 'mac:keychain:application' + + FORMAT_STRING_PIECES = [ + u'Name: {entry_name}', + u'Account: {account_name}'] + + FORMAT_STRING_SHORT_PIECES = [u'{entry_name}'] + + SOURCE_LONG = 'Keychain Application password' + SOURCE_SHORT = 'LOG' + + +class KeychainInternetRecordFormatter(interface.ConditionalEventFormatter): + """Formatter for keychain internet record event.""" + + DATA_TYPE = 'mac:keychain:internet' + + FORMAT_STRING_PIECES = [ + u'Name: {entry_name}', + u'Account: {account_name}', + u'Where: {where}', + u'Protocol: {protocol}', + u'({type_protocol})'] + + FORMAT_STRING_SHORT_PIECES = [u'{entry_name}'] + + SOURCE_LONG = 'Keychain Internet password' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/mac_securityd.py b/plaso/formatters/mac_securityd.py new file mode 100644 index 0000000..3c25fd2 --- /dev/null +++ b/plaso/formatters/mac_securityd.py @@ -0,0 +1,39 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for ASL securityd log file.""" + +from plaso.formatters import interface + + +class MacSecuritydLogFormatter(interface.ConditionalEventFormatter): + """Formatter for ASL Securityd file.""" + + DATA_TYPE = 'mac:asl:securityd:line' + + FORMAT_STRING_PIECES = [ + u'Sender: {sender}', + u'({sender_pid})', + u'Level: {level}', + u'Facility: {facility}', + u'Text: {message}'] + + FORMAT_STRING_SHORT_PIECES = [u'Text: {message}'] + + SOURCE_LONG = 'Mac ASL Securityd Log' + SOURCE_SHORT = 'LOG' + diff --git a/plaso/formatters/mac_wifi.py b/plaso/formatters/mac_wifi.py new file mode 100644 index 0000000..a5415c5 --- /dev/null +++ b/plaso/formatters/mac_wifi.py @@ -0,0 +1,38 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for Mac wifi.log file.""" + +from plaso.formatters import interface + + +class MacWifiLogFormatter(interface.ConditionalEventFormatter): + """Formatter for Mac Wifi file.""" + + DATA_TYPE = 'mac:wifilog:line' + + FORMAT_STRING_PIECES = [ + u'Action: {action}', + u'Agent: {user}', + u'({function})', + u'Log: {text}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'Action: {action}'] + + SOURCE_LONG = 'Mac Wifi Log' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/mackeeper_cache.py b/plaso/formatters/mackeeper_cache.py new file mode 100644 index 0000000..8175bdd --- /dev/null +++ b/plaso/formatters/mackeeper_cache.py @@ -0,0 +1,35 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a MacKeepr Cache formatter in plaso.""" + +from plaso.formatters import interface + + +class MacKeeperCacheFormatter(interface.ConditionalEventFormatter): + """Formatter for MacKeeper Cache extracted events.""" + + DATA_TYPE = 'mackeeper:cache' + + FORMAT_STRING_PIECES = [ + u'{description}', u'<{event_type}>', u':', u'{text}', u'[', + u'URL: {url}', u'Event ID: {record_id}', 'Room: {room}', u']'] + + FORMAT_STRING_SHORT_PIECES = [u'<{event_type}>', u'{text}'] + + SOURCE_LONG = 'MacKeeper Cache' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/mactime.py b/plaso/formatters/mactime.py new file mode 100644 index 0000000..1bc2c87 --- /dev/null +++ b/plaso/formatters/mactime.py @@ -0,0 +1,32 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for the Sleuthkit (TSK) bodyfile or mactime format.""" + +from plaso.formatters import interface + + +class MactimeFormatter(interface.EventFormatter): + """Class that formats mactime bodyfile events.""" + + DATA_TYPE = 'fs:mactime:line' + + # The format string. + FORMAT_STRING = u'{filename}' + + SOURCE_LONG = 'Mactime Bodyfile' + SOURCE_SHORT = 'FILE' diff --git a/plaso/formatters/manager.py b/plaso/formatters/manager.py new file mode 100644 index 0000000..a0d16ff --- /dev/null +++ b/plaso/formatters/manager.py @@ -0,0 +1,140 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the event formatters manager class.""" + +import logging + +from plaso.formatters import interface +from plaso.lib import utils + + +class DefaultFormatter(interface.EventFormatter): + """Default formatter for events that do not have any defined formatter.""" + + DATA_TYPE = u'event' + FORMAT_STRING = u' Attributes: {attribute_driven}' + FORMAT_STRING_SHORT = u' {attribute_driven}' + + def GetMessages(self, event_object): + """Return a list of messages extracted from an event object.""" + text_pieces = [] + + for key, value in event_object.GetValues().items(): + if key in utils.RESERVED_VARIABLES: + continue + text_pieces.append(u'{0:s}: {1!s}'.format(key, value)) + + event_object.attribute_driven = u' '.join(text_pieces) + # Due to the way the default formatter behaves it requires the data_type + # to be set as 'event', otherwise it will complain and deny processing + # the event. + # TODO: Change this behavior and allow the default formatter to accept + # arbitrary data types (as it should). + old_data_type = getattr(event_object, 'data_type', None) + event_object.data_type = self.DATA_TYPE + msg, msg_short = super(DefaultFormatter, self).GetMessages(event_object) + event_object.data_type = old_data_type + return msg, msg_short + + +class EventFormatterManager(object): + """Class to manage the event formatters.""" + + @classmethod + def GetFormatter(cls, event_object): + """Retrieves the formatter for a specific event object. + + This function builds a map of data types and the corresponding event + formatters. At the moment this map is only build once. + + Args: + event_object: The event object (EventObject) which is used to identify + the formatter. + + Returns: + The corresponding formatter (EventFormatter) if available or None. + + Raises: + RuntimeError if a duplicate event formatter is found while building + the map of event formatters. + """ + if not hasattr(cls, 'event_formatters'): + cls.event_formatters = {} + cls.default_formatter = DefaultFormatter() + for cls_formatter in interface.EventFormatter.classes: + try: + formatter = interface.EventFormatter.classes[cls_formatter]() + + # Raise on duplicate formatters. + if formatter.DATA_TYPE in cls.event_formatters: + raise RuntimeError(( + u'event formatter for data type: {0:s} defined in: {1:s} and ' + u'{2:s}.').format( + formatter.DATA_TYPE, cls_formatter, + cls.event_formatters[ + formatter.DATA_TYPE].__class__.__name__)) + cls.event_formatters[formatter.DATA_TYPE] = formatter + except RuntimeError as exeception: + # Ignore broken formatters. + logging.warning(u'{0:s}'.format(exeception)) + + cls.event_formatters.setdefault(None) + + if event_object.data_type in cls.event_formatters: + return cls.event_formatters[event_object.data_type] + else: + logging.warning( + u'Using default formatter for data type: {0:s}'.format( + event_object.data_type)) + return cls.default_formatter + + @classmethod + def GetMessageStrings(cls, event_object): + """Retrieves the formatted message strings for a specific event object. + + Args: + event_object: The event object (EventObject) which is used to identify + the formatter. + + Returns: + A list that contains both the longer and shorter version of the message + string. + """ + formatter = cls.GetFormatter(event_object) + if not formatter: + return u'', u'' + return formatter.GetMessages(event_object) + + @classmethod + def GetSourceStrings(cls, event_object): + """Retrieves the formatted source long and short strings for an event. + + Args: + event_object: The event object (EventObject) which is used to identify + the formatter. + + Returns: + A list that contains the source_short and source_long version of the + event. + """ + # TODO: change this to return the long variant first so it is consistent + # with GetMessageStrings. + formatter = cls.GetFormatter(event_object) + if not formatter: + return u'', u'' + return formatter.GetSources(event_object) diff --git a/plaso/formatters/manager_test.py b/plaso/formatters/manager_test.py new file mode 100644 index 0000000..608d068 --- /dev/null +++ b/plaso/formatters/manager_test.py @@ -0,0 +1,163 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a unit test for the event formatters.""" + +import unittest + +from plaso.formatters import interface +from plaso.formatters import manager +from plaso.formatters import winreg # pylint: disable=unused-import +from plaso.lib import event_test + + +class TestEvent1Formatter(interface.EventFormatter): + """Test event 1 formatter.""" + DATA_TYPE = 'test:event1' + FORMAT_STRING = u'{text}' + + SOURCE_SHORT = 'FILE' + SOURCE_LONG = 'Weird Log File' + + +class WrongEventFormatter(interface.EventFormatter): + """A simple event formatter.""" + DATA_TYPE = 'test:wrong' + FORMAT_STRING = u'This format string does not match {body}.' + + SOURCE_SHORT = 'FILE' + SOURCE_LONG = 'Weird Log File' + + +class EventFormatterUnitTest(unittest.TestCase): + """The unit test for the event formatter.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._formatters_manager = manager.EventFormatterManager + self.event_objects = event_test.GetEventObjects() + + def GetCSVLine(self, event_object): + """Takes an EventObject and prints out a simple CSV line from it.""" + try: + msg, _ = self._formatters_manager.GetMessageStrings(event_object) + source_short, source_long = self._formatters_manager.GetSourceStrings( + event_object) + except KeyError: + print event_object.GetAttributes() + return u'{0:d},{1:s},{2:s},{3:s}'.format( + event_object.timestamp, source_short, source_long, msg) + + def testInitialization(self): + """Test the initialization.""" + self.assertTrue(TestEvent1Formatter()) + + def testAttributes(self): + """Test if we can read the event attributes correctly.""" + events = {} + for event_object in self.event_objects: + events[self.GetCSVLine(event_object)] = True + + self.assertIn(( + u'1334961526929596,REG,UNKNOWN key,[MY AutoRun key] Run: ' + u'c:/Temp/evil.exe'), events) + + self.assertIn( + (u'1334966206929596,REG,UNKNOWN key,[//HKCU/Secret/EvilEmpire/' + u'Malicious_key] Value: send all the exes to the other ' + u'world'), events) + self.assertIn((u'1334940286000000,REG,UNKNOWN key,[//HKCU/Windows' + u'/Normal] Value: run all the benign stuff'), events) + self.assertIn((u'1335781787929596,FILE,Weird Log File,This log line reads ' + u'ohh so much.'), events) + self.assertIn((u'1335781787929596,FILE,Weird Log File,Nothing of interest' + u' here, move on.'), events) + self.assertIn((u'1335791207939596,FILE,Weird Log File,Mr. Evil just logged' + u' into the machine and got root.'), events) + + def testTextBasedEvent(self): + """Test a text based event.""" + for event_object in self.event_objects: + source_short, _ = self._formatters_manager.GetSourceStrings(event_object) + if source_short == 'LOG': + msg, msg_short = self._formatters_manager.GetMessageStrings( + event_object) + + self.assertEquals(msg, ( + u'This is a line by someone not reading the log line properly. And ' + u'since this log line exceeds the accepted 80 chars it will be ' + u'shortened.')) + self.assertEquals(msg_short, ( + u'This is a line by someone not reading the log line properly. ' + u'And since this l...')) + + +class ConditionalTestEvent1(event_test.TestEvent1): + DATA_TYPE = 'test:conditional_event1' + + +class ConditionalTestEvent1Formatter(interface.ConditionalEventFormatter): + """Test event 1 conditional (event) formatter.""" + DATA_TYPE = 'test:conditional_event1' + FORMAT_STRING_PIECES = [ + u'Description: {description}', + u'Comment', + u'Value: 0x{numeric:02x}', + u'Optional: {optional}', + u'Text: {text}'] + + SOURCE_SHORT = 'LOG' + SOURCE_LONG = 'Some Text File.' + + +class BrokenConditionalEventFormatter(interface.ConditionalEventFormatter): + """A broken conditional event formatter.""" + DATA_TYPE = 'test:broken_conditional' + FORMAT_STRING_PIECES = [u'{too} {many} formatting placeholders'] + + SOURCE_SHORT = 'LOG' + SOURCE_LONG = 'Some Text File.' + + +class ConditionalEventFormatterUnitTest(unittest.TestCase): + """The unit test for the conditional event formatter.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self.event_object = ConditionalTestEvent1(1335791207939596, { + 'numeric': 12, 'description': 'this is beyond words', + 'text': 'but we\'re still trying to say something about the event'}) + + def testInitialization(self): + """Test the initialization.""" + self.assertTrue(ConditionalTestEvent1Formatter()) + with self.assertRaises(RuntimeError): + BrokenConditionalEventFormatter() + + def testGetMessages(self): + """Test get messages.""" + event_formatter = ConditionalTestEvent1Formatter() + msg, _ = event_formatter.GetMessages(self.event_object) + + expected_msg = ( + u'Description: this is beyond words Comment Value: 0x0c ' + u'Text: but we\'re still trying to say something about the event') + self.assertEquals(msg, expected_msg) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/formatters/mcafeeav.py b/plaso/formatters/mcafeeav.py new file mode 100644 index 0000000..a66663f --- /dev/null +++ b/plaso/formatters/mcafeeav.py @@ -0,0 +1,34 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for the McAfee AV Logs files.""" + +from plaso.formatters import interface + + +class McafeeAccessProtectionLogEventFormatter(interface.EventFormatter): + """Class that formats the McAfee Access Protection Log events.""" + + DATA_TYPE = 'av:mcafee:accessprotectionlog' + + # The format string. + FORMAT_STRING = (u'File Name: {filename} User: {username} {trigger_location} ' + u'{status} {rule} {action}') + FORMAT_STRING_SHORT = u'{filename} {action}' + + SOURCE_LONG = 'McAfee Access Protection Log' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/msie_webcache.py b/plaso/formatters/msie_webcache.py new file mode 100644 index 0000000..06cef60 --- /dev/null +++ b/plaso/formatters/msie_webcache.py @@ -0,0 +1,99 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatters for the MSIE WebCache ESE database events.""" + +from plaso.formatters import interface + + +class MsieWebCacheContainerEventFormatter(interface.ConditionalEventFormatter): + """Formatter for a MSIE WebCache ESE database Container_# table record.""" + + DATA_TYPE = 'msie:webcache:container' + + FORMAT_STRING_PIECES = [ + u'Entry identifier: {entry_identifier}', + u'Container identifier: {container_identifier}', + u'Cache identifier: {cache_identifier}', + u'URL: {url}', + u'Redirect URL: {redirect_url}', + u'Access count: {access_count}', + u'Sync count: {sync_count}', + u'Filename: {cached_filename}', + u'File extension: {file_extension}', + u'Cached file size: {cached_file_size}', + u'Request headers: {request_headers}', + u'Response headers: {response_headers}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'URL: {url}'] + + SOURCE_LONG = 'MSIE WebCache container record' + SOURCE_SHORT = 'WEBHIST' + + +class MsieWebCacheContainersEventFormatter(interface.ConditionalEventFormatter): + """Formatter for a MSIE WebCache ESE database Containers table record.""" + + DATA_TYPE = 'msie:webcache:containers' + + FORMAT_STRING_PIECES = [ + u'Container identifier: {container_identifier}', + u'Set identifier: {set_identifier}', + u'Name: {name}', + u'Directory: {directory}', + u'Table: Container_{container_identifier}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'Directory: {directory}'] + + SOURCE_LONG = 'MSIE WebCache containers record' + SOURCE_SHORT = 'WEBHIST' + + +class MsieWebCacheLeakFilesEventFormatter(interface.ConditionalEventFormatter): + """Formatter for a MSIE WebCache ESE database LeakFiles table record.""" + + DATA_TYPE = 'msie:webcache:leak_file' + + FORMAT_STRING_PIECES = [ + u'Leak identifier: {leak_identifier}', + u'Filename: {cached_filename}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'Filename: {cached_filename}'] + + SOURCE_LONG = 'MSIE WebCache partitions record' + SOURCE_SHORT = 'WEBHIST' + + +class MsieWebCachePartitionsEventFormatter(interface.ConditionalEventFormatter): + """Formatter for a MSIE WebCache ESE database Partitions table record.""" + + DATA_TYPE = 'msie:webcache:partitions' + + FORMAT_STRING_PIECES = [ + u'Partition identifier: {partition_identifier}', + u'Partition type: {partition_type}', + u'Directory: {directory}', + u'Table identifier: {table_identifier}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'Directory: {directory}'] + + SOURCE_LONG = 'MSIE WebCache partitions record' + SOURCE_SHORT = 'WEBHIST' diff --git a/plaso/formatters/msiecf.py b/plaso/formatters/msiecf.py new file mode 100644 index 0000000..f55a898 --- /dev/null +++ b/plaso/formatters/msiecf.py @@ -0,0 +1,65 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for Microsoft Internet Explorer (MSIE) Cache Files (CF) events.""" + +from plaso.lib import errors +from plaso.formatters import interface + + +class MsiecfUrlFormatter(interface.ConditionalEventFormatter): + """Formatter for a MSIECF URL item.""" + + DATA_TYPE = 'msiecf:url' + + FORMAT_STRING_PIECES = [ + u'Location: {url}', + u'Number of hits: {number_of_hits}', + u'Cached file size: {cached_file_size}', + u'HTTP headers: {http_headers_cleaned}', + u'{recovered_string}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'Location: {url}'] + + SOURCE_LONG = 'MSIE Cache File URL record' + SOURCE_SHORT = 'WEBHIST' + + def GetMessages(self, event_object): + """Returns a list of messages extracted from an event object. + + Args: + event_object: The event object (EventObject) containing the event + specific data. + + Returns: + A list that contains both the longer and shorter version of the message + string. + """ + if self.DATA_TYPE != event_object.data_type: + raise errors.WrongFormatter(u'Unsupported data type: {0:s}.'.format( + event_object.data_type)) + + if hasattr(event_object, 'http_headers'): + event_object.http_headers_cleaned = event_object.http_headers.replace( + '\r\n', ' - ') + # TODO: Could this be moved upstream since this is done in other parsers + # as well? + if getattr(event_object, 'recovered', None): + event_object.recovered_string = '[Recovered Entry]' + + return super(MsiecfUrlFormatter, self).GetMessages(event_object) diff --git a/plaso/formatters/olecf.py b/plaso/formatters/olecf.py new file mode 100644 index 0000000..d2d4f1a --- /dev/null +++ b/plaso/formatters/olecf.py @@ -0,0 +1,149 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatters for OLE Compound File (OLECF) events.""" + +from plaso.formatters import interface +from plaso.lib import errors + + +class OleCfItemFormatter(interface.EventFormatter): + """Formatter for an OLECF item.""" + + DATA_TYPE = 'olecf:item' + + FORMAT_STRING = u'Name: {name}' + FORMAT_STRING_SHORT = u'Name: {name}' + + SOURCE_LONG = 'OLECF Item' + SOURCE_SHORT = 'OLECF' + + +class OleCfDestListEntryFormatter(interface.ConditionalEventFormatter): + """Formatter for an OLECF DestList stream.""" + + DATA_TYPE = 'olecf:dest_list:entry' + + FORMAT_STRING_PIECES = [ + u'Entry: {entry_number}', + u'Pin status: {pin_status_string}', + u'Hostname: {hostname}', + u'Path: {path}', + u'Droid volume identifier: {droid_volume_identifier}', + u'Droid file identifier: {droid_file_identifier}', + u'Birth droid volume identifier: {birth_droid_volume_identifier}', + u'Birth droid file identifier: {birth_droid_file_identifier}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'Entry: {entry_number}', + u'Pin status: {pin_status_string}', + u'Path: {path}'] + + def GetMessages(self, event_object): + """Returns a list of messages extracted from an event object. + + Args: + event_object: The event object (EventObject) containing the event + specific data. + + Returns: + A list that contains both the longer and shorter version of the message + string. + """ + if self.DATA_TYPE != event_object.data_type: + raise errors.WrongFormatter(u'Unsupported data type: {0:s}.'.format( + event_object.data_type)) + + pin_status = getattr(event_object, 'pin_status', None) + if pin_status == 0xffffffff: + event_object.pin_status_string = u'Unpinned' + else: + event_object.pin_status_string = u'Pinned' + + return super(OleCfDestListEntryFormatter, self).GetMessages(event_object) + + +class OleCfDocumentSummaryInfoFormatter(interface.ConditionalEventFormatter): + """Formatter for an OLECF Summary Info property set stream.""" + + DATA_TYPE = 'olecf:document_summary_info' + + FORMAT_STRING_PIECES = [ + u'Number of bytes: {number_of_bytes}', + u'Number of lines: {number_of_lines}', + u'Number of paragraphs: {number_of_paragraphs}', + u'Number of slides: {number_of_slides}', + u'Number of notes: {number_of_notes}', + u'Number of hidden slides: {number_of_hidden_slides}', + u'Number of multi-media clips: {number_of_clips}', + u'Company: {company}', + u'Manager: {manager}', + u'Shared document: {shared_document}', + u'Application version: {application_version}', + u'Content type: {content_type}', + u'Content status: {content_status}', + u'Language: {language}', + u'Document version: {document_version}'] + + # TODO: add support for the following properties. + # u'Digital signature: {digital_signature}', + + FORMAT_STRING_SHORT_PIECES = [ + u'Company: {company}'] + + SOURCE_LONG = 'OLECF Document Summary Info' + SOURCE_SHORT = 'OLECF' + + +class OleCfSummaryInfoFormatter(interface.ConditionalEventFormatter): + """Formatter for an OLECF Summary Info property set stream.""" + + DATA_TYPE = 'olecf:summary_info' + + FORMAT_STRING_PIECES = [ + u'Title: {title}', + u'Subject: {subject}', + u'Author: {author}', + u'Keywords: {keywords}', + u'Comments: {comments}', + u'Template: {template}', + u'Revision number: {revision_number}', + u'Last saved by: {last_saved_by}', + u'Total edit time: {total_edit_time}', + u'Number of pages: {number_of_pages}', + u'Number of words: {number_of_words}', + u'Number of characters: {number_of_characters}', + u'Application: {application}', + u'Security: {security}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'Title: {title}', + u'Subject: {subject}', + u'Author: {author}', + u'Revision number: {revision_number}'] + + SOURCE_LONG = 'OLECF Summary Info' + SOURCE_SHORT = 'OLECF' + + # TODO: add a function to print the security as a descriptive string. + _SECURITY_VALUES = { + 0x00000001: 'Password protected', + 0x00000002: 'Read-only recommended', + 0x00000004: 'Read-only enforced', + 0x00000008: 'Locked for annotations', + } + diff --git a/plaso/formatters/opera.py b/plaso/formatters/opera.py new file mode 100644 index 0000000..5eb1b97 --- /dev/null +++ b/plaso/formatters/opera.py @@ -0,0 +1,47 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for Opera history events.""" + +from plaso.formatters import interface + + +class OperaGlobalHistoryFormatter(interface.ConditionalEventFormatter): + """Formatter for an Opera global history event.""" + + DATA_TYPE = 'opera:history:entry' + + FORMAT_STRING_PIECES = [ + u'{url}', + u'({title})', + u'[{description}]'] + + SOURCE_LONG = 'Opera Browser History' + SOURCE_SHORT = 'WEBHIST' + + +class OperaTypedHistoryFormatter(interface.ConditionalEventFormatter): + """Formatter for an Opera typed history event.""" + + DATA_TYPE = 'opera:history:typed_entry' + + FORMAT_STRING_PIECES = [ + u'{url}', + u'({entry_selection})'] + + SOURCE_LONG = 'Opera Browser History' + SOURCE_SHORT = 'WEBHIST' diff --git a/plaso/formatters/oxml.py b/plaso/formatters/oxml.py new file mode 100644 index 0000000..177b437 --- /dev/null +++ b/plaso/formatters/oxml.py @@ -0,0 +1,67 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for OpenXML events.""" + +from plaso.formatters import interface + +__author__ = 'David Nides (david.nides@gmail.com)' + + +class OpenXMLParserFormatter(interface.ConditionalEventFormatter): + """Formatter for OXML events.""" + + DATA_TYPE = 'metadata:openxml' + + FORMAT_STRING_PIECES = [ + u'Creating App: {creating_app}', + u'App version: {app_version}', + u'Title: {title}', + u'Subject: {subject}', + u'Last saved by: {last_saved_by}', + u'Author: {author}', + u'Total edit time (secs): {total_edit_time}', + u'Keywords: {keywords}', + u'Comments: {comments}', + u'Revision Num: {revision_num}', + u'Template: {template}', + u'Num pages: {num_pages}', + u'Num words: {num_words}', + u'Num chars: {num_chars}', + u'Num chars with spaces: {num_chars_w_spaces}', + u'Num lines: {num_lines}', + u'Company: {company}', + u'Manager: {manager}', + u'Shared: {shared}', + u'Security: {security}', + u'Hyperlinks changed: {hyperlinks_changed}', + u'Links up to date: {links_up_to_date}', + u'Scale crop: {scale_crop}', + u'Digital signature: {dig_sig}', + u'Slides: {slides}', + u'Hidden slides: {hidden_slides}', + u'Presentation format: {presentation_format}', + u'MM clips: {mm_clips}', + u'Notes: {notes}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'Title: {title}', + u'Subject: {subject}', + u'Author: {author}'] + + SOURCE_LONG = 'Open XML Metadata' + SOURCE_SHORT = 'META' diff --git a/plaso/formatters/pcap.py b/plaso/formatters/pcap.py new file mode 100644 index 0000000..9e67dbd --- /dev/null +++ b/plaso/formatters/pcap.py @@ -0,0 +1,50 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for PCAP files.""" + +from plaso.formatters import interface + + +__author__ = 'Dominique Kilman (lexistar97@gmail.com)' + + +class PCAPFormatter(interface.ConditionalEventFormatter): + """Define the formatting PCAP record.""" + + DATA_TYPE = 'metadata:pcap' + + FORMAT_STRING_PIECES = [ + u'Source IP: {source_ip}', + u'Destination IP: {dest_ip}', + u'Source Port: {source_port}', + u'Destination Port: {dest_port}', + u'Protocol: {protocol}', + u'Type: {stream_type}', + u'Size: {size}', + u'Protocol Data: {protocol_data}', + u'Stream Data: {stream_data}', + u'First Packet ID: {first_packet_id}', + u'Last Packet ID: {last_packet_id}', + u'Packet Count: {packet_count}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'Type: {stream_type}', + u'First Packet ID: {first_packet_id}'] + + SOURCE_LONG = 'Packet Capture File (pcap)' + SOURCE_SHORT = 'PCAP' diff --git a/plaso/formatters/plist.py b/plaso/formatters/plist.py new file mode 100644 index 0000000..854451c --- /dev/null +++ b/plaso/formatters/plist.py @@ -0,0 +1,36 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a formatter for Plist Events.""" + +from plaso.formatters import interface + + +class PlistFormatter(interface.ConditionalEventFormatter): + """Event Formatter for plist keys.""" + + DATA_TYPE = 'plist:key' + + FORMAT_STRING_SEPARATOR = u'' + + FORMAT_STRING_PIECES = [ + u'{root}/', + u'{key}', + u' {desc}'] + + SOURCE_LONG = 'Plist Entry' + SOURCE_SHORT = 'PLIST' diff --git a/plaso/formatters/pls_recall.py b/plaso/formatters/pls_recall.py new file mode 100644 index 0000000..44e011e --- /dev/null +++ b/plaso/formatters/pls_recall.py @@ -0,0 +1,33 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for PL-Sql Recall events.""" + +from plaso.formatters import interface + + +class PlsRecallFormatter(interface.EventFormatter): + """Formatter for a for a PL-Sql Recall file container.""" + DATA_TYPE = 'PLSRecall:event' + SOURCE_LONG = 'PL-Sql Developer Recall file' + SOURCE_SHORT = 'PLSRecall' + + # The format string. + FORMAT_STRING = (u'Sequence #{sequence} User: {username} ' + u'Database Name: {database_name} Query: {query}') + FORMAT_STRING_SHORT = u'{sequence} {username} {database_name} {query}' + diff --git a/plaso/formatters/popcontest.py b/plaso/formatters/popcontest.py new file mode 100644 index 0000000..3b12227 --- /dev/null +++ b/plaso/formatters/popcontest.py @@ -0,0 +1,55 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for the Popularity Contest parser events.""" + +from plaso.formatters import interface + + +class PopularityContestSessionFormatter(interface.ConditionalEventFormatter): + """Formatter for Popularity Contest Session information.""" + + DATA_TYPE = 'popularity_contest:session:event' + + FORMAT_STRING_PIECES = [ + u'Session {session}', + u'{status}', + u'ID {hostid}', + u'[{details}]'] + + FORMAT_STRING_SHORT_PIECES = [ + u'Session {session}', + u'{status}'] + + SOURCE_LONG = 'Popularity Contest Session' + SOURCE_SHORT = 'LOG' + + +class PopularityContestLogFormatter(interface.ConditionalEventFormatter): + """Formatter for Popularity Contest Log events.""" + + DATA_TYPE = 'popularity_contest:log:event' + + FORMAT_STRING_PIECES = [ + u'mru [{mru}]', + u'package [{package}]', + u'tag [{record_tag}]'] + + FORMAT_STRING_SHORT_PIECES = [u'{mru}'] + + SOURCE_LONG = 'Popularity Contest Log' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/recycler.py b/plaso/formatters/recycler.py new file mode 100644 index 0000000..48ecb0a --- /dev/null +++ b/plaso/formatters/recycler.py @@ -0,0 +1,82 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for the Windows recycle files.""" + +from plaso.lib import errors +from plaso.formatters import interface + + +class WinRecyclerFormatter(interface.ConditionalEventFormatter): + """Formatter for Windows recycle bin events.""" + + DATA_TYPE = 'windows:metadata:deleted_item' + + DRIVE_LIST = { + 0x00: 'A', + 0x01: 'B', + 0x02: 'C', + 0x03: 'D', + 0x04: 'E', + 0x05: 'F', + 0x06: 'G', + 0x07: 'H', + 0x08: 'I', + 0x09: 'J', + 0x0A: 'K', + 0x0B: 'L', + 0x0C: 'M', + 0x0D: 'N', + 0x0E: 'O', + 0x0F: 'P', + 0x10: 'Q', + 0x11: 'R', + 0x12: 'S', + 0x13: 'T', + 0x14: 'U', + 0x15: 'V', + 0x16: 'W', + 0x17: 'X', + 0x18: 'Y', + 0x19: 'Z', + } + + # The format string. + FORMAT_STRING_PIECES = [ + u'DC{index} ->', + u'{orig_filename}', + u'[{orig_filename_legacy}]', + u'(from drive {drive_letter})'] + + FORMAT_STRING_SHORT_PIECES = [ + u'Deleted file: {orig_filename}'] + + SOURCE_LONG = 'Recycle Bin' + SOURCE_SHORT = 'RECBIN' + + def GetMessages(self, event_object): + """Return the message strings.""" + if self.DATA_TYPE != event_object.data_type: + raise errors.WrongFormatter('Unsupported data type: {0:s}.'.format( + event_object.data_type)) + + if hasattr(event_object, 'drive_number'): + event_object.drive_letter = self.DRIVE_LIST.get( + event_object.drive_number, 'C?') + + return super(WinRecyclerFormatter, self).GetMessages(event_object) + diff --git a/plaso/formatters/rubanetra.py b/plaso/formatters/rubanetra.py new file mode 100755 index 0000000..fff002b --- /dev/null +++ b/plaso/formatters/rubanetra.py @@ -0,0 +1,422 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This file contains formatters for the parsed Rubanetra events. Additionally, a Java Instant formatter was defined +as well.""" +from plaso.formatters import interface + +__author__ = 'Stefan Swerk (stefan_rubanetra@swerk.priv.at)' + + +class RubanetraBaseActivityFormatter(interface.ConditionalEventFormatter): + """ Formatter for a Rubanetra BaseActivity """ + + DATA_TYPE = 'java:rubanetra:base_activity' + SOURCE_SHORT = 'LOG' + SOURCE_LONG = 'at.jku.fim.rubanetra.BaseActivity' + + FORMAT_STRING_PIECES = [ + u'activityType: \'{activity_type}\'', + u'firstTimestamp: \'{first_timestamp}\'', + u'lastTimestamp: \'{last_timestamp}\'', + u'description: \'{description}\'', + u'sourceAddress: \'{source_address}\'', + u'destinationAddress: \'{destination_address}\'', + u'compoundFrameNumbers: \'{compound_frame_number_list}\'', + u'isReplaced: \'{replaced}\'', + u'optionalFields: \'{optional_field_dict}\''] + + +class RubanetraPcapActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:pcap_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.PcapActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES \ + + [u'totalSize: \'{pcap_total_size}\'', + u'frameNumber: \'{pcap_frame_number}\'', + u'wireLength: \'{pcap_packet_wirelen}\'', + u'headerCount: \'{pcap_header_count}\''] + + +class RubanetraHttpRequestActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:http_request_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.HttpRequestActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'serverAddress: \'{server_address}\'', + u'clientAddress: \'{client_address}\'', + u'httpVersion: \'{http_version}\'', + u'httpMethod: \'{http_method}\'', + u'httpQueryString: \'{http_query_string}\'', + u'httpQueryParameters: \'{http_query_parameters}\'', + u'httpRequestHeader: \'{http_request_header_dict}\'', + u'url: \'{url}\'', + u'originalHttpHeader: \'{orig_http_header}\'', + u'contentType: \'{content_type}\'', + u'isResponse: \'{is_response}\'', + u'JNetPcapHttpString: \'{jnetpcap_http_string}\''] + + +class RubanetraHttpResponseActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:http_response_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.HttpResponseActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'httpVersion: \'{http_version}\'', + u'httpStatusCode: \'{response_status_code}\'', + u'httpStatusLine: \'{response_status_line}\'', + u'httpResponseHeader: \'{response_header_dict}\'', + u'originalHttpHeader: \'{orig_http_header}\'', + u'contentType: \'{content_type}\'', + u'JNetPcapHttpString: \'{jnetpcap_http_string}\''] + + +class RubanetraDnsActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:dns_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.DnsActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'questionRecords: \'{question_record_list}\'', + u'answerRecords: \'{answer_record_list}\'', + u'authorityRecords: \'{authority_record_list}\'', + u'additionalRecords: \'{additional_record_list}\'', + u'dnsMessageHeader: \'{dns_message_header}\'', + u'isResponse: \'{is_response_bool}\''] + + +class RubanetraHttpImageActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:http_image_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.HttpImageActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'imageType: \'{image_type}\'', + u'imagePath: \'{image_path}\''] + + +class RubanetraArpActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:arp_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.ArpActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'hardwareType: \'{hardware_type}\'', + u'protocolType: \'{protocol_type}\'', + u'hardwareAddressLength: \'{hardware_address_length}\'', + u'protocolAddressLength: \'{protocol_address_length}\'', + u'senderHardwareAddress: \'{sender_mac_address}\'', + u'targetHardwareAddress: \'{target_mac_address}\'', + u'senderProtocolAddress: \'{sender_protocol_address}\'', + u'targetProtocolAddress: \'{target_protocol_address}\'', + u'JNetPcapArpString: \'{jnetpcap_arp}\''] + + +class RubanetraDhcpActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:dhcp_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.DhcpActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'dhcpMessage: \'{dhcp_message}\''] + + +class RubanetraEthernetActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:ethernet_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.EthernetActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'sourceMacAddress: \'{source_mac_address}\'', + u'destinationMacAddress: \'{destination_mac_address}\'', + u'ethernetType: \'{ethernet_type}\'', + u'ethernetTypeEnum: \'{ethernet_type_enum}\'', + u'JNetPcapEthernetString: \'{jnetpcap_ethernet}\''] + + +class RubanetraFtpActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:ftp_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.FtpActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'ftpActivityType: \'{ftp_type}\'', + u'command: \'{command}\'', + u'reply: \'{reply}\'', + u'list: \'{list}\''] + + +class RubanetraIcmpv4ActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:icmpv4_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.Icmpv4Activity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'icmpSubType: \'{icmp_subtype}\'', + u'icmpPacket: \'{icmp_packet}\'', + u'icmpMessage: \'{icmp_message}\'', + u'icmpType: \'{icmp_type}\'', + u'icmpCode: \'{icmp_code}\'', + u'sourceAddress: \'{source_address}\'', + u'destinationAddress: \'{destination_address}\'', + u'identifier: \'{identifier}\'', + u'sequence: \'{sequence}\'', + u'JNetPcapIcmpString: \'{jnetpcap_icmp}\''] + + +class RubanetraIcmpv6ActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:icmpv6_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.Icmpv6Activity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'icmpSubType: \'{icmp_subtype}\'', + u'icmpPacket: \'{icmp_packet}\'', + u'icmpMessage: \'{icmp_message}\'', + u'icmpType: \'{icmp_type}\'', + u'JNetPcapIcmpString: \'{jnetpcap_icmp}\''] + + +class RubanetraIpActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:ip_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.IpActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'version: \'{version}\'', + u'protocol: \'{protocol}\'', + u'sourceAddress: \'{source_address}\'', + u'destinationAddress: \'{destination_address}\''] + + +class RubanetraIpv4ActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:ipv4_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.Ipv4Activity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'internetHeaderLength: \'{internet_header_length}\'', + u'differentiatedServicesCodePoint: \'{differentiated_services_code_point}\'', + u'totalLength: \'{total_length}\'', + u'identification: \'{identification}\'', + u'flags: \'{flags}\'', + u'fragmentOffset: \'{fragment_offset}\'', + u'timeToLive: \'{time_to_live}\'', + u'headerChecksum: \'{header_checksum}\'', + u'options: \'{options}\'', + u'JNetPcapIpv4String: \'{jnetpcap_ip4}\''] + + +class RubanetraIpv6ActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:ipv6_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.Ipv6Activity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'trafficClass: \'{traffic_class}\'', + u'flowLabel: \'{flow_label}\'', + u'payloadLength: \'{payload_length}\'', + u'nextHeader: \'{next_header}\'', + u'hopLimit: \'{hop_limit}\'', + u'JNetPcapIpv6String: \'{jnetpcap_ip6}\'', + u'KrakenIpv6String: \'{kraken_ip6}\''] + + +class RubanetraMsnActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:msn_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.MsnActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'account: \'{account}\'', + u'chat: \'{chat}\''] + + +class RubanetraNetbiosActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:Netbios_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.NetbiosActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'datagramPacket: \'{datagram_packet}\'', + u'namePacket: \'{name_packet}\''] + + +class RubanetraPop3ActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:pop3_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.Pop3Activity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'subType: \'{sub_type}\'', + u'header: \'{header}\'', + u'data: \'{data}\'', + u'command: \'{command}\'', + u'response: \'{response}\''] + + +class RubanetraSmtpCommandActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:smtp_command_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.SmtpCommandActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'command: \'{command}\'', + u'parameter: \'{parameter}\''] + + +class RubanetraSmtpReplyActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:smtp_reply_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.SmtpReplyActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'code: \'{code}\'', + u'message: \'{message}\''] + + +class RubanetraSmtpSendActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:smtp_send_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.SmtpSendActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'header: \'{header}\'', + u'data: \'{data}\''] + + +class RubanetraSnmpv1ActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:snmpv1_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.Snmpv1Activity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'pdu: \'{pdu}\'', + u'sourceSocketAddress: \'{source_socket_address}\'', + u'destinationSocketAddress: \'{destination_socket_address}\''] + + +class RubanetraSnmpv2ActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:snmpv2_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.Snmpv2Activity' + + FORMAT_STRING_PIECES = RubanetraSnmpv1ActivityFormatter.FORMAT_STRING_PIECES + + +class RubanetraTcpActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:tcp_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.TcpActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'sourcePort: \'{source_port}\'', + u'destinationPort: \'{destination_port}\'', + u'sequenceNumber: \'{sequence_number}\'', + u'acknowledgeNumber: \'{acknowledge_number}\'', + u'relativeSequenceNumber: \'{relative_sequence_number}\'', + u'relativeAcknowledgeNumber: \'{relative_acknowledge_number}\'', + u'dataOffset: \'{data_offset}\'', + u'controlBits: \'{control_bits}\'', + u'windowSize: \'{window_size}\'', + u'checksum: \'{checksum}\'', + u'urgentPointer: \'{urgent_pointer}\'', + u'tcpLength: \'{tcp_length}\'', + u'options: \'{options}\'', + u'padding: \'{padding}\'', + u'syn: \'{syn}\'', + u'ack: \'{ack}\'', + u'psh: \'{psh}\'', + u'fin: \'{fin}\'', + u'rst: \'{rst}\'', + u'urg: \'{urg}\'', + u'direction: \'{direction}\'', + u'clientState: \'{client_state}\'', + u'serverState: \'{server_state}\'', + u'JNetPcapTcpString: \'{jnetpcap_tcp}\'', + u'sourceAddress: \'{source_address}\'', + u'destinationAddress: \'{destination_address}\'', + u'sourceSocketAddress: \'{source_socket_address}\'', + u'destinationSocketAddress: \'{destination_socket_address}\''] + + +class RubanetraTelnetActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:telnet_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.TelnetActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'subType: \'{sub_type}\'', + u'command: \'{command}\'', + u'option: \'{option}\'', + u'ansiMode: \'{ansi_mode}\'', + u'arguments: \'{arguments}\'', + u'text: \'{text}\'', + u'title: \'{title}\''] + + +class RubanetraTlsActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:tls_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.TlsActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'clientToServerTraffic: \'{client_to_server_traffic}\'', + u'serverToClientTraffic: \'{server_to_client_traffic}\''] + + +class RubanetraUdpActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:udp_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.UdpActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'sourcePort: \'{source_port}\'', + u'destinationPort: \'{destination_port}\'', + u'length: \'{length}\'', + u'checksum: \'{checksum}\'', + u'JNetPcapUdpString: \'{jnetpcap_udp}\'', + u'sourceSocketAddress: \'{source_socket_address}\'', + u'destinationSocketAddress: \'{destination_socket_address}\''] + + +class RubanetraOpenSSHActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:open_ssh_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.OpenSSHActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'clientToServerTraffic: \'{client_to_server_traffic}\'', + u'serverToClientTraffic: \'{server_to_client_traffic}\''] + + +class RubanetraDropboxTlsActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:dropbox_tls_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.DropboxActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'clientAddress: \'{client_address}\'', + u'serverAddress: \'{server_address}\''] + + +class RubanetraSpiderOakActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:spideroak_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.SpiderOakActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'clientAddress: \'{client_address}\'', + u'serverAddress: \'{server_address}\''] + + +class RubanetraSkypePayloadActivityFormatter(RubanetraBaseActivityFormatter): + DATA_TYPE = 'java:rubanetra:skype_payload_activity' + SOURCE_LONG = 'at.jku.fim.rubanetra.SkypePayloadActivity' + + FORMAT_STRING_PIECES = RubanetraBaseActivityFormatter.FORMAT_STRING_PIECES + \ + [u'sourceObjectId: \'{source_object_id}\'', + u'destinationObjectId: \'{destination_object_id}\'', + u'sourceHost: \'{source_host}\'', + u'destinationHost: \'{destination_host}\''] + + +class JavaInstantFormatter(interface.EventFormatter): + """ Formatter for a Java Instant """ + + DATA_TYPE = 'java:time:Instant' + SOURCE_SHORT = 'JAVA' + SOURCE_LONG = 'java.time.Instant' + + FORMAT_STRING = ( + u'epoch_seconds: \'{instant_epoch_seconds}, nano: \'{instant_nano}\'') + FORMAT_STRING_SHORT = (u'{instant_epoch_seconds}.{instant_nano}\'') diff --git a/plaso/formatters/safari.py b/plaso/formatters/safari.py new file mode 100644 index 0000000..bad4734 --- /dev/null +++ b/plaso/formatters/safari.py @@ -0,0 +1,33 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for the Safari History events.""" + +from plaso.formatters import interface + + +class SafariHistoryFormatter(interface.ConditionalEventFormatter): + """Formatter for Safari history events.""" + + DATA_TYPE = 'safari:history:visit' + + FORMAT_STRING_PIECES = [ + u'Visited: {url}', u'({title}', u'- {display_title}', ')', + 'Visit Count: {visit_count}'] + + SOURCE_LONG = 'Safari History' + SOURCE_SHORT = 'WEBHIST' diff --git a/plaso/formatters/selinux.py b/plaso/formatters/selinux.py new file mode 100644 index 0000000..e25afe8 --- /dev/null +++ b/plaso/formatters/selinux.py @@ -0,0 +1,34 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a selinux formatter in plaso.""" + +from plaso.formatters import interface + + +class SELinuxFormatter(interface.ConditionalEventFormatter): + """Formatter for selinux files.""" + + DATA_TYPE = 'selinux:line' + + FORMAT_STRING_SEPARATOR = u'' + + FORMAT_STRING_PIECES = [u'[', u'audit_type: {audit_type}', + u', pid: {pid}', u']', u' {body}'] + + SOURCE_LONG = 'Audit log File' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/shell_items.py b/plaso/formatters/shell_items.py new file mode 100644 index 0000000..96ba3eb --- /dev/null +++ b/plaso/formatters/shell_items.py @@ -0,0 +1,41 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for the shell item events.""" + +from plaso.formatters import interface + + +class ShellItemFileEntryEventFormatter(interface.ConditionalEventFormatter): + """Class that formats Windows volume creation events.""" + + DATA_TYPE = 'windows:shell_item:file_entry' + + FORMAT_STRING_PIECES = [ + u'Name: {name}', + u'Long name: {long_name}', + u'Localized name: {localized_name}', + u'NTFS file reference: {file_reference}', + u'Origin: {origin}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'Name: {name}', + u'NTFS file reference: {file_reference}', + u'Origin: {origin}'] + + SOURCE_LONG = 'File entry shell item' + SOURCE_SHORT = 'FILE' diff --git a/plaso/formatters/skydrivelog.py b/plaso/formatters/skydrivelog.py new file mode 100644 index 0000000..627f04f --- /dev/null +++ b/plaso/formatters/skydrivelog.py @@ -0,0 +1,36 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a skydrivelog formatter in plaso.""" + +from plaso.formatters import interface + + +class SkyDriveLogFormatter(interface.ConditionalEventFormatter): + """Formatter for SkyDrive log files events.""" + + DATA_TYPE = 'skydrive:log:line' + + FORMAT_STRING_PIECES = [ + u'[{source_code}]', + u'({log_level})', + u'{text}'] + + FORMAT_STRING_SHORT_PIECES = [u'{text}'] + + SOURCE_LONG = 'SkyDrive Log File' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/skydrivelogerr.py b/plaso/formatters/skydrivelogerr.py new file mode 100644 index 0000000..7c408f5 --- /dev/null +++ b/plaso/formatters/skydrivelogerr.py @@ -0,0 +1,37 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a skydrivelogerr formatter in plaso.""" + +from plaso.formatters import interface + + +class SkyDriveLogErrorFormatter(interface.ConditionalEventFormatter): + """Formatter for SkyDrive log error files events.""" + + DATA_TYPE = 'skydrive:error:line' + + FORMAT_STRING_PIECES = [ + u'[{module}', + u'{source_code}]', + u'{text}', + u'({detail})'] + + FORMAT_STRING_SHORT_PIECES = [u'{text}'] + + SOURCE_LONG = 'SkyDrive Error Log File' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/skype.py b/plaso/formatters/skype.py new file mode 100644 index 0000000..f0b1553 --- /dev/null +++ b/plaso/formatters/skype.py @@ -0,0 +1,88 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for the Skype Main database events.""" + +from plaso.formatters import interface + + +class SkypeAccountFormatter(interface.ConditionalEventFormatter): + """Formatter for Skype Account information.""" + + DATA_TYPE = 'skype:event:account' + + FORMAT_STRING_PIECES = [u'{username}', u'[{email}]', u'Country: {country}'] + + SOURCE_LONG = 'Skype Account' + SOURCE_SHORT = 'LOG' + + +class SkypeChatFormatter(interface.ConditionalEventFormatter): + """Formatter for Skype chat events.""" + + DATA_TYPE = 'skype:event:chat' + + FORMAT_STRING_PIECES = [ + u'From: {from_account}', + u'To: {to_account}', + u'[{title}]', + u'Message: [{text}]'] + + FORMAT_STRING_SHORT_PIECES = [u'From: {from_account}', u' To: {to_account}'] + + SOURCE_LONG = 'Skype Chat MSG' + SOURCE_SHORT = 'LOG' + + +class SkypeSMSFormatter(interface.ConditionalEventFormatter): + """Formatter for Skype SMS.""" + + DATA_TYPE = 'skype:event:sms' + + FORMAT_STRING_PIECES = [u'To: {number}', u'[{text}]'] + + SOURCE_LONG = 'Skype SMS' + SOURCE_SHORT = 'LOG' + + +class SkypeCallFormatter(interface.ConditionalEventFormatter): + """Formatter for Skype calls.""" + + DATA_TYPE = 'skype:event:call' + + FORMAT_STRING_PIECES = [ + u'From: {src_call}', + u'To: {dst_call}', + u'[{call_type}]'] + + SOURCE_LONG = 'Skype Call' + SOURCE_SHORT = 'LOG' + + +class SkypeTransferFileFormatter(interface.ConditionalEventFormatter): + """Formatter for Skype transfer files""" + + DATA_TYPE = 'skype:event:transferfile' + + FORMAT_STRING_PIECES = [ + u'Source: {source}', + u'Destination: {destination}', + u'File: {transferred_filename}', + u'[{action_type}]'] + + SOURCE_LONG = 'Skype Transfer Files' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/symantec.py b/plaso/formatters/symantec.py new file mode 100644 index 0000000..1d24e38 --- /dev/null +++ b/plaso/formatters/symantec.py @@ -0,0 +1,197 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a formatter for Symantec logs.""" + +from plaso.lib import errors +from plaso.formatters import interface + + +__author__ = 'David Nides (david.nides@gmail.com)' + + +class SymantecFormatter(interface.ConditionalEventFormatter): + """Define the formatting for Symantec events.""" + + DATA_TYPE = 'av:symantec:scanlog' + + EVENT_NAMES = { + '1': 'GL_EVENT_IS_ALERT', + '2': 'GL_EVENT_SCAN_STOP', + '3': 'GL_EVENT_SCAN_START', + '4': 'GL_EVENT_PATTERN_UPDATE', + '5': 'GL_EVENT_INFECTION', + '6': 'GL_EVENT_FILE_NOT_OPEN', + '7': 'GL_EVENT_LOAD_PATTERN', + '8': 'GL_STD_MESSAGE_INFO', + '9': 'GL_STD_MESSAGE_ERROR', + '10': 'GL_EVENT_CHECKSUM', + '11': 'GL_EVENT_TRAP', + '12': 'GL_EVENT_CONFIG_CHANGE', + '13': 'GL_EVENT_SHUTDOWN', + '14': 'GL_EVENT_STARTUP', + '16': 'GL_EVENT_PATTERN_DOWNLOAD', + '17': 'GL_EVENT_TOO_MANY_VIRUSES', + '18': 'GL_EVENT_FWD_TO_QSERVER', + '19': 'GL_EVENT_SCANDLVR', + '20': 'GL_EVENT_BACKUP', + '21': 'GL_EVENT_SCAN_ABORT', + '22': 'GL_EVENT_RTS_LOAD_ERROR', + '23': 'GL_EVENT_RTS_LOAD', + '24': 'GL_EVENT_RTS_UNLOAD', + '25': 'GL_EVENT_REMOVE_CLIENT', + '26': 'GL_EVENT_SCAN_DELAYED', + '27': 'GL_EVENT_SCAN_RESTART', + '28': 'GL_EVENT_ADD_SAVROAMCLIENT_TOSERVER', + '29': 'GL_EVENT_REMOVE_SAVROAMCLIENT_FROMSERVER', + '30': 'GL_EVENT_LICENSE_WARNING', + '31': 'GL_EVENT_LICENSE_ERROR', + '32': 'GL_EVENT_LICENSE_GRACE', + '33': 'GL_EVENT_UNAUTHORIZED_COMM', + '34': 'GL_EVENT_LOG_FWD_THRD_ERR', + '35': 'GL_EVENT_LICENSE_INSTALLED', + '36': 'GL_EVENT_LICENSE_ALLOCATED', + '37': 'GL_EVENT_LICENSE_OK', + '38': 'GL_EVENT_LICENSE_DEALLOCATED', + '39': 'GL_EVENT_BAD_DEFS_ROLLBACK', + '40': 'GL_EVENT_BAD_DEFS_UNPROTECTED', + '41': 'GL_EVENT_SAV_PROVIDER_PARSING_ERROR', + '42': 'GL_EVENT_RTS_ERROR', + '43': 'GL_EVENT_COMPLIANCE_FAIL', + '44': 'GL_EVENT_COMPLIANCE_SUCCESS', + '45': 'GL_EVENT_SECURITY_SYMPROTECT_POLICYVIOLATION', + '46': 'GL_EVENT_ANOMALY_START', + '47': 'GL_EVENT_DETECTION_ACTION_TAKEN', + '48': 'GL_EVENT_REMEDIATION_ACTION_PENDING', + '49': 'GL_EVENT_REMEDIATION_ACTION_FAILED', + '50': 'GL_EVENT_REMEDIATION_ACTION_SUCCESSFUL', + '51': 'GL_EVENT_ANOMALY_FINISH', + '52': 'GL_EVENT_COMMS_LOGIN_FAILED', + '53': 'GL_EVENT_COMMS_LOGIN_SUCCESS', + '54': 'GL_EVENT_COMMS_UNAUTHORIZED_COMM', + '55': 'GL_EVENT_CLIENT_INSTALL_AV', + '56': 'GL_EVENT_CLIENT_INSTALL_FW', + '57': 'GL_EVENT_CLIENT_UNINSTALL', + '58': 'GL_EVENT_CLIENT_UNINSTALL_ROLLBACK', + '59': 'GL_EVENT_COMMS_SERVER_GROUP_ROOT_CERT_ISSUE', + '60': 'GL_EVENT_COMMS_SERVER_CERT_ISSUE', + '61': 'GL_EVENT_COMMS_TRUSTED_ROOT_CHANGE', + '62': 'GL_EVENT_COMMS_SERVER_CERT_STARTUP_FAILED', + '63': 'GL_EVENT_CLIENT_CHECKIN', + '64': 'GL_EVENT_CLIENT_NO_CHECKIN', + '65': 'GL_EVENT_SCAN_SUSPENDED', + '66': 'GL_EVENT_SCAN_RESUMED', + '67': 'GL_EVENT_SCAN_DURATION_INSUFFICIENT', + '68': 'GL_EVENT_CLIENT_MOVE', + '69': 'GL_EVENT_SCAN_FAILED_ENHANCED', + '70': 'GL_EVENT_MAX_event_name', + '71': 'GL_EVENT_HEUR_THREAT_NOW_WHITELISTED', + '72': 'GL_EVENT_INTERESTING_PROCESS_DETECTED_START', + '73': 'GL_EVENT_LOAD_ERROR_COH', + '74': 'GL_EVENT_LOAD_ERROR_SYKNAPPS', + '75': 'GL_EVENT_INTERESTING_PROCESS_DETECTED_FINISH', + '76': 'GL_EVENT_HPP_SCAN_NOT_SUPPORTED_FOR_OS', + '77': 'GL_EVENT_HEUR_THREAT_NOW_KNOWN' + } + CATEGORY_NAMES = { + '1': 'GL_CAT_INFECTION', + '2': 'GL_CAT_SUMMARY', + '3': 'GL_CAT_PATTERN', + '4': 'GL_CAT_SECURITY' + } + ACTION_1_2_NAMES = { + '1': 'Quarantine infected file', + '2': 'Rename infected file', + '3': 'Delete infected file', + '4': 'Leave alone (log only)', + '5': 'Clean virus from file', + '6': 'Clean or delete macros' + } + ACTION_0_NAMES = { + '1': 'Quarantined', + '2': 'Renamed', + '3': 'Deleted', + '4': 'Left alone', + '5': 'Cleaned', + '6': ('Cleaned or macros deleted (no longer used as of ' + 'Symantec AntiVirus 9.x)'), + '7': 'Saved file as...', + '8': 'Sent to Intel (AMS)', + '9': 'Moved to backup location', + '10': 'Renamed backup file', + '11': 'Undo action in Quarantine View', + '12': 'Write protected or lack of permissions - Unable to act on file', + '13': 'Backed up file' + } + + # The identifier for the formatter (a regular expression) + FORMAT_STRING_SEPARATOR = u'; ' + FORMAT_STRING_PIECES = [ + u'Event Name: {event_map}', + u'Category Name: {category_map}', + u'Malware Name: {virus}', + u'Malware Path: {file}', + u'Action0: {action0_map}', + u'Action1: {action1_map}', + u'Action2: {action2_map}', + u'Description: {description}', + u'Scan ID: {scanid}', + u'Event Data: {event_data}', + u'Remote Machine: {remote_machine}', + u'Remote IP: {remote_machine_ip}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'{file}', + u'{virus}', + u'{action0_map}', + u'{action1_map}', + u'{action2_map}'] + + SOURCE_LONG = 'Symantec AV Log' + SOURCE_SHORT = 'LOG' + + def GetMessages(self, event_object): + """Returns a list of messages extracted from an event object. + + Args: + event_object: The event object (EventObject) containing the event + specific data. + + Returns: + A list that contains both the longer and shorter version of the message + string. + """ + if self.DATA_TYPE != event_object.data_type: + raise errors.WrongFormatter(u'Unsupported data type: {0:s}.'.format( + event_object.data_type)) + + if hasattr(event_object, 'event'): + event_object.event_map = self.EVENT_NAMES.get( + event_object.event, 'Unknown') + if hasattr(event_object, 'cat'): + event_object.category_map = self.CATEGORY_NAMES.get( + event_object.cat, 'Unknown') + if hasattr(event_object, 'action1'): + event_object.action1_map = self.ACTION_1_2_NAMES.get( + event_object.action1, 'Unknown') + if hasattr(event_object, 'action2'): + event_object.action2_map = self.ACTION_1_2_NAMES.get( + event_object.action2, 'Unknown') + if hasattr(event_object, 'action0'): + event_object.action0_map = self.ACTION_0_NAMES.get( + event_object.action0, 'Unknown') + return super(SymantecFormatter, self).GetMessages(event_object) diff --git a/plaso/formatters/syslog.py b/plaso/formatters/syslog.py new file mode 100644 index 0000000..e54b8be --- /dev/null +++ b/plaso/formatters/syslog.py @@ -0,0 +1,33 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a syslog formatter in plaso.""" + +from plaso.formatters import interface + + +class SyslogLineFormatter(interface.ConditionalEventFormatter): + """Formatter for syslog files.""" + + DATA_TYPE = 'syslog:line' + + FORMAT_STRING_SEPARATOR = u'' + + FORMAT_STRING_PIECES = [u'[', u'{reporter}', u', pid: {pid}', u'] {body}'] + + SOURCE_LONG = 'Log File' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/task_scheduler.py b/plaso/formatters/task_scheduler.py new file mode 100644 index 0000000..b614243 --- /dev/null +++ b/plaso/formatters/task_scheduler.py @@ -0,0 +1,36 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for Task Scheduler events.""" + +from plaso.formatters import interface + + +class TaskCacheEventFormatter(interface.ConditionalEventFormatter): + """Formatter for a generic Task Cache event.""" + + DATA_TYPE = 'task_scheduler:task_cache:entry' + + FORMAT_STRING_PIECES = [ + u'Task: {task_name}', + u'[Identifier: {task_identifier}]'] + + FORMAT_STRING_SHORT_PIECES = [ + u'Task: {task_name}'] + + SOURCE_LONG = 'Task Cache' + SOURCE_SHORT = 'REG' diff --git a/plaso/formatters/text.py b/plaso/formatters/text.py new file mode 100644 index 0000000..109d8ca --- /dev/null +++ b/plaso/formatters/text.py @@ -0,0 +1,30 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for text file-based events.""" + +from plaso.formatters import interface + + +class TextEventFormatter(interface.EventFormatter): + """Text event formatter.""" + + DATA_TYPE = u'text:entry' + FORMAT_STRING = u'{text}' + + SOURCE_SHORT = u'LOG' + SOURCE_LONG = u'Text File' diff --git a/plaso/formatters/utmp.py b/plaso/formatters/utmp.py new file mode 100644 index 0000000..c3b568f --- /dev/null +++ b/plaso/formatters/utmp.py @@ -0,0 +1,41 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for the UTMP binary files.""" + +from plaso.formatters import interface + + +class UtmpSessionFormatter(interface.ConditionalEventFormatter): + """Formatter for UTMP session.""" + + DATA_TYPE = 'linux:utmp:event' + + FORMAT_STRING_PIECES = [ + u'User: {user}', + u'Computer Name: {computer_name}', + u'Terminal: {terminal}', + u'PID: {pid}', + u'Terminal_ID: {terminal_id}', + u'Status: {status}', + u'IP Address: {ip_address}', + u'Exit: {exit}'] + + FORMAT_STRING_SHORT_PIECES = [u'User: {user}'] + + SOURCE_LONG = 'UTMP session' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/utmpx.py b/plaso/formatters/utmpx.py new file mode 100644 index 0000000..1e8ade3 --- /dev/null +++ b/plaso/formatters/utmpx.py @@ -0,0 +1,36 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for the UTMPX binary files.""" + +from plaso.formatters import interface + +class UtmpxSessionFormatter(interface.ConditionalEventFormatter): + """Formatter for UTMPX session.""" + + DATA_TYPE = 'mac:utmpx:event' + + FORMAT_STRING_PIECES = [ + u'User: {user}', + u'Status: {status}', + u'Computer Name: {computer_name}', + u'Terminal: {terminal}'] + + FORMAT_STRING_SHORT_PIECES = [u'User: {user}'] + + SOURCE_LONG = 'UTMPX session' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/windows.py b/plaso/formatters/windows.py new file mode 100644 index 0000000..fdd5adb --- /dev/null +++ b/plaso/formatters/windows.py @@ -0,0 +1,38 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for the Windows events.""" + +from plaso.formatters import interface + + +class WindowsVolumeCreationEventFormatter(interface.ConditionalEventFormatter): + """Class that formats Windows volume creation events.""" + + DATA_TYPE = 'windows:volume:creation' + + FORMAT_STRING_PIECES = [ + u'{device_path}', + u'Serial number: 0x{serial_number:08X}', + u'Origin: {origin}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'{device_path}', + u'Origin: {origin}'] + + SOURCE_LONG = 'System' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/winevt.py b/plaso/formatters/winevt.py new file mode 100644 index 0000000..5eef4ee --- /dev/null +++ b/plaso/formatters/winevt.py @@ -0,0 +1,113 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for Windows EventLog (EVT) files.""" + +from plaso.lib import errors +from plaso.formatters import interface + + +class WinEvtFormatter(interface.ConditionalEventFormatter): + """Define the formatting for Windows EventLog (EVT) record.""" + + DATA_TYPE = 'windows:evt:record' + + # TODO: add string representation of facility. + FORMAT_STRING_PIECES = [ + u'[{event_identifier} /', + u'0x{event_identifier:04x}]', + u'Severity: {severity_string}', + u'Record Number: {record_number}', + u'Event Type: {event_type_string}', + u'Event Category: {event_category}', + u'Source Name: {source_name}', + u'Computer Name: {computer_name}', + u'Strings: {strings}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'[{event_identifier} /', + u'0x{event_identifier:04x}]', + u'Strings: {strings}'] + + SOURCE_LONG = 'WinEVT' + SOURCE_SHORT = 'EVT' + + # Mapping of the numeric event types to a descriptive string. + _EVENT_TYPES = [ + u'Error event', + u'Warning event', + u'Information event', + u'Success Audit event', + u'Failure Audit event'] + + _SEVERITY = [ + u'Success', + u'Informational', + u'Warning', + u'Error'] + + def GetEventTypeString(self, event_type): + """Retrieves a string representation of the event type. + + Args: + event_type: The numeric event type. + + Returns: + An Unicode string containing a description of the event type. + """ + if event_type >= 0 and event_type < len(self._EVENT_TYPES): + return self._EVENT_TYPES[event_type] + return u'Unknown {0:d}'.format(event_type) + + def GetSeverityString(self, severity): + """Retrieves a string representation of the severity. + + Args: + severity: The numeric severity. + + Returns: + An Unicode string containing a description of the event type. + """ + if severity >= 0 and severity < len(self._SEVERITY): + return self._SEVERITY[severity] + return u'Unknown {0:d}'.format(severity) + + def GetMessages(self, event_object): + """Returns a list of messages extracted from an event object. + + Args: + event_object: The event object (EventObject) containing the event + specific data. + + Returns: + A list that contains both the longer and shorter version of the message + string. + """ + if self.DATA_TYPE != event_object.data_type: + raise errors.WrongFormatter(u'Unsupported data type: {0:s}.'.format( + event_object.data_type)) + + # Update event object with the event type string. + event_object.event_type_string = self.GetEventTypeString( + event_object.event_type) + + # TODO: add string representation of facility. + + # Update event object with the severity string. + event_object.severity_string = self.GetSeverityString(event_object.severity) + + return super(WinEvtFormatter, self).GetMessages(event_object) diff --git a/plaso/formatters/winevtx.py b/plaso/formatters/winevtx.py new file mode 100644 index 0000000..e667347 --- /dev/null +++ b/plaso/formatters/winevtx.py @@ -0,0 +1,41 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatters for Windows XML EventLog (EVTX) related events.""" +from plaso.formatters import interface + + +class WinEvtxFormatter(interface.ConditionalEventFormatter): + """Formatter for a Windows XML EventLog (EVTX) record.""" + DATA_TYPE = 'windows:evtx:record' + + FORMAT_STRING_PIECES = [ + u'[{event_identifier} /', + u'0x{event_identifier:04x}]', + u'Record Number: {record_number}', + u'Event Level: {event_level}', + u'Source Name: {source_name}', + u'Computer Name: {computer_name}', + u'Strings: {strings}', + u'XML string: {xml_strings}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'[{event_identifier} /', + u'0x{event_identifier:04x}]', + u'Strings: {strings}'] + + SOURCE_LONG = 'WinEVTX' + SOURCE_SHORT = 'EVT' diff --git a/plaso/formatters/winfirewall.py b/plaso/formatters/winfirewall.py new file mode 100644 index 0000000..875721a --- /dev/null +++ b/plaso/formatters/winfirewall.py @@ -0,0 +1,63 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for Windows firewall log files.""" + +from plaso.formatters import interface + + +class WinFirewallFormatter(interface.ConditionalEventFormatter): + """A formatter for Windows firewall log entries.""" + + DATA_TYPE = 'windows:firewall:log_entry' + + # TODO: Add more "elegant" formatting, as in transform ICMP code/type into + # a more human readable format as well as translating the additional info + # column (meaning may depend on action field). + FORMAT_STRING_PIECES = [ + u'{action}', + u'[', + u'{protocol}', + u'{path}', + u']', + u'From: {source_ip}', + u':{source_port}', + u'>', + u'{dest_ip}', + u':{dest_port}', + u'Size (bytes): {size}', + u'Flags [{flags}]', + u'TCP Seq Number: {tcp_seq}', + u'TCP ACK Number: {tcp_ack}', + u'TCP Window Size (bytes): {tcp_win}', + u'ICMP type: {icmp_type}', + u'ICMP code: {icmp_code}', + u'Additional info: {info}', + ] + + FORMAT_STRING_SHORT_PIECES = [ + u'{action}', + u'[{protocol}]', + u'{source_ip}', + u': {source_port}', + u'>', + u'{dest_ip}', + u': {dest_port}', + ] + + SOURCE_LONG = 'Windows Firewall Log' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/winjob.py b/plaso/formatters/winjob.py new file mode 100644 index 0000000..8a8a7a9 --- /dev/null +++ b/plaso/formatters/winjob.py @@ -0,0 +1,36 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for Windows Scheduled Task job events.""" + +from plaso.formatters import interface + + +class WinJobFormatter(interface.ConditionalEventFormatter): + """Formatter for a Java Cache IDX download item.""" + + DATA_TYPE = 'windows:tasks:job' + + FORMAT_STRING_PIECES = [ + u'Application: {application}', + u'{parameter}', + u'Scheduled by: {username}', + u'Working Directory: {working_dir}', + u'Run Iteration: {trigger}'] + + SOURCE_LONG = 'Windows Scheduled Task Job' + SOURCE_SHORT = 'JOB' diff --git a/plaso/formatters/winlnk.py b/plaso/formatters/winlnk.py new file mode 100644 index 0000000..c4d0529 --- /dev/null +++ b/plaso/formatters/winlnk.py @@ -0,0 +1,101 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for Windows Shortcut (LNK) files.""" + +from plaso.lib import errors +from plaso.formatters import interface + + +class WinLnkLinkFormatter(interface.ConditionalEventFormatter): + """Formatter for a Windows Shortcut (LNK) link event.""" + + DATA_TYPE = 'windows:lnk:link' + + FORMAT_STRING_PIECES = [ + u'[{description}]', + u'File size: {file_size}', + u'File attribute flags: 0x{file_attribute_flags:08x}', + u'Drive type: {drive_type}', + u'Drive serial number: 0x{drive_serial_number:08x}', + u'Volume label: {volume_label}', + u'Local path: {local_path}', + u'Network path: {network_path}', + u'cmd arguments: {command_line_arguments}', + u'env location: {env_var_location}', + u'Relative path: {relative_path}', + u'Working dir: {working_directory}', + u'Icon location: {icon_location}', + u'Link target: [{link_target}]'] + + FORMAT_STRING_SHORT_PIECES = [ + u'[{description}]', + u'{linked_path}', + u'{command_line_arguments}'] + + SOURCE_LONG = 'Windows Shortcut' + SOURCE_SHORT = 'LNK' + + def _GetLinkedPath(self, event_object): + """Determines the linked path. + + Args: + event_object: The event object (EventObject) containing the event + specific data. + + Returns: + A string containing the linked path. + """ + if hasattr(event_object, 'local_path'): + return event_object.local_path + + if hasattr(event_object, 'network_path'): + return event_object.network_path + + if hasattr(event_object, 'relative_path'): + paths = [] + if hasattr(event_object, 'working_directory'): + paths.append(event_object.working_directory) + paths.append(event_object.relative_path) + + return u'\\'.join(paths) + + return 'Unknown' + + def GetMessages(self, event_object): + """Returns a list of messages extracted from an event object. + + Args: + event_object: The event object (EventObject) containing the event + specific data. + + Returns: + A list that contains both the longer and shorter version of the message + string. + """ + if self.DATA_TYPE != event_object.data_type: + raise errors.WrongFormatter(u'Unsupported data type: {0:s}.'.format( + event_object.data_type)) + + # Update event object with a description if necessary. + if not hasattr(event_object, 'description'): + event_object.description = u'Empty description' + + # Update event object with the linked path. + event_object.linked_path = self._GetLinkedPath(event_object) + + return super(WinLnkLinkFormatter, self).GetMessages(event_object) diff --git a/plaso/formatters/winprefetch.py b/plaso/formatters/winprefetch.py new file mode 100644 index 0000000..b0d12a9 --- /dev/null +++ b/plaso/formatters/winprefetch.py @@ -0,0 +1,76 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for the Windows Prefetch events.""" + +from plaso.lib import errors +from plaso.formatters import interface + + +class WinPrefetchExecutionFormatter(interface.ConditionalEventFormatter): + """Class that formats Windows Prefetch execution events.""" + + DATA_TYPE = 'windows:prefetch:execution' + + FORMAT_STRING_PIECES = [ + u'Prefetch', + u'[{executable}] was executed -', + u'run count {run_count}', + u'path: {path}', + u'hash: 0x{prefetch_hash:08X}', + u'{volumes_string}'] + + FORMAT_STRING_SHORT_PIECES = [ + u'{executable} was run', + u'{run_count} time(s)'] + + SOURCE_LONG = 'WinPrefetch' + SOURCE_SHORT = 'LOG' + + def GetMessages(self, event_object): + """Returns a list of messages extracted from an event object. + + Args: + event_object: The event object (instance of EventObject) containing + the event specific data. + + Returns: + A list that contains both the longer and shorter version of the message + string. + + Raises: + WrongFormatter: when the data type of the formatter does not match + that of the event object. + """ + if self.DATA_TYPE != event_object.data_type: + raise errors.WrongFormatter( + u'Invalid event object - unsupported data type: {0:s}'.format( + event_object.data_type)) + + volumes_strings = [] + for volume_index in range(0, event_object.number_of_volumes): + volumes_strings.append(( + u'volume: {0:d} [serial number: 0x{1:08X}, device path: ' + u'{2:s}]').format( + volume_index + 1, + event_object.volume_serial_numbers[volume_index], + event_object.volume_device_paths[volume_index])) + + if volumes_strings: + event_object.volumes_string = u', '.join(volumes_strings) + + return super(WinPrefetchExecutionFormatter, self).GetMessages(event_object) diff --git a/plaso/formatters/winreg.py b/plaso/formatters/winreg.py new file mode 100644 index 0000000..148d6b5 --- /dev/null +++ b/plaso/formatters/winreg.py @@ -0,0 +1,81 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for Windows NT Registry (REGF) files.""" + +from plaso.lib import errors +from plaso.formatters import interface + + +class WinRegistryGenericFormatter(interface.EventFormatter): + """Formatter for a generic Windows Registry key or value.""" + + DATA_TYPE = 'windows:registry:key_value' + + FORMAT_STRING = u'[{keyname}] {text}' + FORMAT_STRING_ALTERNATIVE = u'{text}' + + SOURCE_LONG = 'Registry Key' + SOURCE_SHORT = 'REG' + + def GetMessages(self, event_object): + """Returns a list of messages extracted from an event object. + + Args: + event_object: The event object (EventObject) containing the event + specific data. + + Returns: + A list that contains both the longer and shorter version of the message + string. + """ + if self.DATA_TYPE != event_object.data_type: + raise errors.WrongFormatter(u'Unsupported data type: {0:s}.'.format( + event_object.data_type)) + + regvalue = getattr(event_object, 'regvalue', {}) + + string_parts = [] + for key, value in sorted(regvalue.items()): + string_parts.append(u'{0:s}: {1!s}'.format(key, value)) + + text = u' '.join(string_parts) + + event_object.text = text + if hasattr(event_object, 'keyname'): + self.format_string = self.FORMAT_STRING + else: + self.format_string = self.FORMAT_STRING_ALTERNATIVE + + return super(WinRegistryGenericFormatter, self).GetMessages(event_object) + + def GetSources(self, event_object): + """Returns a list of source short and long messages for the event.""" + if self.DATA_TYPE != event_object.data_type: + raise errors.WrongFormatter(u'Unsupported data type: {0:s}.'.format( + event_object.data_type)) + + self.source_string = getattr(event_object, 'source_long', None) + + if not self.source_string: + registry_type = getattr(event_object, 'registry_type', 'UNKNOWN') + self.source_string = u'{0:s} key'.format(registry_type) + + if hasattr(event_object, 'source_append'): + self.source_string += u' {0:s}'.format(event_object.source_append) + + return super(WinRegistryGenericFormatter, self).GetSources(event_object) diff --git a/plaso/formatters/winregservice.py b/plaso/formatters/winregservice.py new file mode 100644 index 0000000..3b2e2c8 --- /dev/null +++ b/plaso/formatters/winregservice.py @@ -0,0 +1,58 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Formatter for service entries derived from Windows Registry files.""" + +from plaso.formatters import winreg +from plaso.winnt import human_readable_service_enums + + +class WinRegistryServiceFormatter(winreg.WinRegistryGenericFormatter): + """Formatter for a Windows service event extracted from the Registry.""" + + DATA_TYPE = 'windows:registry:service' + + def GetMessages(self, event_object): + """Returns a list of messages extracted from the event object. + + This formatter will make the values of certain service parameters more + readable by humans. + + Args: + event_object: The event object (an instance of EventObject) containing + the event specific data. + + Returns: + A list that contains both the longer and shorter version of the message + string. + """ + regvalue = getattr(event_object, 'regvalue', {}) + # Loop over all the registry value names in the service key. + for service_value_name in regvalue.keys(): + # A temporary variable so we can refer to this long name more easily. + service_enums = human_readable_service_enums.SERVICE_ENUMS + # Check if we need to can make the value more human readable. + if service_value_name in service_enums.keys(): + service_enum = service_enums[service_value_name] + # Find the human readable version of the name and fall back to the + # raw value if it's not found. + human_readable_value = service_enum.get( + regvalue[service_value_name], + regvalue[service_value_name]) + regvalue[service_value_name] = human_readable_value + + return super(WinRegistryServiceFormatter, self).GetMessages(event_object) diff --git a/plaso/formatters/xchatlog.py b/plaso/formatters/xchatlog.py new file mode 100644 index 0000000..95ef24d --- /dev/null +++ b/plaso/formatters/xchatlog.py @@ -0,0 +1,31 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a xchatlog formatter in plaso.""" + +from plaso.formatters import interface + + +class XChatLogFormatter(interface.ConditionalEventFormatter): + """Formatter for XChat log files.""" + + DATA_TYPE = 'xchat:log:line' + + FORMAT_STRING_PIECES = [u'[nickname: {nickname}]', u'{text}'] + + SOURCE_LONG = 'XChat Log File' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/xchatscrollback.py b/plaso/formatters/xchatscrollback.py new file mode 100644 index 0000000..e6188dc --- /dev/null +++ b/plaso/formatters/xchatscrollback.py @@ -0,0 +1,33 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a xchatscrollback formatter in plaso.""" + +from plaso.formatters import interface + + +class XChatScrollbackFormatter(interface.ConditionalEventFormatter): + """Formatter for XChat scrollback files.""" + + DATA_TYPE = 'xchat:scrollback:line' + + FORMAT_STRING_SEPARATOR = u'' + + FORMAT_STRING_PIECES = [u'[', u'nickname: {nickname}', u']', u' {text}'] + + SOURCE_LONG = 'XChat Scrollback File' + SOURCE_SHORT = 'LOG' diff --git a/plaso/formatters/zeitgeist.py b/plaso/formatters/zeitgeist.py new file mode 100644 index 0000000..9dca044 --- /dev/null +++ b/plaso/formatters/zeitgeist.py @@ -0,0 +1,31 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a formatter for Zeitgeist.""" + +from plaso.formatters import interface + + +class ZeitgeistEventFormatter(interface.EventFormatter): + """The event formatter for Zeitgeist event.""" + + DATA_TYPE = 'zeitgeist:activity' + + FORMAT_STRING = u'{subject_uri}' + + SOURCE_LONG = 'Zeitgeist activity log' + SOURCE_SHORT = 'LOG' diff --git a/plaso/frontend/__init__.py b/plaso/frontend/__init__.py new file mode 100755 index 0000000..1f5c4b3 --- /dev/null +++ b/plaso/frontend/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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/plaso/frontend/frontend.py b/plaso/frontend/frontend.py new file mode 100755 index 0000000..80098a2 --- /dev/null +++ b/plaso/frontend/frontend.py @@ -0,0 +1,1693 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The common front-end functionality.""" + +import abc +import locale +import logging +import os +import pdb +import sys +import traceback + +from dfvfs.helpers import source_scanner +from dfvfs.lib import definitions as dfvfs_definitions +from dfvfs.lib import errors as dfvfs_errors +from dfvfs.resolver import context +from dfvfs.volume import tsk_volume_system +from dfvfs.volume import vshadow_volume_system + +import plaso +from plaso import parsers # pylint: disable=unused-import +from plaso.engine import single_process +from plaso.engine import utils as engine_utils +from plaso.engine import worker +from plaso.lib import errors +from plaso.lib import event +from plaso.lib import pfilter +from plaso.lib import storage +from plaso.lib import timelib +from plaso.multi_processing import multi_process +from plaso.parsers import manager as parsers_manager + +import pytz + + +class FrontendInputReader(object): + """Class that implements the input reader interface for the engine.""" + + @abc.abstractmethod + def Read(self): + """Reads a string from the input. + + Returns: + A string containing the input. + """ + + +class FrontendOutputWriter(object): + """Class that implements the output writer interface for the engine.""" + + @abc.abstractmethod + def Write(self, string): + """Writes a string to the output. + + Args: + string: A string containing the output. + """ + + +class StdinFrontendInputReader(object): + """Class that implements a stdin input reader.""" + + def Read(self): + """Reads a string from the input. + + Returns: + A string containing the input. + """ + return sys.stdin.readline() + + +class StdoutFrontendOutputWriter(object): + """Class that implements a stdout output writer.""" + + ENCODING = u'utf-8' + + def Write(self, string): + """Writes a string to the output. + + Args: + string: A string containing the output. + """ + try: + sys.stdout.write(string.encode(self.ENCODING)) + except UnicodeEncodeError: + logging.error( + u'Unable to properly write output, line will be partially ' + u'written out.') + sys.stdout.write(u'LINE ERROR') + sys.stdout.write(string.encode(self.ENCODING, 'ignore')) + + +class Frontend(object): + """Class that implements a front-end.""" + + # The maximum length of the line in number of characters. + _LINE_LENGTH = 80 + + def __init__(self, input_reader, output_writer): + """Initializes the front-end object. + + Args: + input_reader: the input reader (instance of FrontendInputReader). + The default is None which indicates to use the stdin + input reader. + output_writer: the output writer (instance of FrontendOutputWriter). + The default is None which indicates to use the stdout + output writer. + """ + super(Frontend, self).__init__() + self._input_reader = input_reader + self._output_writer = output_writer + + # TODO: add preferred_encoding support of the output writer. + self.preferred_encoding = locale.getpreferredencoding().lower() + + def PrintColumnValue(self, name, description, column_length=25): + """Prints a value with a name and description aligned to the column length. + + Args: + name: The name. + description: The description. + column_length: Optional column length. The default is 25. + """ + line_length = self._LINE_LENGTH - column_length - 3 + + # The format string of the first line of the column value. + primary_format_string = u'{{0:>{0:d}s}} : {{1:s}}\n'.format(column_length) + + # The format string of successive lines of the column value. + secondary_format_string = u'{{0:<{0:d}s}}{{1:s}}\n'.format( + column_length + 3) + + if len(description) < line_length: + self._output_writer.Write(primary_format_string.format(name, description)) + return + + # Split the description in words. + words = description.split() + + current = 0 + + lines = [] + word_buffer = [] + for word in words: + current += len(word) + 1 + if current >= line_length: + current = len(word) + lines.append(u' '.join(word_buffer)) + word_buffer = [word] + else: + word_buffer.append(word) + lines.append(u' '.join(word_buffer)) + + # Print the column value on multiple lines. + self._output_writer.Write(primary_format_string.format(name, lines[0])) + for line in lines[1:]: + self._output_writer.Write(secondary_format_string.format(u'', line)) + + def PrintHeader(self, text, character='*'): + """Prints the header as a line with centered text. + + Args: + text: The header text. + character: Optional header line character. The default is '*'. + """ + self._output_writer.Write(u'\n') + + format_string = u'{{0:{0:s}^{1:d}}}\n'.format(character, self._LINE_LENGTH) + header_string = format_string.format(u' {0:s} '.format(text)) + self._output_writer.Write(header_string) + + def PrintSeparatorLine(self): + """Prints a separator line.""" + self._output_writer.Write(u'{0:s}\n'.format(u'-' * self._LINE_LENGTH)) + + +class StorageMediaFrontend(Frontend): + """Class that implements a front-end with storage media support.""" + + # For context see: http://en.wikipedia.org/wiki/Byte + _UNITS_1000 = ['B', 'kB', 'MB', 'GB', 'TB', 'EB', 'ZB', 'YB'] + _UNITS_1024 = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'EiB', 'ZiB', 'YiB'] + + def __init__(self, input_reader, output_writer): + """Initializes the front-end object. + + Args: + input_reader: the input reader (instance of FrontendInputReader). + The default is None which indicates to use the stdin + input reader. + output_writer: the output writer (instance of FrontendOutputWriter). + The default is None which indicates to use the stdout + output writer. + """ + super(StorageMediaFrontend, self).__init__(input_reader, output_writer) + self._partition_offset = None + self._process_vss = True + self._resolver_context = context.Context() + self._scan_context = source_scanner.SourceScannerContext() + self._source_path = None + self._source_scanner = source_scanner.SourceScanner() + self._vss_stores = None + + def _GetHumanReadableSize(self, size): + """Retrieves a human readable string of the size. + + Args: + size: The size in bytes. + + Returns: + A human readable string of the size. + """ + magnitude_1000 = 0 + size_1000 = float(size) + while size_1000 >= 1000: + size_1000 /= 1000 + magnitude_1000 += 1 + + magnitude_1024 = 0 + size_1024 = float(size) + while size_1024 >= 1024: + size_1024 /= 1024 + magnitude_1024 += 1 + + size_string_1000 = None + if magnitude_1000 > 0 and magnitude_1000 <= 7: + size_string_1000 = u'{0:.1f}{1:s}'.format( + size_1000, self._UNITS_1000[magnitude_1000]) + + size_string_1024 = None + if magnitude_1024 > 0 and magnitude_1024 <= 7: + size_string_1024 = u'{0:.1f}{1:s}'.format( + size_1024, self._UNITS_1024[magnitude_1024]) + + if not size_string_1000 or not size_string_1024: + return u'{0:d} B'.format(size) + + return u'{0:s} / {1:s} ({2:d} B)'.format( + size_string_1024, size_string_1000, size) + + def _GetPartionIdentifierFromUser(self, volume_system, volume_identifiers): + """Asks the user to provide the partitioned volume identifier. + + Args: + volume_system: The volume system (instance of dfvfs.TSKVolumeSystem). + volume_identifiers: List of allowed volume identifiers. + + Raises: + FileSystemScannerError: if the source cannot be processed. + """ + self._output_writer.Write( + u'The following partitions were found:\n' + u'Identifier\tOffset (in bytes)\tSize (in bytes)\n') + + for volume_identifier in volume_identifiers: + volume = volume_system.GetVolumeByIdentifier(volume_identifier) + if not volume: + raise errors.FileSystemScannerError( + u'Volume missing for identifier: {0:s}.'.format(volume_identifier)) + + volume_extent = volume.extents[0] + self._output_writer.Write( + u'{0:s}\t\t{1:d} (0x{1:08x})\t{2:s}\n'.format( + volume.identifier, volume_extent.offset, + self._GetHumanReadableSize(volume_extent.size))) + + self._output_writer.Write(u'\n') + + while True: + self._output_writer.Write( + u'Please specify the identifier of the partition that should ' + u'be processed:\nNote that you can abort with Ctrl^C.\n') + + selected_volume_identifier = self._input_reader.Read() + selected_volume_identifier = selected_volume_identifier.strip() + + if selected_volume_identifier in volume_identifiers: + break + + self._output_writer.Write( + u'\n' + u'Unsupported partition identifier, please try again or abort ' + u'with Ctrl^C.\n' + u'\n') + + return selected_volume_identifier + + def _GetVolumeTSKPartition( + self, scan_context, partition_number=None, partition_offset=None): + """Determines the volume path specification. + + Args: + scan_context: the scan context (instance of dfvfs.ScanContext). + partition_number: Optional preferred partition number. The default is + None. + partition_offset: Optional preferred partition byte offset. The default + is None. + + Returns: + The volume scan node (instance of dfvfs.SourceScanNode) or None + if no supported partition was found. + + Raises: + SourceScannerError: if the format of or within the source + is not supported or the the scan context + is invalid. + RuntimeError: if the volume for a specific identifier cannot be + retrieved. + """ + if (not scan_context or not scan_context.last_scan_node or + not scan_context.last_scan_node.path_spec): + raise errors.SourceScannerError(u'Invalid scan context.') + + volume_system = tsk_volume_system.TSKVolumeSystem() + volume_system.Open(scan_context.last_scan_node.path_spec) + + volume_identifiers = self._source_scanner.GetVolumeIdentifiers( + volume_system) + if not volume_identifiers: + logging.info(u'No supported partitions found.') + return + + if partition_number is not None and partition_number > 0: + # Plaso uses partition numbers starting with 1 while dfvfs expects + # the volume index to start with 0. + volume = volume_system.GetVolumeByIndex(partition_number - 1) + if volume: + volume_location = u'/{0:s}'.format(volume.identifier) + volume_scan_node = scan_context.last_scan_node.GetSubNodeByLocation( + volume_location) + if not volume_scan_node: + raise RuntimeError( + u'Unable to retrieve volume scan node by location: {0:s}'.format( + volume_location)) + return volume_scan_node + + logging.warning(u'No such partition: {0:d}.'.format(partition_number)) + + if partition_offset is not None: + for volume in volume_system.volumes: + volume_extent = volume.extents[0] + if volume_extent.offset == partition_offset: + volume_location = u'/{0:s}'.format(volume.identifier) + volume_scan_node = scan_context.last_scan_node.GetSubNodeByLocation( + volume_location) + if not volume_scan_node: + raise RuntimeError(( + u'Unable to retrieve volume scan node by location: ' + u'{0:s}').format(volume_location)) + return volume_scan_node + + logging.warning( + u'No such partition with offset: {0:d} (0x{0:08x}).'.format( + partition_offset)) + + if len(volume_identifiers) == 1: + volume_location = u'/{0:s}'.format(volume_identifiers[0]) + + else: + try: + selected_volume_identifier = self._GetPartionIdentifierFromUser( + volume_system, volume_identifiers) + except KeyboardInterrupt: + raise errors.UserAbort(u'File system scan aborted.') + + volume = volume_system.GetVolumeByIdentifier(selected_volume_identifier) + if not volume: + raise RuntimeError( + u'Unable to retrieve volume by identifier: {0:s}'.format( + selected_volume_identifier)) + + volume_location = u'/{0:s}'.format(selected_volume_identifier) + + volume_scan_node = scan_context.last_scan_node.GetSubNodeByLocation( + volume_location) + if not volume_scan_node: + raise RuntimeError( + u'Unable to retrieve volume scan node by location: {0:s}'.format( + volume_location)) + return volume_scan_node + + def _GetVolumeVssStoreIdentifiers(self, scan_context, vss_stores=None): + """Determines the VSS store identifiers. + + Args: + scan_context: the scan context (instance of dfvfs.ScanContext). + vss_stores: Optional list of preferred VSS stored identifiers. The + default is None. + + Raises: + SourceScannerError: if the format of or within the source + is not supported or the the scan context + is invalid. + """ + if (not scan_context or not scan_context.last_scan_node or + not scan_context.last_scan_node.path_spec): + raise errors.SourceScannerError(u'Invalid scan context.') + + volume_system = vshadow_volume_system.VShadowVolumeSystem() + volume_system.Open(scan_context.last_scan_node.path_spec) + + volume_identifiers = self._source_scanner.GetVolumeIdentifiers( + volume_system) + if not volume_identifiers: + return + + try: + self._vss_stores = self._GetVssStoreIdentifiersFromUser( + volume_system, volume_identifiers, vss_stores=vss_stores) + except KeyboardInterrupt: + raise errors.UserAbort(u'File system scan aborted.') + + return + + def _GetVssStoreIdentifiersFromUser( + self, volume_system, volume_identifiers, vss_stores=None): + """Asks the user to provide the VSS store identifiers. + + Args: + volume_system: The volume system (instance of dfvfs.VShadowVolumeSystem). + volume_identifiers: List of allowed volume identifiers. + vss_stores: Optional list of preferred VSS stored identifiers. The + default is None. + + Returns: + The list of selected VSS store identifiers or None. + + Raises: + SourceScannerError: if the source cannot be processed. + """ + normalized_volume_identifiers = [] + for volume_identifier in volume_identifiers: + volume = volume_system.GetVolumeByIdentifier(volume_identifier) + if not volume: + raise errors.SourceScannerError( + u'Volume missing for identifier: {0:s}.'.format(volume_identifier)) + + try: + volume_identifier = int(volume.identifier[3:], 10) + normalized_volume_identifiers.append(volume_identifier) + except ValueError: + pass + + if vss_stores: + if len(vss_stores) == 1 and vss_stores[0] == 'all': + # We need to set the stores to cover all vss stores. + vss_stores = range(1, volume_system.number_of_volumes + 1) + + if not set(vss_stores).difference( + normalized_volume_identifiers): + return vss_stores + + print_header = True + while True: + if print_header: + self._output_writer.Write( + u'The following Volume Shadow Snapshots (VSS) were found:\n' + u'Identifier\tVSS store identifier\tCreation Time\n') + + for volume_identifier in volume_identifiers: + volume = volume_system.GetVolumeByIdentifier(volume_identifier) + if not volume: + raise errors.SourceScannerError( + u'Volume missing for identifier: {0:s}.'.format( + volume_identifier)) + + vss_identifier = volume.GetAttribute('identifier') + vss_creation_time = volume.GetAttribute('creation_time') + vss_creation_time = timelib.Timestamp.FromFiletime( + vss_creation_time.value) + vss_creation_time = timelib.Timestamp.CopyToIsoFormat( + vss_creation_time) + self._output_writer.Write(u'{0:s}\t\t{1:s}\t{2:s}\n'.format( + volume.identifier, vss_identifier.value, vss_creation_time)) + + self._output_writer.Write(u'\n') + + print_header = False + + self._output_writer.Write( + u'Please specify the identifier(s) of the VSS that should be ' + u'processed:\nNote that a range of stores can be defined as: 3..5. ' + u'Multiple stores can\nbe defined as: 1,3,5 (a list of comma ' + u'separated values). Ranges and lists can\nalso be combined ' + u'as: 1,3..5. The first store is 1. If no stores are specified\n' + u'none will be processed. You can abort with Ctrl^C.\n') + + selected_vss_stores = self._input_reader.Read() + + selected_vss_stores = selected_vss_stores.strip() + if not selected_vss_stores: + break + + try: + selected_vss_stores = self._ParseVssStores(selected_vss_stores) + except errors.BadConfigOption: + selected_vss_stores = [] + + if not set(selected_vss_stores).difference(normalized_volume_identifiers): + break + + self._output_writer.Write( + u'\n' + u'Unsupported VSS identifier(s), please try again or abort with ' + u'Ctrl^C.\n' + u'\n') + + return selected_vss_stores + + def _ParseVssStores(self, vss_stores): + """Parses the user specified VSS stores stirng. + + Args: + vss_stores: a string containing the VSS stores. + Where 1 represents the first store. + + Returns: + The list of VSS stores. + + Raises: + BadConfigOption: if the VSS stores option is invalid. + """ + if not vss_stores: + return [] + + if vss_stores == 'all': + # We want to process all the VSS stores. + return ['all'] + + stores = [] + for vss_store_range in vss_stores.split(','): + # Determine if the range is formatted as 1..3 otherwise it indicates + # a single store number. + if '..' in vss_store_range: + first_store, last_store = vss_store_range.split('..') + try: + first_store = int(first_store, 10) + last_store = int(last_store, 10) + except ValueError: + raise errors.BadConfigOption( + u'Invalid VSS store range: {0:s}.'.format(vss_store_range)) + + for store_number in range(first_store, last_store + 1): + if store_number not in stores: + stores.append(store_number) + else: + try: + store_number = int(vss_store_range, 10) + except ValueError: + raise errors.BadConfigOption( + u'Invalid VSS store range: {0:s}.'.format(vss_store_range)) + + if store_number not in stores: + stores.append(store_number) + + return sorted(stores) + + def AddImageOptions(self, argument_group): + """Adds the storage media image options to the argument group. + + Args: + argument_group: The argparse argument group (instance of + argparse._ArgumentGroup). + """ + argument_group.add_argument( + '-o', '--offset', dest='image_offset', action='store', default=None, + type=int, help=( + u'The offset of the volume within the storage media image in ' + u'number of sectors. A sector is 512 bytes in size by default ' + u'this can be overwritten with the --sector_size option.')) + + argument_group.add_argument( + '--sector_size', '--sector-size', dest='bytes_per_sector', + action='store', type=int, default=512, help=( + u'The number of bytes per sector, which is 512 by default.')) + + argument_group.add_argument( + '--ob', '--offset_bytes', '--offset_bytes', dest='image_offset_bytes', + action='store', default=None, type=int, help=( + u'The offset of the volume within the storage media image in ' + u'number of bytes.')) + + def AddVssProcessingOptions(self, argument_group): + """Adds the VSS processing options to the argument group. + + Args: + argument_group: The argparse argument group (instance of + argparse._ArgumentGroup). + """ + argument_group.add_argument( + '--no_vss', '--no-vss', dest='no_vss', action='store_true', + default=False, help=( + u'Do not scan for Volume Shadow Snapshots (VSS). This means that ' + u'VSS information will not be included in the extraction phase.')) + + argument_group.add_argument( + '--vss_stores', '--vss-stores', dest='vss_stores', action='store', + type=str, default=None, help=( + u'Define Volume Shadow Snapshots (VSS) (or stores that need to be ' + u'processed. A range of stores can be defined as: \'3..5\'. ' + u'Multiple stores can be defined as: \'1,3,5\' (a list of comma ' + u'separated values). Ranges and lists can also be combined as: ' + u'\'1,3..5\'. The first store is 1.')) + + # TODO: remove this when support to handle multiple partitions is added. + def GetSourcePathSpec(self): + """Retrieves the source path specification. + + Returns: + The source path specification (instance of dfvfs.PathSpec) or None. + """ + if self._scan_context and self._scan_context.last_scan_node: + return self._scan_context.last_scan_node.path_spec + + def ParseOptions(self, options, source_option='source'): + """Parses the options and initializes the front-end. + + Args: + options: the command line arguments (instance of argparse.Namespace). + source_option: optional name of the source option. The default is source. + + Raises: + BadConfigOption: if the options are invalid. + """ + if not options: + raise errors.BadConfigOption(u'Missing options.') + + self._source_path = getattr(options, source_option, None) + if not self._source_path: + raise errors.BadConfigOption(u'Missing source path.') + + if isinstance(self._source_path, str): + encoding = sys.stdin.encoding + + # Note that sys.stdin.encoding can be None. + if not encoding: + encoding = self.preferred_encoding + + # Note that the source path option can be an encoded byte string + # and we need to turn it into an Unicode string. + try: + self._source_path = unicode( + self._source_path.decode(encoding)) + except UnicodeDecodeError as exception: + raise errors.BadConfigOption(( + u'Unable to convert source path to Unicode with error: ' + u'{0:s}.').format(exception)) + + elif not isinstance(self._source_path, unicode): + raise errors.BadConfigOption( + u'Unsupported source path, string type required.') + + self._source_path = os.path.abspath(self._source_path) + + def ScanSource(self, options): + """Scans the source path for volume and file systems. + + This functions sets the internal source path specification and source + type values. + + Args: + options: the command line arguments (instance of argparse.Namespace). + + Raises: + SourceScannerError: if the format of or within the source + is not supported or the the scan context + is invalid. + """ + partition_number = getattr(options, 'partition_number', None) + if (partition_number is not None and + isinstance(partition_number, basestring)): + try: + partition_number = int(partition_number, 10) + except ValueError: + logging.warning(u'Invalid partition number: {0:s}.'.format( + partition_number)) + partition_number = None + + partition_offset = getattr(options, 'image_offset_bytes', None) + if (partition_offset is not None and + isinstance(partition_offset, basestring)): + try: + partition_offset = int(partition_offset, 10) + except ValueError: + logging.warning(u'Invalid image offset bytes: {0:s}.'.format( + partition_offset)) + partition_offset = None + + if partition_offset is None and hasattr(options, 'image_offset'): + image_offset = getattr(options, 'image_offset') + bytes_per_sector = getattr(options, 'bytes_per_sector', 512) + + if isinstance(image_offset, basestring): + try: + image_offset = int(image_offset, 10) + except ValueError: + logging.warning(u'Invalid image offset: {0:s}.'.format(image_offset)) + image_offset = None + + if isinstance(bytes_per_sector, basestring): + try: + bytes_per_sector = int(bytes_per_sector, 10) + except ValueError: + logging.warning(u'Invalid bytes per sector: {0:s}.'.format( + bytes_per_sector)) + bytes_per_sector = 512 + + if image_offset: + partition_offset = image_offset * bytes_per_sector + + self._process_vss = not getattr(options, 'no_vss', False) + if self._process_vss: + vss_stores = getattr(options, 'vss_stores', None) + if vss_stores: + vss_stores = self._ParseVssStores(vss_stores) + + # Note that os.path.exists() does not support Windows device paths. + if (not self._source_path.startswith('\\\\.\\') and + not os.path.exists(self._source_path)): + raise errors.SourceScannerError( + u'No such device, file or directory: {0:s}.'.format( + self._source_path)) + + # Use the dfVFS source scanner to do the actual scanning. + scan_path_spec = None + + self._scan_context.OpenSourcePath(self._source_path) + + while True: + last_scan_node = self._scan_context.last_scan_node + try: + self._scan_context = self._source_scanner.Scan( + self._scan_context, scan_path_spec=scan_path_spec) + except dfvfs_errors.BackEndError as exception: + raise errors.SourceScannerError( + u'Unable to scan source, with error: {0:s}'.format(exception)) + + # The source is a directory or file. + if self._scan_context.source_type in [ + self._scan_context.SOURCE_TYPE_DIRECTORY, + self._scan_context.SOURCE_TYPE_FILE]: + break + + if (not self._scan_context.last_scan_node or + self._scan_context.last_scan_node == last_scan_node): + raise errors.SourceScannerError( + u'No supported file system found in source: {0:s}.'.format( + self._source_path)) + + # The source scanner found a file system. + if self._scan_context.last_scan_node.type_indicator in [ + dfvfs_definitions.TYPE_INDICATOR_TSK]: + break + + # The source scanner found a BitLocker encrypted volume and we need + # a credential to unlock the volume. + if self._scan_context.last_scan_node.type_indicator in [ + dfvfs_definitions.TYPE_INDICATOR_BDE]: + # TODO: ask for password. + raise errors.SourceScannerError( + u'BitLocker encrypted volume not yet supported.') + + # The source scanner found a partition table and we need to determine + # which partition needs to be processed. + elif self._scan_context.last_scan_node.type_indicator in [ + dfvfs_definitions.TYPE_INDICATOR_TSK_PARTITION]: + scan_node = self._GetVolumeTSKPartition( + self._scan_context, partition_number=partition_number, + partition_offset=partition_offset) + if not scan_node: + break + self._scan_context.last_scan_node = scan_node + + self._partition_offset = getattr(scan_node.path_spec, 'start_offset', 0) + + elif self._scan_context.last_scan_node.type_indicator in [ + dfvfs_definitions.TYPE_INDICATOR_VSHADOW]: + if self._process_vss: + self._GetVolumeVssStoreIdentifiers( + self._scan_context, vss_stores=vss_stores) + + # Get the scan node of the current volume. + scan_node = self._scan_context.last_scan_node.GetSubNodeByLocation(u'/') + self._scan_context.last_scan_node = scan_node + break + + else: + raise errors.SourceScannerError( + u'Unsupported volume system found in source: {0:s}.'.format( + self._source_path)) + + self._source_type = self._scan_context.source_type + + if self._scan_context.source_type in [ + self._scan_context.SOURCE_TYPE_STORAGE_MEDIA_DEVICE, + self._scan_context.SOURCE_TYPE_STORAGE_MEDIA_IMAGE]: + + if self._scan_context.last_scan_node.type_indicator not in [ + dfvfs_definitions.TYPE_INDICATOR_TSK]: + logging.warning( + u'Unsupported file system falling back to single file mode.') + self._source_type = self._scan_context.source_type + + elif self._partition_offset is None: + self._partition_offset = 0 + + +class ExtractionFrontend(StorageMediaFrontend): + """Class that implements an extraction front-end.""" + + _DEFAULT_PROFILING_SAMPLE_RATE = 1000 + + # Approximately 250 MB of queued items per worker. + _DEFAULT_QUEUE_SIZE = 125000 + + _EVENT_SERIALIZER_FORMAT_PROTO = u'proto' + _EVENT_SERIALIZER_FORMAT_JSON = u'json' + + def __init__(self, input_reader, output_writer): + """Initializes the front-end object. + + Args: + input_reader: the input reader (instance of FrontendInputReader). + The default is None which indicates to use the stdin + input reader. + output_writer: the output writer (instance of FrontendOutputWriter). + The default is None which indicates to use the stdout + output writer. + """ + super(ExtractionFrontend, self).__init__(input_reader, output_writer) + self._buffer_size = 0 + self._collection_process = None + self._collector = None + self._debug_mode = False + self._enable_profiling = False + self._engine = None + self._filter_expression = None + self._filter_object = None + self._mount_path = None + self._number_of_worker_processes = 0 + self._old_preprocess = False + self._open_files = False + self._operating_system = None + self._output_module = None + self._parser_names = None + self._preprocess = False + self._profiling_sample_rate = self._DEFAULT_PROFILING_SAMPLE_RATE + self._queue_size = self._DEFAULT_QUEUE_SIZE + self._run_foreman = True + self._single_process_mode = False + self._show_worker_memory_information = False + self._storage_file_path = None + self._storage_serializer_format = self._EVENT_SERIALIZER_FORMAT_PROTO + self._timezone = pytz.utc + + def _CheckStorageFile(self, storage_file_path): + """Checks if the storage file path is valid. + + Args: + storage_file_path: The path of the storage file. + + Raises: + BadConfigOption: if the storage file path is invalid. + """ + if os.path.exists(storage_file_path): + if not os.path.isfile(storage_file_path): + raise errors.BadConfigOption( + u'Storage file: {0:s} already exists and is not a file.'.format( + storage_file_path)) + logging.warning(u'Appending to an already existing storage file.') + + dirname = os.path.dirname(storage_file_path) + if not dirname: + dirname = '.' + + # TODO: add a more thorough check to see if the storage file really is + # a plaso storage file. + + if not os.access(dirname, os.W_OK): + raise errors.BadConfigOption( + u'Unable to write to storage file: {0:s}'.format(storage_file_path)) + + # Note that this function is not called by the normal termination. + def _CleanUpAfterAbort(self): + """Signals the tool to stop running nicely after an abort.""" + if self._single_process_mode and self._debug_mode: + logging.warning(u'Running in debug mode, set up debugger.') + pdb.post_mortem() + return + + if self._collector: + logging.warning(u'Stopping collector.') + self._collector.SignalEndOfInput() + + if self._engine: + self._engine.SignalAbort() + + def _DebugPrintCollector(self, options): + """Prints debug information about the collector. + + Args: + options: the command line arguments (instance of argparse.Namespace). + """ + filter_file = getattr(options, 'file_filter', None) + if self._scan_context.source_type in [ + self._scan_context.SOURCE_TYPE_STORAGE_MEDIA_DEVICE, + self._scan_context.SOURCE_TYPE_STORAGE_MEDIA_IMAGE]: + if filter_file: + logging.debug(u'Starting a collection on image with filter.') + else: + logging.debug(u'Starting a collection on image.') + + elif self._scan_context.source_type in [ + self._scan_context.SOURCE_TYPE_DIRECTORY]: + if filter_file: + logging.debug(u'Starting a collection on directory with filter.') + else: + logging.debug(u'Starting a collection on directory.') + + elif self._scan_context.source_type == self._scan_context.SOURCE_TYPE_FILE: + logging.debug(u'Starting a collection on a single file.') + + else: + logging.warning(u'Unsupported source type.') + + # TODO: have the frontend fill collecton information gradually + # and set it as the last step of preprocessing? + def _PreprocessSetCollectionInformation(self, options, pre_obj): + """Sets the collection information as part of the preprocessing. + + Args: + options: the command line arguments (instance of argparse.Namespace). + pre_obj: the preprocess object (instance of PreprocessObject). + """ + collection_information = {} + + collection_information['version'] = plaso.GetVersion() + collection_information['configured_zone'] = self._timezone + collection_information['file_processed'] = self._source_path + collection_information['output_file'] = self._storage_file_path + collection_information['protobuf_size'] = self._buffer_size + collection_information['parser_selection'] = getattr( + options, 'parsers', '(no list set)') + collection_information['preferred_encoding'] = self.preferred_encoding + collection_information['time_of_run'] = timelib.Timestamp.GetNow() + + collection_information['parsers'] = self._parser_names + collection_information['preprocess'] = self._preprocess + + if self._scan_context.source_type in [ + self._scan_context.SOURCE_TYPE_DIRECTORY]: + recursive = True + else: + recursive = False + collection_information['recursive'] = recursive + collection_information['debug'] = self._debug_mode + collection_information['vss parsing'] = bool(self._vss_stores) + + if self._filter_expression: + collection_information['filter'] = self._filter_expression + + filter_file = getattr(options, 'file_filter', None) + if filter_file: + if os.path.isfile(filter_file): + filters = [] + with open(filter_file, 'rb') as fh: + for line in fh: + filters.append(line.rstrip()) + collection_information['file_filter'] = ', '.join(filters) + + if self._operating_system: + collection_information['os_detected'] = self._operating_system + else: + collection_information['os_detected'] = 'N/A' + + if self._scan_context.source_type in [ + self._scan_context.SOURCE_TYPE_STORAGE_MEDIA_DEVICE, + self._scan_context.SOURCE_TYPE_STORAGE_MEDIA_IMAGE]: + collection_information['method'] = 'imaged processed' + collection_information['image_offset'] = self._partition_offset + else: + collection_information['method'] = 'OS collection' + + if self._single_process_mode: + collection_information['runtime'] = 'single process mode' + else: + collection_information['runtime'] = 'multi process mode' + collection_information['workers'] = self._number_of_worker_processes + + pre_obj.collection_information = collection_information + + def _PreprocessSetParserFilter(self, options, pre_obj): + """Sets the parser filter as part of the preprocessing. + + Args: + options: the command line arguments (instance of argparse.Namespace). + pre_obj: The previously created preprocessing object (instance of + PreprocessObject) or None. + """ + # TODO: Make this more sane. Currently we are only checking against + # one possible version of Windows, and then making the assumption if + # that is not correct we default to Windows 7. Same thing with other + # OS's, no assumption or checks are really made there. + # Also this is done by default, and no way for the user to turn off + # this behavior, need to add a parameter to the frontend that takes + # care of overwriting this behavior. + + # TODO: refactor putting the filter into the options object. + # See if it can be passed in another way. + if not getattr(options, 'filter', None): + options.filter = u'' + + if not options.filter: + options.filter = u'' + + parser_filter_string = u'' + + # If no parser filter is set, let's use our best guess of the OS + # to build that list. + if not getattr(options, 'parsers', ''): + if hasattr(pre_obj, 'osversion'): + os_version = pre_obj.osversion.lower() + # TODO: Improve this detection, this should be more 'intelligent', since + # there are quite a lot of versions out there that would benefit from + # loading up the set of 'winxp' parsers. + if 'windows xp' in os_version: + parser_filter_string = 'winxp' + elif 'windows server 2000' in os_version: + parser_filter_string = 'winxp' + elif 'windows server 2003' in os_version: + parser_filter_string = 'winxp' + else: + parser_filter_string = 'win7' + + if getattr(pre_obj, 'guessed_os', None): + if pre_obj.guessed_os == 'MacOSX': + parser_filter_string = u'macosx' + elif pre_obj.guessed_os == 'Linux': + parser_filter_string = 'linux' + + if parser_filter_string: + options.parsers = parser_filter_string + logging.info(u'Parser filter expression changed to: {0:s}'.format( + options.parsers)) + + def _PreprocessSetTimezone(self, options, pre_obj): + """Sets the timezone as part of the preprocessing. + + Args: + options: the command line arguments (instance of argparse.Namespace). + pre_obj: The previously created preprocessing object (instance of + PreprocessObject) or None. + """ + if hasattr(pre_obj, 'time_zone_str'): + logging.info(u'Setting timezone to: {0:s}'.format(pre_obj.time_zone_str)) + try: + pre_obj.zone = pytz.timezone(pre_obj.time_zone_str) + except pytz.UnknownTimeZoneError: + if hasattr(options, 'zone'): + logging.warning(( + u'Unable to automatically configure timezone, falling back ' + u'to the user supplied one: {0:s}').format(self._timezone)) + pre_obj.zone = self._timezone + else: + logging.warning(u'TimeZone was not properly set, defaulting to UTC') + pre_obj.zone = pytz.utc + else: + # TODO: shouldn't the user to be able to always override the timezone + # detection? Or do we need an input sanitation function. + pre_obj.zone = self._timezone + + if not getattr(pre_obj, 'zone', None): + pre_obj.zone = self._timezone + + def _ProcessSourceMultiProcessMode(self, options): + """Processes the source in a multiple process. + + Muliprocessing is used to start up separate processes. + + Args: + options: the command line arguments (instance of argparse.Namespace). + """ + # TODO: replace by an option. + start_collection_process = True + + self._number_of_worker_processes = getattr(options, 'workers', 0) + + logging.info(u'Starting extraction in multi process mode.') + + self._engine = multi_process.MultiProcessEngine( + maximum_number_of_queued_items=self._queue_size) + + self._engine.SetEnableDebugOutput(self._debug_mode) + self._engine.SetEnableProfiling( + self._enable_profiling, + profiling_sample_rate=self._profiling_sample_rate) + self._engine.SetOpenFiles(self._open_files) + + if self._filter_object: + self._engine.SetFilterObject(self._filter_object) + + if self._mount_path: + self._engine.SetMountPath(self._mount_path) + + if self._text_prepend: + self._engine.SetTextPrepend(self._text_prepend) + # TODO: add support to handle multiple partitions. + self._engine.SetSource( + self.GetSourcePathSpec(), resolver_context=self._resolver_context) + + logging.debug(u'Starting preprocessing.') + pre_obj = self.PreprocessSource(options) + logging.debug(u'Preprocessing done.') + + # TODO: make sure parsers option is not set by preprocessing. + parser_filter_string = getattr(options, 'parsers', '') + + self._parser_names = [] + for _, parser_class in parsers_manager.ParsersManager.GetParsers( + parser_filter_string=parser_filter_string): + self._parser_names.append(parser_class.NAME) + + self._PreprocessSetCollectionInformation(options, pre_obj) + + if 'filestat' in self._parser_names: + include_directory_stat = True + else: + include_directory_stat = False + + filter_file = getattr(options, 'file_filter', None) + if filter_file: + filter_find_specs = engine_utils.BuildFindSpecsFromFile( + filter_file, pre_obj=pre_obj) + else: + filter_find_specs = None + + if start_collection_process: + resolver_context = context.Context() + else: + resolver_context = self._resolver_context + + # TODO: create multi process collector. + self._collector = self._engine.CreateCollector( + include_directory_stat, vss_stores=self._vss_stores, + filter_find_specs=filter_find_specs, resolver_context=resolver_context) + + self._DebugPrintCollector(options) + + if self._output_module: + storage_writer = storage.BypassStorageWriter( + self._engine.storage_queue, self._storage_file_path, + output_module_string=self._output_module, pre_obj=pre_obj) + else: + storage_writer = storage.StorageFileWriter( + self._engine.storage_queue, self._storage_file_path, + buffer_size=self._buffer_size, pre_obj=pre_obj, + serializer_format=self._storage_serializer_format) + + try: + self._engine.ProcessSource( + self._collector, storage_writer, + parser_filter_string=parser_filter_string, + number_of_extraction_workers=self._number_of_worker_processes, + have_collection_process=start_collection_process, + have_foreman_process=self._run_foreman, + show_memory_usage=self._show_worker_memory_information) + + except KeyboardInterrupt: + self._CleanUpAfterAbort() + raise errors.UserAbort(u'Process source aborted.') + + def _ProcessSourceSingleProcessMode(self, options): + """Processes the source in a single process. + + Args: + options: the command line arguments (instance of argparse.Namespace). + """ + logging.info(u'Starting extraction in single process mode.') + + try: + self._StartSingleThread(options) + except Exception as exception: + # The tool should generally not be run in single process mode + # for other reasons than to debug. Hence the general error + # catching. + logging.error(u'An uncaught exception occured: {0:s}.\n{1:s}'.format( + exception, traceback.format_exc())) + if self._debug_mode: + pdb.post_mortem() + + def _StartSingleThread(self, options): + """Starts everything up in a single process. + + This should not normally be used, since running the tool in a single + process buffers up everything into memory until the storage is called. + + Just to make it clear, this starts up the collection, completes that + before calling the worker that extracts all EventObjects and stores + them in memory. when that is all done, the storage function is called + to drain the buffer. Hence the tool's excessive use of memory in this + mode and the reason why it is not suggested to be used except for + debugging reasons (and mostly to get into the debugger). + + This is therefore mostly useful during debugging sessions for some + limited parsing. + + Args: + options: the command line arguments (instance of argparse.Namespace). + """ + self._engine = single_process.SingleProcessEngine(self._queue_size) + self._engine.SetEnableDebugOutput(self._debug_mode) + self._engine.SetEnableProfiling( + self._enable_profiling, + profiling_sample_rate=self._profiling_sample_rate) + self._engine.SetOpenFiles(self._open_files) + + if self._filter_object: + self._engine.SetFilterObject(self._filter_object) + + if self._mount_path: + self._engine.SetMountPath(self._mount_path) + + if self._text_prepend: + self._engine.SetTextPrepend(self._text_prepend) + + # TODO: add support to handle multiple partitions. + self._engine.SetSource( + self.GetSourcePathSpec(), resolver_context=self._resolver_context) + + logging.debug(u'Starting preprocessing.') + pre_obj = self.PreprocessSource(options) + + logging.debug(u'Preprocessing done.') + + # TODO: make sure parsers option is not set by preprocessing. + parser_filter_string = getattr(options, 'parsers', '') + + self._parser_names = [] + for _, parser_class in parsers_manager.ParsersManager.GetParsers( + parser_filter_string=parser_filter_string): + self._parser_names.append(parser_class.NAME) + + self._PreprocessSetCollectionInformation(options, pre_obj) + + if 'filestat' in self._parser_names: + include_directory_stat = True + else: + include_directory_stat = False + + filter_file = getattr(options, 'file_filter', None) + if filter_file: + filter_find_specs = engine_utils.BuildFindSpecsFromFile( + filter_file, pre_obj=pre_obj) + else: + filter_find_specs = None + + self._collector = self._engine.CreateCollector( + include_directory_stat, vss_stores=self._vss_stores, + filter_find_specs=filter_find_specs, + resolver_context=self._resolver_context) + + self._DebugPrintCollector(options) + + if self._output_module: + storage_writer = storage.BypassStorageWriter( + self._engine.storage_queue, self._storage_file_path, + output_module_string=self._output_module, pre_obj=pre_obj) + else: + storage_writer = storage.StorageFileWriter( + self._engine.storage_queue, self._storage_file_path, + buffer_size=self._buffer_size, pre_obj=pre_obj, + serializer_format=self._storage_serializer_format) + + try: + self._engine.ProcessSource( + self._collector, storage_writer, + parser_filter_string=parser_filter_string) + + except KeyboardInterrupt: + self._CleanUpAfterAbort() + raise errors.UserAbort(u'Process source aborted.') + + finally: + self._resolver_context.Empty() + + def AddExtractionOptions(self, argument_group): + """Adds the extraction options to the argument group. + + Args: + argument_group: The argparse argument group (instance of + argparse._ArgumentGroup). + """ + argument_group.add_argument( + '--use_old_preprocess', '--use-old-preprocess', dest='old_preprocess', + action='store_true', default=False, help=( + u'Only used in conjunction when appending to a previous storage ' + u'file. When this option is used then a new preprocessing object ' + u'is not calculated and instead the last one that got added to ' + u'the storage file is used. This can be handy when parsing an ' + u'image that contains more than a single partition.')) + + def AddInformationalOptions(self, argument_group): + """Adds the informational options to the argument group. + + Args: + argument_group: The argparse argument group (instance of + argparse._ArgumentGroup). + """ + argument_group.add_argument( + '-d', '--debug', dest='debug', action='store_true', default=False, + help=( + u'Enable debug mode. Intended for troubleshooting parsing ' + u'issues.')) + + def AddPerformanceOptions(self, argument_group): + """Adds the performance options to the argument group. + + Args: + argument_group: The argparse argument group (instance of + argparse._ArgumentGroup). + """ + argument_group.add_argument( + '--buffer_size', '--buffer-size', '--bs', dest='buffer_size', + action='store', default=0, + help=u'The buffer size for the output (defaults to 196MiB).') + + argument_group.add_argument( + '--queue_size', '--queue-size', dest='queue_size', action='store', + default=0, help=( + u'The maximum number of queued items per worker ' + u'(defaults to {0:d})').format(self._DEFAULT_QUEUE_SIZE)) + + if worker.BaseEventExtractionWorker.SupportsProfiling(): + argument_group.add_argument( + '--profile', dest='enable_profiling', action='store_true', + default=False, help=( + u'Enable profiling of memory usage. Intended for ' + u'troubleshooting memory issues.')) + + argument_group.add_argument( + '--profile_sample_rate', '--profile-sample-rate', + dest='profile_sample_rate', action='store', default=0, help=( + u'The profile sample rate (defaults to a sample every {0:d} ' + u'files).').format(self._DEFAULT_PROFILING_SAMPLE_RATE)) + + def GetSourceFileSystemSearcher(self): + """Retrieves the file system searcher of the source. + + Returns: + The file system searcher object (instance of dfvfs.FileSystemSearcher). + """ + return self._engine.GetSourceFileSystemSearcher( + resolver_context=self._resolver_context) + + def ParseOptions(self, options, source_option='source'): + """Parses the options and initializes the front-end. + + Args: + options: the command line arguments (instance of argparse.Namespace). + source_option: optional name of the source option. The default is source. + + Raises: + BadConfigOption: if the options are invalid. + """ + super(ExtractionFrontend, self).ParseOptions( + options, source_option=source_option) + + self._buffer_size = getattr(options, 'buffer_size', 0) + if self._buffer_size: + # TODO: turn this into a generic function that supports more size + # suffixes both MB and MiB and also that does not allow m as a valid + # indicator for MiB since m represents milli not Mega. + try: + if self._buffer_size[-1].lower() == 'm': + self._buffer_size = int(self._buffer_size[:-1], 10) + self._buffer_size *= self._BYTES_IN_A_MIB + else: + self._buffer_size = int(self._buffer_size, 10) + except ValueError: + raise errors.BadConfigOption( + u'Invalid buffer size: {0:s}.'.format(self._buffer_size)) + + queue_size = getattr(options, 'queue_size', None) + if queue_size: + try: + self._queue_size = int(queue_size, 10) + except ValueError: + raise errors.BadConfigOption( + u'Invalid queue size: {0:s}.'.format(queue_size)) + + self._enable_profiling = getattr(options, 'enable_profiling', False) + + profile_sample_rate = getattr(options, 'profile_sample_rate', None) + if profile_sample_rate: + try: + self._profiling_sample_rate = int(profile_sample_rate, 10) + except ValueError: + raise errors.BadConfigOption( + u'Invalid profile sample rate: {0:s}.'.format(profile_sample_rate)) + + serializer_format = getattr( + options, 'serializer_format', self._EVENT_SERIALIZER_FORMAT_PROTO) + if serializer_format: + self.SetStorageSerializer(serializer_format) + + self._filter_expression = getattr(options, 'filter', None) + if self._filter_expression: + self._filter_object = pfilter.GetMatcher(self._filter_expression) + if not self._filter_object: + raise errors.BadConfigOption( + u'Invalid filter expression: {0:s}'.format(self._filter_expression)) + + filter_file = getattr(options, 'file_filter', None) + if filter_file and not os.path.isfile(filter_file): + raise errors.BadConfigOption( + u'No such collection filter file: {0:s}.'.format(filter_file)) + + self._debug_mode = getattr(options, 'debug', False) + + self._old_preprocess = getattr(options, 'old_preprocess', False) + + timezone_string = getattr(options, 'timezone', None) + if timezone_string: + self._timezone = pytz.timezone(timezone_string) + + self._single_process_mode = getattr( + options, 'single_process', False) + + self._output_module = getattr(options, 'output_module', None) + + self._operating_system = getattr(options, 'os', None) + self._open_files = getattr(options, 'open_files', False) + self._text_prepend = getattr(options, 'text_prepend', None) + + if self._operating_system: + self._mount_path = getattr(options, 'filename', None) + + def PreprocessSource(self, options): + """Preprocesses the source. + + Args: + options: the command line arguments (instance of argparse.Namespace). + + Returns: + The preprocessing object (instance of PreprocessObject). + """ + pre_obj = None + + if self._old_preprocess and os.path.isfile(self._storage_file_path): + # Check if the storage file contains a preprocessing object. + try: + with storage.StorageFile( + self._storage_file_path, read_only=True) as store: + storage_information = store.GetStorageInformation() + if storage_information: + logging.info(u'Using preprocessing information from a prior run.') + pre_obj = storage_information[-1] + self._preprocess = False + except IOError: + logging.warning(u'Storage file does not exist, running preprocess.') + + if self._preprocess and self._scan_context.source_type in [ + self._scan_context.SOURCE_TYPE_DIRECTORY, + self._scan_context.SOURCE_TYPE_STORAGE_MEDIA_DEVICE, + self._scan_context.SOURCE_TYPE_STORAGE_MEDIA_IMAGE]: + try: + self._engine.PreprocessSource( + self._operating_system, resolver_context=self._resolver_context) + except IOError as exception: + logging.error(u'Unable to preprocess with error: {0:s}'.format( + exception)) + return + + # TODO: Remove the need for direct access to the pre_obj in favor + # of the knowledge base. + pre_obj = getattr(self._engine.knowledge_base, '_pre_obj', None) + + if not pre_obj: + pre_obj = event.PreprocessObject() + + self._PreprocessSetTimezone(options, pre_obj) + self._PreprocessSetParserFilter(options, pre_obj) + + return pre_obj + + def PrintOptions(self, options, source_path): + """Prints the options. + + Args: + options: the command line arguments (instance of argparse.Namespace). + source_path: the source path. + """ + self._output_writer.Write(u'\n') + self._output_writer.Write( + u'Source path\t\t\t\t: {0:s}\n'.format(source_path)) + + if self._scan_context.source_type in [ + self._scan_context.SOURCE_TYPE_STORAGE_MEDIA_DEVICE, + self._scan_context.SOURCE_TYPE_STORAGE_MEDIA_IMAGE]: + is_image = True + else: + is_image = False + + self._output_writer.Write( + u'Is storage media image or device\t: {0!s}\n'.format(is_image)) + + if is_image: + image_offset_bytes = self._partition_offset + if isinstance(image_offset_bytes, basestring): + try: + image_offset_bytes = int(image_offset_bytes, 10) + except ValueError: + image_offset_bytes = 0 + elif image_offset_bytes is None: + image_offset_bytes = 0 + + self._output_writer.Write( + u'Partition offset\t\t\t: {0:d} (0x{0:08x})\n'.format( + image_offset_bytes)) + + if self._process_vss and self._vss_stores: + self._output_writer.Write( + u'VSS stores\t\t\t\t: {0!s}\n'.format(self._vss_stores)) + + filter_file = getattr(options, 'file_filter', None) + if filter_file: + self._output_writer.Write(u'Filter file\t\t\t\t: {0:s}\n'.format( + filter_file)) + + self._output_writer.Write(u'\n') + + def ProcessSource(self, options): + """Processes the source. + + Args: + options: the command line arguments (instance of argparse.Namespace). + + Raises: + SourceScannerError: if the source scanner could not find a supported + file system. + UserAbort: if the user initiated an abort. + """ + self.ScanSource(options) + + self.PrintOptions(options, self._source_path) + + if self._partition_offset is None: + self._preprocess = False + + else: + # If we're dealing with a storage media image always run pre-processing. + self._preprocess = True + + self._CheckStorageFile(self._storage_file_path) + + # No need to multi process when we're only processing a single file. + if self._scan_context.source_type == self._scan_context.SOURCE_TYPE_FILE: + # If we are only processing a single file we don't need more than a + # single worker. + # TODO: Refactor this use of using the options object. + options.workers = 1 + self._single_process_mode = True + + if self._scan_context.source_type in [ + self._scan_context.SOURCE_TYPE_DIRECTORY]: + # If we are dealing with a directory we would like to attempt + # pre-processing. + self._preprocess = True + + if self._single_process_mode: + self._ProcessSourceSingleProcessMode(options) + else: + self._ProcessSourceMultiProcessMode(options) + + def SetStorageFile(self, storage_file_path): + """Sets the storage file path. + + Args: + storage_file_path: The path of the storage file. + """ + self._storage_file_path = storage_file_path + + def SetStorageSerializer(self, storage_serializer_format): + """Sets the storage serializer. + + Args: + storage_serializer_format: String denoting the type of serializer + to be used in the storage. The values + can be either "proto" or "json". + """ + if storage_serializer_format not in ( + self._EVENT_SERIALIZER_FORMAT_JSON, + self._EVENT_SERIALIZER_FORMAT_PROTO): + return + self._storage_serializer_format = storage_serializer_format + + def SetRunForeman(self, run_foreman=True): + """Sets a flag indicating whether the frontend should monitor workers. + + Args: + run_foreman: A boolean (defaults to true) that indicates whether or not + the frontend should start a foreman that monitors workers. + """ + self._run_foreman = run_foreman + + def SetShowMemoryInformation(self, show_memory=True): + """Sets a flag telling the worker monitor to show memory information. + + Args: + show_memory: A boolean (defaults to True) that indicates whether or not + the foreman should include memory information as part of + the worker monitoring. + """ + self._show_worker_memory_information = show_memory + + +class AnalysisFrontend(Frontend): + """Class that implements an analysis front-end.""" + + def __init__(self, input_reader, output_writer): + """Initializes the front-end object. + + Args: + input_reader: the input reader (instance of FrontendInputReader). + The default is None which indicates to use the stdin + input reader. + output_writer: the output writer (instance of FrontendOutputWriter). + The default is None which indicates to use the stdout + output writer. + """ + super(AnalysisFrontend, self).__init__(input_reader, output_writer) + + self._storage_file_path = None + + def AddStorageFileOptions(self, argument_group): + """Adds the storage file options to the argument group. + + Args: + argument_group: The argparse argument group (instance of + argparse._ArgumentGroup) or argument parser (instance of + argparse.ArgumentParser). + """ + argument_group.add_argument( + 'storage_file', metavar='STORAGE_FILE', action='store', nargs='?', + type=unicode, default=None, help='The path of the storage file.') + + def OpenStorageFile(self, read_only=True): + """Opens the storage file. + + Args: + read_only: Optional boolean value to indicate the storage file should + be opened in read-only mode. The default is True. + + Returns: + The storage file object (instance of StorageFile). + """ + return storage.StorageFile(self._storage_file_path, read_only=read_only) + + def ParseOptions(self, options): + """Parses the options and initializes the front-end. + + Args: + options: the command line arguments (instance of argparse.Namespace). + + Raises: + BadConfigOption: if the options are invalid. + """ + if not options: + raise errors.BadConfigOption(u'Missing options.') + + self._storage_file_path = getattr(options, 'storage_file', None) + if not self._storage_file_path: + raise errors.BadConfigOption(u'Missing storage file.') + + if not os.path.isfile(self._storage_file_path): + raise errors.BadConfigOption( + u'No such storage file {0:s}.'.format(self._storage_file_path)) diff --git a/plaso/frontend/frontend_test.py b/plaso/frontend/frontend_test.py new file mode 100644 index 0000000..f03f360 --- /dev/null +++ b/plaso/frontend/frontend_test.py @@ -0,0 +1,279 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the front-end object.""" + +import os +import unittest + +from dfvfs.lib import definitions as dfvfs_definitions + +from plaso.frontend import frontend +from plaso.frontend import test_lib +from plaso.lib import errors +from plaso.lib import storage + + +class ExtractionFrontendTests(test_lib.FrontendTestCase): + """Tests for the extraction front-end object.""" + + def _TestScanSourceDirectory(self, test_file): + """Tests the ScanSource function on a directory. + + Args: + test_file: the path of the test file. + """ + test_front_end = frontend.ExtractionFrontend( + self._input_reader, self._output_writer) + + options = test_lib.Options() + options.source = test_file + + test_front_end.ParseOptions(options) + + test_front_end.ScanSource(options) + path_spec = test_front_end.GetSourcePathSpec() + self.assertNotEquals(path_spec, None) + self.assertEquals(path_spec.location, os.path.abspath(test_file)) + self.assertEquals( + path_spec.type_indicator, dfvfs_definitions.TYPE_INDICATOR_OS) + # pylint: disable=protected-access + self.assertEquals(test_front_end._partition_offset, None) + + def _TestScanSourceImage(self, test_file): + """Tests the ScanSource function on the test image. + + Args: + test_file: the path of the test file. + """ + test_front_end = frontend.ExtractionFrontend( + self._input_reader, self._output_writer) + + options = test_lib.Options() + options.source = test_file + + test_front_end.ParseOptions(options) + + test_front_end.ScanSource(options) + path_spec = test_front_end.GetSourcePathSpec() + self.assertNotEquals(path_spec, None) + self.assertEquals( + path_spec.type_indicator, dfvfs_definitions.TYPE_INDICATOR_TSK) + # pylint: disable=protected-access + self.assertEquals(test_front_end._partition_offset, 0) + + def _TestScanSourcePartitionedImage(self, test_file): + """Tests the ScanSource function on the partitioned test image. + + Args: + test_file: the path of the test file. + """ + test_front_end = frontend.ExtractionFrontend( + self._input_reader, self._output_writer) + + options = test_lib.Options() + options.source = test_file + options.image_offset_bytes = 0x0002c000 + + test_front_end.ParseOptions(options) + + test_front_end.ScanSource(options) + path_spec = test_front_end.GetSourcePathSpec() + self.assertNotEquals(path_spec, None) + self.assertEquals( + path_spec.type_indicator, dfvfs_definitions.TYPE_INDICATOR_TSK) + # pylint: disable=protected-access + self.assertEquals(test_front_end._partition_offset, 180224) + + options = test_lib.Options() + options.source = test_file + options.image_offset = 352 + options.bytes_per_sector = 512 + + test_front_end.ParseOptions(options) + + test_front_end.ScanSource(options) + path_spec = test_front_end.GetSourcePathSpec() + self.assertNotEquals(path_spec, None) + self.assertEquals( + path_spec.type_indicator, dfvfs_definitions.TYPE_INDICATOR_TSK) + # pylint: disable=protected-access + self.assertEquals(test_front_end._partition_offset, 180224) + + options = test_lib.Options() + options.source = test_file + options.partition_number = 2 + + test_front_end.ParseOptions(options) + + test_front_end.ScanSource(options) + path_spec = test_front_end.GetSourcePathSpec() + self.assertNotEquals(path_spec, None) + self.assertEquals( + path_spec.type_indicator, dfvfs_definitions.TYPE_INDICATOR_TSK) + # pylint: disable=protected-access + self.assertEquals(test_front_end._partition_offset, 180224) + + def _TestScanSourceVssImage(self, test_file): + """Tests the ScanSource function on the VSS test image. + + Args: + test_file: the path of the test file. + """ + test_front_end = frontend.ExtractionFrontend( + self._input_reader, self._output_writer) + + options = test_lib.Options() + options.source = test_file + options.vss_stores = '1,2' + + test_front_end.ParseOptions(options) + + test_front_end.ScanSource(options) + path_spec = test_front_end.GetSourcePathSpec() + self.assertNotEquals(path_spec, None) + self.assertEquals( + path_spec.type_indicator, dfvfs_definitions.TYPE_INDICATOR_TSK) + # pylint: disable=protected-access + self.assertEquals(test_front_end._partition_offset, 0) + self.assertEquals(test_front_end._vss_stores, [1, 2]) + + options = test_lib.Options() + options.source = test_file + options.vss_stores = '1' + + test_front_end.ParseOptions(options) + + test_front_end.ScanSource(options) + path_spec = test_front_end.GetSourcePathSpec() + self.assertNotEquals(path_spec, None) + self.assertEquals( + path_spec.type_indicator, dfvfs_definitions.TYPE_INDICATOR_TSK) + # pylint: disable=protected-access + self.assertEquals(test_front_end._partition_offset, 0) + self.assertEquals(test_front_end._vss_stores, [1]) + + options = test_lib.Options() + options.source = test_file + options.vss_stores = 'all' + + test_front_end.ParseOptions(options) + + test_front_end.ScanSource(options) + path_spec = test_front_end.GetSourcePathSpec() + self.assertNotEquals(path_spec, None) + self.assertEquals( + path_spec.type_indicator, dfvfs_definitions.TYPE_INDICATOR_TSK) + # pylint: disable=protected-access + self.assertEquals(test_front_end._partition_offset, 0) + self.assertEquals(test_front_end._vss_stores, [1, 2]) + + def setUp(self): + """Sets up the objects used throughout the test.""" + self._input_reader = frontend.StdinFrontendInputReader() + self._output_writer = frontend.StdoutFrontendOutputWriter() + + def testParseOptions(self): + """Tests the parse options function.""" + test_front_end = frontend.ExtractionFrontend( + self._input_reader, self._output_writer) + + options = test_lib.Options() + + with self.assertRaises(errors.BadConfigOption): + test_front_end.ParseOptions(options) + + options.source = self._GetTestFilePath([u'ímynd.dd']) + + test_front_end.ParseOptions(options) + + def testScanSource(self): + """Tests the ScanSource function.""" + test_file = self._GetTestFilePath([u'tsk_volume_system.raw']) + self._TestScanSourcePartitionedImage(test_file) + + test_file = self._GetTestFilePath([u'image-split.E01']) + self._TestScanSourcePartitionedImage(test_file) + + test_file = self._GetTestFilePath([u'image.E01']) + self._TestScanSourceImage(test_file) + + test_file = self._GetTestFilePath([u'image.qcow2']) + self._TestScanSourceImage(test_file) + + test_file = self._GetTestFilePath([u'vsstest.qcow2']) + self._TestScanSourceVssImage(test_file) + + test_file = self._GetTestFilePath([u'text_parser']) + self._TestScanSourceDirectory(test_file) + + test_file = self._GetTestFilePath([u'image.vhd']) + self._TestScanSourceImage(test_file) + + test_file = self._GetTestFilePath([u'image.vmdk']) + self._TestScanSourceImage(test_file) + + with self.assertRaises(errors.SourceScannerError): + test_file = self._GetTestFilePath(['nosuchfile.raw']) + self._TestScanSourceImage(test_file) + + +class AnalysisFrontendTests(test_lib.FrontendTestCase): + """Tests for the analysis front-end object.""" + + def setUp(self): + """Sets up the objects used throughout the test.""" + self._input_reader = frontend.StdinFrontendInputReader() + self._output_writer = frontend.StdoutFrontendOutputWriter() + + def testOpenStorageFile(self): + """Tests the open storage file function.""" + test_front_end = frontend.AnalysisFrontend( + self._input_reader, self._output_writer) + + options = test_lib.Options() + options.storage_file = self._GetTestFilePath([u'psort_test.out']) + + test_front_end.ParseOptions(options) + storage_file = test_front_end.OpenStorageFile() + + self.assertIsInstance(storage_file, storage.StorageFile) + + storage_file.Close() + + def testParseOptions(self): + """Tests the parse options function.""" + test_front_end = frontend.AnalysisFrontend( + self._input_reader, self._output_writer) + + options = test_lib.Options() + + with self.assertRaises(errors.BadConfigOption): + test_front_end.ParseOptions(options) + + options.storage_file = self._GetTestFilePath([u'no_such_file.out']) + + with self.assertRaises(errors.BadConfigOption): + test_front_end.ParseOptions(options) + + options.storage_file = self._GetTestFilePath([u'psort_test.out']) + + test_front_end.ParseOptions(options) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/frontend/image_export.py b/plaso/frontend/image_export.py new file mode 100755 index 0000000..36c56da --- /dev/null +++ b/plaso/frontend/image_export.py @@ -0,0 +1,700 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The image export front-end.""" + +import argparse +import collections +import hashlib +import logging +import os +import sys + +from dfvfs.helpers import file_system_searcher +from dfvfs.lib import definitions as dfvfs_definitions +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver import resolver as path_spec_resolver + +from plaso.artifacts import knowledge_base +from plaso.engine import collector +from plaso.engine import utils as engine_utils +from plaso.engine import queue +from plaso.engine import single_process +from plaso.frontend import frontend +from plaso.frontend import utils as frontend_utils +from plaso.lib import errors +from plaso.lib import timelib +from plaso.preprocessors import interface as preprocess_interface +from plaso.preprocessors import manager as preprocess_manager + + +def CalculateHash(file_object): + """Return a hash for a given file object.""" + md5 = hashlib.md5() + file_object.seek(0) + + data = file_object.read(4098) + while data: + md5.update(data) + data = file_object.read(4098) + + return md5.hexdigest() + + +class DateFilter(object): + """Class that implements a date filter for file entries.""" + + DATE_FILTER_INSTANCE = collections.namedtuple( + 'date_filter_instance', 'type start end') + + DATE_FILTER_TYPES = frozenset([ + u'atime', u'bkup', u'ctime', u'crtime', u'dtime', u'mtime']) + + def __init__(self): + """Initialize the date filter object.""" + super(DateFilter, self).__init__() + self._filters = [] + + @property + def number_of_filters(self): + """Return back the filter count.""" + return len(self._filters) + + def Add(self, filter_type, filter_start=None, filter_end=None): + """Add a date filter. + + Args: + filter_type: String that defines what timestamp is affected by the + date filter, valid values are atime, ctime, crtime, + dtime, bkup and mtime. + filter_start: Optional start date of the filter. This is a string + in the form of "YYYY-MM-DD HH:MM:SS", or "YYYY-MM-DD". + If not supplied there will be no limitation to the initial + timeframe. + filter_end: Optional end date of the filter. This is a string + in the form of "YYYY-MM-DD HH:MM:SS", or "YYYY-MM-DD". + If not supplied there will be no limitation to the initial + timeframe. + + Raises: + errors.WrongFilterOption: If the filter is badly formed. + """ + if not isinstance(filter_type, basestring): + raise errors.WrongFilterOption(u'Filter type must be a string.') + + if filter_start is None and filter_end is None: + raise errors.WrongFilterOption( + u'A date filter has to have either a start or an end date.') + + filter_type_lower = filter_type.lower() + if filter_type_lower not in self.DATE_FILTER_TYPES: + raise errors.WrongFilterOption(u'Unknown filter type: {0:s}.'.format( + filter_type)) + + date_filter_type = filter_type_lower + date_filter_start = None + date_filter_end = None + + if filter_start is not None: + # If the date string is invalid the timestamp will be set to zero, + # which is also a valid date. Thus all invalid timestamp strings + # will be set to filter from the POSIX epoch time. + # Thus the actual value of the filter is printed out so that the user + # may catch this potentially unwanted behavior. + date_filter_start = timelib.Timestamp.FromTimeString(filter_start) + logging.info( + u'Date filter for start date configured: [{0:s}] {1:s}'.format( + date_filter_type, + timelib.Timestamp.CopyToIsoFormat(date_filter_start))) + + if filter_end is not None: + date_filter_end = timelib.Timestamp.FromTimeString(filter_end) + logging.info( + u'Date filter for end date configured: [{0:s}] {1:s}'.format( + date_filter_type, + timelib.Timestamp.CopyToIsoFormat(date_filter_end))) + + # Make sure that the end timestamp occurs after the beginning. + # If not then we need to reverse the time range. + if (date_filter_start is not None and + date_filter_start > date_filter_end): + temporary_placeholder = date_filter_end + date_filter_end = date_filter_start + date_filter_start = temporary_placeholder + + self._filters.append(self.DATE_FILTER_INSTANCE( + date_filter_type, date_filter_start, date_filter_end)) + + def CompareFileEntry(self, file_entry): + """Compare the set date filters against timestamps of a file entry. + + Args: + file_entry: The file entry (instance of dfvfs.FileEntry). + + Returns: + True, if there are no date filters set. Otherwise the date filters are + compared and True only returned if the timestamps are outside of the time + range. + + Raises: + errors.WrongFilterOption: If an attempt is made to filter against + a date type that is not stored in the stat + object. + """ + if not self._filters: + return True + + # Compare timestamps of the file entry. + stat = file_entry.GetStat() + + # Go over each filter. + for date_filter in self._filters: + posix_time = getattr(stat, date_filter.type, None) + + if posix_time is None: + # Trying to filter against a date type that is not saved in the stat + # object. + raise errors.WrongFilterOption( + u'Date type: {0:s} is not stored in the file entry'.format( + date_filter.type)) + + timestamp = timelib.Timestamp.FromPosixTime(posix_time) + + if date_filter.start is not None and (timestamp < date_filter.start): + logging.debug(( + u'[skipping] Not saving file: {0:s}, timestamp out of ' + u'range.').format(file_entry.path_spec.location)) + return False + + if date_filter.end is not None and (timestamp > date_filter.end): + logging.debug(( + u'[skipping] Not saving file: {0:s}, timestamp out of ' + u'range.').format(file_entry.path_spec.location)) + return False + + return True + + def Remove(self, filter_type, filter_start=None, filter_end=None): + """Remove a date filter from the set of defined date filters. + + Args: + filter_type: String that defines what timestamp is affected by the + date filter, valid values are atime, ctime, crtime, + dtime, bkup and mtime. + filter_start: Optional start date of the filter. This is a string + in the form of "YYYY-MM-DD HH:MM:SS", or "YYYY-MM-DD". + If not supplied there will be no limitation to the initial + timeframe. + filter_end: Optional end date of the filter. This is a string + in the form of "YYYY-MM-DD HH:MM:SS", or "YYYY-MM-DD". + If not supplied there will be no limitation to the initial + timeframe. + """ + if not self._filters: + return + + # TODO: Instead of doing it this way calculate a hash for every filter + # that is stored and use that for removals. + for date_filter_index, date_filter in enumerate(self._filters): + if filter_start is None: + date_filter_start = filter_start + else: + date_filter_start = timelib.Timestamp.FromTimeString(filter_start) + if filter_end is None: + date_filter_end = filter_end + else: + date_filter_end = timelib.Timestamp.FromTimeString(filter_end) + + if (date_filter.type == filter_type and + date_filter.start == date_filter_start and + date_filter.end == date_filter_end): + del self._filters[date_filter_index] + return + + def Reset(self): + """Resets the date filter.""" + self._filters = [] + + +class FileSaver(object): + """A simple class that is used to save files.""" + + md5_dict = {} + calc_md5 = False + # TODO: Move this functionality into the frontend as a state attribute. + _date_filter = None + + @classmethod + def SetDateFilter(cls, date_filter): + """Set a date filter for the file saver. + + If a date filter is set files will not be saved unless they are within + the time boundaries. + + Args: + date_filter: A date filter object (instance of DateFilter). + """ + cls._date_filter = date_filter + + @classmethod + def WriteFile(cls, source_path_spec, destination_path, filename_prefix=''): + """Writes the contents of the source to the destination file. + + Args: + source_path_spec: the path specification of the source file. + destination_path: the path of the destination file. + filename_prefix: optional prefix for the filename. The default is an + empty string. + """ + file_entry = path_spec_resolver.Resolver.OpenFileEntry(source_path_spec) + directory = u'' + filename = getattr(source_path_spec, 'location', None) + if not filename: + filename = source_path_spec.file_path + + # There will be issues on systems that use a different separator than a + # forward slash. However a forward slash is always used in the pathspec. + if os.path.sep != u'/': + filename = filename.replace(u'/', os.path.sep) + + if os.path.sep in filename: + directory_string, _, filename = filename.rpartition(os.path.sep) + if directory_string: + directory = os.path.join( + destination_path, *directory_string.split(os.path.sep)) + + if filename_prefix: + extracted_filename = u'{0:s}_{1:s}'.format(filename_prefix, filename) + else: + extracted_filename = filename + + while extracted_filename.startswith(os.path.sep): + extracted_filename = extracted_filename[1:] + + if directory: + if not os.path.isdir(directory): + os.makedirs(directory) + else: + directory = destination_path + + if cls.calc_md5: + stat = file_entry.GetStat() + inode = getattr(stat, 'ino', 0) + file_object = file_entry.GetFileObject() + md5sum = CalculateHash(file_object) + if inode in cls.md5_dict: + if md5sum in cls.md5_dict[inode]: + return + cls.md5_dict[inode].append(md5sum) + else: + cls.md5_dict[inode] = [md5sum] + + # Check if we do not want to save the file. + if cls._date_filter and not cls._date_filter.CompareFileEntry(file_entry): + return + + try: + file_object = file_entry.GetFileObject() + frontend_utils.OutputWriter.WriteFile( + file_object, os.path.join(directory, extracted_filename)) + except IOError as exception: + logging.error( + u'[skipping] unable to save file: {0:s} with error: {1:s}'.format( + filename, exception)) + + +class ImageExtractorQueueConsumer(queue.ItemQueueConsumer): + """Class that implements an image extractor queue consumer.""" + + def __init__(self, process_queue, extensions, destination_path): + """Initializes the image extractor queue consumer. + + Args: + process_queue: the process queue (instance of Queue). + extensions: a list of extensions. + destination_path: the path where the extracted files should be stored. + """ + super(ImageExtractorQueueConsumer, self).__init__(process_queue) + self._destination_path = destination_path + self._extensions = extensions + + def _ConsumeItem(self, path_spec): + """Consumes an item callback for ConsumeItems. + + Args: + path_spec: a path specification (instance of dfvfs.PathSpec). + """ + # TODO: move this into a function of path spec e.g. GetExtension(). + location = getattr(path_spec, 'location', None) + if not location: + location = path_spec.file_path + _, _, extension = location.rpartition('.') + if extension.lower() in self._extensions: + vss_store_number = getattr(path_spec, 'vss_store_number', None) + if vss_store_number is not None: + filename_prefix = 'vss_{0:d}'.format(vss_store_number + 1) + else: + filename_prefix = '' + + FileSaver.WriteFile( + path_spec, self._destination_path, filename_prefix=filename_prefix) + + +class ImageExportFrontend(frontend.StorageMediaFrontend): + """Class that implements the image export front-end.""" + + def __init__(self): + """Initializes the front-end object.""" + input_reader = frontend.StdinFrontendInputReader() + output_writer = frontend.StdoutFrontendOutputWriter() + + super(ImageExportFrontend, self).__init__(input_reader, output_writer) + + self._knowledge_base = None + self._remove_duplicates = True + self._source_path_spec = None + + # TODO: merge with collector and/or engine. + def _ExtractWithExtensions(self, extensions, destination_path): + """Extracts files using extensions. + + Args: + extensions: a list of extensions. + destination_path: the path where the extracted files should be stored. + """ + logging.info(u'Finding files with extensions: {0:s}'.format(extensions)) + + if not os.path.isdir(destination_path): + os.makedirs(destination_path) + + input_queue = single_process.SingleProcessQueue() + + # TODO: add support to handle multiple partitions. + self._source_path_spec = self.GetSourcePathSpec() + + image_collector = collector.Collector( + input_queue, self._source_path, self._source_path_spec) + + image_collector.Collect() + + FileSaver.calc_md5 = self._remove_duplicates + + input_queue_consumer = ImageExtractorQueueConsumer( + input_queue, extensions, destination_path) + input_queue_consumer.ConsumeItems() + + # TODO: merge with collector and/or engine. + def _ExtractWithFilter(self, filter_file_path, destination_path): + """Extracts files using a filter expression. + + This method runs the file extraction process on the image and + potentially on every VSS if that is wanted. + + Args: + filter_file_path: The path of the file that contains the filter + expressions. + destination_path: The path where the extracted files should be stored. + """ + # TODO: add support to handle multiple partitions. + self._source_path_spec = self.GetSourcePathSpec() + + searcher = self._GetSourceFileSystemSearcher( + resolver_context=self._resolver_context) + + if self._knowledge_base is None: + self._Preprocess(searcher) + + if not os.path.isdir(destination_path): + os.makedirs(destination_path) + + find_specs = engine_utils.BuildFindSpecsFromFile( + filter_file_path, pre_obj=self._knowledge_base.pre_obj) + + # Save the regular files. + FileSaver.calc_md5 = self._remove_duplicates + + for path_spec in searcher.Find(find_specs=find_specs): + FileSaver.WriteFile(path_spec, destination_path) + + if self._process_vss and self._vss_stores: + volume_path_spec = self._source_path_spec.parent + + logging.info(u'Extracting files from VSS.') + vss_path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_VSHADOW, location=u'/', + parent=volume_path_spec) + + vss_file_entry = path_spec_resolver.Resolver.OpenFileEntry(vss_path_spec) + + number_of_vss = vss_file_entry.number_of_sub_file_entries + + # In plaso 1 represents the first store index in dfvfs and pyvshadow 0 + # represents the first store index so 1 is subtracted. + vss_store_range = [store_nr - 1 for store_nr in self._vss_stores] + + for store_index in vss_store_range: + logging.info(u'Extracting files from VSS {0:d} out of {1:d}'.format( + store_index + 1, number_of_vss)) + + vss_path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_VSHADOW, store_index=store_index, + parent=volume_path_spec) + path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_TSK, location=u'/', + parent=vss_path_spec) + + filename_prefix = 'vss_{0:d}'.format(store_index) + + file_system = path_spec_resolver.Resolver.OpenFileSystem( + path_spec, resolver_context=self._resolver_context) + searcher = file_system_searcher.FileSystemSearcher( + file_system, vss_path_spec) + + for path_spec in searcher.Find(find_specs=find_specs): + FileSaver.WriteFile( + path_spec, destination_path, filename_prefix=filename_prefix) + + # TODO: refactor, this is a duplicate of the function in engine. + def _GetSourceFileSystemSearcher(self, resolver_context=None): + """Retrieves the file system searcher of the source. + + Args: + resolver_context: Optional resolver context (instance of dfvfs.Context). + The default is None. Note that every thread or process + must have its own resolver context. + + Returns: + The file system searcher object (instance of dfvfs.FileSystemSearcher). + + Raises: + RuntimeError: if source path specification is not set. + """ + if not self._source_path_spec: + raise RuntimeError(u'Missing source.') + + file_system = path_spec_resolver.Resolver.OpenFileSystem( + self._source_path_spec, resolver_context=resolver_context) + + type_indicator = self._source_path_spec.type_indicator + if type_indicator == dfvfs_definitions.TYPE_INDICATOR_OS: + mount_point = self._source_path_spec + else: + mount_point = self._source_path_spec.parent + + return file_system_searcher.FileSystemSearcher(file_system, mount_point) + + def _Preprocess(self, searcher): + """Preprocesses the image. + + Args: + searcher: The file system searcher object (instance of + dfvfs.FileSystemSearcher). + """ + if self._knowledge_base is not None: + return + + self._knowledge_base = knowledge_base.KnowledgeBase() + + logging.info(u'Guessing OS') + + platform = preprocess_interface.GuessOS(searcher) + logging.info(u'OS: {0:s}'.format(platform)) + + logging.info(u'Running preprocess.') + + preprocess_manager.PreprocessPluginsManager.RunPlugins( + platform, searcher, self._knowledge_base) + + logging.info(u'Preprocess done, saving files from image.') + + def ParseOptions(self, options, source_option='source'): + """Parses the options and initializes the front-end. + + Args: + options: the command line arguments (instance of argparse.Namespace). + source_option: optional name of the source option. The default is source. + + Raises: + BadConfigOption: if the options are invalid. + """ + super(ImageExportFrontend, self).ParseOptions( + options, source_option=source_option) + + filter_file = getattr(options, 'filter', None) + if not filter_file and not getattr(options, 'extension_string', None): + raise errors.BadConfigOption( + u'Neither an extension string or a filter is defined.') + + if filter_file and not os.path.isfile(filter_file): + raise errors.BadConfigOption( + u'Unable to proceed, filter file: {0:s} does not exist.'.format( + filter_file)) + + if (getattr(options, 'no_vss', False) or + getattr(options, 'include_duplicates', False)): + self._remove_duplicates = False + + # Process date filter. + date_filters = getattr(options, 'date_filters', []) + if date_filters: + date_filter_object = DateFilter() + + for date_filter in date_filters: + date_filter_pieces = date_filter.split(',') + if len(date_filter_pieces) != 3: + raise errors.BadConfigOption( + u'Date filter badly formed: {0:s}'.format(date_filter)) + + filter_type, filter_start, filter_end = date_filter_pieces + date_filter_object.Add( + filter_type=filter_type.strip(), filter_start=filter_start.strip(), + filter_end=filter_end.strip()) + + # TODO: Move the date filter to the front-end as an attribute. + FileSaver.SetDateFilter(date_filter_object) + + def ProcessSource(self, options): + """Processes the source. + + Args: + options: the command line arguments (instance of argparse.Namespace). + + Raises: + SourceScannerError: if the source scanner could not find a supported + file system. + UserAbort: if the user initiated an abort. + """ + self.ScanSource(options) + + filter_file = getattr(options, 'filter', None) + if filter_file: + self._ExtractWithFilter(filter_file, options.path) + + extension_string = getattr(options, 'extension_string', None) + if extension_string: + extensions = [x.strip() for x in extension_string.split(',')] + + self._ExtractWithExtensions(extensions, options.path) + logging.info(u'Files based on extension extracted.') + + +def Main(): + """The main function, running the show.""" + front_end = ImageExportFrontend() + + arg_parser = argparse.ArgumentParser( + description=( + 'This is a simple collector designed to export files inside an ' + 'image, both within a regular RAW image as well as inside a VSS. ' + 'The tool uses a collection filter that uses the same syntax as a ' + 'targeted plaso filter.'), + epilog='And that\'s how you export files, plaso style.') + + arg_parser.add_argument( + '-d', '--debug', dest='debug', action='store_true', default=False, + help='Turn on debugging information.') + + arg_parser.add_argument( + '-w', '--write', dest='path', action='store', default='.', type=str, + help='The directory in which extracted files should be stored in.') + + arg_parser.add_argument( + '-x', '--extensions', dest='extension_string', action='store', + type=str, metavar='EXTENSION_STRING', help=( + 'If the purpose is to find all files given a certain extension ' + 'this options should be used. This option accepts a comma separated ' + 'string denoting all file extensions, eg: -x "csv,docx,pst".')) + + arg_parser.add_argument( + '-f', '--filter', action='store', dest='filter', metavar='FILTER_FILE', + type=str, help=( + 'Full path to the file that contains the collection filter, ' + 'the file can use variables that are defined in preprocesing, ' + 'just like any other log2timeline/plaso collection filter.')) + + arg_parser.add_argument( + '--date-filter', '--date_filter', action='append', type=str, + dest='date_filters', metavar="TYPE_START_END", default=None, help=( + 'Add a date based filter to the export criteria. If a date based ' + 'filter is set no file is saved unless it\'s within the date ' + 'boundary. This parameter should be in the form of "TYPE,START,END" ' + 'where TYPE defines which timestamp this date filter affects, eg: ' + 'atime, ctime, crtime, bkup, etc. START defines the start date and ' + 'time of the boundary and END defines the end time. Both timestamps ' + 'are optional and should be set as - if not needed. The correct form ' + 'of the timestamp value is in the form of "YYYY-MM-DD HH:MM:SS" or ' + '"YYYY-MM-DD". Examples are "atime, 2013-01-01 23:12:14, 2013-02-23" ' + 'This parameter can be repeated as needed to add additional date ' + 'date boundaries, eg: once for atime, once for crtime, etc.')) + + arg_parser.add_argument( + '--include_duplicates', dest='include_duplicates', action='store_true', + default=False, help=( + 'By default if VSS is turned on all files saved will have their ' + 'MD5 sum calculated and compared to other files already saved ' + 'with the same inode value. If the MD5 sum is the same the file ' + 'does not get saved again. This option turns off that behavior ' + 'so that all files will get stored, even if they are duplicates.')) + + front_end.AddImageOptions(arg_parser) + front_end.AddVssProcessingOptions(arg_parser) + + arg_parser.add_argument( + 'image', action='store', metavar='IMAGE', default=None, type=str, help=( + 'The full path to the image file that we are about to extract files ' + 'from, it should be a raw image or another image that plaso ' + 'supports.')) + + options = arg_parser.parse_args() + + format_str = u'%(asctime)s [%(levelname)s] %(message)s' + if options.debug: + logging.basicConfig(level=logging.DEBUG, format=format_str) + else: + logging.basicConfig(level=logging.INFO, format=format_str) + + try: + front_end.ParseOptions(options, source_option='image') + except errors.BadConfigOption as exception: + arg_parser.print_help() + print u'' + logging.error(u'{0:s}'.format(exception)) + return False + + try: + front_end.ProcessSource(options) + logging.info(u'Processing completed.') + + except (KeyboardInterrupt, errors.UserAbort): + logging.warning(u'Aborted by user.') + return False + + except errors.SourceScannerError as exception: + logging.warning(( + u'Unable to scan for a supported filesystem with error: {0:s}\n' + u'Most likely the image format is not supported by the ' + u'tool.').format(exception)) + return False + + return True + + +if __name__ == '__main__': + if not Main(): + sys.exit(1) + else: + sys.exit(0) diff --git a/plaso/frontend/image_export_test.py b/plaso/frontend/image_export_test.py new file mode 100644 index 0000000..a95bdcb --- /dev/null +++ b/plaso/frontend/image_export_test.py @@ -0,0 +1,237 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the image export front-end.""" + +import glob +import os +import shutil +import tempfile +import unittest + +from dfvfs.lib import definitions +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver import resolver as path_spec_resolver + +from plaso.frontend import image_export +from plaso.frontend import test_lib +from plaso.lib import errors + + +class Log2TimelineFrontendTest(test_lib.FrontendTestCase): + """Tests for the image export front-end.""" + + def setUp(self): + """Sets up the objects used throughout the test.""" + self._temp_directory = tempfile.mkdtemp() + + def tearDown(self): + """Cleans up the objects used throughout the test.""" + shutil.rmtree(self._temp_directory, True) + + def testProcessSourceExtractWithDateFilter(self): + """Tests extract with file filter and date filter functionality.""" + test_front_end = image_export.ImageExportFrontend() + + options = test_lib.Options() + options.image = self._GetTestFilePath([u'image.qcow2']) + options.path = self._temp_directory + options.include_duplicates = True + + options.filter = os.path.join(self._temp_directory, u'filter.txt') + with open(options.filter, 'wb') as file_object: + file_object.write('/a_directory/.+_file\n') + + test_front_end.ParseOptions(options, source_option='image') + + # Set the date filter. + filter_start = '2012-05-25 15:59:00' + filter_end = '2012-05-25 15:59:20' + + date_filter_object = image_export.DateFilter() + date_filter_object.Add( + filter_start=filter_start, filter_end=filter_end, + filter_type='ctime') + image_export.FileSaver.SetDateFilter(date_filter_object) + + test_front_end.ProcessSource(options) + + expected_text_files = sorted([ + os.path.join(self._temp_directory, u'a_directory', u'a_file')]) + + text_files = glob.glob(os.path.join( + self._temp_directory, u'a_directory', u'*')) + + self.assertEquals(sorted(text_files), expected_text_files) + + # We need to reset the date filter to not affect other tests. + # pylint: disable=protected-access + # TODO: Remove this once filtering has been moved to the front end object. + image_export.FileSaver._date_filter = None + + def testProcessSourceExtractWithExtensions(self): + """Tests extract with extensions process source functionality.""" + test_front_end = image_export.ImageExportFrontend() + + options = test_lib.Options() + options.image = self._GetTestFilePath([u'image.qcow2']) + options.path = self._temp_directory + options.extension_string = u'txt' + + test_front_end.ParseOptions(options, source_option='image') + + test_front_end.ProcessSource(options) + + expected_text_files = sorted([ + os.path.join(self._temp_directory, u'passwords.txt')]) + + text_files = glob.glob(os.path.join(self._temp_directory, u'*')) + + self.assertEquals(sorted(text_files), expected_text_files) + + def testProcessSourceExtractWithFilter(self): + """Tests extract with filter process source functionality.""" + test_front_end = image_export.ImageExportFrontend() + + options = test_lib.Options() + options.image = self._GetTestFilePath([u'image.qcow2']) + options.path = self._temp_directory + + options.filter = os.path.join(self._temp_directory, u'filter.txt') + with open(options.filter, 'wb') as file_object: + file_object.write('/a_directory/.+_file\n') + + test_front_end.ParseOptions(options, source_option='image') + + test_front_end.ProcessSource(options) + + expected_text_files = sorted([ + os.path.join(self._temp_directory, u'a_directory', u'another_file'), + os.path.join(self._temp_directory, u'a_directory', u'a_file')]) + + text_files = glob.glob(os.path.join( + self._temp_directory, u'a_directory', u'*')) + + self.assertEquals(sorted(text_files), expected_text_files) + + def testDateFilter(self): + """Test the save file based on date filter function.""" + # Open up a file entry. + path = self._GetTestFilePath([u'ímynd.dd']) + os_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=path) + tsk_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_TSK, inode=16, + location=u'/a_directory/another_file', parent=os_path_spec) + + file_entry = path_spec_resolver.Resolver.OpenFileEntry(tsk_path_spec) + + # Timestamps of file: + # Modified: 2012-05-25T15:59:23+00:00 + # Accessed: 2012-05-25T15:59:23+00:00 + # Created: 2012-05-25T15:59:23+00:00 + + # Create the date filter object. + date_filter = image_export.DateFilter() + + # No date filter set + self.assertTrue( + date_filter.CompareFileEntry(file_entry)) + + # Add a date to the date filter. + date_filter.Add( + filter_start='2012-05-25 15:59:20', filter_end='2012-05-25 15:59:25', + filter_type='ctime') + + self.assertTrue(date_filter.CompareFileEntry(file_entry)) + date_filter.Reset() + + date_filter.Add( + filter_start='2012-05-25 15:59:24', filter_end='2012-05-25 15:59:55', + filter_type='ctime') + self.assertFalse(date_filter.CompareFileEntry(file_entry)) + date_filter.Reset() + + # Testing a timestamp that does not exist in the stat object. + date_filter.Add(filter_type='bkup', filter_start='2012-02-02 12:12:12') + with self.assertRaises(errors.WrongFilterOption): + date_filter.CompareFileEntry(file_entry) + + # Testing adding a badly formatter filter. + with self.assertRaises(errors.WrongFilterOption): + date_filter.Add(filter_type='foobar', filter_start='2012-02-01 01:01:01') + date_filter.Reset() + + # Testing adding a badly formatter filter, no date set. + with self.assertRaises(errors.WrongFilterOption): + date_filter.Add(filter_type='atime') + date_filter.Reset() + + # Just end date set. + date_filter.Add( + filter_end='2012-05-25 15:59:55', filter_type='mtime') + self.assertTrue(date_filter.CompareFileEntry(file_entry)) + date_filter.Reset() + + # Just with a start date but within range. + date_filter.Add( + filter_start='2012-03-25 15:59:55', filter_type='atime') + self.assertTrue(date_filter.CompareFileEntry(file_entry)) + date_filter.Reset() + + # And now with a start date, but out of range. + date_filter.Add( + filter_start='2012-05-25 15:59:55', filter_type='ctime') + self.assertFalse(date_filter.CompareFileEntry(file_entry)) + date_filter.Reset() + + # Test with more than one date filter. + date_filter.Add( + filter_start='2012-05-25 15:59:55', filter_type='ctime', + filter_end='2012-05-25 17:34:12') + date_filter.Add( + filter_start='2012-05-25 15:59:20', filter_end='2012-05-25 15:59:25', + filter_type='atime') + date_filter.Add( + filter_start='2012-05-25 15:59:24', filter_end='2012-05-25 15:59:55', + filter_type='mtime') + self.assertFalse(date_filter.CompareFileEntry(file_entry)) + self.assertEquals(date_filter.number_of_filters, 3) + # Remove a filter. + date_filter.Remove( + filter_start='2012-05-25 15:59:55', filter_type='ctime', + filter_end='2012-05-25 17:34:12') + self.assertEquals(date_filter.number_of_filters, 2) + + # Remove a date filter that does not exist. + date_filter.Remove( + filter_start='2012-05-25 11:59:55', filter_type='ctime', + filter_end='2012-05-25 17:34:12') + self.assertEquals(date_filter.number_of_filters, 2) + + date_filter.Add( + filter_end='2012-05-25 15:59:25', filter_type='atime') + self.assertEquals(date_filter.number_of_filters, 3) + date_filter.Remove( + filter_end='2012-05-25 15:59:25', filter_type='atime') + self.assertEquals(date_filter.number_of_filters, 2) + + date_filter.Reset() + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/frontend/log2timeline.py b/plaso/frontend/log2timeline.py new file mode 100755 index 0000000..b65e661 --- /dev/null +++ b/plaso/frontend/log2timeline.py @@ -0,0 +1,454 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The log2timeline front-end.""" + +import argparse +import logging +import multiprocessing +import sys +import time +import textwrap + +import plaso + +# Registering output modules so that output bypass works. +from plaso import output as _ # pylint: disable=unused-import +from plaso.frontend import frontend +from plaso.frontend import utils as frontend_utils +from plaso.lib import errors +from plaso.parsers import manager as parsers_manager + +import pytz + + +class LoggingFilter(logging.Filter): + """Class that implements basic filtering of log events for plaso. + + Some libraries, like binplist, introduce excessive amounts of + logging that clutters down the debug logs of plaso, making them + almost non-usable. This class implements a filter designed to make + the debug logs more clutter-free. + """ + + def filter(self, record): + """Filter messages sent to the logging infrastructure.""" + if record.module == 'binplist' and record.levelno == logging.DEBUG: + return False + + return True + + +class Log2TimelineFrontend(frontend.ExtractionFrontend): + """Class that implements the log2timeline front-end.""" + + _BYTES_IN_A_MIB = 1024 * 1024 + + def __init__(self): + """Initializes the front-end object.""" + input_reader = frontend.StdinFrontendInputReader() + output_writer = frontend.StdoutFrontendOutputWriter() + + super(Log2TimelineFrontend, self).__init__(input_reader, output_writer) + + def _GetPluginData(self): + """Return a dict object with a list of all available parsers and plugins.""" + return_dict = {} + + # Import all plugins and parsers to print out the necessary information. + # This is not import at top since this is only required if this parameter + # is set, otherwise these libraries get imported in their respected + # locations. + + # The reason why some of these libraries are imported as '_' is to make sure + # all appropriate parsers and plugins are registered, yet we don't need to + # directly call these libraries, it is enough to load them up to get them + # registered. + + # TODO: remove this hack includes should be a the top if this does not work + # remove the need for implicit behavior on import. + from plaso import filters + from plaso import parsers as _ + from plaso import output as _ + from plaso.frontend import presets + from plaso.lib import output + + return_dict['Versions'] = [ + ('plaso engine', plaso.GetVersion()), + ('python', sys.version)] + + return_dict['Parsers'] = [] + for _, parser_class in parsers_manager.ParsersManager.GetParsers(): + description = getattr(parser_class, 'DESCRIPTION', u'') + return_dict['Parsers'].append((parser_class.NAME, description)) + + return_dict['Parser Lists'] = [] + for category, parsers in sorted(presets.categories.items()): + return_dict['Parser Lists'].append((category, ', '.join(parsers))) + + return_dict['Output Modules'] = [] + for name, description in sorted(output.ListOutputFormatters()): + return_dict['Output Modules'].append((name, description)) + + return_dict['Plugins'] = [] + for _, parser_class in parsers_manager.ParsersManager.GetParsers(): + if parser_class.SupportsPlugins(): + for _, plugin_class in parser_class.GetPlugins(): + description = getattr(plugin_class, 'DESCRIPTION', u'') + return_dict['Plugins'].append((plugin_class.NAME, description)) + + return_dict['Filters'] = [] + for filter_obj in sorted(filters.ListFilters()): + doc_string, _, _ = filter_obj.__doc__.partition('\n') + return_dict['Filters'].append((filter_obj.filter_name, doc_string)) + + return return_dict + + def _GetTimeZones(self): + """Returns a generator of the names of all the supported time zones.""" + yield 'local' + for zone in pytz.all_timezones: + yield zone + + def ListPluginInformation(self): + """Lists all plugin and parser information.""" + plugin_list = self._GetPluginData() + return_string_pieces = [] + + return_string_pieces.append( + u'{:=^80}'.format(u' log2timeline/plaso information. ')) + + for header, data in plugin_list.items(): + # TODO: Using the frontend utils here instead of "self.PrintHeader" + # since the desired output here is a string that can be sent later + # to an output writer. Change this entire function so it can utilize + # PrintHeader or something similar. + return_string_pieces.append(frontend_utils.FormatHeader(header)) + for entry_header, entry_data in sorted(data): + return_string_pieces.append( + frontend_utils.FormatOutputString(entry_header, entry_data)) + + return_string_pieces.append(u'') + self._output_writer.Write(u'\n'.join(return_string_pieces)) + + def ListTimeZones(self): + """Lists the time zones.""" + self._output_writer.Write(u'=' * 40) + self._output_writer.Write(u' ZONES') + self._output_writer.Write(u'-' * 40) + for timezone in self._GetTimeZones(): + self._output_writer.Write(u' {0:s}'.format(timezone)) + self._output_writer.Write(u'=' * 40) + + +def Main(): + """Start the tool.""" + multiprocessing.freeze_support() + + front_end = Log2TimelineFrontend() + + epilog = u'\n'.join([ + u'', + u'Example usage:', + u'', + u'Run the tool against an image (full kitchen sink)', + u' log2timeline.py /cases/mycase/plaso.dump ímynd.dd', + u'', + u'Instead of answering questions, indicate some of the options on the', + u'command line (including data from particular VSS stores).', + (u' log2timeline.py -o 63 --vss_stores 1,2 /cases/plaso_vss.dump ' + u'image.E01'), + u'', + u'And that\'s how you build a timeline using log2timeline...', + u'']) + + description = u'\n'.join([ + u'', + u'log2timeline is the main front-end to the plaso back-end, used to', + u'collect and correlate events extracted from a filesystem.', + u'', + u'More information can be gathered from here:', + u' http://plaso.kiddaland.net/usage/log2timeline', + u'']) + + arg_parser = argparse.ArgumentParser( + description=textwrap.dedent(description), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=textwrap.dedent(epilog), add_help=False) + + # Create few argument groups to make formatting help messages clearer. + info_group = arg_parser.add_argument_group('Informational Arguments') + function_group = arg_parser.add_argument_group('Functional Arguments') + deep_group = arg_parser.add_argument_group('Deep Analysis Arguments') + performance_group = arg_parser.add_argument_group('Performance Arguments') + + function_group.add_argument( + '-z', '--zone', '--timezone', dest='timezone', action='store', type=str, + default='UTC', help=( + u'Define the timezone of the IMAGE (not the output). This is usually ' + u'discovered automatically by preprocessing but might need to be ' + u'specifically set if preprocessing does not properly detect or to ' + u'overwrite the detected time zone.')) + + function_group.add_argument( + '-t', '--text', dest='text_prepend', action='store', type=unicode, + default=u'', metavar='TEXT', help=( + u'Define a free form text string that is prepended to each path ' + u'to make it easier to distinguish one record from another in a ' + u'timeline (like c:\\, or host_w_c:\\)')) + + function_group.add_argument( + '--parsers', dest='parsers', type=str, action='store', default='', + metavar='PARSER_LIST', help=( + u'Define a list of parsers to use by the tool. This is a comma ' + u'separated list where each entry can be either a name of a parser ' + u'or a parser list. Each entry can be prepended with a minus sign ' + u'to negate the selection (exclude it). The list match is an ' + u'exact match while an individual parser matching is a case ' + u'insensitive substring match, with support for glob patterns. ' + u'Examples would be: "reg" that matches the substring "reg" in ' + u'all parser names or the glob pattern "sky[pd]" that would match ' + u'all parsers that have the string "skyp" or "skyd" in it\'s name. ' + u'All matching is case insensitive.')) + + info_group.add_argument( + '-h', '--help', action='help', help=u'Show this help message and exit.') + + info_group.add_argument( + '--logfile', action='store', metavar='FILENAME', dest='logfile', + type=unicode, default=u'', help=( + u'If defined all log messages will be redirected to this file ' + u'instead the default STDERR.')) + + function_group.add_argument( + '-p', '--preprocess', dest='preprocess', action='store_true', + default=False, help=( + u'Turn on preprocessing. Preprocessing is turned on by default ' + u'when parsing image files, however if a mount point is being ' + u'parsed then this parameter needs to be set manually.')) + + front_end.AddPerformanceOptions(performance_group) + + performance_group.add_argument( + '--workers', dest='workers', action='store', type=int, default=0, + help=(u'The number of worker threads [defaults to available system ' + u'CPU\'s minus three].')) + + # TODO: seems to be no longer used, remove. + # function_group.add_argument( + # '-i', '--image', dest='image', action='store_true', default=False, + # help=( + # 'Indicates that this is an image instead of a regular file. It is ' + # 'not necessary to include this option if -o (offset) is used, then ' + # 'this option is assumed. Use this when parsing an image with an ' + # 'offset of zero.')) + + front_end.AddVssProcessingOptions(deep_group) + + performance_group.add_argument( + '--single_thread', '--single-thread', '--single_process', + '--single-process', dest='single_process', action='store_true', + default=False, help=( + u'Indicate that the tool should run in a single process.')) + + function_group.add_argument( + '-f', '--file_filter', '--file-filter', dest='file_filter', + action='store', type=unicode, default=None, help=( + u'List of files to include for targeted collection of files to ' + u'parse, one line per file path, setup is /path|file - where each ' + u'element can contain either a variable set in the preprocessing ' + u'stage or a regular expression.')) + + deep_group.add_argument( + '--scan_archives', dest='open_files', action='store_true', default=False, + help=argparse.SUPPRESS) + + # This option is "hidden" for the time being, still left in there for testing + # purposes, but hidden from the tool usage and help messages. + # help=('Indicate that the tool should try to open files to extract embedd' + # 'ed files within them, for instance to extract files from compress' + # 'ed containers, etc. Be AWARE THAT THIS IS EXTREMELY SLOW.')) + + front_end.AddImageOptions(function_group) + + function_group.add_argument( + '--partition', dest='partition_number', action='store', type=int, + default=None, help=( + u'Choose a partition number from a disk image. This partition ' + u'number should correspond to the partion number on the disk ' + u'image, starting from partition 1.')) + + # Build the version information. + version_string = u'log2timeline - plaso back-end {0:s}'.format( + plaso.GetVersion()) + + info_group.add_argument( + '-v', '--version', action='version', version=version_string, + help=u'Show the current version of the back-end.') + + info_group.add_argument( + '--info', dest='show_info', action='store_true', default=False, + help=u'Print out information about supported plugins and parsers.') + + info_group.add_argument( + '--show_memory_usage', '--show-memory-usage', action='store_true', + default=False, dest='foreman_verbose', help=( + u'Indicates that basic memory usage should be included in the ' + u'output of the process monitor. If this option is not set the ' + u'tool only displays basic status and counter information.')) + + info_group.add_argument( + '--disable_worker_monitor', '--disable-worker-monitor', + action='store_false', default=True, dest='foreman_enabled', help=( + u'Turn off the foreman. The foreman monitors all worker processes ' + u'and periodically prints out information about all running workers.' + u'By default the foreman is run, but it can be turned off using this ' + u'parameter.')) + + front_end.AddExtractionOptions(function_group) + + function_group.add_argument( + '--output', dest='output_module', action='store', type=unicode, + default='', help=( + u'Bypass the storage module directly storing events according to ' + u'the output module. This means that the output will not be in the ' + u'pstorage format but in the format chosen by the output module. ' + u'[Please not this feature is EXPERIMENTAL at this time, use at ' + u'own risk (eg. sqlite output does not yet work)]')) + + function_group.add_argument( + '--serializer-format', '--serializer_format', dest='serializer_format', + action='store', default='proto', metavar='FORMAT', help=( + u'By default the storage uses protobufs for serializing event ' + u'objects. This parameter can be used to change that behavior. ' + u'The choices are "proto" and "json".')) + + front_end.AddInformationalOptions(info_group) + + arg_parser.add_argument( + 'output', action='store', metavar='STORAGE_FILE', nargs='?', + type=unicode, help=( + u'The path to the output file, if the file exists it will get ' + u'appended to.')) + + arg_parser.add_argument( + 'source', action='store', metavar='SOURCE', + nargs='?', type=unicode, help=( + u'The path to the source device, file or directory. If the source is ' + u'a supported storage media device or image file, archive file or ' + u'a directory, the files within are processed recursively.')) + + arg_parser.add_argument( + 'filter', action='store', metavar='FILTER', nargs='?', default=None, + type=unicode, help=( + u'A filter that can be used to filter the dataset before it ' + u'is written into storage. More information about the filters ' + u'and it\'s usage can be found here: http://plaso.kiddaland.' + u'net/usage/filters')) + + # Properly prepare the attributes according to local encoding. + if front_end.preferred_encoding == 'ascii': + logging.warning( + u'The preferred encoding of your system is ASCII, which is not optimal ' + u'for the typically non-ASCII characters that need to be parsed and ' + u'processed. The tool will most likely crash and die, perhaps in a way ' + u'that may not be recoverable. A five second delay is introduced to ' + u'give you time to cancel the runtime and reconfigure your preferred ' + u'encoding, otherwise continue at own risk.') + time.sleep(5) + + u_argv = [x.decode(front_end.preferred_encoding) for x in sys.argv] + sys.argv = u_argv + try: + options = arg_parser.parse_args() + except UnicodeEncodeError: + # If we get here we are attempting to print help in a "dumb" terminal. + print arg_parser.format_help().encode(front_end.preferred_encoding) + return False + + if options.timezone == 'list': + front_end.ListTimeZones() + return True + + if options.show_info: + front_end.ListPluginInformation() + return True + + format_str = ( + u'%(asctime)s [%(levelname)s] (%(processName)-10s) PID:%(process)d ' + u'<%(module)s> %(message)s') + + if options.debug: + if options.logfile: + logging.basicConfig( + level=logging.DEBUG, format=format_str, filename=options.logfile) + else: + logging.basicConfig(level=logging.DEBUG, format=format_str) + + logging_filter = LoggingFilter() + root_logger = logging.getLogger() + root_logger.addFilter(logging_filter) + elif options.logfile: + logging.basicConfig( + level=logging.INFO, format=format_str, filename=options.logfile) + else: + logging.basicConfig(level=logging.INFO, format=format_str) + + if not options.output: + arg_parser.print_help() + print u'' + arg_parser.print_usage() + print u'' + logging.error(u'Wrong usage: need to define an output.') + return False + + try: + front_end.ParseOptions(options) + front_end.SetStorageFile(options.output) + except errors.BadConfigOption as exception: + arg_parser.print_help() + print u'' + logging.error(u'{0:s}'.format(exception)) + return False + + # Configure the foreman (monitors workers). + front_end.SetShowMemoryInformation(show_memory=options.foreman_verbose) + front_end.SetRunForeman(run_foreman=options.foreman_enabled) + + try: + front_end.ProcessSource(options) + logging.info(u'Processing completed.') + + except (KeyboardInterrupt, errors.UserAbort): + logging.warning(u'Aborted by user.') + return False + + except errors.SourceScannerError as exception: + logging.warning(( + u'Unable to scan for a supported filesystem with error: {0:s}\n' + u'Most likely the image format is not supported by the ' + u'tool.').format(exception)) + return False + + return True + + +if __name__ == '__main__': + if not Main(): + sys.exit(1) + else: + sys.exit(0) diff --git a/plaso/frontend/log2timeline_test.py b/plaso/frontend/log2timeline_test.py new file mode 100644 index 0000000..ab7dabe --- /dev/null +++ b/plaso/frontend/log2timeline_test.py @@ -0,0 +1,75 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the log2timeline front-end.""" + +import os +import shutil +import tempfile +import unittest + +from plaso.frontend import log2timeline +from plaso.frontend import test_lib +from plaso.lib import pfilter +from plaso.lib import storage + + +class Log2TimelineFrontendTest(test_lib.FrontendTestCase): + """Tests for the log2timeline front-end.""" + + def setUp(self): + """Sets up the objects used throughout the test.""" + # This is necessary since TimeRangeCache uses class members. + # TODO: remove this work around and properly fix TimeRangeCache. + pfilter.TimeRangeCache.ResetTimeConstraints() + + self._temp_directory = tempfile.mkdtemp() + + def tearDown(self): + """Cleans up the objects used throughout the test.""" + shutil.rmtree(self._temp_directory, True) + + def testGetStorageInformation(self): + """Tests the get storage information function.""" + test_front_end = log2timeline.Log2TimelineFrontend() + + options = test_lib.Options() + options.source = self._GetTestFilePath([u'ímynd.dd']) + + storage_file_path = os.path.join(self._temp_directory, u'plaso.db') + + test_front_end.ParseOptions(options) + test_front_end.SetStorageFile(storage_file_path=storage_file_path) + test_front_end.SetRunForeman(run_foreman=False) + + test_front_end.ProcessSource(options) + + try: + storage_file = storage.StorageFile(storage_file_path, read_only=True) + except IOError: + # This is not a storage file, we should fail. + self.assertTrue(False) + + # Make sure we can read an event out of the storage. + event_object = storage_file.GetSortedEntry() + self.assertIsNotNone(event_object) + + # TODO: add more tests that cover more of the functionality of the frontend. + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/frontend/pinfo.py b/plaso/frontend/pinfo.py new file mode 100755 index 0000000..66f5bf5 --- /dev/null +++ b/plaso/frontend/pinfo.py @@ -0,0 +1,266 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""A simple dump information gathered from a plaso storage container. + +pinfo stands for Plaso INniheldurFleiriOrd or plaso contains more words. +""" +# TODO: To make YAML loading work. + +import argparse +import logging +import pprint +import sys + +from plaso.frontend import frontend +from plaso.lib import errors +from plaso.lib import timelib + + +class PinfoFrontend(frontend.AnalysisFrontend): + """Class that implements the pinfo front-end.""" + + def __init__(self): + """Initializes the front-end object.""" + input_reader = frontend.StdinFrontendInputReader() + output_writer = frontend.StdoutFrontendOutputWriter() + + super(PinfoFrontend, self).__init__(input_reader, output_writer) + + self._printer = pprint.PrettyPrinter(indent=8) + self._verbose = False + + def _AddCollectionInformation(self, lines_of_text, collection_information): + """Adds the lines of text that make up the collection information. + + Args: + lines_of_text: A list containing the lines of text. + collection_information: The collection information dict. + """ + filename = collection_information.get('file_processed', 'N/A') + time_of_run = collection_information.get('time_of_run', 0) + time_of_run = timelib.Timestamp.CopyToIsoFormat(time_of_run) + + lines_of_text.append(u'Storage file:\t\t{0:s}'.format( + self._storage_file_path)) + lines_of_text.append(u'Source processed:\t{0:s}'.format(filename)) + lines_of_text.append(u'Time of processing:\t{0:s}'.format(time_of_run)) + + lines_of_text.append(u'') + lines_of_text.append(u'Collection information:') + + for key, value in collection_information.items(): + if key not in ['file_processed', 'time_of_run']: + lines_of_text.append(u'\t{0:s} = {1!s}'.format(key, value)) + + def _AddCounterInformation( + self, lines_of_text, description, counter_information): + """Adds the lines of text that make up the counter information. + + Args: + lines_of_text: A list containing the lines of text. + description: The counter information description. + counter_information: The counter information dict. + """ + lines_of_text.append(u'') + lines_of_text.append(u'{0:s}:'.format(description)) + + for key, value in counter_information.most_common(): + lines_of_text.append(u'\tCounter: {0:s} = {1:d}'.format(key, value)) + + def _AddHeader(self, lines_of_text): + """Adds the lines of text that make up the header. + + Args: + lines_of_text: A list containing the lines of text. + """ + lines_of_text.append(u'-' * self._LINE_LENGTH) + lines_of_text.append(u'\t\tPlaso Storage Information') + lines_of_text.append(u'-' * self._LINE_LENGTH) + + def _AddStoreInformation(self, lines_of_text, store_information): + """Adds the lines of text that make up the store information. + + Args: + lines_of_text: A list containing the lines of text. + store_information: The store information dict. + """ + lines_of_text.append(u'') + lines_of_text.append(u'Store information:') + lines_of_text.append(u'\tNumber of available stores: {0:d}'.format( + store_information['Number'])) + + if not self._verbose: + lines_of_text.append( + u'\tStore information details omitted (to see use: --verbose)') + else: + for key, value in store_information.iteritems(): + if key not in ['Number']: + lines_of_text.append( + u'\t{0:s} =\n{1!s}'.format(key, self._printer.pformat(value))) + + def _FormatStorageInformation(self, info, storage_file, last_entry=False): + """Formats the storage information. + + Args: + info: The storage information object (instance of PreprocessObject). + storage_file: The storage file (instance of StorageFile). + last_entry: Optional boolean value to indicate this is the last + information entry. The default is False. + + Returns: + A string containing the formatted storage information. + """ + lines_of_text = [] + + collection_information = getattr(info, 'collection_information', None) + if collection_information: + self._AddHeader(lines_of_text) + self._AddCollectionInformation(lines_of_text, collection_information) + else: + lines_of_text.append(u'Missing collection information.') + + counter_information = getattr(info, 'counter', None) + if counter_information: + self._AddCounterInformation( + lines_of_text, u'Parser counter information', counter_information) + + counter_information = getattr(info, 'plugin_counter', None) + if counter_information: + self._AddCounterInformation( + lines_of_text, u'Plugin counter information', counter_information) + + store_information = getattr(info, 'stores', None) + if store_information: + self._AddStoreInformation(lines_of_text, store_information) + + information = u'\n'.join(lines_of_text) + + if not self._verbose: + preprocessing = ( + u'Preprocessing information omitted (to see use: --verbose).') + else: + preprocessing = u'Preprocessing information:\n' + for key, value in info.__dict__.items(): + if key == 'collection_information': + continue + elif key == 'counter' or key == 'stores': + continue + if isinstance(value, list): + preprocessing += u'\t{0:s} =\n{1!s}\n'.format( + key, self._printer.pformat(value)) + else: + preprocessing += u'\t{0:s} = {1!s}\n'.format(key, value) + + if not last_entry: + reports = u'' + elif storage_file.HasReports(): + reports = u'Reporting information omitted (to see use: --verbose).' + else: + reports = u'No reports stored.' + + if self._verbose and last_entry and storage_file.HasReports(): + report_list = [] + for report in storage_file.GetReports(): + report_list.append(report.GetString()) + reports = u'\n'.join(report_list) + + return u'\n'.join([ + information, u'', preprocessing, u'', reports, u'-+' * 40]) + + def GetStorageInformation(self): + """Returns a formatted storage information generator.""" + try: + storage_file = self.OpenStorageFile() + except IOError as exception: + logging.error( + u'Unable to open storage file: {0:s} with error: {1:s}'.format( + self._storage_file_path, exception)) + return + + list_of_storage_information = storage_file.GetStorageInformation() + if not list_of_storage_information: + yield '' + return + + last_entry = False + + for index, info in enumerate(list_of_storage_information): + if index + 1 == len(list_of_storage_information): + last_entry = True + yield self._FormatStorageInformation( + info, storage_file, last_entry=last_entry) + + def ParseOptions(self, options): + """Parses the options and initializes the front-end. + + Args: + options: the command line arguments (instance of argparse.Namespace). + + Raises: + BadConfigOption: if the options are invalid. + """ + super(PinfoFrontend, self).ParseOptions(options) + + self._verbose = getattr(options, 'verbose', False) + + +def Main(): + """Start the tool.""" + front_end = PinfoFrontend() + + usage = """ +Gives you information about the storage file, how it was +collected, what information was gained from the image, etc. + """ + arg_parser = argparse.ArgumentParser(description=usage) + + format_str = '[%(levelname)s] %(message)s' + logging.basicConfig(level=logging.INFO, format=format_str) + + arg_parser.add_argument( + '-v', '--verbose', dest='verbose', action='store_true', default=False, + help='Be extra verbose in the information printed out.') + + front_end.AddStorageFileOptions(arg_parser) + + options = arg_parser.parse_args() + + try: + front_end.ParseOptions(options) + except errors.BadConfigOption as exception: + arg_parser.print_help() + print u'' + logging.error(u'{0:s}'.format(exception)) + return False + + storage_information_found = False + for storage_information in front_end.GetStorageInformation(): + storage_information_found = True + print storage_information.encode(front_end.preferred_encoding) + + if not storage_information_found: + print u'No Plaso storage information found.' + + return True + + +if __name__ == '__main__': + if not Main(): + sys.exit(1) + else: + sys.exit(0) diff --git a/plaso/frontend/pinfo_test.py b/plaso/frontend/pinfo_test.py new file mode 100644 index 0000000..e7ed674 --- /dev/null +++ b/plaso/frontend/pinfo_test.py @@ -0,0 +1,65 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for test pinfo front-end.""" + +import os +import unittest + +from plaso.frontend import pinfo +from plaso.frontend import test_lib + + +class PinfoFrontendTest(test_lib.FrontendTestCase): + """Tests for test pinfo front-end.""" + + def testGetStorageInformation(self): + """Tests the get storage information function.""" + test_front_end = pinfo.PinfoFrontend() + + options = test_lib.Options() + options.storage_file = os.path.join(self._TEST_DATA_PATH, 'psort_test.out') + + test_front_end.ParseOptions(options) + + storage_information_list = list(test_front_end.GetStorageInformation()) + + self.assertEquals(len(storage_information_list), 1) + + lines_of_text = storage_information_list[0].split(u'\n') + + expected_line_of_text = u'-' * 80 + self.assertEquals(lines_of_text[0], expected_line_of_text) + self.assertEquals(lines_of_text[2], expected_line_of_text) + + self.assertEquals(lines_of_text[1], u'\t\tPlaso Storage Information') + + expected_line_of_text = u'Storage file:\t\t{0:s}'.format( + options.storage_file) + self.assertEquals(lines_of_text[3], expected_line_of_text) + + self.assertEquals(lines_of_text[4], u'Source processed:\tsyslog') + + expected_line_of_text = u'Time of processing:\t2014-02-15T04:33:16+00:00' + self.assertEquals(lines_of_text[5], expected_line_of_text) + + self.assertEquals(lines_of_text[6], u'') + self.assertEquals(lines_of_text[7], u'Collection information:') + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/frontend/plasm.py b/plaso/frontend/plasm.py new file mode 100755 index 0000000..0e756aa --- /dev/null +++ b/plaso/frontend/plasm.py @@ -0,0 +1,832 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the plasm front-end to plaso.""" + +import argparse +import hashlib +import logging +import operator +import os +import pickle +import sets +import sys +import textwrap + +from plaso import filters + +from plaso.frontend import frontend +from plaso.lib import errors +from plaso.lib import event +from plaso.lib import output as output_lib +from plaso.lib import storage +from plaso.output import pstorage # pylint: disable=unused-import + + +class PlasmFrontend(frontend.AnalysisFrontend): + """Class that implements the psort front-end.""" + + def __init__(self): + """Initializes the front-end object.""" + input_reader = frontend.StdinFrontendInputReader() + output_writer = frontend.StdoutFrontendOutputWriter() + + super(PlasmFrontend, self).__init__(input_reader, output_writer) + + self._cluster_closeness = None + self._cluster_threshold = None + self._quiet = False + self._tagging_file_path = None + + self.mode = None + + def ClusterEvents(self): + """Clusters the event objects in the storage file.""" + clustering_engine = ClusteringEngine( + self._storage_file_path, self._cluster_threshold, + self._cluster_closeness) + clustering_engine.Run() + + def GroupEvents(self): + """Groups the event objects in the storage file. + + Raises: + RuntimeError: if a non-recoverable situation is encountered. + """ + if not self._quiet: + self._output_writer.Write(u'Grouping tagged events.\n') + + try: + storage_file = self.OpenStorageFile(read_only=False) + except IOError as exception: + raise RuntimeError( + u'Unable to open storage file: {0:s} with error: {1:s}.'.format( + self._storage_file_path, exception)) + + grouping_engine = GroupingEngine() + grouping_engine.Run(storage_file, quiet=self._quiet) + storage_file.Close() + + if not self._quiet: + self._output_writer.Write(u'Grouping DONE.\n') + + def TagEvents(self): + """Tags the event objects in the storage file.""" + tagging_engine = TaggingEngine( + self._storage_file_path, self._tagging_file_path, quiet=self._quiet) + tagging_engine.Run() + + def ParseOptions(self, options): + """Parses the options and initializes the front-end. + + Args: + options: the command line arguments (instance of argparse.Namespace). + + Raises: + BadConfigOption: if the options are invalid. + """ + super(PlasmFrontend, self).ParseOptions(options) + + self.mode = getattr(options, 'subcommand', None) + if not self.mode: + raise errors.BadConfigOption(u'Missing mode subcommand.') + + if self.mode not in ['cluster', 'group', 'tag']: + raise errors.BadConfigOption( + u'Unsupported mode subcommand: {0:s}.'.format(self.mode)) + + if self.mode == 'cluster': + self._cluster_threshold = getattr(options, 'cluster_threshold', None) + if not self._cluster_threshold: + raise errors.BadConfigOption(u'Missing cluster threshold value.') + + try: + self._cluster_threshold = int(self._cluster_threshold, 10) + except ValueError: + raise errors.BadConfigOption(u'Invalid cluster threshold value.') + + self._cluster_closeness = getattr(options, 'cluster_closeness', None) + if not self._cluster_closeness: + raise errors.BadConfigOption(u'Missing cluster closeness value.') + + try: + self._cluster_closeness = int(self._cluster_closeness, 10) + except ValueError: + raise errors.BadConfigOption(u'Invalid cluster closeness value.') + + elif self.mode == 'tag': + tagging_file_path = getattr(options, 'tag_filename', None) + if not tagging_file_path: + raise errors.BadConfigOption(u'Missing tagging file path.') + + if not os.path.isfile(tagging_file_path): + errors.BadConfigOption( + u'No such tagging file: {0:s}'.format(tagging_file_path)) + + self._tagging_file_path = tagging_file_path + + +def SetupStorage(input_file_path, pre_obj=None): + """Sets up the storage object. + + Attempts to initialize a storage file. If we fail on a IOError, for which + a common cause are typos, log a warning and gracefully exit. + + Args: + input_file_path: Filesystem path to the plaso storage container. + pre_obj: A plaso preprocessing object. + + Returns: + A storage.StorageFile object. + """ + try: + return storage.StorageFile( + input_file_path, pre_obj=pre_obj, read_only=False) + except IOError as exception: + logging.error(u'IO ERROR: {0:s}'.format(exception)) + else: + logging.error(u'Other Critical Failure Reading Files') + sys.exit(1) + + +def EventObjectGenerator(plaso_storage, quiet=False): + """Yields EventObject objects. + + Yields event_objects out of a StorageFile object. If the 'quiet' argument + is not present, it also outputs 50 '.'s indicating progress. + + Args: + plaso_storage: a storage.StorageFile object. + quiet: boolean value indicating whether to suppress progress output. + + Yields: + EventObject objects. + """ + total_events = plaso_storage.GetNumberOfEvents() + if total_events > 0: + events_per_dot = operator.floordiv(total_events, 80) + counter = 0 + else: + quiet = True + + event_object = plaso_storage.GetSortedEntry() + while event_object: + if not quiet: + counter += 1 + if counter % events_per_dot == 0: + sys.stdout.write(u'.') + sys.stdout.flush() + yield event_object + event_object = plaso_storage.GetSortedEntry() + + +def ParseTaggingFile(tag_input): + """Parses Tagging Input file. + + Parses a tagging input file and returns a dictionary of tags, where each + key represents a tag and each entry is a list of plaso filters. + + Args: + tag_input: filesystem path to the tagging input file. + + Returns: + A dictionary whose keys are tags and values are EventObjectFilter objects. + """ + with open(tag_input, 'rb') as tag_input_file: + tags = {} + current_tag = u'' + for line in tag_input_file: + line_rstrip = line.rstrip() + line_strip = line_rstrip.lstrip() + if not line_strip or line_strip.startswith(u'#'): + continue + if not line_rstrip[0].isspace(): + current_tag = line_rstrip + tags[current_tag] = [] + else: + if not current_tag: + continue + compiled_filter = filters.GetFilter(line_strip) + if compiled_filter: + if compiled_filter not in tags[current_tag]: + tags[current_tag].append(compiled_filter) + else: + logging.warning(u'Tag "{0:s}" contains invalid filter: {1:s}'.format( + current_tag, line_strip)) + return tags + + +class TaggingEngine(object): + """Class that defines a tagging engine.""" + + def __init__(self, target_filename, tag_input, quiet=False): + """Initializes the tagging engine object. + + Args: + target_filename: filename for a Plaso storage file to be tagged. + tag_input: filesystem path to the tagging input file. + quiet: Optional boolean value to indicate the progress output should + be suppressed. The default is False. + """ + self.target_filename = target_filename + self.tag_input = tag_input + self._quiet = quiet + + def Run(self): + """Iterates through a Plaso Store file, tagging events according to the + tagging input file specified on the command line. It writes the tagging + information to the Plaso Store file.""" + pre_obj = event.PreprocessObject() + pre_obj.collection_information = {} + pre_obj.collection_information['file_processed'] = self.target_filename + pre_obj.collection_information['method'] = u'Applying tags.' + pre_obj.collection_information['tag_file'] = self.tag_input + pre_obj.collection_information['tagging_engine'] = u'plasm' + + if not self._quiet: + sys.stdout.write(u'Applying tags...\n') + with SetupStorage(self.target_filename, pre_obj) as store: + tags = ParseTaggingFile(self.tag_input) + num_tags = 0 + event_tags = [] + for event_object in EventObjectGenerator(store, self._quiet): + matched_tags = [] + for tag, my_filters in tags.iteritems(): + for my_filter in my_filters: + if my_filter.Match(event_object): + matched_tags.append(tag) + # Don't want to evaluate other tags once a tag is discovered. + break + if len(matched_tags) > 0: + event_tag = event.EventTag() + event_tag.store_number = getattr(event_object, 'store_number') + event_tag.store_index = getattr(event_object, 'store_index') + event_tag.comment = u'Tag applied by PLASM tagging engine' + event_tag.tags = matched_tags + event_tags.append(event_tag) + num_tags += 1 + store.StoreTagging(event_tags) + + if not self._quiet: + sys.stdout.write(u'DONE (applied {} tags)\n'.format(num_tags)) + + +class GroupingEngine(object): + """Class that defines a grouping engine.""" + + def _GroupEvents(self, storage_file, tags, quiet=False): + """Separates each tag list into groups, and writes them to the Plaso Store. + + Args: + storage_file: the storage file (instance of StorageFile). + tags: dictionary of the form {tag: [event_object, ...]}. + quiet: suppress the progress output (default: False). + """ + # TODO(ojensen): make this smarter - for now, separates via time interval. + time_interval = 1000000 # 1 second. + groups = [] + for tag in tags: + if not quiet: + sys.stdout.write(u' proccessing tag "{0:s}"...\n'.format(tag)) + locations = tags[tag] + last_time = 0 + groups_in_tag = 0 + for location in locations: + store_number, store_index = location + # TODO(ojensen): getting higher number event_objects seems to be slow. + event_object = storage_file.GetEventObject(store_number, store_index) + if not hasattr(event_object, 'timestamp'): + continue + timestamp = getattr(event_object, 'timestamp') + if timestamp - last_time > time_interval: + groups_in_tag += 1 + groups.append(type('obj', (object,), { + 'name': u'{0:s}:{1:d}'.format(tag, groups_in_tag), + 'category': tag, + 'events': [location]})) + else: + groups[-1].events.append(location) + last_time = timestamp + + return groups + + # TODO: move this functionality to storage. + def _ReadTags(self, storage_file): + """Iterates through an opened Plaso Store, creating a dictionary of tags + pointing to a list of events. + + Args: + storage_file: the storage file (instance of StorageFile). + """ + all_tags = {} + for event_tag in storage_file.GetTagging(): + tags = event_tag.tags + location = (event_tag.store_number, event_tag.store_index) + for tag in tags: + if tag in all_tags: + all_tags[tag].append(location) + else: + all_tags[tag] = [location] + return all_tags + + def Run(self, storage_file, quiet=False): + """Iterates through a tagged Plaso Store file, grouping events with the same + tag into groups indicating a single instance of an action. It writes the + grouping information to the Plaso Store file. + + Args: + storage_file: the storage file (instance of StorageFile). + quiet: Optional boolean value to indicate the progress output should + be suppressed. The default is False. + """ + if not storage_file.HasTagging(): + logging.error(u'Plaso storage file does not contain tagged events') + return + + tags = self._ReadTags(storage_file) + groups = self._GroupEvents(storage_file, tags, quiet) + + storage_file.StoreGrouping(groups) + + +class ClusteringEngine(object): + """Clusters events in a Plaso Store to assist Tag Input creation. + + Most methods in this class are staticmethods, to avoid relying excessively on + internal state, and to maintain a clear description of which method acts on + what data. + """ + + IGNORE_BASE = frozenset([ + 'hostname', 'timestamp_desc', 'plugin', 'parser', 'user_sid', + 'registry_type', 'computer_name', 'offset', 'allocated', 'file_size', + 'record_number']) + + def __init__(self, target_filename, threshold, closeness): + """Constructor for the Clustering Engine. + + Args: + target_filename: filename for a Plaso storage file to be clustered. + threshold: support threshold for pruning attributes and event types. + closeness: number of milliseconds to cut off the closeness function. + """ + self.target_filename = target_filename + self.threshold = threshold + self.closeness = closeness + sys.stdout.write("Support threshold: {0:d}\nCloseness: {1:d}ms\n\n".format( + threshold, closeness)) + + self.ignore = False + self.frequent_words = [] + self.vector_size = 20000 + + @staticmethod + def HashFile(filename, block_size=2**20): + """Calculates an md5sum of a file from a given filename. + + Returns an MD5 (hash) in ASCII characters, used for naming incremental + progress files that are written to disk. + + Args: + filename: the file to be hashed. + block_size: (optional) block size. + """ + md5 = hashlib.md5() + with open(filename, 'rb') as f: + while True: + data = f.read(block_size) + if not data: + break + md5.update(data) + return md5.hexdigest() + + @staticmethod + def StringJoin(first, second): + """Joins two strings together with a separator. + + In spite of being fairly trivial, this is separated out as a function of + its own to ensure it stays consistent, as it happens in multiple places in + the code. + + Args: + first: first string. + second: second string. + """ + return u':||:'.join([unicode(first), unicode(second)]) + + @staticmethod + def PreHash(field_name, attribute): + """Constructs a string fit to be hashed from an event_object attribute. + + Takes both the attribute's name and value, and produces a consistent string + representation. This string can then be hashed to produce a consistent + name/value hash (see hash_attr). + + Args: + field_name: an event_object attribute name. + attribute: the corresponding event_object attribute. + """ + if type(attribute) in [dict, sets.Set]: + value = repr(sorted(attribute.items())) + else: + value = unicode(attribute) + return ClusteringEngine.StringJoin(field_name, value) + + @staticmethod + def HashAttr(field_name, attribute, vector_size): + """Consistently hashes an event_object attribute/value pair. + + Uses pre_hash to generate a consistent string representation of the + attribute, and then hashes and mods it down to fit within the vector_size. + + Args: + field_name: an event_object attribute name. + attribute: the corresponding event_object attribute. + """ + return hash(ClusteringEngine.PreHash(field_name, attribute)) % vector_size + + @staticmethod + def EventRepresentation(event_object, ignore, frequent_words=None): + """Constructs a consistent representation of an event_object. + + Returns a dict representing our view of an event_object, stripping out + attributes we ignore. If the frequent_words parameter is set, this strips + out any attribute not listed therein as well. Attribute list order is + undefined, i.e. event_object list attributes are treated as sets instead of + lists. + + Args: + event_object: a Plaso event_object. + ignore: a list or set of event_object attributes to ignore. + frequent_words: (optional) whitelist of attributes not to ignore. + """ + if not frequent_words: + frequent_words = [] + + event_field_names = event_object.GetAttributes().difference(ignore) + representation = {} + for field_name in event_field_names: + attribute = getattr(event_object, field_name) + if hasattr(attribute, '__iter__'): + if isinstance(attribute, dict): + indices = sorted(attribute.keys()) + else: + indices = range(len(attribute)) + for index in indices: + # quick fix to ignore list order. + index_identifier = index if isinstance(attribute, dict) else '' + subfield_name = ':plasm-sub:'.join( + [field_name, unicode(index_identifier)]) + if not frequent_words or ClusteringEngine.StringJoin( + subfield_name, attribute[index]) in frequent_words: + representation[subfield_name] = attribute[index] + else: + if not frequent_words or ClusteringEngine.StringJoin( + field_name, attribute) in frequent_words: + representation[field_name] = attribute + return representation + + def EventObjectRepresentationGenerator(self, filename, frequent_words=None): + """Yields event_representations. + + Yields event_representations from a plaso store. Essentially it simply wraps + the EventObjectGenerator and yields event_representations of the resulting + event_objects. If frequent_words is set, the event representation will + exclude any attributes not listed in the frequent_words list. + + Args: + filename: a Plaso Store filename. + frequent_words: (optional) whitelist of attributes not to ignore. + """ + with SetupStorage(filename) as store: + for event_object in EventObjectGenerator(store): + if not self.ignore: + self.ignore = event_object.COMPARE_EXCLUDE.union(self.IGNORE_BASE) + yield ClusteringEngine.EventRepresentation( + event_object, self.ignore, frequent_words) + + def NoDuplicates(self, dump_filename): + """Saves a de-duped Plaso Storage. + + This goes through the Plaso storage file, and saves a new dump with + duplicates removed. The filename is '.[dump_hash]_dedup', and is returned + at the end of the function. Note that if this function is interrupted, + incomplete results are recorded and this file must be deleted or subsequent + runs will use this incomplete data. + + Args: + dump_filename: the filename of the Plaso Storage to be deduped. + """ + sys.stdout.write(u'Removing duplicates...\n') + sys.stdout.flush() + # Whether these incremental files should remain a feature or not is still + # being decided. They're just here for now to make development faster. + nodup_filename = '.{}_dedup'.format(self.plaso_hash) + if os.path.isfile(nodup_filename): + sys.stdout.write(u'Using previously calculated results.\n') + else: + with SetupStorage(dump_filename) as store: + total_events = store.GetNumberOfEvents() + events_per_dot = operator.floordiv(total_events, 80) + formatter_cls = output_lib.GetOutputFormatter('Pstorage') + store_dedup = open(nodup_filename, 'wb') + formatter = formatter_cls(store, store_dedup) + with output_lib.EventBuffer( + formatter, check_dedups=True) as output_buffer: + event_object = formatter.FetchEntry() + counter = 0 + while event_object: + output_buffer.Append(event_object) + counter += 1 + if counter % events_per_dot == 0: + sys.stdout.write(u'.') + sys.stdout.flush() + event_object = formatter.FetchEntry() + sys.stdout.write(u'\n') + return nodup_filename + + def ConstructHashVector(self, nodup_filename, vector_size): + """Constructs the vector which tallies the hashes of attributes. + + The purpose of this vector is to save memory. Since many attributes are + fairly unique, we first hash them and keep a count of how many times the + hash appears. Later when constructing our vocabulary, we can ignore any + attributes whose hash points to a value in this vector smaller than the + support threshold value, since we are guaranteed that it appears in the + data at most this tally number of times. + + Args: + nodup_filename: the filename of a de-duplicated plaso storage file. + vector_size: size of this vector. + """ + sys.stdout.write(u'Constructing word vector...\n') + sys.stdout.flush() + vector_filename = '.{0:s}_vector_{1:s}'.format( + self.plaso_hash, vector_size) + if os.path.isfile(vector_filename): + sys.stdout.write(u'Using previously calculated results.\n') + x = open(vector_filename, 'rb') + vector = pickle.load(x) + x.close() + else: + vector = [0]*vector_size + for representation in self.EventObjectRepresentationGenerator( + nodup_filename): + for field_name, attribute in representation.iteritems(): + index = ClusteringEngine.HashAttr(field_name, attribute, vector_size) + vector[index] += 1 + x = open(vector_filename, 'wb') + pickle.dump(vector, x) + x.close() + sys.stdout.write(u'\n') + return vector + + def FindFrequentWords(self, nodup_filename, threshold, vector=None): + """Constructs a list of attributes which appear "often". + + This goes through a plaso store, and finds all name-attribute pairs which + appear no less than the support threshold value number of times. If + available it uses the hash vector in order to ignore attributes and save + memory. + + Args: + nodup_filename: the filename of a de-duplicated plaso storage file. + threshold: the support threshold value. + vector: (optional) vector of hash tallies. + """ + if not vector: + vector = [] + + sys.stdout.write(u'Constructing 1-dense clusters... \n') + sys.stdout.flush() + frequent_filename = '.{0:s}_freq_{1:s}'.format( + self.plaso_hash, str(threshold)) + if os.path.isfile(frequent_filename): + sys.stdout.write(u'Using previously calculated results.\n') + x = open(frequent_filename, 'rb') + frequent_words = pickle.load(x) + x.close() + else: + word_count = {} + vector_size = len(vector) + for representation in self.EventObjectRepresentationGenerator( + nodup_filename): + for field_name, attribute in representation.iteritems(): + word = ClusteringEngine.PreHash(field_name, attribute) + keep = vector[hash(word) % vector_size] > threshold + if not vector_size or keep: + if word in word_count: + word_count[word] += 1 + else: + word_count[word] = 1 + wordlist = [word for word in word_count if word_count[word] >= threshold] + frequent_words = sets.Set(wordlist) + x = open(frequent_filename, 'wb') + pickle.dump(frequent_words, x) + x.close() + sys.stdout.write(u'\n') + return frequent_words + + def BuildEventTypes(self, nodup_filename, threshold, frequent_words): + """Builds out the event_types from the frequent attributes. + + This uses the frequent words set in order to ignore attributes from plaso + events and thereby create event_types (events which have infrequent + attributes ignored). Currently event types which do not appear at least + as ofter as the support threshold dictates are ignored, although whether + this is what we actually want is still under consideration. Returns the + list of event types, as well as a reverse-lookup structure. + + Args: + nodup_filename: the filename of a de-duplicated plaso storage file. + threshold: the support threshold value. + frequent_words: the set of attributes not to ignore. + """ + sys.stdout.write(u'Calculating event type candidates...\n') + sys.stdout.flush() + eventtype_filename = ".{0:s}_evtt_{1:s}".format( + self.plaso_hash, str(threshold)) + if os.path.isfile(eventtype_filename): + sys.stdout.write(u'Using previously calculated results.\n') + x = open(eventtype_filename, 'rb') + evttypes = pickle.load(x) + evttype_indices = pickle.load(x) + x.close() + else: + evttype_candidates = {} + for representation in self.EventObjectRepresentationGenerator( + nodup_filename, frequent_words=frequent_words): + candidate = repr(representation) + if candidate in evttype_candidates: + evttype_candidates[candidate] += 1 + else: + evttype_candidates[candidate] = 1 + sys.stdout.write(u'\n') + # clean up memory a little + sys.stdout.write(u'Pruning event type candidates...') + sys.stdout.flush() + evttypes = [] + evttype_indices = {} + for candidate, score in evttype_candidates.iteritems(): + if score < threshold: + evttype_indices[candidate] = len(evttypes) + evttypes.append(candidate) + del evttype_candidates + + # write everything out + x = open(eventtype_filename, 'wb') + pickle.dump(evttypes, x) + pickle.dump(evttype_indices, x) + x.close() + sys.stdout.write(u'\n') + return (evttypes, evttype_indices) + + def Run(self): + """Iterates through a tagged Plaso Store file, attempting to cluster events + into groups that tend to happen together, to help creating Tag Input files. + Future work includes the ability to parse multiple Plaso Store files at + once. By default this will write incremental progress to dotfiles in the + current directory.""" + self.plaso_hash = ClusteringEngine.HashFile(self.target_filename) + self.nodup_filename = self.NoDuplicates(self.target_filename) + self.vector = self.ConstructHashVector( + self.nodup_filename, self.vector_size) + self.frequent_words = self.FindFrequentWords( + self.nodup_filename, self.threshold, self.vector) + (self.event_types, self.event_type_indices) = self.BuildEventTypes( + self.nodup_filename, self.threshold, self.frequent_words) + # Next step, clustering the event types + + # TODO: implement clustering. + + +def Main(): + """The main application function.""" + front_end = PlasmFrontend() + + epilog_tag = (""" + Notes: + + When applying tags, a tag input file must be given. Currently, + the format of this file is simply the tag name, followed by + indented lines indicating conditions for the tag, treating any + lines beginning with # as comments. For example, a valid tagging + input file might look like this:' + + ------------------------------ + Obvious Malware + # anything with 'malware' in the name or path + filename contains 'malware' + + # anything with the malware datatype + datatype is 'windows:malware:this_is_not_a_real_datatype' + + File Download + timestamp_desc is 'File Downloaded' + ------------------------------ + + Tag files can be found in the "extra" directory of plaso. + """) + + epilog_group = (""" + When applying groups, the Plaso storage file *must* contain tags, + as only tagged events are grouped. Plasm can be run such that it + both applies tags and applies groups, in which case an untagged + Plaso storage file may be used, since tags will be applied before + the grouping is calculated. + """) + + epilog_main = (""" + For help with a specific action, use "plasm.py {cluster,group,tag} -h". + """) + + description = ( + u'PLASM (Plaso Langar Ad Safna Minna)- Application to tag and group ' + u'Plaso storage files.') + + arg_parser = argparse.ArgumentParser( + description=textwrap.dedent(description), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=textwrap.dedent(epilog_main)) + + arg_parser.add_argument( + '-q', '--quiet', action='store_true', dest='quiet', default=False, + help='Suppress nonessential output.') + + subparsers = arg_parser.add_subparsers(dest='subcommand') + + cluster_subparser = subparsers.add_parser( + 'cluster', formatter_class=argparse.RawDescriptionHelpFormatter) + + cluster_subparser.add_argument( + '--closeness', action='store', type=int, metavar='MSEC', + dest='cluster_closeness', default=5000, help=( + 'Number of miliseconds before we stop considering two ' + 'events to be at all "close" to each other')) + + cluster_subparser.add_argument( + '--threshold', action='store', type=int, metavar='NUMBER', + dest='cluster_threshold', default=5, + help='Support threshold for pruning attributes.') + + front_end.AddStorageFileOptions(cluster_subparser) + + group_subparser = subparsers.add_parser( + 'group', formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=textwrap.dedent(epilog_group)) + + front_end.AddStorageFileOptions(group_subparser) + + tag_subparser = subparsers.add_parser( + 'tag', formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=textwrap.dedent(epilog_tag)) + + tag_subparser.add_argument( + '--tagfile', '--tag_file', '--tag-file', action='store', type=unicode, + metavar='FILE', dest='tag_filename', help=( + 'Name of the file containing a description of tags and rules ' + 'for tagging events.')) + + front_end.AddStorageFileOptions(tag_subparser) + + options = arg_parser.parse_args() + + try: + front_end.ParseOptions(options) + except errors.BadConfigOption as exception: + arg_parser.print_help() + print u'' + logging.error(u'{0:s}'.format(exception)) + return False + + if front_end.mode == 'cluster': + front_end.ClusterEvents() + + elif front_end.mode == 'group': + front_end.GroupEvents() + + elif front_end.mode == 'tag': + front_end.TagEvents() + + return True + + +if __name__ == '__main__': + if not Main(): + sys.exit(1) + else: + sys.exit(0) diff --git a/plaso/frontend/plasm_test.py b/plaso/frontend/plasm_test.py new file mode 100644 index 0000000..fd73236 --- /dev/null +++ b/plaso/frontend/plasm_test.py @@ -0,0 +1,195 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the plasm front-end.""" + +import os +import shutil +import tempfile +import unittest + +from plaso.engine import queue +from plaso.frontend import plasm +from plaso.frontend import test_lib +from plaso.lib import event +from plaso.lib import pfilter +from plaso.lib import storage +from plaso.multi_processing import multi_process + + +class TestEvent(event.EventObject): + DATA_TYPE = 'test:plasm:1' + + def __init__(self, timestamp, filename='/dev/null', stuff='bar'): + super(TestEvent, self).__init__() + self.timestamp = timestamp + self.filename = filename + self.timestamp_desc = 'Last Written' + self.parser = 'TestEvent' + self.display_name = 'fake:{}'.format(filename) + self.stuff = stuff + + +class PlasmTest(test_lib.FrontendTestCase): + """Tests for the plasm front-end.""" + + def setUp(self): + """Sets up the objects used throughout the test.""" + self._temp_directory = tempfile.mkdtemp() + self._storage_filename = os.path.join(self._temp_directory, 'plaso.db') + self._tag_input_filename = os.path.join(self._temp_directory, 'input1.tag') + + tag_input_file = open(self._tag_input_filename, 'wb') + tag_input_file.write('\n'.join([ + 'Test Tag', + ' filename contains \'/tmp/whoaaaa\'', + ' parser is \'TestEvent\' and stuff is \'dude\''])) + tag_input_file.close() + + pfilter.TimeRangeCache.ResetTimeConstraints() + + # TODO: add upper queue limit. + test_queue = multi_process.MultiProcessingQueue() + test_queue_producer = queue.ItemQueueProducer(test_queue) + test_queue_producer.ProduceItems([ + TestEvent(0), + TestEvent(1000), + TestEvent(2000000, '/tmp/whoaaaaa'), + TestEvent(2500000, '/tmp/whoaaaaa'), + TestEvent(5000000, '/tmp/whoaaaaa', 'dude')]) + test_queue_producer.SignalEndOfInput() + + storage_writer = storage.StorageFileWriter( + test_queue, self._storage_filename) + storage_writer.WriteEventObjects() + + self._storage_file = storage.StorageFile(self._storage_filename) + self._storage_file.SetStoreLimit() + + def tearDown(self): + """Cleans up the objects used throughout the test.""" + shutil.rmtree(self._temp_directory, True) + + def testTagParsing(self): + """Test if plasm can parse Tagging Input files.""" + tags = plasm.ParseTaggingFile(self._tag_input_filename) + self.assertEquals(len(tags), 1) + self.assertTrue('Test Tag' in tags) + self.assertEquals(len(tags['Test Tag']), 2) + + def testInvalidTagParsing(self): + """Test what happens when Tagging Input files contain invalid conditions.""" + tag_input_filename = os.path.join(self._temp_directory, 'input2.tag') + + tag_input_file = open(tag_input_filename, 'wb') + tag_input_file.write('\n'.join([ + 'Invalid Tag', ' my hovercraft is full of eels'])) + tag_input_file.close() + + tags = plasm.ParseTaggingFile(tag_input_filename) + self.assertEquals(len(tags), 1) + self.assertTrue('Invalid Tag' in tags) + self.assertEquals(len(tags['Invalid Tag']), 0) + + def testMixedValidityTagParsing(self): + """Tagging Input file contains a mix of valid and invalid conditions.""" + tag_input_filename = os.path.join(self._temp_directory, 'input3.tag') + + tag_input_file = open(tag_input_filename, 'wb') + tag_input_file.write('\n'.join([ + 'Semivalid Tag', ' filename contains \'/tmp/whoaaaa\'', + ' Yandelavasa grldenwi stravenka'])) + tag_input_file.close() + + tags = plasm.ParseTaggingFile(tag_input_filename) + self.assertEquals(len(tags), 1) + self.assertTrue('Semivalid Tag' in tags) + self.assertEquals(len(tags['Semivalid Tag']), 1) + + def testIteratingOverPlasoStore(self): + """Tests the plaso storage iterator""" + counter = 0 + for _ in plasm.EventObjectGenerator(self._storage_file, quiet=True): + counter += 1 + self.assertEquals(counter, 5) + + self._storage_file.Close() + + pfilter.TimeRangeCache.ResetTimeConstraints() + self._storage_file = storage.StorageFile(self._storage_filename) + self._storage_file.SetStoreLimit() + + counter = 0 + for _ in plasm.EventObjectGenerator(self._storage_file, quiet=False): + counter += 1 + self.assertEquals(counter, 5) + + def testTaggingEngine(self): + """Tests the Tagging engine's functionality.""" + self.assertFalse(self._storage_file.HasTagging()) + tagging_engine = plasm.TaggingEngine( + self._storage_filename, self._tag_input_filename, quiet=True) + tagging_engine.Run() + test = storage.StorageFile(self._storage_filename) + self.assertTrue(test.HasTagging()) + tagging = test.GetTagging() + count = 0 + for tag_event in tagging: + count += 1 + self.assertEquals(tag_event.tags, ['Test Tag']) + self.assertEquals(count, 3) + + def testGroupingEngineUntagged(self): + """Grouping engine should do nothing if dealing with untagged storage.""" + storage_file = storage.StorageFile(self._storage_filename, read_only=False) + grouping_engine = plasm.GroupingEngine() + grouping_engine.Run(storage_file, quiet=True) + storage_file.Close() + + storage_file = storage.StorageFile(self._storage_filename, read_only=True) + + self.assertFalse(storage_file.HasGrouping()) + + storage_file.Close() + + def testGroupingEngine(self): + """Tests the Grouping engine's functionality.""" + pfilter.TimeRangeCache.ResetTimeConstraints() + tagging_engine = plasm.TaggingEngine( + self._storage_filename, self._tag_input_filename, quiet=True) + tagging_engine.Run() + + storage_file = storage.StorageFile(self._storage_filename, read_only=False) + grouping_engine = plasm.GroupingEngine() + grouping_engine.Run(storage_file, quiet=True) + storage_file.Close() + + storage_file = storage.StorageFile(self._storage_filename, read_only=True) + + storage_file.SetStoreLimit() + self.assertTrue(storage_file.HasGrouping()) + groups = storage_file.GetGrouping() + count = 0 + for group_event in groups: + count += 1 + self.assertEquals(group_event.category, 'Test Tag') + self.assertEquals(count, 2) + + storage_file.Close() + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/frontend/pprof.py b/plaso/frontend/pprof.py new file mode 100755 index 0000000..4a70095 --- /dev/null +++ b/plaso/frontend/pprof.py @@ -0,0 +1,364 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 run for a single file and a display of how many events are collected.""" + +import argparse +import collections +import cProfile +import logging +import os +import pstats +import sys +import time + +from dfvfs.lib import definitions +from dfvfs.path import factory as path_spec_factory +from dfvfs.proto import transmission_pb2 +from dfvfs.resolver import resolver as path_spec_resolver +from dfvfs.serializer import protobuf_serializer + +from google.protobuf import text_format + +try: + # Support version 1.X of IPython. + # pylint: disable=no-name-in-module + from IPython.terminal.embed import InteractiveShellEmbed +except ImportError: + # Support version older than 1.X of IPython. + # pylint: disable=no-name-in-module + from IPython.frontend.terminal.embed import InteractiveShellEmbed + +import pyevt +import pyevtx +import pylnk +import pymsiecf +import pyregf + +import plaso +from plaso.engine import engine +from plaso.engine import queue +from plaso.engine import single_process +from plaso.frontend import psort +from plaso.frontend import utils as frontend_utils + + +# TODO: Remove this after the dfVFS integration. +# TODO: Make sure we don't need to implement the method _ConsumeItem, or +# to have that not as an abstract method. +# pylint: disable=abstract-method +class PprofEventObjectQueueConsumer(queue.EventObjectQueueConsumer): + """Class that implements an event object queue consumer for pprof.""" + + def __init__(self, queue_object): + """Initializes the queue consumer. + + Args: + queue_object: the queue object (instance of Queue). + """ + super(PprofEventObjectQueueConsumer, self).__init__(queue_object) + self.counter = collections.Counter() + self.parsers = [] + self.plugins = [] + + def _ConsumeEventObject(self, event_object, **unused_kwargs): + """Consumes an event object callback for ConsumeEventObject.""" + parser = getattr(event_object, 'parser', u'N/A') + if parser not in self.parsers: + self.parsers.append(parser) + + plugin = getattr(event_object, 'plugin', u'N/A') + if plugin not in self.plugins: + self.plugins.append(plugin) + + self.counter[parser] += 1 + if plugin != u'N/A': + self.counter[u'[Plugin] {}'.format(plugin)] += 1 + self.counter['Total'] += 1 + + +def PrintHeader(options): + """Print header information, including library versions.""" + print frontend_utils.FormatHeader('File Parsed') + print u'{:>20s}'.format(options.file_to_parse) + + print frontend_utils.FormatHeader('Versions') + print frontend_utils.FormatOutputString('plaso engine', plaso.GetVersion()) + print frontend_utils.FormatOutputString('pyevt', pyevt.get_version()) + print frontend_utils.FormatOutputString('pyevtx', pyevtx.get_version()) + print frontend_utils.FormatOutputString('pylnk', pylnk.get_version()) + print frontend_utils.FormatOutputString('pymsiecf', pymsiecf.get_version()) + print frontend_utils.FormatOutputString('pyregf', pyregf.get_version()) + + if options.filter: + print frontend_utils.FormatHeader('Filter Used') + print frontend_utils.FormatOutputString('Filter String', options.filter) + + if options.parsers: + print frontend_utils.FormatHeader('Parser Filter Used') + print frontend_utils.FormatOutputString('Parser String', options.parsers) + + +def ProcessStorage(options): + """Process a storage file and produce profile results. + + Args: + options: the command line arguments (instance of argparse.Namespace). + + Returns: + The profiling statistics or None on error. + """ + storage_parameters = options.storage.split() + storage_parameters.append(options.file_to_parse) + + if options.filter: + storage_parameters.append(options.filter) + + if options.verbose: + # TODO: why not move this functionality into psort? + profiler = cProfile.Profile() + profiler.enable() + else: + time_start = time.time() + + # Call psort and process output. + return_value = psort.Main(storage_parameters) + + if options.verbose: + profiler.disable() + else: + time_end = time.time() + + if return_value: + print u'Parsed storage file.' + else: + print u'It appears the storage file may not have processed correctly.' + + if options.verbose: + return GetStats(profiler) + else: + print frontend_utils.FormatHeader('Time Used') + print u'{:>20f}s'.format(time_end - time_start) + + +def ProcessFile(options): + """Process a file and produce profile results.""" + if options.proto_file and os.path.isfile(options.proto_file): + with open(options.proto_file) as fh: + proto_string = fh.read() + + proto = transmission_pb2.PathSpec() + try: + text_format.Merge(proto_string, proto) + except text_format.ParseError as exception: + logging.error(u'Unable to parse file, error: {}'.format( + exception)) + sys.exit(1) + + serializer = protobuf_serializer.ProtobufPathSpecSerializer + path_spec = serializer.ReadSerializedObject(proto) + else: + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=options.file_to_parse) + + file_entry = path_spec_resolver.Resolver.OpenFileEntry(path_spec) + + if file_entry is None: + logging.error(u'Unable to open file: {0:s}'.format(options.file_to_parse)) + sys.exit(1) + + # Set few options the engine expects to be there. + # TODO: Can we rather set this directly in argparse? + options.single_process = True + options.debug = False + options.text_prepend = u'' + + # Set up the engine. + # TODO: refactor and add queue limit. + collection_queue = single_process.SingleProcessQueue() + storage_queue = single_process.SingleProcessQueue() + parse_error_queue = single_process.SingleProcessQueue() + engine_object = engine.BaseEngine( + collection_queue, storage_queue, parse_error_queue) + + # Create a worker. + worker_object = engine_object.CreateExtractionWorker('0') + # TODO: add support for parser_filter_string. + worker_object.InitalizeParserObjects() + + if options.verbose: + profiler = cProfile.Profile() + profiler.enable() + else: + time_start = time.time() + worker_object.ParseFileEntry(file_entry) + + if options.verbose: + profiler.disable() + else: + time_end = time.time() + + engine_object.SignalEndOfInputStorageQueue() + + event_object_consumer = PprofEventObjectQueueConsumer(storage_queue) + event_object_consumer.ConsumeEventObjects() + + if not options.verbose: + print frontend_utils.FormatHeader('Time Used') + print u'{:>20f}s'.format(time_end - time_start) + + print frontend_utils.FormatHeader('Parsers Loaded') + # Accessing protected member. + # pylint: disable=protected-access + plugins = [] + for parser_object in sorted(worker_object._parser_objects): + print frontend_utils.FormatOutputString('', parser_object.NAME) + parser_plugins = getattr(parser_object, '_plugins', []) + plugins.extend(parser_plugins) + + print frontend_utils.FormatHeader('Plugins Loaded') + for plugin in sorted(plugins): + if isinstance(plugin, basestring): + print frontend_utils.FormatOutputString('', plugin) + else: + plugin_string = getattr(plugin, 'NAME', u'N/A') + print frontend_utils.FormatOutputString('', plugin_string) + + print frontend_utils.FormatHeader('Parsers Used') + for parser in sorted(event_object_consumer.parsers): + print frontend_utils.FormatOutputString('', parser) + + print frontend_utils.FormatHeader('Plugins Used') + for plugin in sorted(event_object_consumer.plugins): + print frontend_utils.FormatOutputString('', plugin) + + print frontend_utils.FormatHeader('Counter') + for key, value in event_object_consumer.counter.most_common(): + print frontend_utils.FormatOutputString(key, value) + + if options.verbose: + return GetStats(profiler) + + +def GetStats(profiler): + """Print verbose information from profiler and return a stats object.""" + stats = pstats.Stats(profiler, stream=sys.stdout) + print frontend_utils.FormatHeader('Profiler') + + print '\n{:-^20}'.format(' Top 10 Time Spent ') + stats.sort_stats('cumulative') + stats.print_stats(10) + + print '\n{:-^20}'.format(' Sorted By Function Calls ') + stats.sort_stats('calls') + stats.print_stats() + + return stats + + +def Main(): + """Start the tool.""" + usage = ( + u'Run this tool against a single file to see how many events are ' + u'extracted from it and which parsers recognize it.') + + arg_parser = argparse.ArgumentParser(description=usage) + + format_str = '[%(levelname)s] %(message)s' + logging.basicConfig(level=logging.INFO, format=format_str) + + arg_parser.add_argument( + '-v', '--verbose', dest='verbose', action='store_true', default=False, + help=( + 'Be extra verbose in the information printed out (include full ' + 'stats).')) + + arg_parser.add_argument( + '-c', '--console', dest='console', action='store_true', + default=False, help='After processing drop to an interactive shell.') + + arg_parser.add_argument( + '-p', '--parsers', dest='parsers', action='store', default='', type=str, + help='A list of parsers to include (see log2timeline documentation).') + + arg_parser.add_argument( + '--proto', dest='proto_file', action='store', default='', type=unicode, + metavar='PROTO_FILE', help=( + 'A file containing an ASCII PathSpec protobuf describing how to ' + 'open up the file for parsing.')) + + arg_parser.add_argument( + '-s', '--storage', dest='storage', action='store', type=unicode, + metavar='PSORT_PARAMETER', default='', help=( + 'Run the profiler against a storage file, with the parameters ' + 'provided with this option, eg: "-q -w /dev/null". The storage ' + 'file has to be passed in as the FILE_TO_PARSE argument to the ' + 'tool and filters are also optional. This is equivilant to calling ' + 'psort.py STORAGE_PARAMETER FILE_TO_PARSE [FILTER]. Where the ' + 'storage parameters are the ones defined with this parameter.')) + + # TODO: Add the option of dropping into a python shell that contains the + # stats attribute and others, just print out basic information and do the + # profiling, then drop into a ipython shell that allows you to work with + # the stats object. + + arg_parser.add_argument( + 'file_to_parse', nargs='?', action='store', metavar='FILE_TO_PARSE', + default=None, help='A path to the file that is to be parsed.') + + arg_parser.add_argument( + 'filter', action='store', metavar='FILTER', nargs='?', default=None, + help=('A filter that can be used to filter the dataset before it ' + 'is written into storage. More information about the filters' + ' and it\'s usage can be found here: http://plaso.kiddaland.' + 'net/usage/filters')) + + options = arg_parser.parse_args() + + if not (options.file_to_parse or options.proto_file): + arg_parser.print_help() + print '' + arg_parser.print_usage() + print '' + logging.error('Not able to run without a file to process.') + return False + + if options.file_to_parse and not os.path.isfile(options.file_to_parse): + logging.error(u'File [{0:s}] needs to exist.'.format(options.file_to_parse)) + return False + + PrintHeader(options) + # Stats attribute used for console sessions. + # pylint: disable=unused-variable + if options.storage: + stats = ProcessStorage(options) + else: + stats = ProcessFile(options) + + if options.console: + ipshell = InteractiveShellEmbed() + ipshell.confirm_exit = False + ipshell() + + return True + + +if __name__ == '__main__': + if not Main(): + sys.exit(1) + else: + sys.exit(0) diff --git a/plaso/frontend/preg.py b/plaso/frontend/preg.py new file mode 100755 index 0000000..171db6f --- /dev/null +++ b/plaso/frontend/preg.py @@ -0,0 +1,2161 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parse your Windows Registry files using preg. + +preg is a simple Windows Registry parser using the plaso Registry plugins and +image parsing capabilities. It uses the back-end libraries of plaso to read +raw image files and extract Registry files from VSS and restore points and then +runs the Registry plugins of plaso against the Registry hive and presents it +in a textual format. +""" + +import argparse +import binascii +import logging +import os +import re +import sys +import textwrap + +from dfvfs.helpers import file_system_searcher +from dfvfs.lib import definitions as dfvfs_definitions +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver import resolver as path_spec_resolver + +try: + # Support version 1.X of IPython. + # pylint: disable=no-name-in-module + from IPython.terminal.embed import InteractiveShellEmbed +except ImportError: + # pylint: disable=no-name-in-module + from IPython.frontend.terminal.embed import InteractiveShellEmbed + +import IPython +from IPython.config.loader import Config +from IPython.core import magic + +import pysmdev + +from plaso.artifacts import knowledge_base +from plaso.engine import queue +from plaso.engine import single_process + +# Import the winreg formatter to register it, adding the option +# to print event objects using the default formatter. +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter + +from plaso.frontend import frontend +from plaso.frontend import utils as frontend_utils +from plaso.lib import errors +from plaso.lib import event +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import context as parsers_context +from plaso.parsers import manager as parsers_manager +from plaso.parsers import winreg as winreg_parser +from plaso.parsers import winreg_plugins # pylint: disable=unused-import +from plaso.preprocessors import interface as preprocess_interface +from plaso.preprocessors import manager as preprocess_manager +from plaso.winreg import cache +from plaso.winreg import path_expander as winreg_path_expander +from plaso.winreg import winregistry + + +# Older versions of IPython don't have a version_info attribute. +if getattr(IPython, 'version_info', (0, 0, 0)) < (1, 2, 1): + raise ImportWarning( + 'Preg requires at least IPython version 1.2.1.') + + +class ConsoleConfig(object): + """Class that contains functions to configure console actions.""" + + @classmethod + def GetConfig(cls): + """Retrieves the iPython config. + + Returns: + The IPython config object (instance of + IPython.terminal.embed.InteractiveShellEmbed) + """ + try: + # The "get_ipython" function does not exist except within an IPython + # session. + return get_ipython() # pylint: disable=undefined-variable + except NameError: + return Config() + + @classmethod + def SetPrompt( + cls, hive_path=None, config=None, prepend_string=None): + """Sets the prompt string on the console. + + Args: + hive_path: The hive name or path as a string, this is optional name or + location of the loaded hive. If not defined the name is derived + from a default string. + config: The IPython configuration object (instance of + IPython.terminal.embed.InteractiveShellEmbed), this is optional + and is automatically derived if not used. + prepend_string: An optional string that can be injected into the prompt + just prior to the command count. + """ + if hive_path is None: + path_string = u'Unknown hive loaded' + else: + path_string = hive_path + + prompt_strings = [ + r'[{color.LightBlue}\T{color.Normal}]', + r'{color.LightPurple} ', + path_string, + r'\n{color.Normal}'] + if prepend_string is not None: + prompt_strings.append(u'{0:s} '.format(prepend_string)) + prompt_strings.append(r'[{color.Red}\#{color.Normal}] \$ ') + + if config is None: + ipython_config = cls.GetConfig() + else: + ipython_config = config + + try: + ipython_config.PromptManager.in_template = r''.join(prompt_strings) + except AttributeError: + ipython_config.prompt_manager.in_template = r''.join(prompt_strings) + + +class PregCache(object): + """Cache storage used for iPython and other aspects of preg.""" + + events_from_last_parse = [] + + knowledge_base_object = knowledge_base.KnowledgeBase() + + # Parser context, used when parsing Registry keys. + parser_context = None + + hive_storage = None + shell_helper = None + + +class PregEventObjectQueueConsumer(queue.EventObjectQueueConsumer): + """Class that implements a list event object queue consumer.""" + + def __init__(self, event_queue): + """Initializes the list event object queue consumer. + + Args: + event_queue: the event object queue (instance of Queue). + """ + super(PregEventObjectQueueConsumer, self).__init__(event_queue) + self.event_objects = [] + + def _ConsumeEventObject(self, event_object, **unused_kwargs): + """Consumes an event object callback for ConsumeEventObjects. + + Args: + event_object: the event object (instance of EventObject). + """ + self.event_objects.append(event_object) + + +class PregFrontend(frontend.ExtractionFrontend): + """Class that implements the preg front-end.""" + + # All Registry plugins start with "winreg_", thus the Preg library cuts that + # part of, both for display and matching. That way a plugin can be called by + # the second half of the name, eg: "userassist" instead of + # "winreg_userassist". + PLUGIN_UNIQUE_NAME_START = len('winreg_') + + # Define the different run modes. + RUN_MODE_CONSOLE = 1 + RUN_MODE_REG_FILE = 2 + RUN_MODE_REG_PLUGIN = 3 + RUN_MODE_REG_KEY = 4 + + def __init__(self, output_writer): + """Initializes the front-end object.""" + input_reader = frontend.StdinFrontendInputReader() + + super(PregFrontend, self).__init__(input_reader, output_writer) + self._key_path = None + self._parse_restore_points = False + self._verbose_output = False + self.plugins = None + + def GetListOfAllPlugins(self): + """Returns information about the supported plugins.""" + return_strings = [] + # TODO: replace frontend_utils.FormatHeader by frontend function. + return_strings.append(frontend_utils.FormatHeader(u'Supported Plugins')) + all_plugins = parsers_manager.ParsersManager.GetWindowsRegistryPlugins() + + return_strings.append(frontend_utils.FormatHeader(u'Key Plugins')) + for plugin_obj in all_plugins.GetAllKeyPlugins(): + return_strings.append(frontend_utils.FormatOutputString( + plugin_obj.NAME[self.PLUGIN_UNIQUE_NAME_START:], + plugin_obj.DESCRIPTION)) + + return_strings.append(frontend_utils.FormatHeader(u'Value Plugins')) + for plugin_obj in all_plugins.GetAllValuePlugins(): + return_strings.append(frontend_utils.FormatOutputString( + plugin_obj.NAME[self.PLUGIN_UNIQUE_NAME_START:], + plugin_obj.DESCRIPTION)) + + return u'\n'.join(return_strings) + + def ParseHive( + self, hive_path_or_path_spec, hive_collectors, shell_helper, + key_paths=None, use_plugins=None, verbose=False): + """Opens a hive file, and returns information about parsed keys. + + This function takes a path to a hive and a list of collectors (or + none if the Registry file is passed to the tool). + + The function then opens up the hive inside each collector and runs + the plugins defined (or all if no plugins are defined) against all + the keys supplied to it. + + Args: + hive_path: Full path to the hive file in question. + hive_collectors: A list of collectors to use (instance of + dfvfs.helpers.file_system_searcher.FileSystemSearcher) + shell_helper: A helper object (instance of PregHelper). + key_paths: A list of Registry keys paths that are to be parsed. + use_plugins: A list of plugins used to parse the key, None if all + plugins should be used. + verbose: Print more verbose content, like hex dump of extracted events. + + Returns: + A string containing extracted information. + """ + if isinstance(hive_path_or_path_spec, basestring): + hive_path_spec = None + hive_path = hive_path_or_path_spec + else: + hive_path_spec = hive_path_or_path_spec + hive_path = hive_path_spec.location + + if key_paths is None: + key_paths = [] + + print_strings = [] + for name, hive_collector in hive_collectors: + # Printing '*' 80 times. + print_strings.append(u'*' * 80) + print_strings.append( + u'{0:>15} : {1:s}{2:s}'.format(u'Hive File', hive_path, name)) + if hive_path_spec: + current_hive = shell_helper.OpenHive(hive_path_spec, hive_collector) + else: + current_hive = shell_helper.OpenHive(hive_path, hive_collector) + + if not current_hive: + continue + + for key_path in key_paths: + key_texts = [] + key_dict = {} + if current_hive.reg_cache: + key_dict.update(current_hive.reg_cache.attributes.items()) + + if PregCache.knowledge_base_object.pre_obj: + key_dict.update( + PregCache.knowledge_base_object.pre_obj.__dict__.items()) + + key = current_hive.GetKeyByPath(key_path) + key_texts.append(u'{0:>15} : {1:s}'.format(u'Key Name', key_path)) + if not key: + key_texts.append(u'Unable to open key: {0:s}'.format(key_path)) + if verbose: + print_strings.extend(key_texts) + continue + key_texts.append( + u'{0:>15} : {1:d}'.format(u'Subkeys', key.number_of_subkeys)) + key_texts.append(u'{0:>15} : {1:d}'.format( + u'Values', key.number_of_values)) + key_texts.append(u'') + + if verbose: + key_texts.append(u'{0:-^80}'.format(u' SubKeys ')) + for subkey in key.GetSubkeys(): + key_texts.append( + u'{0:>15} : {1:s}'.format(u'Key Name', subkey.path)) + + key_texts.append(u'') + key_texts.append(u'{0:-^80}'.format(u' Plugins ')) + + output_string = ParseKey( + key=key, shell_helper=shell_helper, verbose=verbose, + use_plugins=use_plugins, hive_helper=current_hive) + key_texts.extend(output_string) + + print_strings.extend(key_texts) + + return u'\n'.join(print_strings) + + def ParseOptions(self, options, source_option='source'): + """Parses the options and initializes the front-end. + + Args: + options: the command line arguments (instance of argparse.Namespace). + source_option: optional name of the source option. The default is source. + + Raises: + BadConfigOption: if the options are invalid. + """ + if not options: + raise errors.BadConfigOption(u'Missing options.') + + image = getattr(options, 'image', None) + regfile = getattr(options, 'regfile', None) + + if not image and not regfile: + raise errors.BadConfigOption(u'Not enough parameters to proceed.') + + if image: + self._source_path = image + + if regfile: + if not image and not os.path.isfile(regfile): + raise errors.BadConfigOption( + u'Registry file: {0:s} does not exist.'.format(regfile)) + + self._key_path = getattr(options, 'key', None) + self._parse_restore_points = getattr(options, 'restore_points', False) + + self._verbose_output = getattr(options, 'verbose', False) + + self.plugins = parsers_manager.ParsersManager.GetWindowsRegistryPlugins() + + if image: + file_to_check = image + else: + file_to_check = regfile + + is_file, reason = PathExists(file_to_check) + if not is_file: + raise errors.BadConfigOption( + u'Unable to read the input file with error: {0:s}'.format(reason)) + + if getattr(options, 'console', False): + self.run_mode = self.RUN_MODE_CONSOLE + elif getattr(options, 'key', u'') and regfile: + self.run_mode = self.RUN_MODE_REG_KEY + elif getattr(options, 'plugin_names', u''): + self.run_mode = self.RUN_MODE_REG_PLUGIN + elif regfile: + self.run_mode = self.RUN_MODE_REG_PLUGIN + else: + raise errors.BadConfigOption( + u'Incorrect usage. You\'ll need to define the path of either ' + u'a storage media image or a Windows Registry file.') + + def _ExpandKeysRedirect(self, keys): + """Expands a list of Registry key paths with their redirect equivalents. + + Args: + keys: a list of Windows Registry key paths. + """ + for key in keys: + if key.startswith('\\Software') and 'Wow6432Node' not in key: + _, first, second = key.partition('\\Software') + keys.append(u'{0:s}\\Wow6432Node{1:s}'.format(first, second)) + + # TODO: clean up this function as part of dfvfs find integration. + # TODO: a duplicate of this function exists in class: WinRegistryPreprocess + # method: GetValue; merge them. + def _FindRegistryPaths(self, searcher, pattern): + """Return a list of Windows Registry file paths. + + Args: + searcher: The file system searcher object (instance of + dfvfs.FileSystemSearcher). + pattern: The pattern to find. + """ + # TODO: optimize this in one find. + hive_paths = [] + file_path, _, file_name = pattern.rpartition(u'/') + + # The path is split in segments to make it path segement separator + # independent (and thus platform independent). + path_segments = file_path.split(u'/') + if not path_segments[0]: + path_segments = path_segments[1:] + + find_spec = file_system_searcher.FindSpec( + location_regex=path_segments, case_sensitive=False) + path_specs = list(searcher.Find(find_specs=[find_spec])) + + if not path_specs: + logging.debug(u'Directory: {0:s} not found'.format(file_path)) + return hive_paths + + for path_spec in path_specs: + directory_location = getattr(path_spec, 'location', None) + if not directory_location: + raise errors.PreProcessFail( + u'Missing directory location for: {0:s}'.format(file_path)) + + # The path is split in segments to make it path segment separator + # independent (and thus platform independent). + path_segments = searcher.SplitPath(directory_location) + path_segments.append(file_name) + + find_spec = file_system_searcher.FindSpec( + location_regex=path_segments, case_sensitive=False) + fh_path_specs = list(searcher.Find(find_specs=[find_spec])) + + if not fh_path_specs: + logging.debug(u'File: {0:s} not found in directory: {1:s}'.format( + file_name, directory_location)) + continue + + hive_paths.extend(fh_path_specs) + + return hive_paths + + def _GetRegistryFilePaths(self, plugin_name=None, registry_type=None): + """Returns a list of Registry paths from a configuration object. + + Args: + plugin_name: optional string containing the name of the plugin or an empty + string or None for all the types. Defaults to None. + registry_type: optional Registry type string. None by default. + + Returns: + A list of path names for registry files. + """ + if self._parse_restore_points: + restore_path = u'/System Volume Information/_restor.+/RP[0-9]+/snapshot/' + else: + restore_path = u'' + + if registry_type: + types = [registry_type] + else: + types = self._GetRegistryTypes(plugin_name) + + # Gather the Registry files to fetch. + paths = [] + + for reg_type in types: + if reg_type == 'NTUSER': + paths.append('/Documents And Settings/.+/NTUSER.DAT') + paths.append('/Users/.+/NTUSER.DAT') + if restore_path: + paths.append('{0:s}/_REGISTRY_USER_NTUSER.+'.format(restore_path)) + + elif reg_type == 'SOFTWARE': + paths.append('{sysregistry}/SOFTWARE') + if restore_path: + paths.append('{0:s}/_REGISTRY_MACHINE_SOFTWARE'.format(restore_path)) + + elif reg_type == 'SYSTEM': + paths.append('{sysregistry}/SYSTEM') + if restore_path: + paths.append('{0:s}/_REGISTRY_MACHINE_SYSTEM'.format(restore_path)) + + elif reg_type == 'SECURITY': + paths.append('{sysregistry}/SECURITY') + if restore_path: + paths.append('{0:s}/_REGISTRY_MACHINE_SECURITY'.format(restore_path)) + + elif reg_type == 'USRCLASS': + paths.append('/Users/.+/AppData/Local/Microsoft/Windows/UsrClass.dat') + + elif reg_type == 'SAM': + paths.append('{sysregistry}/SAM') + if restore_path: + paths.append('{0:s}/_REGISTRY_MACHINE_SAM'.format(restore_path)) + + # Expand all the paths. + expanded_paths = [] + expander = winreg_path_expander.WinRegistryKeyPathExpander() + for path in paths: + try: + expanded_paths.append(expander.ExpandPath( + path, pre_obj=PregCache.knowledge_base_object.pre_obj)) + + except KeyError as exception: + logging.error(u'Unable to expand keys with error: {0:s}'.format( + exception)) + + return expanded_paths + + def _GetRegistryKeysFromHive(self, hive_helper, parser_context): + """Retrieves a list of all key plugins for a given Registry type. + + Args: + hive_helper: A hive object (instance of PregHiveHelper). + parser_context: A parser context object (instance of ParserContext). + + Returns: + A list of Windows Registry keys. + """ + keys = [] + if not hive_helper: + return + for key_plugin_cls in self.plugins.GetAllKeyPlugins(): + temp_obj = key_plugin_cls(reg_cache=hive_helper.reg_cache) + if temp_obj.REG_TYPE == hive_helper.type: + temp_obj.ExpandKeys(parser_context) + keys.extend(temp_obj.expanded_keys) + + return keys + + def _GetRegistryPlugins(self, plugin_name): + """Retrieves the Windows Registry plugins based on a filter string. + + Args: + plugin_name: string containing the name of the plugin or an empty + string for all the plugins. + + Returns: + A list of Windows Registry plugins. + """ + key_plugin_names = [] + for plugin in self.plugins.GetAllKeyPlugins(): + temp_obj = plugin(None) + key_plugin_names.append(temp_obj.plugin_name) + + if not plugin_name: + return key_plugin_names + + plugin_name = plugin_name.lower() + if not plugin_name.startswith('winreg'): + plugin_name = u'winreg_{0:s}'.format(plugin_name) + + plugins_to_run = [] + for key_plugin in key_plugin_names: + if plugin_name in key_plugin.lower(): + plugins_to_run.append(key_plugin) + + return plugins_to_run + + def _GetRegistryTypes(self, plugin_name): + """Retrieves the Windows Registry types based on a filter string. + + Args: + plugin_name: string containing the name of the plugin or an empty + string for all the types. + + Returns: + A list of Windows Registry types. + """ + reg_cache = cache.WinRegistryCache() + types = [] + for plugin in self._GetRegistryPlugins(plugin_name): + for key_plugin_cls in self.plugins.GetAllKeyPlugins(): + temp_obj = key_plugin_cls(reg_cache=reg_cache) + if plugin is temp_obj.plugin_name: + if temp_obj.REG_TYPE not in types: + types.append(temp_obj.REG_TYPE) + break + + return types + + def _GetSearchersForImage(self, volume_path_spec): + """Retrieves the file systems searchers for searching the image. + + Args: + volume_path_spec: The path specification of the volume containing + the file system (instance of dfvfs.PathSpec). + + Returns: + A list of tuples containing the a string identifying the file system + searcher and a file system searcher object (instance of + dfvfs.FileSystemSearcher). + """ + searchers = [] + + path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_TSK, location=u'/', + parent=volume_path_spec) + + file_system = path_spec_resolver.Resolver.OpenFileSystem(path_spec) + searcher = file_system_searcher.FileSystemSearcher( + file_system, volume_path_spec) + + searchers.append((u'', searcher)) + + vss_stores = self._vss_stores + + if not vss_stores: + return searchers + + for store_index in vss_stores: + vss_path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_VSHADOW, store_index=store_index - 1, + parent=volume_path_spec) + path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_TSK, location=u'/', + parent=vss_path_spec) + + file_system = path_spec_resolver.Resolver.OpenFileSystem(path_spec) + searcher = file_system_searcher.FileSystemSearcher( + file_system, vss_path_spec) + + searchers.append(( + u':VSS Store {0:d}'.format(store_index), searcher)) + + return searchers + + def GetHivesAndCollectors( + self, options, registry_types=None, plugin_names=None): + """Returns a list of discovered Registry hives and collectors. + + Args: + options: the command line arguments (instance of argparse.Namespace). + registry_types: an optional list of Registry types, eg: NTUSER, SAM, etc + that should be included. Defaults to None. + plugin_names: an optional list of strings containing the name of the + plugin(s) or an empty string for all the types. Defaults to + None. + + Returns: + A tuple of hives and searchers, where hives is a list that contains + either a string (location of a Registry hive) or path specs (instance of + dfvfs.path.path_spec.PathSpec). The searchers is a list of tuples that + contain the name of the searcher and a searcher object (instance of + dfvfs.helpers.file_system_searcher.FileSystemSearcher) or None (if no + searcher is required). + + Raises: + ValueError: If neither registry_types nor plugin name is passed + as a parameter. + BadConfigOption: If the source scanner is unable to complete due to + a source scanner error or back end error in dfvfs. + """ + if registry_types is None and plugin_names is None: + raise ValueError( + u'Missing Registry_types or plugin_name value.') + + if plugin_names is None: + plugin_names = [] + else: + plugin_names = [plugin_name.lower() for plugin_name in plugin_names] + + # TODO: use non-preprocess collector with filter to collect hives. + + # TODO: rewrite to always use collector or equiv. + if not self._source_path: + searchers = [(u'', None)] + return registry_types, searchers + + try: + self.ScanSource(options) + except errors.SourceScannerError as exception: + raise errors.BadConfigOption(( + u'Unable to scan for a supported filesystem with error: {0:s}\n' + u'Most likely the image format is not supported by the ' + u'tool.').format(exception)) + + searchers = self._GetSearchersForImage(self.GetSourcePathSpec().parent) + _, searcher = searchers[0] + + # Run preprocessing on image. + platform = preprocess_interface.GuessOS(searcher) + + preprocess_manager.PreprocessPluginsManager.RunPlugins( + platform, searcher, PregCache.knowledge_base_object) + + # Create the keyword list if plugins are used. + plugins_list = parsers_manager.ParsersManager.GetWindowsRegistryPlugins() + if plugin_names: + if registry_types is None: + registry_types = [] + for plugin_name in plugin_names: + if not plugin_name.startswith('winreg_'): + plugin_name = u'winreg_{0:s}'.format(plugin_name) + + for plugin_cls in plugins_list.GetAllKeyPlugins(): + if plugin_name == plugin_cls.NAME.lower(): + # If a plugin is available for every Registry type + # we need to make sure all Registry hives are included. + if plugin_cls.REG_TYPE == u'any': + for available_type in PregHiveHelper.REG_TYPES.iterkeys(): + if available_type is u'Unknown': + continue + + if available_type not in registry_types: + registry_types.append(available_type) + + if plugin_cls.REG_TYPE not in registry_types: + registry_types.append(plugin_cls.REG_TYPE) + + # Find all the Registry paths we need to check. + paths = [] + if registry_types: + for registry_type in registry_types: + paths.extend(self._GetRegistryFilePaths( + registry_type=registry_type.upper())) + else: + for plugin_name in plugin_names: + paths.extend(self._GetRegistryFilePaths(plugin_name=plugin_name)) + + hives = [] + for path in paths: + hives.extend(self._FindRegistryPaths(searcher, path)) + + return hives, searchers + + def RunModeRegistryKey(self, options, plugin_names): + """Run against a specific Registry key. + + Finds and opens all Registry hives as configured in the configuration + object and tries to open the Registry key that is stored in the + configuration object for every detected hive file and parses it using + all available plugins. + + Args: + options: the command line arguments (instance of argparse.Namespace). + plugin_names: a list of strings containing the name of the plugin(s) or + an empty list for all the types. + """ + regfile = getattr(options, 'regfile', u'') + + hives, hive_collectors = self.GetHivesAndCollectors( + options, registry_types=[regfile], + plugin_names=plugin_names) + + key_paths = [self._key_path] + + # Expand the keys paths if there is a need (due to Windows redirect). + self._ExpandKeysRedirect(key_paths) + + hive_storage = PregStorage() + shell_helper = PregHelper(options, self, hive_storage) + + if hives is None: + hives = [regfile] + + for hive in hives: + output_string = self.ParseHive( + hive, hive_collectors, shell_helper, + key_paths=key_paths, verbose=self._verbose_output) + self._output_writer.Write(output_string) + + def RunModeRegistryPlugin(self, options, plugin_names): + """Run against a set of Registry plugins. + + Args: + options: the command line arguments (instance of argparse.Namespace). + plugin_names: a list of strings containing the name of the plugin(s) or + an empty string for all the types. + """ + # TODO: Add support for splitting the output to separate files based on + # each plugin name. + hives, hive_collectors = self.GetHivesAndCollectors( + options, plugin_names=plugin_names) + + if hives is None: + hives = [getattr(options, 'regfile', None)] + + plugin_list = [] + for plugin_name in plugin_names: + plugin_list.extend(self._GetRegistryPlugins(plugin_name)) + + # In order to get all the Registry keys we need to expand + # them, but to do so we need to open up one hive so that we + # create the reg_cache object, which is necessary to fully + # expand all keys. + _, hive_collector = hive_collectors[0] + hive_storage = PregStorage() + shell_helper = PregHelper(options, self, hive_storage) + hive_helper = shell_helper.OpenHive(hives[0], hive_collector) + parser_context = shell_helper.BuildParserContext() + + # Get all the appropriate keys from these plugins. + key_paths = self.plugins.GetExpandedKeyPaths( + parser_context, reg_cache=hive_helper.reg_cache, + plugin_names=plugin_list) + + for hive in hives: + output_string = self.ParseHive( + hive, hive_collectors, shell_helper, + key_paths=key_paths, use_plugins=plugin_list, + verbose=self._verbose_output) + self._output_writer.Write(output_string) + + def RunModeRegistryFile(self, options, regfile): + """Run against a Registry file. + + Finds and opens all Registry hives as configured in the configuration + object and determines the type of Registry file opened. Then it will + load up all the Registry plugins suitable for that particular Registry + file, find all Registry keys they are able to parse and run through + them, one by one. + + Args: + options: the command line arguments (instance of argparse.Namespace). + regfile: A string containing either full path to the Registry hive or + a keyword to match it. + """ + # Get all the hives and collectors. + hives, hive_collectors = self.GetHivesAndCollectors( + options, registry_types=[regfile]) + + hive_storage = PregStorage() + shell_helper = PregHelper(options, self, hive_storage) + parser_context = shell_helper.BuildParserContext() + + for hive in hives: + for collector_name, hive_collector in hive_collectors: + hive_helper = shell_helper.OpenHive( + hive, hive_collector=hive_collector, + hive_collector_name=collector_name) + hive_type = hive_helper.type + + key_paths = self._GetRegistryKeysFromHive(hive_helper, parser_context) + self._ExpandKeysRedirect(key_paths) + + plugins_to_run = self._GetRegistryPlugins(hive_type) + output_string = self.ParseHive( + hive, hive_collectors, shell_helper, key_paths=key_paths, + use_plugins=plugins_to_run, verbose=self._verbose_output) + self._output_writer.Write(output_string) + + +class PregHelper(object): + """Class that defines various helper functions. + + The purpose of this class is to bridge the plaso generated objects + with the IPython objects, making it easier to create magic classes + and provide additional helper functions to the IPython shell. + """ + + def __init__(self, tool_options, tool_front_end, hive_storage): + """Initialize the helper object. + + Args: + tool_options: A configuration object. + tool_front_end: A front end object (instance of PregFrontend). + hive_storage: A hive storage object (instance of PregStorage). + """ + super(PregHelper, self).__init__() + self.tool_options = tool_options + self.tool_front_end = tool_front_end + self.hive_storage = hive_storage + + def BuildParserContext(self, event_queue=None): + """Build the parser object. + + Args: + event_queue: An event queue object (instance of Queue). This is + optional and if a queue is not provided a default + one will be provided. + + Returns: + A parser context object (instance of parsers_context.ParserContext). + """ + if event_queue is None: + event_queue = single_process.SingleProcessQueue() + event_queue_producer = queue.ItemQueueProducer(event_queue) + + parse_error_queue = single_process.SingleProcessQueue() + parse_error_queue_producer = queue.ItemQueueProducer(parse_error_queue) + + return parsers_context.ParserContext( + event_queue_producer, parse_error_queue_producer, + PregCache.knowledge_base_object) + + def OpenHive( + self, filename_or_path_spec, hive_collector, hive_collector_name=None, + codepage='cp1252'): + """Open a Registry hive based on a collector or a filename. + + Args: + filename_or_path_spec: file path to the hive as a string or a path spec + object (instance of dfvfs.path.path_spec.PathSpec) + hive_collector: the collector to use (instance of + dfvfs.helpers.file_system_searcher.FileSystemSearcher) + hive_collector_name: optional string denoting the name of the collector + used. The default value is None. + codepage: the default codepage, default is cp1252. + + Returns: + A hive helper object (instance of PregHiveHelper). + """ + PregCache.knowledge_base_object.SetDefaultCodepage(codepage) + + if isinstance(filename_or_path_spec, basestring): + filename = filename_or_path_spec + path_spec = None + else: + filename = filename_or_path_spec.location + path_spec = filename_or_path_spec + + if not hive_collector: + path_spec = path_spec_factory.Factory.NewPathSpec( + dfvfs_definitions.TYPE_INDICATOR_OS, location=filename) + file_entry = path_spec_resolver.Resolver.OpenFileEntry(path_spec) + else: + file_entry = hive_collector.GetFileEntryByPathSpec(path_spec) + + win_registry = winregistry.WinRegistry( + winregistry.WinRegistry.BACKEND_PYREGF) + + try: + hive_object = win_registry.OpenFile( + file_entry, codepage=PregCache.knowledge_base_object.codepage) + except IOError: + if filename is not None: + filename_string = filename + elif path_spec: + filename_string = path_spec.location + else: + filename_string = u'unknown file path' + logging.error( + u'Unable to open Registry hive: {0:s} [{1:s}]'.format( + filename_string, hive_collector_name)) + return + + return PregHiveHelper( + hive_object, file_entry=file_entry, collector_name=hive_collector_name) + + def Scan(self, registry_types): + """Scan for available hives using keyword. + + Args: + registry_types: A list of keywords to scan for, eg: "NTUSER", + "SOFTWARE", etc. + """ + if not registry_types: + print ( + u'Unable to scan for an empty keyword. Please specify a keyword, ' + u'eg: NTUSER, SOFTWARE, etc') + return + + hives, collectors = self.tool_front_end.GetHivesAndCollectors( + self.tool_options, registry_types=registry_types) + + if not hives: + print u'No new discovered hives.' + return + + if type(hives) in (list, tuple): + for hive in hives: + for name, collector in collectors: + hive_helper = self.OpenHive( + hive, hive_collector=collector, hive_collector_name=name) + if hive_helper: + self.hive_storage.AppendHive(hive_helper) + else: + for name, collector in collectors: + hive_helper = self.OpenHive( + hives, hive_collector=collector, hive_collector_name=name) + if hive_helper: + self.hive_storage.AppendHive(hive_helper) + + +class PregHiveHelper(object): + """Class that defines few helper functions for Registry operations.""" + + _currently_loaded_registry_key = '' + _hive = None + _hive_type = u'UNKNOWN' + + collector_name = None + file_entry = None + path_expander = None + reg_cache = None + + REG_TYPES = { + u'NTUSER': ('\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer',), + u'SOFTWARE': ('\\Microsoft\\Windows\\CurrentVersion\\App Paths',), + u'SECURITY': ('\\Policy\\PolAdtEv',), + u'SYSTEM': ('\\Select',), + u'SAM': ('\\SAM\\Domains\\Account\\Users',), + u'USRCLASS': ( + '\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion',), + u'UNKNOWN': (), + } + + @property + def name(self): + """Return the name of the hive.""" + return getattr(self._hive, 'name', u'N/A') + + @property + def path(self): + """Return the file path of the hive.""" + path_spec = getattr(self.file_entry, 'path_spec', None) + if not path_spec: + return u'N/A' + + return getattr(path_spec, 'location', u'N/A') + + @property + def root_key(self): + """Return the root key of the Registry hive.""" + return self._hive.GetKeyByPath(u'\\') + + @property + def type(self): + """Return the hive type.""" + return self._hive_type + + def __init__(self, hive, file_entry, collector_name): + """Initialize the Registry hive helper. + + Args: + hive: A hive object (instance of WinPyregfFile). + file_entry: A file entry object (instance of dfvfs.FileEntry). + collector_name: Name of the collector used as a string. + """ + self._hive = hive + self.file_entry = file_entry + self.collector_name = collector_name + + # Determine type and build cache. + self._SetHiveType() + self._BuildHiveCache() + + # Initialize the hive to the root key. + _ = self.GetKeyByPath(u'\\') + + def _BuildHiveCache(self): + """Calculate the Registry cache.""" + self.reg_cache = cache.WinRegistryCache() + self.reg_cache.BuildCache(self._hive, self._hive_type) + self.path_expander = winreg_path_expander.WinRegistryKeyPathExpander( + reg_cache=self.reg_cache) + + def _SetHiveType(self): + """Detect and set the hive type.""" + get_key_by_path = self._hive.GetKeyByPath + for reg_type in self.REG_TYPES: + if reg_type == u'UNKNOWN': + continue + + # For a hive to be considered a specific type all of the keys need to + # be found. + found = True + for reg_key in self.REG_TYPES[reg_type]: + if not get_key_by_path(reg_key): + found = False + break + + if found: + self._hive_type = reg_type + return + + def GetCurrentRegistryKey(self): + """Return the currently loaded Registry key.""" + return self._currently_loaded_registry_key + + def GetCurrentRegistryPath(self): + """Return the loaded Registry key path or None if no key is loaded.""" + key = self._currently_loaded_registry_key + if not key: + return + + return key.path + + def GetKeyByPath(self, path): + """Retrieves a specific key defined by the Registry path. + + Args: + path: the Registry path. + + Returns: + The key (instance of WinRegKey) if available or None otherwise. + """ + if not path: + return + + key = self._hive.GetKeyByPath(path) + if not key: + return + + self._currently_loaded_registry_key = key + return key + + +class PregStorage(object): + """Class for storing discovered hives.""" + + # Index number of the currently loaded Registry hive. + _current_index = -1 + _currently_loaded_hive = None + + _hive_list = [] + + @property + def loaded_hive(self): + """Return the currently loaded hive or None if no hive loaded.""" + if not self._currently_loaded_hive: + return + + return self._currently_loaded_hive + + def __len__(self): + """Return the number of available hives.""" + return len(self._hive_list) + + def AppendHive(self, hive_helper): + """Append a hive object to the Registry hive storage. + + Args: + hive_helper: A hive object (instance of PregHiveHelper) + """ + self._hive_list.append(hive_helper) + + def AppendHives(self, hive_helpers): + """Append hives to the Registry hive storage. + + Args: + hive_helpers: A list of hive objects (instance of PregHiveHelper) + """ + if type(hive_helpers) not in (list, tuple): + hive_helpers = [hive_helpers] + + self._hive_list.extend(hive_helpers) + + def ListHives(self): + """Return a string with a list of all available hives and collectors. + + Returns: + A string with a list of all available hives and collectors. If there are + no loaded hives None will be returned. + """ + if not self._hive_list: + return + + return_strings = [u'Index Hive [collector]'] + for index, hive in enumerate(self._hive_list): + collector = hive.collector_name + if not collector: + collector = u'Currently Allocated' + + if self._current_index == index: + star = u'*' + else: + star = u'' + return_strings.append(u'{0:<5d} {1:s}{2:s} [{3:s}]'.format( + index, star, hive.path, collector)) + + return u'\n'.join(return_strings) + + def SetOpenHive(self, hive_index): + """Set the current open hive. + + Args: + hive_index: An index (integer) into the hive list. + """ + if not self._hive_list: + return + + index = hive_index + if isinstance(hive_index, basestring): + try: + index = int(hive_index, 10) + except ValueError: + print u'Wrong hive index, value should be decimal.' + return + + try: + hive_helper = self._hive_list[index] + except IndexError: + print u'Hive not found, index out of range?' + return + + self._current_index = index + self._currently_loaded_hive = hive_helper + + +def CdCompleter(unused_self, unused_event): + """Completer function for the cd command, returning back sub keys.""" + return_list = [] + current_hive = PregCache.hive_storage.loaded_hive + current_key = current_hive.GetCurrentRegistryKey() + for key in current_key.GetSubkeys(): + return_list.append(key.name) + + return return_list + + +def PluginCompleter(unused_self, event_object): + """Completer function that returns a list of available plugins.""" + ret_list = [] + + if not IsLoaded(): + return ret_list + + if not '-h' in event_object.line: + ret_list.append('-h') + + plugins_list = parsers_manager.ParsersManager.GetWindowsRegistryPlugins() + + current_hive = PregCache.hive_storage.loaded_hive + hive_type = current_hive.type + + for plugin_cls in plugins_list.GetKeyPlugins(hive_type): + plugins_list = plugin_cls(reg_cache=current_hive.reg_cache) + + plugin_name = plugins_list.plugin_name + if plugin_name.startswith('winreg'): + plugin_name = plugin_name[PregFrontend.PLUGIN_UNIQUE_NAME_START:] + + if plugin_name == 'default': + continue + ret_list.append(plugin_name) + + return ret_list + + +def VerboseCompleter(unused_self, event_object): + """Completer function that suggests simple verbose settings.""" + if '-v' in event_object.line: + return [] + else: + return ['-v'] + + +@magic.magics_class +class MyMagics(magic.Magics): + """A simple class holding all magic functions for console.""" + + EXPANSION_KEY_OPEN = r'{' + EXPANSION_KEY_CLOSE = r'}' + + # Match against one instance, not two of the expansion key. + EXPANSION_RE = re.compile(r'{0:s}{{1}}[^{1:s}]+?{1:s}'.format( + EXPANSION_KEY_OPEN, EXPANSION_KEY_CLOSE)) + + output_writer = sys.stdout + + @magic.line_magic('cd') + def ChangeDirectory(self, key): + """Change between Registry keys, like a directory tree. + + The key path can either be an absolute path or a relative one. + Absolute paths can use '.' and '..' to denote current and parent + directory/key path. + + Args: + key: The path to the key to traverse to. + """ + registry_key = None + key_path = key + + if not key: + self.ChangeDirectory('\\') + + loaded_hive = PregCache.hive_storage.loaded_hive + + if not loaded_hive: + return + + # Check if we need to expand environment attributes. + match = self.EXPANSION_RE.search(key) + if match and u'{0:s}{0:s}'.format( + self.EXPANSION_KEY_OPEN) not in match.group(0): + try: + key = loaded_hive.path_expander.ExpandPath( + key, pre_obj=PregCache.knowledge_base_object.pre_obj) + except (KeyError, IndexError): + pass + + if key.startswith(u'\\'): + registry_key = loaded_hive.GetKeyByPath(key) + elif key == '.': + return + elif key.startswith(u'.\\'): + current_path = loaded_hive.GetCurrentRegistryPath() + _, _, key_path = key.partition(u'\\') + registry_key = loaded_hive.GetKeyByPath(u'{0:s}\\{1:s}'.format( + current_path, key_path)) + elif key.startswith(u'..'): + parent_path, _, _ = loaded_hive.GetCurrentRegistryPath().rpartition(u'\\') + # We know the path starts with a "..". + if len(key) == 2: + key_path = u'' + else: + key_path = key[3:] + if parent_path: + if key_path: + path = u'{0:s}\\{1:s}'.format(parent_path, key_path) + else: + path = parent_path + registry_key = loaded_hive.GetKeyByPath(path) + else: + registry_key = loaded_hive.GetKeyByPath(u'\\{0:s}'.format(key_path)) + + else: + # Check if key is not set at all, then assume traversal from root. + if not loaded_hive.GetCurrentRegistryPath(): + _ = loaded_hive.GetKeyByPath(u'\\') + + current_key = loaded_hive.GetCurrentRegistryKey() + if current_key.name == loaded_hive.root_key.name: + key_path = u'\\{0:s}'.format(key) + else: + key_path = u'{0:s}\\{1:s}'.format(current_key.path, key) + registry_key = loaded_hive.GetKeyByPath(key_path) + + if registry_key: + if key_path == '\\': + path = '\\' + else: + path = registry_key.path + + ConsoleConfig.SetPrompt( + hive_path=loaded_hive.path, + prepend_string=StripCurlyBrace(path).replace('\\', '\\\\')) + else: + print u'Unable to change to: {0:s}'.format(key_path) + + @magic.line_magic('hive') + def HiveActions(self, line): + """Define the hive command on the console prompt.""" + if line.startswith('list'): + print PregCache.hive_storage.ListHives() + + print u'' + print u'To open a hive, use: hive_open INDEX' + elif line.startswith('open ') or line.startswith('load '): + PregCache.hive_storage.SetOpenHive(line[5:]) + hive_helper = PregCache.hive_storage.loaded_hive + print u'Opening hive: {0:s} [{1:s}]'.format( + hive_helper.path, hive_helper.collector_name) + ConsoleConfig.SetPrompt(hive_path=hive_helper.path) + elif line.startswith('scan'): + items = line.split() + if len(items) < 2: + print ( + u'Unable to scan for an empty keyword. Please specify a keyword, ' + u'eg: NTUSER, SOFTWARE, etc') + return + + PregCache.hive_storage.Scan(items[1:]) + + @magic.line_magic('ls') + def ListDirectoryContent(self, line): + """List all subkeys and values of the current key.""" + if not IsLoaded(): + return + + if 'true' in line.lower(): + verbose = True + elif '-v' in line.lower(): + verbose = True + else: + verbose = False + + sub = [] + current_hive = PregCache.hive_storage.loaded_hive + if not current_hive: + return + + current_key = current_hive.GetCurrentRegistryKey() + for key in current_key.GetSubkeys(): + # TODO: move this construction into a separate function in OutputWriter. + timestamp, _, _ = frontend_utils.OutputWriter.GetDateTimeString( + key.last_written_timestamp).partition('.') + + sub.append((u'{0:>19s} {1:>15s} {2:s}'.format( + timestamp.replace('T', ' '), '[KEY]', + key.name), True)) + + for value in current_key.GetValues(): + if not verbose: + sub.append((u'{0:>19s} {1:>14s}] {2:s}'.format( + u'', '[' + value.data_type_string, value.name), False)) + else: + if value.DataIsString(): + value_string = u'{0:s}'.format(value.data) + elif value.DataIsInteger(): + value_string = u'{0:d}'.format(value.data) + elif value.DataIsMultiString(): + value_string = u'{0:s}'.format(u''.join(value.data)) + elif value.DataIsBinaryData(): + hex_string = binascii.hexlify(value.data) + # We'll just print the first few bytes, but we need to pad them + # to make it fit in a single line if shorter. + if len(hex_string) % 32: + breakpoint = len(hex_string) / 32 + leftovers = hex_string[breakpoint:] + pad = ' ' * (32 - len(leftovers)) + hex_string += pad + + value_string = frontend_utils.OutputWriter.GetHexDumpLine( + hex_string, 0) + else: + value_string = u'' + + sub.append(( + u'{0:>19s} {1:>14s}] {2:<25s} {3:s}'.format( + u'', '[' + value.data_type_string, value.name, value_string), + False)) + + for entry, subkey in sorted(sub): + if subkey: + self.output_writer.write(u'dr-xr-xr-x {0:s}\n'.format(entry)) + else: + self.output_writer.write(u'-r-xr-xr-x {0:s}\n'.format(entry)) + + @magic.line_magic('parse') + def ParseCurrentKey(self, line): + """Parse the current key.""" + if 'true' in line.lower(): + verbose = True + elif '-v' in line.lower(): + verbose = True + else: + verbose = False + + if not IsLoaded(): + return + + current_hive = PregCache.hive_storage.loaded_hive + if not current_hive: + return + + # Clear the last results from parse key. + PregCache.events_from_last_parse = [] + + print_strings = ParseKey( + key=current_hive.GetCurrentRegistryKey(), hive_helper=current_hive, + shell_helper=PregCache.shell_helper, verbose=verbose) + self.output_writer.write(u'\n'.join(print_strings)) + + # Print out a hex dump of all binary values. + if verbose: + header_shown = False + for value in current_hive.GetCurrentRegistryKey().GetValues(): + if value.DataIsBinaryData(): + if not header_shown: + header_shown = True + print frontend_utils.FormatHeader('Hex Dump') + # Print '-' 80 times. + self.output_writer.write(u'-'*80) + self.output_writer.write(u'\n') + self.output_writer.write( + frontend_utils.FormatOutputString('Attribute', value.name)) + self.output_writer.write(u'-'*80) + self.output_writer.write(u'\n') + self.output_writer.write( + frontend_utils.OutputWriter.GetHexDump(value.data)) + self.output_writer.write(u'\n') + self.output_writer.write(u'+-'*40) + self.output_writer.write(u'\n') + + self.output_writer.flush() + + @magic.line_magic('plugin') + def ParseWithPlugin(self, line): + """Parse a Registry key using a specific plugin.""" + if not IsLoaded(): + print u'No hive loaded, unable to parse.' + return + + current_hive = PregCache.hive_storage.loaded_hive + if not current_hive: + return + + if not line: + print u'No plugin name added.' + return + + plugin_name = line + if '-h' in line: + items = line.split() + if len(items) != 2: + print u'Wrong usage: plugin [-h] PluginName' + return + if items[0] == '-h': + plugin_name = items[1] + else: + plugin_name = items[0] + + if not plugin_name.startswith('winreg'): + plugin_name = u'winreg_{0:s}'.format(plugin_name) + + hive_type = current_hive.type + plugins_list = parsers_manager.ParsersManager.GetWindowsRegistryPlugins() + plugin_found = False + for plugin_cls in plugins_list.GetKeyPlugins(hive_type): + plugin = plugin_cls(reg_cache=current_hive.reg_cache) + if plugin.plugin_name == plugin_name: + # If we found the correct plugin. + plugin_found = True + break + + if not plugin_found: + print u'No plugin named: {0:s} available for Registry type {1:s}'.format( + plugin_name, hive_type) + return + + if not hasattr(plugin, 'REG_KEYS'): + print u'Plugin: {0:s} has no key information.'.format(line) + return + + if '-h' in line: + print frontend_utils.FormatHeader(plugin_name) + print frontend_utils.FormatOutputString('Description', plugin.__doc__) + print u'' + for registry_key in plugin.expanded_keys: + print frontend_utils.FormatOutputString('Registry Key', registry_key) + return + + if not plugin.expanded_keys: + plugin.ExpandKeys(PregCache.parser_context) + + # Clear the last results from parse key. + PregCache.events_from_last_parse = [] + + # Defining outside of for loop for optimization. + get_key_by_path = current_hive.GetKeyByPath + for registry_key in plugin.expanded_keys: + key = get_key_by_path(registry_key) + if not key: + print u'Key: {0:s} not found'.format(registry_key) + continue + + # Move the current location to the key to be parsed. + self.ChangeDirectory(registry_key) + # Parse the key. + print_strings = ParseKey( + key=current_hive.GetCurrentRegistryKey(), hive_helper=current_hive, + shell_helper=PregCache.shell_helper, verbose=False, + use_plugins=[plugin_name]) + self.output_writer.write(u'\n'.join(print_strings)) + self.output_writer.flush() + + @magic.line_magic('pwd') + def PrintCurrentWorkingDirectory(self, unused_line): + """Print the current path.""" + if not IsLoaded(): + return + + current_hive = PregCache.hive_storage.loaded_hive + if not current_hive: + return + + self.output_writer.write(u'{0:s}\n'.format( + current_hive.GetCurrentRegistryPath())) + + @magic.line_magic('redirect_output') + def RedirectOutput(self, output_object): + """Change the output writer to redirect plugin output to a file.""" + + if isinstance(output_object, basestring): + output_object = open(output_object, 'wb') + + if hasattr(output_object, 'write'): + self.output_writer = output_object + + +def StripCurlyBrace(string): + """Return a format "safe" string.""" + return string.replace('}', '}}').replace('{', '{{') + + +def IsLoaded(): + """Checks if a Windows Registry Hive is loaded.""" + current_hive = PregCache.hive_storage.loaded_hive + if not current_hive: + return False + + current_key = current_hive.GetCurrentRegistryKey() + if hasattr(current_key, 'path'): + return True + + if current_hive.name != 'N/A': + return True + + print ( + u'No hive loaded, cannot complete action. Use "hive list" ' + u'and "hive open" to load a hive.') + return False + + +def GetValue(value_name): + """Return a value object from the currently loaded Registry key. + + Args: + value_name: A string containing the name of the value to be retrieved. + + Returns: + The Registry value (instance of WinPyregfValue) if it exists, None if + either there is no currently loaded Registry key or if the value does + not exist. + """ + current_hive = PregCache.hive_storage.loaded_hive + current_key = current_hive.GetCurrentRegistryKey() + + if not current_key: + return + + return current_key.GetValue(value_name) + + +def GetValueData(value_name): + """Return the value data from a value in the currently loaded Registry key. + + Args: + value_name: A string containing the name of the value to be retrieved. + + Returns: + The data from a Registry value if it exists, None if either there is no + currently loaded Registry key or if the value does not exist. + """ + value = GetValue(value_name) + + if not value: + return + + return value.data + + +def GetCurrentKey(): + """Return the currently loaded Registry key (instance of WinPyregfKey). + + Returns: + The currently loaded Registry key (instance of WinPyregfKey) or None + if there is no loaded key. + """ + current_hive = PregCache.hive_storage.loaded_hive + return current_hive.GetCurrentRegistryKey() + + +def GetFormatString(event_object): + """Return back a format string that can be used for a given event object.""" + # Assign a default value to font align length. + align_length = 15 + + # Go through the attributes and see if there is an attribute + # value that is longer than the default font align length, and adjust + # it accordingly if found. + if hasattr(event_object, 'regvalue'): + attributes = event_object.regvalue.keys() + else: + attributes = event_object.GetAttributes().difference( + event_object.COMPARE_EXCLUDE) + + for attribute in attributes: + attribute_len = len(attribute) + if attribute_len > align_length and attribute_len < 30: + align_length = len(attribute) + + # Create the format string that will be used, using variable length + # font align length (calculated in the prior step). + return u'{{0:>{0:d}s}} : {{1!s}}'.format(align_length) + + +def GetEventHeader(event_object, descriptions, exclude_timestamp): + """Returns a list of strings that contains a header for the event. + + Args: + event_object: An event object (instance of event.EventObject). + descriptions: A list of strings describing the value of the header + timestamp. + exclude_timestamp: A boolean. If it is set to True the method + will not include the timestamp in the header. + + Returns: + A list of strings containing header information for the event. + """ + format_string = GetFormatString(event_object) + + # Create the strings to return. + ret_strings = [] + ret_strings.append(u'Key information.') + if not exclude_timestamp: + for description in descriptions: + ret_strings.append(format_string.format( + description, timelib.Timestamp.CopyToIsoFormat( + event_object.timestamp))) + if hasattr(event_object, 'keyname'): + ret_strings.append(format_string.format(u'Key Path', event_object.keyname)) + if event_object.timestamp_desc != eventdata.EventTimestamp.WRITTEN_TIME: + ret_strings.append(format_string.format( + u'Description', event_object.timestamp_desc)) + + ret_strings.append(frontend_utils.FormatHeader(u'Data', u'-')) + + return ret_strings + + +def GetEventBody(event_object, file_entry=None, show_hex=False): + """Returns a list of strings containing information from an event. + + Args: + event_object: An event object (instance of event.EventObject). + file_entry: An optional file entry object (instance of dfvfs.FileEntry) that + the event originated from. Default is None. + show_hex: A boolean, if set to True hex dump of the value is included in + the output. The default value is False. + + Returns: + A list of strings containing the event body. + """ + format_string = GetFormatString(event_object) + + ret_strings = [] + + timestamp_description = getattr( + event_object, 'timestamp_desc', eventdata.EventTimestamp.WRITTEN_TIME) + + if timestamp_description != eventdata.EventTimestamp.WRITTEN_TIME: + ret_strings.append(u'<{0:s}>'.format(timestamp_description)) + + if hasattr(event_object, 'regvalue'): + attributes = event_object.regvalue + else: + # TODO: Add a function for this to avoid repeating code. + keys = event_object.GetAttributes().difference( + event_object.COMPARE_EXCLUDE) + keys.discard('offset') + keys.discard('timestamp_desc') + attributes = {} + for key in keys: + attributes[key] = getattr(event_object, key) + + for attribute, value in attributes.items(): + ret_strings.append(format_string.format(attribute, value)) + + if show_hex and file_entry: + event_object.pathspec = file_entry.path_spec + ret_strings.append(frontend_utils.FormatHeader( + u'Hex Output From Event.', '-')) + ret_strings.append( + frontend_utils.OutputWriter.GetEventDataHexDump(event_object)) + + return ret_strings + + +def GetRangeForAllLoadedHives(): + """Return a range or a list of all loaded hives.""" + return range(0, GetTotalNumberOfLoadedHives()) + + +def GetTotalNumberOfLoadedHives(): + """Return the total number of Registy hives that are loaded.""" + return len(PregCache.hive_storage) + + +def ParseKey(key, shell_helper, hive_helper, verbose=False, use_plugins=None): + """Parse a single Registry key and return parsed information. + + Parses the Registry key either using the supplied plugin or trying against + all avilable plugins. + + Args: + key: The Registry key to parse, WinRegKey object or a string. + shell_helper: A shell helper object (instance of PregHelper). + hive_helper: A hive object (instance of PregHiveHelper). + verbose: Print additional information, such as a hex dump. + use_plugins: A list of plugin names to use, or none if all should be used. + + Returns: + A list of strings. + """ + print_strings = [] + if not hive_helper: + return + + if isinstance(key, basestring): + key = hive_helper.GetKeyByPath(key) + + if not key: + return + + # Detect Registry type. + registry_type = hive_helper.type + + plugins = {} + plugins_list = parsers_manager.ParsersManager.GetWindowsRegistryPlugins() + + # Compile a list of plugins we are about to use. + for weight in plugins_list.GetWeights(): + plugin_list = plugins_list.GetWeightPlugins(weight, registry_type) + plugins[weight] = [] + for plugin in plugin_list: + if use_plugins: + plugin_obj = plugin(reg_cache=hive_helper.reg_cache) + if plugin_obj.NAME in use_plugins: + plugins[weight].append(plugin_obj) + else: + plugins[weight].append(plugin( + reg_cache=hive_helper.reg_cache)) + + event_queue = single_process.SingleProcessQueue() + event_queue_consumer = PregEventObjectQueueConsumer(event_queue) + + # Build a parser context. + parser_context = shell_helper.BuildParserContext(event_queue) + + # Run all the plugins in the correct order of weight. + for weight in plugins: + for plugin in plugins[weight]: + plugin.Process(parser_context, key=key) + event_queue_consumer.ConsumeEventObjects() + if not event_queue_consumer.event_objects: + continue + + print_strings.append(u'') + print_strings.append( + u'{0:^80}'.format(u' ** Plugin : {0:s} **'.format( + plugin.plugin_name))) + print_strings.append(u'') + print_strings.append(u'[{0:s}] {1:s}'.format( + plugin.REG_TYPE, plugin.DESCRIPTION)) + print_strings.append(u'') + if plugin.URLS: + print_strings.append(u'Additional information can be found here:') + + for url in plugin.URLS: + print_strings.append(u'{0:>17s} {1:s}'.format(u'URL :', url)) + print_strings.append(u'') + + # TODO: move into the event queue consumer. + event_objects_and_timestamps = {} + event_object = event_queue_consumer.event_objects.pop(0) + while event_object: + PregCache.events_from_last_parse.append(event_object) + event_objects_and_timestamps.setdefault( + event_object.timestamp, []).append(event_object) + + if event_queue_consumer.event_objects: + event_object = event_queue_consumer.event_objects.pop(0) + else: + event_object = None + + if not event_objects_and_timestamps: + continue + + # If there is only a single timestamp then we'll include it in the + # header, otherwise each event will have it's own timestamp. + if len(event_objects_and_timestamps) > 1: + exclude_timestamp_in_header = True + else: + exclude_timestamp_in_header = False + + first = True + for event_timestamp in sorted(event_objects_and_timestamps): + if first: + first_event = event_objects_and_timestamps[event_timestamp][0] + descriptions = set() + for event_object in event_objects_and_timestamps[event_timestamp]: + descriptions.add(getattr(event_object, 'timestamp_desc', u'')) + print_strings.extend(GetEventHeader( + first_event, list(descriptions), exclude_timestamp_in_header)) + first = False + + if exclude_timestamp_in_header: + print_strings.append(u'') + print_strings.append(u'[{0:s}]'.format( + timelib.Timestamp.CopyToIsoFormat(event_timestamp))) + + for event_object in event_objects_and_timestamps[event_timestamp]: + print_strings.append(u'') + print_strings.extend(GetEventBody( + event_object, hive_helper.file_entry, verbose)) + + print_strings.append(u'') + + # Printing '*' 80 times. + print_strings.append(u'*'*80) + print_strings.append(u'') + + return print_strings + + +# TODO: Move this to dfVFS and improve. +def PathExists(file_path): + """Determine whether given file path exists as a file, directory or a device. + + Args: + file_path: A string denoting the file path that needs checking. + + Returns: + A tuple, a boolean indicating whether or not the path exists and a string + that contains the reason, if any, why this was not determined to be a file. + """ + if os.path.exists(file_path): + return True, u'' + + try: + if pysmdev.check_device(file_path): + return True, u'' + except IOError as exception: + return False, u'Unable to determine, with error: {0:s}'.format(exception) + + return False, u'Not an existing file.' + + +def RunModeConsole(front_end, options): + """Open up an iPython console. + + Args: + options: the command line arguments (instance of argparse.Namespace). + """ + namespace = {} + + function_name_length = 23 + banners = [] + banners.append(frontend_utils.FormatHeader( + u'Welcome to PREG - home of the Plaso Windows Registry Parsing.')) + banners.append(u'') + banners.append(u'Some of the commands that are available for use are:') + banners.append(u'') + banners.append(frontend_utils.FormatOutputString( + u'cd key', u'Navigate the Registry like a directory structure.', + function_name_length)) + banners.append(frontend_utils.FormatOutputString( + u'ls [-v]', ( + u'List all subkeys and values of a Registry key. If called as ' + u'ls True then values of keys will be included in the output.'), + function_name_length)) + banners.append(frontend_utils.FormatOutputString( + u'parse -[v]', u'Parse the current key using all plugins.', + function_name_length)) + banners.append(frontend_utils.FormatOutputString( + u'pwd', u'Print the working "directory" or the path of the current key.', + function_name_length)) + banners.append(frontend_utils.FormatOutputString( + u'plugin [-h] plugin_name', ( + u'Run a particular key-based plugin on the loaded hive. The correct ' + u'Registry key will be loaded, opened and then parsed.'), + function_name_length)) + banners.append(frontend_utils.FormatOutputString( + u'get_value value_name', ( + u'Get a value from the currently loaded Registry key.'))) + banners.append(frontend_utils.FormatOutputString( + u'get_value_data value_name', ( + u'Get a value data from a value stored in the currently loaded ' + u'Registry key.'))) + banners.append(frontend_utils.FormatOutputString( + u'get_key', u'Return the currently loaded Registry key.')) + + banners.append(u'') + + # Build the global cache and prepare the tool. + hive_storage = PregStorage() + shell_helper = PregHelper(options, front_end, hive_storage) + parser_context = shell_helper.BuildParserContext() + + PregCache.parser_context = parser_context + PregCache.shell_helper = shell_helper + PregCache.hive_storage = hive_storage + + registry_types = getattr(options, 'regfile', None) + if isinstance(registry_types, basestring): + registry_types = registry_types.split(u',') + + if not registry_types: + registry_types = [ + 'NTUSER', 'USRCLASS', 'SOFTWARE', 'SYSTEM', 'SAM', 'SECURITY'] + PregCache.shell_helper.Scan(registry_types) + + if len(PregCache.hive_storage) == 1: + PregCache.hive_storage.SetOpenHive(0) + hive_helper = PregCache.hive_storage.loaded_hive + banners.append( + u'Opening hive: {0:s} [{1:s}]'.format( + hive_helper.path, hive_helper.collector_name)) + ConsoleConfig.SetPrompt(hive_path=hive_helper.path) + + loaded_hive = PregCache.hive_storage.loaded_hive + + if loaded_hive and loaded_hive.name != u'N/A': + banners.append( + u'Registry hive: {0:s} is available and loaded.'.format( + loaded_hive.name)) + else: + banners.append(u'More than one Registry file ready for use.') + banners.append(u'') + banners.append(PregCache.hive_storage.ListHives()) + banners.append(u'') + banners.append(( + u'Use "hive open INDEX" to load a hive and "hive list" to see a ' + u'list of available hives.')) + + banners.append(u'') + banners.append(u'Happy command line console fu-ing.') + + # Adding variables in scope. + namespace.update(globals()) + namespace.update({ + 'get_current_key': GetCurrentKey, + 'get_key': GetCurrentKey, + 'get_value': GetValue, + 'get_value_data': GetValueData, + 'number_of_hives': GetTotalNumberOfLoadedHives, + 'range_of_hives': GetRangeForAllLoadedHives, + 'options': options}) + + ipshell_config = ConsoleConfig.GetConfig() + + if loaded_hive: + ConsoleConfig.SetPrompt( + hive_path=loaded_hive.name, config=ipshell_config) + else: + ConsoleConfig.SetPrompt(hive_path=u'NO HIVE LOADED', config=ipshell_config) + + # Starting the shell. + ipshell = InteractiveShellEmbed( + user_ns=namespace, config=ipshell_config, banner1=u'\n'.join(banners), + exit_msg='') + ipshell.confirm_exit = False + # Adding "magic" functions. + ipshell.register_magics(MyMagics) + # Set autocall to two, making parenthesis not necessary when calling + # function names (although they can be used and are necessary sometimes, + # like in variable assignements, etc). + ipshell.autocall = 2 + # Registering command completion for the magic commands. + ipshell.set_hook('complete_command', CdCompleter, str_key='%cd') + ipshell.set_hook('complete_command', VerboseCompleter, str_key='%ls') + ipshell.set_hook('complete_command', VerboseCompleter, str_key='%parse') + ipshell.set_hook('complete_command', PluginCompleter, str_key='%plugin') + + ipshell() + + +def Main(): + """Run the tool.""" + output_writer = frontend.StdoutFrontendOutputWriter() + front_end = PregFrontend(output_writer) + + epilog = textwrap.dedent(""" + +Example usage: + +Parse the SOFTWARE hive from an image: + {0:s} [--vss] [--vss-stores VSS_STORES] -i IMAGE_PATH [-o OFFSET] -c SOFTWARE + +Parse an userassist key within an extracted hive: + {0:s} -p userassist MYNTUSER.DAT + +Parse the run key from all Registry keys (in vss too): + {0:s} --vss -i IMAGE_PATH [-o OFFSET] -p run + +Open up a console session for the SYSTEM hive inside an image: + {0:s} -i IMAGE_PATH [-o OFFSET] -c SYSTEM + """).format(os.path.basename(sys.argv[0])) + + description = textwrap.dedent(""" +preg is a simple Windows Registry parser using the plaso Registry +plugins and image parsing capabilities. + +It uses the back-end libraries of plaso to read raw image files and +extract Registry files from VSS and restore points and then runs the +Registry plugins of plaso against the Registry hive and presents it +in a textual format. + + """) + + arg_parser = argparse.ArgumentParser( + epilog=epilog, description=description, add_help=False, + formatter_class=argparse.RawDescriptionHelpFormatter) + + # Create the different argument groups. + mode_options = arg_parser.add_argument_group(u'Run Mode Options') + image_options = arg_parser.add_argument_group(u'Image Options') + info_options = arg_parser.add_argument_group(u'Informational Options') + additional_data = arg_parser.add_argument_group(u'Additional Options') + + mode_options.add_argument( + '-c', '--console', dest='console', action='store_true', default=False, + help=u'Drop into a console session Instead of printing output to STDOUT.') + + additional_data.add_argument( + '-r', '--restore_points', dest='restore_points', action='store_true', + default=False, help=u'Include restore points for hive locations.') + + image_options.add_argument( + '-i', '--image', dest='image', action='store', type=unicode, default='', + metavar='IMAGE_PATH', + help=(u'If the Registry file is contained within a storage media image, ' + u'set this option to specify the path of image file.')) + + front_end.AddImageOptions(image_options) + + info_options.add_argument( + '-v', '--verbose', dest='verbose', action='store_true', default=False, + help=u'Print sub key information.') + + info_options.add_argument( + '-h', '--help', action='help', help=u'Show this help message and exit.') + + front_end.AddVssProcessingOptions(additional_data) + + info_options.add_argument( + '--info', dest='info', action='store_true', default=False, + help=u'Print out information about supported plugins.') + + mode_options.add_argument( + '-p', '--plugins', dest='plugin_names', action='append', default=[], + type=unicode, metavar='PLUGIN_NAME', + help=( + u'Substring match of the Registry plugin to be used, this ' + u'parameter can be repeated to create a list of plugins to be ' + u'run against, eg: "-p userassist -p rdp" or "-p userassist".')) + + mode_options.add_argument( + '-k', '--key', dest='key', action='store', default='', type=unicode, + metavar='REGISTRY_KEYPATH', + help=(u'A Registry key path that the tool should parse using all ' + u'available plugins.')) + + arg_parser.add_argument( + 'regfile', action='store', metavar='REGHIVE', nargs='?', + help=(u'The Registry hive to read key from (not needed if running ' + u'using a plugin)')) + + # Parse the command line arguments. + options = arg_parser.parse_args() + + if options.info: + print front_end.GetListOfAllPlugins() + return True + + try: + front_end.ParseOptions(options, source_option='image') + except errors.BadConfigOption as exception: + arg_parser.print_usage() + print u'' + logging.error('{0:s}'.format(exception)) + return False + + # Run the tool, using the run mode according to the options passed + # to the tool. + if front_end.run_mode == front_end.RUN_MODE_CONSOLE: + RunModeConsole(front_end, options) + if front_end.run_mode == front_end.RUN_MODE_REG_KEY: + front_end.RunModeRegistryKey(options, options.plugin_names) + elif front_end.run_mode == front_end.RUN_MODE_REG_PLUGIN: + front_end.RunModeRegistryPlugin(options, options.plugin_names) + elif front_end.run_mode == front_end.RUN_MODE_REG_FILE: + front_end.RunModeRegistryFile(options, options.regfile) + + return True + + +if __name__ == '__main__': + if not Main(): + sys.exit(1) + else: + sys.exit(0) diff --git a/plaso/frontend/preg_test.py b/plaso/frontend/preg_test.py new file mode 100644 index 0000000..10397f5 --- /dev/null +++ b/plaso/frontend/preg_test.py @@ -0,0 +1,353 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the preg front-end.""" + +import StringIO +import unittest + +from dfvfs.lib import definitions +from dfvfs.path import factory as path_spec_factory + +from plaso.frontend import preg +from plaso.frontend import test_lib + +from plaso.lib import errors + + +class StringIOOutputWriter(object): + """Class that implements a StringIO output writer.""" + + def __init__(self): + """Initialize the string output writer.""" + super(StringIOOutputWriter, self).__init__() + self._string_obj = StringIO.StringIO() + + # Make the output writer compatible with a filehandle interface. + self.write = self.Write + + def flush(self): + """Flush the internal buffer.""" + self._string_obj.flush() + + def GetValue(self): + """Returns the write buffer from the output writer.""" + return self._string_obj.getvalue() + + def GetLine(self): + """Returns a single line read from the output buffer.""" + return self._string_obj.readline() + + def SeekToBeginning(self): + """Seeks the output buffer to the beginning of the buffer.""" + self._string_obj.seek(0) + + def Write(self, string): + """Writes a string to the StringIO object.""" + self._string_obj.write(string) + + +class PregFrontendTest(test_lib.FrontendTestCase): + """Tests for the preg front-end.""" + + def _GetHelperAndOutputWriter(self): + """Return a helper object (instance of PregHelper) and an output writer.""" + hive_storage = preg.PregStorage() + options = test_lib.Options() + + output_writer = StringIOOutputWriter() + test_front_end = preg.PregFrontend(output_writer) + + shell_helper = preg.PregHelper(options, test_front_end, hive_storage) + + return shell_helper, output_writer + + def testBadRun(self): + """Test few functions that should raise exceptions.""" + shell_helper, _ = self._GetHelperAndOutputWriter() + + options = test_lib.Options() + options.foo = u'bar' + + with self.assertRaises(errors.BadConfigOption): + shell_helper.tool_front_end.ParseOptions(options) + + options.regfile = 'this_path_does_not_exist' + with self.assertRaises(errors.BadConfigOption): + shell_helper.tool_front_end.ParseOptions(options) + + def testFrontEnd(self): + """Test various functions inside the front end object.""" + shell_helper, _ = self._GetHelperAndOutputWriter() + front_end = shell_helper.tool_front_end + + options = test_lib.Options() + hive_path = self._GetTestFilePath([u'NTUSER.DAT']) + options.regfile = hive_path + + front_end.ParseOptions(options, source_option='image') + + # Test the --info parameter to the tool. + info_string = front_end.GetListOfAllPlugins() + self.assertTrue(u'* Supported Plugins *' in info_string) + self.assertTrue( + u'userassist : Parser for User Assist Registry data' in info_string) + self.assertTrue( + u'services : Parser for services and drivers Registry ' in info_string) + + # Get paths to various registry files. + hive_paths_for_usersassist = set([ + u'/Documents And Settings/.+/NTUSER.DAT', '/Users/.+/NTUSER.DAT']) + # Testing functions within the front end, thus need to access protected + # members. + # pylint: disable=protected-access + test_paths_for_userassist = set( + front_end._GetRegistryFilePaths(u'userassist')) + + self.assertEquals(hive_paths_for_usersassist, test_paths_for_userassist) + + # Set the path to the system registry. + preg.PregCache.knowledge_base_object.pre_obj.sysregistry = u'C:/Windows/Foo' + + # Test the SOFTWARE hive. + test_paths = front_end._GetRegistryFilePaths(u'', u'SOFTWARE') + self.assertEqual(test_paths, [u'C:/Windows/Foo/SOFTWARE']) + + def testMagicClass(self): + """Test the magic class functions.""" + # Open up a hive. + hive_path = self._GetTestFilePath([u'NTUSER.DAT']) + shell_helper, _ = self._GetHelperAndOutputWriter() + + hive_helper = shell_helper.OpenHive(hive_path, None) + self.assertEqual(hive_helper.name, u'NTUSER.DAT') + + preg.PregCache.shell_helper = shell_helper + preg.PregCache.hive_storage = shell_helper.hive_storage + preg.PregCache.parser_context = shell_helper.BuildParserContext() + + # Mark this hive as the currently opened one. + preg.PregCache.hive_storage.AppendHive(hive_helper) + storage_length = len(preg.PregCache.hive_storage) + preg.PregCache.hive_storage.SetOpenHive(storage_length - 1) + + magic_obj = preg.MyMagics(None) + + # Change directory and verify it worked. + registry_key_path = u'\\Software\\JavaSoft\\Java Update\\Policy' + magic_obj.ChangeDirectory(registry_key_path) + registry_key = preg.GetCurrentKey() + self.assertEquals(registry_key.path, registry_key_path) + self.assertEquals( + hive_helper.GetCurrentRegistryKey().path, registry_key_path) + + # List the directory content. + output_string = StringIOOutputWriter() + magic_obj.RedirectOutput(output_string) + magic_obj.ListDirectoryContent(u'') + expected_strings = [ + u'-r-xr-xr-x [REG_SZ] LastUpdateBeginTime', + u'-r-xr-xr-x [REG_SZ] LastUpdateFinishTime', + u'-r-xr-xr-x [REG_SZ] VersionXmlURL\n'] + self.assertEquals(output_string.GetValue(), u'\n'.join(expected_strings)) + + # Parse the current key. + output_string = StringIOOutputWriter() + magic_obj.RedirectOutput(output_string) + magic_obj.ParseCurrentKey(u'') + partial_string = ( + u'LastUpdateFinishTime : [REG_SZ] Tue, 04 Aug 2009 15:18:35 GMT') + self.assertTrue(partial_string in output_string.GetValue()) + + # Parse using a plugin. + output_string = StringIOOutputWriter() + magic_obj.RedirectOutput(output_string) + magic_obj.ParseWithPlugin(u'userassist') + + partial_string = ( + u'UEME_RUNPIDL:%csidl2%\\BCWipe 3.0\\BCWipe Task Manager.lnk ' + u': [Count: 1]') + self.assertTrue(partial_string in output_string.GetValue()) + + # Let's see where we are at the moment. + output_string = StringIOOutputWriter() + magic_obj.RedirectOutput(output_string) + magic_obj.PrintCurrentWorkingDirectory(u'') + + current_directory = ( + u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer' + u'\\UserAssist\\{5E6AB780-7743-11CF-A12B-00AA004AE837}\n') + + self.assertEquals(current_directory, output_string.GetValue()) + + def testParseHive(self): + """Test the ParseHive function.""" + shell_helper, _ = self._GetHelperAndOutputWriter() + + # TODO: Replace this once _GetTestFileEntry is pushed in. + system_hive_path = self._GetTestFilePath(['SYSTEM']) + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=system_hive_path) + collectors = [('current', None)] + + key_paths = [ + u'\\ControlSet001\\Enum\\USBSTOR', + u'\\ControlSet001\\Enum\\USB', + u'\\ControlSet001\\Control\\Windows'] + + output = shell_helper.tool_front_end.ParseHive( + path_spec, collectors, shell_helper, key_paths=key_paths, + use_plugins=None, verbose=False) + + self.assertTrue(u'ComponentizedBuild : [REG_DWORD_LE] 1' in output) + self.assertTrue(u'subkey_name : Disk&Ven_HP&Prod_v100w&Rev_1024' in output) + + def testRunPlugin(self): + """Tests running the preg frontend against a plugin.""" + shell_helper, output_writer = self._GetHelperAndOutputWriter() + + options = shell_helper.tool_options + options.regfile = self._GetTestFilePath(['NTUSER.DAT']) + options.verbose = False + + shell_helper.tool_front_end.ParseOptions(options, source_option='image') + shell_helper.tool_front_end.RunModeRegistryPlugin(options, u'userassist') + + self.assertTrue(( + u'UEME_RUNPATH:C:\\Program Files\\Internet Explorer\\iexplore.exe : ' + u'[Count: 1]') in output_writer.GetValue()) + + # TODO: Add tests that parse a disk image. Test both Registry key parsing + # and plugin parsing. + + def testRunAgainstKey(self): + """Tests running the preg frontend against a Registry key.""" + shell_helper, output_writer = self._GetHelperAndOutputWriter() + + options = shell_helper.tool_options + options.key = u'\\Microsoft\\Windows NT\\CurrentVersion' + options.regfile = self._GetTestFilePath(['SOFTWARE']) + options.verbose = False + + shell_helper.tool_front_end.ParseOptions(options, source_option='image') + shell_helper.tool_front_end.RunModeRegistryKey(options, u'') + + self.assertTrue( + u'Product name : Windows 7 Ultimate' in output_writer.GetValue()) + + def testRunAgainstFile(self): + """Tests running the preg frontend against a whole Registry file.""" + shell_helper, output_writer = self._GetHelperAndOutputWriter() + + options = shell_helper.tool_options + options.regfile = self._GetTestFilePath(['SOFTWARE']) + + shell_helper.tool_front_end.ParseOptions(options, source_option='image') + shell_helper.tool_front_end.RunModeRegistryFile(options, options.regfile) + + plugins = set() + registry_keys = set() + line_count = 0 + + output_writer.SeekToBeginning() + line = output_writer.GetLine() + while line: + line_count += 1 + line = line.lstrip() + if line.startswith('** Plugin'): + _, _, plugin_name = line.rpartition(':') + plugins.add(plugin_name.strip()) + if line.startswith('Key Path :'): + _, _, key_name = line.rpartition(':') + registry_keys.add(key_name.strip()) + line = output_writer.GetLine() + + # Define the minimum set of plugins that need to be in the output. + expected_plugins = set([ + u'winreg_run_software **', u'winreg_task_cache **', u'winreg_winver **', + u'winreg_msie_zone_software **', u'winreg_default **']) + + self.assertTrue(expected_plugins.issubset(plugins)) + + self.assertTrue(( + u'\\Microsoft\\Windows NT\\CurrentVersion\\Schedule\\' + u'TaskCache') in registry_keys) + self.assertTrue( + u'\\Microsoft\\Windows\\CurrentVersion\\RunOnce' in registry_keys) + + # The output should grow with each newly added plugin, and it might be + # reduced with changes to the codebase, yet there should be at least 1.500 + # lines in the output. + self.assertGreater(line_count, 1500) + + def testTopLevelMethods(self): + """Test few of the top level methods in the preg module.""" + shell_helper, _ = self._GetHelperAndOutputWriter() + + # Set the cache. + preg.PregCache.shell_helper = shell_helper + preg.PregCache.hive_storage = shell_helper.hive_storage + preg.PregCache.parser_context = shell_helper.BuildParserContext() + + # Open up a hive. + hive_path = self._GetTestFilePath([u'NTUSER.DAT']) + hive_helper = shell_helper.OpenHive(hive_path, None) + preg.PregCache.hive_storage.AppendHive(hive_helper) + preg.PregCache.hive_storage.SetOpenHive( + len(preg.PregCache.hive_storage) - 1) + + self.assertTrue(preg.IsLoaded()) + self.assertEqual( + preg.PregCache.hive_storage.loaded_hive.name, u'NTUSER.DAT') + + # Open a Registry key using the magic class. + registry_key_path = u'\\Software\\JavaSoft\\Java Update\\Policy' + magic_obj = preg.MyMagics(None) + magic_obj.ChangeDirectory(registry_key_path) + + registry_key = preg.GetCurrentKey() + hive_helper = preg.PregCache.hive_storage.loaded_hive + self.assertEquals(registry_key.path, registry_key_path) + self.assertEquals( + hive_helper.GetCurrentRegistryKey().path, registry_key_path) + + # Get a value out of the currently loaded Registry key. + value = preg.GetValue(u'VersionXmlURL') + self.assertEquals(value.name, u'VersionXmlURL') + + value_data = preg.GetValueData(u'VersionXmlURL') + self.assertEquals( + value_data, + u'http://javadl.sun.com/webapps/download/AutoDL?BundleId=33742') + + # Parse a Registry key. + parsed_strings = preg.ParseKey( + registry_key, shell_helper=shell_helper, hive_helper=hive_helper) + self.assertTrue(parsed_strings[1].lstrip().startswith(u'** Plugin : ')) + + # Change back to the root key. + magic_obj.ChangeDirectory(u'') + registry_key = preg.GetCurrentKey() + self.assertEquals(registry_key.path, u'\\') + + # TODO: Add tests for formatting of events, eg: parse a key, get the event + # objects and test the formatting of said event object. + # TODO: Add tests for running in console mode. + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/frontend/presets.py b/plaso/frontend/presets.py new file mode 100644 index 0000000..f76cb82 --- /dev/null +++ b/plaso/frontend/presets.py @@ -0,0 +1,72 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Helper file for filtering out parsers.""" + +categories = { + 'win_gen': [ + 'bencode', 'esedb', 'filestat', 'google_drive', 'java_idx', 'lnk', + 'mcafee_protection', 'olecf', 'openxml', 'prefetch', + 'skydrive_log_error', 'skydrive_log', 'skype', + 'symantec_scanlog', 'webhist', 'winfirewall', 'winjob', + 'winreg'], + 'winxp': [ + 'recycle_bin_info2', 'win_gen', 'winevt'], + 'winxp_slow': [ + 'hachoir', 'winxp'], + 'win7': [ + 'recycle_bin', 'custom_destinations', 'olecf_automatic_destinations', + 'win_gen', 'winevtx'], + 'win7_slow': [ + 'hachoir', 'win7'], + 'webhist': [ + 'chrome_cache', 'chrome_cookies', 'chrome_extension_activity', + 'chrome_history', 'firefox_cache', 'firefox_cookies', + 'firefox_downloads', 'firefox_history', 'java_idx', 'msie_webcache', + 'msiecf', 'opera_global', 'opera_typed_history', 'safari_history'], + 'linux': [ + 'bencode', 'filestat', 'google_drive', 'java_idx', 'olecf', 'openxml', + 'pls_recall', 'popularity_contest', 'selinux', 'skype', 'syslog', + 'utmp', 'webhist', 'xchatlog', 'xchatscrollback', 'zeitgeist'], + 'macosx': [ + 'appusage', 'asl_log', 'bencode', 'bsm_log', 'cups_ipp', 'filestat', + 'google_drive', 'java_idx', 'ls_quarantine', 'mac_appfirewall_log', + 'mac_document_versions', 'mac_keychain', 'mac_securityd', + 'mackeeper_cache', 'macwifi', 'olecf', 'openxml', 'plist', 'skype', + 'utmpx', 'webhist'], + # TODO: Once syslog parser has been rewritten to be faster than the current + # one it's moved out of the default parsers for Mac OS X and into the "slow" + # mode. + 'macosx_slow': ['macosx', 'syslog'], + 'android': [ + 'android_app_usage', 'android_calls', 'android_sms'], +} + + +def GetParsersFromCategory(category): + """Return a list of parsers from a parser category.""" + return_list = [] + if category not in categories: + return return_list + + for item in categories.get(category): + if item in categories: + return_list.extend(GetParsersFromCategory(item)) + else: + return_list.append(item) + + return return_list diff --git a/plaso/frontend/pshell.py b/plaso/frontend/pshell.py new file mode 100755 index 0000000..4e33403 --- /dev/null +++ b/plaso/frontend/pshell.py @@ -0,0 +1,498 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a console, the CLI friendly front-end to plaso.""" + +import argparse +import logging +import os +import random +import sys +import tempfile + +from dfvfs.lib import definitions +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver import resolver as path_spec_resolver + +try: + # Support version 1.X of IPython. + # pylint: disable=no-name-in-module + from IPython.terminal.embed import InteractiveShellEmbed +except ImportError: + # Support version older than 1.X of IPython. + # pylint: disable=no-name-in-module + from IPython.frontend.terminal.embed import InteractiveShellEmbed + +from IPython.config.loader import Config + +# pylint: disable=unused-import +from plaso import analysis +from plaso import filters +from plaso import formatters +from plaso import output +from plaso import parsers +from plaso import preprocessors + +from plaso.classifier import scanner + +from plaso.engine import collector +from plaso.engine import engine +from plaso.engine import queue +from plaso.engine import single_process +from plaso.engine import utils as engine_utils + +from plaso.frontend import frontend +from plaso.frontend import utils as frontend_utils + +from plaso.lib import binary +from plaso.lib import bufferlib +from plaso.lib import errors +from plaso.lib import event +from plaso.lib import eventdata +from plaso.lib import filter_interface +from plaso.lib import lexer +from plaso.lib import objectfilter +from plaso.lib import output as output_lib +from plaso.lib import pfilter +from plaso.lib import proxy +from plaso.lib import putils +from plaso.lib import registry as class_registry +from plaso.lib import storage +from plaso.lib import timelib +from plaso.lib import utils + +from plaso.multi_processing import foreman +from plaso.multi_processing import rpc_proxy +from plaso.multi_processing import process_info + +from plaso.output import helper as output_helper + +from plaso.parsers import manager as parsers_manager +from plaso.parsers import plugins +from plaso.parsers import text_parser +from plaso.proto import plaso_storage_pb2 + +from plaso.serializer import interface as serializer_interface +from plaso.serializer import json_serializer +from plaso.serializer import protobuf_serializer + +from plaso.unix import bsmtoken + +from plaso.winnt import environ_expand +from plaso.winnt import known_folder_ids + +from plaso.winreg import cache as win_registry_cache +from plaso.winreg import interface as win_registry_interface +from plaso.winreg import path_expander +from plaso.winreg import utils as win_registry_utils +from plaso.winreg import winpyregf +from plaso.winreg import winregistry + + +class PshellFrontend(frontend.ExtractionFrontend): + """Class that implements the pshell front-end.""" + + _BYTES_IN_A_MIB = 1024 * 1024 + + def __init__(self): + """Initializes the front-end object.""" + input_reader = frontend.StdinFrontendInputReader() + output_writer = frontend.StdoutFrontendOutputWriter() + + super(PshellFrontend, self).__init__(input_reader, output_writer) + + +def FindAllOutputs(): + """FindAllOutputs() - All available outputs.""" + return putils.FindAllOutputs() + + +def GetEventData(event_proto, before=0): + """Prints a hexdump of the event data.""" + return frontend_utils.OutputWriter.GetEventDataHexDump(event_proto, before) + + +def GetFileEntryFromEventObject(event_object): + """Return a file entry object from a pathspec object. + + Args: + event_object: An event object (an instance of EventObject). + + Returns: + A file entry object (instance of vfs.file_entry.FileEntry) or + None if the event object doesn't have a defined path spec. + """ + path_spec = getattr(event_object, 'pathspec', None) + + if not path_spec: + return + + return path_spec_resolver.Resolver.OpenFileEntry(path_spec) + + +def GetParserNames(parser_filter_string=None): + """Retrieves the parser names. + + Args: + parser_filter_string: Optional parser filter string. The default is None. + + Returns: + A list of parser names. + """ + return parsers_manager.ParsersManager.GetParserNames( + parser_filter_string=parser_filter_string) + + +def GetParserObjects(parser_filter_string=None): + """Retrieves the parser objects. + + Args: + parser_filter_string: Optional parser filter string. The default is None. + + Returns: + A list of parser objects (instances of BaseParser). + """ + return parsers_manager.ParsersManager.GetParserObjects( + parser_filter_string=parser_filter_string) + + +def OpenOSFile(path): + """Opens a file entry from the OS.""" + if not os.path.isfile(path): + logging.error(u'File: {0:s} does not exist.'.format(path)) + return + + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=path) + return path_spec_resolver.Resolver.OpenFileEntry(path_spec) + + +def OpenStorageFile(storage_path): + """Opens a storage file and returns the storage file object.""" + if not os.path.isfile(storage_path): + return + + try: + store = storage.StorageFile(storage_path, read_only=True) + except IOError: + print 'Unable to load storage file, not a storage file?' + + return store + + +def OpenTskFile(image_path, image_offset, path=None, inode=None): + """Opens a file entry of a file inside an image file.""" + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=image_path) + + if image_offset > 0: + volume_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_TSK_PARTITION, start_offset=image_offset, + parent=path_spec) + else: + volume_path_spec = path_spec + + if inode is not None: + if path is None: + path = u'' + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_TSK, inode=inode, location=path, + parent=volume_path_spec) + else: + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_TSK, location=path, parent=volume_path_spec) + + return path_spec_resolver.Resolver.OpenFileEntry(path_spec) + + +def OpenVssFile(path, image_path, store_number, image_offset): + """Opens a file entry inside a VSS inside an image file.""" + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=image_path) + + if image_offset > 0: + volume_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_TSK_PARTITION, start_offset=image_offset, + parent=path_spec) + else: + volume_path_spec = path_spec + + store_number -= 1 + + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_VSHADOW, store_index=store_number, + parent=volume_path_spec) + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_TSK, location=path, parent=path_spec) + + return path_spec_resolver.Resolver.OpenFileEntry(path_spec) + + +def ParseFile(file_entry): + """Parse a file given a file entry or path and return a list of results. + + Args: + file_entry: Either a file entry object (instance of dfvfs.FileEntry) + or a string containing a path (absolute or relative) to a + local file. + + Returns: + A list of event object (instance of EventObject) that were extracted from + the file (or an empty list if no events were extracted). + """ + if not file_entry: + return + + if isinstance(file_entry, basestring): + file_entry = OpenOSFile(file_entry) + + # Set up the engine. + # TODO: refactor and add queue limit. + collection_queue = single_process.SingleProcessQueue() + storage_queue = single_process.SingleProcessQueue() + parse_error_queue = single_process.SingleProcessQueue() + engine_object = engine.BaseEngine( + collection_queue, storage_queue, parse_error_queue) + + # Create a worker. + worker_object = engine_object.CreateExtractionWorker(0) + # TODO: add support for parser_filter_string. + worker_object.InitalizeParserObjects() + worker_object.ParseFileEntry(file_entry) + + collection_queue.SignalEndOfInput() + engine_object.SignalEndOfInputStorageQueue() + + results = [] + while True: + try: + item = storage_queue.PopItem() + except errors.QueueEmpty: + break + + if isinstance(item, queue.QueueEndOfInput): + break + + results.append(item) + return results + + +def Pfile2File(file_object, path): + """Saves a file-like object to the path.""" + return frontend_utils.OutputWriter.WriteFile(file_object, path) + + +def PrintTimestamp(timestamp): + """Prints a human readable timestamp from a timestamp value.""" + return frontend_utils.OutputWriter.GetDateTimeString(timestamp) + + +def PrintTimestampFromEvent(event_object): + """Prints a human readable timestamp from values stored in an event object.""" + return PrintTimestamp(getattr(event_object, 'timestamp', 0)) + + +def Main(): + """Start the tool.""" + temp_location = tempfile.gettempdir() + + options = putils.Options() + + # Set the default options. + options.buffer_size = 0 + options.debug = False + options.filename = '.' + options.file_filter = '' + options.filter = '' + options.image = False + options.image_offset = None + options.image_offset_bytes = None + options.old_preprocess = False + options.open_files = False + options.output = os.path.join(temp_location, 'wheredidmytimelinego.dump') + options.output_module = '' + options.parsers = '' + options.parse_vss = False + options.preprocess = False + options.recursive = False + options.single_process = False + options.timezone = 'UTC' + options.workers = 5 + + format_str = '[%(levelname)s] (%(processName)-10s) %(message)s' + logging.basicConfig(format=format_str) + + front_end = PshellFrontend() + + try: + front_end.ParseOptions(options, source_option='filename') + front_end.SetStorageFile(options.output) + except errors.BadConfigOption as exception: + logging.error(u'{0:s}'.format(exception)) + + # TODO: move to frontend object. + if options.image and options.image_offset_bytes is None: + if options.image_offset is not None: + bytes_per_sector = getattr(options, 'bytes_per_sector', 512) + options.image_offset_bytes = options.image_offset * bytes_per_sector + else: + options.image_offset_bytes = 0 + + namespace = {} + + pre_obj = event.PreprocessObject() + + namespace.update(globals()) + namespace.update({ + 'frontend': front_end, + 'pre_obj': pre_obj, + 'options': options, + 'find_all_output': FindAllOutputs, + 'parse_file': ParseFile, + 'timestamp_from_event': PrintTimestampFromEvent, + 'message': formatters.manager.EventFormatterManager.GetMessageStrings}) + + # Include few random phrases that get thrown in once the user exists the + # shell. + _my_random_phrases = [ + u'I haven\'t seen timelines like this since yesterday.', + u'Timelining is super relaxing.', + u'Why did I not use the shell before?', + u'I like a do da cha cha', + u'I AM the Shogun of Harlem!', + (u'It doesn\'t matter if you win or lose, it\'s what you do with your ' + u'dancin\' shoes'), + u'I have not had a night like that since the seventies.', + u'Baker Team. They\'re all dead, sir.', + (u'I could have killed \'em all, I could\'ve killed you. In town ' + u'you\'re the law, out here it\'s me.'), + (u'Are you telling me that 200 of our men against your boy is a no-win ' + u'situation for us?'), + u'Hunting? We ain\'t huntin\' him, he\'s huntin\' us!', + u'You picked the wrong man to push', + u'Live for nothing or die for something', + u'I am the Fred Astaire of karate.', + (u'God gave me a great body and it\'s my duty to take care of my ' + u'physical temple.'), + u'This maniac should be wearing a number, not a badge', + u'Imagination is more important than knowledge.', + u'Do you hate being dead?', + u'You\'ve got 5 seconds... and 3 are up.', + u'He is in a gunfight right now. I\'m gonna have to take a message', + u'That would be better than losing your teeth', + u'The less you know, the more you make', + (u'A SQL query goes into a bar, walks up to two tables and asks, ' + u'"Can I join you?"'), + u'This is your captor speaking.', + (u'If I find out you\'re lying, I\'ll come back and kill you in your ' + u'own kitchen.'), + u'That would be better than losing your teeth', + (u'He\'s the kind of guy who would drink a gallon of gasoline so ' + u'that he can p*ss into your campfire.'), + u'I\'m gonna take you to the bank, Senator Trent. To the blood bank!', + u'I missed! I never miss! They must have been smaller than I thought', + u'Nah. I\'m just a cook.', + u'Next thing I know, you\'ll be dating musicians.', + u'Another cold day in hell', + u'Yeah, but I bet you she doesn\'t see these boys in the choir.', + u'You guys think you\'re above the law... well you ain\'t above mine!', + (u'One thought he was invincible... the other thought he could fly... ' + u'They were both wrong'), + u'To understand what recursion is, you must first understand recursion'] + + arg_description = ( + u'pshell is the interactive session tool that can be used to' + u'MISSING') + + arg_parser = argparse.ArgumentParser(description=arg_description) + + arg_parser.add_argument( + '-s', '--storage_file', '--storage-file', dest='storage_file', + type=unicode, default=u'', help=u'Path to a plaso storage file.', + action='store', metavar='PATH') + + configuration = arg_parser.parse_args() + + if configuration.storage_file: + store = OpenStorageFile(configuration.storage_file) + if store: + namespace.update({'store': store}) + + functions = [ + FindAllOutputs, GetEventData, GetParserNames, GetParserObjects, + OpenOSFile, OpenStorageFile, OpenTskFile, OpenVssFile, + ParseFile, Pfile2File, + PrintTimestamp, PrintTimestampFromEvent] + + functions_strings = [] + for function in functions: + docstring, _, _ = function.__doc__.partition(u'\n') + docstring = u'\t{0:s} - {1:s}'.format(function.__name__, docstring) + functions_strings.append(docstring) + functions_strings = u'\n'.join(functions_strings) + + banner = ( + u'--------------------------------------------------------------\n' + u' Welcome to Plaso console - home of the Plaso adventure land.\n' + u'--------------------------------------------------------------\n' + u'This is the place where everything is allowed, as long as it is ' + u'written in Python.\n\n' + u'Objects available:\n\toptions - set of options to the frontend.\n' + u'\tfrontend - A copy of the pshell frontend.\n' + u'\n' + u'All libraries have been imported and can be used, see help(frontend) ' + u'or help(parser).\n' + u'\n' + u'Base methods:\n' + u'{0:s}' + u'\n\tmessage - Print message strings from an event object.' + u'\n' + u'\n' + u'p.s. typing in "pdb" and pressing enter puts the shell in debug' + u'mode which causes all exceptions being sent to pdb.\n' + u'Happy command line console fu-ing.\n\n').format(functions_strings) + + exit_message = u'You are now leaving the winter wonderland.\n\n{}'.format( + random.choice(_my_random_phrases)) + + shell_config = Config() + # Make slight adjustments to the iPython prompt. + shell_config.PromptManager.out_template = ( + r'{color.Normal}[{color.Red}\#{color.Normal}]<<< ') + shell_config.PromptManager.in_template = ( + r'[{color.LightBlue}\T{color.Normal}] {color.LightPurple}\Y2\n' + r'{color.Normal}[{color.Red}\#{color.Normal}] \$ ') + shell_config.PromptManager.in2_template = r'.\D.>>>' + + ipshell = InteractiveShellEmbed( + user_ns=namespace, config=shell_config, banner1=banner, + exit_msg=exit_message) + ipshell.confirm_exit = False + # Set autocall to two, making parenthesis not necessary when calling + # function names (although they can be used and are necessary sometimes, + # like in variable assignments, etc). + ipshell.autocall = 2 + ipshell() + + return True + + +if __name__ == '__main__': + if not Main(): + sys.exit(1) + else: + sys.exit(0) diff --git a/plaso/frontend/psort.py b/plaso/frontend/psort.py new file mode 100755 index 0000000..c534f44 --- /dev/null +++ b/plaso/frontend/psort.py @@ -0,0 +1,764 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Psort (Plaso Síar Og Raðar Þessu) - Makes output from Plaso Storage files. + +Sample Usage: + psort.py /tmp/mystorage.dump "date > '01-06-2012'" + +See additional details here: http://plaso.kiddaland.net/usage/psort +""" + +import argparse +import collections +import datetime +import time +import multiprocessing +import logging +import pdb +import sys + +import plaso +from plaso import analysis +from plaso import filters +from plaso import formatters # pylint: disable=unused-import +from plaso import output # pylint: disable=unused-import + +from plaso.analysis import context as analysis_context +from plaso.analysis import interface as analysis_interface +from plaso.artifacts import knowledge_base +from plaso.engine import queue +from plaso.frontend import frontend +from plaso.frontend import utils as frontend_utils +from plaso.lib import bufferlib +from plaso.lib import errors +from plaso.lib import output as output_lib +from plaso.lib import pfilter +from plaso.lib import timelib +from plaso.multi_processing import multi_process +from plaso.proto import plaso_storage_pb2 +from plaso.serializer import protobuf_serializer + +import pytz + + +class PsortFrontend(frontend.AnalysisFrontend): + """Class that implements the psort front-end.""" + + def __init__(self): + """Initializes the front-end object.""" + input_reader = frontend.StdinFrontendInputReader() + output_writer = frontend.StdoutFrontendOutputWriter() + + super(PsortFrontend, self).__init__(input_reader, output_writer) + + self._analysis_processes = [] + self._filter_buffer = None + self._filter_expression = None + self._filter_object = None + self._output_module_class = None + self._output_stream = None + self._slice_size = 5 + + def AddAnalysisPluginOptions(self, argument_group, plugin_names): + """Adds the analysis plugin options to the argument group + + Args: + argument_group: The argparse argument group (instance of + argparse._ArgumentGroup). + plugin_names: a string containing comma separated analysis plugin names. + + Raises: + BadConfigOption: if non-existing analysis plugin names are specified. + """ + if plugin_names == 'list': + return + + plugin_list = set([ + name.strip().lower() for name in plugin_names.split(',')]) + + # Get a list of all available plugins. + analysis_plugins = set([ + name.lower() for name, _, _ in analysis.ListAllPluginNames()]) + + # Get a list of the selected plugins (ignoring selections that did not + # have an actual plugin behind it). + plugins_to_load = analysis_plugins.intersection(plugin_list) + + # Check to see if we are trying to load plugins that do not exist. + difference = plugin_list.difference(analysis_plugins) + if difference: + raise errors.BadConfigOption( + u'Non-existing analysis plugins specified: {0:s}'.format( + u' '.join(difference))) + + plugins = analysis.LoadPlugins(plugins_to_load, None) + for plugin in plugins: + if plugin.ARGUMENTS: + for parameter, config in plugin.ARGUMENTS: + argument_group.add_argument(parameter, **config) + + def AddOutputModuleOptions(self, argument_group, module_names): + """Adds the output module options to the argument group + + Args: + argument_group: The argparse argument group (instance of + argparse._ArgumentGroup). + module_names: a string containing comma separated output module names. + """ + if module_names == 'list': + return + + modules_list = set([name.lower() for name in module_names]) + + for output_module_string, _ in output_lib.ListOutputFormatters(): + if not output_module_string.lower() in modules_list: + continue + + output_module = output_lib.GetOutputFormatter(output_module_string) + if output_module.ARGUMENTS: + for parameter, config in output_module.ARGUMENTS: + argument_group.add_argument(parameter, **config) + + def ListAnalysisPlugins(self): + """Lists the analysis modules.""" + self.PrintHeader('Analysis Modules') + format_length = 10 + for name, _, _ in analysis.ListAllPluginNames(): + if len(name) > format_length: + format_length = len(name) + + for name, description, plugin_type in analysis.ListAllPluginNames(): + if plugin_type == analysis_interface.AnalysisPlugin.TYPE_ANNOTATION: + type_string = 'Annotation/tagging plugin' + elif plugin_type == analysis_interface.AnalysisPlugin.TYPE_ANOMALY: + type_string = 'Anomaly plugin' + elif plugin_type == analysis_interface.AnalysisPlugin.TYPE_REPORT: + type_string = 'Summary/Report plugin' + elif plugin_type == analysis_interface.AnalysisPlugin.TYPE_STATISTICS: + type_string = 'Statistics plugin' + else: + type_string = 'Unknown type' + + description = u'{0:s} [{1:s}]'.format(description, type_string) + self.PrintColumnValue(name, description, format_length) + self.PrintSeparatorLine() + + def ListOutputModules(self): + """Lists the output modules.""" + self.PrintHeader('Output Modules') + for name, description in output_lib.ListOutputFormatters(): + self.PrintColumnValue(name, description, 10) + self.PrintSeparatorLine() + + def ListTimeZones(self): + """Lists the timezones.""" + self.PrintHeader('Zones') + max_length = 0 + for zone in pytz.all_timezones: + if len(zone) > max_length: + max_length = len(zone) + + self.PrintColumnValue('Timezone', 'UTC Offset', max_length) + for zone in pytz.all_timezones: + zone_obj = pytz.timezone(zone) + date_str = unicode(zone_obj.localize(datetime.datetime.utcnow())) + if '+' in date_str: + _, _, diff = date_str.rpartition('+') + diff_string = u'+{0:s}'.format(diff) + else: + _, _, diff = date_str.rpartition('-') + diff_string = u'-{0:s}'.format(diff) + self.PrintColumnValue(zone, diff_string, max_length) + self.PrintSeparatorLine() + + def ParseOptions(self, options): + """Parses the options and initializes the front-end. + + Args: + options: the command line arguments (instance of argparse.Namespace). + + Raises: + BadConfigOption: if the options are invalid. + """ + super(PsortFrontend, self).ParseOptions(options) + + output_format = getattr(options, 'output_format', None) + if not output_format: + raise errors.BadConfigOption(u'Missing output format.') + + self._output_module_class = output_lib.GetOutputFormatter(output_format) + if not self._output_module_class: + raise errors.BadConfigOption( + u'Invalid output format: {0:s}.'.format(output_format)) + + self._output_stream = getattr(options, 'write', None) + if not self._output_stream: + self._output_stream = sys.stdout + + self._filter_expression = getattr(options, 'filter', None) + if self._filter_expression: + self._filter_object = filters.GetFilter(self._filter_expression) + if not self._filter_object: + raise errors.BadConfigOption( + u'Invalid filter expression: {0:s}'.format(self._filter_expression)) + + # Check to see if we need to create a circular buffer. + if getattr(options, 'slicer', None): + self._slice_size = getattr(options, 'slice_size', 5) + self._filter_buffer = bufferlib.CircularBuffer(self._slice_size) + + def ParseStorage(self, options): + """Open a storage file and parse through it. + + Args: + options: the command line arguments (instance of argparse.Namespace). + + Returns: + A counter. + + Raises: + RuntimeError: if a non-recoverable situation is encountered. + """ + counter = None + + if options.slice: + if options.timezone == 'UTC': + zone = pytz.utc + else: + zone = pytz.timezone(options.timezone) + + timestamp = timelib.Timestamp.FromTimeString(options.slice, timezone=zone) + + # Convert number of minutes to microseconds. + range_operator = self._slice_size * 60 * 1000000 + + # Set the time range. + pfilter.TimeRangeCache.SetLowerTimestamp(timestamp - range_operator) + pfilter.TimeRangeCache.SetUpperTimestamp(timestamp + range_operator) + + if options.analysis_plugins: + read_only = False + else: + read_only = True + + try: + storage_file = self.OpenStorageFile(read_only=read_only) + except IOError as exception: + raise RuntimeError( + u'Unable to open storage file: {0:s} with error: {1:s}.'.format( + self._storage_file_path, exception)) + + with storage_file: + storage_file.SetStoreLimit(self._filter_object) + + try: + output_module = self._output_module_class( + storage_file, self._output_stream, options, self._filter_object) + except IOError as exception: + raise RuntimeError( + u'Unable to create output module with error: {0:s}'.format( + exception)) + + if not output_module: + raise RuntimeError(u'Missing output module.') + + if options.analysis_plugins: + logging.info(u'Starting analysis plugins.') + # Within all preprocessing objects, try to get the last one that has + # time zone information stored in it, the highest chance of it + # containing the information we are seeking (defaulting to the last + # one). + pre_objs = storage_file.GetStorageInformation() + pre_obj = pre_objs[-1] + for obj in pre_objs: + if getattr(obj, 'time_zone_str', ''): + pre_obj = obj + + # Fill in the collection information. + pre_obj.collection_information = {} + encoding = getattr(pre_obj, 'preferred_encoding', None) + if encoding: + cmd_line = ' '.join(sys.argv) + try: + pre_obj.collection_information['cmd_line'] = cmd_line.decode( + encoding) + except UnicodeDecodeError: + pass + pre_obj.collection_information['file_processed'] = ( + self._storage_file_path) + pre_obj.collection_information['method'] = 'Running Analysis Plugins' + pre_obj.collection_information['plugins'] = options.analysis_plugins + time_of_run = timelib.Timestamp.GetNow() + pre_obj.collection_information['time_of_run'] = time_of_run + + pre_obj.counter = collections.Counter() + + # Assign the preprocessing object to the storage. + # This is normally done in the construction of the storage object, + # however we cannot do that here since the preprocessing object is + # stored inside the storage file, so we need to open it first to + # be able to read it in, before we make changes to it. Thus we need + # to access this protected member of the class. + # pylint: disable=protected-access + storage_file._pre_obj = pre_obj + + # Start queues and load up plugins. + # TODO: add upper queue limit. + analysis_output_queue = multi_process.MultiProcessingQueue() + event_queue_producers = [] + event_queues = [] + analysis_plugins_list = [ + x.strip() for x in options.analysis_plugins.split(',')] + + for _ in xrange(0, len(analysis_plugins_list)): + # TODO: add upper queue limit. + analysis_plugin_queue = multi_process.MultiProcessingQueue() + event_queues.append(analysis_plugin_queue) + event_queue_producers.append( + queue.ItemQueueProducer(event_queues[-1])) + + knowledge_base_object = knowledge_base.KnowledgeBase() + + analysis_plugins = analysis.LoadPlugins( + analysis_plugins_list, event_queues, options) + + # Now we need to start all the plugins. + for analysis_plugin in analysis_plugins: + analysis_report_queue_producer = queue.ItemQueueProducer( + analysis_output_queue) + analysis_context_object = analysis_context.AnalysisContext( + analysis_report_queue_producer, knowledge_base_object) + analysis_process = multiprocessing.Process( + name='Analysis {0:s}'.format(analysis_plugin.plugin_name), + target=analysis_plugin.RunPlugin, args=(analysis_context_object,)) + self._analysis_processes.append(analysis_process) + + analysis_process.start() + logging.info( + u'Plugin: [{0:s}] started.'.format(analysis_plugin.plugin_name)) + else: + event_queue_producers = [] + + output_buffer = output_lib.EventBuffer(output_module, options.dedup) + with output_buffer: + counter = ProcessOutput( + output_buffer, output_module, self._filter_object, + self._filter_buffer, event_queue_producers) + + for information in storage_file.GetStorageInformation(): + if hasattr(information, 'counter'): + counter['Stored Events'] += information.counter['total'] + + if not options.quiet: + logging.info(u'Output processing is done.') + + # Get all reports and tags from analysis plugins. + if options.analysis_plugins: + logging.info(u'Processing data from analysis plugins.') + for event_queue_producer in event_queue_producers: + event_queue_producer.SignalEndOfInput() + + # Wait for all analysis plugins to complete. + for number, analysis_process in enumerate(self._analysis_processes): + logging.debug( + u'Waiting for analysis plugin: {0:d} to complete.'.format(number)) + if analysis_process.is_alive(): + analysis_process.join(10) + else: + logging.warning(u'Plugin {0:d} already stopped.'.format(number)) + analysis_process.terminate() + logging.debug(u'All analysis plugins are now stopped.') + + # Close the output queue. + analysis_output_queue.SignalEndOfInput() + + # Go over each output. + analysis_queue_consumer = PsortAnalysisReportQueueConsumer( + analysis_output_queue, storage_file, self._filter_expression, + self.preferred_encoding) + + analysis_queue_consumer.ConsumeItems() + + if analysis_queue_consumer.tags: + storage_file.StoreTagging(analysis_queue_consumer.tags) + + # TODO: analysis_queue_consumer.anomalies: + + for item, value in analysis_queue_consumer.counter.iteritems(): + counter[item] = value + + if self._filter_object and not counter['Limited By']: + counter['Filter By Date'] = ( + counter['Stored Events'] - counter['Events Included'] - + counter['Events Filtered Out']) + + return counter + + +# TODO: Function: _ConsumeItem is not defined, inspect if we need to define it +# or change the interface so that is not an abstract method. +# TODO: Remove this after dfVFS integration. +# pylint: disable=abstract-method +class PsortAnalysisReportQueueConsumer(queue.ItemQueueConsumer): + """Class that implements an analysis report queue consumer for psort.""" + + def __init__( + self, queue_object, storage_file, filter_string, preferred_encoding): + """Initializes the queue consumer. + + Args: + queue_object: the queue object (instance of Queue). + storage_file: the storage file (instance of StorageFile). + filter_string: the filter string. + preferred_encoding: the preferred encoding. + """ + super(PsortAnalysisReportQueueConsumer, self).__init__(queue_object) + self._filter_string = filter_string + self._preferred_encoding = preferred_encoding + self._storage_file = storage_file + self.anomalies = [] + self.counter = collections.Counter() + self.tags = [] + + def _ConsumeItem(self, analysis_report): + """Consumes an item callback for ConsumeItems. + + Args: + analysis_report: the analysis report (instance of AnalysisReport). + """ + self.counter['Total Reports'] += 1 + self.counter[u'Report: {0:s}'.format(analysis_report.plugin_name)] += 1 + + self.anomalies.extend(analysis_report.GetAnomalies()) + self.tags.extend(analysis_report.GetTags()) + + if self._filter_string: + analysis_report.filter_string = self._filter_string + + # For now we print the report to disk and then save it. + # TODO: Have the option of saving to a separate file and + # do something more here, for instance saving into a HTML + # file, or something else (including potential images). + self._storage_file.StoreReport(analysis_report) + + report_string = analysis_report.GetString() + try: + print report_string.encode(self._preferred_encoding) + except UnicodeDecodeError: + logging.error( + u'Unable to print report due to an unicode decode error. ' + u'The report is stored inside the storage file and can be ' + u'viewed using pinfo [if unable to view please submit a ' + u'bug report https://github.com/log2timeline/plaso/issues') + + +def _AppendEvent(event_object, output_buffer, event_queues): + """Appends an event object to an output buffer and queues. + + Args: + event_object: an event object (instance of EventObject). + output_buffer: the output buffer. + event_queues: a list of event queues that serve as input for + the analysis plugins. + """ + output_buffer.Append(event_object) + + # Needed due to duplicate removals, if two events + # are merged then we'll just pick the first inode value. + inode = getattr(event_object, 'inode', None) + if isinstance(inode, basestring): + inode_list = inode.split(';') + try: + new_inode = int(inode_list[0], 10) + except (ValueError, IndexError): + new_inode = 0 + + event_object.inode = new_inode + + for event_queue in event_queues: + event_queue.ProduceItem(event_object) + + +def ProcessOutput( + output_buffer, output_module, my_filter=None, filter_buffer=None, + analysis_queues=None): + """Fetch EventObjects from storage and process and filter them. + + Args: + output_buffer: output.EventBuffer object. + output_module: The output module (instance of OutputFormatter). + my_filter: A filter object. + filter_buffer: A filter buffer used to store previously discarded + events to store time slice history. + analysis_queues: A list of analysis queues. + """ + counter = collections.Counter() + my_limit = getattr(my_filter, 'limit', 0) + forward_entries = 0 + if not analysis_queues: + analysis_queues = [] + + event_object = output_module.FetchEntry() + while event_object: + if my_filter: + event_match = event_object + if isinstance(event_object, plaso_storage_pb2.EventObject): + # TODO: move serialization to storage, if low-level filtering is needed + # storage should provide functions for it. + serializer = protobuf_serializer.ProtobufEventObjectSerializer + event_match = serializer.ReadSerialized(event_object) + + if my_filter.Match(event_match): + counter['Events Included'] += 1 + if filter_buffer: + # Indicate we want forward buffering. + forward_entries = 1 + # Empty the buffer. + for event_in_buffer in filter_buffer.Flush(): + counter['Events Added From Slice'] += 1 + counter['Events Included'] += 1 + counter['Events Filtered Out'] -= 1 + _AppendEvent(event_in_buffer, output_buffer, analysis_queues) + _AppendEvent(event_object, output_buffer, analysis_queues) + if my_limit: + if counter['Events Included'] == my_limit: + break + else: + if filter_buffer and forward_entries: + if forward_entries <= filter_buffer.size: + _AppendEvent(event_object, output_buffer, analysis_queues) + forward_entries += 1 + counter['Events Added From Slice'] += 1 + counter['Events Included'] += 1 + else: + # Reached the max, don't include other entries. + forward_entries = 0 + counter['Events Filtered Out'] += 1 + elif filter_buffer: + filter_buffer.Append(event_object) + counter['Events Filtered Out'] += 1 + else: + counter['Events Filtered Out'] += 1 + else: + counter['Events Included'] += 1 + _AppendEvent(event_object, output_buffer, analysis_queues) + + event_object = output_module.FetchEntry() + + if output_buffer.duplicate_counter: + counter['Duplicate Removals'] = output_buffer.duplicate_counter + + if my_limit: + counter['Limited By'] = my_limit + return counter + + +def Main(arguments=None): + """Start the tool.""" + multiprocessing.freeze_support() + + front_end = PsortFrontend() + + arg_parser = argparse.ArgumentParser( + description=( + u'PSORT - Application to read, filter and process ' + u'output from a plaso storage file.'), add_help=False) + + tool_group = arg_parser.add_argument_group('Optional Arguments For Psort') + output_group = arg_parser.add_argument_group( + 'Optional Arguments For Output Modules') + analysis_group = arg_parser.add_argument_group( + 'Optional Arguments For Analysis Modules') + + tool_group.add_argument( + '-d', '--debug', action='store_true', dest='debug', default=False, + help='Fall back to debug shell if psort fails.') + + tool_group.add_argument( + '-q', '--quiet', action='store_true', dest='quiet', default=False, + help='Don\'t print out counter information after processing.') + + tool_group.add_argument( + '-h', '--help', action='help', help='Show this help message and exit.') + + tool_group.add_argument( + '-a', '--include_all', action='store_false', dest='dedup', default=True, + help=( + 'By default the tool removes duplicate entries from the output. ' + 'This parameter changes that behavior so all events are included.')) + + tool_group.add_argument( + '-o', '--output_format', '--output-format', metavar='FORMAT', + dest='output_format', default='dynamic', help=( + 'The output format or "-o list" to see a list of available ' + 'output formats.')) + + tool_group.add_argument( + '--analysis', metavar='PLUGIN_LIST', dest='analysis_plugins', + default='', action='store', type=unicode, help=( + 'A comma separated list of analysis plugin names to be loaded ' + 'or "--analysis list" to see a list of available plugins.')) + + tool_group.add_argument( + '-z', '--zone', metavar='TIMEZONE', default='UTC', dest='timezone', help=( + 'The timezone of the output or "-z list" to see a list of available ' + 'timezones.')) + + tool_group.add_argument( + '-w', '--write', metavar='OUTPUTFILE', dest='write', + help='Output filename. Defaults to stdout.') + + tool_group.add_argument( + '--slice', metavar='DATE', dest='slice', type=str, + default='', action='store', help=( + 'Create a time slice around a certain date. This parameter, if ' + 'defined will display all events that happened X minutes before and ' + 'after the defined date. X is controlled by the parameter ' + '--slice_size but defaults to 5 minutes.')) + + tool_group.add_argument( + '--slicer', dest='slicer', action='store_true', default=False, help=( + 'Create a time slice around every filter match. This parameter, if ' + 'defined will save all X events before and after a filter match has ' + 'been made. X is defined by the --slice_size parameter.')) + + tool_group.add_argument( + '--slice_size', dest='slice_size', type=int, default=5, action='store', + help=( + 'Defines the slice size. In the case of a regular time slice it ' + 'defines the number of minutes the slice size should be. In the ' + 'case of the --slicer it determines the number of events before ' + 'and after a filter match has been made that will be included in ' + 'the result set. The default value is 5]. See --slice or --slicer ' + 'for more details about this option.')) + + tool_group.add_argument( + '-v', '--version', dest='version', action='version', + version='log2timeline - psort version {0:s}'.format(plaso.GetVersion()), + help='Show the current version of psort.') + + front_end.AddStorageFileOptions(tool_group) + + tool_group.add_argument( + 'filter', nargs='?', action='store', metavar='FILTER', default=None, + type=unicode, help=( + 'A filter that can be used to filter the dataset before it ' + 'is written into storage. More information about the filters' + ' and it\'s usage can be found here: http://plaso.kiddaland.' + 'net/usage/filters')) + + if arguments is None: + arguments = sys.argv[1:] + + # Add the output module options. + if '-o' in arguments: + argument_index = arguments.index('-o') + 1 + elif '--output_format' in arguments: + argument_index = arguments.index('--output_format') + 1 + elif '--output-format' in arguments: + argument_index = arguments.index('--output-format') + 1 + else: + argument_index = 0 + + if argument_index > 0: + module_names = arguments[argument_index] + front_end.AddOutputModuleOptions(output_group, [module_names]) + + # Add the analysis plugin options. + if '--analysis' in arguments: + argument_index = arguments.index('--analysis') + 1 + + # Get the names of the analysis plugins that should be loaded. + plugin_names = arguments[argument_index] + try: + front_end.AddAnalysisPluginOptions(analysis_group, plugin_names) + except errors.BadConfigOption as exception: + arg_parser.print_help() + print u'' + logging.error('{0:s}'.format(exception)) + return False + + options = arg_parser.parse_args(args=arguments) + + format_str = '[%(levelname)s] %(message)s' + if getattr(options, 'debug', False): + logging.basicConfig(level=logging.DEBUG, format=format_str) + else: + logging.basicConfig(level=logging.INFO, format=format_str) + + if options.timezone == 'list': + front_end.ListTimeZones() + return True + + if options.analysis_plugins == 'list': + front_end.ListAnalysisPlugins() + return True + + if options.output_format == 'list': + front_end.ListOutputModules() + return True + + try: + front_end.ParseOptions(options) + except errors.BadConfigOption as exception: + arg_parser.print_help() + print u'' + logging.error(u'{0:s}'.format(exception)) + return False + + if front_end.preferred_encoding == 'ascii': + logging.warning( + u'The preferred encoding of your system is ASCII, which is not optimal ' + u'for the typically non-ASCII characters that need to be parsed and ' + u'processed. The tool will most likely crash and die, perhaps in a way ' + u'that may not be recoverable. A five second delay is introduced to ' + u'give you time to cancel the runtime and reconfigure your preferred ' + u'encoding, otherwise continue at own risk.') + time.sleep(5) + + try: + counter = front_end.ParseStorage(options) + + if not options.quiet: + logging.info(frontend_utils.FormatHeader('Counter')) + for element, count in counter.most_common(): + logging.info(frontend_utils.FormatOutputString(element, count)) + + except IOError as exception: + # Piping results to "|head" for instance causes an IOError. + if u'Broken pipe' not in exception: + logging.error(u'Processing stopped early: {0:s}.'.format(exception)) + + except KeyboardInterrupt: + pass + + # Catching every remaining exception in case we are debugging. + except Exception as exception: + if not options.debug: + raise + logging.error(u'{0:s}'.format(exception)) + pdb.post_mortem() + + return True + + +if __name__ == '__main__': + if not Main(): + sys.exit(1) + else: + sys.exit(0) diff --git a/plaso/frontend/psort_test.py b/plaso/frontend/psort_test.py new file mode 100644 index 0000000..9353a21 --- /dev/null +++ b/plaso/frontend/psort_test.py @@ -0,0 +1,197 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the psort front-end.""" + +import os +import StringIO +import unittest + +from plaso.formatters import interface as formatters_interface +from plaso.formatters import manager as formatters_manager +from plaso.frontend import psort +from plaso.frontend import test_lib +from plaso.lib import event +from plaso.lib import output +from plaso.lib import pfilter +from plaso.lib import storage +from plaso.lib import timelib_test + + +class TestEvent1(event.EventObject): + DATA_TYPE = 'test:psort:1' + + def __init__(self): + super(TestEvent1, self).__init__() + self.timestamp = 123456 + + +class TestEvent2(event.EventObject): + DATA_TYPE = 'test:psort:2' + + def __init__(self, timestamp): + super(TestEvent2, self).__init__() + self.timestamp = timestamp + self.timestamp_desc = 'Last Written' + + self.parser = 'TestEvent' + + self.display_name = '/dev/none' + self.filename = '/dev/none' + self.some = u'My text dude.' + self.var = {'Issue': False, 'Closed': True} + + +class TestEvent2Formatter(formatters_interface.EventFormatter): + DATA_TYPE = 'test:psort:2' + + FORMAT_STRING = 'My text goes along: {some} lines' + + SOURCE_SHORT = 'LOG' + SOURCE_LONG = 'None in Particular' + + +class TestFormatter(output.LogOutputFormatter): + """Dummy formatter.""" + + def FetchEntry(self, store_number=-1, store_index=-1): + return self.store.GetSortedEntry() + + def Start(self): + self.filehandle.write(( + 'date,time,timezone,MACB,source,sourcetype,type,user,host,' + 'short,desc,version,filename,inode,notes,format,extra\n')) + + def EventBody(self, event_object): + """Writes the event body. + + Args: + event_object: The event object (instance of EventObject). + """ + event_formatter = formatters_manager.EventFormatterManager.GetFormatter( + event_object) + msg, _ = event_formatter.GetMessages(event_object) + source_short, source_long = event_formatter.GetSources(event_object) + self.filehandle.write(u'{0:s}/{1:s} {2:s}\n'.format( + source_short, source_long, msg)) + + +class TestEventBuffer(output.EventBuffer): + """A test event buffer.""" + + def __init__(self, store, formatter=None): + self.record_count = 0 + self.store = store + if not formatter: + formatter = TestFormatter(store) + super(TestEventBuffer, self).__init__(formatter, False) + + def Append(self, event_object): + self._buffer_dict[event_object.EqualityString()] = event_object + self.record_count += 1 + + def Flush(self): + for event_object_key in self._buffer_dict: + self.formatter.EventBody(self._buffer_dict[event_object_key]) + self._buffer_dict = {} + + def End(self): + pass + + +class PsortFrontendTest(test_lib.FrontendTestCase): + """Tests for the psort front-end.""" + + def setUp(self): + """Setup sets parameters that will be reused throughout this test.""" + self._front_end = psort.PsortFrontend() + + # TODO: have sample output generated from the test. + self._test_file = os.path.join(self._TEST_DATA_PATH, 'psort_test.out') + self.first = timelib_test.CopyStringToTimestamp('2012-07-24 21:45:24') + self.last = timelib_test.CopyStringToTimestamp('2016-11-18 01:15:43') + + def testReadEntries(self): + """Ensure returned EventObjects from the storage are within timebounds.""" + timestamp_list = [] + pfilter.TimeRangeCache.ResetTimeConstraints() + pfilter.TimeRangeCache.SetUpperTimestamp(self.last) + pfilter.TimeRangeCache.SetLowerTimestamp(self.first) + + storage_file = storage.StorageFile(self._test_file, read_only=True) + storage_file.SetStoreLimit() + + event_object = storage_file.GetSortedEntry() + while event_object: + timestamp_list.append(event_object.timestamp) + event_object = storage_file.GetSortedEntry() + + self.assertEquals(len(timestamp_list), 8) + self.assertTrue( + timestamp_list[0] >= self.first and timestamp_list[-1] <= self.last) + + storage_file.Close() + + def testOutput(self): + """Testing if psort can output data.""" + events = [] + events.append(TestEvent2(5134324321)) + events.append(TestEvent2(2134324321)) + events.append(TestEvent2(9134324321)) + events.append(TestEvent2(15134324321)) + events.append(TestEvent2(5134324322)) + events.append(TestEvent2(5134024321)) + + output_fd = StringIO.StringIO() + + with test_lib.TempDirectory() as dirname: + temp_file = os.path.join(dirname, 'plaso.db') + + storage_file = storage.StorageFile(temp_file, read_only=False) + pfilter.TimeRangeCache.ResetTimeConstraints() + storage_file.SetStoreLimit() + storage_file.AddEventObjects(events) + storage_file.Close() + + storage_file = storage.StorageFile(temp_file) + with storage_file: + storage_file.store_range = [1] + formatter = TestFormatter(storage_file, output_fd) + event_buffer = TestEventBuffer(storage_file, formatter) + + psort.ProcessOutput(event_buffer, formatter, None) + + event_buffer.Flush() + lines = [] + for line in output_fd.getvalue().split('\n'): + if line == '.': + continue + if line: + lines.append(line) + + # One more line than events (header row). + self.assertEquals(len(lines), 7) + self.assertTrue('My text goes along: My text dude. lines' in lines[2]) + self.assertTrue('LOG/' in lines[2]) + self.assertTrue('None in Particular' in lines[2]) + self.assertEquals(lines[0], ( + 'date,time,timezone,MACB,source,sourcetype,type,user,host,short,desc,' + 'version,filename,inode,notes,format,extra')) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/frontend/test_lib.py b/plaso/frontend/test_lib.py new file mode 100644 index 0000000..0ef9f52 --- /dev/null +++ b/plaso/frontend/test_lib.py @@ -0,0 +1,68 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Front-end related functions and classes for testing.""" + +import os +import shutil +import tempfile +import unittest + + +class Options(object): + """A simple configuration object.""" + + +class TempDirectory(object): + """A self cleaning temporary directory.""" + + def __init__(self): + """Initializes the temporary directory.""" + super(TempDirectory, self).__init__() + self.name = u'' + + def __enter__(self): + """Make this work with the 'with' statement.""" + self.name = tempfile.mkdtemp() + return self.name + + def __exit__(self, unused_type, unused_value, unused_traceback): + """Make this work with the 'with' statement.""" + shutil.rmtree(self.name, True) + + +class FrontendTestCase(unittest.TestCase): + """The unit test case for a front-end.""" + + _TEST_DATA_PATH = os.path.join(os.getcwd(), 'test_data') + + # Show full diff results, part of TestCase so does not follow our naming + # conventions. + maxDiff = None + + def _GetTestFilePath(self, path_segments): + """Retrieves the path of a test file relative to the test data directory. + + Args: + path_segments: the path segments inside the test data directory. + + Returns: + A path of the test file. + """ + # Note that we need to pass the individual path segments to os.path.join + # and not a list. + return os.path.join(self._TEST_DATA_PATH, *path_segments) diff --git a/plaso/frontend/utils.py b/plaso/frontend/utils.py new file mode 100644 index 0000000..8686392 --- /dev/null +++ b/plaso/frontend/utils.py @@ -0,0 +1,212 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Frontend utility classes and functions.""" + +import binascii +import tempfile +import os + +from dfvfs.resolver import resolver as path_spec_resolver + +from plaso.lib import timelib + + +# TODO: add tests for the functions in this class. +class OutputWriter(object): + """Class that defines output writing methods for the frontends and tools.""" + + DATA_BUFFER_SIZE = 32768 + + @classmethod + def GetDateTimeString(cls, timestamp): + """Returns a human readable date and time string in the ISO 8601 format.""" + return timelib.Timestamp.CopyToIsoFormat(timestamp) + + @classmethod + def GetEventDataHexDump(cls, event_object, before=0, length=20): + """Returns a hexadecimal representation of the event data. + + This function creates a hexadecimal string representation based on + the event data described by the event object. + + Args: + event_object: The event object (instance of EventObject). + before: Optional number of bytes to include in the output before + the event. The default is none. + length: Optional number of lines to include in the output. + The default is 20. + + Returns: + A string that contains the hexadecimal representation of the event data. + """ + if not event_object: + return u'Missing event object.' + + if not hasattr(event_object, 'pathspec'): + return u'Event object has no path specification.' + + try: + file_entry = path_spec_resolver.Resolver.OpenFileEntry( + event_object.pathspec) + except IOError as exception: + return u'Unable to open file with error: {0:s}'.format(exception) + + offset = getattr(event_object, 'offset', 0) + if offset - before > 0: + offset -= before + + file_object = file_entry.GetFileObject() + file_object.seek(offset, os.SEEK_SET) + data = file_object.read(int(length) * 16) + file_object.close() + + return cls.GetHexDump(data, offset) + + @classmethod + def GetHexDump(cls, data, offset=0): + """Returns a hexadecimal representation of the contents of a binary string. + + All ASCII characters in the hexadecimal representation (hexdump) are + translated back to their character representation. + + Args: + data: The binary string. + offset: An optional start point in bytes where the data lies, for + presentation purposes. + + Returns: + A string that contains the hexadecimal representation of the binary + string. + """ + hexdata = binascii.hexlify(data) + output_strings = [] + # Note that the // statement is a Python specific method of ensuring + # an integer division. + hexdata_length = len(hexdata) + lines_of_hexdata = hexdata_length // 32 + + line_number = 0 + point = 0 + while line_number < lines_of_hexdata: + line_of_hexdata = hexdata[point:point + 32] + output_strings.append( + cls.GetHexDumpLine(line_of_hexdata, offset, line_number)) + hexdata_length -= 32 + line_number += 1 + point += 32 + + if hexdata_length > 0: + line_of_hexdata = '{0:s}{1:s}'.format( + hexdata[point:], ' ' * (32 - hexdata_length)) + output_strings.append( + cls.GetHexDumpLine(line_of_hexdata, offset, line_number)) + + return '\n'.join(output_strings) + + @classmethod + def GetHexDumpLine(cls, line, orig_ofs, entry_nr=0): + """Returns a single line of 'xxd'-like hexadecimal representation.""" + output_strings = [] + output_strings.append('{0:07x}: '.format(orig_ofs + entry_nr * 16)) + + for bit in range(0, 8): + output_strings.append('{0:s} '.format(line[bit * 4:bit * 4 + 4])) + + for bit in range(0, 16): + try: + data = binascii.unhexlify(line[bit * 2: bit * 2 + 2]) + except TypeError: + data = '.' + + if ord(data) > 31 and ord(data) < 128: + output_strings.append(data) + else: + output_strings.append('.') + + return ''.join(output_strings) + + @classmethod + def WriteFile(cls, input_file_object, output_path=None): + """Writes the data of a file-like object to a "regular" file. + + Args: + input_file_object: the input file-like object. + output_path: the path of the output path. The default is None which will + write the data to a temporary file. + + Returns: + The path of the output file. + """ + if output_path: + output_file_object = open(output_path, 'wb') + else: + output_file_object = tempfile.NamedTemporaryFile() + output_path = output_file_object.name + + input_file_object.seek(0, os.SEEK_SET) + data = input_file_object.read(cls.DATA_BUFFER_SIZE) + while data: + output_file_object.write(data) + data = input_file_object.read(cls.DATA_BUFFER_SIZE) + + output_file_object.close() + return output_path + + +def FormatHeader(header, char='*'): + """Formats the header as a line of 80 chars with the header text centered.""" + format_string = '\n{{0:{0:s}^80}}'.format(char) + return format_string.format(u' {0:s} '.format(header)) + + +def FormatOutputString(name, description, col_length=25): + """Return a formatted string ready for output.""" + max_width = 80 + line_length = max_width - col_length - 3 + + # TODO: add an explanation what this code is doing. + fmt = u'{{:>{0:d}s}} : {{}}'.format(col_length) + fmt_second = u'{{:<{0:d}}}{{}}'.format(col_length + 3) + + description = unicode(description) + if len(description) < line_length: + return fmt.format(name, description) + + # Split each word up in the description. + words = description.split() + + current = 0 + + lines = [] + word_buffer = [] + for word in words: + current += len(word) + 1 + if current >= line_length: + current = len(word) + lines.append(u' '.join(word_buffer)) + word_buffer = [word] + else: + word_buffer.append(word) + lines.append(u' '.join(word_buffer)) + + ret = [] + ret.append(fmt.format(name, lines[0])) + for line in lines[1:]: + ret.append(fmt_second.format('', line)) + + return u'\n'.join(ret) diff --git a/plaso/lib/__init__.py b/plaso/lib/__init__.py new file mode 100644 index 0000000..0c8696c --- /dev/null +++ b/plaso/lib/__init__.py @@ -0,0 +1,17 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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/plaso/lib/binary.py b/plaso/lib/binary.py new file mode 100644 index 0000000..eeef22b --- /dev/null +++ b/plaso/lib/binary.py @@ -0,0 +1,280 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a helper library to read binary files.""" + +import binascii +import logging +import os + + +def ByteArrayCopyToString(byte_array, codepage='utf-8'): + """Copies a UTF-8 encoded byte array into a Unicode string. + + Args: + byte_array: A byte array containing an UTF-8 encoded string. + codepage: The codepage of the byte stream. The default is utf-8. + + Returns: + A Unicode string. + """ + byte_stream = ''.join(map(chr, byte_array)) + return ByteStreamCopyToString(byte_stream, codepage=codepage) + + +def ByteStreamCopyToString(byte_stream, codepage='utf-8'): + """Copies a UTF-8 encoded byte stream into a Unicode string. + + Args: + byte_stream: A byte stream containing an UTF-8 encoded string. + codepage: The codepage of the byte stream. The default is utf-8. + + Returns: + A Unicode string. + """ + try: + string = byte_stream.decode(codepage) + except UnicodeDecodeError: + logging.warning( + u'Unable to decode {0:s} formatted byte stream.'.format(codepage)) + string = byte_stream.decode(codepage, errors='ignore') + + string, _, _ = string.partition('\x00') + return string + + +def ByteStreamCopyToGuid(byte_stream, byte_order='little-endian'): + """Reads a GUID from the byte stream. + + Args: + byte_stream: The byte stream that contains the UTF-16 formatted stream. + byte_order: The byte order, either big- or little-endian. The default is + little-endian. + + Returns: + String containing the GUID. + """ + if len(byte_stream) >= 16: + if byte_order == 'big-endian': + return ( + u'{{{0:02x}{1:02x}{2:02x}{3:02x}-{4:02x}{5:02x}-' + u'{6:02x}{7:02x}-{8:02x}{9:02x}-' + u'{10:02x}{11:02x}{12:02x}{13:02x}{14:02x}{15:02x}}}').format( + *byte_stream[:16]) + elif byte_order == 'little-endian': + return ( + u'{{{3:02x}{2:02x}{1:02x}{0:02x}-{5:02x}{4:02x}-' + u'{7:02x}{6:02x}-{8:02x}{9:02x}-' + u'{10:02x}{11:02x}{12:02x}{13:02x}{14:02x}{15:02x}}}').format( + *byte_stream[:16]) + return u'' + + +def ByteStreamCopyToUtf16Stream(byte_stream, byte_stream_size=None): + """Reads an UTF-16 formatted stream from a byte stream. + + The UTF-16 formatted stream should be terminated by an end-of-string + character (\x00\x00). Otherwise the function reads up to the byte stream size. + + Args: + byte_stream: The byte stream that contains the UTF-16 formatted stream. + byte_stream_size: Optional byte stream size or None if the entire + byte stream should be read. The default is None. + + Returns: + String containing the UTF-16 formatted stream. + """ + byte_stream_index = 0 + if not byte_stream_size: + byte_stream_size = len(byte_stream) + + while byte_stream_index + 1 < byte_stream_size: + if (byte_stream[byte_stream_index] == '\x00' and + byte_stream[byte_stream_index + 1] == '\x00'): + break + + byte_stream_index += 2 + + return byte_stream[0:byte_stream_index] + + +def ReadUtf16Stream(file_object, offset=None, byte_size=0): + """Reads an UTF-16 formatted stream from a file-like object. + + Reads an UTF-16 formatted stream that's terminated by + an end-of-string character (\x00\x00) or upto the byte size. + + Args: + file_object: A file-like object to read the data from. + offset: An offset into the file object data, if -1 or not set + the current location into the file object data is used. + byte_size: Maximum number of bytes to read or 0 if the function + should keep reading upto the end of file. + + Returns: + An Unicode string. + """ + if offset is not None: + file_object.seek(offset, os.SEEK_SET) + + char_buffer = [] + + stream_index = 0 + char_raw = file_object.read(2) + while char_raw: + if byte_size and stream_index >= byte_size: + break + + if '\x00\x00' in char_raw: + break + char_buffer.append(char_raw) + stream_index += 2 + char_raw = file_object.read(2) + + return ReadUtf16(''.join(char_buffer)) + + +def Ut16StreamCopyToString(byte_stream, byte_stream_size=None): + """Copies an UTF-16 formatted byte stream to a string. + + The UTF-16 formatted byte stream should be terminated by an end-of-string + character (\x00\x00). Otherwise the function reads up to the byte stream size. + + Args: + byte_stream: The UTF-16 formatted byte stream. + byte_stream_size: The byte stream size or None if the entire byte stream + should be used. + + Returns: + An Unicode string. + """ + utf16_stream = ByteStreamCopyToUtf16Stream( + byte_stream, byte_stream_size=byte_stream_size) + + try: + return utf16_stream.decode('utf-16-le') + except (UnicodeDecodeError, UnicodeEncodeError) as exception: + logging.error(u'Unable to decode string: {0:s} with error: {1:s}'.format( + HexifyBuffer(utf16_stream), exception)) + + return utf16_stream.decode('utf-16-le', errors='ignore') + + +def ArrayOfUt16StreamCopyToString(byte_stream, byte_stream_size=None): + """Copies an array of UTF-16 formatted byte streams to an array of strings. + + The UTF-16 formatted byte stream should be terminated by an end-of-string + character (\x00\x00). Otherwise the function reads upto the byte stream size. + + Args: + byte_stream: The UTF-16 formatted byte stream. + byte_stream_size: The byte stream size or None if the entire byte stream + should be used. + + Returns: + An array of Unicode strings. + """ + array_of_strings = [] + utf16_stream_start = 0 + byte_stream_index = 0 + if not byte_stream_size: + byte_stream_size = len(byte_stream) + + while byte_stream_index + 1 < byte_stream_size: + if (byte_stream[byte_stream_index] == '\x00' and + byte_stream[byte_stream_index + 1] == '\x00'): + + if byte_stream_index - utf16_stream_start <= 2: + break + + array_of_strings.append( + byte_stream[utf16_stream_start:byte_stream_index].decode( + 'utf-16-le')) + utf16_stream_start = byte_stream_index + 2 + + byte_stream_index += 2 + + return array_of_strings + + +def ArrayOfUt16StreamCopyToStringTable(byte_stream, byte_stream_size=None): + """Copies an array of UTF-16 formatted byte streams to a string table. + + The string table is a dict of strings with the byte offset as their key. + The UTF-16 formatted byte stream should be terminated by an end-of-string + character (\x00\x00). Otherwise the function reads upto the byte stream size. + + Args: + byte_stream: The UTF-16 formatted byte stream. + byte_stream_size: The byte stream size or None if the entire byte stream + should be used. + + Returns: + A dict of Unicode strings with the byte offset as their key. + """ + string_table = {} + utf16_stream_start = 0 + byte_stream_index = 0 + if not byte_stream_size: + byte_stream_size = len(byte_stream) + + while byte_stream_index + 1 < byte_stream_size: + if (byte_stream[byte_stream_index] == '\x00' and + byte_stream[byte_stream_index + 1] == '\x00'): + + if byte_stream_index - utf16_stream_start <= 2: + break + + string = byte_stream[utf16_stream_start:byte_stream_index].decode( + 'utf-16-le') + string_table[utf16_stream_start] = string + utf16_stream_start = byte_stream_index + 2 + + byte_stream_index += 2 + + return string_table + + +def ReadUtf16(string_buffer): + """Returns a decoded UTF-16 string from a string buffer.""" + if type(string_buffer) in (list, tuple): + use_buffer = u''.join(string_buffer) + else: + use_buffer = string_buffer + + if not type(use_buffer) in (str, unicode): + return u'' + + try: + return use_buffer.decode('utf-16').replace('\x00', '') + except SyntaxError as exception: + logging.error(u'Unable to decode string: {0:s} with error: {1:s}.'.format( + HexifyBuffer(string_buffer), exception)) + except (UnicodeDecodeError, UnicodeEncodeError) as exception: + logging.error(u'Unable to decode string: {0:s} with error: {1:s}'.format( + HexifyBuffer(string_buffer), exception)) + + return use_buffer.decode('utf-16', errors='ignore').replace('\x00', '') + + +def HexifyBuffer(string_buffer): + """Return a string with the hex representation of a string buffer.""" + chars = [] + for char in string_buffer: + chars.append(binascii.hexlify(char)) + + return u'\\x{0:s}'.format(u'\\x'.join(chars)) diff --git a/plaso/lib/binary_test.py b/plaso/lib/binary_test.py new file mode 100644 index 0000000..fce4f06 --- /dev/null +++ b/plaso/lib/binary_test.py @@ -0,0 +1,206 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a unit test for the binary helper in Plaso.""" +import os +import unittest + +from plaso.lib import binary + + +class BinaryTests(unittest.TestCase): + """A unit test for the binary helper functions.""" + + def setUp(self): + """Set up the needed variables used througout.""" + # String: "þrándur" - uses surrogate pairs to test four byte character + # decoding. + self._unicode_string_1 = ( + '\xff\xfe\xfe\x00\x72\x00\xe1\x00\x6E\x00\x64\x00\x75\x00\x72\x00') + + # String: "What\x00is". + self._ascii_string_1 = ( + '\x57\x00\x68\x00\x61\x00\x74\x00\x00\x00\x69\x00\x73\x00') + + # String: "What is this?". + self._ascii_string_2 = ( + '\x57\x00\x68\x00\x61\x00\x74\x00\x20\x00\x69\x00\x73\x00' + '\x20\x00\x74\x00\x68\x00\x69\x00\x73\x00\x3F\x00') + + # Show full diff results, part of TestCase so does not follow our naming + # conventions. + self.maxDiff = None + + def testReadUtf16Stream(self): + """Test reading an UTF-16 stream from a file-like object.""" + path = os.path.join('test_data', 'PING.EXE-B29F6629.pf') + with open(path, 'rb') as fh: + # Read a null char terminated string. + fh.seek(0x10) + self.assertEquals(binary.ReadUtf16Stream(fh), 'PING.EXE') + + # Read a fixed size string. + fh.seek(0x27f8) + expected_string = u'\\DEVICE\\HARDDISKVOLUME' + string = binary.ReadUtf16Stream(fh, byte_size=44) + self.assertEquals(string, expected_string) + + fh.seek(0x27f8) + expected_string = u'\\DEVICE\\HARDDISKVOLUME1' + string = binary.ReadUtf16Stream(fh, byte_size=46) + self.assertEquals(string, expected_string) + + # Read another null char terminated string. + fh.seek(7236) + self.assertEquals( + binary.ReadUtf16Stream(fh), + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\NTDLL.DLL') + + def testUt16StreamCopyToString(self): + """Test copying an UTF-16 byte stream to a string.""" + path = os.path.join('test_data', 'PING.EXE-B29F6629.pf') + with open(path, 'rb') as fh: + byte_stream = fh.read() + + # Read a null char terminated string. + self.assertEquals( + binary.Ut16StreamCopyToString(byte_stream[0x10:]), 'PING.EXE') + + # Read a fixed size string. + expected_string = u'\\DEVICE\\HARDDISKVOLUME' + string = binary.Ut16StreamCopyToString( + byte_stream[0x27f8:], byte_stream_size=44) + self.assertEquals(string, expected_string) + + expected_string = u'\\DEVICE\\HARDDISKVOLUME1' + string = binary.Ut16StreamCopyToString( + byte_stream[0x27f8:], byte_stream_size=46) + self.assertEquals(string, expected_string) + + # Read another null char terminated string. + expected_string = ( + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\NTDLL.DLL') + + string = binary.Ut16StreamCopyToString(byte_stream[7236:]) + self.assertEquals(string, expected_string) + + def testArrayOfUt16StreamCopyToString(self): + """Test copying an array of UTF-16 byte streams to strings.""" + path = os.path.join('test_data', 'PING.EXE-B29F6629.pf') + with open(path, 'rb') as fh: + byte_stream = fh.read() + + strings_array = binary.ArrayOfUt16StreamCopyToString( + byte_stream[0x1c44:], byte_stream_size=2876) + expected_strings_array = [ + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\NTDLL.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\KERNEL32.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\APISETSCHEMA.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\KERNELBASE.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\LOCALE.NLS', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\PING.EXE', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\ADVAPI32.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\MSVCRT.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\SECHOST.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\RPCRT4.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\IPHLPAPI.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\NSI.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\WINNSI.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\USER32.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\GDI32.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\LPK.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\USP10.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\WS2_32.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\IMM32.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\MSCTF.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\EN-US\\PING.EXE.MUI', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\GLOBALIZATION\\SORTING\\' + u'SORTDEFAULT.NLS', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\MSWSOCK.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\WSHQOS.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\WSHTCPIP.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\WSHIP6.DLL'] + + self.assertEquals(strings_array, expected_strings_array) + + def testArrayOfUt16StreamCopyToStringTable(self): + """Test copying an array of UTF-16 byte streams to a string table.""" + path = os.path.join('test_data', 'PING.EXE-B29F6629.pf') + with open(path, 'rb') as fh: + byte_stream = fh.read() + + string_table = binary.ArrayOfUt16StreamCopyToStringTable( + byte_stream[0x1c44:], byte_stream_size=2876) + expected_string_table = { + 0: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\NTDLL.DLL', + 102: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\KERNEL32.DLL', + 210: (u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\' + u'APISETSCHEMA.DLL'), + 326: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\KERNELBASE.DLL', + 438: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\LOCALE.NLS', + 542: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\PING.EXE', + 642: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\ADVAPI32.DLL', + 750: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\MSVCRT.DLL', + 854: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\SECHOST.DLL', + 960: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\RPCRT4.DLL', + 1064: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\IPHLPAPI.DLL', + 1172: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\NSI.DLL', + 1270: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\WINNSI.DLL', + 1374: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\USER32.DLL', + 1478: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\GDI32.DLL', + 1580: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\LPK.DLL', + 1678: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\USP10.DLL', + 1780: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\WS2_32.DLL', + 1884: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\IMM32.DLL', + 1986: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\MSCTF.DLL', + 2088: (u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\EN-US\\' + u'PING.EXE.MUI'), + 2208: (u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\GLOBALIZATION\\' + u'SORTING\\SORTDEFAULT.NLS'), + 2348: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\MSWSOCK.DLL', + 2454: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\WSHQOS.DLL', + 2558: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\WSHTCPIP.DLL', + 2666: u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\WSHIP6.DLL'} + + self.assertEquals(string_table, expected_string_table) + + def testStringParsing(self): + """Test parsing the ASCII string.""" + self.assertEquals(binary.ReadUtf16(self._ascii_string_1), 'Whatis') + + self.assertEquals(binary.ReadUtf16(self._ascii_string_2), 'What is this?') + + uni_text = binary.ReadUtf16(self._unicode_string_1) + self.assertEquals(uni_text, u'þrándur') + + def testHex(self): + """Test the hexadecimal representation of data.""" + hex_string_1 = binary.HexifyBuffer(self._ascii_string_1) + hex_compare = ( + '\\x57\\x00\\x68\\x00\\x61\\x00\\x74\\x00\\x00\\x00\\x69\\x00' + '\\x73\\x00') + self.assertEquals(hex_string_1, hex_compare) + + hex_string_2 = binary.HexifyBuffer(self._unicode_string_1) + hex_compare_unicode = ( + '\\xff\\xfe\\xfe\\x00\\x72\\x00\\xe1\\x00\\x6e\\x00\\x64\\x00' + '\\x75\\x00\\x72\\x00') + + self.assertEquals(hex_string_2, hex_compare_unicode) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/lib/bufferlib.py b/plaso/lib/bufferlib.py new file mode 100644 index 0000000..3a5bada --- /dev/null +++ b/plaso/lib/bufferlib.py @@ -0,0 +1,77 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains buffer related objects used in plaso.""" + + +class CircularBuffer(object): + """Simple circular buffer for storing EventObjects.""" + + def __init__(self, size): + """Initialize a fixed size circular buffer. + + Args: + size: An integer indicating the number of elements in the buffer. + """ + self._size = size + self._index = 0 + self._list = [] + + def __len__(self): + """Return the length (the fixed size).""" + return self._size + + @property + def size(self): + return self._size + + def GetCurrent(self): + """Return the current item that index points to.""" + index = self._index - 1 + if index < 0: + return + + return self._list[index] + + def Clear(self): + """Clear all elements in the list.""" + self._list = [] + self._index = 0 + + def __iter__(self): + """Return all elements from the list.""" + for index in range(0, self._size): + try: + yield self._list[(self._index + index) % self._size] + except IndexError: + pass + + def Flush(self): + """Return a generator for all items and clear the buffer.""" + for item in self: + yield item + self.Clear() + + def Append(self, item): + """Add an item to the list.""" + if self._index >= self._size: + self._index = self._index % self._size + + try: + self._list[self._index] = item + except IndexError: + self._list.append(item) + self._index += 1 diff --git a/plaso/lib/bufferlib_test.py b/plaso/lib/bufferlib_test.py new file mode 100644 index 0000000..3eb051f --- /dev/null +++ b/plaso/lib/bufferlib_test.py @@ -0,0 +1,60 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for plaso.lib.buffer""" + +import unittest + +from plaso.lib import bufferlib + + +class TestBuffer(unittest.TestCase): + """Test the circular buffer.""" + + def testBuffer(self): + items = range(1, 11) + + circular_buffer = bufferlib.CircularBuffer(10) + + self.assertEquals(len(circular_buffer), 10) + self.assertEquals(circular_buffer.size, 10) + self.assertTrue(circular_buffer.GetCurrent() is None) + + for item in items: + circular_buffer.Append(item) + self.assertEquals(circular_buffer.GetCurrent(), item) + self.assertEquals(circular_buffer.size, 10) + + content = list(circular_buffer) + self.assertEquals(items, content) + + circular_buffer.Append(11) + self.assertEquals( + [2, 3, 4, 5, 6, 7, 8, 9, 10, 11], list(circular_buffer.Flush())) + + self.assertEquals(circular_buffer.GetCurrent(), None) + + new_items = range(1, 51) + for item in new_items: + circular_buffer.Append(item) + self.assertEquals(circular_buffer.GetCurrent(), item) + self.assertEquals(circular_buffer.size, 10) + + self.assertEquals(range(41, 51), list(circular_buffer)) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/lib/errors.py b/plaso/lib/errors.py new file mode 100644 index 0000000..595ba3b --- /dev/null +++ b/plaso/lib/errors.py @@ -0,0 +1,113 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the error classes.""" + +class Error(Exception): + """Base error class.""" + + +class BadConfigOption(Error): + """Raised when the engine is started with a faulty parameter.""" + + +class CollectorError(Error): + """Class that defines collector errors.""" + + +class NotAText(Error): + """Raised when trying to read a text on a non-text sample.""" + + +class NoFormatterFound(Error): + """Raised when no formatter is found for a particular event.""" + + +class PathNotFound(Error): + """Raised when a preprocessor fails to fill in a path variable.""" + + +class PreProcessFail(Error): + """Raised when a preprocess module is unable to gather information.""" + + +class ProxyFailedToStart(Error): + """Raised when unable to start a proxy.""" + + +class QueueEmpty(Error): + """Class that implements a queue empty exception.""" + + +class QueueFull(Error): + """Class that implements a queue full exception.""" + + +class SameFileType(Error): + """Raised when a file is being evaluated against the same driver type.""" + + +class SourceScannerError(Error): + """Class that defines source scanner errors.""" + + +class TimestampNotCorrectlyFormed(Error): + """Raised when there is an error adding a timestamp to an EventObject.""" + + +class UnableToOpenFile(Error): + """Raised when a PlasoFile class attempts to open a file it cannot open.""" + + +class UnableToOpenFilesystem(Error): + """Raised when unable to open filesystem.""" + + +class UnableToParseFile(Error): + """Raised when a parser is not designed to parse a file.""" + + +class UserAbort(Error): + """Class that defines an user initiated abort exception.""" + + +class WrongBencodePlugin(Error): + """Error reporting wrong bencode plugin used.""" + + +class WrongFilterOption(Error): + """Raised when the filter option is badly formed.""" + + +class WrongFormatter(Error): + """Raised when the formatter is not applicable for a particular event.""" + + +class WrongPlistPlugin(Error): + """Error reporting wrong plist plugin used.""" + + +class WrongPlugin(Error): + """Raised when the plugin is of the wrong type.""" + + +class WrongProtobufEntry(Error): + """Raised when an EventObject cannot be serialized as a protobuf.""" + + +class WinRegistryValueError(Error): + """Raised when there is an issue reading a registry value.""" diff --git a/plaso/lib/event.py b/plaso/lib/event.py new file mode 100644 index 0000000..16cc98b --- /dev/null +++ b/plaso/lib/event.py @@ -0,0 +1,478 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The core object definitions, e.g. the event object.""" + +import collections +import logging +import uuid + +from plaso.formatters import manager as formatters_manager +from plaso.lib import timelib +from plaso.lib import utils + +import pytz + + +class AnalysisReport(object): + """Class that defines an analysis report.""" + + def __init__(self): + """Initializes the analysis report.""" + super(AnalysisReport, self).__init__() + self._anomalies = [] + self._tags = [] + + def __unicode__(self): + """Return an unicode string representation of the report.""" + return self.GetString() + + def GetAnomalies(self): + """Retrieves the list of anomalies that are attached to the report.""" + return self._anomalies + + def GetString(self): + """Return an unicode string representation of the report.""" + # TODO: Make this a more complete function that includes images + # and the option of saving as a full fledged HTML document. + string_list = [] + string_list.append(u'Report generated from: {0:s}'.format(self.plugin_name)) + + time_compiled = getattr(self, 'time_compiled', 0) + if time_compiled: + time_compiled = timelib.Timestamp.CopyToIsoFormat(time_compiled) + string_list.append(u'Generated on: {0:s}'.format(time_compiled)) + + filter_string = getattr(self, 'filter_string', '') + if filter_string: + string_list.append(u'Filter String: {0:s}'.format(filter_string)) + + string_list.append(u'') + string_list.append(u'Report text:') + string_list.append(self.text) + + return u'\n'.join(string_list) + + def GetTags(self): + """Retrieves the list of event tags that are attached to the report.""" + return self._tags + + # TODO: rename text to body? + def SetText(self, lines_of_text): + """Sets the text based on a list of lines of text. + + Args: + lines_of_text: a list containing lines of text. + """ + # Append one empty string to make sure a new line is added to the last + # line of text as well. + lines_of_text.append(u'') + + self.text = u'\n'.join(lines_of_text) + + +# TODO: Re-design the event object to make it lighter, perhaps template +# based. The current design is too slow and needs to be improved. +class EventObject(object): + """An event object is the main datastore for an event in plaso. + + The framework is designed to parse files and create an event + from every single record, line or key extracted from the file. + + An EventObject is the main data storage for an event in plaso. + + This class defines the high level interface of EventObject. + Before creating an EventObject a class needs to be implemented + that inherits from EventObject and implements the functions in it. + + The EventObject is then used by output processing for saving + in other forms, such as a protobuff, AFF4 container, CSV files, + databases, etc. + + The goal of the EventObject is to provide a easily extensible + data storage of each events internally in the tool. + + The main EventObject only exposes those functions that the + implementations need to implement. The functions that are needed + simply provide information about the event, or describe the + attributes that are necessary. How they are assembled is totally + up to the implementation. + + All required attributes of the EventObject are passed to the + constructor of the object while the optional ones are set + using the method SetValue(attribute, value). + """ + # This is a convenience variable to define event object as + # simple value objects. Its runtime equivalent data_type + # should be used in code logic. + DATA_TYPE = '' + + # This is a reserved variable just used for comparison operation and defines + # attributes that should not be used during evaluation of whether two + # EventObjects are the same. + COMPARE_EXCLUDE = frozenset([ + 'timestamp', 'inode', 'pathspec', 'filename', 'uuid', + 'data_type', 'display_name', 'store_number', 'store_index', 'tag']) + + def __init__(self): + """Initializes the event object.""" + self.uuid = uuid.uuid4().get_hex() + if self.DATA_TYPE: + self.data_type = self.DATA_TYPE + + def EqualityString(self): + """Return a string describing the EventObject in terms of object equality. + + The details of this function must match the logic of __eq__. EqualityStrings + of two event objects should be the same if and only if the EventObjects are + equal as described in __eq__. + + Returns: + String: will match another EventObject's Equality String if and only if + the EventObjects are equal + """ + fields = sorted(list(self.GetAttributes().difference(self.COMPARE_EXCLUDE))) + + # TODO: Review this (after 1.1.0 release). Is there a better/more clean + # method of removing the timestamp description field out of the fields list? + parser = getattr(self, 'parser', u'') + if parser == u'filestat': + # We don't want to compare the timestamp description field when comparing + # filestat events. This is done to be able to join together FILE events + # that have the same timestamp, yet different description field (as in an + # event that has for instance the same timestamp for mtime and atime, + # joining it together into a single event). + try: + timestamp_desc_index = fields.index('timestamp_desc') + del fields[timestamp_desc_index] + except ValueError: + pass + + basic = [self.timestamp, self.data_type] + attributes = [] + for attribute in fields: + value = getattr(self, attribute) + if type(value) is dict: + attributes.append(sorted(value.items())) + elif type(value) is set: + attributes.append(sorted(list(value))) + else: + attributes.append(value) + identity = basic + [x for pair in zip(fields, attributes) for x in pair] + + if parser == 'filestat': + inode = getattr(self, 'inode', 'a') + if inode == 'a': + inode = '_' + str(uuid.uuid4()) + identity.append('inode') + identity.append(inode) + + return u'|'.join(map(unicode, identity)) + + def __eq__(self, event_object): + """Return a boolean indicating if two EventObject are considered equal. + + Compares two EventObject objects together and evaluates if they are + the same or close enough to be considered to represent the same event. + + For two EventObject objects to be considered the same they need to + have the following conditions: + + Have the same timestamp. + + Have the same data_type value. + + Have the same set of attributes. + + Compare all other attributes than those that are reserved, and + they all have to match. + + The following attributes are considered to be 'reserved' and not used + for the comparison, so they may be different yet the EventObject is still + considered to be equal: + + inode + + pathspec + + filename + + display_name + + store_number + + store_index + + Args: + event_object: The EventObject that is being compared to this one. + + Returns: + True: if both EventObjects are considered equal, otherwise False. + """ + + # Note: if this method changes, the above EqualityString method MUST be + # updated as well + if not isinstance(event_object, EventObject): + return False + + if self.timestamp != event_object.timestamp: + return False + + if self.data_type != event_object.data_type: + return False + + attributes = self.GetAttributes() + if attributes != event_object.GetAttributes(): + return False + + # Here we have to deal with "near" duplicates, so not all attributes + # should be compared. + for attribute in attributes.difference(self.COMPARE_EXCLUDE): + if getattr(self, attribute) != getattr(event_object, attribute): + return False + + # If we are dealing with the stat parser the inode number is the one + # attribute that really matters, unlike others. + if 'filestat' in getattr(self, 'parser', ''): + return utils.GetUnicodeString(getattr( + self, 'inode', 'a')) == utils.GetUnicodeString(getattr( + event_object, 'inode', 'b')) + + return True + + def GetAttributes(self): + """Return a list of all defined attributes.""" + return set(self.__dict__.keys()) + + def GetValues(self): + """Returns a dictionary of all defined attributes and their values.""" + values = {} + for attribute_name in self.GetAttributes(): + values[attribute_name] = getattr(self, attribute_name) + return values + + def GetString(self): + """Return a unicode string representation of an EventObject.""" + return unicode(self) + + def __str__(self): + """Return a string object of the EventObject.""" + return unicode(self).encode('utf-8') + + def __unicode__(self): + """Print a human readable string from the EventObject.""" + out_write = [] + + out_write.append(u'+-' * 40) + out_write.append(u'[Timestamp]:\n {0:s}'.format( + timelib.Timestamp.CopyToIsoFormat(self.timestamp))) + out_write.append(u'\n[Message Strings]:') + + # TODO: move formatting testing to a formatters (manager) test. + event_formatter = formatters_manager.EventFormatterManager.GetFormatter( + self) + if not event_formatter: + out_write.append(u'None') + else: + msg, msg_short = event_formatter.GetMessages(self) + source_short, source_long = event_formatter.GetSources(self) + out_write.append(u'{2:>7}: {0}\n{3:>7}: {1}\n'.format( + utils.GetUnicodeString(msg_short), utils.GetUnicodeString(msg), + 'Short', 'Long')) + out_write.append(u'{2:>7}: {0}\n{3:>7}: {1}\n'.format( + utils.GetUnicodeString(source_short), + utils.GetUnicodeString(source_long), 'Source Short', 'Source Long')) + + if hasattr(self, 'pathspec'): + pathspec_string = self.pathspec.comparable + out_write.append(u'[Pathspec]:\n {0:s}\n'.format( + pathspec_string.replace('\n', '\n '))) + + out_additional = [] + out_write.append(u'[Reserved attributes]:') + out_additional.append(u'[Additional attributes]:') + + for attr_key, attr_value in sorted(self.GetValues().items()): + if attr_key in utils.RESERVED_VARIABLES: + if attr_key == 'pathspec': + continue + else: + out_write.append( + u' {{{key}}} {value}'.format(key=attr_key, value=attr_value)) + else: + out_additional.append( + u' {{{key}}} {value}'.format(key=attr_key, value=attr_value)) + + out_write.append(u'\n') + out_additional.append(u'') + + part_1 = u'\n'.join(out_write) + part_2 = u'\n'.join(out_additional) + return part_1 + part_2 + + +class EventTag(object): + """A native Python object for the EventTagging protobuf. + + The EventTag object should have the following attributes: + (optional attributes surrounded with brackets) + + store_number: An integer, pointing to the store the EventObject is. + + store_index: An index into the store where the EventObject is. + + event_uuid: An UUID value of the event this tag belongs to. + + [comment]: An arbitrary string containing comments about the event. + + [color]: A string containing color information. + + [tags]: A list of strings with tags, eg: 'Malware', 'Entry Point'. + + The tag either needs to have an event_uuid defined or both the store_number + and store_index to be valid (not both, if both defined the store_number and + store_index will be used). + """ + + # TODO: Enable __slots__ once we tested the first round of changes. + @property + def string_key(self): + """Return a string index key for this tag.""" + if not self.IsValidForSerialization(): + return '' + + uuid_string = getattr(self, 'event_uuid', None) + if uuid_string: + return uuid_string + + return u'{}:{}'.format(self.store_number, self.store_index) + + def GetString(self): + """Retrieves a string representation of the event.""" + ret = [] + ret.append(u'-' * 50) + if getattr(self, 'store_number', 0): + ret.append(u'{0:>7}:\n\tNumber: {1}\n\tIndex: {2}'.format( + 'Store', self.store_number, self.store_index)) + else: + ret.append(u'{0:>7}:\n\tUUID: {1}'.format('Store', self.event_uuid)) + if hasattr(self, 'comment'): + ret.append(u'{:>7}: {}'.format('Comment', self.comment)) + if hasattr(self, 'color'): + ret.append(u'{:>7}: {}'.format('Color', self.color)) + if hasattr(self, 'tags'): + ret.append(u'{:>7}: {}'.format('Tags', u','.join(self.tags))) + + return u'\n'.join(ret) + + def IsValidForSerialization(self): + """Return whether or not this is a valid tag object.""" + if getattr(self, 'event_uuid', None): + return True + + if getattr(self, 'store_number', 0) and getattr( + self, 'store_index', -1) >= 0: + return True + + return False + + +class PreprocessObject(object): + """Object used to store all information gained from preprocessing.""" + + def __init__(self): + """Initializes the preprocess object.""" + super(PreprocessObject, self).__init__() + self._user_ids_to_names = None + self.zone = pytz.UTC + + def GetUserMappings(self): + """Returns a dictionary objects mapping SIDs or UIDs to usernames.""" + if self._user_ids_to_names is None: + self._user_ids_to_names = {} + + if self._user_ids_to_names: + return self._user_ids_to_names + + for user in getattr(self, 'users', []): + if 'sid' in user: + user_id = user.get('sid', u'') + elif 'uid' in user: + user_id = user.get('uid', u'') + else: + user_id = u'' + + if user_id: + self._user_ids_to_names[user_id] = user.get('name', user_id) + + return self._user_ids_to_names + + def GetUsernameById(self, user_id): + """Returns a username for a specific user identifier. + + Args: + user_id: The user identifier, either a SID or UID. + + Returns: + If available the user name for the identifier, otherwise the string '-'. + """ + user_ids_to_names = self.GetUserMappings() + + return user_ids_to_names.get(user_id, '-') + + # TODO: change to property with getter and setter. + def SetTimezone(self, timezone_identifier): + """Sets the timezone. + + Args: + timezone_identifier: string containing the identifier of the timezone, + e.g. 'UTC' or 'Iceland'. + """ + try: + self.zone = pytz.timezone(timezone_identifier) + except pytz.UnknownTimeZoneError as exception: + logging.warning( + u'Unable to set timezone: {0:s} with error: {1:s}.'.format( + timezone_identifier, exception)) + + def SetCollectionInformationValues(self, dict_object): + """Sets the collection information values. + + Args: + dict_object: dictionary object containing the collection information + values. + """ + self.collection_information = dict(dict_object) + + if 'configure_zone' in self.collection_information: + self.collection_information['configure_zone'] = pytz.timezone( + self.collection_information['configure_zone']) + + def SetCounterValues(self, dict_object): + """Sets the counter values. + + Args: + dict_object: dictionary object containing the counter values. + """ + self.counter = collections.Counter() + for key, value in dict_object.iteritems(): + self.counter[key] = value + + def SetPluginCounterValues(self, dict_object): + """Sets the plugin counter values. + + Args: + dict_object: dictionary object containing the plugin counter values. + """ + self.plugin_counter = collections.Counter() + for key, value in dict_object.iteritems(): + self.plugin_counter[key] = value + + +# Named tuple that defines a parse error. +# +# Attributes: +# name: The parser or plugin name. +# description: The description of the error. +# path_spec: Optional path specification of the file entry (instance of +# dfvfs.PathSpec). The default is None. +ParseError = collections.namedtuple( + 'ParseError', 'name description path_spec') diff --git a/plaso/lib/event_test.py b/plaso/lib/event_test.py new file mode 100644 index 0000000..16e7c95 --- /dev/null +++ b/plaso/lib/event_test.py @@ -0,0 +1,324 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a unit test for the EventObject. + +This is an implementation of an unit test for EventObject storage mechanism for +plaso. + +The test consists of creating six EventObjects. + +Error handling. The following tests are performed for error handling: + + Access attributes that are not set. +""" + +import unittest + +from plaso.events import text_events +from plaso.events import windows_events +from plaso.lib import event +from plaso.lib import timelib_test + + +class TestEvent1(event.EventObject): + """A test event object.""" + DATA_TYPE = 'test:event1' + + def __init__(self, timestamp, attributes): + """Initializes the test event object.""" + super(TestEvent1, self).__init__() + self.timestamp = timestamp + self.timestamp_desc = 'Some time in the future' + for attribute, value in attributes.iteritems(): + setattr(self, attribute, value) + + +class FailEvent(event.EventObject): + """An test event object without the minimal required initialization.""" + + +def GetEventObjects(): + """Returns a list of test event objects.""" + event_objects = [] + hostname = 'MYHOSTNAME' + data_type = 'test:event1' + + event_a = event.EventObject() + event_a.username = 'joesmith' + event_a.filename = 'c:/Users/joesmith/NTUSER.DAT' + event_a.hostname = hostname + event_a.timestamp = 0 + event_a.data_type = data_type + + # TODO: move this to a WindowRegistrysEvent unit test. + timestamp = timelib_test.CopyStringToTimestamp( + '2012-04-20 22:38:46.929596') + event_b = windows_events.WindowsRegistryEvent( + timestamp, u'MY AutoRun key', {u'Run': u'c:/Temp/evil.exe'}) + event_b.hostname = hostname + event_objects.append(event_b) + + timestamp = timelib_test.CopyStringToTimestamp( + '2012-04-20 23:56:46.929596') + event_c = windows_events.WindowsRegistryEvent( + timestamp, u'//HKCU/Secret/EvilEmpire/Malicious_key', + {u'Value': u'send all the exes to the other world'}) + event_c.hostname = hostname + event_objects.append(event_c) + + timestamp = timelib_test.CopyStringToTimestamp( + '2012-04-20 16:44:46.000000') + event_d = windows_events.WindowsRegistryEvent( + timestamp, u'//HKCU/Windows/Normal', + {u'Value': u'run all the benign stuff'}) + event_d.hostname = hostname + event_objects.append(event_d) + + event_objects.append(event_a) + + timestamp = timelib_test.CopyStringToTimestamp( + '2012-04-30 10:29:47.929596') + filename = 'c:/Temp/evil.exe' + event_e = TestEvent1(timestamp, { + 'text': 'This log line reads ohh so much.'}) + event_e.filename = filename + event_e.hostname = hostname + + event_objects.append(event_e) + + timestamp = timelib_test.CopyStringToTimestamp( + '2012-04-30 10:29:47.929596') + event_f = TestEvent1(timestamp, { + 'text': 'Nothing of interest here, move on.'}) + event_f.filename = filename + event_f.hostname = hostname + + event_objects.append(event_f) + + timestamp = timelib_test.CopyStringToTimestamp( + '2012-04-30 13:06:47.939596') + event_g = TestEvent1(timestamp, { + 'text': 'Mr. Evil just logged into the machine and got root.'}) + event_g.filename = filename + event_g.hostname = hostname + + event_objects.append(event_g) + + text_dict = {'body': ( + u'This is a line by someone not reading the log line properly. And ' + u'since this log line exceeds the accepted 80 chars it will be ' + u'shortened.'), 'hostname': u'nomachine', 'username': u'johndoe'} + + # TODO: move this to a TextEvent unit test. + timestamp = timelib_test.CopyStringToTimestamp( + '2012-06-05 22:14:19.000000') + event_h = text_events.TextEvent(timestamp, 12, text_dict) + event_h.text = event_h.body + event_h.hostname = hostname + event_h.filename = filename + + event_objects.append(event_h) + + return event_objects + + +class EventObjectTest(unittest.TestCase): + """Tests for the event object.""" + + def testSameEvent(self): + """Test the EventObject comparison.""" + event_a = event.EventObject() + event_b = event.EventObject() + event_c = event.EventObject() + event_d = event.EventObject() + event_e = event.EventObject() + + event_a.timestamp = 123 + event_a.timestamp_desc = u'LAST WRITTEN' + event_a.data_type = 'mock:nothing' + event_a.inode = 124 + event_a.filename = u'c:/bull/skrytinmappa/skra.txt' + event_a.another_attribute = False + event_a.metadata = { + u'author': u'Some Random Dude', + u'version': 1245L, + u'last_changed': u'Long time ago'} + event_a.strings = [ + u'This ', u'is a ', u'long string'] + + event_b.timestamp = 123 + event_b.timestamp_desc = 'LAST WRITTEN' + event_b.data_type = 'mock:nothing' + event_b.inode = 124 + event_b.filename = 'c:/bull/skrytinmappa/skra.txt' + event_b.another_attribute = False + event_b.metadata = { + 'author': 'Some Random Dude', + 'version': 1245L, + 'last_changed': 'Long time ago'} + event_b.strings = [ + 'This ', 'is a ', 'long string'] + + event_c.timestamp = 123 + event_c.timestamp_desc = 'LAST UPDATED' + event_c.data_type = 'mock:nothing' + event_c.inode = 124 + event_c.filename = 'c:/bull/skrytinmappa/skra.txt' + event_c.another_attribute = False + + event_d.timestamp = 14523 + event_d.timestamp_desc = 'LAST WRITTEN' + event_d.data_type = 'mock:nothing' + event_d.inode = 124 + event_d.filename = 'c:/bull/skrytinmappa/skra.txt' + event_d.another_attribute = False + + event_e.timestamp = 123 + event_e.timestamp_desc = 'LAST WRITTEN' + event_e.data_type = 'mock:nothing' + event_e.inode = 623423 + event_e.filename = 'c:/afrit/onnurskra.txt' + event_e.another_attribute = False + event_e.metadata = { + 'author': 'Some Random Dude', + 'version': 1245, + 'last_changed': 'Long time ago'} + event_e.strings = [ + 'This ', 'is a ', 'long string'] + + self.assertEquals(event_a, event_b) + self.assertNotEquals(event_a, event_c) + self.assertEquals(event_a, event_e) + self.assertNotEquals(event_c, event_d) + + def testEqualityString(self): + """Test the EventObject EqualityString.""" + event_a = event.EventObject() + event_b = event.EventObject() + event_c = event.EventObject() + event_d = event.EventObject() + event_e = event.EventObject() + event_f = event.EventObject() + + event_a.timestamp = 123 + event_a.timestamp_desc = 'LAST WRITTEN' + event_a.data_type = 'mock:nothing' + event_a.inode = 124 + event_a.filename = 'c:/bull/skrytinmappa/skra.txt' + event_a.another_attribute = False + + event_b.timestamp = 123 + event_b.timestamp_desc = 'LAST WRITTEN' + event_b.data_type = 'mock:nothing' + event_b.inode = 124 + event_b.filename = 'c:/bull/skrytinmappa/skra.txt' + event_b.another_attribute = False + + event_c.timestamp = 123 + event_c.timestamp_desc = 'LAST UPDATED' + event_c.data_type = 'mock:nothing' + event_c.inode = 124 + event_c.filename = 'c:/bull/skrytinmappa/skra.txt' + event_c.another_attribute = False + + event_d.timestamp = 14523 + event_d.timestamp_desc = 'LAST WRITTEN' + event_d.data_type = 'mock:nothing' + event_d.inode = 124 + event_d.filename = 'c:/bull/skrytinmappa/skra.txt' + event_d.another_attribute = False + + event_e.timestamp = 123 + event_e.timestamp_desc = 'LAST WRITTEN' + event_e.data_type = 'mock:nothing' + event_e.inode = 623423 + event_e.filename = 'c:/afrit/öñṅûŗ₅ḱŖūα.txt' + event_e.another_attribute = False + + event_f.timestamp = 14523 + event_f.timestamp_desc = 'LAST WRITTEN' + event_f.data_type = 'mock:nothing' + event_f.inode = 124 + event_f.filename = 'c:/bull/skrytinmappa/skra.txt' + event_f.another_attribute = False + event_f.weirdness = 'I am a potato' + + self.assertEquals(event_a.EqualityString(), event_b.EqualityString()) + self.assertNotEquals(event_a.EqualityString(), event_c.EqualityString()) + self.assertEquals(event_a.EqualityString(), event_e.EqualityString()) + self.assertNotEquals(event_c.EqualityString(), event_d.EqualityString()) + self.assertNotEquals(event_d.EqualityString(), event_f.EqualityString()) + + def testEqualityFileStatParserMissingInode(self): + """Test that FileStatParser files with missing inodes are distinct""" + event_a = event.EventObject() + event_b = event.EventObject() + + event_a.timestamp = 123 + event_a.timestamp_desc = 'LAST WRITTEN' + event_a.data_type = 'mock:nothing' + event_a.parser = 'filestat' + event_a.filename = 'c:/bull/skrytinmappa/skra.txt' + event_a.another_attribute = False + + event_b.timestamp = 123 + event_b.timestamp_desc = 'LAST WRITTEN' + event_b.data_type = 'mock:nothing' + event_b.parser = 'filestat' + event_b.filename = 'c:/bull/skrytinmappa/skra.txt' + event_b.another_attribute = False + + self.assertNotEquals(event_a, event_b) + + def testEqualityStringFileStatParserMissingInode(self): + """Test that FileStatParser files with missing inodes are distinct""" + event_a = event.EventObject() + event_b = event.EventObject() + + event_a.timestamp = 123 + event_a.timestamp_desc = 'LAST WRITTEN' + event_a.data_type = 'mock:nothing' + event_a.parser = 'filestat' + event_a.filename = 'c:/bull/skrytinmappa/skra.txt' + event_a.another_attribute = False + + event_b.timestamp = 123 + event_b.timestamp_desc = 'LAST WRITTEN' + event_b.data_type = 'mock:nothing' + event_b.parser = 'filestat' + event_b.filename = 'c:/bull/skrytinmappa/skra.txt' + event_b.another_attribute = False + + self.assertNotEquals(event_a.EqualityString(), event_b.EqualityString()) + + def testNotInEventAndNoParent(self): + """Call to an attribute that does not exist.""" + event_object = TestEvent1(0, {}) + + with self.assertRaises(AttributeError): + getattr(event_object, 'doesnotexist') + + def testFailEvent(self): + """Calls to format_string_short that has not been defined.""" + e = FailEvent() + + with self.assertRaises(AttributeError): + getattr(e, 'format_string_short') + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/lib/eventdata.py b/plaso/lib/eventdata.py new file mode 100644 index 0000000..9a45685 --- /dev/null +++ b/plaso/lib/eventdata.py @@ -0,0 +1,65 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""A place to store information about events, such as format strings, etc.""" + + +# TODO: move this class to events/definitions.py or equiv. +class EventTimestamp(object): + """Class to manage event data.""" + # The timestamp_desc values. + ACCESS_TIME = u'Last Access Time' + CHANGE_TIME = u'Metadata Modification Time' + CREATION_TIME = u'Creation Time' + MODIFICATION_TIME = u'Content Modification Time' + ENTRY_MODIFICATION_TIME = u'Metadata Modification Time' + # Added time and Creation time are considered the same. + ADDED_TIME = u'Creation Time' + # Written time and Modification time are considered the same. + WRITTEN_TIME = u'Content Modification Time' + EXIT_TIME = u'Exit Time' + LAST_RUNTIME = u'Last Time Executed' + DELETED_TIME = u'Content Deletion Time' + + FILE_DOWNLOADED = u'File Downloaded' + PAGE_VISITED = u'Page Visited' + # TODO: change page visited into last visited time. + LAST_VISITED_TIME = u'Last Visited Time' + + LAST_CHECKED_TIME = u'Last Checked Time' + + EXPIRATION_TIME = u'Expiration Time' + START_TIME = u'Start Time' + END_TIME = u'End Time' + + LAST_SHUTDOWN = u'Last Shutdown Time' + + ACCOUNT_CREATED = u'Account Created' + LAST_LOGIN_TIME = u'Last Login Time' + LAST_PASSWORD_RESET = u'Last Password Reset' + + FIRST_CONNECTED = u'First Connection Time' + LAST_CONNECTED = u'Last Connection Time' + + LAST_PRINTED = u'Last Printed Time' + + LAST_RESUME_TIME = u'Last Resume Time' + + # Note that the unknown time is used for date and time values + # of which the exact meaning is unknown and being researched. + # For most cases do not use this timestamp description. + UNKNOWN = u'Unknown Time' diff --git a/plaso/lib/filter_interface.py b/plaso/lib/filter_interface.py new file mode 100644 index 0000000..7047614 --- /dev/null +++ b/plaso/lib/filter_interface.py @@ -0,0 +1,94 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""A definition of the filter interface for filters in plaso.""" + +import abc + +from plaso.lib import errors +from plaso.lib import registry + + +class FilterObject(object): + """The interface that each filter needs to implement in plaso.""" + + __metaclass__ = registry.MetaclassRegistry + __abstract = True + + @property + def filter_name(self): + """Return the name of the filter.""" + return self.__class__.__name__ + + @property + def last_decision(self): + """Return the last matching decision.""" + return getattr(self, '_decision', None) + + @property + def last_reason(self): + """Return the last reason for the match, if there was one.""" + if getattr(self, 'last_decision', False): + return getattr(self, '_reason', '') + + @property + def fields(self): + """Return a list of fields for adaptive output modules.""" + return [] + + @property + def separator(self): + """Return a separator for adaptive output modules.""" + return ',' + + @property + def limit(self): + """Returns the max number of records to return, or zero for all records.""" + return 0 + + @abc.abstractmethod + def CompileFilter(self, unused_filter_string): + """Verify filter string and prepare the filter for later usage. + + This function verifies the filter string matches the definition of + the class and if necessary compiles or prepares the filter so it can start + matching against passed in EventObjects. + + Args: + unused_filter_string: A string passed in that should be recognized by + the filter class. + + Raises: + errors.WrongPlugin: If this filter string does not match the filter + class. + """ + raise errors.WrongPlugin('Not the correct filter for this string.') + + def Match(self, unused_event_object): + """Compare an EventObject to the filter expression and return a boolean. + + This function returns True if the filter should be passed through the filter + and False otherwise. + + Args: + unused_event_object: An event object (instance of EventObject) that + should be evaluated against the filter. + + Returns: + Boolean indicating whether the filter matches the object or not. + """ + return False diff --git a/plaso/lib/lexer.py b/plaso/lib/lexer.py new file mode 100644 index 0000000..d40f624 --- /dev/null +++ b/plaso/lib/lexer.py @@ -0,0 +1,514 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 LL(1) lexer. This lexer is very tolerant of errors and can resync. + +This lexer is originally copied from the GRR project: +https://code.google.com/p/grr +""" + +import logging +import re + + +class Token(object): + """A token action.""" + + def __init__(self, state_regex, regex, actions, next_state, flags=re.I): + """Initializes the token object. + + Args: + + state_regex: If this regular expression matches the current state this + rule is considered. + regex: A regular expression to try and match from the current point. + actions: A command separated list of method names in the Lexer to call. + next_state: The next state we transition to if this Token matches. + flags: re flags. + """ + self.state_regex = re.compile( + state_regex, re.DOTALL | re.M | re.S | re.U | flags) + self.regex = re.compile(regex, re.DOTALL | re.M | re.S | re.U | flags) + self.re_str = regex + self.actions = [] + if actions: + self.actions = actions.split(',') + + self.next_state = next_state + + def Action(self, lexer): + """Method is called when the token matches.""" + + +class Error(Exception): + """Module exception.""" + + +class ParseError(Error): + """A parse error occured.""" + + +class Lexer(object): + """A generic feed lexer.""" + _CONTINUE_STATE = 'CONTINUE' + _INITIAL_STATE = 'INITIAL' + + _ERROR_TOKEN = 'Error' + + # A list of Token() instances. + tokens = [] + + def __init__(self, data=''): + """Initializes the lexer object.""" + super(Lexer, self).__init__() + self.buffer = data + self.error = 0 + self.flags = 0 + self.processed = 0 + self.processed_buffer = '' + self.state = self._INITIAL_STATE + self.state_stack = [] + self.verbose = 0 + + def NextToken(self): + """Fetch the next token by trying to match any of the regexes in order.""" + current_state = self.state + for token in self.tokens: + # Does the rule apply to us? + if not token.state_regex.match(current_state): + continue + + # Try to match the rule + m = token.regex.match(self.buffer) + if not m: + continue + + # The match consumes the data off the buffer (the handler can put it back + # if it likes) + # TODO: using joins might be more efficient here. + self.processed_buffer += self.buffer[:m.end()] + self.buffer = self.buffer[m.end():] + self.processed += m.end() + + next_state = token.next_state + for action in token.actions: + + # Is there a callback to handle this action? + callback = getattr(self, action, self.Default) + + # Allow a callback to skip other callbacks. + try: + possible_next_state = callback(string=m.group(0), match=m) + if possible_next_state == self._CONTINUE_STATE: + continue + # Override the state from the Token + elif possible_next_state: + next_state = possible_next_state + except ParseError as exception: + self.Error(exception) + + # Update the next state + if next_state: + self.state = next_state + + return token + + # Check that we are making progress - if we are too full, we assume we are + # stuck. + self.Error(u'Expected {0:s}'.format(self.state)) + self.processed_buffer += self.buffer[:1] + self.buffer = self.buffer[1:] + return self._ERROR_TOKEN + + def Feed(self, data): + """Feed the buffer with data.""" + self.buffer = ''.join([self.buffer, data]) + + def Empty(self): + """Return a boolean indicating if the buffer is empty.""" + return not self.buffer + + def Default(self, **kwarg): + """The default callback handler.""" + logging.debug(u'Default handler: {0:s}'.format(kwarg)) + + def Error(self, message=None, weight=1): + """Log an error down.""" + logging.debug(u'Error({0:d}): {1:s}'.format(weight, message)) + # Keep a count of errors + self.error += weight + + def PushState(self, **_): + """Push the current state on the state stack.""" + logging.debug(u'Storing state {0:s}'.format(repr(self.state))) + self.state_stack.append(self.state) + + def PopState(self, **_): + """Pop the previous state from the stack.""" + try: + self.state = self.state_stack.pop() + logging.debug(u'Returned state to {0:s}'.format(self.state)) + + return self.state + except IndexError: + self.Error( + u'Tried to pop the state but failed - possible recursion error') + + def PushBack(self, string='', **_): + """Push the match back on the stream.""" + self.buffer = string + self.buffer + self.processed_buffer = self.processed_buffer[:-len(string)] + + def Close(self): + """A convenience function to force us to parse all the data.""" + while self.NextToken(): + if not self.buffer: + return + + +class SelfFeederMixIn(Lexer): + """This mixin is used to make a lexer which feeds itself. + + Note that self.file_object must be the file object we read from. + """ + + # TODO: fix this, file object either needs to be set or not passed here. + def __init__(self, file_object=None): + """Initializes the lexer feeder min object. + + Args: + file_object: Optional file-like object. The default is None. + """ + super(SelfFeederMixIn, self).__init__() + self.file_object = file_object + + def NextToken(self): + """Return the next token.""" + # If we don't have enough data - feed ourselves: We assume + # that we must have at least one sector in our buffer. + if len(self.buffer) < 512: + if self.Feed() == 0 and not self.buffer: + return None + + return Lexer.NextToken(self) + + def Feed(self, size=512): + """Feed data into the buffer.""" + data = self.file_object.read(size) + Lexer.Feed(self, data) + return len(data) + + +class Expression(object): + """A class representing an expression.""" + attribute = None + args = None + operator = None + + # The expected number of args + number_of_args = 1 + + def __init__(self): + """Initializes the expression object.""" + self.args = [] + + def SetAttribute(self, attribute): + """Set the attribute.""" + self.attribute = attribute + + def SetOperator(self, operator): + """Set the operator.""" + self.operator = operator + + def AddArg(self, arg): + """Adds a new arg to this expression. + + Args: + arg: The argument to add (string). + + Returns: + True if this arg is the last arg, False otherwise. + + Raises: + ParseError: If there are too many args. + """ + self.args.append(arg) + if len(self.args) > self.number_of_args: + raise ParseError(u'Too many args for this expression.') + + elif len(self.args) == self.number_of_args: + return True + + return False + + def __str__(self): + """Return a string representation of the expression.""" + return 'Expression: ({0:s}) ({1:s}) {2:s}'.format( + self.attribute, self.operator, self.args) + + # TODO: rename this function to GetTreeAsString or equivalent. + def PrintTree(self, depth=''): + """Print the tree.""" + return u'{0:s} {1:s}'.format(depth, self) + + def Compile(self, unused_filter_implemention): + """Given a filter implementation, compile this expression.""" + raise NotImplementedError( + u'{0:s} does not implement Compile.'.format(self.__class__.__name__)) + + +class BinaryExpression(Expression): + """An expression which takes two other expressions.""" + + def __init__(self, operator='', part=None): + """Initializes the expression object.""" + self.operator = operator + self.args = [] + if part: + self.args.append(part) + super(BinaryExpression, self).__init__() + + def __str__(self): + """Return a string representation of the binary expression.""" + return 'Binary Expression: {0:s} {1:s}'.format( + self.operator, [str(x) for x in self.args]) + + def AddOperands(self, lhs, rhs): + """Add an operant.""" + if isinstance(lhs, Expression) and isinstance(rhs, Expression): + self.args = [lhs, rhs] + else: + raise ParseError(u'Expected expression, got {0:s} {1:s} {2:s}'.format( + lhs, self.operator, rhs)) + + # TODO: rename this function to GetTreeAsString or equivalent. + def PrintTree(self, depth=''): + """Print the tree.""" + result = u'{0:s}{1:s}\n'.format(depth, self.operator) + for part in self.args: + result += u'{0:s}-{1:s}\n'.format(depth, part.PrintTree(depth + ' ')) + + return result + + def Compile(self, filter_implemention): + """Compile the binary expression into a filter object.""" + operator = self.operator.lower() + if operator == 'and' or operator == '&&': + method = 'AndFilter' + elif operator == 'or' or operator == '||': + method = 'OrFilter' + else: + raise ParseError(u'Invalid binary operator {0:s}'.format(operator)) + + args = [x.Compile(filter_implemention) for x in self.args] + return getattr(filter_implemention, method)(*args) + + +class IdentityExpression(Expression): + """An Expression which always evaluates to True.""" + + def Compile(self, filter_implemention): + """Compile the expression.""" + return filter_implemention.IdentityFilter() + + +class SearchParser(Lexer): + """This parser can parse the mini query language and build an AST. + + Examples of valid syntax: + filename contains "foo" and (size > 100k or date before "2011-10") + date between 2011 and 2010 + files older than 1 year + """ + + expression_cls = Expression + binary_expression_cls = BinaryExpression + + tokens = [ + # Double quoted string + Token('STRING', '"', 'PopState,StringFinish', None), + Token('STRING', r'\\(.)', 'StringEscape', None), + Token('STRING', r'[^\\"]+', 'StringInsert', None), + + # Single quoted string + Token('SQ_STRING', '\'', 'PopState,StringFinish', None), + Token('SQ_STRING', r'\\(.)', 'StringEscape', None), + Token('SQ_STRING', r'[^\\\']+', 'StringInsert', None), + + # TODO: Implement a unary not operator. + # The first thing we see in the initial state takes up to the ATTRIBUTE + Token('INITIAL', r'(and|or|\&\&|\|\|)', 'BinaryOperator', None), + Token('INITIAL', r'[^\s\(\)]', 'PushState,PushBack', 'ATTRIBUTE'), + Token('INITIAL', r'\(', 'BracketOpen', None), + Token('INITIAL', r'\)', 'BracketClose', None), + + Token('ATTRIBUTE', r'[\w._0-9]+', 'StoreAttribute', 'OPERATOR'), + Token('OPERATOR', r'[a-z0-9<>=\-\+\!\^\&%]+', 'StoreOperator', + 'ARG_LIST'), + Token('OPERATOR', r'(!=|[<>=])', 'StoreSpecialOperator', 'ARG_LIST'), + Token('ARG_LIST', r'[^\s\'"]+', 'InsertArg', None), + + # Start a string. + Token('.', '"', 'PushState,StringStart', 'STRING'), + Token('.', '\'', 'PushState,StringStart', 'SQ_STRING'), + + # Skip whitespace. + Token('.', r'\s+', None, None), + ] + + def __init__(self, data): + """Initializes the search parser object.""" + # Holds expression + self.current_expression = self.expression_cls() + self.filter_string = data + + # The token stack + self.stack = [] + Lexer.__init__(self, data) + + def BinaryOperator(self, string=None, **_): + """Set the binary operator.""" + self.stack.append(self.binary_expression_cls(string)) + + def BracketOpen(self, **_): + """Define an open bracket.""" + self.stack.append('(') + + def BracketClose(self, **_): + """Close the bracket.""" + self.stack.append(')') + + def StringStart(self, **_): + """Initialize the string.""" + self.string = '' + + def StringEscape(self, string, match, **_): + """Escape backslashes found inside a string quote. + + Backslashes followed by anything other than ['"rnbt] will just be included + in the string. + + Args: + string: The string that matched. + match: The match object (m.group(1) is the escaped code) + """ + if match.group(1) in '\'"rnbt': + self.string += string.decode('string_escape') + else: + self.string += string + + def StringInsert(self, string='', **_): + """Add to the string.""" + self.string += string + + def StringFinish(self, **_): + """Finish the string operation.""" + if self.state == 'ATTRIBUTE': + return self.StoreAttribute(string=self.string) + + elif self.state == 'ARG_LIST': + return self.InsertArg(string=self.string) + + def StoreAttribute(self, string='', **_): + """Store the attribute.""" + logging.debug(u'Storing attribute {0:s}'.format(repr(string))) + + # TODO: Update the expected number_of_args + try: + self.current_expression.SetAttribute(string) + except AttributeError: + raise ParseError(u'Invalid attribute \'{0:s}\''.format(string)) + + return 'OPERATOR' + + def StoreOperator(self, string='', **_): + """Store the operator.""" + logging.debug(u'Storing operator {0:s}'.format(repr(string))) + self.current_expression.SetOperator(string) + + def InsertArg(self, string='', **_): + """Insert an arg to the current expression.""" + logging.debug(u'Storing Argument {0:s}'.format(string)) + + # This expression is complete + if self.current_expression.AddArg(string): + self.stack.append(self.current_expression) + self.current_expression = self.expression_cls() + return self.PopState() + + def _CombineBinaryExpressions(self, operator): + """Combine binary expressions.""" + for i in range(1, len(self.stack)-1): + item = self.stack[i] + if (isinstance(item, BinaryExpression) and item.operator == operator and + isinstance(self.stack[i-1], Expression) and + isinstance(self.stack[i+1], Expression)): + lhs = self.stack[i-1] + rhs = self.stack[i+1] + + self.stack[i].AddOperands(lhs, rhs) + self.stack[i-1] = None + self.stack[i+1] = None + + self.stack = filter(None, self.stack) + + def _CombineParenthesis(self): + """Combine parenthesis.""" + for i in range(len(self.stack)-2): + if (self.stack[i] == '(' and self.stack[i+2] == ')' and + isinstance(self.stack[i+1], Expression)): + self.stack[i] = None + self.stack[i+2] = None + + self.stack = filter(None, self.stack) + + def Reduce(self): + """Reduce the token stack into an AST.""" + # Check for sanity + if self.state != 'INITIAL': + self.Error(u'Premature end of expression') + + length = len(self.stack) + while length > 1: + # Precendence order + self._CombineParenthesis() + self._CombineBinaryExpressions('and') + self._CombineBinaryExpressions('or') + + # No change + if len(self.stack) == length: + break + length = len(self.stack) + + if length != 1: + self.Error(u'Illegal query expression') + + return self.stack[0] + + def Error(self, message=None, unused_weight=1): + """Raise an error message.""" + raise ParseError(u'{0:s} in position {1:s}: {2:s} <----> {3:s} )'.format( + message, len(self.processed_buffer), self.processed_buffer, + self.buffer)) + + def Parse(self): + """Parse.""" + if not self.filter_string: + return IdentityExpression() + + self.Close() + return self.Reduce() diff --git a/plaso/lib/limit.py b/plaso/lib/limit.py new file mode 100644 index 0000000..68b6485 --- /dev/null +++ b/plaso/lib/limit.py @@ -0,0 +1,20 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains few class variables that define various limits.""" + + +MAX_INT64 = 2**64-1 diff --git a/plaso/lib/objectfilter.py b/plaso/lib/objectfilter.py new file mode 100644 index 0000000..b1fdc14 --- /dev/null +++ b/plaso/lib/objectfilter.py @@ -0,0 +1,925 @@ +#!/usr/bin/env python +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Originally copied from the GRR project: +# http://code.google.com/p/grr/source/browse/lib/objectfilter.py +# Copied on 11/15/2012 +# Minor changes made to make it work in plaso. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Classes to perform filtering of objects based on their data members. + +Given a list of objects and a textual filter expression, these classes allow +you to determine which objects match the filter. The system has two main +pieces: A parser for the supported grammar and a filter implementation. + +Given any complying user-supplied grammar, it is parsed with a custom lexer +based on GRR's lexer and then compiled into an actual implementation by using +the filter implementation. A filter implementation simply provides actual +implementations for the primitives required to perform filtering. The compiled +result is always a class supporting the Filter interface. + +If we define a class called Car such as: + + +class Car(object): + def __init__(self, code, color="white", doors=3): + self.code = code + self.color = color + self.doors = 3 + +And we have two instances: + + ford_ka = Car("FORDKA1", color="grey") + toyota_corolla = Car("COROLLA1", color="white", doors=5) + fleet = [ford_ka, toyota_corolla] + +We want to find cars that are grey and have 3 or more doors. We could filter +our fleet like this: + + criteria = "(color is grey) and (doors >= 3)" + parser = ContextFilterParser(criteria).Parse() + compiled_filter = parser.Compile(LowercaseAttributeFilterImp) + + for car in fleet: + if compiled_filter.Matches(car): + print "Car %s matches the supplied filter." % car.code + +The filter expression contains two subexpressions joined by an AND operator: + "color is grey" and "doors >= 3" +This means we want to search for objects matching these two subexpressions. +Let's analyze the first one in depth "color is grey": + + "color": the left operand specifies a search path to look for the data. This + tells our filtering system to look for the color property on passed objects. + "is": the operator. Values retrieved for the "color" property will be checked + against the right operand to see if they are equal. + "grey": the right operand. It specifies an explicit value to check for. + +So each time an object is passed through the filter, it will expand the value +of the color data member, and compare its value against "grey". + +Because data members of objects are often not simple datatypes but other +objects, the system allows you to reference data members within other data +members by separating each by a dot. Let's see an example: + +Let's add a more complex Car class with default tyre data: + + +class CarWithTyres(Car): + def __init__(self, code, tyres=None, color="white", doors=3): + super(self, CarWithTyres).__init__(code, color, doors) + tyres = tyres or Tyre("Pirelli", "PZERO") + + +class Tyre(object): + def __init__(self, brand, code): + self.brand = brand + self.code = code + +And two new instances: + ford_ka = CarWithTyres("FORDKA", color="grey", tyres=Tyre("AVON", "ZT5")) + toyota_corolla = Car("COROLLA1", color="white", doors=5) + fleet = [ford_ka, toyota_corolla] + +To filter a car based on the tyre brand, we would use a search path of +"tyres.brand". + +Because the filter implementation provides the actual classes that perform +handling of the search paths, operators, etc. customizing the behaviour of the +filter is easy. Three basic filter implementations are given: + BaseFilterImplementation: search path expansion is done on attribute names + as provided (case-sensitive). + LowercaseAttributeFilterImp: search path expansion is done on the lowercased + attribute name, so that it only accesses attributes, not methods. + DictFilterImplementation: search path expansion is done on dictionary access + to the given object. So "a.b" expands the object obj to obj["a"]["b"] +""" + +import abc +import binascii +import logging +import re + +from plaso.lib import lexer +from plaso.lib import utils + + +class Error(Exception): + """Base module exception.""" + + +class MalformedQueryError(Error): + """The provided filter query is malformed.""" + + +class ParseError(Error): + """The parser for textual queries returned invalid results.""" + + +class InvalidNumberOfOperands(Error): + """The number of operands provided to this operator is wrong.""" + + +class Filter(object): + """Base class for every filter.""" + + def __init__(self, arguments=None, value_expander=None): + """Constructor. + + Args: + arguments: Arguments to the filter. + value_expander: A callable that will be used to expand values for the + objects passed to this filter. Implementations expanders are provided by + subclassing ValueExpander. + + Raises: + Error: If the given value_expander is not a subclass of ValueExpander + """ + self.value_expander = None + self.value_expander_cls = value_expander + if self.value_expander_cls: + if not issubclass(self.value_expander_cls, ValueExpander): + raise Error(u'{0:s} is not a valid value expander'.format( + self.value_expander_cls)) + self.value_expander = self.value_expander_cls() + self.args = arguments or [] + logging.debug(u'Adding {0:s}'.format(arguments)) + + @abc.abstractmethod + def Matches(self, obj): + """Whether object obj matches this filter.""" + + def Filter(self, objects): + """Returns a list of objects that pass the filter.""" + return filter(self.Matches, objects) + + def __str__(self): + return '{0:s}({1:s})'.format( + self.__class__.__name__, ', '.join([str(arg) for arg in self.args])) + + +class AndFilter(Filter): + """Performs a boolean AND of the given Filter instances as arguments. + + Note that if no conditions are passed, all objects will pass. + """ + def Matches(self, obj): + for child_filter in self.args: + if not child_filter.Matches(obj): + return False + return True + + +class OrFilter(Filter): + """Performs a boolean OR of the given Filter instances as arguments. + + Note that if no conditions are passed, all objects will pass. + """ + def Matches(self, obj): + if not self.args: + return True + + for child_filter in self.args: + if child_filter.Matches(obj): + return True + return False + + +# pylint: disable=abstract-method +class Operator(Filter): + """Base class for all operators.""" + + +class IdentityFilter(Operator): + def Matches(self, _): + return True + + +class UnaryOperator(Operator): + """Base class for unary operators.""" + + def __init__(self, operand, **kwargs): + """Constructor.""" + super(UnaryOperator, self).__init__(arguments=[operand], **kwargs) + if len(self.args) != 1: + raise InvalidNumberOfOperands( + u'Only one operand is accepted by {0:s}. Received {1:d}.'.format( + self.__class__.__name__, len(self.args))) + + +class BinaryOperator(Operator): + """Base class for binary operators. + + The left operand is always a path into the object which will be expanded for + values. The right operand is a value defined at initialization and is stored + at self.right_operand. + """ + def __init__(self, arguments=None, **kwargs): + super(BinaryOperator, self).__init__(arguments=arguments, **kwargs) + if len(self.args) != 2: + raise InvalidNumberOfOperands( + u'Only two operands are accepted by {0:s}. Received {1:s}.'.format( + self.__class__.__name__, len(self.args))) + + self.left_operand = self.args[0] + self.right_operand = self.args[1] + + +class GenericBinaryOperator(BinaryOperator): + """Allows easy implementations of operators.""" + + def __init__(self, **kwargs): + super(GenericBinaryOperator, self).__init__(**kwargs) + self.bool_value = True + + def FlipBool(self): + logging.debug(u'Negative matching.') + self.bool_value = not self.bool_value + + def Operation(self, x, y): + """Performs the operation between two values.""" + + def Operate(self, values): + """Takes a list of values and if at least one matches, returns True.""" + for val in values: + try: + if self.Operation(val, self.right_operand): + return True + else: + continue + except (ValueError, TypeError): + continue + return False + + def Matches(self, obj): + key = self.left_operand + values = self.value_expander.Expand(obj, key) + if values and self.Operate(values): + return self.bool_value + return not self.bool_value + + +class Equals(GenericBinaryOperator): + """Matches objects when the right operand equals the expanded value.""" + + def Operation(self, x, y): + return x == y + + +class NotEquals(Equals): + """Matches when the right operand isn't equal to the expanded value.""" + + def __init__(self, **kwargs): + super(NotEquals, self).__init__(**kwargs) + self.bool_value = False + + +class Less(GenericBinaryOperator): + """Whether the expanded value >= right_operand.""" + + def Operation(self, x, y): + return x < y + + +class LessEqual(GenericBinaryOperator): + """Whether the expanded value <= right_operand.""" + + def Operation(self, x, y): + return x <= y + + +class Greater(GenericBinaryOperator): + """Whether the expanded value > right_operand.""" + + def Operation(self, x, y): + return x > y + + +class GreaterEqual(GenericBinaryOperator): + """Whether the expanded value >= right_operand.""" + + def Operation(self, x, y): + return x >= y + + +class Contains(GenericBinaryOperator): + """Whether the right operand is contained in the value.""" + + def Operation(self, x, y): + if type(x) in (str, unicode): + return y.lower() in x.lower() + + return y in x + + +class InSet(GenericBinaryOperator): + # TODO(user): Change to an N-ary Operator? + """Whether all values are contained within the right operand.""" + + def Operation(self, x, y): + """Whether x is fully contained in y.""" + if x in y: + return True + + # x might be an iterable + # first we need to skip strings or we'll do silly things + if (isinstance(x, basestring) + or isinstance(x, bytes)): + return False + + try: + for value in x: + if value not in y: + return False + return True + except TypeError: + return False + + +class Regexp(GenericBinaryOperator): + """Whether the value matches the regexp in the right operand.""" + + def __init__(self, *children, **kwargs): + super(Regexp, self).__init__(*children, **kwargs) + # Note that right_operand is not necessarily a string. + logging.debug(u'Compiled: {0!s}'.format(self.right_operand)) + try: + self.compiled_re = re.compile( + utils.GetUnicodeString(self.right_operand), re.DOTALL) + except re.error: + raise ValueError(u'Regular expression "{0!s}" is malformed.'.format( + self.right_operand)) + + def Operation(self, x, unused_y): + try: + if self.compiled_re.search(utils.GetUnicodeString(x)): + return True + except TypeError: + pass + + return False + + +class RegexpInsensitive(Regexp): + """Whether the value matches the regexp in the right operand.""" + + def __init__(self, *children, **kwargs): + super(RegexpInsensitive, self).__init__(*children, **kwargs) + # Note that right_operand is not necessarily a string. + logging.debug(u'Compiled: {0!s}'.format(self.right_operand)) + try: + self.compiled_re = re.compile(utils.GetUnicodeString(self.right_operand), + re.I | re.DOTALL) + except re.error: + raise ValueError(u'Regular expression "{0!s}" is malformed.'.format( + self.right_operand)) + + +class Context(Operator): + """Restricts the child operators to a specific context within the object. + + Solves the context problem. The context problem is the following: + Suppose you store a list of loaded DLLs within a process. Suppose that for + each of these DLLs you store the number of imported functions and each of the + imported functions name. + + Imagine that a malicious DLL is injected into processes and its indicators are + that it only imports one function and that it is RegQueryValueEx. You'd write + your indicator like this: + + + AndOperator( + Equal("ImportedDLLs.ImpFunctions.Name", "RegQueryValueEx"), + Equal("ImportedDLLs.NumImpFunctions", "1") + ) + + Now imagine you have these two processes on a given system. + + Process1 + +[0]__ImportedDlls + +[0]__Name: "notevil.dll" + |[0]__ImpFunctions + | +[1]__Name: "CreateFileA" + |[0]__NumImpFunctions: 1 + | + +[1]__Name: "alsonotevil.dll" + |[1]__ImpFunctions + | +[0]__Name: "RegQueryValueEx" + | +[1]__Name: "CreateFileA" + |[1]__NumImpFunctions: 2 + + Process2 + +[0]__ImportedDlls + +[0]__Name: "evil.dll" + |[0]__ImpFunctions + | +[0]__Name: "RegQueryValueEx" + |[0]__NumImpFunctions: 1 + + Both Process1 and Process2 match your query, as each of the indicators are + evaluated separatedly. While you wanted to express "find me processes that + have a DLL that has both one imported function and ReqQueryValueEx is in the + list of imported functions", your indicator actually means "find processes + that have at least a DLL with 1 imported functions and at least one DLL that + imports the ReqQueryValueEx function". + + To write such an indicator you need to specify a context of ImportedDLLs for + these two clauses. Such that you convert your indicator to: + + Context("ImportedDLLs", + AndOperator( + Equal("ImpFunctions.Name", "RegQueryValueEx"), + Equal("NumImpFunctions", "1") + )) + + Context will execute the filter specified as the second parameter for each of + the objects under "ImportedDLLs", thus applying the condition per DLL, not per + object and returning the right result. + """ + + def __init__(self, arguments=None, **kwargs): + if len(arguments) != 2: + raise InvalidNumberOfOperands(u'Context accepts only 2 operands.') + super(Context, self).__init__(arguments=arguments, **kwargs) + self.context, self.condition = self.args + + def Matches(self, obj): + for object_list in self.value_expander.Expand(obj, self.context): + for sub_object in object_list: + if self.condition.Matches(sub_object): + return True + return False + + +OP2FN = { + 'equals': Equals, + 'is': Equals, + '==': Equals, + '!=': NotEquals, + 'contains': Contains, + '>': Greater, + '>=': GreaterEqual, + '<': Less, + '<=': LessEqual, + 'inset': InSet, + 'regexp': Regexp, + 'iregexp': RegexpInsensitive} + + +class ValueExpander(object): + """Encapsulates the logic to expand values available in an object. + + Once instantiated and called, this class returns all the values that follow a + given field path. + """ + + FIELD_SEPARATOR = '.' + + def _GetAttributeName(self, path): + """Returns the attribute name to fetch given a path.""" + return path[0] + + def _GetValue(self, unused_obj, unused_attr_name): + """Returns the value of tha attribute attr_name.""" + raise NotImplementedError() + + def _AtLeaf(self, attr_value): + """Called when at a leaf value. Should yield a value.""" + yield attr_value + + def _AtNonLeaf(self, attr_value, path): + """Called when at a non-leaf value. Should recurse and yield values.""" + try: + # Check first for iterables + # If it's a dictionary, we yield it + if isinstance(attr_value, dict): + yield attr_value + else: + # If it's an iterable, we recurse on each value. + for sub_obj in attr_value: + for value in self.Expand(sub_obj, path[1:]): + yield value + except TypeError: # This is then not iterable, we recurse with the value + for value in self.Expand(attr_value, path[1:]): + yield value + + def Expand(self, obj, path): + """Returns a list of all the values for the given path in the object obj. + + Given a path such as ["sub1", "sub2"] it returns all the values available + in obj.sub1.sub2 as a list. sub1 and sub2 must be data attributes or + properties. + + If sub1 returns a list of objects, or a generator, Expand aggregates the + values for the remaining path for each of the objects, thus returning a + list of all the values under the given path for the input object. + + Args: + obj: An object that will be traversed for the given path + path: A list of strings + + Yields: + The values once the object is traversed. + """ + if isinstance(path, basestring): + path = path.split(self.FIELD_SEPARATOR) + + attr_name = self._GetAttributeName(path) + attr_value = self._GetValue(obj, attr_name) + if attr_value is None: + return + + if len(path) == 1: + for value in self._AtLeaf(attr_value): + yield value + else: + for value in self._AtNonLeaf(attr_value, path): + yield value + + +class AttributeValueExpander(ValueExpander): + """An expander that gives values based on object attribute names.""" + + def _GetValue(self, obj, attr_name): + return getattr(obj, attr_name, None) + + +class LowercaseAttributeValueExpander(AttributeValueExpander): + """An expander that lowercases all attribute names before access.""" + + def _GetAttributeName(self, path): + return path[0].lower() + + +class DictValueExpander(ValueExpander): + """An expander that gets values from dictionary access to the object.""" + + def _GetValue(self, obj, attr_name): + return obj.get(attr_name, None) + + +class BasicExpression(lexer.Expression): + """Basic Expression.""" + + def __init__(self): + super(BasicExpression, self).__init__() + self.bool_value = True + + def FlipBool(self): + self.bool_value = not self.bool_value + + def Compile(self, filter_implementation): + arguments = [self.attribute] + op_str = self.operator.lower() + operator = filter_implementation.OPS.get(op_str, None) + + if not operator: + raise ParseError(u'Unknown operator {0:s} provided.'.format( + self.operator)) + + arguments.extend(self.args) + expander = filter_implementation.FILTERS['ValueExpander'] + ops = operator(arguments=arguments, value_expander=expander) + if not self.bool_value: + if hasattr(ops, 'FlipBool'): + ops.FlipBool() + + return ops + + +class ContextExpression(lexer.Expression): + """Represents the context operator.""" + + def __init__(self, attribute="", part=None): + self.attribute = attribute + self.args = [] + if part: + self.args.append(part) + super(ContextExpression, self).__init__() + + def __str__(self): + return 'Context({0:s} {1:s})'.format( + self.attribute, [str(x) for x in self.args]) + + def SetExpression(self, expression): + """Set the expression.""" + if isinstance(expression, lexer.Expression): + self.args = [expression] + else: + raise ParseError(u'Expected expression, got {0:s}.'.format(expression)) + + def Compile(self, filter_implementation): + """Compile the expression.""" + arguments = [self.attribute] + for arg in self.args: + arguments.append(arg.Compile(filter_implementation)) + expander = filter_implementation.FILTERS['ValueExpander'] + context_cls = filter_implementation.FILTERS['Context'] + return context_cls(arguments=arguments, + value_expander=expander) + + +class BinaryExpression(lexer.BinaryExpression): + def Compile(self, filter_implementation): + """Compile the binary expression into a filter object.""" + operator = self.operator.lower() + if operator == 'and' or operator == '&&': + method = 'AndFilter' + elif operator == 'or' or operator == '||': + method = 'OrFilter' + else: + raise ParseError(u'Invalid binary operator {0:s}.'.format(operator)) + + args = [x.Compile(filter_implementation) for x in self.args] + return filter_implementation.FILTERS[method](arguments=args) + + +class Parser(lexer.SearchParser): + """Parses and generates an AST for a query written in the described language. + + Examples of valid syntax: + size is 40 + (name contains "Program Files" AND hash.md5 is "123abc") + @imported_modules (num_symbols = 14 AND symbol.name is "FindWindow") + """ + expression_cls = BasicExpression + binary_expression_cls = BinaryExpression + context_cls = ContextExpression + + tokens = [ + # Operators and related tokens + lexer.Token('INITIAL', r'\@[\w._0-9]+', + 'ContextOperator,PushState', 'CONTEXTOPEN'), + lexer.Token('INITIAL', r'[^\s\(\)]', 'PushState,PushBack', 'ATTRIBUTE'), + lexer.Token('INITIAL', r'\(', 'PushState,BracketOpen', None), + lexer.Token('INITIAL', r'\)', 'BracketClose', 'BINARY'), + + # Context + lexer.Token('CONTEXTOPEN', r'\(', 'BracketOpen', 'INITIAL'), + + # Double quoted string + lexer.Token('STRING', '"', 'PopState,StringFinish', None), + lexer.Token('STRING', r'\\x(..)', 'HexEscape', None), + lexer.Token('STRING', r'\\(.)', 'StringEscape', None), + lexer.Token('STRING', r'[^\\"]+', 'StringInsert', None), + + # Single quoted string + lexer.Token('SQ_STRING', '\'', 'PopState,StringFinish', None), + lexer.Token('SQ_STRING', r'\\x(..)', 'HexEscape', None), + lexer.Token('SQ_STRING', r'\\(.)', 'StringEscape', None), + lexer.Token('SQ_STRING', r'[^\\\']+', 'StringInsert', None), + + # Basic expression + lexer.Token('ATTRIBUTE', r'[\w._0-9]+', 'StoreAttribute', 'OPERATOR'), + lexer.Token('OPERATOR', r'not ', 'FlipLogic', None), + lexer.Token('OPERATOR', r'(\w+|[<>!=]=?)', 'StoreOperator', 'CHECKNOT'), + lexer.Token('CHECKNOT', r'not', 'FlipLogic', 'ARG'), + lexer.Token('CHECKNOT', r'\s+', None, None), + lexer.Token('CHECKNOT', r'([^not])', 'PushBack', 'ARG'), + lexer.Token('ARG', r'(\d+\.\d+)', 'InsertFloatArg', 'ARG'), + lexer.Token('ARG', r'(0x\d+)', 'InsertInt16Arg', 'ARG'), + lexer.Token('ARG', r'(\d+)', 'InsertIntArg', 'ARG'), + lexer.Token('ARG', '"', 'PushState,StringStart', 'STRING'), + lexer.Token('ARG', '\'', 'PushState,StringStart', 'SQ_STRING'), + # When the last parameter from arg_list has been pushed + + # State where binary operators are supported (AND, OR) + lexer.Token('BINARY', r'(?i)(and|or|\&\&|\|\|)', + 'BinaryOperator', 'INITIAL'), + # - We can also skip spaces + lexer.Token('BINARY', r'\s+', None, None), + # - But if it's not "and" or just spaces we have to go back + lexer.Token('BINARY', '.', 'PushBack,PopState', None), + + # Skip whitespace. + lexer.Token('.', r'\s+', None, None), + ] + + def StoreAttribute(self, string='', **kwargs): + self.flipped = False + super(Parser, self).StoreAttribute(string, **kwargs) + + def FlipAllowed(self): + """Raise an error if the not keyword is used where it is not allowed.""" + if not hasattr(self, 'flipped'): + raise ParseError(u'Not defined.') + + if not self.flipped: + return + + if self.current_expression.operator: + if not self.current_expression.operator.lower() in ( + 'is', 'contains', 'inset', 'equals'): + raise ParseError( + u'Keyword \'not\' does not work against operator: {0:s}'.format( + self.current_expression.operator)) + + def FlipLogic(self, **unused_kwargs): + """Flip the boolean logic of the expression. + + If an expression is configured to return True when the condition + is met this logic will flip that to False, and vice versa. + """ + if hasattr(self, 'flipped') and self.flipped: + raise ParseError(u'The operator \'not\' can only be expressed once.') + + if self.current_expression.args: + raise ParseError( + u'Unable to place the keyword \'not\' after an argument.') + + self.flipped = True + + # Check if this flip operation should be allowed. + self.FlipAllowed() + + if hasattr(self.current_expression, 'FlipBool'): + self.current_expression.FlipBool() + logging.debug(u'Negative matching [flipping boolean logic].') + else: + logging.warning( + u'Unable to perform a negative match, issuing a positive one.') + + def InsertArg(self, string='', **unused_kwargs): + """Insert an arg to the current expression.""" + # Note that "string" is not necessarily of type string. + logging.debug(u'Storing argument: {0!s}'.format(string)) + + # Check if this flip operation should be allowed. + self.FlipAllowed() + + # This expression is complete + if self.current_expression.AddArg(string): + self.stack.append(self.current_expression) + self.current_expression = self.expression_cls() + # We go to the BINARY state, to find if there's an AND or OR operator + return 'BINARY' + + def InsertFloatArg(self, string='', **unused_kwargs): + """Inserts a Float argument.""" + try: + float_value = float(string) + except (TypeError, ValueError): + raise ParseError(u'{0:s} is not a valid float.'.format(string)) + return self.InsertArg(float_value) + + def InsertIntArg(self, string='', **unused_kwargs): + """Inserts an Integer argument.""" + try: + int_value = int(string) + except (TypeError, ValueError): + raise ParseError(u'{0:s} is not a valid integer.'.format(string)) + return self.InsertArg(int_value) + + def InsertInt16Arg(self, string='', **unused_kwargs): + """Inserts an Integer in base16 argument.""" + try: + int_value = int(string, 16) + except (TypeError, ValueError): + raise ParseError(u'{0:s} is not a valid base16 integer.'.format(string)) + return self.InsertArg(int_value) + + def StringFinish(self, **unused_kwargs): + if self.state == 'ATTRIBUTE': + return self.StoreAttribute(string=self.string) + + elif self.state == 'ARG': + return self.InsertArg(string=self.string) + + def StringEscape(self, string, match, **unused_kwargs): + """Escape backslashes found inside a string quote. + + Backslashes followed by anything other than [\'"rnbt.ws] will raise + an Error. + + Args: + string: The string that matched. + match: The match object (m.group(1) is the escaped code) + + Raises: + ParseError: When the escaped string is not one of [\'"rnbt] + """ + if match.group(1) in '\\\'"rnbt\\.ws': + self.string += string.decode('string_escape') + else: + raise ParseError(u'Invalid escape character {0:s}.'.format(string)) + + def HexEscape(self, string, match, **unused_kwargs): + """Converts a hex escaped string.""" + logging.debug(u'HexEscape matched {0:s}.'.format(string)) + hex_string = match.group(1) + try: + self.string += binascii.unhexlify(hex_string) + except TypeError: + raise ParseError(u'Invalid hex escape {0:s}.'.format(string)) + + def ContextOperator(self, string='', **unused_kwargs): + self.stack.append(self.context_cls(string[1:])) + + def Reduce(self): + """Reduce the token stack into an AST.""" + # Check for sanity + if self.state != 'INITIAL' and self.state != 'BINARY': + self.Error(u'Premature end of expression') + + length = len(self.stack) + while length > 1: + # Precendence order + self._CombineParenthesis() + self._CombineBinaryExpressions('and') + self._CombineBinaryExpressions('or') + self._CombineContext() + + # No change + if len(self.stack) == length: + break + length = len(self.stack) + + if length != 1: + self.Error(u'Illegal query expression.') + + return self.stack[0] + + def Error(self, message=None, _=None): + # Note that none of the values necessarily are strings. + raise ParseError(u'{0!s} in position {1!s}: {2!s} <----> {3!s} )'.format( + message, len(self.processed_buffer), self.processed_buffer, + self.buffer)) + + def _CombineBinaryExpressions(self, operator): + for i in range(1, len(self.stack)-1): + item = self.stack[i] + if (isinstance(item, lexer.BinaryExpression) and + item.operator.lower() == operator.lower() and + isinstance(self.stack[i-1], lexer.Expression) and + isinstance(self.stack[i+1], lexer.Expression)): + lhs = self.stack[i-1] + rhs = self.stack[i+1] + + self.stack[i].AddOperands(lhs, rhs) + self.stack[i-1] = None + self.stack[i+1] = None + + self.stack = filter(None, self.stack) + + def _CombineContext(self): + # Context can merge from item 0 + for i in range(len(self.stack)-1, 0, -1): + item = self.stack[i-1] + if (isinstance(item, ContextExpression) and + isinstance(self.stack[i], lexer.Expression)): + expression = self.stack[i] + self.stack[i-1].SetExpression(expression) + self.stack[i] = None + + self.stack = filter(None, self.stack) + + +### FILTER IMPLEMENTATIONS +class BaseFilterImplementation(object): + """Defines the base implementation of an object filter by its attributes. + + Inherit from this class, switch any of the needed operators and pass it to + the Compile method of a parsed string to obtain an executable filter. + """ + + OPS = OP2FN + FILTERS = { + 'ValueExpander': AttributeValueExpander, + 'AndFilter': AndFilter, + 'OrFilter': OrFilter, + 'IdentityFilter': IdentityFilter, + 'Context': Context} + + +class LowercaseAttributeFilterImplementation(BaseFilterImplementation): + """Does field name access on the lowercase version of names. + + Useful to only access attributes and properties with Google's python naming + style. + """ + + FILTERS = {} + FILTERS.update(BaseFilterImplementation.FILTERS) + FILTERS.update({'ValueExpander': LowercaseAttributeValueExpander}) + + +class DictFilterImplementation(BaseFilterImplementation): + """Does value fetching by dictionary access on the object.""" + + FILTERS = {} + FILTERS.update(BaseFilterImplementation.FILTERS) + FILTERS.update({'ValueExpander': DictValueExpander}) + + diff --git a/plaso/lib/objectfilter_test.py b/plaso/lib/objectfilter_test.py new file mode 100644 index 0000000..a4c3761 --- /dev/null +++ b/plaso/lib/objectfilter_test.py @@ -0,0 +1,519 @@ +#!/usr/bin/env python +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains tests for the object filter.""" + +import unittest + +from plaso.lib import objectfilter + + +class DummyObject(object): + def __init__(self, key, value): + setattr(self, key, value) + + +class HashObject(object): + def __init__(self, hash_value=None): + self.value = hash_value + + @property + def md5(self): + return self.value + + def __eq__(self, y): + return self.value == y + + def __lt__(self, y): + return self.value < y + + +class Dll(object): + def __init__(self, name, imported_functions=None, exported_functions=None): + self.name = name + self._imported_functions = imported_functions or [] + self.num_imported_functions = len(self._imported_functions) + self.exported_functions = exported_functions or [] + self.num_exported_functions = len(self.exported_functions) + + @property + def imported_functions(self): + for fn in self._imported_functions: + yield fn + + +class DummyFile(object): + _FILENAME = 'boot.ini' + + ATTR1 = 'Backup' + ATTR2 = 'Archive' + HASH1 = '123abc' + HASH2 = '456def' + + non_callable_leaf = 'yoda' + + def __init__(self): + self.non_callable = HashObject(self.HASH1) + self.non_callable_repeated = [ + DummyObject('desmond', ['brotha', 'brotha']), + DummyObject('desmond', ['brotha', 'sista'])] + self.imported_dll1 = Dll('a.dll', ['FindWindow', 'CreateFileA']) + self.imported_dll2 = Dll('b.dll', ['RegQueryValueEx']) + + @property + def name(self): + return self._FILENAME + + @property + def attributes(self): + return [self.ATTR1, self.ATTR2] + + @property + def hash(self): + return [HashObject(self.HASH1), HashObject(self.HASH2)] + + @property + def size(self): + return 10 + + @property + def deferred_values(self): + for v in ['a', 'b']: + yield v + + @property + def novalues(self): + return [] + + @property + def imported_dlls(self): + return [self.imported_dll1, self.imported_dll2] + + def Callable(self): + raise RuntimeError(u'This can not be called.') + + @property + def float(self): + return 123.9823 + + +class ObjectFilterTest(unittest.TestCase): + def setUp(self): + self.file = DummyFile() + self.filter_imp = objectfilter.LowercaseAttributeFilterImplementation + self.value_expander = self.filter_imp.FILTERS['ValueExpander'] + + operator_tests = { + objectfilter.Less: [ + (True, ['size', 1000]), + (True, ['size', 11]), + (False, ['size', 10]), + (False, ['size', 0]), + (False, ['float', 1.0]), + (True, ['float', 123.9824])], + objectfilter.LessEqual: [ + (True, ['size', 1000]), + (True, ['size', 11]), + (True, ['size', 10]), + (False, ['size', 9]), + (False, ['float', 1.0]), + (True, ['float', 123.9823])], + objectfilter.Greater: [ + (True, ['size', 1]), + (True, ['size', 9.23]), + (False, ['size', 10]), + (False, ['size', 1000]), + (True, ['float', 122]), + (True, ['float', 1.0])], + objectfilter.GreaterEqual: [ + (False, ['size', 1000]), + (False, ['size', 11]), + (True, ['size', 10]), + (True, ['size', 0]), + # Floats work fine too. + (True, ['float', 122]), + (True, ['float', 123.9823]), + # Comparisons works with strings, although it might be a bit silly. + (True, ['name', 'aoot.ini'])], + objectfilter.Contains: [ + # Contains works with strings. + (True, ['name', 'boot.ini']), + (True, ['name', 'boot']), + (False, ['name', 'meh']), + # Works with generators. + (True, ['imported_dlls.imported_functions', 'FindWindow']), + # But not with numbers. + (False, ['size', 12])], + objectfilter.Equals: [ + (True, ['name', 'boot.ini']), + (False, ['name', 'foobar']), + (True, ['float', 123.9823])], + objectfilter.NotEquals: [ + (False, ['name', 'boot.ini']), + (True, ['name', 'foobar']), + (True, ['float', 25])], + objectfilter.InSet: [ + (True, ['name', ['boot.ini', 'autoexec.bat']]), + (True, ['name', 'boot.ini']), + (False, ['name', 'NOPE']), + # All values of attributes are within these. + (True, ['attributes', ['Archive', 'Backup', 'Nonexisting']]), + # Not all values of attributes are within these. + (False, ['attributes', ['Executable', 'Sparse']])], + objectfilter.Regexp: [ + (True, ['name', '^boot.ini$']), + (True, ['name', 'boot.ini']), + (False, ['name', '^$']), + (True, ['attributes', 'Archive']), + # One can regexp numbers if he's inclined to. + (True, ['size', 0]), + # But regexp doesn't work with lists or generators for the moment. + (False, ['imported_dlls.imported_functions', 'FindWindow'])], + } + + def testBinaryOperators(self): + for operator, test_data in self.operator_tests.items(): + for test_unit in test_data: + # TODO: why is there a print statement here? + print (u'Testing {0:s} with {1!s} and {2!s}'.format( + operator, test_unit[0], test_unit[1])) + kwargs = {'arguments': test_unit[1], + 'value_expander': self.value_expander} + ops = operator(**kwargs) + self.assertEqual(test_unit[0], ops.Matches(self.file)) + if hasattr(ops, 'FlipBool'): + ops.FlipBool() + # TODO: why is there a print statement here? + print u'Testing negative matching.' + self.assertEqual(not test_unit[0], ops.Matches(self.file)) + + def testExpand(self): + # Case insensitivity. + values_lowercase = self.value_expander().Expand(self.file, 'size') + values_uppercase = self.value_expander().Expand(self.file, 'Size') + self.assertListEqual(list(values_lowercase), list(values_uppercase)) + + # Existing, non-repeated, leaf is a value. + values = self.value_expander().Expand(self.file, 'size') + self.assertListEqual(list(values), [10]) + + # Existing, non-repeated, leaf is iterable. + values = self.value_expander().Expand(self.file, 'attributes') + self.assertListEqual(list(values), [[DummyFile.ATTR1, DummyFile.ATTR2]]) + + # Existing, repeated, leaf is value. + values = self.value_expander().Expand(self.file, 'hash.md5') + self.assertListEqual(list(values), [DummyFile.HASH1, DummyFile.HASH2]) + + # Existing, repeated, leaf is iterable. + values = self.value_expander().Expand( + self.file, 'non_callable_repeated.desmond') + self.assertListEqual( + list(values), [['brotha', 'brotha'], ['brotha', 'sista']]) + + # Now with an iterator. + values = self.value_expander().Expand(self.file, 'deferred_values') + self.assertListEqual([list(value) for value in values], [['a', 'b']]) + + # Iterator > generator. + values = self.value_expander().Expand( + self.file, 'imported_dlls.imported_functions') + expected = [['FindWindow', 'CreateFileA'], ['RegQueryValueEx']] + self.assertListEqual([list(value) for value in values], expected) + + # Non-existing first path. + values = self.value_expander().Expand(self.file, 'nonexistant') + self.assertListEqual(list(values), []) + + # Non-existing in the middle. + values = self.value_expander().Expand(self.file, 'hash.mink.boo') + self.assertListEqual(list(values), []) + + # Non-existing as a leaf. + values = self.value_expander().Expand(self.file, 'hash.mink') + self.assertListEqual(list(values), []) + + # Non-callable leaf. + values = self.value_expander().Expand(self.file, 'non_callable_leaf') + self.assertListEqual(list(values), [DummyFile.non_callable_leaf]) + + # callable. + values = self.value_expander().Expand(self.file, 'Callable') + self.assertListEqual(list(values), []) + + # leaf under a callable. Will return nothing. + values = self.value_expander().Expand(self.file, 'Callable.a') + self.assertListEqual(list(values), []) + + def testGenericBinaryOperator(self): + class TestBinaryOperator(objectfilter.GenericBinaryOperator): + values = list() + + def Operation(self, x, _): + return self.values.append(x) + + # Test a common binary operator. + tbo = TestBinaryOperator( + arguments=['whatever', 0], value_expander=self.value_expander) + self.assertEqual(tbo.right_operand, 0) + self.assertEqual(tbo.args[0], 'whatever') + tbo.Matches(DummyObject('whatever', 'id')) + tbo.Matches(DummyObject('whatever', 'id2')) + tbo.Matches(DummyObject('whatever', 'bg')) + tbo.Matches(DummyObject('whatever', 'bg2')) + self.assertListEqual(tbo.values, ['id', 'id2', 'bg', 'bg2']) + + def testContext(self): + self.assertRaises( + objectfilter.InvalidNumberOfOperands, objectfilter.Context, + arguments=['context'], value_expander=self.value_expander) + + self.assertRaises( + objectfilter.InvalidNumberOfOperands, objectfilter.Context, + arguments=[ + 'context', objectfilter.Equals( + arguments=['path', 'value'], + value_expander=self.value_expander), + objectfilter.Equals( + arguments=['another_path', 'value'], + value_expander=self.value_expander)], + value_expander=self.value_expander) + + # One imported_dll imports 2 functions AND one imported_dll imports + # function RegQueryValueEx. + arguments = [ + objectfilter.Equals( + arguments=['imported_dlls.num_imported_functions', 1], + value_expander=self.value_expander), + objectfilter.Contains( + arguments=['imported_dlls.imported_functions', + 'RegQueryValueEx'], + value_expander=self.value_expander)] + condition = objectfilter.AndFilter(arguments=arguments) + # Without context, it matches because both filters match separately. + self.assertEqual(True, condition.Matches(self.file)) + + arguments = [ + objectfilter.Equals( + arguments=['num_imported_functions', 2], + value_expander=self.value_expander), + objectfilter.Contains( + arguments=['imported_functions', 'RegQueryValueEx'], + value_expander=self.value_expander)] + condition = objectfilter.AndFilter(arguments=arguments) + # The same DLL imports 2 functions AND one of these is RegQueryValueEx. + context = objectfilter.Context(arguments=['imported_dlls', condition], + value_expander=self.value_expander) + # With context, it doesn't match because both don't match in the same dll. + self.assertEqual(False, context.Matches(self.file)) + + # One imported_dll imports only 1 function AND one imported_dll imports + # function RegQueryValueEx. + condition = objectfilter.AndFilter(arguments=[ + objectfilter.Equals( + arguments=['num_imported_functions', 1], + value_expander=self.value_expander), + objectfilter.Contains( + arguments=['imported_functions', 'RegQueryValueEx'], + value_expander=self.value_expander)]) + # The same DLL imports 1 function AND it's RegQueryValueEx. + context = objectfilter.Context(['imported_dlls', condition], + value_expander=self.value_expander) + self.assertEqual(True, context.Matches(self.file)) + + # Now test the context with a straight query. + query = u'\n'.join([ + '@imported_dlls', + '(', + ' imported_functions contains "RegQueryValueEx"', + ' AND num_imported_functions == 1', + ')']) + + filter_ = objectfilter.Parser(query).Parse() + filter_ = filter_.Compile(self.filter_imp) + self.assertEqual(True, filter_.Matches(self.file)) + + def testRegexpRaises(self): + with self.assertRaises(ValueError): + objectfilter.Regexp( + arguments=['name', 'I [dont compile'], + value_expander=self.value_expander) + + def testEscaping(self): + parser = objectfilter.Parser(r'a is "\n"').Parse() + self.assertEqual(parser.args[0], '\n') + # Invalid escape sequence. + parser = objectfilter.Parser(r'a is "\z"') + with self.assertRaises(objectfilter.ParseError): + parser.Parse() + + # Can escape the backslash. + parser = objectfilter.Parser(r'a is "\\"').Parse() + self.assertEqual(parser.args[0], '\\') + + # Test hexadecimal escaping. + + # This fails as it's not really a hex escaped string. + parser = objectfilter.Parser(r'a is "\xJZ"') + with self.assertRaises(objectfilter.ParseError): + parser.Parse() + + # Instead, this is what one should write. + parser = objectfilter.Parser(r'a is "\\xJZ"').Parse() + self.assertEqual(parser.args[0], r'\xJZ') + # Standard hex-escape. + parser = objectfilter.Parser(r'a is "\x41\x41\x41"').Parse() + self.assertEqual(parser.args[0], 'AAA') + # Hex-escape + a character. + parser = objectfilter.Parser(r'a is "\x414"').Parse() + self.assertEqual(parser.args[0], r'A4') + # How to include r'\x41'. + parser = objectfilter.Parser(r'a is "\\x41"').Parse() + self.assertEqual(parser.args[0], r'\x41') + + def testParse(self): + # Arguments are either int, float or quoted string. + objectfilter.Parser('attribute == 1').Parse() + objectfilter.Parser('attribute == 0x10').Parse() + parser = objectfilter.Parser('attribute == 1a') + with self.assertRaises(objectfilter.ParseError): + parser.Parse() + + objectfilter.Parser('attribute == 1.2').Parse() + objectfilter.Parser('attribute == \'bla\'').Parse() + objectfilter.Parser('attribute == "bla"').Parse() + parser = objectfilter.Parser('something == red') + self.assertRaises(objectfilter.ParseError, parser.Parse) + + # Can't start with AND. + parser = objectfilter.Parser('and something is \'Blue\'') + with self.assertRaises(objectfilter.ParseError): + parser.Parse() + + # Test negative filters. + parser = objectfilter.Parser('attribute not == \'dancer\'') + with self.assertRaises(objectfilter.ParseError): + parser.Parse() + + parser = objectfilter.Parser('attribute == not \'dancer\'') + with self.assertRaises(objectfilter.ParseError): + parser.Parse() + + parser = objectfilter.Parser('attribute not not equals \'dancer\'') + with self.assertRaises(objectfilter.ParseError): + parser.Parse() + + parser = objectfilter.Parser('attribute not > 23') + with self.assertRaises(objectfilter.ParseError): + parser.Parse() + + # Need to close braces. + objectfilter.Parser('(a is 3)').Parse() + parser = objectfilter.Parser('(a is 3') + self.assertRaises(objectfilter.ParseError, parser.Parse) + # Need to open braces to close them. + parser = objectfilter.Parser('a is 3)') + self.assertRaises(objectfilter.ParseError, parser.Parse) + + # Context Operator alone is not accepted. + parser = objectfilter.Parser('@attributes') + with self.assertRaises(objectfilter.ParseError): + parser.Parse() + + # Accepted only with braces. + objectfilter.Parser('@attributes( name is \'adrien\')').Parse() + # Not without them. + parser = objectfilter.Parser('@attributes name is \'adrien\'') + with self.assertRaises(objectfilter.ParseError): + parser.Parse() + + # Can nest context operators. + query = '@imported_dlls( @imported_function( name is \'OpenFileA\'))' + objectfilter.Parser(query).Parse() + # Can nest context operators and mix braces without it messing up. + query = '@imported_dlls( @imported_function( name is \'OpenFileA\'))' + parser = objectfilter.Parser(query).Parse() + query = u'\n'.join([ + '@imported_dlls', + '(', + ' @imported_function', + ' (', + ' name is "OpenFileA" and ordinal == 12', + ' )', + ')']) + + parser = objectfilter.Parser(query).Parse() + # Mix context and binary operators. + query = u'\n'.join([ + '@imported_dlls', + '(', + ' @imported_function', + ' (', + ' name is "OpenFileA"', + ' ) AND num_functions == 2', + ')']) + + parser = objectfilter.Parser(query).Parse() + # Also on the right. + query = u'\n'.join([ + '@imported_dlls', + '(', + ' num_functions == 2 AND', + ' @imported_function', + ' (', + ' name is "OpenFileA"', + ' )', + ')']) + + # Altogether. + # There's an imported dll that imports OpenFileA AND + # an imported DLL matching advapi32.dll that imports RegQueryValueExA AND + # and it exports a symbol called 'inject'. + query = u'\n'.join([ + '@imported_dlls( @imported_function ( name is "OpenFileA" ) )', + 'AND', + '@imported_dlls (', + ' name regexp "(?i)advapi32.dll"', + ' AND @imported_function ( name is "RegQueryValueEx" )', + ')', + 'AND @exported_symbols(name is "inject")']) + + def testCompile(self): + obj = DummyObject('something', 'Blue') + parser = objectfilter.Parser('something == \'Blue\'').Parse() + filter_ = parser.Compile(self.filter_imp) + self.assertEqual(filter_.Matches(obj), True) + parser = objectfilter.Parser('something == \'Red\'').Parse() + filter_ = parser.Compile(self.filter_imp) + self.assertEqual(filter_.Matches(obj), False) + parser = objectfilter.Parser('something == "Red"').Parse() + filter_ = parser.Compile(self.filter_imp) + self.assertEqual(filter_.Matches(obj), False) + obj = DummyObject('size', 4) + parser = objectfilter.Parser('size < 3').Parse() + filter_ = parser.Compile(self.filter_imp) + self.assertEqual(filter_.Matches(obj), False) + parser = objectfilter.Parser('size == 4').Parse() + filter_ = parser.Compile(self.filter_imp) + self.assertEqual(filter_.Matches(obj), True) + query = 'something is \'Blue\' and size not contains 3' + parser = objectfilter.Parser(query).Parse() + filter_ = parser.Compile(self.filter_imp) + self.assertEqual(filter_.Matches(obj), False) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/lib/output.py b/plaso/lib/output.py new file mode 100644 index 0000000..6782299 --- /dev/null +++ b/plaso/lib/output.py @@ -0,0 +1,394 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the interface for output parsing of plaso. + +The default output or storage mechanism of Plaso is not in a human +readable format. There needs to be a way to define the output in such +a way. + +After the timeline is collected and stored another tool can read, filter, +sort and process the output inside the storage, and send each processed +entry to an output formatter that takes care of parsing the output into +a human readable format for easy human consumption/analysis. + +""" + +import abc +import logging +import sys + +from plaso.lib import errors +from plaso.lib import registry +from plaso.lib import utils + +import pytz + + +class LogOutputFormatter(object): + """A base class for formatting output produced by plaso. + + This class exists mostly for documentation purposes. Subclasses should + override the relevant methods to act on the callbacks. + """ + + __metaclass__ = registry.MetaclassRegistry + __abstract = True + + # Optional arguments to be added to the argument parser. + # An example would be: + # ARGUMENTS = [('--myparameter', { + # 'action': 'store', + # 'help': 'This is my parameter help', + # 'dest': 'myparameter', + # 'default': '', + # 'type': 'unicode'})] + # + # Where all arguments into the dict object have a direct translation + # into the argparse parser. + ARGUMENTS = [] + + def __init__(self, store, filehandle=sys.stdout, config=None, + filter_use=None): + """Constructor for the output module. + + Args: + store: A StorageFile object that defines the storage. + filehandle: A file-like object that can be written to. + config: The configuration object, containing config information. + filter_use: A filter_interface.FilterObject object. + """ + zone = getattr(config, 'timezone', 'UTC') + try: + self.zone = pytz.timezone(zone) + except pytz.UnknownTimeZoneError: + logging.warning(u'Unkown timezone: {0:s} defaulting to: UTC'.format( + zone)) + self.zone = pytz.utc + + self.filehandle = filehandle + self.store = store + self._filter = filter_use + self._config = config + + self.encoding = getattr(config, 'preferred_encoding', 'utf-8') + + # TODO: this function seems to be only called with the default arguments, + # so refactor this function away. + def FetchEntry(self, store_number=-1, store_index=-1): + """Fetches an entry from the storage. + + Fetches the next entry in the storage file, except if location + is explicitly indicated. + + Args: + store_number: The store number if explicit location is to be read. + store_index: The index into the store, if explicit location is to be + read. + + Returns: + An EventObject, either the next one or from a specific location. + """ + if store_number > 0: + return self.store.GetEventObject(store_number, store_index) + else: + return self.store.GetSortedEntry() + + def WriteEvent(self, evt): + """Write the output of a single entry to the output filehandle. + + This method takes care of actually outputting each event in + question. It does so by first prepending it with potential + start of event, then processes the main body before appending + a potential end of event. + + Args: + evt: An EventObject, defined in the event library. + """ + self.StartEvent() + self.EventBody(evt) + self.EndEvent() + + @abc.abstractmethod + def EventBody(self, evt): + """Writes the main body of an event to the output filehandle. + + Args: + evt: An EventObject, defined in the event library. + + Raises: + NotImplementedError: When not implemented. + """ + + def StartEvent(self): + """This should be extended by specific implementations. + + This method does all preprocessing or output before each event + is printed, for instance to surround XML events with tags, etc. + """ + pass + + def EndEvent(self): + """This should be extended by specific implementations. + + This method does all the post-processing or output after + each event has been printed, such as closing XML tags, etc. + """ + pass + + def Start(self): + """This should be extended by specific implementations. + + Depending on the file format of the output it may need + a header. This method should return a header if one is + defined in that output format. + """ + pass + + def End(self): + """This should be extended by specific implementations. + + Depending on the file format of the output it may need + a footer. This method should return a footer if one is + defined in that output format. + """ + pass + + +# Need to suppress this since these classes do not implement the +# abstract method EventBody, classes that inherit from one of these +# classes need to implement that function. +# pylint: disable=abstract-method +class FileLogOutputFormatter(LogOutputFormatter): + """A simple file based output formatter.""" + + __abstract = True + + def __init__(self, store, filehandle=sys.stdout, config=None, + filter_use=None): + """Set up the formatter.""" + super(FileLogOutputFormatter, self).__init__( + store, filehandle, config, filter_use) + if isinstance(filehandle, basestring): + open_filehandle = open(filehandle, 'wb') + elif hasattr(filehandle, 'write'): + open_filehandle = filehandle + else: + raise IOError( + u'Unable to determine how to use filehandle passed in: {}'.format( + type(filehandle))) + + self.filehandle = OutputFilehandle(self.encoding) + self.filehandle.Open(open_filehandle) + + def End(self): + """Close the open filehandle after the last output.""" + super(FileLogOutputFormatter, self).End() + self.filehandle.Close() + + +class EventBuffer(object): + """Buffer class for EventObject output processing.""" + + MERGE_ATTRIBUTES = ['inode', 'filename', 'display_name'] + + def __init__(self, formatter, check_dedups=True): + """Initialize the EventBuffer. + + This class is used for buffering up events for duplicate removals + and for other post-processing/analysis of events before being presented + by the appropriate output module. + + Args: + formatter: An OutputFormatter object. + check_dedups: Boolean value indicating whether or not the buffer should + check and merge duplicate entries or not. + """ + self._buffer_dict = {} + self._current_timestamp = 0 + self.duplicate_counter = 0 + self.check_dedups = check_dedups + + self.formatter = formatter + self.formatter.Start() + + def Append(self, event_object): + """Append an EventObject into the processing pipeline. + + Args: + event_object: The EventObject that is being added. + """ + if not self.check_dedups: + self.formatter.WriteEvent(event_object) + return + + if event_object.timestamp != self._current_timestamp: + self._current_timestamp = event_object.timestamp + self.Flush() + + key = event_object.EqualityString() + if key in self._buffer_dict: + self.JoinEvents(event_object, self._buffer_dict.pop(key)) + self._buffer_dict[key] = event_object + + def Flush(self): + """Flushes the buffer by sending records to a formatter and prints.""" + if not self._buffer_dict: + return + + for event_object in self._buffer_dict.values(): + try: + self.formatter.WriteEvent(event_object) + except errors.WrongFormatter as exception: + logging.error(u'Unable to write event: {:s}'.format(exception)) + + self._buffer_dict = {} + + def JoinEvents(self, event_a, event_b): + """Join this EventObject with another one.""" + self.duplicate_counter += 1 + # TODO: Currently we are using the first event pathspec, perhaps that + # is not the best approach. There is no need to have all the pathspecs + # inside the combined event, however which one should be chosen is + # perhaps something that can be evaluated here (regular TSK in favor of + # an event stored deep inside a VSS for instance). + for attr in self.MERGE_ATTRIBUTES: + val_a = set(utils.GetUnicodeString(getattr(event_a, attr, '')).split(';')) + val_b = set(utils.GetUnicodeString(getattr(event_b, attr, '')).split(';')) + values_list = list(val_a | val_b) + values_list.sort() # keeping this consistent across runs helps with diffs + setattr(event_a, attr, u';'.join(values_list)) + + # Special instance if this is a filestat entry we need to combine the + # description field. + if getattr(event_a, 'parser', u'') == 'filestat': + description_a = set(getattr(event_a, 'timestamp_desc', u'').split(';')) + description_b = set(getattr(event_b, 'timestamp_desc', u'').split(';')) + descriptions = list(description_a | description_b) + descriptions.sort() + if event_b.timestamp_desc not in event_a.timestamp_desc: + setattr(event_a, 'timestamp_desc', u';'.join(descriptions)) + + def End(self): + """Call the formatter to produce the closing line.""" + self.Flush() + + if self.formatter: + self.formatter.End() + + def __exit__(self, unused_type, unused_value, unused_traceback): + """Make usable with "with" statement.""" + self.End() + + def __enter__(self): + """Make usable with "with" statement.""" + return self + + +class OutputFilehandle(object): + """A simple wrapper for filehandles to make character encoding easier. + + All data is stored as an unicode text internally. However there are some + issues with clients that try to output unicode text to a non-unicode terminal. + Therefore a wrapper is created that checks if we are writing to a file, thus + using the default unicode encoding or if the attempt is to write to the + terminal, for which the default encoding of that terminal is used to encode + the text (if possible). + """ + + DEFAULT_ENCODING = 'utf-8' + + def __init__(self, encoding='utf-8'): + """Initialize the output file handler. + + Args: + encoding: The default terminal encoding, only used if attempted to write + to the terminal. + """ + self._filehandle = None + self._encoding = encoding + # An attribute stating whether or not this is STDOUT. + self._standard_out = False + + def Open(self, filehandle=sys.stdout, path=''): + """Open a filehandle to an output file. + + Args: + filehandle: A file-like-object that is used to write data to. + path: If a file like object is not passed in it is possible + to pass in a path to a file, and a file-like-objec will be created. + """ + if path: + self._filehandle = open(path, 'wb') + else: + self._filehandle = filehandle + + if not hasattr(self._filehandle, 'name'): + self._standard_out = True + elif self._filehandle.name.startswith(''): + self._standard_out = True + + def WriteLine(self, line): + """Write a single line to the supplied filehandle.""" + if not self._filehandle: + return + + if self._standard_out: + # Write using preferred user encoding. + try: + self._filehandle.write(line.encode(self._encoding)) + except UnicodeEncodeError: + logging.error( + u'Unable to properly write logline, save output to a file to ' + u'prevent missing data.') + self._filehandle.write(line.encode(self._encoding, 'ignore')) + + else: + # Write to a file, use unicode. + self._filehandle.write(line.encode(self.DEFAULT_ENCODING)) + + def Close(self): + """Close the filehandle, if applicable.""" + if self._filehandle and not self._standard_out: + self._filehandle.close() + + def __exit__(self, unused_type, unused_value, unused_traceback): + """Make usable with "with" statement.""" + self.Close() + + def __enter__(self): + """Make usable with "with" statement.""" + return self + + +def GetOutputFormatter(output_string): + """Return an output formatter that matches the provided string.""" + # Format the output string (make the input case in-sensitive). + if type(output_string) not in (str, unicode): + return None + + format_str = ''.join( + [output_string[0].upper(), output_string[1:].lower()]) + return LogOutputFormatter.classes.get(format_str, None) + + +def ListOutputFormatters(): + """Generate a list of all available output formatters.""" + for cl in LogOutputFormatter.classes: + formatter_class = LogOutputFormatter.classes[cl](None) + doc_string, _, _ = formatter_class.__doc__.partition('\n') + yield cl, doc_string diff --git a/plaso/lib/output_test.py b/plaso/lib/output_test.py new file mode 100644 index 0000000..b944d72 --- /dev/null +++ b/plaso/lib/output_test.py @@ -0,0 +1,193 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains tests for the output formatter.""" +import os +import locale +import sys +import tempfile +import unittest + +from plaso.lib import output + + +class DummyEvent(object): + """Simple class that defines a dummy event.""" + + def __init__(self, timestamp, entry): + self.date = u'03/01/2012' + try: + self.timestamp = int(timestamp) + except ValueError: + self.timestamp = 0 + self.entry = entry + def EqualityString(self): + return u';'.join(map(str, [self.timestamp, self.entry])) + + +class TestOutput(output.LogOutputFormatter): + """This is a test output module that provides a simple XML.""" + + def __init__(self, filehandle): + """Fake the store.""" + super(TestOutput, self).__init__(store=None, filehandle=filehandle) + + def StartEvent(self): + self.filehandle.write(u'\n') + + def EventBody(self, event_object): + self.filehandle.write(( + u'\t{0:s}\n\t\n' + u'\t{2:s}\n').format( + event_object.date, event_object.timestamp, event_object.entry)) + + def EndEvent(self): + self.filehandle.write(u'\n') + + def FetchEntry(self, **_): + pass + + def Start(self): + self.filehandle.write(u'\n') + + def End(self): + self.filehandle.write(u'\n') + + +class PlasoOutputUnitTest(unittest.TestCase): + """The unit test for plaso output formatting.""" + + def testOutput(self): + """Test a test implementation of the output formatter.""" + events = [DummyEvent(123456, u'My Event Is Now!'), + DummyEvent(123458, u'There is no tomorrow.'), + DummyEvent(123462, u'Tomorrow is now.'), + DummyEvent(123489, u'This is just some stuff to fill the line.')] + + lines = [] + with tempfile.NamedTemporaryFile() as fh: + formatter = TestOutput(fh) + formatter.Start() + for event_object in events: + formatter.WriteEvent(event_object) + formatter.End() + + fh.seek(0) + for line in fh: + lines.append(line) + + self.assertEquals(len(lines), 22) + self.assertEquals(lines[0], u'\n') + self.assertEquals(lines[1], u'\n') + self.assertEquals(lines[2], u'\t03/01/2012\n') + self.assertEquals(lines[3], u'\t\n') + self.assertEquals(lines[4], u'\tMy Event Is Now!\n') + self.assertEquals(lines[5], u'\n') + self.assertEquals(lines[6], u'\n') + self.assertEquals(lines[7], u'\t03/01/2012\n') + self.assertEquals(lines[8], u'\t\n') + self.assertEquals(lines[9], u'\tThere is no tomorrow.\n') + self.assertEquals(lines[10], u'\n') + self.assertEquals(lines[11], u'\n') + self.assertEquals(lines[-1], u'\n') + + def testOutputList(self): + """Test listing up all available registered modules.""" + module_seen = False + for name, description in output.ListOutputFormatters(): + if 'TestOutput' in name: + module_seen = True + self.assertEquals(description, ( + u'This is a test output module that provides a simple XML.')) + + self.assertTrue(module_seen) + + +class EventBufferTest(unittest.TestCase): + """Few unit tests for the EventBuffer class.""" + + def testFlush(self): + """Test to ensure we empty our buffers and sends to output properly.""" + with tempfile.NamedTemporaryFile() as fh: + + def CheckBufferLength(event_buffer, expected): + if not event_buffer.check_dedups: + expected = 0 + # pylint: disable=protected-access + self.assertEquals(len(event_buffer._buffer_dict), expected) + + formatter = TestOutput(fh) + event_buffer = output.EventBuffer(formatter, False) + + event_buffer.Append(DummyEvent(123456, u'Now is now')) + CheckBufferLength(event_buffer, 1) + + # Add three events. + event_buffer.Append(DummyEvent(123456, u'OMG I AM DIFFERENT')) + event_buffer.Append(DummyEvent(123456, u'Now is now')) + event_buffer.Append(DummyEvent(123456, u'Now is now')) + CheckBufferLength(event_buffer, 2) + + event_buffer.Flush() + CheckBufferLength(event_buffer, 0) + + event_buffer.Append(DummyEvent(123456, u'Now is now')) + event_buffer.Append(DummyEvent(123456, u'Now is now')) + event_buffer.Append(DummyEvent(123456, u'Different again :)')) + CheckBufferLength(event_buffer, 2) + event_buffer.Append(DummyEvent(123457, u'Now is different')) + CheckBufferLength(event_buffer, 1) + + +class OutputFilehandleTest(unittest.TestCase): + """Few unit tests for the OutputFilehandle.""" + + def setUp(self): + self.preferred_encoding = locale.getpreferredencoding() + + def _GetLine(self): + # Time, Þorri allra landsmanna hlýddu á atburðinn. + return ('Time, \xc3\x9eorri allra landsmanna hl\xc3\xbdddu \xc3\xa1 ' + 'atbur\xc3\xb0inn.\n').decode('utf-8') + + def testFilePath(self): + temp_path = '' + with tempfile.NamedTemporaryFile(delete=True) as temp_file: + temp_path = temp_file.name + + with output.OutputFilehandle(self.preferred_encoding) as fh: + fh.Open(path=temp_path) + fh.WriteLine(self._GetLine()) + + line_read = u'' + with open(temp_path, 'rb') as output_file: + line_read = output_file.read() + + os.remove(temp_path) + self.assertEquals(line_read, self._GetLine().encode('utf-8')) + + def testStdOut(self): + with output.OutputFilehandle(self.preferred_encoding) as fh: + fh.Open(sys.stdout) + try: + fh.WriteLine(self._GetLine()) + self.assertTrue(True) + except (UnicodeEncodeError, UnicodeDecodeError): + self.assertTrue(False) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/lib/pfilter.py b/plaso/lib/pfilter.py new file mode 100644 index 0000000..736e749 --- /dev/null +++ b/plaso/lib/pfilter.py @@ -0,0 +1,455 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 extension of the objectfilter to provide plaso specific options.""" + +import datetime +import logging + +from plaso.formatters import manager as formatters_manager + +# TODO: Changes this so it becomes an attribute instead of having backend +# load a front-end library. +from plaso.frontend import presets + +from plaso.lib import limit +from plaso.lib import objectfilter +from plaso.lib import timelib +from plaso.lib import utils + + +class DictObject(object): + """A simple object representing a dict object. + + To filter against an object that is stored as a dictionary the dict + is converted into a simple object. Since keys can contain spaces + and/or other symbols they are stripped out to make filtering work + like it is another object. + + Example dict: + {'A value': 234, + 'this (my) key_': 'value', + 'random': True, + } + + This object would then allow access to object.thismykey that would access + the key 'this (my) key_' inside the dict. + """ + + def __init__(self, dict_object): + """Initialize the object and build a secondary dict.""" + # TODO: Move some of this code to a more value typed system. + self._dict_object = dict_object + + self._dict_translated = {} + for key, value in dict_object.items(): + self._dict_translated[self._StripKey(key)] = value + + def _StripKey(self, key): + """Return a stripped version of the dict key without symbols.""" + try: + return str(key).lower().translate(None, ' (){}+_=-<>[]') + except UnicodeEncodeError: + pass + + def __getattr__(self, attr): + """Return back entries from the dictionary.""" + if attr in self._dict_object: + return self._dict_object.get(attr) + + # Special case of getting all the key/value pairs. + if attr == '__all__': + ret = [] + for key, value in self._dict_translated.items(): + ret.append(u'{}:{}'.format(key, value)) + return u' '.join(ret) + + test = self._StripKey(attr) + if test in self._dict_translated: + return self._dict_translated.get(test) + + +class PlasoValueExpander(objectfilter.AttributeValueExpander): + """An expander that gives values based on object attribute names.""" + + def __init__(self): + """Initialize an attribue value expander.""" + super(PlasoValueExpander, self).__init__() + self._formatters_manager = formatters_manager.EventFormatterManager + + def _GetMessage(self, obj): + """Return a properly formatted message string.""" + ret = u'' + + try: + ret, _ = self._formatters_manager.GetMessageStrings(obj) + except KeyError as exception: + logging.warning(u'Unable to correctly assemble event: {0:s}'.format( + exception)) + + return ret + + def _GetSources(self, obj): + """Return a properly formatted source strings.""" + try: + source_short, source_long = self._formatters_manager.GetSourceStrings(obj) + except KeyError as exception: + logging.warning(u'Unable to correctly assemble event: {0:s}'.format( + exception)) + + return source_short, source_long + + def _GetValue(self, obj, attr_name): + ret = getattr(obj, attr_name, None) + + if ret: + if isinstance(ret, dict): + ret = DictObject(ret) + + if attr_name == 'tag': + return ret.tags + + return ret + + # Check if this is a message request and we have a regular EventObject. + if attr_name == 'message': + return self._GetMessage(obj) + + # Check if this is a source_short request. + if attr_name in ('source', 'source_short'): + source_short, _ = self._GetSources(obj) + return source_short + + # Check if this is a source_long request. + if attr_name in ('source_long', 'sourcetype'): + _, source_long = self._GetSources(obj) + return source_long + + def _GetAttributeName(self, path): + return path[0].lower() + + +class PlasoExpression(objectfilter.BasicExpression): + """A Plaso specific expression.""" + # A simple dictionary used to swap attributes so other names can be used + # to reference some core attributes (implementation specific). + swap_source = { + 'date': 'timestamp', + 'datetime': 'timestamp', + 'time': 'timestamp', + 'description_long': 'message', + 'description': 'message', + 'description_short': 'message_short', + } + + def Compile(self, filter_implementation): + self.attribute = self.swap_source.get(self.attribute, self.attribute) + arguments = [self.attribute] + op_str = self.operator.lower() + operator = filter_implementation.OPS.get(op_str, None) + + if not operator: + raise objectfilter.ParseError(u'Unknown operator {0:s} provided.'.format( + self.operator)) + + # Plaso specific implementation - if we are comparing a timestamp + # to a value, we use our specific implementation that compares + # timestamps in a "human readable" format. + if self.attribute == 'timestamp': + args = [] + for arg in self.args: + args.append(DateCompareObject(arg)) + self.args = args + + for arg in self.args: + if isinstance(arg, DateCompareObject): + if 'Less' in str(operator): + TimeRangeCache.SetUpperTimestamp(arg.data) + else: + TimeRangeCache.SetLowerTimestamp(arg.data) + arguments.extend(self.args) + expander = filter_implementation.FILTERS['ValueExpander'] + ops = operator(arguments=arguments, value_expander=expander) + if not self.bool_value: + if hasattr(ops, 'FlipBool'): + ops.FlipBool() + + return ops + + +class ParserList(objectfilter.GenericBinaryOperator): + """Matches when a parser is inside a predefined list of parsers.""" + + def __init__(self, *children, **kwargs): + """Construct the parser list and retrieve a list of available parsers.""" + super(ParserList, self).__init__(*children, **kwargs) + self.compiled_list = presets.categories.get( + self.right_operand.lower(), []) + + def Operation(self, x, unused_y): + """Return a bool depending on the parser list contains the parser.""" + if self.left_operand != 'parser': + raise objectfilter.MalformedQueryError( + u'Unable to use keyword "inlist" for other than parser.') + + if x in self.compiled_list: + return True + + return False + + +class PlasoAttributeFilterImplementation(objectfilter.BaseFilterImplementation): + """Does field name access on the lowercase version of names. + + Useful to only access attributes and properties with Google's python naming + style. + """ + + FILTERS = {} + FILTERS.update(objectfilter.BaseFilterImplementation.FILTERS) + FILTERS.update({'ValueExpander': PlasoValueExpander}) + OPS = objectfilter.OP2FN + OPS.update({'inlist': ParserList,}) + + +class DateCompareObject(object): + """A specific class created for date comparison. + + This object takes a date representation, whether that is a direct integer + datetime object or a string presenting the date, and uses that for comparing + against timestamps stored in microseconds in in microseconds since + Jan 1, 1970 00:00:00 UTC. + + This makes it possible to use regular comparison operators for date, + irrelevant of the format the date comes in, since plaso stores all timestamps + in the same format, which is an integer/long, it is a simple manner of + changing the input into the same format (int) and compare that. + """ + + def __init__(self, data): + """Take a date object and use that for comparison. + + Args: + data: A string, datetime object or an integer that + represents the time to compare against. Time should be stored + as microseconds since UTC in Epoch format. + + Raises: + ValueError: if the date string is invalid. + """ + self.text = utils.GetUnicodeString(data) + if type(data) in (int, long): + self.data = data + elif type(data) == float: + self.data = long(data) + elif type(data) in (str, unicode): + try: + self.data = timelib.Timestamp.FromTimeString( + utils.GetUnicodeString(data)) + except ValueError as exception: + raise ValueError(u'Wrongly formatted date string: {0:s} - {1:s}'.format( + data, exception)) + elif type(data) == datetime.datetime: + self.data = timelib.Timestamp.FromPythonDatetime(data) + elif isinstance(DateCompareObject, data): + self.data = data.data + else: + raise ValueError(u'Unsupported type: {0:s}.'.format(type(data))) + + def __cmp__(self, x): + """A simple comparison operation.""" + try: + x_date = DateCompareObject(x) + return cmp(self.data, x_date.data) + except ValueError: + return False + + def __le__(self, x): + """Less or equal comparison.""" + return self.data <= x + + def __ge__(self, x): + """Greater or equal comparison.""" + return self.data >= x + + def __eq__(self, x): + """Check if equal.""" + return x == self.data + + def __ne__(self, x): + """Check if not equal.""" + return x != self.data + + def __str__(self): + """Return a string representation of the object.""" + return self.text + + +class BaseParser(objectfilter.Parser): + """Plaso version of the Parser.""" + + expression_cls = PlasoExpression + + +class TrueObject(object): + """A simple object that always returns true for all comparison. + + This object is used for testing certain conditions inside filter queries. + By returning true for all comparisons this object can be used to evaluate + specific portions of a filter query. + """ + + def __init__(self, txt=''): + """Save the text object so it can be used when comparing text.""" + self.txt = txt + + def __getattr__(self, unused_attr): + """Return a TrueObject for every attribute request.""" + return self + + def __eq__(self, unused_x): + """Return true for tests of equality.""" + return True + + def __gt__(self, unused_x): + """Return true for checks for greater.""" + return True + + def __ge__(self, unused_x): + """Return true for checks for greater or equal.""" + return True + + def __lt__(self, unused_x): + """Return true for checks of less.""" + return True + + def __le__(self, unused_x): + """Return true for checks of less or equal.""" + return True + + def __ne__(self, unused_x): + """Return true for all not equal comparisons.""" + return True + + def __iter__(self): + """Return a generator so a test for the in keyword can be used.""" + yield self + + def __str__(self): + """Return a string to make regular expression searches possible. + + Returns: + A string that containes the original query with some of the matches + expanded, perhaps several times. + """ + # Regular expressions in pfilter may include the following escapes: + # "\\'\"rnbt\.ws": + txt = self.txt + if r'\.' in self.txt: + txt += self.txt.replace(r'\.', ' _ text _ ') + + if r'\b' in self.txt: + txt += self.txt.replace(r'\b', ' ') + + if r'\s' in self.txt: + txt += self.txt.replace(r'\s', ' ') + + return txt + + +class MockTestFilter(object): + """A mock test filter object used to test certain portion of test queries. + + The logic behind this object is that a single attribute can be isolated + for comparison. That is to say all calls to attributes will lead to a TRUE + response, except those attributes that are specifically stated in the + constructor. This way it is simple to test for instance whether or not + to include a parser at all, before actually running the tool. The same applies + to filtering out certain filenames, etc. + """ + + def __init__(self, query, **kwargs): + """Constructor, only valid attribute is the parser one.""" + self.attributes = kwargs + self.txt = query + + def __getattr__(self, attr): + """Return TrueObject for all requests except for stored attributes.""" + if attr in self.attributes: + return self.attributes.get(attr, None) + + # TODO: Either delete this entire object (MockTestFilter) or implement + # a false object and return the correct one depending on whether we + # are looking for a true or negative response (eg "not" keyword included). + return TrueObject(self.txt) + + +class TimeRangeCache(object): + """A class that stores timeranges from filters.""" + + @classmethod + def ResetTimeConstraints(cls): + """Resets the time constraints.""" + if hasattr(cls, '_lower'): + del cls._lower + if hasattr(cls, '_upper'): + del cls._upper + + @classmethod + def SetLowerTimestamp(cls, timestamp): + """Sets the lower bound timestamp.""" + if not hasattr(cls, '_lower'): + cls._lower = timestamp + return + + if timestamp < cls._lower: + cls._lower = timestamp + + @classmethod + def SetUpperTimestamp(cls, timestamp): + """Sets the upper bound timestamp.""" + if not hasattr(cls, '_upper'): + cls._upper = timestamp + return + + if timestamp > cls._upper: + cls._upper = timestamp + + @classmethod + def GetTimeRange(cls): + """Return the first and last timestamp of filter range.""" + first = getattr(cls, '_lower', 0) + last = getattr(cls, '_upper', limit.MAX_INT64) + + if first < last: + return first, last + else: + return last, first + + +def GetMatcher(query, quiet=False): + """Return a filter match object for a given query.""" + matcher = None + try: + parser = BaseParser(query).Parse() + matcher = parser.Compile(PlasoAttributeFilterImplementation) + except objectfilter.ParseError as exception: + if not quiet: + logging.error(u'Filter <{0:s}> malformed: {1:s}'.format( + query, exception)) + + return matcher diff --git a/plaso/lib/pfilter_test.py b/plaso/lib/pfilter_test.py new file mode 100644 index 0000000..0d83996 --- /dev/null +++ b/plaso/lib/pfilter_test.py @@ -0,0 +1,238 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the filters.""" + +import unittest + +from plaso.formatters import interface as formatters_interface +from plaso.lib import event +from plaso.lib import objectfilter +from plaso.lib import pfilter +from plaso.lib import timelib_test +from plaso.parsers import interface as parsers_interface + +import pytz + + +class Empty(object): + """An empty object.""" + + +class PfilterFakeFormatter(formatters_interface.EventFormatter): + """A formatter for this fake class.""" + DATA_TYPE = 'Weirdo:Made up Source:Last Written' + + FORMAT_STRING = '{text}' + FORMAT_STRING_SHORT = '{text_short}' + + SOURCE_LONG = 'Fake Parsing Source' + SOURCE_SHORT = 'REG' + + +class PfilterFakeParser(parsers_interface.BaseParser): + """A fake parser that does not parse anything, but registers.""" + + NAME = 'pfilter_fake_parser' + + DATA_TYPE = 'Weirdo:Made up Source:Last Written' + + def Parse(self, unused_parser_context, unused_file_entry): + """Extract data from a fake plist file for testing. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + + Yields: + An event object (instance of EventObject) that contains the parsed + attributes. + """ + event_object = event.EventObject() + event_object.timestamp = timelib_test.CopyStringToTimestamp( + '2015-11-18 01:15:43') + event_object.timestamp_desc = 'Last Written' + event_object.text_short = 'This description is different than the long one.' + event_object.text = ( + u'User did a very bad thing, bad, bad thing that awoke Dr. Evil.') + event_object.filename = ( + u'/My Documents/goodfella/Documents/Hideout/myfile.txt') + event_object.hostname = 'Agrabah' + event_object.parser = 'Weirdo' + event_object.inode = 1245 + event_object.display_name = u'unknown:{0:s}'.format(event_object.filename) + event_object.data_type = self.DATA_TYPE + + yield event_object + + +class PfilterAnotherParser(PfilterFakeParser): + """Another fake parser that does nothing but register as a parser.""" + + NAME = 'pfilter_another_fake' + + DATA_TYPE = 'Weirdo:AnotherFakeSource' + + +class PfilterAnotherFakeFormatter(PfilterFakeFormatter): + """Formatter for the AnotherParser event.""" + + DATA_TYPE = 'Weirdo:AnotherFakeSource' + SOURCE_LONG = 'Another Fake Source' + + +class PfilterAllEvilParser(PfilterFakeParser): + """A class that does nothing but has a fancy name.""" + + NAME = 'pfilter_evil_fake_parser' + + DATA_TYPE = 'Weirdo:AllEvil' + + +class PfilterEvilFormatter(PfilterFakeFormatter): + """Formatter for the AllEvilParser.""" + + DATA_TYPE = 'Weirdo:AllEvil' + SOURCE_LONG = 'A Truly Evil' + + +class PFilterTest(unittest.TestCase): + """Simple plaso specific tests to the pfilter implementation.""" + + def setUp(self): + """Set up the necessary variables used in tests.""" + self._pre = Empty() + self._pre.zone = pytz.UTC + + def testPlasoEvents(self): + """Test plaso EventObjects, both Python and Protobuf version. + + These are more plaso specific tests than the more generic + objectfilter ones. It will create an EventObject that stores + some attributes. These objects will then be serialzed into an + EventObject protobuf and all tests run against both the native + Python object as well as the protobuf. + """ + event_object = event.EventObject() + event_object.data_type = 'Weirdo:Made up Source:Last Written' + event_object.timestamp = timelib_test.CopyStringToTimestamp( + '2015-11-18 01:15:43') + event_object.timestamp_desc = 'Last Written' + event_object.text_short = 'This description is different than the long one.' + event_object.text = ( + u'User did a very bad thing, bad, bad thing that awoke Dr. Evil.') + event_object.filename = ( + u'/My Documents/goodfella/Documents/Hideout/myfile.txt') + event_object.hostname = 'Agrabah' + event_object.parser = 'Weirdo' + event_object.inode = 1245 + event_object.mydict = { + 'value': 134, 'another': 'value', 'A Key (with stuff)': 'Here'} + event_object.display_name = u'unknown:{0:s}'.format(event_object.filename) + + # Series of tests. + query = 'filename contains \'GoodFella\'' + self.RunPlasoTest(event_object, query, True) + + # Double negative matching -> should be the same + # as a positive one. + query = 'filename not not contains \'GoodFella\'' + my_parser = pfilter.BaseParser(query) + self.assertRaises( + objectfilter.ParseError, + my_parser.Parse) + + # Test date filtering. + query = 'date >= \'2015-11-18\'' + self.RunPlasoTest(event_object, query, True) + + query = 'date < \'2015-11-19\'' + self.RunPlasoTest(event_object, query, True) + + # 2015-11-18T01:15:43 + query = ( + 'date < \'2015-11-18T01:15:44.341\' and date > \'2015-11-18 01:15:42\'') + self.RunPlasoTest(event_object, query, True) + + query = 'date > \'2015-11-19\'' + self.RunPlasoTest(event_object, query, False) + + # Perform few attribute tests. + query = 'filename not contains \'sometext\'' + self.RunPlasoTest(event_object, query, True) + + query = ( + 'timestamp_desc CONTAINS \'written\' AND date > \'2015-11-18\' AND ' + 'date < \'2015-11-25 12:56:21\' AND (source_short contains \'LOG\' or ' + 'source_short CONTAINS \'REG\')') + self.RunPlasoTest(event_object, query, True) + + query = 'parser is not \'Made\'' + self.RunPlasoTest(event_object, query, True) + + query = 'parser is not \'Weirdo\'' + self.RunPlasoTest(event_object, query, False) + + query = 'mydict.value is 123' + self.RunPlasoTest(event_object, query, False) + + query = 'mydict.akeywithstuff contains "ere"' + self.RunPlasoTest(event_object, query, True) + + query = 'mydict.value is 134' + self.RunPlasoTest(event_object, query, True) + + query = 'mydict.value < 200' + self.RunPlasoTest(event_object, query, True) + + query = 'mydict.another contains "val"' + self.RunPlasoTest(event_object, query, True) + + query = 'mydict.notthere is 123' + self.RunPlasoTest(event_object, query, False) + + query = 'source_long not contains \'Fake\'' + self.RunPlasoTest(event_object, query, False) + + query = 'source is \'REG\'' + self.RunPlasoTest(event_object, query, True) + + query = 'source is not \'FILE\'' + self.RunPlasoTest(event_object, query, True) + + # Multiple attributes. + query = ( + 'source_long is \'Fake Parsing Source\' AND description_long ' + 'regexp \'bad, bad thing [\\sa-zA-Z\\.]+ evil\'') + self.RunPlasoTest(event_object, query, False) + + query = ( + 'source_long is \'Fake Parsing Source\' AND text iregexp ' + '\'bad, bad thing [\\sa-zA-Z\\.]+ evil\'') + self.RunPlasoTest(event_object, query, True) + + def RunPlasoTest(self, obj, query, result): + """Run a simple test against an event object.""" + my_parser = pfilter.BaseParser(query).Parse() + matcher = my_parser.Compile( + pfilter.PlasoAttributeFilterImplementation) + + self.assertEqual(result, matcher.Matches(obj)) + + +if __name__ == "__main__": + unittest.main() diff --git a/plaso/lib/proxy.py b/plaso/lib/proxy.py new file mode 100644 index 0000000..a40f034 --- /dev/null +++ b/plaso/lib/proxy.py @@ -0,0 +1,130 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a proxy object that can be used to provide RPC access.""" + +import abc + + +def GetProxyPortNumberFromPID(process_id): + """Simple mechanism to set the port number based on a PID value. + + Args: + process_id: An integer, process ID (PID), value that should be used to find + a port number. + + Returns: + An integer indicating a possible port number for the process to listen on. + """ + # TODO: Improve this method of selecting ports. + # This is in now way a perfect algorightm for choosing port numbers (what if + # it is already assigned?, etc) + if process_id < 1024: + return process_id + 1024 + + if process_id > 65535: + # Return the remainder of highest port number, sent back to the + # function itself, since this number could be lower than 1024. + return GetProxyPortNumberFromPID(process_id % 65535) + + return process_id + + +class ProxyServer(object): + """An interface defining functions needed for a proxy object.""" + + def __init__(self, port=0): + """Initialize the proxy object. + + Args: + port: An integer indicating the port number the proxy listens to. + This is optional and defaults to port zero. + """ + super(ProxyServer, self).__init__() + self._port_number = port + + def __enter__(self): + """Make usable with "with" statement.""" + return self + + def __exit__(self, unused_type, unused_value, unused_traceback): + """Make usable with "with" statement.""" + self.Close() + + @property + def listening_port(self): + """Returns back the port the proxy listens to.""" + return self._port_number + + @abc.abstractmethod + def Close(self): + """Close the proxy server.""" + + @abc.abstractmethod + def Open(self): + """Sets up the necessary objects in order for the proxy to be started.""" + + @abc.abstractmethod + def RegisterFunction(self, function_name, function): + """Register a function for this proxy. + + Args: + function_name: The name of the registered proxy function. + function: The callback for the function providing the answer. + """ + + @abc.abstractmethod + def StartProxy(self): + """Start the proxy. + + This usually involves setting up the proxy to bind to an address and + listen to requests. + """ + + @abc.abstractmethod + def SetListeningPort(self, new_port_number): + """Change the port the proxy listens to.""" + + +class ProxyClient(object): + """An interface defining functions needed to implement a proxy client.""" + + def __init__(self, port=0): + """Initialize the proxy client. + + Args: + port: An integer indicating the port number the proxy connects to. + This is optional and defaults to port zero. + """ + super(ProxyClient, self).__init__() + self._port_number = port + + @abc.abstractmethod + def Open(self): + """Sets up the necessary objects in order for the proxy to be started.""" + + @abc.abstractmethod + def GetData(self, call_back_name): + """Return data extracted from a RPC callback. + + Args: + call_back_name: The name of the call back function or attribute registered + in the RPC service. + + Returns: + The data returned by the RPC server. + """ diff --git a/plaso/lib/putils.py b/plaso/lib/putils.py new file mode 100644 index 0000000..618f30d --- /dev/null +++ b/plaso/lib/putils.py @@ -0,0 +1,58 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains few methods for Plaso.""" + +import logging + +from plaso.lib import output + + +# TODO: Refactor the putils library so it does not end up being a trash can +# for all things core/front-end. We don't want this to be end up being a +# collection for all methods that have no other home. +class Options(object): + """A simple configuration object.""" + + +def _FindClasses(class_object, *args): + """Find all registered classes. + + A method to find all registered classes of a particular + class. + + Args: + class_object: The parent class. + + Returns: + A list of registered classes of that class. + """ + results = [] + for cls in class_object.classes: + try: + results.append(class_object.classes[cls](*args)) + except Exception: + logging.error( + u'_FindClasses: exception while appending: {0:s}'.format(cls)) + raise + + return results + + +def FindAllOutputs(): + """Find all available output modules.""" + return _FindClasses(output.LogOutputFormatter, None) diff --git a/plaso/lib/registry.py b/plaso/lib/registry.py new file mode 100644 index 0000000..0e60b85 --- /dev/null +++ b/plaso/lib/registry.py @@ -0,0 +1,80 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a class registration system for plugins.""" + +import abc + + +class MetaclassRegistry(abc.ABCMeta): + """Automatic Plugin Registration through metaclasses.""" + + def __init__(cls, name, bases, env_dict): + """Initialize a metaclass. + + Args: + name: The interface class name. + bases: A tuple of base names. + env_dict: The namespace of the object. + + Raises: + KeyError: If a classes given name is already registered, to make sure + no two classes that inherit from the same interface can have + the same name attribute. + """ + abc.ABCMeta.__init__(cls, name, bases, env_dict) + + # Register the name of the immediate parent class. + if bases: + cls.parent_class_name = getattr(bases[0], 'NAME', bases[0]) + cls.parent_class = bases[0] + + # Attach the classes dict to the baseclass and have all derived classes + # use the same one: + for base in bases: + try: + cls.classes = base.classes + cls.plugin_feature = base.plugin_feature + cls.top_level_class = base.top_level_class + break + except AttributeError: + cls.classes = {} + cls.plugin_feature = cls.__name__ + # Keep a reference to the top level class + cls.top_level_class = cls + + # The following should not be registered as they are abstract. Classes + # are abstract if the have the __abstract attribute (note this is not + # inheritable so each abstract class must be explicitely marked). + abstract_attribute = '_{0:s}__abstract'.format(name) + if getattr(cls, abstract_attribute, None): + return + + if not cls.__name__.startswith('Abstract'): + cls_name = getattr(cls, 'NAME', cls.__name__) + + if cls_name in cls.classes: + raise KeyError(u'Class: {0:s} already registered. [{1:s}]'.format( + cls_name, repr(cls))) + + cls.classes[cls_name] = cls + + try: + if cls.top_level_class.include_plugins_as_attributes: + setattr(cls.top_level_class, cls.__name__, cls) + except AttributeError: + pass diff --git a/plaso/lib/storage.py b/plaso/lib/storage.py new file mode 100644 index 0000000..141f647 --- /dev/null +++ b/plaso/lib/storage.py @@ -0,0 +1,1564 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The storage mechanism. + +The storage mechanism can be described as a collection of storage files +that are stored together in a single ZIP compressed container. + +The storage file is essentially split up in two categories: + + A store file (further described below). + + Other files, these contain grouping information, tag, collection + information or other metadata describing the content of the store files. + +The store itself is a collection of four files: + plaso_meta. + plaso_proto. + plaso_index. + plaso_timestamps. + +The plaso_proto file within each store contains several serialized EventObjects +or events that are serialized (as a protobuf). All of the EventObjects within +the plaso_proto file are fully sorted based on time however since the storage +container can contain more than one store the overall storage is not fully +sorted. + +The other files that make up the store are: + + + plaso_meta + +Simple text file using YAML for storing metadata information about the store. +definition, example: + variable: value + a_list: [value, value, value] + +This can be used to filter out which proto files should be included +in processing. + + + plaso_index + +The index file contains an index to all the entries stored within +the protobuf file, so that it can be easily seeked. The layout is: + ++-----+-----+-...+ +| int | int | ...| ++-----+-----+-...+ + +Where int is an unsigned integer '= self._file_number: + self._file_number = file_number + 1 + except ValueError: + # Ignore invalid metadata stream names. + pass + + self._first_file_number = self._file_number + + def __enter__(self): + """Make usable with "with" statement.""" + return self + + def __exit__(self, unused_type, unused_value, unused_traceback): + """Make usable with "with" statement.""" + self.Close() + + def _BuildTagIndex(self): + """Builds the tag index that contains the offsets for each tag. + + Raises: + IOError: if the stream cannot be opened. + """ + self._event_tag_index = {} + + for stream_name in self._GetStreamNames(): + if not stream_name.startswith('plaso_tag_index.'): + continue + + file_object = self._OpenStream(stream_name, 'r') + if file_object is None: + raise IOError(u'Unable to open stream: {0:s}'.format(stream_name)) + + _, _, store_number = stream_name.rpartition('.') + # TODO: catch exception. + store_number = int(store_number, 10) + + while True: + tag_index_value = _EventTagIndexValue.Read( + file_object, store_number) + if tag_index_value is None: + break + + self._event_tag_index[tag_index_value.identifier] = tag_index_value + + def _FlushBuffer(self): + """Flushes the buffered streams to disk.""" + if not self._buffer_size: + return + + yaml_dict = { + 'range': (self._buffer_first_timestamp, self._buffer_last_timestamp), + 'version': self.STORAGE_VERSION, + 'data_type': list(self._count_data_type.viewkeys()), + 'parsers': list(self._count_parser.viewkeys()), + 'count': len(self._buffer), + 'type_count': self._count_data_type.most_common()} + self._count_data_type = collections.Counter() + self._count_parser = collections.Counter() + + stream_name = 'plaso_meta.{0:06d}'.format(self._file_number) + self._WriteStream(stream_name, yaml.safe_dump(yaml_dict)) + + ofs = 0 + proto_str = [] + index_str = [] + timestamp_str = [] + for _ in range(len(self._buffer)): + timestamp, entry = heapq.heappop(self._buffer) + # TODO: Instead of appending to an array + # which is not optimal (loads up the entire max file + # size into memory) Zipfile should be extended to + # allow appending to files (implement lock). + try: + # Appending a timestamp to the timestamp index, this is used during + # time based filtering. If this is not done we would need to unserialize + # all events to get the timestamp value which is really slow. + timestamp_str.append(struct.pack('= 0: + stream_offset = self._GetProtoStreamOffset(stream_number, entry_index) + if stream_offset is None: + logging.error(( + u'Unable to read entry index: {0:d} from proto stream: ' + u'{1:d}').format(entry_index, stream_number)) + + return None, None + + file_object, last_entry_index = self._GetProtoStreamSeekOffset( + stream_number, entry_index, stream_offset) + + if (not last_entry_index and entry_index == -1 and + self._bound_first is not None): + # We only get here if the following conditions are met: + # 1. last_entry_index is not set (so this is the first read + # from this file). + # 2. There is a lower bound (so we have a date filter). + # 3. The lower bound is higher than zero (basically set to a value). + # 4. We are accessing this function using 'get me the next entry' as an + # opposed to the 'get me entry X', where we just want to server entry + # X. + # + # The purpose: speed seeking into the storage file based on time. Instead + # of spending precious time reading through the storage file and + # deserializing protobufs just to compare timestamps we read a much + # 'cheaper' file, one that only contains timestamps to find the proper + # entry into the storage file. That way we'll get to the right place in + # the file and can start reading protobufs from the right location. + + stream_name = 'plaso_timestamps.{0:06d}'.format(stream_number) + + if stream_name in self._GetStreamNames(): + timestamp_file_object = self._OpenStream(stream_name, 'r') + if timestamp_file_object is None: + raise IOError(u'Unable to open stream: {0:s}'.format(stream_name)) + + index = 0 + timestamp_compare = 0 + encountered_error = False + while timestamp_compare < self._bound_first: + timestamp_raw = timestamp_file_object.read(8) + if len(timestamp_raw) != 8: + encountered_error = True + break + + timestamp_compare = struct.unpack(' self.MAX_PROTO_STRING_SIZE: + raise errors.WrongProtobufEntry( + u'Protobuf string size value exceeds maximum: {0:d}'.format( + proto_string_size)) + + event_object_data = file_object.read(proto_string_size) + self._proto_streams[stream_number] = (file_object, last_entry_index + 1) + + return event_object_data, last_entry_index + + def _GetEventGroupProto(self, file_object): + """Return a single group entry.""" + unpacked = file_object.read(4) + if len(unpacked) != 4: + return None + + size = struct.unpack(' StorageFile.MAX_PROTO_STRING_SIZE: + raise errors.WrongProtobufEntry( + u'Protobuf size too large: {0:d}'.format(size)) + + proto_serialized = file_object.read(size) + proto = plaso_storage_pb2.EventGroup() + + proto.ParseFromString(proto_serialized) + return proto + + def _GetProtoStream(self, stream_number): + """Retrieves the proto stream. + + Args: + stream_number: the number of the stream. + + Returns: + A tuple of the stream file-like object and the last entry index to + which the offset of the stream file-like object points. + + Raises: + IOError: if the stream cannot be opened. + """ + if stream_number not in self._proto_streams: + stream_name = 'plaso_proto.{0:06d}'.format(stream_number) + + file_object = self._OpenStream(stream_name, 'r') + if file_object is None: + raise IOError(u'Unable to open stream: {0:s}'.format(stream_name)) + + # TODO: change this to a value object and track the stream offset as well. + # This allows to reduce the number of re-opens when the seek offset is + # beyond the current offset. + self._proto_streams[stream_number] = (file_object, 0) + + return self._proto_streams[stream_number] + + def _GetProtoStreamSeekOffset( + self, stream_number, entry_index, stream_offset): + """Retrieves the proto stream and seeks a specified offset in the stream. + + Args: + stream_number: the number of the stream. + entry_index: the entry index. + stream_offset: the offset relative to the start of the stream. + + Returns: + A tuple of the stream file-like object and the last index. + + Raises: + IOError: if the stream cannot be opened. + """ + # Since zipfile.ZipExtFile is not seekable we need to close the stream + # and reopen it to fake a seek. + if stream_number in self._proto_streams: + previous_file_object, _ = self._proto_streams[stream_number] + del self._proto_streams[stream_number] + previous_file_object.close() + + stream_name = 'plaso_proto.{0:06d}'.format(stream_number) + file_object = self._OpenStream(stream_name, 'r') + if file_object is None: + raise IOError(u'Unable to open stream: {0:s}'.format(stream_name)) + + # Since zipfile.ZipExtFile is not seekable we need to read upto + # the stream offset. + _ = file_object.read(stream_offset) + + self._proto_streams[stream_number] = (file_object, entry_index) + + return self._proto_streams[stream_number] + + def _GetProtoStreamOffset(self, stream_number, entry_index): + """Retrieves the offset of a proto stream entry from the index stream. + + Args: + stream_number: the number of the stream. + entry_index: the entry index. + + Returns: + The offset of the entry in the corresponding proto stream + or None on error. + + Raises: + IOError: if the stream cannot be opened. + """ + # TODO: cache the index file object in the same way as the proto + # stream file objects. + + # TODO: once cached use the last entry index to determine if the stream + # file object should be re-opened. + + stream_name = 'plaso_index.{0:06d}'.format(stream_number) + index_file_object = self._OpenStream(stream_name, 'r') + if index_file_object is None: + raise IOError(u'Unable to open stream: {0:s}'.format(stream_name)) + + # Since zipfile.ZipExtFile is not seekable we need to read upto + # the stream offset. + _ = index_file_object.read(entry_index * 4) + + index_data = index_file_object.read(4) + + index_file_object.close() + + if len(index_data) != 4: + return None + + return struct.unpack(' self.MAX_PROTO_STRING_SIZE: + raise errors.WrongProtobufEntry( + u'Protobuf string size value exceeds maximum: {0:d}'.format( + proto_string_size)) + + proto_string = file_object.read(proto_string_size) + return self._event_tag_serializer.ReadSerialized(proto_string) + + def _ReadEventTagByIdentifier(self, store_number, store_index, uuid): + """Reads an event tag by identifier. + + Args: + store_number: the store number. + store_index: the store index. + uuid: the UUID string. + + Returns: + The event tag (instance of EventTag). + + Raises: + IOError: if the stream cannot be opened. + """ + tag_index_value = self._GetEventTagIndexValue( + store_number, store_index, uuid) + if tag_index_value is None: + return + + stream_name = 'plaso_tagging.{0:06d}'.format(tag_index_value.store_number) + tag_file_object = self._OpenStream(stream_name, 'r') + if tag_file_object is None: + raise IOError(u'Unable to open stream: {0:s}'.format(stream_name)) + + # Since zipfile.ZipExtFile is not seekable we need to read upto + # the store offset. + _ = tag_file_object.read(tag_index_value.store_offset) + return self._ReadEventTag(tag_file_object) + + def _ReadStream(self, stream_name): + """Reads the data in a stream. + + Args: + stream_name: the name of the stream. + + Returns: + A byte string containing the data of the stream. + """ + data_segments = [] + file_object = self._OpenStream(stream_name, 'r') + + # zipfile.ZipExtFile does not support the with-statement interface. + if file_object: + data = file_object.read(self._STREAM_DATA_SEGMENT_SIZE) + while data: + data_segments.append(data) + data = file_object.read(self._STREAM_DATA_SEGMENT_SIZE) + + file_object.close() + + return ''.join(data_segments) + + def _SetEventObjectSerializer(self, serializer_string): + """Set the serializer for the event object.""" + if serializer_string == 'json': + self._event_object_serializer = ( + json_serializer.JsonEventObjectSerializer) + self._event_serializer_format_string = 'json' + else: + self._event_object_serializer = ( + protobuf_serializer.ProtobufEventObjectSerializer) + self._event_serializer_format_string = 'proto' + + def _WritePreprocessObject(self, pre_obj): + """Writes a preprocess object to the storage file. + + Args: + pre_obj: the preprocess object (instance of PreprocessObject). + + Raises: + IOError: if the stream cannot be opened. + """ + existing_stream_data = self._ReadStream('information.dump') + + # Store information about store range for this particular + # preprocessing object. This will determine which stores + # this information is applicaple for. + stores = list(self.GetProtoNumbers()) + if stores: + end = stores[-1] + 1 + else: + end = self._first_file_number + pre_obj.store_range = (self._first_file_number, end) + + pre_obj_data = self._pre_obj_serializer.WriteSerialized(pre_obj) + + stream_data = ''.join([ + existing_stream_data, + struct.pack(' self.MAX_PROTO_STRING_SIZE: + raise errors.WrongProtobufEntry( + u'Protobuf size too large: {0:d}'.format(size)) + + serialized_pre_obj = file_object.read(size) + try: + info = self._pre_obj_serializer.ReadSerialized(serialized_pre_obj) + except message.DecodeError: + logging.error(u'Unable to parse preprocessing object, bailing out.') + break + + information.append(info) + + stores = list(self.GetProtoNumbers()) + information[-1].stores = {} + information[-1].stores['Number'] = len(stores) + for store_number in stores: + store_identifier = 'Store {0:d}'.format(store_number) + information[-1].stores[store_identifier] = self.ReadMeta(store_number) + + return information + + def SetStoreLimit(self, unused_my_filter=None): + """Set a limit to the stores used for returning data.""" + # Retrieve set first and last timestamps. + self._bound_first, self._bound_last = pfilter.TimeRangeCache.GetTimeRange() + + self.store_range = [] + + # TODO: Fetch a filter object from the filter query. + + for number in self.GetProtoNumbers(): + # TODO: Read more criteria from here. + first, last = self.ReadMeta(number).get('range', (0, limit.MAX_INT64)) + if last < first: + logging.error( + u'last: {0:d} first: {1:d} container: {2:d} (last < first)'.format( + last, first, number)) + + if first <= self._bound_last and self._bound_first <= last: + # TODO: Check at least parser and data_type (stored in metadata). + # Check whether these attributes exist in filter, if so use the filter + # to determine whether the stores should be included. + self.store_range.append(number) + + else: + logging.debug(u'Store [{0:d}] not used'.format(number)) + + def GetSortedEntry(self): + """Return a sorted entry from the storage file. + + Returns: + An event object (instance of EventObject). + """ + if self._bound_first is None: + self._bound_first, self._bound_last = ( + pfilter.TimeRangeCache.GetTimeRange()) + + if not hasattr(self, '_merge_buffer'): + self._merge_buffer = [] + number_range = getattr(self, 'store_range', list(self.GetProtoNumbers())) + for store_number in number_range: + event_object = self.GetEventObject(store_number) + if not event_object: + return + + while event_object.timestamp < self._bound_first: + event_object = self.GetEventObject(store_number) + if not event_object: + return + + heapq.heappush( + self._merge_buffer, + (event_object.timestamp, store_number, event_object)) + + if not self._merge_buffer: + return + + _, store_number, event_read = heapq.heappop(self._merge_buffer) + if not event_read: + return + + # Stop as soon as we hit the upper bound. + if event_read.timestamp > self._bound_last: + return + + new_event_object = self.GetEventObject(store_number) + + if new_event_object: + heapq.heappush( + self._merge_buffer, + (new_event_object.timestamp, store_number, new_event_object)) + + event_read.tag = self._ReadEventTagByIdentifier( + event_read.store_number, event_read.store_index, event_read.uuid) + + return event_read + + def GetEventObject(self, stream_number, entry_index=-1): + """Reads an event object from the store. + + By default the next entry in the appropriate proto file is read + and returned, however any entry can be read using the index file. + + Args: + stream_number: The proto stream number. + entry_index: Read a specific entry in the file. The default is -1, + which represents the next available entry. + + Returns: + An event object (instance of EventObject) entry read from the file or + None if not able to read in a new event. + """ + event_object_data, entry_index = self._GetEventObjectProtobufString( + stream_number, entry_index=entry_index) + if not event_object_data: + return + + event_object = self._event_object_serializer.ReadSerialized( + event_object_data) + event_object.store_number = stream_number + event_object.store_index = entry_index + + return event_object + + def GetEntries(self, number): + """A generator to read all plaso_storage protobufs. + + The storage mechanism of Plaso works in the way that it creates potentially + several files inside the ZIP container. As soon as the number of protobufs + stored exceed the size of buffer_size they will be flushed to disk as: + + plaso_proto.XXX + + Where XXX is an increasing integer, starting from one. To get all the files + or the numbers that are available this class implements a method called + GetProtoNumbers() that returns a list of all available protobuf files within + the container. + + This method returns a generator that returns all plaso_storage protobufs in + the named container, as indicated by the number argument. So if this method + is called as storage_object.GetEntries(1) the generator will return the + entries found in the file plaso_proto.000001. + + Args: + number: The protofile number. + + Yields: + A protobuf object from the protobuf file. + """ + # TODO: Change this function, don't accept a store number and implement the + # MergeSort functionality of the psort file in here. This will then always + # return the sorted entries from the storage file, implementing the second + # stage of the sort/merge algorithm. + while True: + try: + proto = self.GetEventObject(number) + if not proto: + logging.debug( + u'End of protobuf file plaso_proto.{0:06d} reached.'.format( + number)) + break + yield proto + except errors.WrongProtobufEntry as exception: + logging.warning(( + u'Problem while parsing a protobuf entry from: ' + u'plaso_proto.{0:06d} with error: {1:s}').format(number, exception)) + + def GetProtoNumbers(self): + """Return all available protobuf numbers.""" + numbers = [] + for name in self._GetStreamNames(): + if 'plaso_proto' in name: + _, num = name.split('.') + numbers.append(int(num)) + + for number in sorted(numbers): + yield number + + def ReadMeta(self, number): + """Return a dict with the metadata entries. + + Args: + number: The number of the metadata file (name is plaso_meta_XXX where + XXX is this number. + + Returns: + A dict object containing all the variables inside the metadata file. + + Raises: + IOError: if the stream cannot be opened. + """ + stream_name = 'plaso_meta.{0:06d}'.format(number) + file_object = self._OpenStream(stream_name, 'r') + if file_object is None: + raise IOError(u'Unable to open stream: {0:s}'.format(stream_name)) + return yaml.safe_load(file_object) + + def GetBufferSize(self): + """Return the size of the buffer.""" + return self._buffer_size + + def GetFileNumber(self): + """Return the current file number of the storage.""" + return self._file_number + + def AddEventObject(self, event_object): + """Adds an event object to the storage. + + Args: + event_object: an event object (instance of EventObject). + + Raises: + IOError: When trying to write to a closed storage file. + """ + if not self._file_open: + raise IOError(u'Trying to add an entry to a closed storage file.') + + if event_object.timestamp > self._buffer_last_timestamp: + self._buffer_last_timestamp = event_object.timestamp + + # TODO: support negative timestamps. + if (event_object.timestamp < self._buffer_first_timestamp and + event_object.timestamp > 0): + self._buffer_first_timestamp = event_object.timestamp + + attributes = event_object.GetValues() + # Add values to counters. + if self._pre_obj: + self._pre_obj.counter['total'] += 1 + self._pre_obj.counter[attributes.get('parser', 'N/A')] += 1 + if 'plugin' in attributes: + self._pre_obj.plugin_counter[attributes.get('plugin', 'N/A')] += 1 + + # Add to temporary counter. + self._count_data_type[event_object.data_type] += 1 + parser = attributes.get('parser', 'unknown_parser') + self._count_parser[parser] += 1 + + event_object_data = self._event_object_serializer.WriteSerialized( + event_object) + + # TODO: Re-think this approach with the re-design of the storage. + # Check if the event object failed to serialize (none is returned). + if event_object_data is None: + return + + heapq.heappush( + self._buffer, (event_object.timestamp, event_object_data)) + self._buffer_size += len(event_object_data) + self._write_counter += 1 + + if self._buffer_size > self._max_buffer_size: + self._FlushBuffer() + + def AddEventObjects(self, event_objects): + """Adds an event objects to the storage. + + Args: + event_objects: a list or generator of event objects (instances of + EventObject). + """ + for event_object in event_objects: + self.AddEventObject(event_object) + + def HasTagging(self): + """Return a bool indicating whether or not a Tag file is stored.""" + for name in self._GetStreamNames(): + if 'plaso_tagging.' in name: + return True + return False + + def HasGrouping(self): + """Return a bool indicating whether or not a Group file is stored.""" + for name in self._GetStreamNames(): + if 'plaso_grouping.' in name: + return True + return False + + def HasReports(self): + """Return a bool indicating whether or not a Report file is stored.""" + for name in self._GetStreamNames(): + if 'plaso_report.' in name: + return True + + return False + + def StoreReport(self, analysis_report): + """Store an analysis report. + + Args: + analysis_report: An analysis report object (instance of AnalysisReport). + """ + report_number = 1 + for name in self._GetStreamNames(): + if 'plaso_report.' in name: + _, _, number_string = name.partition('.') + try: + number = int(number_string, 10) + except ValueError: + logging.error(u'Unable to read in report number.') + number = 0 + if number >= report_number: + report_number = number + 1 + + stream_name = 'plaso_report.{0:06}'.format(report_number) + serialized_report_proto = self._analysis_report_serializer.WriteSerialized( + analysis_report) + self._WriteStream(stream_name, serialized_report_proto) + + def GetReports(self): + """Read in all stored analysis reports from storage and yield them. + + Raises: + IOError: if the stream cannot be opened. + """ + for stream_name in self._GetStreamNames(): + if stream_name.startswith('plaso_report.'): + file_object = self._OpenStream(stream_name, 'r') + if file_object is None: + raise IOError(u'Unable to open stream: {0:s}'.format(stream_name)) + + report_string = file_object.read(self.MAX_REPORT_PROTOBUF_SIZE) + yield self._analysis_report_serializer.ReadSerialized(report_string) + + def StoreGrouping(self, rows): + """Store group information into the storage file. + + An EventGroup protobuf stores information about several + EventObjects that belong to the same behavior or action. It can then + be used to group similar events together to create a super event, or + a higher level event. + + This function is used to store that information inside the storage + file so it can be read later. + + The object that is passed in needs to have an iterator implemented + and has to implement the following attributes (optional names within + bracket): + name - The name of the grouped event. + [description] - More detailed description of the event. + [category] - If this group of events falls into a specific category. + [color] - To highlight this particular group with a HTML color tag. + [first_timestamp] - The first timestamp if applicable of the group. + [last_timestamp] - The last timestamp if applicable of the group. + events - A list of tuples (store_number and store_index of the + EventObject protobuf that belongs to this group of events). + + Args: + rows: An object that contains the necessary fields to construct + an EventGroup. Has to be a generator object or an object that implements + an iterator. + """ + group_number = 1 + if self.HasGrouping(): + for name in self._GetStreamNames(): + if 'plaso_grouping.' in name: + _, number = name.split('.') + if int(number) >= group_number: + group_number = int(number) + 1 + + group_packed = [] + size = 0 + for row in rows: + group = plaso_storage_pb2.EventGroup() + group.name = row.name + if hasattr(row, 'description'): + group.description = utils.GetUnicodeString(row.description) + if hasattr(row, 'category'): + group.category = utils.GetUnicodeString(row.category) + if hasattr(row, 'color'): + group.color = utils.GetUnicodeString(row.color) + + for number, index in row.events: + evt = group.events.add() + evt.store_number = int(number) + evt.store_index = int(index) + + if hasattr(row, 'first_timestamp'): + group.first_timestamp = int(row.first_timestamp) + if hasattr(row, 'last_timestamp'): + group.last_timestamp = int(row.last_timestamp) + + # TODO: implement event grouping. + group_str = group.SerializeToString() + packed = struct.pack(' self._max_buffer_size: + logging.warning(u'Grouping has outgrown buffer size.') + group_packed.append(packed) + + stream_name = 'plaso_grouping.{0:06d}'.format(group_number) + self._WriteStream(stream_name, ''.join(group_packed)) + + def StoreTagging(self, tags): + """Store tag information into the storage file. + + Each EventObject can be tagged either manually or automatically + to make analysis simpler, by providing more context to certain + events or to highlight events for later viewing. + + The object passed in needs to be a list (or otherwise an iterator) + that contains EventTag objects (event.EventTag). + + Args: + tags: A list or an object providing an iterator that contains + EventTag objects. + + Raises: + IOError: if the stream cannot be opened. + """ + if not self._pre_obj: + self._pre_obj = event.PreprocessObject() + + if not hasattr(self._pre_obj, 'collection_information'): + self._pre_obj.collection_information = {} + + self._pre_obj.collection_information['Action'] = 'Adding tags to storage.' + self._pre_obj.collection_information['time_of_run'] = ( + timelib.Timestamp.GetNow()) + if not hasattr(self._pre_obj, 'counter'): + self._pre_obj.counter = collections.Counter() + + tag_number = 1 + for name in self._GetStreamNames(): + if 'plaso_tagging.' in name: + _, number = name.split('.') + if int(number) >= tag_number: + tag_number = int(number) + 1 + if self._event_tag_index is None: + self._BuildTagIndex() + + tag_packed = [] + tag_index = [] + size = 0 + for tag in tags: + self._pre_obj.counter['Total Tags'] += 1 + if hasattr(tag, 'tags'): + for tag_entry in tag.tags: + self._pre_obj.counter[tag_entry] += 1 + + if self._event_tag_index is not None: + tag_index_value = self._event_tag_index.get(tag.string_key, None) + else: + tag_index_value = None + + # This particular event has already been tagged on a previous occasion, + # we need to make sure we are appending to that particular tag. + if tag_index_value is not None: + stream_name = 'plaso_tagging.{0:06d}'.format( + tag_index_value.store_number) + + tag_file_object = self._OpenStream(stream_name, 'r') + if tag_file_object is None: + raise IOError(u'Unable to open stream: {0:s}'.format(stream_name)) + + # Since zipfile.ZipExtFile is not seekable we need to read upto + # the store offset. + _ = tag_file_object.read(tag_index_value.store_offset) + + old_tag = self._ReadEventTag(tag_file_object) + + # TODO: move the append functionality into EventTag. + # Maybe name the function extend or update? + if hasattr(old_tag, 'tags'): + tag.tags.extend(old_tag.tags) + + if hasattr(old_tag, 'comment'): + if hasattr(tag, 'comment'): + tag.comment += old_tag.comment + else: + tag.comment = old_tag.comment + + if hasattr(old_tag, 'color') and not hasattr(tag, 'color'): + tag.color = old_tag.color + + serialized_event_tag = self._event_tag_serializer.WriteSerialized(tag) + + # TODO: move to write class function of _EventTagIndexValue. + packed = ( + struct.pack(' cls.TIMESTAMP_MAX_SECONDS): + return 0 + + return cls.FromPosixTime(int(posix_time)) + + @classmethod + def FromFatDateTime(cls, fat_date_time): + """Converts a FAT date and time into a timestamp. + + FAT date time is mainly used in DOS/Windows file formats and FAT. + + The FAT date and time is a 32-bit value containing two 16-bit values: + * The date (lower 16-bit). + * bits 0 - 4: day of month, where 1 represents the first day + * bits 5 - 8: month of year, where 1 represent January + * bits 9 - 15: year since 1980 + * The time of day (upper 16-bit). + * bits 0 - 4: seconds (in 2 second intervals) + * bits 5 - 10: minutes + * bits 11 - 15: hours + + Args: + fat_date_time: The 32-bit FAT date time. + + Returns: + An integer containing the timestamp or 0 on error. + """ + number_of_seconds = cls.FAT_DATE_TO_POSIX_BASE + + day_of_month = (fat_date_time & 0x1f) - 1 + month = ((fat_date_time >> 5) & 0x0f) - 1 + year = (fat_date_time >> 9) & 0x7f + + if day_of_month < 0 or day_of_month > 30 or month < 0 or month > 11: + return 0 + + number_of_days = cls.DayOfYear(day_of_month, month, 1980 + year) + for past_year in range(0, year): + number_of_days += cls.DaysInYear(past_year) + + fat_date_time >>= 16 + + seconds = (fat_date_time & 0x1f) * 2 + minutes = (fat_date_time >> 5) & 0x3f + hours = (fat_date_time >> 11) & 0x1f + + if hours > 23 or minutes > 59 or seconds > 59: + return 0 + + number_of_seconds += (((hours * 60) + minutes) * 60) + seconds + + number_of_seconds += number_of_days * cls.SECONDS_PER_DAY + + return number_of_seconds * cls.MICRO_SECONDS_PER_SECOND + + @classmethod + def FromFiletime(cls, filetime): + """Converts a FILETIME into a timestamp. + + FILETIME is mainly used in Windows file formats and NTFS. + + The FILETIME is a 64-bit value containing: + 100th nano seconds since 1601-01-01 00:00:00 + + Technically FILETIME consists of 2 x 32-bit parts and is presumed + to be unsigned. + + Args: + filetime: The 64-bit FILETIME timestamp. + + Returns: + An integer containing the timestamp or 0 on error. + """ + # TODO: Add a handling for if the timestamp equals to zero. + if filetime < 0: + return 0 + timestamp = (filetime - cls.FILETIME_TO_POSIX_BASE) / 10 + + if timestamp > cls.TIMESTAMP_MAX_MICRO_SECONDS: + return 0 + return timestamp + + @classmethod + def FromHfsTime(cls, hfs_time, timezone=pytz.utc, is_dst=False): + """Converts a HFS time to a timestamp. + + HFS time is the same as HFS+ time, except stored in the local + timezone of the user. + + Args: + hfs_time: Timestamp in the hfs format (32 bit unsigned int). + timezone: The timezone object of the system's local time. + is_dst: A boolean to indicate the timestamp is corrected for daylight + savings time (DST) only used for the DST transition period. + The default is false. + + Returns: + An integer containing the timestamp or 0 on error. + """ + timestamp_local = cls.FromHfsPlusTime(hfs_time) + return cls.LocaltimeToUTC(timestamp_local, timezone, is_dst) + + @classmethod + def FromHfsPlusTime(cls, hfs_time): + """Converts a HFS+ time to a timestamp. + + In HFS+ date and time values are stored in an unsigned 32-bit integer + containing the number of seconds since January 1, 1904 at 00:00:00 + (midnight) UTC (GMT). + + Args: + hfs_time: The timestamp in HFS+ format. + + Returns: + An integer containing the timestamp or 0 on error. + """ + return cls.FromPosixTime(hfs_time - cls.HFSTIME_TO_POSIX_BASE) + + @classmethod + def FromJavaTime(cls, java_time): + """Converts a Java time to a timestamp. + + Jave time is the number of milliseconds since + January 1, 1970, 00:00:00 UTC. + + URL: http://docs.oracle.com/javase/7/docs/api/ + java/sql/Timestamp.html#getTime%28%29 + + Args: + java_time: The Java Timestamp. + + Returns: + An integer containing the timestamp or 0 on error. + """ + return java_time * cls.MILLI_SECONDS_TO_MICRO_SECONDS + + @classmethod + def FromPosixTime(cls, posix_time): + """Converts a POSIX timestamp into a timestamp. + + The POSIX time is a signed 32-bit or 64-bit value containing: + seconds since 1970-01-01 00:00:00 + + Args: + posix_time: The POSIX timestamp. + + Returns: + An integer containing the timestamp or 0 on error. + """ + if (posix_time < cls.TIMESTAMP_MIN_SECONDS or + posix_time > cls.TIMESTAMP_MAX_SECONDS): + return 0 + return int(posix_time) * cls.MICRO_SECONDS_PER_SECOND + + @classmethod + def FromPosixTimeWithMicrosecond(cls, posix_time, microsecond): + """Converts a POSIX timestamp with microsecond into a timestamp. + + The POSIX time is a signed 32-bit or 64-bit value containing: + seconds since 1970-01-01 00:00:00 + + Args: + posix_time: The POSIX timestamp. + microsecond: The microseconds to add to the timestamp. + + Returns: + An integer containing the timestamp or 0 on error. + """ + timestamp = cls.FromPosixTime(posix_time) + if not timestamp: + return 0 + return timestamp + microsecond + + @classmethod + def FromPythonDatetime(cls, datetime_object): + """Converts a Python datetime object into a timestamp.""" + if not isinstance(datetime_object, datetime.datetime): + return 0 + + posix_epoch = int(calendar.timegm(datetime_object.utctimetuple())) + epoch = cls.FromPosixTime(posix_epoch) + return epoch + datetime_object.microsecond + + @classmethod + def FromTimeParts( + cls, year, month, day, hour, minutes, seconds, microseconds=0, + timezone=pytz.utc): + """Converts a list of time entries to a timestamp. + + Args: + year: An integer representing the year. + month: An integer between 1 and 12. + day: An integer representing the number of day in the month. + hour: An integer representing the hour, 0 <= hour < 24. + minutes: An integer, 0 <= minute < 60. + seconds: An integer, 0 <= second < 60. + microseconds: Optional number of microseconds ranging from: + 0 <= microsecond < 1000000. The default is 0. + timezone: Optional timezone (instance of pytz.timezone). + The default is UTC. + + Returns: + An integer containing the timestamp or 0 on error. + """ + try: + date = datetime.datetime( + year, month, day, hour, minutes, seconds, microseconds) + except ValueError as exception: + logging.warning(( + u'Unable to create timestamp from {0:04d}-{1:02d}-{2:02d} ' + u'{3:02d}:{4:02d}:{5:02d}.{6:06d} with error: {7:s}').format( + year, month, day, hour, minutes, seconds, microseconds, + exception)) + return 0 + + if type(timezone) is str: + timezone = pytz.timezone(timezone) + + date_use = timezone.localize(date) + epoch = int(calendar.timegm(date_use.utctimetuple())) + + return cls.FromPosixTime(epoch) + microseconds + + @classmethod + def FromTimeString( + cls, time_string, timezone=pytz.utc, dayfirst=False, + gmt_as_timezone=True): + """Converts a string containing a date and time value into a timestamp. + + Args: + time_string: String that contains a date and time value. + timezone: Optional timezone object (instance of pytz.timezone) that + the data and time value in the string represents. This value + is used when the timezone cannot be determined from the string. + dayfirst: An optional boolean argument. If set to true then the + parser will change the precedence in which it parses timestamps + from MM-DD-YYYY to DD-MM-YYYY (and YYYY-MM-DD will be + YYYY-DD-MM, etc). + gmt_as_timezone: Sometimes the dateutil parser will interpret GMT and UTC + the same way, that is not make a distinction. By default + this is set to true, that is GMT can be intepreted + differently than UTC. If that is not the expected result + this attribute can be set to false. + + Returns: + An integer containing the timestamp or 0 on error. + """ + datetime_object = StringToDatetime( + time_string, timezone=timezone, dayfirst=dayfirst, + gmt_as_timezone=gmt_as_timezone) + return cls.FromPythonDatetime(datetime_object) + + @classmethod + def FromWebKitTime(cls, webkit_time): + """Converts a WebKit time into a timestamp. + + The WebKit time is a 64-bit value containing: + micro seconds since 1601-01-01 00:00:00 + + Args: + webkit_time: The 64-bit WebKit time timestamp. + + Returns: + An integer containing the timestamp or 0 on error. + """ + if webkit_time < (cls.TIMESTAMP_MIN_MICRO_SECONDS + + cls.WEBKIT_TIME_TO_POSIX_BASE): + return 0 + return webkit_time - cls.WEBKIT_TIME_TO_POSIX_BASE + + @classmethod + def GetNow(cls): + """Retrieves the current time (now) as a timestamp in UTC.""" + time_elements = time.gmtime() + return calendar.timegm(time_elements) * 1000000 + + @classmethod + def IsLeapYear(cls, year): + """Determines if a year is a leap year. + + A leap year is dividable by 4 and not by 100 or by 400 + without a remainder. + + Args: + year: The year as in 1970. + + Returns: + A boolean value indicating the year is a leap year. + """ + return (year % 4 == 0 and year % 100 != 0) or year % 400 == 0 + + @classmethod + def LocaltimeToUTC(cls, timestamp, timezone, is_dst=False): + """Converts the timestamp in localtime of the timezone to UTC. + + Args: + timestamp: An integer containing the timestamp. + timezone: The timezone (pytz.timezone) object. + is_dst: A boolean to indicate the timestamp is corrected for daylight + savings time (DST) only used for the DST transition period. + The default is false. + + Returns: + An integer containing the timestamp or 0 on error. + """ + if timezone and timezone != pytz.utc: + datetime_object = ( + datetime.datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=None) + + datetime.timedelta(microseconds=timestamp)) + + # Check if timezone is UTC since utcoffset() does not support is_dst + # for UTC and will raise. + datetime_delta = timezone.utcoffset(datetime_object, is_dst=is_dst) + seconds_delta = int(datetime_delta.total_seconds()) + timestamp -= seconds_delta * cls.MICRO_SECONDS_PER_SECOND + + return timestamp + + @classmethod + def RoundToSeconds(cls, timestamp): + """Takes a timestamp value and rounds it to a second precision.""" + leftovers = timestamp % cls.MICRO_SECONDS_PER_SECOND + scrubbed = timestamp - leftovers + rounded = round(float(leftovers) / cls.MICRO_SECONDS_PER_SECOND) + + return int(scrubbed + rounded * cls.MICRO_SECONDS_PER_SECOND) + + +def StringToDatetime( + time_string, timezone=pytz.utc, dayfirst=False, gmt_as_timezone=True): + """Converts a string representation of a timestamp into a datetime object. + + Args: + time_string: String that contains a date and time value. + timezone: Optional timezone object (instance of pytz.timezone) that + the data and time value in the string represents. This value + is used when the timezone cannot be determined from the string. + dayfirst: An optional boolean argument. If set to true then the + parser will change the precedence in which it parses timestamps + from MM-DD-YYYY to DD-MM-YYYY (and YYYY-MM-DD will be YYYY-DD-MM, + etc). + gmt_as_timezone: Sometimes the dateutil parser will interpret GMT and UTC + the same way, that is not make a distinction. By default + this is set to true, that is GMT can be intepreted + differently than UTC. If that is not the expected result + this attribute can be set to false. + + Returns: + A datetime object. + """ + if not gmt_as_timezone and time_string.endswith(' GMT'): + time_string = u'{0:s}UTC'.format(time_string[:-3]) + + try: + datetime_object = dateutil.parser.parse(time_string, dayfirst=dayfirst) + + except (TypeError, ValueError) as exception: + logging.error( + u'Unable to copy {0:s} to a datetime object with error: {1:s}'.format( + time_string, exception)) + return datetime.datetime(1970, 1, 1, 0, 0, 0, 0, tzinfo=pytz.utc) + + if datetime_object.tzinfo: + return datetime_object.astimezone(pytz.utc) + + return timezone.localize(datetime_object) + + +def GetCurrentYear(): + """Determines the current year.""" + datetime_object = datetime.datetime.now() + return datetime_object.year diff --git a/plaso/lib/timelib_test.py b/plaso/lib/timelib_test.py new file mode 100644 index 0000000..4af7054 --- /dev/null +++ b/plaso/lib/timelib_test.py @@ -0,0 +1,531 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a unit test for the timelib in Plaso.""" + +import calendar +import datetime +import unittest + +from plaso.lib import timelib + +import pytz + + +def CopyStringToTimestamp(time_string): + """Copies a string containing a date and time value to a timestamp. + + Test function that does not rely on dateutil parser. + + Args: + time_string: A string containing a date and time value formatted as: + YYYY-MM-DD hh:mm:ss.######[+-]##:## + Where # are numeric digits ranging from 0 to 9 and the seconds + fraction can be either 3 or 6 digits. Both the seconds fraction + and timezone offset are optional. The default timezone is UTC. + + Returns: + An integer containing the timestamp. + + Raises: + ValueError: if the time string is invalid or not supported. + """ + time_string_length = len(time_string) + + # The time string should at least contain 'YYYY-MM-DD hh:mm:ss'. + if (time_string_length < 19 or time_string[4] != '-' or + time_string[7] != '-' or time_string[10] != ' ' or + time_string[13] != ':' or time_string[16] != ':'): + raise ValueError(u'Invalid time string.') + + try: + year = int(time_string[0:4], 10) + except ValueError: + raise ValueError(u'Unable to parse year.') + + try: + month = int(time_string[5:7], 10) + except ValueError: + raise ValueError(u'Unable to parse month.') + + if month not in range(1, 13): + raise ValueError(u'Month value out of bounds.') + + try: + day_of_month = int(time_string[8:10], 10) + except ValueError: + raise ValueError(u'Unable to parse day of month.') + + if day_of_month not in range(1, 32): + raise ValueError(u'Day of month value out of bounds.') + + try: + hours = int(time_string[11:13], 10) + except ValueError: + raise ValueError(u'Unable to parse hours.') + + if hours not in range(0, 24): + raise ValueError(u'Hours value out of bounds.') + + try: + minutes = int(time_string[14:16], 10) + except ValueError: + raise ValueError(u'Unable to parse minutes.') + + if minutes not in range(0, 60): + raise ValueError(u'Minutes value out of bounds.') + + try: + seconds = int(time_string[17:19], 10) + except ValueError: + raise ValueError(u'Unable to parse day of seconds.') + + if seconds not in range(0, 60): + raise ValueError(u'Seconds value out of bounds.') + + micro_seconds = 0 + timezone_offset = 0 + + if time_string_length > 19: + if time_string[19] != '.': + timezone_index = 19 + else: + for timezone_index in range(19, time_string_length): + if time_string[timezone_index] in ['+', '-']: + break + + # The calculation that follow rely on the timezone index to point + # beyond the string in case no timezone offset was defined. + if timezone_index == time_string_length - 1: + timezone_index += 1 + + if timezone_index > 19: + fraction_of_seconds_length = timezone_index - 20 + if fraction_of_seconds_length not in [3, 6]: + raise ValueError(u'Invalid time string.') + + try: + micro_seconds = int(time_string[20:timezone_index], 10) + except ValueError: + raise ValueError(u'Unable to parse fraction of seconds.') + + if fraction_of_seconds_length == 3: + micro_seconds *= 1000 + + if timezone_index < time_string_length: + if (time_string_length - timezone_index != 6 or + time_string[timezone_index + 3] != ':'): + raise ValueError(u'Invalid time string.') + + try: + timezone_offset = int(time_string[ + timezone_index + 1:timezone_index + 3]) + except ValueError: + raise ValueError(u'Unable to parse timezone hours offset.') + + if timezone_offset not in range(0, 24): + raise ValueError(u'Timezone hours offset value out of bounds.') + + # Note that when the sign of the timezone offset is negative + # the difference needs to be added. We do so by flipping the sign. + if time_string[timezone_index] == '-': + timezone_offset *= 60 + else: + timezone_offset *= -60 + + try: + timezone_offset += int(time_string[ + timezone_index + 4:timezone_index + 6]) + except ValueError: + raise ValueError(u'Unable to parse timezone minutes offset.') + + timezone_offset *= 60 + + timestamp = int(calendar.timegm(( + year, month, day_of_month, hours, minutes, seconds))) + + return ((timestamp + timezone_offset) * 1000000) + micro_seconds + + +class TimeLibUnitTest(unittest.TestCase): + """A unit test for the timelib.""" + + def testCocoaTime(self): + """Tests the Cocoa timestamp conversion.""" + self.assertEquals( + timelib.Timestamp.FromCocoaTime(395011845), + CopyStringToTimestamp('2013-07-08 21:30:45')) + + self.assertEquals( + timelib.Timestamp.FromCocoaTime(395353142), + CopyStringToTimestamp('2013-07-12 20:19:02')) + + self.assertEquals( + timelib.Timestamp.FromCocoaTime(394993669), + CopyStringToTimestamp('2013-07-08 16:27:49')) + + def testHFSTimes(self): + """Tests the HFS timestamp conversion.""" + self.assertEquals( + timelib.Timestamp.FromHfsTime( + 3458215528, timezone=pytz.timezone('EST5EDT'), is_dst=True), + CopyStringToTimestamp('2013-08-01 15:25:28-04:00')) + + self.assertEquals( + timelib.Timestamp.FromHfsPlusTime(3458215528), + CopyStringToTimestamp('2013-08-01 15:25:28')) + + self.assertEquals( + timelib.Timestamp.FromHfsPlusTime(3413373928), + CopyStringToTimestamp('2012-02-29 15:25:28')) + + def testTimestampIsLeapYear(self): + """Tests the is leap year check.""" + self.assertEquals(timelib.Timestamp.IsLeapYear(2012), True) + self.assertEquals(timelib.Timestamp.IsLeapYear(2013), False) + self.assertEquals(timelib.Timestamp.IsLeapYear(2000), True) + self.assertEquals(timelib.Timestamp.IsLeapYear(1900), False) + + def testTimestampDaysInMonth(self): + """Tests the days in month function.""" + self.assertEquals(timelib.Timestamp.DaysInMonth(0, 2013), 31) + self.assertEquals(timelib.Timestamp.DaysInMonth(1, 2013), 28) + self.assertEquals(timelib.Timestamp.DaysInMonth(1, 2012), 29) + self.assertEquals(timelib.Timestamp.DaysInMonth(2, 2013), 31) + self.assertEquals(timelib.Timestamp.DaysInMonth(3, 2013), 30) + self.assertEquals(timelib.Timestamp.DaysInMonth(4, 2013), 31) + self.assertEquals(timelib.Timestamp.DaysInMonth(5, 2013), 30) + self.assertEquals(timelib.Timestamp.DaysInMonth(6, 2013), 31) + self.assertEquals(timelib.Timestamp.DaysInMonth(7, 2013), 31) + self.assertEquals(timelib.Timestamp.DaysInMonth(8, 2013), 30) + self.assertEquals(timelib.Timestamp.DaysInMonth(9, 2013), 31) + self.assertEquals(timelib.Timestamp.DaysInMonth(10, 2013), 30) + self.assertEquals(timelib.Timestamp.DaysInMonth(11, 2013), 31) + + with self.assertRaises(ValueError): + timelib.Timestamp.DaysInMonth(-1, 2013) + + with self.assertRaises(ValueError): + timelib.Timestamp.DaysInMonth(12, 2013) + + def testTimestampDaysInYear(self): + """Test the days in year function.""" + self.assertEquals(timelib.Timestamp.DaysInYear(2013), 365) + self.assertEquals(timelib.Timestamp.DaysInYear(2012), 366) + + def testTimestampDayOfYear(self): + """Test the day of year function.""" + self.assertEquals(timelib.Timestamp.DayOfYear(0, 0, 2013), 0) + self.assertEquals(timelib.Timestamp.DayOfYear(0, 2, 2013), 31 + 28) + self.assertEquals(timelib.Timestamp.DayOfYear(0, 2, 2012), 31 + 29) + self.assertEquals(timelib.Timestamp.DayOfYear(0, 11, 2013), + 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30) + + def testTimestampFromDelphiTime(self): + """Test the Delphi date time conversion.""" + self.assertEquals( + timelib.Timestamp.FromDelphiTime(41443.8263953), + CopyStringToTimestamp('2013-06-18 19:50:00')) + + def testTimestampFromFatDateTime(self): + """Test the FAT date time conversion.""" + self.assertEquals( + timelib.Timestamp.FromFatDateTime(0xa8d03d0c), + CopyStringToTimestamp('2010-08-12 21:06:32')) + + # Invalid number of seconds. + fat_date_time = (0xa8d03d0c & ~(0x1f << 16)) | ((30 & 0x1f) << 16) + self.assertEquals(timelib.Timestamp.FromFatDateTime(fat_date_time), 0) + + # Invalid number of minutes. + fat_date_time = (0xa8d03d0c & ~(0x3f << 21)) | ((60 & 0x3f) << 21) + self.assertEquals(timelib.Timestamp.FromFatDateTime(fat_date_time), 0) + + # Invalid number of hours. + fat_date_time = (0xa8d03d0c & ~(0x1f << 27)) | ((24 & 0x1f) << 27) + self.assertEquals(timelib.Timestamp.FromFatDateTime(fat_date_time), 0) + + # Invalid day of month. + fat_date_time = (0xa8d03d0c & ~0x1f) | (32 & 0x1f) + self.assertEquals(timelib.Timestamp.FromFatDateTime(fat_date_time), 0) + + # Invalid month. + fat_date_time = (0xa8d03d0c & ~(0x0f << 5)) | ((13 & 0x0f) << 5) + self.assertEquals(timelib.Timestamp.FromFatDateTime(fat_date_time), 0) + + def testTimestampFromWebKitTime(self): + """Test the WebKit time conversion.""" + self.assertEquals( + timelib.Timestamp.FromWebKitTime(0x2dec3d061a9bfb), + CopyStringToTimestamp('2010-08-12 21:06:31.546875')) + + webkit_time = 86400 * 1000000 + self.assertEquals( + timelib.Timestamp.FromWebKitTime(webkit_time), + CopyStringToTimestamp('1601-01-02 00:00:00')) + + # WebKit time that exceeds lower bound. + webkit_time = -((1 << 63L) - 1) + self.assertEquals(timelib.Timestamp.FromWebKitTime(webkit_time), 0) + + def testTimestampFromFiletime(self): + """Test the FILETIME conversion.""" + self.assertEquals( + timelib.Timestamp.FromFiletime(0x01cb3a623d0a17ce), + CopyStringToTimestamp('2010-08-12 21:06:31.546875')) + + filetime = 86400 * 10000000 + self.assertEquals( + timelib.Timestamp.FromFiletime(filetime), + CopyStringToTimestamp('1601-01-02 00:00:00')) + + # FILETIME that exceeds lower bound. + filetime = -1 + self.assertEquals(timelib.Timestamp.FromFiletime(filetime), 0) + + def testTimestampFromPosixTime(self): + """Test the POSIX time conversion.""" + self.assertEquals( + timelib.Timestamp.FromPosixTime(1281647191), + CopyStringToTimestamp('2010-08-12 21:06:31')) + + self.assertEquals( + timelib.Timestamp.FromPosixTime(-122557518), + timelib.Timestamp.FromTimeString('1966-02-12 1966 12:14:42 UTC')) + + # POSIX time that exceeds upper bound. + self.assertEquals(timelib.Timestamp.FromPosixTime(9223372036855), 0) + + # POSIX time that exceeds lower bound. + self.assertEquals(timelib.Timestamp.FromPosixTime(-9223372036855), 0) + + def testMonthDict(self): + """Test the month dict, both inside and outside of scope.""" + self.assertEquals(timelib.MONTH_DICT['nov'], 11) + self.assertEquals(timelib.MONTH_DICT['jan'], 1) + self.assertEquals(timelib.MONTH_DICT['may'], 5) + + month = timelib.MONTH_DICT.get('doesnotexist') + self.assertEquals(month, None) + + def testLocaltimeToUTC(self): + """Test the localtime to UTC conversion.""" + timezone = pytz.timezone('CET') + + local_timestamp = CopyStringToTimestamp('2013-01-01 01:00:00') + self.assertEquals( + timelib.Timestamp.LocaltimeToUTC(local_timestamp, timezone), + CopyStringToTimestamp('2013-01-01 00:00:00')) + + local_timestamp = CopyStringToTimestamp('2013-07-01 02:00:00') + self.assertEquals( + timelib.Timestamp.LocaltimeToUTC(local_timestamp, timezone), + CopyStringToTimestamp('2013-07-01 00:00:00')) + + # In the local timezone this is a non-existent timestamp. + local_timestamp = CopyStringToTimestamp('2013-03-31 02:00:00') + with self.assertRaises(pytz.NonExistentTimeError): + timelib.Timestamp.LocaltimeToUTC(local_timestamp, timezone, is_dst=None) + + self.assertEquals( + timelib.Timestamp.LocaltimeToUTC( + local_timestamp, timezone, is_dst=True), + CopyStringToTimestamp('2013-03-31 00:00:00')) + + self.assertEquals( + timelib.Timestamp.LocaltimeToUTC( + local_timestamp, timezone, is_dst=False), + CopyStringToTimestamp('2013-03-31 01:00:00')) + + # In the local timezone this is an ambiguous timestamp. + local_timestamp = CopyStringToTimestamp('2013-10-27 02:30:00') + + with self.assertRaises(pytz.AmbiguousTimeError): + timelib.Timestamp.LocaltimeToUTC(local_timestamp, timezone, is_dst=None) + + self.assertEquals( + timelib.Timestamp.LocaltimeToUTC( + local_timestamp, timezone, is_dst=True), + CopyStringToTimestamp('2013-10-27 00:30:00')) + + self.assertEquals( + timelib.Timestamp.LocaltimeToUTC(local_timestamp, timezone), + CopyStringToTimestamp('2013-10-27 01:30:00')) + + # Use the UTC timezone. + self.assertEquals( + timelib.Timestamp.LocaltimeToUTC(local_timestamp, pytz.utc), + local_timestamp) + + # Use a timezone in the Western Hemisphere. + timezone = pytz.timezone('EST') + + local_timestamp = CopyStringToTimestamp('2013-01-01 00:00:00') + self.assertEquals( + timelib.Timestamp.LocaltimeToUTC(local_timestamp, timezone), + CopyStringToTimestamp('2013-01-01 05:00:00')) + + def testCopyToDatetime(self): + """Test the copy to datetime object.""" + timezone = pytz.timezone('CET') + + timestamp = CopyStringToTimestamp('2013-03-14 20:20:08.850041') + self.assertEquals( + timelib.Timestamp.CopyToDatetime(timestamp, timezone), + datetime.datetime(2013, 3, 14, 21, 20, 8, 850041, tzinfo=timezone)) + + def testCopyToPosix(self): + """Test converting microseconds to seconds.""" + timestamp = CopyStringToTimestamp('2013-10-01 12:00:00') + self.assertEquals( + timelib.Timestamp.CopyToPosix(timestamp), + timestamp // 1000000) + + def testTimestampFromTimeString(self): + """The the FromTimeString function.""" + # Test daylight savings. + expected_timestamp = CopyStringToTimestamp('2013-10-01 12:00:00') + + # Check certain variance of this timestamp. + timestamp = timelib.Timestamp.FromTimeString( + '2013-10-01 14:00:00', pytz.timezone('Europe/Rome')) + self.assertEquals(timestamp, expected_timestamp) + + timestamp = timelib.Timestamp.FromTimeString( + '2013-10-01 12:00:00', pytz.timezone('UTC')) + self.assertEquals(timestamp, expected_timestamp) + + timestamp = timelib.Timestamp.FromTimeString( + '2013-10-01 05:00:00', pytz.timezone('PST8PDT')) + self.assertEquals(timestamp, expected_timestamp) + + # Now to test outside of the daylight savings. + expected_timestamp = CopyStringToTimestamp('2014-02-01 12:00:00') + + timestamp = timelib.Timestamp.FromTimeString( + '2014-02-01 13:00:00', pytz.timezone('Europe/Rome')) + self.assertEquals(timestamp, expected_timestamp) + + timestamp = timelib.Timestamp.FromTimeString( + '2014-02-01 12:00:00', pytz.timezone('UTC')) + self.assertEquals(timestamp, expected_timestamp) + + timestamp = timelib.Timestamp.FromTimeString( + '2014-02-01 04:00:00', pytz.timezone('PST8PDT')) + self.assertEquals(timestamp, expected_timestamp) + + # Define two timestamps, one being GMT and the other UTC. + time_string_utc = 'Wed 05 May 2010 03:52:31 UTC' + time_string_gmt = 'Wed 05 May 2010 03:52:31 GMT' + + timestamp_utc = timelib.Timestamp.FromTimeString(time_string_utc) + timestamp_gmt = timelib.Timestamp.FromTimeString(time_string_gmt) + + # Test if these two are different, and if so, then we'll try again + # using the 'gmt_is_utc' flag, which then should result to the same + # results. + if timestamp_utc != timestamp_gmt: + self.assertEquals(timestamp_utc, timelib.Timestamp.FromTimeString( + time_string_gmt, gmt_as_timezone=False)) + + def testRoundTimestamp(self): + """Test the RoundToSeconds function.""" + # Should be rounded up. + test_one = 442813351785412 + # Should be rounded down. + test_two = 1384381247271976 + + self.assertEquals( + timelib.Timestamp.RoundToSeconds(test_one), 442813352000000) + self.assertEquals( + timelib.Timestamp.RoundToSeconds(test_two), 1384381247000000) + + def testTimestampFromTimeParts(self): + """Test the FromTimeParts function.""" + timestamp = timelib.Timestamp.FromTimeParts( + 2013, 6, 25, 22, 19, 46, 0, timezone=pytz.timezone('PST8PDT')) + self.assertEquals( + timestamp, CopyStringToTimestamp('2013-06-25 22:19:46-07:00')) + + timestamp = timelib.Timestamp.FromTimeParts(2013, 6, 26, 5, 19, 46) + self.assertEquals( + timestamp, CopyStringToTimestamp('2013-06-26 05:19:46')) + + timestamp = timelib.Timestamp.FromTimeParts( + 2013, 6, 26, 5, 19, 46, 542) + self.assertEquals( + timestamp, CopyStringToTimestamp('2013-06-26 05:19:46.000542')) + + def _TestStringToDatetime( + self, expected_timestamp, time_string, timezone=pytz.utc, dayfirst=False): + """Tests the StringToDatetime function. + + Args: + expected_timestamp: The expected timesamp. + time_string: String that contains a date and time value. + timezone: The timezone (pytz.timezone) object. + dayfirst: Change precedence of day vs. month. + + Returns: + A result object. + """ + date_time = timelib.StringToDatetime( + time_string, timezone=timezone, dayfirst=dayfirst) + timestamp = int(calendar.timegm((date_time.utctimetuple()))) + self.assertEquals(timestamp, expected_timestamp) + + def testStringToDatetime(self): + """Test the StringToDatetime function.""" + self._TestStringToDatetime( + 471953580, '12-15-1984 05:13:00', timezone=pytz.timezone('EST5EDT')) + + # Swap day and month. + self._TestStringToDatetime( + 466420380, '12-10-1984 05:13:00', timezone=pytz.timezone('EST5EDT'), + dayfirst=True) + + self._TestStringToDatetime(471953580, '12-15-1984 10:13:00Z') + + # Setting the timezone for string that already contains a timezone + # indicator should not affect the conversion. + self._TestStringToDatetime( + 471953580, '12-15-1984 10:13:00Z', timezone=pytz.timezone('EST5EDT')) + + self._TestStringToDatetime(471953580, '15/12/1984 10:13:00Z') + + self._TestStringToDatetime(471953580, '15-12-84 10:13:00Z') + + self._TestStringToDatetime( + 471967980, '15-12-84 10:13:00-04', timezone=pytz.timezone('EST5EDT')) + + self._TestStringToDatetime( + 0, 'thisisnotadatetime', timezone=pytz.timezone('EST5EDT')) + + self._TestStringToDatetime( + 471953580, '12-15-1984 04:13:00', + timezone=pytz.timezone('America/Chicago')) + + self._TestStringToDatetime( + 458712780, '07-14-1984 23:13:00', + timezone=pytz.timezone('America/Chicago')) + + self._TestStringToDatetime( + 471964380, '12-15-1984 05:13:00', timezone=pytz.timezone('US/Pacific')) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/lib/utils.py b/plaso/lib/utils.py new file mode 100644 index 0000000..3e03b70 --- /dev/null +++ b/plaso/lib/utils.py @@ -0,0 +1,199 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains utility functions.""" + +import logging + +from plaso.lib import errors +from plaso.lib import lexer + + +RESERVED_VARIABLES = frozenset( + ['username', 'inode', 'hostname', 'body', 'parser', 'regvalue', 'timestamp', + 'timestamp_desc', 'source_short', 'source_long', 'timezone', 'filename', + 'display_name', 'pathspec', 'offset', 'store_number', 'store_index', + 'tag', 'data_type', 'metadata', 'http_headers', 'query', 'mapped_files', + 'uuid']) + + +def IsText(bytes_in, encoding=None): + """Examine the bytes in and determine if they are indicative of a text. + + Parsers need quick and at least semi reliable method of discovering whether + or not a particular byte stream is a text or resembles text or not. This can + be used in text parsers to determine if a file is a text file or not for + instance. + + The method assumes the byte sequence is either ASCII, UTF-8, UTF-16 or method + supplied character encoding. Otherwise it will make the assumption the byte + sequence is not text, but a byte sequence. + + Args: + bytes_in: The byte sequence passed to the method that needs examination. + encoding: Optional encoding to test, if not defined only ASCII, UTF-8 and + UTF-16 are tried. + + Returns: + Boolean value indicating whether or not the byte sequence is a text or not. + """ + # TODO: Improve speed and accuracy of this method. + # Start with the assumption we are dealing with a text. + is_ascii = True + + # Check if this is ASCII text string. + for char in bytes_in: + if not 31 < ord(char) < 128: + is_ascii = False + break + + # We have an ASCII string. + if is_ascii: + return is_ascii + + # Is this already a unicode text? + if type(bytes_in) == unicode: + return True + + # Check if this is UTF-8 + try: + _ = bytes_in.decode('utf-8') + return True + except UnicodeDecodeError: + pass + + # TODO: UTF 16 decode is successful in too + # many edge cases where we are not really dealing with + # a text at all. Leaving this out for now, consider + # re-enabling or making a better determination. + #try: + # _ = bytes_in.decode('utf-16-le') + # return True + #except UnicodeDecodeError: + # pass + + if encoding: + try: + _ = bytes_in.decode(encoding) + return True + except UnicodeDecodeError: + pass + except LookupError: + logging.error( + u'String encoding not recognized: {0:s}'.format(encoding)) + + return False + + +def GetBaseName(path): + """Returns back a basename for a path (could be Windows or *NIX separated).""" + # First check the case where both forward and backward slash are in the path. + if '/' and '\\' in path: + # Let's count slashes and guess which one is the right one. + forward_count = len(path.split('/')) + backward_count = len(path.split('\\')) + + if forward_count > backward_count: + _, _, base = path.rpartition('/') + else: + _, _, base = path.rpartition('\\') + + return base + + # Now we are sure there is only one type of separators. + if '/' in path: + _, _, base = path.rpartition('/') + else: + _, _, base = path.rpartition('\\') + + return base + + +def GetUnicodeString(string): + """Converts the string to Unicode if necessary.""" + if type(string) != unicode: + return str(string).decode('utf8', 'ignore') + return string + + +class PathReplacer(lexer.Lexer): + """Replace path variables with values gathered from earlier preprocessing.""" + + tokens = [ + lexer.Token('.', '{{([^}]+)}}', 'ReplaceVariable', ''), + lexer.Token('.', '{([^}]+)}', 'ReplaceString', ''), + lexer.Token('.', '([^{])', 'ParseString', ''), + ] + + def __init__(self, pre_obj, data=''): + """Constructor for a path replacer.""" + super(PathReplacer, self).__init__(data) + self._path = [] + self._pre_obj = pre_obj + + def GetPath(self): + """Run the lexer and replace path.""" + while True: + _ = self.NextToken() + if self.Empty(): + break + + return u''.join(self._path) + + def ParseString(self, match, **_): + """Append a string to the path.""" + self._path.append(match.group(1)) + + def ReplaceVariable(self, match, **_): + """Replace a string that should not be a variable.""" + self._path.append(u'{{{0:s}}}'.format(match.group(1))) + + def ReplaceString(self, match, **_): + """Replace a variable with a given attribute.""" + replace = getattr(self._pre_obj, match.group(1), None) + + if replace: + self._path.append(replace) + else: + raise errors.PathNotFound( + u'Path variable: {} not discovered yet.'.format(match.group(1))) + + +def GetInodeValue(inode_raw): + """Read in a 'raw' inode value and try to convert it into an integer. + + Args: + inode_raw: A string or an int inode value. + + Returns: + An integer inode value. + """ + if type(inode_raw) in (int, long): + return inode_raw + + if type(inode_raw) is float: + return int(inode_raw) + + try: + return int(inode_raw) + except ValueError: + # Let's do one more attempt. + inode_string, _, _ = str(inode_raw).partition('-') + try: + return int(inode_string) + except ValueError: + return -1 diff --git a/plaso/lib/utils_test.py b/plaso/lib/utils_test.py new file mode 100644 index 0000000..4de0230 --- /dev/null +++ b/plaso/lib/utils_test.py @@ -0,0 +1,48 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the unit tests for the utils library of methods.""" +import unittest + +from plaso.lib import utils + + +class UtilsTestCase(unittest.TestCase): + """The unit test for utils method collection.""" + + def testIsText(self): + """Test the IsText method.""" + bytes_in = 'this is My Weird ASCII and non whatever string.' + self.assertTrue(utils.IsText(bytes_in)) + + bytes_in = u'Plaso Síar Og Raðar Þessu' + self.assertTrue(utils.IsText(bytes_in)) + + bytes_in = '\x01\62LSO\xFF' + self.assertFalse(utils.IsText(bytes_in)) + + bytes_in = 'T\x00h\x00i\x00s\x00\x20\x00' + self.assertTrue(utils.IsText(bytes_in)) + + bytes_in = 'Ascii\x00' + self.assertTrue(utils.IsText(bytes_in)) + + bytes_in = 'Ascii Start then...\x00\x99\x23' + self.assertFalse(utils.IsText(bytes_in)) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/multi_processing/__init__.py b/plaso/multi_processing/__init__.py new file mode 100644 index 0000000..f462564 --- /dev/null +++ b/plaso/multi_processing/__init__.py @@ -0,0 +1,17 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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/plaso/multi_processing/foreman.py b/plaso/multi_processing/foreman.py new file mode 100644 index 0000000..21c1718 --- /dev/null +++ b/plaso/multi_processing/foreman.py @@ -0,0 +1,332 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a foreman class for monitoring workers.""" + +import collections +import logging + +from plaso.multi_processing import process_info + + +class Foreman(object): + """A foreman class that monitors workers. + + The Foreman is responsible for monitoring worker processes + and give back status information. The status information contains + among other things: + + Number of events extracted from each worker. + + Path of the current file the worker is processing. + + Indications whether the worker is alive or not. + + Memory consumption of the worker. + + This information is gathered using both RPC calls to the worker + itself as well as data provided by the psutil library. + + In the future the Foreman should be able to actively monitor + the health of the processes and terminate and restart processes + that are stuck. + """ + + PROCESS_LABEL = collections.namedtuple('process_label', 'label pid process') + + def __init__(self, show_memory_usage=False): + """Initialize the foreman process. + + Args: + show_memory_usage: Optional boolean value to indicate memory information + should be included in logging. The default is false. + """ + self._last_status_dict = {} + self._process_information = process_info.ProcessInfo() + self._process_labels = [] + self._processing_done = False + self._show_memory_usage = show_memory_usage + + @property + def labels(self): + """Return a list of all currently watched labels.""" + return self._process_labels + + @property + def number_of_processes_in_watch_list(self): + """Return the number of processes in the watch list.""" + return len(self._process_labels) + + def CheckStatus(self, label=None): + """Checks status of either a single process or all from the watch list. + + Args: + label: A process label (instance of PROCESS_LABEL), if not provided + all processes from the watch list are checked. Defaults to None. + """ + if label is not None: + self._CheckStatus(label) + return + + for process_label in self._process_labels: + self._CheckStatus(process_label) + + def GetLabel(self, name=None, pid=None): + """Return a label if found using either name or PID value. + + Args: + name: String value that should match an already existing label. + pid: A process ID (PID) value for a process that is monitored. + + Returns: + A label (instance of PROCESS_LABEL) if found. If neither name + nor pid value is given or the process does not exist a None value + will be returned. + """ + if name is not None: + for process_label in self._process_labels: + if process_label.label == name: + return process_label + + if pid is not None: + for process_label in self._process_labels: + if process_label.pid == pid: + return process_label + + def MonitorWorker(self, label=None, pid=None, name=None): + """Starts monitoring a worker by adding it to the monitor list. + + This function requires either a label to be set or a PID and a process + name. If the label is empty or if both a PID and a name is not provided + the function does nothing, as in no process is added to the list of + workers to monitor (and no indication). + + Args: + label: A process label (instance of PROCESS_LABEL), if not provided + then a pid and a name is required. Defaults to None (if None + then both a pid and name have to be provided). + pid: The process ID (PID) of the worker that should be added to the + monitor list. This is only required if label is not provided. + Defaults to None. This is only used if label is set to None, in + which case it has to be set. + name: The name of the worker process, only required if label is not + provided. Defaults to None, only used if label is set to None, + in which case it has to be set. + """ + if label is None: + if pid is None or name is None: + return + label = self.PROCESS_LABEL(name, pid, process_info.ProcessInfo(pid=pid)) + + if not label: + return + + if label not in self._process_labels: + self._process_labels.append(label) + + def StopMonitoringWorker(self, label=None, pid=None, name=None): + """Stop monitoring a particular worker and remove it from monitor list. + + The purpose of this function is to remove a worker from the list of + monitored workers. In order to do that the function requires either a + label or a pid and a name. + + Args: + label: A process label (instance of PROCESS_LABEL). Defaults to None, and + so then a pid and name are required. + pid: The process ID (PID) of the worker that should no longer be + monitored. This is only required if label is not provided and + defaults to None. + name: The name of the worker process, defaults to None and is only + required if label is not set. + """ + if label is None: + if pid is None or name is None: + return + label = self.PROCESS_LABEL( + name, pid, process_info.ProcessInfo(pid=pid)) + + if label not in self._process_labels: + return + + index = self._process_labels.index(label) + del self._process_labels[index] + logging.info( + u'{0:s} [{1:d}] has been removed from foreman monitoring.'.format( + label.label, label.pid)) + + def SignalEndOfProcessing(self): + """Indicate that processing is done.""" + self._processing_done = True + # TODO: Reconsider this as an info signal. Should this not be moved to + # a debug one? + logging.info( + u'Foreman received a signal indicating that processing is completed.') + + # This function may be called via RPC functions that expects a value to be + # returned. + return True + + def TerminateProcess(self, label=None, pid=None, name=None): + """Terminate a process, even if it is not in the watch list. + + Args: + label: A process label (instance of PROCESS_LABEL), if not provided + then a pid and a name is required. It defaults to None, in which + case you need to provide a pid and/or a name. + pid: The process ID (PID) of the worker. This is only required if label + is not provided and defaults to None. + name: The name of the worker process, only required if label is not + provided and defaults to None. + """ + if label is not None: + self._TerminateProcess(label) + return + + if pid is not None: + for process_label in self._process_labels: + if process_label.pid == pid: + self._TerminateProcess(process_label) + return + + if name is not None: + for process_label in self._process_labels: + if process_label.label == name: + self._TerminateProcess(process_label) + return + + # If we reach here the process is not in our watch list. + if pid is not None and name is not None: + process_label = self.PROCESS_LABEL( + name, pid, process_info.ProcessInfo(pid=pid)) + self._TerminateProcess(process_label) + + def _CheckStatus(self, label): + """Check status for a single process from the watch list. + + This function will take a single label, which describes a worker process + and check if it is alive, call the appropriate functions to log down + information extracted from the worker and if a process is no longer alive + and processing has been marked as done, it will remove the worker from + the list of monitored workers. This function is also reponsible for killing + or terminating a process that is alive and hanging, or not alive while + it should be alive. + + In the future this function will also be responsible for restarting + a worker, or signalling the engine that it needs to spin up a new worker + in the case of a worker dying or being in an effective zombie state. + + Args: + label: A process label (instance of PROCESS_LABEL). + """ + if label not in self._process_labels: + return + + process = label.process + + if process.IsAlive(): + status_dict = process.GetProcessStatus() + if not status_dict and not self._processing_done: + logging.warning(( + u'Unable to connect to RPC socket to: {0:s} at ' + u'http://localhost:{1:d}').format(label.label, label.pid)) + + if status_dict: + self._last_status_dict[label.pid] = status_dict + if status_dict.get('is_running', False): + self._LogWorkerInformation(label, status_dict) + if self._show_memory_usage: + self._LogMemoryUsage(label) + return + else: + logging.info( + u'Process {0:s} [{1:d}] has complete it\'s processing. Total of ' + u'{2:d} events extracted'.format( + label.label, label.pid, status_dict.get('counter', 0))) + + else: + logging.info(u'Process {0:s} [{1:d}] is not alive.'.format( + label.label, label.pid)) + + # Check if this process should be alive. + if self._processing_done: + # This process exited properly and should have. Let's remove it from our + # list of labels. + self.StopMonitoringWorker(label=label) + return + + # We need to terminate the process. + # TODO: Add a function to start a new instance of a worker instead of + # just removing and killing it. + logging.error( + u'Process {0:s} [{1:d}] is not functioning when it should be. ' + u'Terminating it and removing from list.'.format( + label.label, label.pid)) + self._TerminateProcess(label) + + def _LogMemoryUsage(self, label): + """Logs memory information gathered from a process. + + This function will take a label and call the logging infrastructure to + log information about the process's memory information. + + Args: + label: A process label (instance of PROCESS_LABEL). + """ + mem_info = label.process.GetMemoryInformation() + logging.info(( + u'{0:s} - RSS: {1:d}, VMS: {2:d}, Shared: {3:d}, Text: {4:d}, lib: ' + u'{5:d}, data: {6:d}, dirty: {7:d}, Memory Percent: {8:0.2f}%').format( + label.label, mem_info.rss, mem_info.vms, mem_info.shared, + mem_info.text, mem_info.lib, mem_info.data, mem_info.dirty, + mem_info.percent * 100)) + + def _LogWorkerInformation(self, label, status=None): + """Log information gathered from the worker. + + Args: + label: A process label (instance of PROCESS_LABEL). + """ + if status: + logging.info(( + u'{0:s} [{1:d}] - Events Extracted: {2:d} - File ({3:s}) - Running: ' + u'{4!s} <{5:s}>').format( + label.label, label.pid, status.get('counter', -1), + status.get('current_file', u''), status.get('is_running', False), + unicode(label.process.status))) + + def _TerminateProcess(self, label): + """Terminate a process given a process label. + + Attempts to terminate a process and if successful + removes the label from the watch list. + + Args: + label: A process label (instance of PROCESS_LABEL). + """ + if label is None: + return + + label.process.TerminateProcess() + + # Double check the process is dead. + if label.process.IsAlive(): + logging.warning(u'Process {0:s} [{1:d}] is still alive.'.format( + label.label, label.pid)) + elif label.process.status != 'exited': + logging.warning(u'Process {0:s} [{1:d}] may still be alive.'.format( + label.label, label.pid)) + else: + logging.info(u'Process: {0:s} [{1:d}] has been terminated.'.format( + label.label, label.pid)) + self.StopMonitoringWorker(label) diff --git a/plaso/multi_processing/multi_process.py b/plaso/multi_processing/multi_process.py new file mode 100644 index 0000000..709e9db --- /dev/null +++ b/plaso/multi_processing/multi_process.py @@ -0,0 +1,700 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The multi-process processing engine.""" + +import ctypes +import logging +import multiprocessing +import os +import signal +import sys +import threading + +from plaso.engine import collector +from plaso.engine import engine +from plaso.engine import queue +from plaso.engine import worker +from plaso.lib import errors +from plaso.multi_processing import foreman +from plaso.multi_processing import rpc_proxy +from plaso.parsers import context as parsers_context + + +def SigKill(pid): + """Convenience function to issue a SIGKILL or equivalent. + + Args: + pid: The process identifier. + """ + if sys.platform.startswith('win'): + process_terminate = 1 + handle = ctypes.windll.kernel32.OpenProcess( + process_terminate, False, pid) + ctypes.windll.kernel32.TerminateProcess(handle, -1) + ctypes.windll.kernel32.CloseHandle(handle) + + else: + try: + os.kill(pid, signal.SIGKILL) + except OSError as exception: + logging.error( + u'Unable to kill process {0:d} with error: {1:s}'.format( + pid, exception)) + + +class MultiProcessEngine(engine.BaseEngine): + """Class that defines the multi-process engine.""" + + _WORKER_PROCESSES_MINIMUM = 2 + _WORKER_PROCESSES_MAXIMUM = 15 + + def __init__(self, maximum_number_of_queued_items=0): + """Initialize the multi-process engine object. + + Args: + maximum_number_of_queued_items: The maximum number of queued items. + The default is 0, which represents + no limit. + """ + collection_queue = MultiProcessingQueue( + maximum_number_of_queued_items=maximum_number_of_queued_items) + storage_queue = MultiProcessingQueue( + maximum_number_of_queued_items=maximum_number_of_queued_items) + parse_error_queue = MultiProcessingQueue( + maximum_number_of_queued_items=maximum_number_of_queued_items) + + super(MultiProcessEngine, self).__init__( + collection_queue, storage_queue, parse_error_queue) + + self._collection_process = None + self._foreman_object = None + self._storage_process = None + + # TODO: turn into a process pool. + self._worker_processes = {} + + # Attributes for RPC proxy server thread. + self._proxy_thread = None + self._rpc_proxy_server = None + self._rpc_port_number = 0 + + def _StartRPCProxyServerThread(self, foreman_object): + """Starts the RPC proxy server thread. + + Args: + foreman_object: a foreman object (instance of Foreman). + """ + if self._rpc_proxy_server or self._proxy_thread: + return + + self._rpc_proxy_server = rpc_proxy.StandardRpcProxyServer(os.getpid()) + + try: + self._rpc_proxy_server.Open() + self._rpc_proxy_server.RegisterFunction( + 'signal_end_of_collection', foreman_object.SignalEndOfProcessing) + + self._proxy_thread = threading.Thread( + name='rpc_proxy', target=self._rpc_proxy_server.StartProxy) + self._proxy_thread.start() + + self._rpc_port_number = self._rpc_proxy_server.listening_port + + except errors.ProxyFailedToStart as exception: + logging.error(( + u'Unable to setup a RPC server for the engine with error ' + u'{0:s}').format(exception)) + + def _StopRPCProxyServerThread(self): + """Stops the RPC proxy server thread.""" + if not self._rpc_proxy_server or not self._proxy_thread: + return + + # Close the proxy, free up resources so we can shut down the thread. + self._rpc_proxy_server.Close() + + if self._proxy_thread.isAlive(): + self._proxy_thread.join() + + self._proxy_thread = None + self._rpc_proxy_server = None + self._rpc_port_number = 0 + + def CreateCollector( + self, include_directory_stat, vss_stores=None, filter_find_specs=None, + resolver_context=None): + """Creates a collector object. + + The collector discovers all the files that need to be processed by + the workers. Once a file is discovered it is added to the process queue + as a path specification (instance of dfvfs.PathSpec). + + Args: + include_directory_stat: Boolean value to indicate whether directory + stat information should be collected. + vss_stores: Optional list of VSS stores to include in the collection, + where 1 represents the first store. Set to None if no + VSS stores should be processed. The default is None. + filter_find_specs: Optional list of filter find specifications (instances + of dfvfs.FindSpec). The default is None. + resolver_context: Optional resolver context (instance of dfvfs.Context). + The default is None. Note that every thread or process + must have its own resolver context. + + Returns: + A collector object (instance of Collector). + + Raises: + RuntimeError: if source path specification is not set. + """ + if not self._source_path_spec: + raise RuntimeError(u'Missing source.') + + collector_object = collector.Collector( + self._collection_queue, self._source, self._source_path_spec, + resolver_context=resolver_context) + + collector_object.SetCollectDirectoryMetadata(include_directory_stat) + + if vss_stores: + collector_object.SetVssInformation(vss_stores) + + if filter_find_specs: + collector_object.SetFilter(filter_find_specs) + + return collector_object + + def CreateExtractionWorker(self, worker_number): + """Creates an extraction worker object. + + Args: + worker_number: A number that identifies the worker. + + Returns: + An extraction worker (instance of worker.ExtractionWorker). + """ + parser_context = parsers_context.ParserContext( + self._event_queue_producer, self._parse_error_queue_producer, + self.knowledge_base) + + extraction_worker = worker.BaseEventExtractionWorker( + worker_number, self._collection_queue, self._event_queue_producer, + self._parse_error_queue_producer, parser_context) + + extraction_worker.SetEnableDebugOutput(self._enable_debug_output) + + # TODO: move profiler in separate object. + extraction_worker.SetEnableProfiling( + self._enable_profiling, + profiling_sample_rate=self._profiling_sample_rate) + + if self._open_files: + extraction_worker.SetOpenFiles(self._open_files) + + if self._filter_object: + extraction_worker.SetFilterObject(self._filter_object) + + if self._mount_path: + extraction_worker.SetMountPath(self._mount_path) + + if self._text_prepend: + extraction_worker.SetTextPrepend(self._text_prepend) + + return extraction_worker + + def ProcessSource( + self, collector_object, storage_writer, parser_filter_string=None, + number_of_extraction_workers=0, have_collection_process=True, + have_foreman_process=True, show_memory_usage=False): + """Processes the source and extracts event objects. + + Args: + collector_object: A collector object (instance of Collector). + storage_writer: A storage writer object (instance of BaseStorageWriter). + parser_filter_string: Optional parser filter string. The default is None. + number_of_extraction_workers: Optional number of extraction worker + processes. The default is 0 which means + the function will determine the suitable + number. + have_collection_process: Optional boolean value to indidate a separate + collection process should be run. The default + is true. + have_foreman_process: Optional boolean value to indidate a separate + foreman process should be run to make sure the + workers are extracting event objects. The default + is true. + show_memory_usage: Optional boolean value to indicate memory information + should be included in logging. The default is false. + """ + if number_of_extraction_workers < 1: + # One worker for each "available" CPU (minus other processes). + # The number here is derived from the fact that the engine starts up: + # + A collector process (optional). + # + A storage process. + # + # If we want to utilize all CPUs on the system we therefore need to start + # up workers that amounts to the total number of CPUs - the other + # processes. + cpu_count = multiprocessing.cpu_count() - 2 + if have_collection_process: + cpu_count -= 1 + + if cpu_count <= self._WORKER_PROCESSES_MINIMUM: + cpu_count = self._WORKER_PROCESSES_MINIMUM + + elif cpu_count >= self._WORKER_PROCESSES_MAXIMUM: + cpu_count = self._WORKER_PROCESSES_MAXIMUM + + number_of_extraction_workers = cpu_count + + if have_foreman_process: + self._foreman_object = foreman.Foreman( + show_memory_usage=show_memory_usage) + self._StartRPCProxyServerThread(self._foreman_object) + + self._storage_process = MultiProcessStorageProcess( + storage_writer, name='StorageProcess') + self._storage_process.start() + + if have_collection_process: + self._collection_process = MultiProcessCollectionProcess( + collector_object, self._rpc_port_number, name='CollectionProcess') + self._collection_process.start() + + logging.info(u'Starting extraction worker processes.') + for worker_number in range(number_of_extraction_workers): + extraction_worker = self.CreateExtractionWorker(worker_number) + + worker_name = u'Worker_{0:d}'.format(worker_number) + + # TODO: Test to see if a process pool can be a better choice. + worker_process = MultiProcessEventExtractionWorkerProcess( + extraction_worker, parser_filter_string, name=worker_name) + worker_process.start() + + if self._foreman_object: + self._foreman_object.MonitorWorker( + pid=worker_process.pid, name=worker_name) + + self._worker_processes[worker_name] = worker_process + + logging.debug(u'Collection started.') + if not self._collection_process: + collector_object.Collect() + + else: + while self._collection_process.is_alive(): + self._collection_process.join(timeout=10) + + # Check the worker status regularly while collection is still ongoing. + if self._foreman_object: + self._foreman_object.CheckStatus() + + # TODO: We get a signal when collection is done, which might happen + # before the collection thread joins. Look at the option of speeding + # up the process of the collector stopping by potentially killing it. + + logging.info(u'Collection stopped.') + + self._StopProcessing() + + def _StopProcessing(self): + """Stops the foreman and worker processes.""" + if self._foreman_object: + self._foreman_object.SignalEndOfProcessing() + self._StopRPCProxyServerThread() + + # Run through the running workers, one by one. + # This will go through a list of all active worker processes and check it's + # status. If a worker has completed it will be removed from the list. + # The process will not wait longer than five seconds for each worker to + # complete, if longer time passes it will simply check it's status and + # move on. That ensures that worker process is monitored and status is + # updated. + while self._worker_processes: + # Note that self._worker_processes is altered in this loop hence we need + # it to be sorted. + for process_name, process_obj in sorted(self._worker_processes.items()): + if self._foreman_object: + worker_label = self._foreman_object.GetLabel( + name=process_name, pid=process_obj.pid) + else: + worker_label = None + + if not worker_label: + if process_obj.is_alive(): + logging.info(( + u'Process {0:s} [{1:d}] is not monitored by the foreman. Most ' + u'likely due to a worker having completed it\'s processing ' + u'while waiting for another worker to complete.').format( + process_name, process_obj.pid)) + logging.info( + u'Waiting for worker {0:s} to complete.'.format(process_name)) + process_obj.join() + logging.info(u'Worker: {0:s} [{1:d}] has completed.'.format( + process_name, process_obj.pid)) + + del self._worker_processes[process_name] + continue + + if process_obj.is_alive(): + # Check status of worker. + self._foreman_object.CheckStatus(label=worker_label) + process_obj.join(timeout=5) + + # Note that we explicitly must test against exitcode 0 here since + # process.exitcode will be None if there is no exitcode. + elif process_obj.exitcode != 0: + logging.warning(( + u'Worker process: {0:s} already exited with code: ' + u'{1:d}.').format(process_name, process_obj.exitcode)) + process_obj.terminate() + self._foreman_object.TerminateProcess(label=worker_label) + + else: + # Process is no longer alive, no need to monitor. + self._foreman_object.StopMonitoringWorker(label=worker_label) + # Remove it from our list of active workers. + del self._worker_processes[process_name] + + if self._foreman_object: + self._foreman_object = None + + logging.info(u'Extraction workers stopped.') + self._event_queue_producer.SignalEndOfInput() + + self._storage_process.join() + logging.info(u'Storage writer stopped.') + + def _AbortNormal(self, timeout=None): + """Abort in a normal way. + + Args: + timeout: The process join timeout. The default is None meaning + no timeout. + """ + if self._collection_process: + logging.warning(u'Signaling collection process to abort.') + self._collection_process.SignalAbort() + + if self._worker_processes: + logging.warning(u'Signaling worker processes to abort.') + for _, worker_process in self._worker_processes.iteritems(): + worker_process.SignalAbort() + + logging.warning(u'Signaling storage process to abort.') + self._event_queue_producer.SignalEndOfInput() + self._storage_process.SignalAbort() + + if self._collection_process: + logging.warning(u'Waiting for collection process: {0:d}.'.format( + self._collection_process.pid)) + # TODO: it looks like xmlrpclib.ServerProxy is not allowing the + # collection process to close. + self._collection_process.join(timeout=timeout) + + if self._worker_processes: + for worker_name, worker_process in self._worker_processes.iteritems(): + logging.warning(u'Waiting for worker: {0:s} process: {1:d}'.format( + worker_name, worker_process.pid)) + worker_process.join(timeout=timeout) + + if self._storage_process: + logging.warning(u'Waiting for storage process: {0:d}.'.format( + self._collection_process.pid)) + self._storage_process.join(timeout=timeout) + + def _AbortTerminate(self): + """Abort processing by sending SIGTERM or equivalent.""" + if self._collection_process and self._collection_process.is_alive(): + logging.warning(u'Terminating collection process: {0:d}.'.format( + self._collection_process.pid)) + self._collection_process.terminate() + + if self._worker_processes: + for worker_name, worker_process in self._worker_processes.iteritems(): + if worker_process.is_alive(): + logging.warning(u'Terminating worker: {0:s} process: {1:d}'.format( + worker_name, worker_process.pid)) + worker_process.terminate() + + if self._storage_process and self._storage_process.is_alive(): + logging.warning(u'Terminating storage process: {0:d}.'.format( + self._storage_process.pid)) + self._storage_process.terminate() + + def _AbortKill(self): + """Abort processing by sending SIGKILL or equivalent.""" + if self._collection_process and self._collection_process.is_alive(): + logging.warning(u'Killing collection process: {0:d}.'.format( + self._collection_process.pid)) + SigKill(self._collection_process.pid) + + if self._worker_processes: + for worker_name, worker_process in self._worker_processes.iteritems(): + if worker_process.is_alive(): + logging.warning(u'Killing worker: {0:s} process: {1:d}'.format( + worker_name, worker_process.pid)) + SigKill(worker_process.pid) + + if self._storage_process and self._storage_process.is_alive(): + logging.warning(u'Killing storage process: {0:d}.'.format( + self._storage_process.pid)) + SigKill(self._storage_process.pid) + + def SignalAbort(self): + """Signals the engine to abort.""" + super(MultiProcessEngine, self).SignalAbort() + + try: + self._AbortNormal(timeout=2) + self._AbortTerminate() + except KeyboardInterrupt: + self._AbortKill() + + # TODO: remove the need for this. + # Sometimes the main process will be unresponsive. + SigKill(os.getpid()) + + +class MultiProcessCollectionProcess(multiprocessing.Process): + """Class that defines a multi-processing collection process.""" + + def __init__(self, collector_object, rpc_port_number, **kwargs): + """Initializes the process object. + + Args: + collector_object: A collector object (instance of Collector). + rpc_port_number: An integer value containing the RPC end point port + number or 0 if not set. + """ + super(MultiProcessCollectionProcess, self).__init__(**kwargs) + self._collector_object = collector_object + self._rpc_port_number = rpc_port_number + + # This method part of the multiprocessing.Process interface hence its name + # is not following the style guide. + def run(self): + """The main loop.""" + # Prevent the KeyboardInterrupt being raised inside the worker process. + # This will prevent a collection process to generate a traceback + # when interrupted. + signal.signal(signal.SIGINT, signal.SIG_IGN) + + logging.debug(u'Collection process: {0!s} started'.format(self._name)) + + rpc_proxy_client = None + if self._rpc_port_number: + try: + rpc_proxy_client = rpc_proxy.StandardRpcProxyClient( + self._rpc_port_number) + rpc_proxy_client.Open() + + except errors.ProxyFailedToStart as exception: + logging.error(( + u'Unable to setup a RPC client for the collector process with ' + u'error {0:s}').format(exception)) + + self._collector_object.Collect() + + logging.debug(u'Collection process: {0!s} stopped'.format(self._name)) + if rpc_proxy_client: + _ = rpc_proxy_client.GetData(u'signal_end_of_collection') + + def SignalAbort(self): + """Signals the process to abort.""" + self._collector_object.SignalAbort() + + +class MultiProcessEventExtractionWorkerProcess(multiprocessing.Process): + """Class that defines a multi-processing event extraction worker process.""" + + def __init__(self, extraction_worker, parser_filter_string, **kwargs): + """Initializes the process object. + + Args: + extraction_worker: The extraction worker object (instance of + MultiProcessEventExtractionWorker). + parser_filter_string: Optional parser filter string. The default is None. + """ + super(MultiProcessEventExtractionWorkerProcess, self).__init__(**kwargs) + self._extraction_worker = extraction_worker + + # TODO: clean this up with the implementation of a task based + # multi-processing approach. + self._parser_filter_string = parser_filter_string + + # Attributes for RPC proxy server thread. + self._proxy_thread = None + self._rpc_proxy_server = None + + def _StartRPCProxyServerThread(self): + """Starts the RPC proxy server thread.""" + if self._rpc_proxy_server or self._proxy_thread: + return + + # Set up a simple XML RPC server for the worker for status indications. + # Since we don't know the worker's PID for now we'll set the initial port + # number to zero and then adjust it later. + self._rpc_proxy_server = rpc_proxy.StandardRpcProxyServer() + + try: + self._rpc_proxy_server.SetListeningPort(os.getpid()) + self._rpc_proxy_server.Open() + self._rpc_proxy_server.RegisterFunction( + 'status', self._extraction_worker.GetStatus) + + self._proxy_thread = threading.Thread( + name='rpc_proxy', target=self._rpc_proxy_server.StartProxy) + self._proxy_thread.start() + + except errors.ProxyFailedToStart as exception: + logging.error(( + u'Unable to setup a RPC server for the worker: {0:d} [PID {1:d}] ' + u'with error: {2:s}').format( + self._identifier, os.getpid(), exception)) + + def _StopRPCProxyServerThread(self): + """Stops the RPC proxy server thread.""" + if not self._rpc_proxy_server or not self._proxy_thread: + return + + # Close the proxy, free up resources so we can shut down the thread. + self._rpc_proxy_server.Close() + + if self._proxy_thread.isAlive(): + self._proxy_thread.join() + + self._rpc_proxy_server = None + self._proxy_thread = None + + # This method part of the multiprocessing.Process interface hence its name + # is not following the style guide. + def run(self): + """The main loop.""" + # Prevent the KeyboardInterrupt being raised inside the worker process. + # This will prevent a worker process to generate a traceback + # when interrupted. + signal.signal(signal.SIGINT, signal.SIG_IGN) + + # We need to initialize the parser object after the process + # has forked otherwise on Windows the "fork" will fail with + # a PickleError for Python modules that cannot be pickled. + self._extraction_worker.InitalizeParserObjects( + parser_filter_string=self._parser_filter_string) + + logging.debug(u'Worker process: {0!s} started'.format(self._name)) + self._StartRPCProxyServerThread() + + self._extraction_worker.Run() + + logging.debug(u'Worker process: {0!s} stopped'.format(self._name)) + self._StopRPCProxyServerThread() + + def SignalAbort(self): + """Signals the process to abort.""" + self._extraction_worker.SignalAbort() + + +class MultiProcessStorageProcess(multiprocessing.Process): + """Class that defines a multi-processing storage process.""" + + def __init__(self, storage_writer, **kwargs): + """Initializes the process object. + + Args: + storage_writer: A storage writer object (instance of BaseStorageWriter). + """ + super(MultiProcessStorageProcess, self).__init__(**kwargs) + self._storage_writer = storage_writer + + # This method part of the multiprocessing.Process interface hence its name + # is not following the style guide. + def run(self): + """The main loop.""" + # Prevent the KeyboardInterrupt being raised inside the worker process. + # This will prevent a storage process to generate a traceback + # when interrupted. + signal.signal(signal.SIGINT, signal.SIG_IGN) + + logging.debug(u'Storage process: {0!s} started'.format(self._name)) + self._storage_writer.WriteEventObjects() + logging.debug(u'Storage process: {0!s} stopped'.format(self._name)) + + def SignalAbort(self): + """Signals the process to abort.""" + self._storage_writer.SignalAbort() + + +class MultiProcessingQueue(queue.Queue): + """Class that defines the multi-processing queue.""" + + def __init__(self, maximum_number_of_queued_items=0): + """Initializes the multi-processing queue object. + + Args: + maximum_number_of_queued_items: The maximum number of queued items. + The default is 0, which represents + no limit. + """ + super(MultiProcessingQueue, self).__init__() + + # maxsize contains the maximum number of items allowed to be queued, + # where 0 represents unlimited. + # We need to check that we aren't asking for a bigger queue than the + # platform supports, which requires access to this protected member. + # pylint: disable=protected-access + queue_max_length = multiprocessing._multiprocessing.SemLock.SEM_VALUE_MAX + # pylint: enable=protected-access + if maximum_number_of_queued_items > queue_max_length: + logging.warn( + u'Maximum queue size requested ({0:d}) is larger than system ' + u'supported maximum size. Setting queue size to maximum supported ' + u'size, ' + u'({1:d})'.format(maximum_number_of_queued_items, queue_max_length)) + maximum_number_of_queued_items = queue_max_length + self._queue = multiprocessing.Queue( + maxsize=maximum_number_of_queued_items) + + def __len__(self): + """Returns the estimated current number of items in the queue.""" + size = 0 + try: + size = self._queue.qsize() + except NotImplementedError: + logging.warning(( + u'Returning queue length does not work on Mac OS X because of broken ' + u'sem_getvalue()')) + raise + + return size + + def IsEmpty(self): + """Determines if the queue is empty.""" + return self._queue.empty() + + def PushItem(self, item): + """Pushes an item onto the queue.""" + self._queue.put(item) + + def PopItem(self): + """Pops an item off the queue.""" + try: + return self._queue.get() + except KeyboardInterrupt: + raise errors.QueueEmpty diff --git a/plaso/multi_processing/multi_process_test.py b/plaso/multi_processing/multi_process_test.py new file mode 100644 index 0000000..0f5c8cf --- /dev/null +++ b/plaso/multi_processing/multi_process_test.py @@ -0,0 +1,52 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests the multi-process processing engine.""" + +import unittest + +from plaso.engine import test_lib +from plaso.multi_processing import multi_process + + +class MultiProcessingQueueTest(unittest.TestCase): + """Tests the multi-processing queue.""" + + _ITEMS = frozenset(['item1', 'item2', 'item3', 'item4']) + + def testPushPopItem(self): + """Tests the PushItem and PopItem functions.""" + test_queue = multi_process.MultiProcessingQueue() + + for item in self._ITEMS: + test_queue.PushItem(item) + + try: + self.assertEquals(len(test_queue), len(self._ITEMS)) + except NotImplementedError: + # On Mac OS X because of broken sem_getvalue() + return + + test_queue.SignalEndOfInput() + test_queue_consumer = test_lib.TestQueueConsumer(test_queue) + test_queue_consumer.ConsumeItems() + + self.assertEquals(test_queue_consumer.number_of_items, len(self._ITEMS)) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/multi_processing/process_info.py b/plaso/multi_processing/process_info.py new file mode 100644 index 0000000..fc22377 --- /dev/null +++ b/plaso/multi_processing/process_info.py @@ -0,0 +1,259 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a class to get process information.""" + +import collections +import os +import SocketServer + +import psutil + +from plaso.lib import timelib +from plaso.multi_processing import rpc_proxy + + +class ProcessInfo(object): + """Class that provides information about a running process.""" + + _MEMORY_INFORMATION = collections.namedtuple( + 'memory_information', 'rss vms shared text lib data dirty percent') + + def __init__(self, pid=None): + """Initialize the process information object. + + Args: + pid: Process ID (PID) value of the process to monitor. The default value + is None in which case the PID of the calling + process will be used. + + Raises: + IOError: If the pid does not exist. + """ + if pid is None: + self._pid = os.getpid() + else: + self._pid = pid + + if not psutil.pid_exists(self._pid): + raise IOError(u'Unable to read data from pid: {0:d}'.format(self._pid)) + + self._command_line = '' + self._parent = None + self._process = psutil.Process(self._pid) + if getattr(psutil, 'version_info', (0, 0, 0)) < (2, 0, 0): + self._psutil_pre_v2 = True + else: + self._psutil_pre_v2 = False + + # TODO: Allow the client proxy object to determined at run time and not + # a fixed value as here. + self._rpc_client = rpc_proxy.StandardRpcProxyClient(self._pid) + self._rpc_client.Open() + + @property + def pid(self): + """Return the process ID (PID).""" + return self._pid + + @property + def name(self): + """Return the name of the process.""" + if self._psutil_pre_v2: + return self._process.name + + return self._process.name() + + @property + def command_line(self): + """Return the full command line used to start the process.""" + if self._command_line: + return self._command_line + + try: + if self._psutil_pre_v2: + command_lines = self._process.cmdline + else: + command_lines = self._process.cmdline() + + self._command_line = u' '.join(command_lines) + except psutil.NoSuchProcess: + return + + return self._command_line + + @property + def parent(self): + """Return a ProcessInfo object for the parent process.""" + if self._parent is not None: + return self._parent + + try: + if self._psutil_pre_v2: + parent_pid = self._process.parent.pid + else: + parent = self._process.parent() # pylint: disable-msg=not-callable + parent_pid = parent.pid + + self._parent = ProcessInfo(pid=parent_pid) + return self._parent + except psutil.NoSuchProcess: + return + + @property + def open_files(self): + """Yield a list of open files the process has.""" + try: + for open_file in self._process.get_open_files(): + yield open_file.path + except (psutil.AccessDenied, psutil.NoSuchProcess): + return + + @property + def children(self): + """Yield all child processes as a ProcessInfo object.""" + try: + for child in self._process.get_children(): + yield ProcessInfo(pid=child.pid) + except psutil.NoSuchProcess: + # We are creating an empty generator here. Yield or return None + # individually don't provide that behavior, neither does raising + # GeneratorExit or StopIteration. + # pylint: disable=unreachable + return + yield + + @property + def number_of_threads(self): + """Return back the number of threads this process has.""" + try: + return self._process.get_num_threads() + except psutil.NoSuchProcess: + return 0 + + @property + def memory_map(self): + """Yield memory map objects (instance of mmap).""" + try: + for memory_map in self._process.get_memory_maps(): + yield memory_map + except psutil.NoSuchProcess: + # We are creating an empty generator here. Yield or return None + # individually don't provide that behavior, neither does raising + # GeneratorExit or StopIteration. + # pylint: disable=unreachable + return + yield + + @property + def status(self): + """Return the process status.""" + try: + if self._psutil_pre_v2: + return self._process.status + else: + return self._process.status() + except psutil.NoSuchProcess: + return u'exited' + + @property + def start_time(self): + """Return back the start time of the process. + + Returns: + An integer representing the number of microseconds since Unix Epoch time + in UTC. + """ + if self._psutil_pre_v2: + create_time = self._process.create_time + else: + create_time = self._process.create_time() + return timelib.Timestamp.FromPosixTime(int(create_time)) + + @property + def io_counters(self): + """Return back IO Counters for the process.""" + try: + return self._process.get_io_counters() + except psutil.NoSuchProcess: + return + + @property + def cpu_times(self): + """Return back CPU times for the process.""" + try: + return self._process.get_cpu_times() + except psutil.NoSuchProcess: + return + + @property + def cpu_percent(self): + """Return back the percent of CPU processing this process consumes.""" + try: + return self._process.get_cpu_percent() + except psutil.NoSuchProcess: + return + + def GetMemoryInformation(self): + """Return back memory information as a memory_information object. + + Returns: + Memory information object (instance of memory_information) a named + tuple that contains the following attributes: rss, vms, shared, text, + lib, data, dirty, percent. + """ + try: + external_information = self._process.get_ext_memory_info() + except psutil.NoSuchProcess: + return + + percent = self._process.get_memory_percent() + + # Psutil will return different memory information depending on what is + # available in that platform. + # TODO: Not be as strict in what gets returned, have this object more + # flexible so that the memory information returned reflects the available + # information in the platform. + return self._MEMORY_INFORMATION( + getattr(external_information, 'rss', 0), + getattr(external_information, 'vms', 0), + getattr(external_information, 'shared', 0), + getattr(external_information, 'text', 0), + getattr(external_information, 'lib', 0), + getattr(external_information, 'data', 0), + getattr(external_information, 'dirty', 0), percent) + + def GetProcessStatus(self): + """Attempt to connect to process via RPC to gather status information.""" + if self._rpc_client is None: + return + try: + status = self._rpc_client.GetData('status') + if isinstance(status, dict): + return status + except SocketServer.socket.error: + return + + def IsAlive(self): + """Return a boolean value indicating if the process is alive or not.""" + return self._process.is_running() + + def TerminateProcess(self): + """Terminate the process.""" + # TODO: Make sure the process has really been terminated. + if self.IsAlive(): + self._process.terminate() diff --git a/plaso/multi_processing/rpc_proxy.py b/plaso/multi_processing/rpc_proxy.py new file mode 100644 index 0000000..70e54d5 --- /dev/null +++ b/plaso/multi_processing/rpc_proxy.py @@ -0,0 +1,134 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Simple RPC proxy server and client.""" + +import logging +import SimpleXMLRPCServer +import SocketServer +import xmlrpclib + +from xml.parsers import expat + +from plaso.lib import errors +from plaso.lib import proxy + + +class StandardRpcProxyServer(proxy.ProxyServer): + """Class that implements a simple XML RPC based proxy server.""" + + def __init__(self, port=0): + """Initializes the RPC proxy server object. + + Args: + port: The port number the proxy should listen on. Defaults to 0. + """ + super(StandardRpcProxyServer, self).__init__( + proxy.GetProxyPortNumberFromPID(port)) + self._proxy = None + + def Close(self): + """Close the proxy object.""" + if not self._proxy: + return + self._proxy.shutdown() + self._proxy = None + + def Open(self): + """Set up the proxy so that it can be started.""" + try: + self._proxy = SimpleXMLRPCServer.SimpleXMLRPCServer( + ('localhost', self.listening_port), logRequests=False, + allow_none=True) + except SocketServer.socket.error as exception: + raise errors.ProxyFailedToStart( + u'Unable to setup a RPC server for listening to port: {0:d} with ' + u'error: {1:s}'.format(self.listening_port, exception)) + + def SetListeningPort(self, new_port_number): + """Change the port number the proxy listens to.""" + # We don't want to change the port after the proxy has been started. + if self._proxy: + logging.warning( + u'Unable to change proxy ports for an already started proxy.') + return + + self._port_number = proxy.GetProxyPortNumberFromPID(new_port_number) + + def StartProxy(self): + """Start the proxy.""" + if not self._proxy: + raise errors.ProxyFailedToStart(u'Proxy not set up yet.') + self._proxy.serve_forever() + + def RegisterFunction(self, function_name, function): + """Register a function to this RPC proxy. + + Args: + function_name: The name of the proxy function. + function: Callback method to the function providing the requested + information. + """ + if not self._proxy: + raise errors.ProxyFailedToStart(( + u'Unable to register a function for a proxy that has not been set ' + u'up yet.')) + self._proxy.register_function(function, function_name) + + +class StandardRpcProxyClient(proxy.ProxyClient): + """Class that implements a simple XML RPC based proxy client.""" + + def __init__(self, port=0): + """Initializes the RPC proxy client object. + + Args: + port: The port number the proxy should connect to. Defaults to 0. + """ + super(StandardRpcProxyClient, self).__init__( + proxy.GetProxyPortNumberFromPID(port)) + self._proxy = None + + def Open(self): + """Set up the proxy so that it can be started.""" + try: + self._proxy = xmlrpclib.ServerProxy( + u'http://localhost:{0:d}'.format(self._port_number), allow_none=True) + except SocketServer.socket.error: + self._proxy = None + + def GetData(self, call_back_name): + """Return back data from the RPC proxy using a callback method. + + Args: + call_back_name: The name of the callback method that the RPC proxy + supports. + + Returns: + The data returned back by the callback method. + """ + if self._proxy is None: + return + + call_back = getattr(self._proxy, call_back_name, None) + if call_back is None: + return + + try: + return call_back() + except (SocketServer.socket.error, expat.ExpatError): + return diff --git a/plaso/output/__init__.py b/plaso/output/__init__.py new file mode 100644 index 0000000..76ca9b8 --- /dev/null +++ b/plaso/output/__init__.py @@ -0,0 +1,34 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains an import statement for each output plugin.""" + +from plaso.output import dynamic +try: + from plaso.output import elastic +except ImportError: + pass +from plaso.output import json_out +from plaso.output import l2t_csv +from plaso.output import l2t_tln +try: + from plaso.output import mysql_4n6 +except ImportError: + pass +from plaso.output import pstorage +from plaso.output import rawpy +from plaso.output import sqlite_4n6 +from plaso.output import tln diff --git a/plaso/output/dynamic.py b/plaso/output/dynamic.py new file mode 100644 index 0000000..b4a8f66 --- /dev/null +++ b/plaso/output/dynamic.py @@ -0,0 +1,300 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains a formatter for a dynamic output module for plaso.""" + +import logging +import re + +from plaso.formatters import manager as formatters_manager +from plaso.lib import errors +from plaso.lib import output +from plaso.lib import timelib +from plaso.output import helper + + +class Dynamic(output.FileLogOutputFormatter): + """Dynamic selection of fields for a separated value output format.""" + + FORMAT_ATTRIBUTE_RE = re.compile('{([^}]+)}') + + # A dict containing mappings between "special" attributes and + # how they should be calculated and presented. + # They should be documented here: + # http://plaso.kiddaland.net/usage/psort/output + SPECIAL_HANDLING = { + 'date': 'ParseDate', + 'datetime': 'ParseDateTime', + 'description': 'ParseMessage', + 'description_short': 'ParseMessageShort', + 'host': 'ParseHostname', + 'hostname': 'ParseHostname', + 'inode': 'ParseInode', + 'macb': 'ParseMacb', + 'message': 'ParseMessage', + 'message_short': 'ParseMessageShort', + 'source': 'ParseSourceShort', + 'sourcetype': 'ParseSource', + 'source_long': 'ParseSource', + 'tag': 'ParseTag', + 'time': 'ParseTime', + 'timezone': 'ParseZone', + 'type': 'ParseTimestampDescription', + 'user': 'ParseUsername', + 'username': 'ParseUsername', + 'zone': 'ParseZone', + } + + def ParseTimestampDescription(self, event_object): + """Return the timestamp description.""" + return getattr(event_object, 'timestamp_desc', '-') + + def ParseTag(self, event_object): + """Return tagging information.""" + tag = getattr(event_object, 'tag', None) + + if not tag: + return u'-' + + return u' '.join(tag.tags) + + def ParseSource(self, event_object): + """Return the source string.""" + # TODO: move this to an output module interface. + event_formatter = formatters_manager.EventFormatterManager.GetFormatter( + event_object) + if not event_formatter: + raise errors.NoFormatterFound( + u'Unable to find no event formatter for: {0:s}.'.format( + event_object.DATA_TYPE)) + + _, source = event_formatter.GetSources(event_object) + return source + + def ParseSourceShort(self, event_object): + """Return the source string.""" + # TODO: move this to an output module interface. + event_formatter = formatters_manager.EventFormatterManager.GetFormatter( + event_object) + if not event_formatter: + raise errors.NoFormatterFound( + u'Unable to find no event formatter for: {0:s}.'.format( + event_object.DATA_TYPE)) + + source, _ = event_formatter.GetSources(event_object) + return source + + def ParseZone(self, _): + """Return a timezone.""" + return self.zone + + def ParseDate(self, event_object): + """Return a date string from a timestamp value.""" + try: + date_use = timelib.Timestamp.CopyToDatetime( + event_object.timestamp, self.zone, raise_error=True) + except OverflowError as exception: + logging.error(( + u'Unable to copy {0:d} into a human readable timestamp with error: ' + u'{1:s}. Event {2:d}:{3:d} triggered the exception.').format( + event_object.timestamp, exception, + getattr(event_object, 'store_number', u''), + getattr(event_object, 'store_index', u''))) + return u'0000-00-00' + return u'{0:04d}-{1:02d}-{2:02d}'.format( + date_use.year, date_use.month, date_use.day) + + def ParseDateTime(self, event_object): + """Return a datetime object from a timestamp, in an ISO format.""" + try: + return timelib.Timestamp.CopyToIsoFormat( + event_object.timestamp, timezone=self.zone, raise_error=True) + + except OverflowError as exception: + logging.error(( + u'Unable to copy {0:d} into a human readable timestamp with error: ' + u'{1:s}. Event {2:d}:{3:d} triggered the exception.').format( + event_object.timestamp, exception, + getattr(event_object, 'store_number', u''), + getattr(event_object, 'store_index', u''))) + return u'0000-00-00T00:00:00' + + def ParseTime(self, event_object): + """Return a timestamp string from an integer timestamp value.""" + try: + date_use = timelib.Timestamp.CopyToDatetime( + event_object.timestamp, self.zone, raise_error=True) + except OverflowError as exception: + logging.error(( + u'Unable to copy {0:d} into a human readable timestamp with error: ' + u'{1:s}. Event {2:d}:{3:d} triggered the exception.').format( + event_object.timestamp, exception, + getattr(event_object, 'store_number', u''), + getattr(event_object, 'store_index', u''))) + return u'00:00:00' + return u'{0:02d}:{1:02d}:{2:02d}'.format( + date_use.hour, date_use.minute, date_use.second) + + def ParseHostname(self, event_object): + """Return a hostname.""" + hostname = getattr(event_object, 'hostname', '') + if self.store: + if not hostname: + hostname = self._hostnames.get(event_object.store_number, '-') + + return hostname + + # TODO: move this into a base output class. + def ParseUsername(self, event_object): + """Determines an username based on an event and extracted information. + + Uses the extracted information from the pre processing information and the + event object itself to determine an username. + + Args: + event_object: The event object (instance of EventObject). + + Returns: + An Unicode string containing the username, or - if none found. + """ + username = getattr(event_object, u'username', u'-') + if self.store: + pre_obj = self._preprocesses.get(event_object.store_number) + if pre_obj: + check_user = pre_obj.GetUsernameById(username) + + if check_user != u'-': + username = check_user + + if username == '-' and hasattr(event_object, u'user_sid'): + if not pre_obj: + return getattr(event_object, u'user_sid', u'-') + + return pre_obj.GetUsernameById( + getattr(event_object, u'user_sid', u'-')) + + return username + + def ParseMessage(self, event_object): + """Return the message string from the EventObject. + + Args: + event_object: The event object (EventObject). + + Raises: + errors.NoFormatterFound: If no formatter for that event is found. + """ + # TODO: move this to an output module interface. + event_formatter = formatters_manager.EventFormatterManager.GetFormatter( + event_object) + if not event_formatter: + raise errors.NoFormatterFound( + u'Unable to find no event formatter for: {0:s}.'.format( + event_object.DATA_TYPE)) + + msg, _ = event_formatter.GetMessages(event_object) + return msg + + def ParseMessageShort(self, event_object): + """Return the message string from the EventObject. + + Args: + event_object: The event object (EventObject). + + Raises: + errors.NoFormatterFound: If no formatter for that event is found. + """ + # TODO: move this to an output module interface. + event_formatter = formatters_manager.EventFormatterManager.GetFormatter( + event_object) + if not event_formatter: + raise errors.NoFormatterFound( + u'Unable to find no event formatter for: {0:s}.'.format( + event_object.DATA_TYPE)) + + _, msg_short = event_formatter.GetMessages(event_object) + return msg_short + + def ParseInode(self, event_object): + """Return an inode number.""" + inode = getattr(event_object, 'inode', '-') + if inode == '-': + if hasattr(event_object, 'pathspec') and hasattr( + event_object.pathspec, 'image_inode'): + inode = event_object.pathspec.image_inode + + return inode + + def ParseMacb(self, event_object): + """Return a legacy MACB representation.""" + return helper.GetLegacy(event_object) + + def Start(self): + """Returns a header for the output.""" + # Start by finding out which fields are to be used. + self.fields = [] + + if self._filter: + self.fields = self._filter.fields + self.separator = self._filter.separator + else: + self.separator = u',' + + if not self.fields: + # TODO: Evaluate which fields should be included by default. + self.fields = [ + 'datetime', 'timestamp_desc', 'source', 'source_long', + 'message', 'parser', 'display_name', 'tag', 'store_number', + 'store_index'] + + if self.store: + self._hostnames = helper.BuildHostDict(self.store) + self._preprocesses = {} + for info in self.store.GetStorageInformation(): + if hasattr(info, 'store_range'): + for store_number in range(info.store_range[0], info.store_range[1]): + self._preprocesses[store_number] = info + + self.filehandle.WriteLine('{0:s}\n'.format( + self.separator.join(self.fields))) + + def WriteEvent(self, event_object): + """Write a single event.""" + try: + self.EventBody(event_object) + except errors.NoFormatterFound: + logging.error(u'Unable to output line, no formatter found.') + logging.error(event_object) + + def EventBody(self, event_object): + """Formats data as "dynamic" CSV and writes to the filehandle.""" + row = [] + for field in self.fields: + has_call_back = self.SPECIAL_HANDLING.get(field, None) + call_back = None + if has_call_back: + call_back = getattr(self, has_call_back, None) + + if call_back: + row.append(call_back(event_object)) + else: + row.append(getattr(event_object, field, u'-')) + + out_write = u'{0:s}\n'.format( + self.separator.join(unicode(x).replace( + self.separator, u' ') for x in row)) + self.filehandle.WriteLine(out_write) diff --git a/plaso/output/dynamic_test.py b/plaso/output/dynamic_test.py new file mode 100644 index 0000000..57082fd --- /dev/null +++ b/plaso/output/dynamic_test.py @@ -0,0 +1,131 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for plaso.output.l2t_csv.""" + +import StringIO +import unittest + +from plaso.formatters import interface as formatters_interface +from plaso.lib import event +from plaso.lib import eventdata +from plaso.output import dynamic + + +class TestEvent(event.EventObject): + DATA_TYPE = 'test:dynamic' + + def __init__(self): + super(TestEvent, self).__init__() + self.timestamp = 1340821021000000 + self.timestamp_desc = eventdata.EventTimestamp.CHANGE_TIME + self.hostname = 'ubuntu' + self.filename = 'log/syslog.1' + self.text = ( + u'Reporter PID: 8442 (pam_unix(cron:session): session\n ' + u'closed for user root)') + + +class TestEventFormatter(formatters_interface.EventFormatter): + DATA_TYPE = 'test:dynamic' + FORMAT_STRING = u'{text}' + + SOURCE_SHORT = 'LOG' + SOURCE_LONG = 'Syslog' + + +class FakeFilter(object): + """Provide a fake filter, that defines which fields to use.""" + + def __init__(self, fields, separator=u','): + self.fields = fields + self.separator = separator + + +class DynamicTest(unittest.TestCase): + """Test the dynamic output module.""" + + def testHeader(self): + output = StringIO.StringIO() + formatter = dynamic.Dynamic(None, output) + correct_line = ( + 'datetime,timestamp_desc,source,source_long,message,parser,' + 'display_name,tag,store_number,store_index\n') + + formatter.Start() + self.assertEquals(output.getvalue(), correct_line) + + output = StringIO.StringIO() + formatter = dynamic.Dynamic(None, output, filter_use=FakeFilter( + ['date', 'time', 'message', 'hostname', 'filename', 'some_stuff'])) + + correct_line = 'date,time,message,hostname,filename,some_stuff\n' + formatter.Start() + self.assertEquals(output.getvalue(), correct_line) + + output = StringIO.StringIO() + formatter = dynamic.Dynamic(None, output, filter_use=FakeFilter( + ['date', 'time', 'message', 'hostname', 'filename', 'some_stuff'], + '@')) + + correct_line = 'date@time@message@hostname@filename@some_stuff\n' + formatter.Start() + self.assertEquals(output.getvalue(), correct_line) + + def testEventBody(self): + """Test ensures that returned lines returned are fmt CSV as expected.""" + event_object = TestEvent() + output = StringIO.StringIO() + + formatter = dynamic.Dynamic(None, output, filter_use=FakeFilter( + ['date', 'time', 'timezone', 'macb', 'source', 'sourcetype', 'type', + 'user', 'host', 'message_short', 'message', 'filename', + 'inode', 'notes', 'format', 'extra'])) + + formatter.Start() + header = ( + 'date,time,timezone,macb,source,sourcetype,type,user,host,' + 'message_short,message,filename,inode,notes,format,extra\n') + self.assertEquals(output.getvalue(), header) + + formatter.EventBody(event_object) + correct = ( + '2012-06-27,18:17:01,UTC,..C.,LOG,Syslog,Metadata Modification Time,-,' + 'ubuntu,Reporter PID: 8442 (pam_unix(cron:session): session ' + 'closed for user root),Reporter PID: 8442 ' + '(pam_unix(cron:session): session closed for user root),log/syslog.1' + ',-,-,-,-\n') + self.assertEquals(output.getvalue(), header + correct) + + output = StringIO.StringIO() + formatter = dynamic.Dynamic(None, output, filter_use=FakeFilter( + ['datetime', 'nonsense', 'hostname', 'message'])) + + header = 'datetime,nonsense,hostname,message\n' + formatter.Start() + self.assertEquals(output.getvalue(), header) + + correct = ( + '2012-06-27T18:17:01+00:00,-,ubuntu,Reporter PID: 8442' + ' (pam_unix(cron:session): session closed for user root)\n') + + formatter.EventBody(event_object) + self.assertEquals(output.getvalue(), header + correct) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/output/elastic.py b/plaso/output/elastic.py new file mode 100644 index 0000000..554e721 --- /dev/null +++ b/plaso/output/elastic.py @@ -0,0 +1,235 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 output module that saves data into an ElasticSearch database.""" + +import logging +import requests +import sys +import uuid + +import pyelasticsearch + +from plaso.formatters import manager as formatters_manager +from plaso.lib import output +from plaso.lib import timelib +from plaso.output import helper + + +class Elastic(output.LogOutputFormatter): + """Saves the events into an ElasticSearch database.""" + + # Add configuration data for this output module. + ARGUMENTS = [ + ('--case_name', { + 'dest': 'case_name', + 'type': unicode, + 'help': 'Add a case name. This will be the name of the index in ' + 'ElasticSearch.', + 'action': 'store', + 'default': ''}), + ('--document_type', { + 'dest': 'document_type', + 'type': unicode, + 'help': 'Name of the document type. This is the name of the document ' + 'type that will be used in ElasticSearch.', + 'action': 'store', + 'default': ''}), + ('--elastic_server_ip', { + 'dest': 'elastic_server', + 'type': unicode, + 'help': ( + 'If the ElasticSearch database resides on a different server ' + 'than localhost this parameter needs to be passed in. This ' + 'should be the IP address or the hostname of the server.'), + 'action': 'store', + 'default': '127.0.0.1'}), + ('--elastic_port', { + 'dest': 'elastic_port', + 'type': int, + 'help': ( + 'By default ElasticSearch uses the port number 9200, if the ' + 'database is listening on a different port this parameter ' + 'can be defined.'), + 'action': 'store', + 'default': 9200})] + + def __init__( + self, store, filehandle=sys.stdout, config=None, filter_use=None): + """Initializes the Elastic output module.""" + super(Elastic, self).__init__(store, filehandle, config, filter_use) + self._counter = 0 + self._data = [] + # TODO: move this to an output module interface. + self._formatters_manager = formatters_manager.EventFormatterManager + + elastic_host = getattr(config, 'elastic_server', '127.0.0.1') + elastic_port = getattr(config, 'elastic_port', 9200) + self._elastic_db = pyelasticsearch.ElasticSearch( + u'http://{0:s}:{1:d}'.format(elastic_host, elastic_port)) + + case_name = getattr(config, 'case_name', u'') + document_type = getattr(config, 'document_type', u'') + + # case_name becomes the index name in Elastic. + if case_name: + self._index_name = case_name.lower() + else: + self._index_name = uuid.uuid4().hex + + # Name of the doc_type that holds the plaso events. + if document_type: + self._doc_type = document_type.lower() + else: + self._doc_type = u'event' + + # Build up a list of available hostnames in this storage file. + self._hostnames = {} + self._preprocesses = {} + + def _EventToDict(self, event_object): + """Returns a dict built from an EventObject.""" + ret_dict = event_object.GetValues() + + # Get rid of few attributes that cause issues (and need correcting). + if 'pathspec' in ret_dict: + del ret_dict['pathspec'] + + #if 'tag' in ret_dict: + # del ret_dict['tag'] + # tag = getattr(event_object, 'tag', None) + # if tag: + # tags = tag.tags + # ret_dict['tag'] = tags + # if getattr(tag, 'comment', ''): + # ret_dict['comment'] = tag.comment + ret_dict['tag'] = [] + + # To not overload the index, remove the regvalue index. + if 'regvalue' in ret_dict: + del ret_dict['regvalue'] + + # Adding attributes in that are calculated/derived. + # We want to remove millisecond precision (causes some issues in + # conversion). + ret_dict['datetime'] = timelib.Timestamp.CopyToIsoFormat( + timelib.Timestamp.RoundToSeconds(event_object.timestamp), + timezone=self.zone) + msg, _ = self._formatters_manager.GetMessageStrings(event_object) + ret_dict['message'] = msg + + source_type, source = self._formatters_manager.GetSourceStrings( + event_object) + + ret_dict['source_short'] = source_type + ret_dict['source_long'] = source + + hostname = getattr(event_object, 'hostname', '') + if self.store and not not hostname: + hostname = self._hostnames.get(event_object.store_number, '-') + + ret_dict['hostname'] = hostname + + # TODO: move this into a base output class. + username = getattr(event_object, 'username', '-') + if self.store: + pre_obj = self._preprocesses.get(event_object.store_number) + if pre_obj: + check_user = pre_obj.GetUsernameById(username) + + if check_user != '-': + username = check_user + + if username == '-' and hasattr(event_object, 'user_sid'): + username = getattr(event_object, 'user_sid', '-') + + ret_dict['username'] = username + + return ret_dict + + def EventBody(self, event_object): + """Prints out to a filehandle string representation of an EventObject. + + Each EventObject contains both attributes that are considered "reserved" + and others that aren't. The 'raw' representation of the object makes a + distinction between these two types as well as extracting the format + strings from the object. + + Args: + event_object: The EventObject. + """ + self._data.append(self._EventToDict(event_object)) + self._counter += 1 + + # Check if we need to flush. + if self._counter % 5000 == 0: + self._elastic_db.bulk_index(self._index_name, self._doc_type, self._data) + self._data = [] + sys.stdout.write('.') + sys.stdout.flush() + + def Start(self): + """Create the necessary mapping.""" + if self.store: + self._hostnames = helper.BuildHostDict(self.store) + for info in self.store.GetStorageInformation(): + if hasattr(info, 'store_range'): + for store_number in range(info.store_range[0], info.store_range[1]): + self._preprocesses[store_number] = info + + mapping = { + self._doc_type: { + u'_timestamp': { + u'enabled': True, + u'path': 'datetime', + u'format': 'date_time_no_millis'}, + } + } + # Check if the mappings exist (only create if not there). + try: + old_mapping_index = self._elastic_db.get_mapping(self._index_name) + old_mapping = old_mapping_index.get(self._index_name, {}) + if self._doc_type not in old_mapping: + self._elastic_db.put_mapping( + self._index_name, self._doc_type, mapping=mapping) + except (pyelasticsearch.ElasticHttpNotFoundError, + pyelasticsearch.exceptions.ElasticHttpError): + try: + self._elastic_db.create_index(self._index_name, settings={ + 'mappings': mapping}) + except pyelasticsearch.IndexAlreadyExistsError: + raise RuntimeError(u'Unable to created the index') + except requests.exceptions.ConnectionError as exception: + logging.error( + u'Unable to proceed, cannot connect to ElasticSearch backend ' + u'with error: {0:s}.\nPlease verify connection.'.format(exception)) + raise RuntimeError(u'Unable to connect to ElasticSearch backend.') + + # pylint: disable=unexpected-keyword-arg + self._elastic_db.health(wait_for_status='yellow') + + sys.stdout.write('Inserting data') + sys.stdout.flush() + + def End(self): + """Flush on last time.""" + self._elastic_db.bulk_index(self._index_name, self._doc_type, self._data) + self._data = [] + sys.stdout.write('. [DONE]\n') + sys.stdout.write('ElasticSearch index name: {0:s}\n'.format( + self._index_name)) + sys.stdout.flush() diff --git a/plaso/output/helper.py b/plaso/output/helper.py new file mode 100644 index 0000000..73af432 --- /dev/null +++ b/plaso/output/helper.py @@ -0,0 +1,109 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains helper functions for output modules.""" + +from plaso.lib import eventdata + + +def GetLegacy(evt): + """Return a legacy MACB representation of the event.""" + # TODO: Fix this function when the MFT parser has been implemented. + # The filestat parser is somewhat limited. + # Also fix this when duplicate entries have been implemented so that + # the function actually returns more than a single entry (as in combined). + if evt.data_type.startswith('fs:'): + letter = evt.timestamp_desc[0] + + if letter == 'm': + return 'M...' + elif letter == 'a': + return '.A..' + elif letter == 'c': + if evt.timestamp_desc[1] == 'r': + return '...B' + + return '..C.' + else: + return '....' + + # Access time. + if evt.timestamp_desc in [ + eventdata.EventTimestamp.ACCESS_TIME, + eventdata.EventTimestamp.ACCOUNT_CREATED, + eventdata.EventTimestamp.PAGE_VISITED, + eventdata.EventTimestamp.LAST_VISITED_TIME, + eventdata.EventTimestamp.START_TIME, + eventdata.EventTimestamp.LAST_SHUTDOWN, + eventdata.EventTimestamp.LAST_LOGIN_TIME, + eventdata.EventTimestamp.LAST_PASSWORD_RESET, + eventdata.EventTimestamp.LAST_CONNECTED, + eventdata.EventTimestamp.LAST_RUNTIME, + eventdata.EventTimestamp.LAST_PRINTED]: + return '.A..' + + # Content modification. + if evt.timestamp_desc in [ + eventdata.EventTimestamp.MODIFICATION_TIME, + eventdata.EventTimestamp.WRITTEN_TIME, + eventdata.EventTimestamp.DELETED_TIME]: + return 'M...' + + # Content creation time. + if evt.timestamp_desc in [ + eventdata.EventTimestamp.CREATION_TIME, + eventdata.EventTimestamp.ADDED_TIME, + eventdata.EventTimestamp.FILE_DOWNLOADED, + eventdata.EventTimestamp.FIRST_CONNECTED]: + return '...B' + + # Metadata modification. + if evt.timestamp_desc in [ + eventdata.EventTimestamp.CHANGE_TIME, + eventdata.EventTimestamp.ENTRY_MODIFICATION_TIME]: + return '..C.' + + return '....' + + +def BuildHostDict(storage_object): + """Return a dict object from a StorageFile object. + + Build a dict object based on the preprocess objects stored inside + a storage file. + + Args: + storage_object: The StorageFile object that stores all the EventObjects. + + Returns: + A dict object that has the store number as a key and the hostname + as the value to that key. + """ + host_dict = {} + if not storage_object: + return host_dict + + if not hasattr(storage_object, 'GetStorageInformation'): + return host_dict + + for info in storage_object.GetStorageInformation(): + if hasattr(info, 'store_range') and hasattr(info, 'hostname'): + for store_number in range(info.store_range[0], info.store_range[1]): + # TODO: A bit wasteful, if the range is large we are wasting keys. + # Rewrite this logic into a more optimal one. + host_dict[store_number] = info.hostname + + return host_dict diff --git a/plaso/output/json_out.py b/plaso/output/json_out.py new file mode 100644 index 0000000..daa11e8 --- /dev/null +++ b/plaso/output/json_out.py @@ -0,0 +1,40 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 output module that saves data into a simple JSON format.""" + +from plaso.lib import output +from plaso.serializer import json_serializer + + +class Json(output.FileLogOutputFormatter): + """Saves the events into a JSON format.""" + + def EventBody(self, event_object): + """Prints out to a filehandle string representation of an EventObject. + + Each event object contains both attributes that are considered "reserved" + and others that aren't. The 'raw' representation of the object makes a + distinction between these two types as well as extracting the format + strings from the object. + + Args: + event_object: The event object (instance of EventObject). + """ + self.filehandle.WriteLine( + json_serializer.JsonEventObjectSerializer.WriteSerialized(event_object)) + self.filehandle.WriteLine(u'\n') diff --git a/plaso/output/json_out_test.py b/plaso/output/json_out_test.py new file mode 100644 index 0000000..b82bf4b --- /dev/null +++ b/plaso/output/json_out_test.py @@ -0,0 +1,90 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the JSON output class.""" + +import StringIO +import unittest + +from dfvfs.lib import definitions +from dfvfs.path import factory as path_spec_factory + +from plaso.lib import event +from plaso.lib import timelib_test +from plaso.output import json_out + + +class JsonTestEvent(event.EventObject): + """Simplified EventObject for testing.""" + DATA_TYPE = 'test:l2tjson' + + def __init__(self): + """Initialize event with data.""" + super(JsonTestEvent, self).__init__() + self.timestamp = timelib_test.CopyStringToTimestamp( + '2012-06-27 18:17:01+00:00') + self.hostname = u'ubuntu' + self.display_name = u'OS: /var/log/syslog.1' + self.inode = 12345678 + self.text = ( + u'Reporter PID: |8442| (pam_unix(cron:session): session\n ' + u'closed for user root)') + self.username = u'root' + + os_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=u'/cases/image.dd') + self.pathspec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_TSK, inode=15, location=u'/var/log/syslog.1', + parent=os_path_spec) + + +class JsonOutputTest(unittest.TestCase): + """Tests for the JSON outputter.""" + + def setUp(self): + """Sets up the objects needed for this test.""" + self.output = StringIO.StringIO() + self.formatter = json_out.Json(None, self.output) + self.event_object = JsonTestEvent() + + def testStartAndEnd(self): + """Test to ensure start and end functions do not add text.""" + self.formatter.Start() + self.assertEquals(self.output.getvalue(), u'') + self.formatter.End() + self.assertEquals(self.output.getvalue(), u'') + + def testEventBody(self): + """Test ensures that returned lines returned are formatted as JSON.""" + + expected_string = ( + '{{"username": "root", "display_name": "OS: /var/log/syslog.1", ' + '"uuid": "{0:s}", "data_type": "test:l2tjson", ' + '"timestamp": 1340821021000000, "hostname": "ubuntu", "text": ' + '"Reporter PID: |8442| (pam_unix(cron:session): session\\n ' + 'closed for user root)", "pathspec": "{{\\"type_indicator\\": ' + '\\"TSK\\", \\"inode\\": 15, \\"location\\": \\"/var/log/syslog.1\\", ' + '\\"parent\\": \\"{{\\\\\\"type_indicator\\\\\\": \\\\\\"OS\\\\\\", ' + '\\\\\\"location\\\\\\": \\\\\\"/cases/image.dd\\\\\\"}}\\"}}", ' + '"inode": 12345678}}\n').format(self.event_object.uuid) + + self.formatter.EventBody(self.event_object) + self.assertEquals(self.output.getvalue(), expected_string) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/output/l2t_csv.py b/plaso/output/l2t_csv.py new file mode 100644 index 0000000..5278703 --- /dev/null +++ b/plaso/output/l2t_csv.py @@ -0,0 +1,144 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains functions for outputting as l2t_csv. + +Author description at: http://code.google.com/p/log2timeline/wiki/l2t_csv +""" + +import logging +import re + +from plaso.formatters import manager as formatters_manager +from plaso.lib import errors +from plaso.lib import output +from plaso.lib import timelib +from plaso.lib import utils +from plaso.output import helper + + +class L2tcsv(output.FileLogOutputFormatter): + """CSV format used by log2timeline, with 17 fixed fields.""" + + FORMAT_ATTRIBUTE_RE = re.compile('{([^}]+)}') + + def Start(self): + """Returns a header for the output.""" + # Build a hostname and username dict objects. + self._hostnames = {} + if self.store: + self._hostnames = helper.BuildHostDict(self.store) + self._preprocesses = {} + for info in self.store.GetStorageInformation(): + if hasattr(info, 'store_range'): + for store_number in range( + info.store_range[0], info.store_range[1] + 1): + self._preprocesses[store_number] = info + + self.filehandle.WriteLine( + u'date,time,timezone,MACB,source,sourcetype,type,user,host,short,desc,' + u'version,filename,inode,notes,format,extra\n') + + def WriteEvent(self, event_object): + """Write a single event.""" + try: + self.EventBody(event_object) + except errors.NoFormatterFound: + logging.error(u'Unable to output line, no formatter found.') + logging.error(event_object) + + def EventBody(self, event_object): + """Formats data as l2t_csv and writes to the filehandle from OutputFormater. + + Args: + event_object: The event object (EventObject). + + Raises: + errors.NoFormatterFound: If no formatter for that event is found. + """ + if not hasattr(event_object, 'timestamp'): + return + + # TODO: move this to an output module interface. + event_formatter = formatters_manager.EventFormatterManager.GetFormatter( + event_object) + if not event_formatter: + raise errors.NoFormatterFound( + u'Unable to find event formatter for: {0:s}.'.format( + event_object.DATA_TYPE)) + + msg, msg_short = event_formatter.GetMessages(event_object) + source_short, source_long = event_formatter.GetSources(event_object) + + date_use = timelib.Timestamp.CopyToDatetime( + event_object.timestamp, self.zone) + extras = [] + format_variables = self.FORMAT_ATTRIBUTE_RE.findall( + event_formatter.format_string) + for key in event_object.GetAttributes(): + if key in utils.RESERVED_VARIABLES or key in format_variables: + continue + # Force a string conversion since some of the extra attributes + # can be numbers or bools. + value = getattr(event_object, key) + extras.append(u'{0:s}: {1!s} '.format(key, value)) + extra = ' '.join(extras) + + inode = getattr(event_object, 'inode', '-') + if inode == '-': + if hasattr(event_object, 'pathspec') and hasattr( + event_object.pathspec, 'image_inode'): + inode = event_object.pathspec.image_inode + + hostname = getattr(event_object, 'hostname', u'') + + # TODO: move this into a base output class. + username = getattr(event_object, 'username', u'-') + if self.store: + if not hostname: + hostname = self._hostnames.get(event_object.store_number, u'-') + + pre_obj = self._preprocesses.get(event_object.store_number) + if pre_obj: + check_user = pre_obj.GetUsernameById(username) + if check_user != '-': + username = check_user + + row = ( + '{0:02d}/{1:02d}/{2:04d}'.format( + date_use.month, date_use.day, date_use.year), + '{0:02d}:{1:02d}:{2:02d}'.format( + date_use.hour, date_use.minute, date_use.second), + self.zone, + helper.GetLegacy(event_object), + source_short, + source_long, + getattr(event_object, 'timestamp_desc', u'-'), + username, + hostname, + msg_short, + msg, + '2', + getattr(event_object, 'display_name', u'-'), + inode, + getattr(event_object, 'notes', u'-'), # Notes field placeholder. + getattr(event_object, 'parser', u'-'), + extra.replace('\n', u'-').replace('\r', u'')) + + out_write = u'{0:s}\n'.format( + u','.join(unicode(x).replace(',', u' ') for x in row)) + self.filehandle.WriteLine(out_write) diff --git a/plaso/output/l2t_csv_test.py b/plaso/output/l2t_csv_test.py new file mode 100644 index 0000000..8cb1cfe --- /dev/null +++ b/plaso/output/l2t_csv_test.py @@ -0,0 +1,95 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the L2tCsv output class.""" + +import StringIO +import unittest + +from plaso.formatters import interface as formatters_interface +from plaso.lib import event +from plaso.lib import eventdata +from plaso.output import l2t_csv + + +class L2tTestEvent(event.EventObject): + """Simplified EventObject for testing.""" + DATA_TYPE = 'test:l2t_csv' + + def __init__(self): + """Initialize event with data.""" + super(L2tTestEvent, self).__init__() + self.timestamp = 1340821021000000 + self.timestamp_desc = eventdata.EventTimestamp.WRITTEN_TIME + self.hostname = u'ubuntu' + self.filename = u'log/syslog.1' + self.display_name = u'log/syslog.1' + self.some_additional_foo = True + self.my_number = 123 + self.text = ( + u'Reporter PID: 8442 (pam_unix(cron:session): session\n ' + u'closed for user root)') + + +class L2tTestEventFormatter(formatters_interface.EventFormatter): + """Formatter for the test event.""" + DATA_TYPE = 'test:l2t_csv' + FORMAT_STRING = u'{text}' + + SOURCE_SHORT = 'LOG' + SOURCE_LONG = 'Syslog' + + +class L2tCsvTest(unittest.TestCase): + """Contains tests to validate the L2tCSV outputter.""" + def setUp(self): + self.output = StringIO.StringIO() + self.formatter = l2t_csv.L2tcsv(None, self.output) + self.event_object = L2tTestEvent() + + def testStart(self): + """Test ensures header line is outputted as expected.""" + + correct_line = ( + u'date,time,timezone,MACB,source,sourcetype,type,user,host,short,desc,' + u'version,filename,inode,notes,format,extra\n') + + self.formatter.Start() + self.assertEquals(self.output.getvalue(), correct_line) + + def testEventBody(self): + """Test ensures that returned lines returned are formatted as L2tCSV.""" + + self.formatter.EventBody(self.event_object) + correct = ( + u'06/27/2012,18:17:01,UTC,M...,LOG,Syslog,Content Modification Time,-,' + u'ubuntu,Reporter PID: 8442 (pam_unix(cron:session): session ' + u'closed for user root),Reporter PID: 8442 ' + u'(pam_unix(cron:session): ' + u'session closed for user root),2,log/syslog.1,-,-,-,my_number: 123 ' + u'some_additional_foo: True \n') + self.assertEquals(self.output.getvalue(), correct) + + def testEventBodyNoExtraCommas(self): + """Test ensures that the only commas returned are the 16 delimeters.""" + + self.formatter.EventBody(self.event_object) + self.assertEquals(self.output.getvalue().count(u','), 16) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/output/l2t_tln.py b/plaso/output/l2t_tln.py new file mode 100644 index 0000000..5246d18 --- /dev/null +++ b/plaso/output/l2t_tln.py @@ -0,0 +1,122 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains a class for outputting in the l2tTLN format. + +l2tTLN is TLN as expanded in L2T 0.65 to 7 fields: + +https://code.google.com/p/log2timeline/source/browse/lib/Log2t/output/tln.pm + +Fields: + Time - 32 bit Unix epoch. + Source - The plugin that produced the data. + Host - The source host system. + User - The user associated with the data. + Description - Message string describing the data. + TZ - L2T 0.65 field. Timezone of the event. + Notes - L2T 0.65 field. Optional notes field or filename and inode. +""" + +import logging + +from plaso.formatters import manager as formatters_manager +from plaso.lib import errors +from plaso.lib import output +from plaso.lib import timelib +from plaso.output import helper + + +class L2ttln(output.FileLogOutputFormatter): + """Extended seven field pipe delimited TLN; L2T 0.65 style.""" + + DELIMITER = u'|' + + def Start(self): + """Returns a header for the output.""" + # Build a hostname and username dict objects. + self._hostnames = {} + if self.store: + self._hostnames = helper.BuildHostDict(self.store) + self._preprocesses = {} + for info in self.store.GetStorageInformation(): + if hasattr(info, 'store_range'): + for store_number in range( + info.store_range[0], info.store_range[1] + 1): + self._preprocesses[store_number] = info + self.filehandle.WriteLine(u'Time|Source|Host|User|Description|TZ|Notes\n') + + def WriteEvent(self, event_object): + """Write a single event.""" + try: + self.EventBody(event_object) + except errors.NoFormatterFound: + logging.error(u'Unable to output line, no formatter found.') + logging.error(event_object.GetString()) + + def EventBody(self, event_object): + """Formats data as TLN and writes to the filehandle from OutputFormater. + + Args: + event_object: The event object (EventObject). + + Raises: + errors.NoFormatterFound: If no formatter for that event is found. + """ + if not hasattr(event_object, 'timestamp'): + return + + # TODO: move this to an output module interface. + event_formatter = formatters_manager.EventFormatterManager.GetFormatter( + event_object) + if not event_formatter: + raise errors.NoFormatterFound( + u'Unable to find event formatter for: {0:s}.'.format( + event_object.DATA_TYPE)) + + msg, _ = event_formatter.GetMessages(event_object) + source_short, _ = event_formatter.GetSources(event_object) + + date_use = timelib.Timestamp.CopyToPosix(event_object.timestamp) + hostname = getattr(event_object, 'hostname', u'') + username = getattr(event_object, 'username', u'') + + if self.store: + if not hostname: + hostname = self._hostnames.get(event_object.store_number, '') + + pre_obj = self._preprocesses.get(event_object.store_number) + if pre_obj: + check_user = pre_obj.GetUsernameById(username) + if check_user != '-': + username = check_user + + notes = getattr(event_object, 'notes', u'') + if not notes: + notes = u'File: {0:s} inode: {1!s}'.format( + getattr(event_object, 'display_name', u''), + getattr(event_object, 'inode', u'')) + + out_write = u'{0!s}|{1:s}|{2:s}|{3:s}|{4:s}|{5:s}|{6!s}\n'.format( + date_use, + source_short.replace(self.DELIMITER, u' '), + hostname.replace(self.DELIMITER, u' '), + username.replace(self.DELIMITER, u' '), + msg.replace(self.DELIMITER, u' '), + self.zone, + notes.replace(self.DELIMITER, u' ')) + + self.filehandle.WriteLine(out_write) diff --git a/plaso/output/l2t_tln_test.py b/plaso/output/l2t_tln_test.py new file mode 100644 index 0000000..5b77ed4 --- /dev/null +++ b/plaso/output/l2t_tln_test.py @@ -0,0 +1,86 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the l2tTLN output class.""" + +import StringIO +import unittest + +from plaso.formatters import interface as formatters_interface +from plaso.lib import event +from plaso.output import l2t_tln + + +class TlnTestEvent(event.EventObject): + """Simplified EventObject for testing.""" + DATA_TYPE = 'test:tln' + + def __init__(self): + """Initialize event with data.""" + super(TlnTestEvent, self).__init__() + self.timestamp = 1340821021000000 + self.hostname = u'ubuntu' + self.display_name = u'OS: log/syslog.1' + self.inode = 12345678 + self.text = ( + u'Reporter PID: |8442| (pam_unix(cron:session): session\n ' + u'closed for user root)') + self.username = u'root' + + +class TlnTestEventFormatter(formatters_interface.EventFormatter): + """Formatter for the test event.""" + DATA_TYPE = 'test:tln' + FORMAT_STRING = u'{text}' + SOURCE_SHORT = 'LOG' + SOURCE_LONG = 'Syslog' + + +class L2TTlnTest(unittest.TestCase): + """Tests for the TLN outputter.""" + + def setUp(self): + """Sets up the objects needed for this test.""" + self.output = StringIO.StringIO() + self.formatter = l2t_tln.L2ttln(None, self.output) + self.event_object = TlnTestEvent() + + def testStart(self): + """Test ensures header line is outputted as expected.""" + correct_line = u'Time|Source|Host|User|Description|TZ|Notes\n' + + self.formatter.Start() + self.assertEquals(self.output.getvalue(), correct_line) + + def testEventBody(self): + """Test ensures that returned lines returned are formatted as TLN.""" + + self.formatter.EventBody(self.event_object) + correct = (u'1340821021|LOG|ubuntu|root|Reporter PID: 8442 ' + u'(pam_unix(cron:session): session closed for user root)|UTC' + u'|File: OS: log/syslog.1 inode: 12345678\n') + self.assertEquals(self.output.getvalue(), correct) + + def testEventBodyNoStrayPipes(self): + """Test ensures that the only pipes are the six field delimiters.""" + + self.formatter.EventBody(self.event_object) + self.assertEquals(self.output.getvalue().count(u'|'), 6) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/output/mysql_4n6.py b/plaso/output/mysql_4n6.py new file mode 100644 index 0000000..b130ddb --- /dev/null +++ b/plaso/output/mysql_4n6.py @@ -0,0 +1,402 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 logging +import re +import sys + +import MySQLdb + +from plaso import formatters +from plaso.formatters import interface as formatters_interface +from plaso.formatters import manager as formatters_manager +from plaso.lib import errors +from plaso.lib import output +from plaso.lib import timelib +from plaso.lib import utils +from plaso.output import helper + + +__author__ = 'David Nides (david.nides@gmail.com)' + + +class Mysql4n6(output.LogOutputFormatter): + """Contains functions for outputting as 4n6time MySQL database.""" + + FORMAT_ATTRIBUTE_RE = re.compile('{([^}]+)}') + + META_FIELDS = ['sourcetype', 'source', 'user', 'host', 'MACB', + 'color', 'type', 'record_number'] + + ARGUMENTS = [ + ('--db_user', { + 'dest': 'db_user', + 'type': unicode, + 'help': 'Defines the database user.', + 'metavar': 'USERNAME', + 'action': 'store', + 'default': 'root'}), + ('--db_host', { + 'dest': 'db_host', + 'metavar': 'HOSTNAME', + 'type': unicode, + 'help': ( + 'Defines the IP address or the hostname of the database ' + 'server.'), + 'action': 'store', + 'default': 'localhost'}), + ('--db_pass', { + 'dest': 'db_pass', + 'metavar': 'PASSWORD', + 'type': unicode, + 'help': 'The password for the database user.', + 'action': 'store', + 'default': 'forensic'}), + ('--db_name', { + 'dest': 'db_name', + 'type': unicode, + 'help': 'The name of the database to connect to.', + 'action': 'store', + 'default': 'log2timeline'}), + ('--append', { + 'dest': 'append', + 'action': 'store_true', + 'help': ( + 'Defines whether the intention is to append to an already ' + 'existing database or overwrite it. Defaults to overwrite.'), + 'default': False}), + ('--fields', { + 'dest': 'fields', + 'action': 'store', + 'type': unicode, + 'nargs': '*', + 'help': 'Defines which fields should be indexed in the database.', + 'default': [ + 'host', 'user', 'source', 'sourcetype', 'type', 'datetime', + 'color']}), + ('--evidence', { + 'dest': 'evidence', + 'action': 'store', + 'help': ( + 'Set the evidence field to a specific value, defaults to ' + 'empty.'), + 'type': unicode, + 'default': '-'})] + + def __init__(self, store, filehandle=sys.stdout, config=None, + filter_use=None): + """Constructor for the output module. + + Args: + store: The storage object. + filehandle: A file-like object that can be written to. + config: The configuration object for the module. + filter_use: The filter object used. + """ + # TODO: Add a unit test for this output module. + super(Mysql4n6, self).__init__(store, filehandle, config, filter_use) + # TODO: move this to an output module interface. + self._formatters_manager = formatters_manager.EventFormatterManager + + self.set_status = getattr(config, 'set_status', None) + + self.host = getattr(config, 'db_host', 'localhost') + self.user = getattr(config, 'db_user', 'root') + self.password = getattr(config, 'db_pass', 'forensic') + self.dbname = getattr(config, 'db_name', 'log2timeline') + self.evidence = getattr(config, 'evidence', '-') + self.append = getattr(config, 'append', False) + self.fields = getattr(config, 'fields', [ + 'host', 'user', 'source', 'sourcetype', 'type', 'datetime', 'color']) + + def Start(self): + """Connect to the database and create the table before inserting.""" + if self.dbname == '': + raise IOError(u'Specify a database name.') + + try: + if self.append: + self.conn = MySQLdb.connect(self.host, self.user, + self.password, self.dbname) + self.curs = self.conn.cursor() + else: + self.conn = MySQLdb.connect(self.host, self.user, self.password) + self.curs = self.conn.cursor() + + self.conn.set_character_set(u'utf8') + self.curs.execute(u'SET NAMES utf8') + self.curs.execute(u'SET CHARACTER SET utf8') + self.curs.execute(u'SET character_set_connection=utf8') + self.curs.execute(u'SET GLOBAL innodb_large_prefix=ON') + self.curs.execute(u'SET GLOBAL innodb_file_format=barracuda') + self.curs.execute(u'SET GLOBAL innodb_file_per_table=ON') + self.curs.execute( + u'CREATE DATABASE IF NOT EXISTS {0:s}'.format(self.dbname)) + self.curs.execute(u'USE {0:s}'.format(self.dbname)) + # Create tables. + self.curs.execute( + (u'CREATE TABLE IF NOT EXISTS log2timeline (' + u'rowid INT NOT NULL AUTO_INCREMENT, timezone VARCHAR(256), ' + u'MACB VARCHAR(256), source VARCHAR(256), sourcetype VARCHAR(256), ' + u'type VARCHAR(256), user VARCHAR(256), host VARCHAR(256), ' + u'description TEXT, filename VARCHAR(256), inode VARCHAR(256), ' + u'notes VARCHAR(256), format VARCHAR(256), ' + u'extra TEXT, datetime datetime, reportnotes VARCHAR(256), ' + u'inreport VARCHAR(256), tag VARCHAR(256), color VARCHAR(256), ' + u'offset INT, store_number INT, store_index INT, ' + u'vss_store_number INT, URL TEXT, ' + u'record_number VARCHAR(256), event_identifier VARCHAR(256), ' + u'event_type VARCHAR(256), source_name VARCHAR(256), ' + u'user_sid VARCHAR(256), computer_name VARCHAR(256), ' + u'evidence VARCHAR(256), ' + u'PRIMARY KEY (rowid)) ENGINE=InnoDB ROW_FORMAT=COMPRESSED')) + if self.set_status: + self.set_status(u'Created table: log2timeline') + + for field in self.META_FIELDS: + self.curs.execute( + u'CREATE TABLE IF NOT EXISTS l2t_{0}s ({0}s TEXT, frequency INT) ' + u'ENGINE=InnoDB ROW_FORMAT=COMPRESSED'.format(field)) + if self.set_status: + self.set_status(u'Created table: l2t_{0:s}'.format(field)) + + self.curs.execute( + u'CREATE TABLE IF NOT EXISTS l2t_tags (tag TEXT) ' + u'ENGINE=InnoDB ROW_FORMAT=COMPRESSED') + if self.set_status: + self.set_status(u'Created table: l2t_tags') + + self.curs.execute( + u'CREATE TABLE IF NOT EXISTS l2t_saved_query (' + u'name TEXT, query TEXT) ' + u'ENGINE=InnoDB ROW_FORMAT=COMPRESSED') + if self.set_status: + self.set_status(u'Created table: l2t_saved_query') + + self.curs.execute( + u'CREATE TABLE IF NOT EXISTS l2t_disk (' + u'disk_type INT, mount_path TEXT, ' + u'dd_path TEXT, dd_offset TEXT, ' + u'storage_file TEXT, export_path TEXT) ' + u'ENGINE=InnoDB ROW_FORMAT=COMPRESSED') + self.curs.execute( + u'INSERT INTO l2t_disk (' + u'disk_type, mount_path, dd_path, ' + u'dd_offset, storage_file, ' + u'export_path) VALUES ' + u'(0, "", "", "", "", "")') + if self.set_status: + self.set_status(u'Created table: l2t_disk') + except MySQLdb.Error as exception: + raise IOError(u'Unable to insert into database with error: {0:s}'.format( + exception)) + + self.count = 0 + + def End(self): + """Create indices and commit the transaction.""" + # Build up indices for the fields specified in the args. + # It will commit the inserts automatically before creating index. + if not self.append: + for field_name in self.fields: + sql = u'CREATE INDEX {0}_idx ON log2timeline ({0:s})'.format(field_name) + self.curs.execute(sql) + if self.set_status: + self.set_status(u'Created index: {0:s}'.format(field_name)) + + # Get meta info and save into their tables. + if self.set_status: + self.set_status(u'Creating metadata...') + + for field in self.META_FIELDS: + vals = self._GetDistinctValues(field) + self.curs.execute(u'DELETE FROM l2t_{0:s}s'.format(field)) + for name, freq in vals.items(): + self.curs.execute(( + u'INSERT INTO l2t_{0:s}s ({1:s}s, frequency) ' + u'VALUES("{2:s}", {3:d}) ').format(field, field, name, freq)) + + self.curs.execute(u'DELETE FROM l2t_tags') + for tag in self._ListTags(): + self.curs.execute( + u'INSERT INTO l2t_tags (tag) VALUES ("{0:s}")'.format(tag)) + + if self.set_status: + self.set_status(u'Database created.') + + self.conn.commit() + self.curs.close() + self.conn.close() + + def _GetDistinctValues(self, field_name): + """Query database for unique field types.""" + self.curs.execute( + u'SELECT {0}, COUNT({0}) FROM log2timeline GROUP BY {0}'.format( + field_name)) + res = {} + for row in self.curs.fetchall(): + if row[0] != '': + res[row[0]] = int(row[1]) + return res + + def _ListTags(self): + """Query database for unique tag types.""" + all_tags = [] + self.curs.execute( + u'SELECT DISTINCT tag FROM log2timeline') + + # This cleans up the messy SQL return. + for tag_row in self.curs.fetchall(): + tag_string = tag_row[0] + if tag_string: + tags = tag_string.split(',') + for tag in tags: + if tag not in all_tags: + all_tags.append(tag) + return all_tags + + def EventBody(self, event_object): + """Formats data as 4n6time database table format and writes to the db. + + Args: + event_object: The event object (EventObject). + + Raises: + raise errors.NoFormatterFound: If no formatter for this event is found. + """ + if not hasattr(event_object, 'timestamp'): + return + + event_formatter = self._formatters_manager.GetFormatter(event_object) + if not event_formatter: + raise errors.NoFormatterFound( + u'Unable to output event, no event formatter found.') + + if (isinstance( + event_formatter, formatters.winreg.WinRegistryGenericFormatter) and + event_formatter.FORMAT_STRING.find('<|>') == -1): + event_formatter.FORMAT_STRING = u'[{keyname}]<|>{text}<|>' + + elif isinstance( + event_formatter, formatters_interface.ConditionalEventFormatter): + event_formatter.FORMAT_STRING_SEPARATOR = u'<|>' + + elif isinstance(event_formatter, formatters_interface.EventFormatter): + event_formatter.format_string = event_formatter.format_string.replace( + '}', '}<|>') + + msg, _ = event_formatter.GetMessages(event_object) + source_short, source_long = event_formatter.GetSources(event_object) + + date_use = timelib.Timestamp.CopyToDatetime( + event_object.timestamp, self.zone) + if not date_use: + logging.error(u'Unable to process date for entry: {0:s}'.format(msg)) + return + extra = [] + format_variables = self.FORMAT_ATTRIBUTE_RE.findall( + event_formatter.format_string) + for key in event_object.GetAttributes(): + if key in utils.RESERVED_VARIABLES or key in format_variables: + continue + extra.append(u'{0:s}: {1!s} '.format( + key, getattr(event_object, key, None))) + + extra = u' '.join(extra) + + inode = getattr(event_object, 'inode', '-') + if inode == '-': + if (hasattr(event_object, 'pathspec') and + hasattr(event_object.pathspec, 'image_inode')): + inode = event_object.pathspec.image_inode + + date_use_string = u'{0:d}-{1:d}-{2:d} {3:d}:{4:d}:{5:d}'.format( + date_use.year, date_use.month, date_use.day, date_use.hour, + date_use.minute, date_use.second) + + tags = [] + if hasattr(event_object, 'tag') and hasattr(event_object.tag, 'tags'): + tags = event_object.tag.tags + else: + tags = u'' + + taglist = u','.join(tags) + row = ( + str(self.zone), + helper.GetLegacy(event_object), + source_short, + source_long, + getattr(event_object, 'timestamp_desc', '-'), + getattr(event_object, 'username', '-'), + getattr(event_object, 'hostname', '-'), + msg, + getattr(event_object, 'filename', '-'), + inode, + getattr(event_object, 'notes', '-'), + getattr(event_object, 'parser', '-'), + extra, + date_use_string, + '', + '', + taglist, + '', + getattr(event_object, 'offset', 0), + event_object.store_number, + event_object.store_index, + self.GetVSSNumber(event_object), + getattr(event_object, 'url', '-'), + getattr(event_object, 'record_number', 0), + getattr(event_object, 'event_identifier', '-'), + getattr(event_object, 'event_type', '-'), + getattr(event_object, 'source_name', '-'), + getattr(event_object, 'user_sid', '-'), + getattr(event_object, 'computer_name', '-'), + self.evidence) + + try: + self.curs.execute( + 'INSERT INTO log2timeline(timezone, MACB, source, ' + 'sourcetype, type, user, host, description, filename, ' + 'inode, notes, format, extra, datetime, reportnotes, ' + 'inreport, tag, color, offset, store_number, ' + 'store_index, vss_store_number, URL, record_number, ' + 'event_identifier, event_type, source_name, user_sid, ' + 'computer_name, evidence) VALUES (' + '%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, ' + '%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, ' + '%s, %s, %s, %s)', row) + except MySQLdb.Error as exception: + logging.warning( + u'Unable to insert into database with error: {0:s}.'.format( + exception)) + + self.count += 1 + + # TODO: Experiment if committing the current transaction + # every 10000 inserts is the optimal approach. + if self.count % 10000 == 0: + self.conn.commit() + if self.set_status: + self.set_status(u'Inserting event: {0:d}'.format(self.count)) + + def GetVSSNumber(self, event_object): + """Return the vss_store_number of the event.""" + if not hasattr(event_object, 'pathspec'): + return -1 + + return getattr(event_object.pathspec, 'vss_store_number', -1) diff --git a/plaso/output/pstorage.py b/plaso/output/pstorage.py new file mode 100644 index 0000000..2da8f1b --- /dev/null +++ b/plaso/output/pstorage.py @@ -0,0 +1,62 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Implements a StorageFile output formatter.""" + +from plaso.lib import event +from plaso.lib import output +from plaso.lib import storage +from plaso.lib import timelib + + +class Pstorage(output.LogOutputFormatter): + """Dumps event objects to a plaso storage file.""" + + def Start(self): + """Sets up the output storage file.""" + pre_obj = event.PreprocessObject() + pre_obj.collection_information = {'time_of_run': timelib.Timestamp.GetNow()} + if hasattr(self._config, 'filter') and self._config.filter: + pre_obj.collection_information['filter'] = self._config.filter + if hasattr(self._config, 'storagefile') and self._config.storagefile: + pre_obj.collection_information[ + 'file_processed'] = self._config.storagefile + self._storage = storage.StorageFile(self.filehandle, pre_obj=pre_obj) + + def EventBody(self, event_object): + """Add an EventObject protobuf to the storage file. + + Args: + proto: The EventObject protobuf. + """ + # Needed due to duplicate removals, if two events + # are merged then we'll just pick the first inode value. + inode = getattr(event_object, 'inode', None) + if type(inode) in (str, unicode): + inode_list = inode.split(';') + try: + new_inode = int(inode_list[0]) + except (ValueError, IndexError): + new_inode = 0 + + event_object.inode = new_inode + + self._storage.AddEventObject(event_object) + + def End(self): + """Closes the storage file.""" + self._storage.Close() diff --git a/plaso/output/pstorage_test.py b/plaso/output/pstorage_test.py new file mode 100644 index 0000000..dae4301 --- /dev/null +++ b/plaso/output/pstorage_test.py @@ -0,0 +1,93 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for plaso.output.pstorage.""" + +import os +import shutil +import tempfile +import unittest + +from plaso.lib import output +from plaso.lib import pfilter +from plaso.lib import storage +from plaso.output import pstorage # pylint: disable=unused-import + + +class TempDirectory(object): + """A self cleaning temporary directory.""" + + def __init__(self): + """Initializes the temporary directory.""" + super(TempDirectory, self).__init__() + self.name = u'' + + def __enter__(self): + """Make this work with the 'with' statement.""" + self.name = tempfile.mkdtemp() + return self.name + + def __exit__(self, unused_type, unused_value, unused_traceback): + """Make this work with the 'with' statement.""" + shutil.rmtree(self.name, True) + + +class PstorageTest(unittest.TestCase): + def setUp(self): + self.test_filename = os.path.join('test_data', 'psort_test.out') + + # Show full diff results, part of TestCase so does not follow our naming + # conventions. + self.maxDiff = None + pfilter.TimeRangeCache.ResetTimeConstraints() + + def testOutput(self): + with TempDirectory() as dirname: + dump_file = os.path.join(dirname, 'plaso.db') + # Copy events to pstorage dump. + with storage.StorageFile(self.test_filename, read_only=True) as store: + formatter_cls = output.GetOutputFormatter('Pstorage') + formatter = formatter_cls(store, dump_file) + with output.EventBuffer(formatter, check_dedups=False) as output_buffer: + event_object = formatter.FetchEntry() + while event_object: + output_buffer.Append(event_object) + event_object = formatter.FetchEntry() + + # Make sure original and dump have the same events. + original = storage.StorageFile(self.test_filename, read_only=True) + dump = storage.StorageFile(dump_file, read_only=True) + event_object_original = original.GetSortedEntry() + event_object_dump = dump.GetSortedEntry() + original_list = [] + dump_list = [] + + while event_object_original: + original_list.append(event_object_original.EqualityString()) + dump_list.append(event_object_dump.EqualityString()) + event_object_original = original.GetSortedEntry() + event_object_dump = dump.GetSortedEntry() + + self.assertFalse(event_object_dump) + + for original_str, dump_str in zip( + sorted(original_list), sorted(dump_list)): + self.assertEqual(original_str, dump_str) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/output/rawpy.py b/plaso/output/rawpy.py new file mode 100644 index 0000000..136dd0b --- /dev/null +++ b/plaso/output/rawpy.py @@ -0,0 +1,41 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Represents an EventObject as a string.""" +from plaso.lib import output + + +class Rawpy(output.FileLogOutputFormatter): + """Prints out a "raw" interpretation of the EventObject.""" + # TODO: Revisit the name of this class, perhaps rename it to + # something more closely similar to what it is doing now, as in + # "native" or something else. + + def EventBody(self, event_object): + """Prints out to a filehandle string representation of an EventObject. + + Each EventObject contains both attributes that are considered "reserved" + and others that aren't. The 'raw' representation of the object makes a + distinction between these two types as well as extracting the format + strings from the object. + + Args: + event_object: The EventObject. + """ + # TODO: Move the unicode cast into the event object itself, expose + # a ToString function or something similar that will send back the + # unicode string. + self.filehandle.WriteLine(unicode(event_object)) diff --git a/plaso/output/sqlite_4n6.py b/plaso/output/sqlite_4n6.py new file mode 100644 index 0000000..96cae45 --- /dev/null +++ b/plaso/output/sqlite_4n6.py @@ -0,0 +1,316 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 logging +import os +import re +import sys + +import sqlite3 + +from plaso import formatters +from plaso.formatters import interface as formatters_interface +from plaso.formatters import manager as formatters_manager +from plaso.lib import errors +from plaso.lib import output +from plaso.lib import timelib +from plaso.lib import utils +from plaso.output import helper + + +__author__ = 'David Nides (david.nides@gmail.com)' + + +class Sql4n6(output.LogOutputFormatter): + """Saves the data in a SQLite database, used by the tool 4n6Time.""" + + FORMAT_ATTRIBUTE_RE = re.compile('{([^}]+)}') + + META_FIELDS = [ + 'sourcetype', 'source', 'user', 'host', 'MACB', 'color', 'type', + 'record_number'] + + def __init__(self, store, filehandle=sys.stdout, config=None, + filter_use=None): + """Constructor for the output module. + + Args: + store: The storage object. + filehandle: A file-like object that can be written to. + config: The configuration object for the module. + filter_use: The filter object used. + """ + # TODO: Add a unit test for this output module. + super(Sql4n6, self).__init__(store, filehandle, config, filter_use) + # TODO: move this to an output module interface. + self._formatters_manager = formatters_manager.EventFormatterManager + self.set_status = getattr(config, 'set_status', None) + + # TODO: Revisit handling this outside of plaso. + self.dbname = filehandle + self.evidence = getattr(config, 'evidence', '-') + self.append = getattr(config, 'append', False) + self.fields = getattr(config, 'fields', [ + 'host', 'user', 'source', 'sourcetype', 'type', 'datetime', 'color']) + + # Override LogOutputFormatter methods so it won't write to the file + # handle any more. + def Start(self): + """Connect to the database and create the table before inserting.""" + if self.filehandle == sys.stdout: + raise IOError( + u'Unable to connect to stdout as database, please specify a file.') + + if (not self.append) and os.path.isfile(self.filehandle): + raise IOError(( + u'Unable to use an already existing file for output ' + u'[{0:s}]').format(self.filehandle)) + + self.conn = sqlite3.connect(self.dbname) + self.conn.text_factory = str + self.curs = self.conn.cursor() + + # Create table in database. + if not self.append: + self.curs.execute( + ('CREATE TABLE log2timeline (timezone TEXT, ' + 'MACB TEXT, source TEXT, sourcetype TEXT, type TEXT, ' + 'user TEXT, host TEXT, description TEXT, filename TEXT, ' + 'inode TEXT, notes TEXT, format TEXT, extra TEXT, ' + 'datetime datetime, reportnotes TEXT, ' + 'inreport TEXT, tag TEXT, color TEXT, offset INT,' + 'store_number INT, store_index INT, vss_store_number INT,' + 'url TEXT, record_number TEXT, event_identifier TEXT, ' + 'event_type TEXT, source_name TEXT, user_sid TEXT, ' + 'computer_name TEXT, evidence TEXT)')) + if self.set_status: + self.set_status('Created table: log2timeline') + + for field in self.META_FIELDS: + self.curs.execute( + 'CREATE TABLE l2t_{0}s ({0}s TEXT, frequency INT)'.format(field)) + if self.set_status: + self.set_status('Created table: l2t_{0:s}'.format(field)) + + self.curs.execute('CREATE TABLE l2t_tags (tag TEXT)') + if self.set_status: + self.set_status('Created table: l2t_tags') + + self.curs.execute('CREATE TABLE l2t_saved_query (name TEXT, query TEXT)') + if self.set_status: + self.set_status('Created table: l2t_saved_query') + + self.curs.execute('CREATE TABLE l2t_disk (disk_type INT, mount_path TEXT,' + ' dd_path TEXT, dd_offset TEXT, storage_file TEXT,' + ' export_path TEXT)') + self.curs.execute('INSERT INTO l2t_disk (disk_type, mount_path, dd_path,' + 'dd_offset, storage_file, export_path) VALUES ' + '(0, "", "", "", "", "")') + if self.set_status: + self.set_status('Created table: l2t_disk') + + self.count = 0 + + def End(self): + """Create indices and commit the transaction.""" + # Build up indices for the fields specified in the args. + # It will commit the inserts automatically before creating index. + if not self.append: + for field_name in self.fields: + sql = 'CREATE INDEX {0}_idx ON log2timeline ({0})'.format(field_name) + self.curs.execute(sql) + if self.set_status: + self.set_status('Created index: {0:s}'.format(field_name)) + + # Get meta info and save into their tables. + if self.set_status: + self.set_status('Creating metadata...') + + for field in self.META_FIELDS: + vals = self._GetDistinctValues(field) + self.curs.execute('DELETE FROM l2t_{0:s}s'.format(field)) + for name, freq in vals.items(): + self.curs.execute(( + 'INSERT INTO l2t_{0:s}s ({1:s}s, frequency) ' + 'VALUES("{2:s}", {3:d}) ').format(field, field, name, freq)) + self.curs.execute('DELETE FROM l2t_tags') + for tag in self._ListTags(): + self.curs.execute('INSERT INTO l2t_tags (tag) VALUES (?)', [tag]) + + if self.set_status: + self.set_status('Database created.') + + self.conn.commit() + self.curs.close() + self.conn.close() + + def _GetDistinctValues(self, field_name): + """Query database for unique field types.""" + self.curs.execute( + u'SELECT {0}, COUNT({0}) FROM log2timeline GROUP BY {0}'.format( + field_name)) + res = {} + for row in self.curs.fetchall(): + if row[0] != '': + res[row[0]] = int(row[1]) + return res + + def _ListTags(self): + """Query database for unique tag types.""" + all_tags = [] + self.curs.execute( + 'SELECT DISTINCT tag FROM log2timeline') + + # This cleans up the messy SQL return. + for tag_row in self.curs.fetchall(): + tag_string = tag_row[0] + if tag_string: + tags = tag_string.split(',') + for tag in tags: + if tag not in all_tags: + all_tags.append(tag) + return all_tags + + def StartEvent(self): + """Do nothing, just override the parent's StartEvent method.""" + pass + + def EndEvent(self): + """Do nothing, just override the parent's EndEvent method.""" + pass + + def EventBody(self, event_object): + """Formats data as the 4n6time table format and writes it to the database. + + Args: + event_object: The event object (EventObject). + + Raises: + raise errors.NoFormatterFound: If no event formatter was found. + """ + if 'timestamp' not in event_object.GetAttributes(): + return + + event_formatter = self._formatters_manager.GetFormatter(event_object) + if not event_formatter: + raise errors.NoFormatterFound( + 'Unable to output event, no event formatter found.') + + if (isinstance( + event_formatter, formatters.winreg.WinRegistryGenericFormatter) and + event_formatter.FORMAT_STRING.find('<|>') == -1): + event_formatter.FORMAT_STRING = u'[{keyname}]<|>{text}<|>' + + elif isinstance( + event_formatter, formatters_interface.ConditionalEventFormatter): + event_formatter.FORMAT_STRING_SEPARATOR = u'<|>' + + elif isinstance(event_formatter, formatters_interface.EventFormatter): + event_formatter.format_string = event_formatter.format_string.replace( + '}', '}<|>') + + msg, _ = event_formatter.GetMessages(event_object) + source_short, source_long = event_formatter.GetSources(event_object) + + date_use = timelib.Timestamp.CopyToDatetime( + event_object.timestamp, self.zone) + if not date_use: + logging.error(u'Unable to process date for entry: {0:s}'.format(msg)) + return + extra = [] + format_variables = self.FORMAT_ATTRIBUTE_RE.findall( + event_formatter.format_string) + for key in event_object.GetAttributes(): + if key in utils.RESERVED_VARIABLES or key in format_variables: + continue + extra.append(u'{0:s}: {1!s} '.format( + key, getattr(event_object, key, None))) + extra = u' '.join(extra) + + inode = getattr(event_object, 'inode', '-') + if inode == '-': + if (hasattr(event_object, 'pathspec') and + hasattr(event_object.pathspec, 'image_inode')): + inode = event_object.pathspec.image_inode + + date_use_string = u'{0:04d}-{1:02d}-{2:02d} {3:02d}:{4:02d}:{5:02d}'.format( + date_use.year, date_use.month, date_use.day, date_use.hour, + date_use.minute, date_use.second) + + tags = [] + if hasattr(event_object, 'tag'): + if hasattr(event_object.tag, 'tags'): + tags = event_object.tag.tags + taglist = ','.join(tags) + row = (str(self.zone), + helper.GetLegacy(event_object), + source_short, + source_long, + getattr(event_object, 'timestamp_desc', '-'), + getattr(event_object, 'username', '-'), + getattr(event_object, 'hostname', '-'), + msg, + getattr(event_object, 'filename', '-'), + inode, + getattr(event_object, 'notes', '-'), + getattr(event_object, 'parser', '-'), + extra, + date_use_string, + '', + '', + taglist, + '', + getattr(event_object, 'offset', 0), + event_object.store_number, + event_object.store_index, + GetVSSNumber(event_object), + getattr(event_object, 'url', '-'), + getattr(event_object, 'record_number', 0), + getattr(event_object, 'event_identifier', '-'), + getattr(event_object, 'event_type', '-'), + getattr(event_object, 'source_name', '-'), + getattr(event_object, 'user_sid', '-'), + getattr(event_object, 'computer_name', '-'), + self.evidence + ) + + self.curs.execute( + ('INSERT INTO log2timeline(timezone, MACB, source, ' + 'sourcetype, type, user, host, description, filename, ' + 'inode, notes, format, extra, datetime, reportnotes, inreport,' + 'tag, color, offset, store_number, store_index, vss_store_number,' + 'URL, record_number, event_identifier, event_type,' + 'source_name, user_sid, computer_name, evidence)' + ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,' + '?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'), row) + + self.count += 1 + + # Commit the current transaction every 10000 inserts. + if self.count % 10000 == 0: + self.conn.commit() + if self.set_status: + self.set_status('Inserting event: {0:d}'.format(self.count)) + + +def GetVSSNumber(event_object): + """Return the vss_store_number of the event.""" + if not hasattr(event_object, 'pathspec'): + return -1 + + return getattr(event_object.pathspec, 'vss_store_number', -1) diff --git a/plaso/output/tln.py b/plaso/output/tln.py new file mode 100644 index 0000000..903ffaa --- /dev/null +++ b/plaso/output/tln.py @@ -0,0 +1,110 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains a class for outputting in a TLN format. + +Output module based on TLN as described by: +http://windowsir.blogspot.com/2010/02/timeline-analysisdo-we-need-standard.html + +Fields: + Time - 32 bit Unix epoch. + Source - The plugin that produced the data. + Host - The source host system. + User - The user associated with the data. + Description - Message string describing the data. +""" + +import logging + +from plaso.formatters import manager as formatters_manager +from plaso.lib import errors +from plaso.lib import output +from plaso.lib import timelib +from plaso.output import helper + + +class Tln(output.FileLogOutputFormatter): + """Five field TLN pipe delimited outputter.""" + + DELIMITER = u'|' + + def Start(self): + """Returns a header for the output.""" + # Build a hostname and username dict objects. + self._hostnames = {} + if self.store: + self._hostnames = helper.BuildHostDict(self.store) + self._preprocesses = {} + for info in self.store.GetStorageInformation(): + if hasattr(info, 'store_range'): + for store_number in range( + info.store_range[0], info.store_range[1] + 1): + self._preprocesses[store_number] = info + self.filehandle.WriteLine(u'Time|Source|Host|User|Description\n') + + def WriteEvent(self, event_object): + """Write a single event.""" + try: + self.EventBody(event_object) + except errors.NoFormatterFound: + logging.error(u'Unable to output line, no formatter found.') + logging.error(event_object.GetString()) + + def EventBody(self, event_object): + """Formats data as TLN and writes to the filehandle from OutputFormater. + + Args: + event_object: The event object (EventObject). + + Raises: + errors.NoFormatterFound: If no formatter for that event is found. + """ + if not hasattr(event_object, 'timestamp'): + return + + # TODO: move this to an output module interface. + event_formatter = formatters_manager.EventFormatterManager.GetFormatter( + event_object) + if not event_formatter: + raise errors.NoFormatterFound( + u'Unable to find event formatter for: {0:s}.'.format( + event_object.DATA_TYPE)) + + msg, _ = event_formatter.GetMessages(event_object) + source_short, _ = event_formatter.GetSources(event_object) + + date_use = timelib.Timestamp.CopyToPosix(event_object.timestamp) + hostname = getattr(event_object, 'hostname', u'') + username = getattr(event_object, 'username', u'') + + if self.store: + if not hostname: + hostname = self._hostnames.get(event_object.store_number, u'') + + pre_obj = self._preprocesses.get(event_object.store_number) + if pre_obj: + check_user = pre_obj.GetUsernameById(username) + if check_user != '-': + username = check_user + + out_write = u'{0!s}|{1:s}|{2:s}|{3:s}|{4!s}\n'.format( + date_use, + source_short.replace(self.DELIMITER, u' '), + hostname.replace(self.DELIMITER, u' '), + username.replace(self.DELIMITER, u' '), + msg.replace(self.DELIMITER, u' ')) + self.filehandle.WriteLine(out_write) diff --git a/plaso/output/tln_test.py b/plaso/output/tln_test.py new file mode 100644 index 0000000..8179a39 --- /dev/null +++ b/plaso/output/tln_test.py @@ -0,0 +1,85 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the TLN output class.""" + +import StringIO +import unittest + +from plaso.formatters import interface as formatters_interface +from plaso.lib import event +from plaso.output import tln + + +class TlnTestEvent(event.EventObject): + """Simplified EventObject for testing.""" + DATA_TYPE = 'test:l2ttln' + + def __init__(self): + """Initialize event with data.""" + super(TlnTestEvent, self).__init__() + self.timestamp = 1340821021000000 + self.hostname = u'ubuntu' + self.display_name = u'OS: log/syslog.1' + self.inode = 12345678 + self.text = ( + u'Reporter PID: |8442| (pam_unix(cron:session): session\n ' + u'closed for user root)') + self.username = u'root' + + +class L2TTlnTestEventFormatter(formatters_interface.EventFormatter): + """Formatter for the test event.""" + DATA_TYPE = 'test:l2ttln' + FORMAT_STRING = u'{text}' + SOURCE_SHORT = 'LOG' + SOURCE_LONG = 'Syslog' + + +class TlnTest(unittest.TestCase): + """Tests for the TLN outputter.""" + + def setUp(self): + """Sets up the objects needed for this test.""" + self.output = StringIO.StringIO() + self.formatter = tln.Tln(None, self.output) + self.event_object = TlnTestEvent() + + def testStart(self): + """Test ensures header line is outputted as expected.""" + correct_line = u'Time|Source|Host|User|Description\n' + + self.formatter.Start() + self.assertEquals(self.output.getvalue(), correct_line) + + def testEventBody(self): + """Test ensures that returned lines returned are formatted as TLN.""" + + self.formatter.EventBody(self.event_object) + correct = (u'1340821021|LOG|ubuntu|root|Reporter PID: 8442 ' + u'(pam_unix(cron:session): session closed for user root)\n') + self.assertEquals(self.output.getvalue(), correct) + + def testEventBodyNoStrayPipes(self): + """Test ensures that the only pipes are the four field delimiters.""" + + self.formatter.EventBody(self.event_object) + self.assertEquals(self.output.getvalue().count(u'|'), 4) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/__init__.py b/plaso/parsers/__init__.py new file mode 100644 index 0000000..76649f1 --- /dev/null +++ b/plaso/parsers/__init__.py @@ -0,0 +1,73 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains an import statement for each parser.""" + +from plaso.parsers import asl +from plaso.parsers import android_app_usage +from plaso.parsers import bencode_parser +from plaso.parsers import bsm +from plaso.parsers import chrome_cache +from plaso.parsers import cups_ipp +from plaso.parsers import custom_destinations +from plaso.parsers import esedb +from plaso.parsers import filestat +from plaso.parsers import firefox_cache +from plaso.parsers import hachoir +from plaso.parsers import iis +from plaso.parsers import java_idx +from plaso.parsers import mac_appfirewall +from plaso.parsers import mac_keychain +from plaso.parsers import mac_securityd +from plaso.parsers import mac_wifi +from plaso.parsers import mactime +from plaso.parsers import mcafeeav +from plaso.parsers import msiecf +from plaso.parsers import olecf +from plaso.parsers import opera +from plaso.parsers import oxml +from plaso.parsers import pcap +from plaso.parsers import plist +from plaso.parsers import popcontest +from plaso.parsers import pls_recall +from plaso.parsers import recycler +from plaso.parsers import rubanetra +from plaso.parsers import selinux +from plaso.parsers import skydrivelog +from plaso.parsers import skydrivelogerr +from plaso.parsers import sqlite +from plaso.parsers import symantec +from plaso.parsers import syslog +from plaso.parsers import utmp +from plaso.parsers import utmpx +from plaso.parsers import winevt +from plaso.parsers import winevtx +from plaso.parsers import winfirewall +from plaso.parsers import winjob +from plaso.parsers import winlnk +from plaso.parsers import winprefetch +from plaso.parsers import winreg +from plaso.parsers import xchatlog +from plaso.parsers import xchatscrollback + +# Register plugins. +from plaso.parsers import bencode_plugins +from plaso.parsers import esedb_plugins +from plaso.parsers import olecf_plugins +from plaso.parsers import plist_plugins +from plaso.parsers import sqlite_plugins +from plaso.parsers import winreg_plugins diff --git a/plaso/parsers/android_app_usage.py b/plaso/parsers/android_app_usage.py new file mode 100644 index 0000000..f9ca0ac --- /dev/null +++ b/plaso/parsers/android_app_usage.py @@ -0,0 +1,126 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a parser for the Android usage-history.xml file.""" + +import os + +from xml.etree import ElementTree +from dfvfs.helpers import text_file + +from plaso.lib import errors +from plaso.lib import event +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import interface +from plaso.parsers import manager + + +class AndroidAppUsageEvent(event.EventObject): + """EventObject for an Android Application Last Resumed event.""" + + DATA_TYPE = 'android:event:last_resume_time' + + def __init__(self, last_resume_time, package, component): + """Initializes the event object. + + Args: + last_resume_time: The Last Resume Time of an Android App with details of + individual components. The timestamp contains the number of + milliseconds since Jan 1, 1970 00:00:00 UTC. + package: The name of the Android App. + component: The individual component of the App. + """ + super(AndroidAppUsageEvent, self).__init__() + self.timestamp = timelib.Timestamp.FromJavaTime(last_resume_time) + self.package = package + self.component = component + + self.timestamp_desc = eventdata.EventTimestamp.LAST_RESUME_TIME + + +class AndroidAppUsageParser(interface.BaseParser): + """Parses the Android usage-history.xml file.""" + + NAME = 'android_app_usage' + DESCRIPTION = u'Parser for the Android usage-history.xml file.' + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract the Android usage-history file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + file_object = file_entry.GetFileObject() + file_object.seek(0, os.SEEK_SET) + + text_file_object = text_file.TextFile(file_object) + + # Need to verify the first line to make sure this is a) XML and + # b) the right XML. + first_line = text_file_object.readline(90) + + # Note that we must check the data here as a string first, otherwise + # forcing first_line to convert to Unicode can raise a UnicodeDecodeError. + if not first_line.startswith('': + raise errors.UnableToParseFile( + u'Not an Android usage history file [wrong XML root key]') + + # For ElementTree to work we need to work on a filehandle seeked + # to the beginning. + file_object.seek(0, os.SEEK_SET) + + xml = ElementTree.parse(file_object) + root = xml.getroot() + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + for app in root: + for part in app.iter(): + if part.tag == 'comp': + package = app.get(u'name', '') + component = part.get(u'name', '') + + try: + last_resume_time = int(part.get('lrt', u''), 10) + except ValueError: + continue + + event_object = AndroidAppUsageEvent( + last_resume_time, package, component) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + file_object.close() + + +manager.ParsersManager.RegisterParser(AndroidAppUsageParser) diff --git a/plaso/parsers/android_app_usage_test.py b/plaso/parsers/android_app_usage_test.py new file mode 100644 index 0000000..72874df --- /dev/null +++ b/plaso/parsers/android_app_usage_test.py @@ -0,0 +1,85 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Android Application Usage history parsers.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import android_app_usage as android_app_usage_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import android_app_usage +from plaso.parsers import test_lib + + +class AndroidAppUsageParserTest(test_lib.ParserTestCase): + """Tests for the Android Application Usage History parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = android_app_usage.AndroidAppUsageParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['usage-history.xml']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 28) + + event_object = event_objects[22] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-12-09 19:28:33.047000') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.component, + 'com.sec.android.widgetapp.ap.hero.accuweather.menu.MenuAdd') + + expected_msg = ( + u'Package: ' + u'com.sec.android.widgetapp.ap.hero.accuweather ' + u'Component: ' + u'com.sec.android.widgetapp.ap.hero.accuweather.menu.MenuAdd') + expected_msg_short = ( + u'Package: com.sec.android.widgetapp.ap.hero.accuweather ' + u'Component: com.sec.and...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + event_object = event_objects[17] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-09-27 19:45:55.675000') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals(event_object.package, 'com.google.android.gsf.login') + + expected_msg = ( + u'Package: ' + u'com.google.android.gsf.login ' + u'Component: ' + u'com.google.android.gsf.login.NameActivity') + expected_msg_short = ( + u'Package: com.google.android.gsf.login ' + u'Component: com.google.android.gsf.login...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/asl.py b/plaso/parsers/asl.py new file mode 100644 index 0000000..f970ba0 --- /dev/null +++ b/plaso/parsers/asl.py @@ -0,0 +1,412 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The Apple System Log Parser.""" + +import construct +import logging +import os + +from plaso.lib import errors +from plaso.lib import event +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import interface +from plaso.parsers import manager + + +__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)' + +# TODO: get the real name for the user of the group having the uid or gid. + + +class AslEvent(event.EventObject): + """Convenience class for an asl event.""" + + DATA_TYPE = 'mac:asl:event' + + def __init__( + self, timestamp, record_position, message_id, + level, record_header, read_uid, read_gid, computer_name, + sender, facility, message, extra_information): + """Initializes the event object. + + Args: + timestamp: timestamp of the entry. + record_position: position where the record start. + message_id: Identification value for an ASL message. + level: level of criticality. + record_header: header of the entry. + pid: identification number of the process. + uid: identification number of the owner of the process. + gid: identification number of the group of the process. + read_uid: the user ID that can read this file. If -1: all. + read_gid: the group ID that can read this file. If -1: all. + computer_name: name of the host. + sender: the process that insert the event. + facility: the part of the sender that create the event. + message: message of the event. + extra_information: extra fields associated to each entry. + """ + super(AslEvent, self).__init__() + self.pid = record_header.pid + self.user_sid = unicode(record_header.uid) + self.group_id = record_header.gid + self.timestamp = timestamp + self.timestamp_desc = eventdata.EventTimestamp.CREATION_TIME + self.record_position = record_position + self.message_id = message_id + self.level = level + self.read_uid = read_uid + self.read_gid = read_gid + self.computer_name = computer_name + self.sender = sender + self.facility = facility + self.message = message + self.extra_information = extra_information + + +class AslParser(interface.BaseParser): + """Parser for ASL log files.""" + + NAME = 'asl_log' + DESCRIPTION = u'Parser for ASL log files.' + + ASL_MAGIC = 'ASL DB\x00\x00\x00\x00\x00\x00' + + # If not right assigned, the value is "-1". + ASL_NO_RIGHTS = 'ffffffff' + + # Priority level (criticity) + ASL_MESSAGE_PRIORITY = { + 0 : 'EMERGENCY', + 1 : 'ALERT', + 2 : 'CRITICAL', + 3 : 'ERROR', + 4 : 'WARNING', + 5 : 'NOTICE', + 6 : 'INFO', + 7 : 'DEBUG'} + + # ASL File header. + # magic: magic number that identify ASL files. + # version: version of the file. + # offset: first record in the file. + # timestamp: epoch time when the first entry was written. + # last_offset: last record in the file. + ASL_HEADER_STRUCT = construct.Struct( + 'asl_header_struct', + construct.String('magic', 12), + construct.UBInt32('version'), + construct.UBInt64('offset'), + construct.UBInt64('timestamp'), + construct.UBInt32('cache_size'), + construct.UBInt64('last_offset'), + construct.Padding(36)) + + # The record structure is: + # [HEAP][STRUCTURE][4xExtraField][2xExtraField]*[PreviousEntry] + # Record static structure. + # tam_entry: it contains the number of bytes from this file position + # until the end of the record, without counts itself. + # next_offset: next record. If is equal to 0x00, it is the last record. + # asl_message_id: integer that has the numeric identification of the event. + # timestamp: Epoch integer that has the time when the entry was created. + # nanosecond: nanosecond to add to the timestamp. + # level: level of priority. + # pid: process identification that ask to save the record. + # uid: user identification that has lunched the process. + # gid: group identification that has lunched the process. + # read_uid: identification id of a user. Only applied if is not -1 (all FF). + # Only root and this user can read the entry. + # read_gid: the same than read_uid, but for the group. + ASL_RECORD_STRUCT = construct.Struct( + 'asl_record_struct', + construct.Padding(2), + construct.UBInt32('tam_entry'), + construct.UBInt64('next_offset'), + construct.UBInt64('asl_message_id'), + construct.UBInt64('timestamp'), + construct.UBInt32('nanosec'), + construct.UBInt16('level'), + construct.UBInt16('flags'), + construct.UBInt32('pid'), + construct.UBInt32('uid'), + construct.UBInt32('gid'), + construct.UBInt32('read_uid'), + construct.UBInt32('read_gid'), + construct.UBInt64('ref_pid')) + + ASL_RECORD_STRUCT_SIZE = ASL_RECORD_STRUCT.sizeof() + + # 8-byte fields, they can be: + # - String: [Nibble = 1000 (8)][Nibble = Length][7 Bytes = String]. + # - Integer: integer that has the byte position in the file that points + # to an ASL_RECORD_DYN_VALUE struct. If the value of the integer + # is equal to 0, it means that it has not data (skip). + + # If the field is a String, we use this structure to decode each + # integer byte in the corresponding character (ASCII Char). + ASL_OCTET_STRING = construct.ExprAdapter( + construct.Octet('string'), + encoder=lambda obj, ctx: ord(obj), + decoder=lambda obj, ctx: chr(obj)) + + # Field string structure. If the first bit is 1, it means that it + # is a String (1000) = 8, then the next nibble has the number of + # characters. The last 7 bytes are the number of bytes. + ASL_STRING = construct.BitStruct( + 'string', + construct.Flag('type'), + construct.Bits('filler', 3), + construct.If( + lambda ctx: ctx.type, + construct.Nibble('string_length')), + construct.If( + lambda ctx: ctx.type, + construct.Array(7, ASL_OCTET_STRING))) + + # 8-byte pointer to a byte position in the file. + ASL_POINTER = construct.UBInt64('pointer') + + # Dynamic data structure pointed by a pointer that contains a String: + # [2 bytes padding][4 bytes lenght of String][String]. + ASL_RECORD_DYN_VALUE = construct.Struct( + 'asl_record_dyn_value', + construct.Padding(2), + construct.PascalString( + 'value', + length_field=construct.UBInt32('length'))) + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract entries from an ASL file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + file_object = file_entry.GetFileObject() + file_object.seek(0, os.SEEK_SET) + + try: + header = self.ASL_HEADER_STRUCT.parse_stream(file_object) + except (IOError, construct.FieldError) as exception: + file_object.close() + raise errors.UnableToParseFile( + u'Unable to parse ASL Header with error: {0:s}.'.format(exception)) + + if header.magic != self.ASL_MAGIC: + file_object.close() + raise errors.UnableToParseFile(u'Not an ASL Header, unable to parse.') + + # Get the first and the last entry. + offset = header.offset + old_offset = header.offset + last_offset_header = header.last_offset + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + # If the ASL file has entries. + if offset: + event_object, offset = self.ReadAslEvent(file_object, offset) + while event_object: + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + # TODO: an anomaly object must be emitted once that is implemented. + # Sanity check, the last read element must be the same as + # indicated by the header. + if offset == 0 and old_offset != last_offset_header: + logging.warning(u'Parsing ended before the header ends.') + old_offset = offset + event_object, offset = self.ReadAslEvent(file_object, offset) + + file_object.close() + + def ReadAslEvent(self, file_object, offset): + """Returns an AslEvent from a single ASL entry. + + Args: + file_object: a file-like object that points to an ASL file. + offset: offset where the static part of the entry starts. + + Returns: + An event object constructed from a single ASL record. + """ + # The heap of the entry is saved to try to avoid seek (performance issue). + # It has the real start position of the entry. + dynamic_start = file_object.tell() + dynamic_part = file_object.read(offset - file_object.tell()) + + if not offset: + return None, None + + try: + record_header = self.ASL_RECORD_STRUCT.parse_stream(file_object) + except (IOError, construct.FieldError) as exception: + logging.warning( + u'Unable to parse ASL event with error: {0:s}'.format(exception)) + return None, None + + # Variable tam_fields = is the real length of the dynamic fields. + # We have this: [Record_Struct] + [Dynamic_Fields] + [Pointer_Entry_Before] + # In Record_Struct we have a field called tam_entry, where it has the number + # of bytes until the end of the entry from the position that the field is. + # The tam_entry is between the 2th and the 6th byte in the [Record_Struct]. + # tam_entry = ([Record_Struct]-6)+[Dynamic_Fields]+[Pointer_Entry_Before] + # Also, we do not need [Point_Entry_Before] and then we delete the size of + # [Point_Entry_Before] that it is 8 bytes (8): + # tam_entry = ([Record_Struct]-6)+[Dynamic_Fields]+[Pointer_Entry_Before] + # [Dynamic_Fields] = tam_entry - [Record_Struct] + 6 - 8 + # [Dynamic_Fields] = tam_entry - [Record_Struct] - 2 + tam_fields = record_header.tam_entry - self.ASL_RECORD_STRUCT_SIZE - 2 + + # Dynamic part of the entry that contains minimal four fields of 8 bytes + # plus 2x[8bytes] fields for each extra ASL_Field. + # The four first fields are always the Host, Sender, Facility and Message. + # After the four first fields, the entry might have extra ASL_Fields. + # For each extra ASL_field, it has a pair of 8-byte fields where the first + # 8 bytes contains the name of the extra ASL_field and the second 8 bytes + # contains the text of the exta field. + # All of this 8-byte field can be saved using one of these three differents + # types: + # - Null value ('0000000000000000'): nothing to do. + # - String: It is string if first bit = 1 or first nibble = 8 (1000). + # Second nibble has the length of string. + # The next 7 bytes have the text characters of the string + # padding the end with null characters: '0x00'. + # Example: [8468 6964 6400 0000] + # [8] String, [4] length, value: [68 69 64 64] = hidd. + # - Pointer: static position in the file to a special struct + # implemented as an ASL_RECORD_DYN_VALUE. + # Example: [0000 0000 0000 0077] + # It points to the file position 0x077 that has a + # ASL_RECORD_DYN_VALUE structure. + values = [] + while tam_fields > 0: + try: + raw_field = file_object.read(8) + except (IOError, construct.FieldError) as exception: + logging.warning( + u'Unable to parse ASL event with error: {0:d}'.format(exception)) + return None, None + try: + # Try to read as a String. + field = self.ASL_STRING.parse(raw_field) + values.append(''.join(field.string[0:field.string_length])) + # Go to parse the next extra field. + tam_fields -= 8 + continue + except ValueError: + pass + # If it is not a string, it must be a pointer. + try: + field = self.ASL_POINTER.parse(raw_field) + except ValueError as exception: + logging.warning( + u'Unable to parse ASL event with error: {0:s}'.format(exception)) + return None, None + if field != 0: + # The next IF ELSE is only for performance issues, avoiding seek. + # If the pointer points a lower position than where the actual entry + # starts, it means that it points to a previuos entry. + pos = field - dynamic_start + # Bigger or equal 0 means that the data is in the actual entry. + if pos >= 0: + try: + values.append((self.ASL_RECORD_DYN_VALUE.parse( + dynamic_part[pos:])).value.partition('\x00')[0]) + except (IOError, construct.FieldError) as exception: + logging.warning( + u'Unable to parse ASL event with error: {0:s}'.format( + exception)) + return None, None + else: + # Only if it is a pointer that points to the + # heap from another entry we use the seek method. + main_position = file_object.tell() + # If the pointer is in a previous entry. + if main_position > field: + file_object.seek(field - main_position, os.SEEK_CUR) + try: + values.append((self.ASL_RECORD_DYN_VALUE.parse_stream( + file_object)).value.partition('\x00')[0]) + except (IOError, construct.FieldError): + logging.warning(( + u'The pointer at {0:d} (0x{0:x}) points to invalid ' + u'information.').format( + main_position - self.ASL_POINTER.sizeof())) + # Come back to the position in the entry. + _ = file_object.read(main_position - file_object.tell()) + else: + _ = file_object.read(field - main_position) + values.append((self.ASL_RECORD_DYN_VALUE.parse_stream( + file_object)).value.partition('\x00')[0]) + # Come back to the position in the entry. + file_object.seek(main_position - file_object.tell(), os.SEEK_CUR) + # Next extra field: 8 bytes more. + tam_fields -= 8 + + # Read the last 8 bytes of the record that points to the previous entry. + _ = file_object.read(8) + + # Parsed section, we translate the read data to an appropriate format. + microsecond = record_header.nanosec // 1000 + timestamp = timelib.Timestamp.FromPosixTimeWithMicrosecond( + record_header.timestamp, microsecond) + record_position = offset + message_id = record_header.asl_message_id + level = u'{0} ({1})'.format( + self.ASL_MESSAGE_PRIORITY[record_header.level], record_header.level) + # If the value is -1 (FFFFFFFF), it can be read by everyone. + if record_header.read_uid != int(self.ASL_NO_RIGHTS, 16): + read_uid = record_header.read_uid + else: + read_uid = 'ALL' + if record_header.read_gid != int(self.ASL_NO_RIGHTS, 16): + read_gid = record_header.read_gid + else: + read_gid = 'ALL' + + # Parsing the dynamic values (text or pointers to position with text). + # The first four are always the host, sender, facility, and message. + computer_name = values[0] + sender = values[1] + facility = values[2] + message = values[3] + + # If the entry has an extra fields, they works as a pairs: + # The first is the name of the field and the second the value. + extra_information = '' + if len(values) > 4: + values = values[4:] + for index in xrange(0, len(values) // 2): + extra_information += (u'[{0}: {1}]'.format( + values[index * 2], values[(index * 2) + 1])) + + # Return the event and the offset for the next entry. + return AslEvent( + timestamp, record_position, message_id, level, record_header, read_uid, + read_gid, computer_name, sender, facility, message, + extra_information), record_header.next_offset + + +manager.ParsersManager.RegisterParser(AslParser) diff --git a/plaso/parsers/asl_test.py b/plaso/parsers/asl_test.py new file mode 100644 index 0000000..68df720 --- /dev/null +++ b/plaso/parsers/asl_test.py @@ -0,0 +1,96 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for Apple System Log file parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import asl as asl_formatter +from plaso.lib import timelib_test +from plaso.parsers import asl +from plaso.parsers import test_lib + + +class AslParserTest(test_lib.ParserTestCase): + """Tests for Apple System Log file parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = asl.AslParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['applesystemlog.asl']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEqual(len(event_objects), 2) + + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-25 09:45:35.705481') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.record_position, 442) + self.assertEqual(event_object.message_id, 101406) + self.assertEqual(event_object.computer_name, u'DarkTemplar-2.local') + self.assertEqual(event_object.sender, u'locationd') + self.assertEqual(event_object.facility, u'com.apple.locationd') + self.assertEqual(event_object.pid, 69) + self.assertEqual(event_object.user_sid, u'205') + self.assertEqual(event_object.group_id, 205) + self.assertEqual(event_object.read_uid, 205) + self.assertEqual(event_object.read_gid, 'ALL') + self.assertEqual(event_object.level, u'WARNING (4)') + + expected_message = ( + u'Incorrect NSStringEncoding value 0x8000100 detected. ' + u'Assuming NSASCIIStringEncoding. Will stop this compatiblity ' + u'mapping behavior in the near future.') + + self.assertEqual(event_object.message, expected_message) + + expected_extra = ( + u'[CFLog Local Time: 2013-11-25 09:45:35.701]' + u'[CFLog Thread: 1007]' + u'[Sender_Mach_UUID: 50E1F76A-60FF-368C-B74E-EB48F6D98C51]') + + self.assertEqual(event_object.extra_information, expected_extra) + + expected_msg = ( + u'MessageID: 101406 ' + u'Level: WARNING (4) ' + u'User ID: 205 ' + u'Group ID: 205 ' + u'Read User: 205 ' + u'Read Group: ALL ' + u'Host: DarkTemplar-2.local ' + u'Sender: locationd ' + u'Facility: com.apple.locationd ' + u'Message: {0:s} {1:s}').format(expected_message, expected_extra) + + expected_msg_short = ( + u'Sender: locationd ' + u'Facility: com.apple.locationd') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/bencode_parser.py b/plaso/parsers/bencode_parser.py new file mode 100644 index 0000000..474c5ec --- /dev/null +++ b/plaso/parsers/bencode_parser.py @@ -0,0 +1,121 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the Bencode Parser. + +Plaso's engine calls BencodeParser when it encounters bencoded files to be +processed, typically seen for BitTorrent data. +""" + +import logging +import re +import os + +import bencode + +from plaso.lib import errors +from plaso.parsers import interface +from plaso.parsers import manager + + +class BencodeParser(interface.BasePluginsParser): + """Deserializes bencoded file; produces a dictionary containing bencoded data. + + The Plaso engine calls parsers by their Parse() method. This parser's + Parse() has GetTopLevel() which deserializes bencoded files using the + BitTorrent-bencode library and calls plugins (BencodePlugin) registered + through the interface by their Process() to produce event objects. + + Plugins are how this parser understands the content inside a bencoded file, + each plugin holds logic specific to a particular bencoded file. See the + bencode_plugins / directory for examples of how bencode plugins are + implemented. + """ + + # Regex match for a bencode dictionary followed by a field size. + BENCODE_RE = re.compile('d[0-9]') + + NAME = 'bencode' + DESCRIPTION = u'Parser for bencoded files.' + + _plugin_classes = {} + + def __init__(self): + """Initializes a parser object.""" + super(BencodeParser, self).__init__() + self._plugins = BencodeParser.GetPluginObjects() + + def GetTopLevel(self, file_object): + """Returns deserialized content of a bencoded file as a dictionary object. + + Args: + file_object: A file-like object. + + Returns: + Dictionary object representing the contents of the bencoded file. + """ + header = file_object.read(2) + file_object.seek(0, os.SEEK_SET) + + if not self.BENCODE_RE.match(header): + raise errors.UnableToParseFile(u'Not a valid Bencoded file.') + + try: + data_object = bencode.bdecode(file_object.read()) + except (IOError, bencode.BTFailure) as exception: + raise errors.UnableToParseFile( + u'Unable to parse invalid Bencoded file with error: {0:s}'.format( + exception)) + + if not data_object: + raise errors.UnableToParseFile(u'Not a valid Bencoded file.') + + return data_object + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Parse and extract values from a bencoded file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + file_object = file_entry.GetFileObject() + data_object = self.GetTopLevel(file_object) + + if not data_object: + file_object.close() + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse: {1:s}. Skipping.'.format( + self.NAME, file_entry.name)) + + parser_chain = self._BuildParserChain(parser_chain) + for plugin_object in self._plugins: + try: + plugin_object.Process( + parser_context, data=data_object, file_entry=file_entry, + parser_chain=parser_chain) + + except errors.WrongBencodePlugin as exception: + logging.debug(u'[{0:s}] wrong plugin: {1:s}'.format( + self.NAME, exception)) + + file_object.close() + + +manager.ParsersManager.RegisterParser(BencodeParser) diff --git a/plaso/parsers/bencode_parser_test.py b/plaso/parsers/bencode_parser_test.py new file mode 100644 index 0000000..7e578ec --- /dev/null +++ b/plaso/parsers/bencode_parser_test.py @@ -0,0 +1,143 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for Bencode file parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import bencode_parser as bencode_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import bencode_parser +from plaso.parsers import test_lib + + +class BencodeTest(test_lib.ParserTestCase): + """Tests for Bencode file parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = bencode_parser.BencodeParser() + + # TODO: Move this to bencode_plugins/tranmission_test.py + def testTransmissionPlugin(self): + """Read Transmission activity files and make few tests.""" + test_file = self._GetTestFilePath(['bencode_transmission']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEqual(len(event_objects), 3) + + event_object = event_objects[0] + + destination_expected = u'/Users/brian/Downloads' + self.assertEqual(event_object.destination, destination_expected) + + self.assertEqual(event_object.seedtime, 4) + + description_expected = eventdata.EventTimestamp.ADDED_TIME + self.assertEqual(event_object.timestamp_desc, description_expected) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-08 15:31:20') + self.assertEqual(event_object.timestamp, expected_timestamp) + + # Test on second event of first torrent. + event_object = event_objects[1] + self.assertEqual(event_object.destination, destination_expected) + self.assertEqual(event_object.seedtime, 4) + + description_expected = eventdata.EventTimestamp.FILE_DOWNLOADED + self.assertEqual(event_object.timestamp_desc, description_expected) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-08 18:24:24') + self.assertEqual(event_object.timestamp, expected_timestamp) + + def testUTorrentPlugin(self): + """Parse a uTorrent resume.dat file and make a few tests.""" + test_file = self._GetTestFilePath(['bencode_utorrent']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEqual(len(event_objects), 4) + + caption_expected = u'plaso test' + path_expected = u'e:\\torrent\\files\\plaso test' + + # First test on when the torrent was added to the client. + event_object = event_objects[3] + + self.assertEqual(event_object.caption, caption_expected) + + self.assertEqual(event_object.path, path_expected) + + self.assertEqual(event_object.seedtime, 511) + + description_expected = eventdata.EventTimestamp.ADDED_TIME + self.assertEqual(event_object.timestamp_desc, description_expected) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-08-03 14:52:12') + self.assertEqual(event_object.timestamp, expected_timestamp) + + # Second test on when the torrent file was completely downloaded. + event_object = event_objects[2] + + self.assertEqual(event_object.caption, caption_expected) + self.assertEqual(event_object.path, path_expected) + self.assertEqual(event_object.seedtime, 511) + + description_expected = eventdata.EventTimestamp.FILE_DOWNLOADED + self.assertEqual(event_object.timestamp_desc, description_expected) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-08-03 18:11:35') + self.assertEqual(event_object.timestamp, expected_timestamp) + + # Third test on when the torrent was first modified. + event_object = event_objects[0] + + self.assertEqual(event_object.caption, caption_expected) + self.assertEqual(event_object.path, path_expected) + self.assertEqual(event_object.seedtime, 511) + + description_expected = eventdata.EventTimestamp.MODIFICATION_TIME + self.assertEqual(event_object.timestamp_desc, description_expected) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-08-03 18:11:34') + self.assertEqual(event_object.timestamp, expected_timestamp) + + # Fourth test on when the torrent was again modified. + event_object = event_objects[1] + + self.assertEqual(event_object.caption, caption_expected) + self.assertEqual(event_object.path, path_expected) + self.assertEqual(event_object.seedtime, 511) + + description_expected = eventdata.EventTimestamp.MODIFICATION_TIME + self.assertEqual(event_object.timestamp_desc, description_expected) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-08-03 16:27:59') + self.assertEqual(event_object.timestamp, expected_timestamp) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/bencode_plugins/__init__.py b/plaso/parsers/bencode_plugins/__init__.py new file mode 100644 index 0000000..8274cec --- /dev/null +++ b/plaso/parsers/bencode_plugins/__init__.py @@ -0,0 +1,20 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains an import statement for each bencode related plugin.""" + +from plaso.parsers.bencode_plugins import transmission +from plaso.parsers.bencode_plugins import utorrent diff --git a/plaso/parsers/bencode_plugins/interface.py b/plaso/parsers/bencode_plugins/interface.py new file mode 100644 index 0000000..c5062aa --- /dev/null +++ b/plaso/parsers/bencode_plugins/interface.py @@ -0,0 +1,205 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""bencode_interface contains basic interface for bencode plugins within Plaso. + +Bencoded files are only one example of a type of object that the Plaso tool is +expected to encounter and process. There can be and are many other parsers +which are designed to process specific data types. + +BencodePlugin defines the attributes necessary for registration, discovery +and operation of plugins for bencoded files which will be used by +BencodeParser. +""" + +import abc +import logging + +from plaso.lib import errors +from plaso.parsers import plugins + + +class BencodePlugin(plugins.BasePlugin): + """This is an abstract class from which plugins should be based.""" + + # BENCODE_KEYS is a list of keys required by a plugin. + # This is expected to be overridden by the processing plugin. + # Ex. frozenset(['activity-date', 'done-date']) + BENCODE_KEYS = frozenset(['any']) + + # This is expected to be overridden by the processing plugin. + # URLS should contain a list of URLs with additional information about + # this key or value. + # Ex. ['https://wiki.theory.org/BitTorrentSpecification#Bencoding'] + URLS = [] + + NAME = 'bencode' + + def _GetKeys(self, data, keys, depth=1): + """Helper function to return keys nested in a bencode dict. + + By default this function will return the values for the named keys requested + by a plugin in match{}. The default setting is to look a single layer down + from the root (same as the check for plugin applicability). This level is + suitable for most cases. + + For cases where there is variability in the name at the first level + (e.g. it is the MAC addresses of a device, or a UUID) it is possible to + override the depth limit and use _GetKeys to fetch from a deeper level. + + Args: + data: bencode data in dictionary form. + keys: A list of keys that should be returned. + depth: Defines how many levels deep to check for a match. + + Returns: + A dictionary with just the keys requested. + """ + keys = set(keys) + match = {} + + if depth == 1: + for key in keys: + match[key] = data[key] + else: + for _, parsed_key, parsed_value in self._RecurseKey( + data, depth=depth): + if parsed_key in keys: + match[parsed_key] = parsed_value + if set(match.keys()) == keys: + return match + return match + + def _RecurseKey(self, recur_item, root='', depth=15): + """Flattens nested dictionaries and lists by yielding it's values. + + The hierarchy of a bencode file is a series of nested dictionaries and + lists. This is a helper function helps plugins navigate the structure + without having to reimplement their own recursive methods. + + This method implements an overridable depth limit to prevent processing + extremely deeply nested dictionaries. If the limit is reached a debug + message is logged indicating which key processing stopped on. + + Args: + recur_item: An object to be checked for additional nested items. + root: The pathname of the current working key. + depth: A counter to ensure we stop at the maximum recursion depth. + + Yields: + A tuple of the root, key, and value from a bencoded file. + """ + if depth < 1: + logging.debug(u'Recursion limit hit for key: {0:s}'.format(root)) + return + + if type(recur_item) in (list, tuple): + for recur in recur_item: + for key in self._RecurseKey(recur, root, depth): + yield key + return + + if not hasattr(recur_item, 'iteritems'): + return + + for key, value in recur_item.iteritems(): + yield root, key, value + if isinstance(value, dict): + value = [value] + if isinstance(value, list): + for item in value: + if isinstance(item, dict): + for keyval in self._RecurseKey( + item, root=root + u'/' + key, depth=depth - 1): + yield keyval + + @abc.abstractmethod + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, data=None, + match=None, **kwargs): + """Extracts event object from the values of entries within a bencoded file. + + This is the main method that a bencode plugin needs to implement. + + The contents of the bencode keys defined in BENCODE_KEYS can be made + available to the plugin as both a matched{'KEY': 'value'} and as the + entire bencoded data dictionary. The plugin should implement logic to parse + the most relevant data set into a useful event for incorporation into the + Plaso timeline. + + The attributes for a BencodeEvent should include the following: + root = Root key this event was extracted from. + key = Key the value resided in. + time = Date this artifact was created in microseconds(usec) from epoch. + desc = Short description. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + data: Bencode data in dictionary form. The default is None. + match: Optional dictionary containing only the keys selected in the + BENCODE_KEYS. The default is None. + """ + + def Process( + self, parser_context, file_entry=None, parser_chain=None, + data=None, **kwargs): + """Determine if this is the correct plugin; if so proceed with processing. + + Process() checks if the current bencode file being processed is a match for + a plugin by comparing the PATH and KEY requirements defined by a plugin. If + both match processing continues; else raise WrongBencodePlugin. + + This function also extracts the required keys as defined in + self.BENCODE_KEYS from the file and stores the result in match[key] + and calls self.GetEntries() which holds the processing logic implemented by + the plugin. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + data: Bencode data in dictionary form. The default is None. + + Raises: + WrongBencodePlugin: If this plugin is not able to process the given file. + ValueError: If top level is not set. + """ + if data is None: + raise ValueError(u'Data is not set.') + + if not set(data.keys()).issuperset(self.BENCODE_KEYS): + raise errors.WrongBencodePlugin(self.NAME) + + # This will raise if unhandled keyword arguments are passed. + super(BencodePlugin, self).Process(parser_context, **kwargs) + + logging.debug(u'Bencode Plugin Used: {0:s}'.format(self.NAME)) + match = self._GetKeys(data, self.BENCODE_KEYS, 3) + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + self.GetEntries( + parser_context, file_entry=file_entry, parser_chain=parser_chain, + data=data, match=match) diff --git a/plaso/parsers/bencode_plugins/test_lib.py b/plaso/parsers/bencode_plugins/test_lib.py new file mode 100644 index 0000000..caeeadd --- /dev/null +++ b/plaso/parsers/bencode_plugins/test_lib.py @@ -0,0 +1,24 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Bencode plugin related functions and classes for testing.""" + +from plaso.parsers import test_lib + + +class BencodePluginTestCase(test_lib.ParserTestCase): + """The unit test case for a bencode plugin.""" diff --git a/plaso/parsers/bencode_plugins/transmission.py b/plaso/parsers/bencode_plugins/transmission.py new file mode 100644 index 0000000..63661f8 --- /dev/null +++ b/plaso/parsers/bencode_plugins/transmission.py @@ -0,0 +1,102 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a bencode plugin for Transmission BitTorrent data.""" + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.parsers import bencode_parser +from plaso.parsers.bencode_plugins import interface + + +class TransmissionEvent(time_events.PosixTimeEvent): + """Convenience class for a Transmission BitTorrent activity event.""" + + DATA_TYPE = 'p2p:bittorrent:transmission' + + def __init__(self, timestamp, timestamp_description, destination, seedtime): + """Initializes the event. + + Args: + timestamp: The POSIX timestamp of the event. + timestamp_desc: A short description of the meaning of the timestamp. + destination: Downloaded file name within .torrent file + seedtime: Number of seconds client seeded torrent + """ + super(TransmissionEvent, self).__init__(timestamp, timestamp_description) + self.destination = destination + self.seedtime = seedtime // 60 # Convert seconds to minutes. + + +class TransmissionPlugin(interface.BencodePlugin): + """Parse Transmission BitTorrent activity file for current torrents.""" + + NAME = 'bencode_transmission' + DESCRIPTION = u'Parser for Transmission bencoded files.' + + BENCODE_KEYS = frozenset([ + 'activity-date', 'done-date', 'added-date', 'destination', + 'seeding-time-seconds']) + + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, data=None, + **unused_kwargs): + """Extract data from Transmission's resume folder files. + + This is the main parsing engine for the parser. It determines if + the selected file is the proper file to parse and extracts current + running torrents. + + Transmission stores an individual Bencoded file for each active download + in a folder named resume under the user's application data folder. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + data: Optional bencode data in dictionary form. The default is None. + """ + # Place the obtained values into the event. + destination = data.get('destination', None) + seeding_time = data.get('seeding-time-seconds', None) + + # Create timeline events based on extracted values. + if data.get('added-date', 0): + event_object = TransmissionEvent( + data.get('added-date'), eventdata.EventTimestamp.ADDED_TIME, + destination, seeding_time) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if data.get('done-date', 0): + event_object = TransmissionEvent( + data.get('done-date'), eventdata.EventTimestamp.FILE_DOWNLOADED, + destination, seeding_time) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if data.get('activity-date', None): + event_object = TransmissionEvent( + data.get('activity-date'), eventdata.EventTimestamp.ACCESS_TIME, + destination, seeding_time) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +bencode_parser.BencodeParser.RegisterPlugin(TransmissionPlugin) diff --git a/plaso/parsers/bencode_plugins/utorrent.py b/plaso/parsers/bencode_plugins/utorrent.py new file mode 100644 index 0000000..c5a9168 --- /dev/null +++ b/plaso/parsers/bencode_plugins/utorrent.py @@ -0,0 +1,137 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a bencode plugin for uTorrent data.""" + +from plaso.events import time_events +from plaso.lib import errors +from plaso.lib import eventdata +from plaso.parsers import bencode_parser +from plaso.parsers.bencode_plugins import interface + + +class UTorrentEvent(time_events.PosixTimeEvent): + """Convenience class for a uTorrent active torrents history entries.""" + + DATA_TYPE = 'p2p:bittorrent:utorrent' + + def __init__( + self, timestamp, timestamp_description, path, caption, seedtime): + """Initialize the event. + + Args: + path: Torrent download location + caption: Official name of package + seedtime: Number of seconds client seeded torrent + """ + super(UTorrentEvent, self).__init__(timestamp, timestamp_description) + self.path = path + self.caption = caption + self.seedtime = seedtime // 60 # Convert seconds to minutes. + + +class UTorrentPlugin(interface.BencodePlugin): + """Plugin to extract uTorrent active torrent events.""" + + NAME = 'bencode_utorrent' + DESCRIPTION = u'Parser for uTorrent bencoded files.' + + # The following set is used to determine if the bencoded data is appropriate + # for this plugin. If there's a match, the entire bencoded data block is + # returned for analysis. + BENCODE_KEYS = frozenset(['.fileguard']) + + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, data=None, + **unused_kwargs): + """Extracts uTorrent active torrents. + + This is the main parsing engine for the plugin. It determines if + the selected file is the proper file to parse and extracts current + running torrents. + + interface.Process() checks for the given BENCODE_KEYS set, ensures + that it matches, and then passes the bencoded data to this function for + parsing. This plugin then parses the entire set of bencoded data to extract + the variable file-name keys to retrieve their values. + + uTorrent creates a file, resume.dat, and a backup, resume.dat.old, to + for all active torrents. This is typically stored in the user's + application data folder. + + These files, at a minimum, contain a '.fileguard' key and a dictionary + with a key name for a particular download with a '.torrent' file + extension. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + data: Optional bencode data in dictionary form. The default is None. + """ + # Walk through one of the torrent keys to ensure it's from a valid file. + for key, value in data.iteritems(): + if not u'.torrent' in key: + continue + + caption = value.get('caption') + path = value.get('path') + seedtime = value.get('seedtime') + if not caption or not path or seedtime < 0: + raise errors.WrongBencodePlugin(self.NAME) + + for torrent, value in data.iteritems(): + if not u'.torrent' in torrent: + continue + + path = value.get('path', None) + caption = value.get('caption', None) + seedtime = value.get('seedtime', None) + + # Create timeline events based on extracted values. + for event_key, event_value in value.iteritems(): + if event_key == 'added_on': + event_object = UTorrentEvent( + event_value, eventdata.EventTimestamp.ADDED_TIME, + path, caption, seedtime) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + elif event_key == 'completed_on': + event_object = UTorrentEvent( + event_value, eventdata.EventTimestamp.FILE_DOWNLOADED, + path, caption, seedtime) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + elif event_key == 'modtimes': + for modtime in event_value: + # Some values are stored as 0, skip those. + if not modtime: + continue + + event_object = UTorrentEvent( + modtime, eventdata.EventTimestamp.MODIFICATION_TIME, + path, caption, seedtime) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, + file_entry=file_entry) + + +bencode_parser.BencodeParser.RegisterPlugin(UTorrentPlugin) diff --git a/plaso/parsers/bsm.py b/plaso/parsers/bsm.py new file mode 100644 index 0000000..1299426 --- /dev/null +++ b/plaso/parsers/bsm.py @@ -0,0 +1,1145 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Basic Security Module Parser.""" + +import binascii +import construct +import logging +import os +import socket + +from plaso.lib import errors +from plaso.lib import event +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.unix import bsmtoken +from plaso.parsers import interface +from plaso.parsers import manager + +import pytz + + +__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)' + + +# Note that we're using Array and a helper function here instead of +# PascalString because the latter seems to break pickling on Windows. + +def _BsmTokenGetLength(context): + """Contruct context parser helper function to replace lambda.""" + return context.length + + +# Note that we're using RepeatUntil and a helper function here instead of +# CString because the latter seems to break pickling on Windows. + +def _BsmTokenIsEndOfString(value, unused_context): + """Construct context parser helper function to replace lambda.""" + return value == '\x00' + + +# Note that we're using Switch and a helper function here instead of +# IfThenElse because the latter seems to break pickling on Windows. + +def _BsmTokenGetNetType(context): + """Construct context parser helper function to replace lambda.""" + return context.net_type + + +def _BsmTokenGetSocketDomain(context): + """Construct context parser helper function to replace lambda.""" + return context.socket_domain + + +class MacBsmEvent(event.EventObject): + """Convenience class for a Mac OS X BSM event.""" + + DATA_TYPE = 'mac:bsm:event' + + def __init__( + self, event_type, timestamp, extra_tokens, + return_value, record_length, offset): + """Initializes the event object. + + Args: + event_type: String with the text and ID that represents the event type. + timestamp: Entry Epoch timestamp in UTC. + extra_tokens: List of the extra tokens of the entry. + return_value: String with the process return value and exit status. + record_length: Record length in bytes (trailer number). + offset: The offset in bytes to where the record starts in the file. + """ + super(MacBsmEvent, self).__init__() + self.timestamp = timestamp + self.timestamp_desc = eventdata.EventTimestamp.CREATION_TIME + self.event_type = event_type + self.extra_tokens = extra_tokens + self.return_value = return_value + self.record_length = record_length + self.offset = offset + + +class BsmEvent(event.EventObject): + """Convenience class for a Generic BSM event.""" + + DATA_TYPE = 'bsm:event' + + def __init__( + self, event_type, timestamp, extra_tokens, record_length, offset): + """Initializes the event object. + + Args: + event_type: Text and integer ID that represents the type of the event. + timestamp: Timestamp of the entry. + extra_tokens: List of the extra tokens of the entry. + record_length: Record length in bytes (trailer number). + offset: The offset in bytes to where the record starts in the file. + """ + super(BsmEvent, self).__init__() + self.timestamp = timestamp + self.timestamp_desc = eventdata.EventTimestamp.CREATION_TIME + self.event_type = event_type + self.extra_tokens = extra_tokens + self.record_length = record_length + self.offset = offset + + +class BsmParser(interface.BaseParser): + """Parser for BSM files.""" + + NAME = 'bsm_log' + DESCRIPTION = u'Parser for BSM log files.' + + # BSM supported version (0x0b = 11). + AUDIT_HEADER_VERSION = 11 + + # Magic Trail Header. + BSM_TOKEN_TRAILER_MAGIC = 'b105' + + # IP Version constants. + AU_IPv4 = 4 + AU_IPv6 = 16 + + IPV4_STRUCT = construct.UBInt32('ipv4') + + IPV6_STRUCT = construct.Struct( + 'ipv6', construct.UBInt64('high'), construct.UBInt64('low')) + + # Tested structures. + # INFO: I have ommited the ID in the structures declaration. + # I used the BSM_TYPE first to read the ID, and then, the structure. + # Tokens always start with an ID value that identifies their token + # type and subsequent structure. + BSM_TYPE = construct.UBInt8('token_id') + + # Data type structures. + BSM_TOKEN_DATA_CHAR = construct.String('value', 1) + BSM_TOKEN_DATA_SHORT = construct.UBInt16('value') + BSM_TOKEN_DATA_INTEGER = construct.UBInt32('value') + + # Common structure used by other structures. + # audit_uid: integer, uid that generates the entry. + # effective_uid: integer, the permission user used. + # effective_gid: integer, the permission group used. + # real_uid: integer, user id of the user that execute the process. + # real_gid: integer, group id of the group that execute the process. + # pid: integer, identification number of the process. + # session_id: unknown, need research. + BSM_TOKEN_SUBJECT_SHORT = construct.Struct( + 'subject_data', + construct.UBInt32('audit_uid'), + construct.UBInt32('effective_uid'), + construct.UBInt32('effective_gid'), + construct.UBInt32('real_uid'), + construct.UBInt32('real_gid'), + construct.UBInt32('pid'), + construct.UBInt32('session_id')) + + # Common structure used by other structures. + # Identify the kind of inet (IPv4 or IPv6) + # TODO: instead of 16, AU_IPv6 must be used. + BSM_IP_TYPE_SHORT = construct.Struct( + 'bsm_ip_type_short', + construct.UBInt32('net_type'), + construct.Switch( + 'ip_addr', + _BsmTokenGetNetType, + {16: IPV6_STRUCT}, + default=IPV4_STRUCT)) + + # Initial fields structure used by header structures. + # length: integer, the length of the entry, equal to trailer (doc: length). + # version: integer, version of BSM (AUDIT_HEADER_VERSION). + # event_type: integer, the type of event (/etc/security/audit_event). + # modifier: integer, unknown, need research (It is always 0). + BSM_HEADER = construct.Struct( + 'bsm_header', + construct.UBInt32('length'), + construct.UBInt8('version'), + construct.UBInt16('event_type'), + construct.UBInt16('modifier')) + + # First token of one entry. + # timestamp: integer, Epoch timestamp of the entry. + # microsecond: integer, the microsecond of the entry. + BSM_HEADER32 = construct.Struct( + 'bsm_header32', + BSM_HEADER, + construct.UBInt32('timestamp'), + construct.UBInt32('microsecond')) + + BSM_HEADER64 = construct.Struct( + 'bsm_header64', + BSM_HEADER, + construct.UBInt64('timestamp'), + construct.UBInt64('microsecond')) + + BSM_HEADER32_EX = construct.Struct( + 'bsm_header32_ex', + BSM_HEADER, + BSM_IP_TYPE_SHORT, + construct.UBInt32('timestamp'), + construct.UBInt32('microsecond')) + + # Token TEXT, provides extra information. + BSM_TOKEN_TEXT = construct.Struct( + 'bsm_token_text', + construct.UBInt16('length'), + construct.Array( + _BsmTokenGetLength, + construct.UBInt8('text'))) + + # Path of the executable. + BSM_TOKEN_PATH = BSM_TOKEN_TEXT + + # Identified the end of the record (follow by TRAILER). + # status: integer that identifies the status of the exit (BSM_ERRORS). + # return: returned value from the operation. + BSM_TOKEN_RETURN32 = construct.Struct( + 'bsm_token_return32', + construct.UBInt8('status'), + construct.UBInt32('return_value')) + + BSM_TOKEN_RETURN64 = construct.Struct( + 'bsm_token_return64', + construct.UBInt8('status'), + construct.UBInt64('return_value')) + + # Identified the number of bytes that was written. + # magic: 2 bytes that identifes the TRAILER (BSM_TOKEN_TRAILER_MAGIC). + # length: integer that has the number of bytes from the entry size. + BSM_TOKEN_TRAILER = construct.Struct( + 'bsm_token_trailer', + construct.UBInt16('magic'), + construct.UBInt32('record_length')) + + # A 32-bits argument. + # num_arg: the number of the argument. + # name_arg: the argument's name. + # text: the string value of the argument. + BSM_TOKEN_ARGUMENT32 = construct.Struct( + 'bsm_token_argument32', + construct.UBInt8('num_arg'), + construct.UBInt32('name_arg'), + construct.UBInt16('length'), + construct.Array( + _BsmTokenGetLength, + construct.UBInt8('text'))) + + # A 64-bits argument. + # num_arg: integer, the number of the argument. + # name_arg: text, the argument's name. + # text: the string value of the argument. + BSM_TOKEN_ARGUMENT64 = construct.Struct( + 'bsm_token_argument64', + construct.UBInt8('num_arg'), + construct.UBInt64('name_arg'), + construct.UBInt16('length'), + construct.Array( + _BsmTokenGetLength, + construct.UBInt8('text'))) + + # Identify an user. + # terminal_id: unknown, research needed. + # terminal_addr: unknown, research needed. + BSM_TOKEN_SUBJECT32 = construct.Struct( + 'bsm_token_subject32', + BSM_TOKEN_SUBJECT_SHORT, + construct.UBInt32('terminal_port'), + IPV4_STRUCT) + + # Identify an user using a extended Token. + # terminal_port: unknown, need research. + # net_type: unknown, need research. + BSM_TOKEN_SUBJECT32_EX = construct.Struct( + 'bsm_token_subject32_ex', + BSM_TOKEN_SUBJECT_SHORT, + construct.UBInt32('terminal_port'), + BSM_IP_TYPE_SHORT) + + # au_to_opaque // AUT_OPAQUE + BSM_TOKEN_OPAQUE = BSM_TOKEN_TEXT + + # au_to_seq // AUT_SEQ + BSM_TOKEN_SEQUENCE = BSM_TOKEN_DATA_INTEGER + + # Program execution with options. + # For each argument we are going to have a string+ "\x00". + # Example: [00 00 00 02][41 42 43 00 42 42 00] + # 2 Arguments, Arg1: [414243] Arg2: [4242]. + BSM_TOKEN_EXEC_ARGUMENTS = construct.UBInt32('number_arguments') + + BSM_TOKEN_EXEC_ARGUMENT = construct.Struct( + 'bsm_token_exec_argument', + construct.RepeatUntil( + _BsmTokenIsEndOfString, + construct.StaticField("text", 1))) + + # au_to_in_addr // AUT_IN_ADDR: + BSM_TOKEN_ADDR = IPV4_STRUCT + + # au_to_in_addr_ext // AUT_IN_ADDR_EX: + BSM_TOKEN_ADDR_EXT = construct.Struct( + 'bsm_token_addr_ext', + construct.UBInt32('net_type'), + IPV6_STRUCT) + + # au_to_ip // AUT_IP: + # TODO: parse this header in the correct way. + BSM_TOKEN_IP = construct.String('binary_ipv4_add', 20) + + # au_to_ipc // AUT_IPC: + BSM_TOKEN_IPC = construct.Struct( + 'bsm_token_ipc', + construct.UBInt8('object_type'), + construct.UBInt32('object_id')) + + # au_to_ipc_perm // au_to_ipc_perm + BSM_TOKEN_IPC_PERM = construct.Struct( + 'bsm_token_ipc_perm', + construct.UBInt32('user_id'), + construct.UBInt32('group_id'), + construct.UBInt32('creator_user_id'), + construct.UBInt32('creator_group_id'), + construct.UBInt32('access_mode'), + construct.UBInt32('slot_seq'), + construct.UBInt32('key')) + + # au_to_iport // AUT_IPORT: + BSM_TOKEN_PORT = construct.UBInt16('port_number') + + # au_to_file // AUT_OTHER_FILE32: + BSM_TOKEN_FILE = construct.Struct( + 'bsm_token_file', + construct.UBInt32('timestamp'), + construct.UBInt32('microsecond'), + construct.UBInt16('length'), + construct.Array( + _BsmTokenGetLength, + construct.UBInt8('text'))) + + # au_to_subject64 // AUT_SUBJECT64: + BSM_TOKEN_SUBJECT64 = construct.Struct( + 'bsm_token_subject64', + BSM_TOKEN_SUBJECT_SHORT, + construct.UBInt64('terminal_port'), + IPV4_STRUCT) + + # au_to_subject64_ex // AU_IPv4: + BSM_TOKEN_SUBJECT64_EX = construct.Struct( + 'bsm_token_subject64_ex', + BSM_TOKEN_SUBJECT_SHORT, + construct.UBInt32('terminal_port'), + construct.UBInt32('terminal_type'), + BSM_IP_TYPE_SHORT) + + # au_to_process32 // AUT_PROCESS32: + BSM_TOKEN_PROCESS32 = construct.Struct( + 'bsm_token_process32', + BSM_TOKEN_SUBJECT_SHORT, + construct.UBInt32('terminal_port'), + IPV4_STRUCT) + + # au_to_process64 // AUT_PROCESS32: + BSM_TOKEN_PROCESS64 = construct.Struct( + 'bsm_token_process64', + BSM_TOKEN_SUBJECT_SHORT, + construct.UBInt64('terminal_port'), + IPV4_STRUCT) + + # au_to_process32_ex // AUT_PROCESS32_EX: + BSM_TOKEN_PROCESS32_EX = construct.Struct( + 'bsm_token_process32_ex', + BSM_TOKEN_SUBJECT_SHORT, + construct.UBInt32('terminal_port'), + BSM_IP_TYPE_SHORT) + + # au_to_process64_ex // AUT_PROCESS64_EX: + BSM_TOKEN_PROCESS64_EX = construct.Struct( + 'bsm_token_process64_ex', + BSM_TOKEN_SUBJECT_SHORT, + construct.UBInt64('terminal_port'), + BSM_IP_TYPE_SHORT) + + # au_to_sock_inet32 // AUT_SOCKINET32: + BSM_TOKEN_AUT_SOCKINET32 = construct.Struct( + 'bsm_token_aut_sockinet32', + construct.UBInt16('net_type'), + construct.UBInt16('port_number'), + IPV4_STRUCT) + + # Info: checked against the source code of XNU, but not against + # real BSM file. + BSM_TOKEN_AUT_SOCKINET128 = construct.Struct( + 'bsm_token_aut_sockinet128', + construct.UBInt16('net_type'), + construct.UBInt16('port_number'), + IPV6_STRUCT) + + INET6_ADDR_TYPE = construct.Struct( + 'addr_type', + construct.UBInt16('ip_type'), + construct.UBInt16('source_port'), + construct.UBInt64('saddr_high'), + construct.UBInt64('saddr_low'), + construct.UBInt16('destination_port'), + construct.UBInt64('daddr_high'), + construct.UBInt64('daddr_low')) + + INET4_ADDR_TYPE = construct.Struct( + 'addr_type', + construct.UBInt16('ip_type'), + construct.UBInt16('source_port'), + construct.UBInt32('source_address'), + construct.UBInt16('destination_port'), + construct.UBInt32('destination_address')) + + # au_to_socket_ex // AUT_SOCKET_EX + # TODO: Change the 26 for unixbsm.BSM_PROTOCOLS.INET6. + BSM_TOKEN_AUT_SOCKINET32_EX = construct.Struct( + 'bsm_token_aut_sockinet32_ex', + construct.UBInt16('socket_domain'), + construct.UBInt16('socket_type'), + construct.Switch( + 'structure_addr_port', + _BsmTokenGetSocketDomain, + {26: INET6_ADDR_TYPE}, + default=INET4_ADDR_TYPE)) + + # au_to_sock_unix // AUT_SOCKUNIX + BSM_TOKEN_SOCKET_UNIX = construct.Struct( + 'bsm_token_au_to_sock_unix', + construct.UBInt16('family'), + construct.RepeatUntil( + _BsmTokenIsEndOfString, + construct.StaticField("path", 1))) + + # au_to_data // au_to_data + # how to print: bsmtoken.BSM_TOKEN_DATA_PRINT. + # type: bsmtoken.BSM_TOKEN_DATA_TYPE. + # unit_count: number of type values. + # BSM_TOKEN_DATA has a end field = type * unit_count + BSM_TOKEN_DATA = construct.Struct( + 'bsm_token_data', + construct.UBInt8('how_to_print'), + construct.UBInt8('data_type'), + construct.UBInt8('unit_count')) + + # au_to_attr32 // AUT_ATTR32 + BSM_TOKEN_ATTR32 = construct.Struct( + 'bsm_token_attr32', + construct.UBInt32('file_mode'), + construct.UBInt32('uid'), + construct.UBInt32('gid'), + construct.UBInt32('file_system_id'), + construct.UBInt64('file_system_node_id'), + construct.UBInt32('device')) + + # au_to_attr64 // AUT_ATTR64 + BSM_TOKEN_ATTR64 = construct.Struct( + 'bsm_token_attr64', + construct.UBInt32('file_mode'), + construct.UBInt32('uid'), + construct.UBInt32('gid'), + construct.UBInt32('file_system_id'), + construct.UBInt64('file_system_node_id'), + construct.UBInt64('device')) + + # au_to_exit // AUT_EXIT + BSM_TOKEN_EXIT = construct.Struct( + 'bsm_token_exit', + construct.UBInt32('status'), + construct.UBInt32('return_value')) + + # au_to_newgroups // AUT_NEWGROUPS + # INFO: we must read BSM_TOKEN_DATA_INTEGER for each group. + BSM_TOKEN_GROUPS = construct.UBInt16('group_number') + + # au_to_exec_env == au_to_exec_args + BSM_TOKEN_EXEC_ENV = BSM_TOKEN_EXEC_ARGUMENTS + + # au_to_zonename //AUT_ZONENAME + BSM_TOKEN_ZONENAME = BSM_TOKEN_TEXT + + # Token ID. + # List of valid Token_ID. + # Token_ID -> [NAME_STRUCTURE, STRUCTURE] + # Only the checked structures are been added to the valid structures lists. + BSM_TYPE_LIST = { + 17: ['BSM_TOKEN_FILE', BSM_TOKEN_FILE], + 19: ['BSM_TOKEN_TRAILER', BSM_TOKEN_TRAILER], + 20: ['BSM_HEADER32', BSM_HEADER32], + 21: ['BSM_HEADER64', BSM_HEADER64], + 33: ['BSM_TOKEN_DATA', BSM_TOKEN_DATA], + 34: ['BSM_TOKEN_IPC', BSM_TOKEN_IPC], + 35: ['BSM_TOKEN_PATH', BSM_TOKEN_PATH], + 36: ['BSM_TOKEN_SUBJECT32', BSM_TOKEN_SUBJECT32], + 38: ['BSM_TOKEN_PROCESS32', BSM_TOKEN_PROCESS32], + 39: ['BSM_TOKEN_RETURN32', BSM_TOKEN_RETURN32], + 40: ['BSM_TOKEN_TEXT', BSM_TOKEN_TEXT], + 41: ['BSM_TOKEN_OPAQUE', BSM_TOKEN_OPAQUE], + 42: ['BSM_TOKEN_ADDR', BSM_TOKEN_ADDR], + 43: ['BSM_TOKEN_IP', BSM_TOKEN_IP], + 44: ['BSM_TOKEN_PORT', BSM_TOKEN_PORT], + 45: ['BSM_TOKEN_ARGUMENT32', BSM_TOKEN_ARGUMENT32], + 47: ['BSM_TOKEN_SEQUENCE', BSM_TOKEN_SEQUENCE], + 96: ['BSM_TOKEN_ZONENAME', BSM_TOKEN_ZONENAME], + 113: ['BSM_TOKEN_ARGUMENT64', BSM_TOKEN_ARGUMENT64], + 114: ['BSM_TOKEN_RETURN64', BSM_TOKEN_RETURN64], + 116: ['BSM_HEADER32_EX', BSM_HEADER32_EX], + 119: ['BSM_TOKEN_PROCESS64', BSM_TOKEN_PROCESS64], + 122: ['BSM_TOKEN_SUBJECT32_EX', BSM_TOKEN_SUBJECT32_EX], + 127: ['BSM_TOKEN_AUT_SOCKINET32_EX', BSM_TOKEN_AUT_SOCKINET32_EX], + 128: ['BSM_TOKEN_AUT_SOCKINET32', BSM_TOKEN_AUT_SOCKINET32]} + + # Untested structures. + # When not tested structure is found, we try to parse using also + # these structures. + BSM_TYPE_LIST_NOT_TESTED = { + 49: ['BSM_TOKEN_ATTR32', BSM_TOKEN_ATTR32], + 50: ['BSM_TOKEN_IPC_PERM', BSM_TOKEN_IPC_PERM], + 52: ['BSM_TOKEN_GROUPS', BSM_TOKEN_GROUPS], + 59: ['BSM_TOKEN_GROUPS', BSM_TOKEN_GROUPS], + 60: ['BSM_TOKEN_EXEC_ARGUMENTS', BSM_TOKEN_EXEC_ARGUMENTS], + 61: ['BSM_TOKEN_EXEC_ENV', BSM_TOKEN_EXEC_ENV], + 62: ['BSM_TOKEN_ATTR32', BSM_TOKEN_ATTR32], + 82: ['BSM_TOKEN_EXIT', BSM_TOKEN_EXIT], + 115: ['BSM_TOKEN_ATTR64', BSM_TOKEN_ATTR64], + 117: ['BSM_TOKEN_SUBJECT64', BSM_TOKEN_SUBJECT64], + 123: ['BSM_TOKEN_PROCESS32_EX', BSM_TOKEN_PROCESS32_EX], + 124: ['BSM_TOKEN_PROCESS64_EX', BSM_TOKEN_PROCESS64_EX], + 125: ['BSM_TOKEN_SUBJECT64_EX', BSM_TOKEN_SUBJECT64_EX], + 126: ['BSM_TOKEN_ADDR_EXT', BSM_TOKEN_ADDR_EXT], + 129: ['BSM_TOKEN_AUT_SOCKINET128', BSM_TOKEN_AUT_SOCKINET128], + 130: ['BSM_TOKEN_SOCKET_UNIX', BSM_TOKEN_SOCKET_UNIX]} + + def __init__(self): + """Initializes a parser object.""" + super(BsmParser, self).__init__() + # Create the dictionary with all token IDs: tested and untested. + self.bsm_type_list_all = self.BSM_TYPE_LIST.copy() + self.bsm_type_list_all.update(self.BSM_TYPE_LIST_NOT_TESTED) + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract entries from a BSM file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + file_object = file_entry.GetFileObject() + file_object.seek(0, os.SEEK_SET) + + try: + is_bsm = self.VerifyFile(parser_context, file_object) + except (IOError, construct.FieldError) as exception: + file_object.close() + raise errors.UnableToParseFile( + u'Unable to parse BSM file with error: {0:s}'.format(exception)) + + if not is_bsm: + file_object.close() + raise errors.UnableToParseFile( + u'Not a BSM File, unable to parse.') + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + event_object = self.ReadBSMEvent(parser_context, file_object) + while event_object: + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + event_object = self.ReadBSMEvent(parser_context, file_object) + + file_object.close() + + def ReadBSMEvent(self, parser_context, file_object): + """Returns a BsmEvent from a single BSM entry. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_object: A file-like object. + + Returns: + An event object. + """ + # A list of tokens that has the entry. + extra_tokens = [] + + offset = file_object.tell() + + # Token header, first token for each entry. + try: + token_id = self.BSM_TYPE.parse_stream(file_object) + except (IOError, construct.FieldError): + return + + bsm_type, structure = self.BSM_TYPE_LIST.get(token_id, ['', '']) + if bsm_type == 'BSM_HEADER32': + token = structure.parse_stream(file_object) + elif bsm_type == 'BSM_HEADER64': + token = structure.parse_stream(file_object) + elif bsm_type == 'BSM_HEADER32_EX': + token = structure.parse_stream(file_object) + else: + logging.warning( + u'Token ID Header {0} not expected at position 0x{1:X}.' + u'The parsing of the file cannot be continued'.format( + token_id, file_object.tell())) + # TODO: if it is a Mac OS X, search for the trailer magic value + # as a end of the entry can be a possibility to continue. + return + + length = token.bsm_header.length + event_type = u'{0} ({1})'.format( + bsmtoken.BSM_AUDIT_EVENT.get(token.bsm_header.event_type, 'UNKNOWN'), + token.bsm_header.event_type) + timestamp = timelib.Timestamp.FromPosixTimeWithMicrosecond( + token.timestamp, token.microsecond) + + # Read until we reach the end of the record. + while file_object.tell() < (offset + length): + # Check if it is a known token. + try: + token_id = self.BSM_TYPE.parse_stream(file_object) + except (IOError, construct.FieldError): + logging.warning( + u'Unable to parse the Token ID at position: {0:d}'.format( + file_object.tell())) + return + if not token_id in self.BSM_TYPE_LIST: + pending = (offset + length) - file_object.tell() + extra_tokens.extend(self.TryWithUntestedStructures( + file_object, token_id, pending)) + else: + token = self.BSM_TYPE_LIST[token_id][1].parse_stream(file_object) + extra_tokens.append(self.FormatToken(token_id, token, file_object)) + + if file_object.tell() > (offset + length): + logging.warning( + u'Token ID {0} not expected at position 0x{1:X}.' + u'Jumping for the next entry.'.format( + token_id, file_object.tell())) + try: + file_object.seek( + (offset + length) - file_object.tell(), os.SEEK_CUR) + except (IOError, construct.FieldError) as exception: + logging.warning( + u'Unable to jump to next entry with error: {0:s}'.format(exception)) + return + + # BSM can be in more than one OS: BSD, Solaris and Mac OS X. + if parser_context.platform == 'MacOSX': + # In Mac OS X the last two tokens are the return status and the trailer. + if len(extra_tokens) >= 2: + return_value = extra_tokens[-2:-1][0] + if (return_value.startswith('[BSM_TOKEN_RETURN32') or + return_value.startswith('[BSM_TOKEN_RETURN64')): + _ = extra_tokens.pop(len(extra_tokens)-2) + else: + return_value = 'Return unknown' + else: + return_value = 'Return unknown' + if extra_tokens: + trailer = extra_tokens[-1] + if trailer.startswith('[BSM_TOKEN_TRAILER'): + _ = extra_tokens.pop(len(extra_tokens)-1) + else: + trailer = 'Trailer unknown' + else: + trailer = 'Trailer unknown' + return MacBsmEvent( + event_type, timestamp, u'. '.join(extra_tokens), + return_value, trailer, offset) + else: + # Generic BSM format. + if extra_tokens: + trailer = extra_tokens[-1] + if trailer.startswith('[BSM_TOKEN_TRAILER'): + _ = extra_tokens.pop(len(extra_tokens)-1) + else: + trailer = 'Trailer unknown' + else: + trailer = 'Trailer unknown' + return BsmEvent( + event_type, timestamp, u'. '.join(extra_tokens), trailer, offset) + + def VerifyFile(self, parser_context, file_object): + """Check if the file is a BSM file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_event: file that we want to check. + + Returns: + True if this is a valid BSM file, otherwise False. + """ + if file_object.tell() != 0: + file_object.seek(0) + + # First part of the entry is always a Header. + try: + token_id = self.BSM_TYPE.parse_stream(file_object) + except (IOError, construct.FieldError): + return False + if token_id not in self.BSM_TYPE_LIST: + return False + + bsm_type, structure = self.BSM_TYPE_LIST.get(token_id, ['', '']) + try: + if bsm_type == 'BSM_HEADER32': + header = structure.parse_stream(file_object) + elif bsm_type == 'BSM_HEADER64': + header = structure.parse_stream(file_object) + elif bsm_type == 'BSM_HEADER32_EX': + header = structure.parse_stream(file_object) + else: + return False + except (IOError, construct.FieldError): + return False + if header.bsm_header.version != self.AUDIT_HEADER_VERSION: + return False + + try: + token_id = self.BSM_TYPE.parse_stream(file_object) + except (IOError, construct.FieldError): + return False + + # If is Mac OS X BSM file, next entry is a text token indicating + # if it is a normal start or it is a recovery track. + if parser_context.platform == 'MacOSX': + bsm_type_list = self.BSM_TYPE_LIST.get(token_id) + if not bsm_type_list: + return False + + if bsm_type_list[0] != 'BSM_TOKEN_TEXT': + logging.warning(u'It is not a valid first entry for Mac OS X BSM.') + return False + try: + token = self.BSM_TOKEN_TEXT.parse_stream(file_object) + except (IOError, construct.FieldError): + return + + text = self._CopyUtf8ByteArrayToString(token.text) + if (text != 'launchctl::Audit startup' and + text != 'launchctl::Audit recovery'): + logging.warning(u'It is not a valid first entry for Mac OS X BSM.') + return False + + file_object.seek(0) + return True + + def TryWithUntestedStructures(self, file_object, token_id, pending): + """Try to parse the pending part of the entry using untested structures. + + Args: + file_object: BSM file. + token_id: integer with the id that comes from the unknown token. + pending: pending length of the entry. + + Returns: + A list of extra tokens data that can be parsed using non-tested + structures. A message indicating that a structure cannot be parsed + is added for unparsed structures. + """ + # Data from the unknown structure. + start_position = file_object.tell() + start_token_id = token_id + extra_tokens = [] + + # Read all the "pending" bytes. + try: + if token_id in self.bsm_type_list_all: + token = self.bsm_type_list_all[token_id][1].parse_stream(file_object) + extra_tokens.append(self.FormatToken(token_id, token, file_object)) + while file_object.tell() < (start_position + pending): + # Check if it is a known token. + try: + token_id = self.BSM_TYPE.parse_stream(file_object) + except (IOError, construct.FieldError): + logging.warning( + u'Unable to parse the Token ID at position: {0:d}'.format( + file_object.tell())) + return + if token_id not in self.bsm_type_list_all: + break + token = self.bsm_type_list_all[token_id][1].parse_stream( + file_object) + extra_tokens.append(self.FormatToken(token_id, token, file_object)) + except (IOError, construct.FieldError): + token_id = 255 + + next_entry = (start_position + pending) + if file_object.tell() != next_entry: + # Unknown Structure. + logging.warning(u'Unknown Token at "0x{0:X}", ID: {1} (0x{2:X})'.format( + start_position-1, token_id, token_id)) + # TODO: another way to save this information must be found. + extra_tokens.append( + u'Plaso: some tokens from this entry can ' + u'not be saved. Entry at 0x{0:X} with unknown ' + u'token id "0x{1:X}".'.format( + start_position-1, start_token_id)) + # Move to next entry. + file_object.seek(next_entry - file_object.tell(), os.SEEK_CUR) + # It returns null list because it doesn't know witch structure was + # the incorrect structure that makes that it can arrive to the spected + # end of the entry. + return [] + return extra_tokens + + # TODO: instead of compare the text to know what structure was parsed + # is better to compare directly the numeric number (token_id), + # less readable, but better performance. + def FormatToken(self, token_id, token, file_object): + """Parse the Token depending of the type of the structure. + + Args: + token_id: Identification integer of the token_type. + token: Token struct to parse. + file_object: BSM file. + + Returns: + String with the parsed Token values. + """ + if token_id not in self.bsm_type_list_all: + return u'Type Unknown: {0:d} (0x{0:X})'.format(token_id) + + bsm_type, _ = self.bsm_type_list_all.get(token_id, ['', '']) + + if bsm_type in [ + 'BSM_TOKEN_TEXT', 'BSM_TOKEN_PATH', 'BSM_TOKEN_ZONENAME']: + try: + string = self._CopyUtf8ByteArrayToString(token.text) + except TypeError: + string = u'Unknown' + return u'[{0}: {1:s}]'.format(bsm_type, string) + + elif bsm_type in [ + 'BSM_TOKEN_RETURN32', 'BSM_TOKEN_RETURN64', 'BSM_TOKEN_EXIT']: + return u'[{0}: {1} ({2}), System call status: {3}]'.format( + bsm_type, bsmtoken.BSM_ERRORS.get(token.status, 'Unknown'), + token.status, token.return_value) + + elif bsm_type in ['BSM_TOKEN_SUBJECT32', 'BSM_TOKEN_SUBJECT64']: + return ( + u'[{0}: aid({1}), euid({2}), egid({3}), uid({4}), gid({5}), ' + u'pid({6}), session_id({7}), terminal_port({8}), ' + u'terminal_ip({9})]').format( + bsm_type, token.subject_data.audit_uid, + token.subject_data.effective_uid, + token.subject_data.effective_gid, + token.subject_data.real_uid, token.subject_data.real_gid, + token.subject_data.pid, token.subject_data.session_id, + token.terminal_port, self._IPv4Format(token.ipv4)) + + elif bsm_type in ['BSM_TOKEN_SUBJECT32_EX', 'BSM_TOKEN_SUBJECT64_EX']: + if token.bsm_ip_type_short.net_type == self.AU_IPv6: + ip = self._IPv6Format( + token.bsm_ip_type_short.ip_addr.high, + token.bsm_ip_type_short.ip_addr.low) + elif token.bsm_ip_type_short.net_type == self.AU_IPv4: + ip = self._IPv4Format(token.bsm_ip_type_short.ip_addr) + else: + ip = 'unknown' + return ( + u'[{0}: aid({1}), euid({2}), egid({3}), uid({4}), gid({5}), ' + u'pid({6}), session_id({7}), terminal_port({8}), ' + u'terminal_ip({9})]').format( + bsm_type, token.subject_data.audit_uid, + token.subject_data.effective_uid, + token.subject_data.effective_gid, + token.subject_data.real_uid, token.subject_data.real_gid, + token.subject_data.pid, token.subject_data.session_id, + token.terminal_port, ip) + + elif bsm_type in ['BSM_TOKEN_ARGUMENT32', 'BSM_TOKEN_ARGUMENT64']: + string = self._CopyUtf8ByteArrayToString(token.text) + return u'[{0}: {1:s}({2}) is 0x{3:X}]'.format( + bsm_type, string, token.num_arg, token.name_arg) + + elif bsm_type in ['BSM_TOKEN_EXEC_ARGUMENTS', 'BSM_TOKEN_EXEC_ENV']: + arguments = [] + for _ in range(0, token): + sub_token = self.BSM_TOKEN_EXEC_ARGUMENT.parse_stream(file_object) + string = self._CopyUtf8ByteArrayToString(sub_token.text) + arguments.append(string) + return u'[{0}: {1:s}]'.format(bsm_type, u' '.join(arguments)) + + elif bsm_type == 'BSM_TOKEN_AUT_SOCKINET32': + return (u'[{0}: {1} ({2}) open in port {3}. Address {4}]'.format( + bsm_type, bsmtoken.BSM_PROTOCOLS.get(token.net_type, 'UNKNOWN'), + token.net_type, token.port_number, self._IPv4Format(token.ipv4))) + + elif bsm_type == 'BSM_TOKEN_AUT_SOCKINET128': + return u'[{0}: {1} ({2}) open in port {3}. Address {4}]'.format( + bsm_type, bsmtoken.BSM_PROTOCOLS.get(token.net_type, 'UNKNOWN'), + token.net_type, token.port_number, + self._IPv6Format(token.ipv6.high, token.ipv6.low)) + + elif bsm_type == 'BSM_TOKEN_ADDR': + return u'[{0}: {1}]'.format(bsm_type, self._IPv4Format(token)) + + elif bsm_type == 'BSM_TOKEN_IP': + return u'[IPv4_Header: 0x{0:s}]'.format(token.encode('hex')) + + elif bsm_type == 'BSM_TOKEN_ADDR_EXT': + return u'[{0}: {1} ({2}). Address {3}]'.format( + bsm_type, + bsmtoken.BSM_PROTOCOLS.get(token.net_type, 'UNKNOWN'), + token.net_type, self._IPv6Format(token.ipv6.high, token.ipv6.low)) + + elif bsm_type == 'BSM_TOKEN_PORT': + return u'[{0}: {1}]'.format(bsm_type, token) + + elif bsm_type == 'BSM_TOKEN_TRAILER': + return u'[{0}: {1}]'.format(bsm_type, token.record_length) + + elif bsm_type == 'BSM_TOKEN_FILE': + # TODO: if this timestamp is usefull, it must be extracted as a separate + # event object. + timestamp = timelib.Timestamp.FromPosixTimeWithMicrosecond( + token.timestamp, token.microsecond) + date_time = timelib.Timestamp.CopyToDatetime(timestamp, pytz.utc) + date_time_string = date_time.strftime('%Y-%m-%d %H:%M:%S') + + string = self._CopyUtf8ByteArrayToString(token.text) + return u'[{0}: {1:s}, timestamp: {2:s}]'.format( + bsm_type, string, date_time_string) + + elif bsm_type == 'BSM_TOKEN_IPC': + return u'[{0}: object type {1}, object id {2}]'.format( + bsm_type, token.object_type, token.object_id) + + elif bsm_type in ['BSM_TOKEN_PROCESS32', 'BSM_TOKEN_PROCESS64']: + return ( + u'[{0}: aid({1}), euid({2}), egid({3}), uid({4}), gid({5}), ' + u'pid({6}), session_id({7}), terminal_port({8}), ' + u'terminal_ip({9})]').format( + bsm_type, token.subject_data.audit_uid, + token.subject_data.effective_uid, + token.subject_data.effective_gid, + token.subject_data.real_uid, token.subject_data.real_gid, + token.subject_data.pid, token.subject_data.session_id, + token.terminal_port, self._IPv4Format(token.ipv4)) + + elif bsm_type in ['BSM_TOKEN_PROCESS32_EX', 'BSM_TOKEN_PROCESS64_EX']: + if token.bsm_ip_type_short.net_type == self.AU_IPv6: + ip = self._IPv6Format( + token.bsm_ip_type_short.ip_addr.high, + token.bsm_ip_type_short.ip_addr.low) + elif token.bsm_ip_type_short.net_type == self.AU_IPv4: + ip = self._IPv4Format(token.bsm_ip_type_short.ip_addr) + else: + ip = 'unknown' + return ( + u'[{0}: aid({1}), euid({2}), egid({3}), uid({4}), gid({5}), ' + u'pid({6}), session_id({7}), terminal_port({8}), ' + u'terminal_ip({9})]').format( + bsm_type, token.subject_data.audit_uid, + token.subject_data.effective_uid, + token.subject_data.effective_gid, + token.subject_data.real_uid, token.subject_data.real_gid, + token.subject_data.pid, token.subject_data.session_id, + token.terminal_port, ip) + + elif bsm_type == 'BSM_TOKEN_DATA': + data = [] + data_type = bsmtoken.BSM_TOKEN_DATA_TYPE.get(token.data_type, '') + if data_type == 'AUR_CHAR': + for _ in range(token.unit_count): + data.append(self.BSM_TOKEN_DATA_CHAR.parse_stream(file_object)) + elif data_type == 'AUR_SHORT': + for _ in range(token.unit_count): + data.append(self.BSM_TOKEN_DAT_SHORT.parse_stream(file_object)) + elif data_type == 'AUR_INT32': + for _ in range(token.unit_count): + data.append(self.BSM_TOKEN_DATA_INTEGER.parse_stream(file_object)) + else: + data.append(u'Unknown type data') + # TODO: the data when it is string ends with ".", HW a space is return + # after uses the UTF-8 conversion. + return u'[{0}: Format data: {1}, Data: {2}]'.format( + bsm_type, bsmtoken.BSM_TOKEN_DATA_PRINT[token.how_to_print], + self._RawToUTF8(u''.join(data))) + + elif bsm_type in ['BSM_TOKEN_ATTR32', 'BSM_TOKEN_ATTR64']: + return ( + u'[{0}: Mode: {1}, UID: {2}, GID: {3}, ' + u'File system ID: {4}, Node ID: {5}, Device: {6}]').format( + bsm_type, token.file_mode, token.uid, token.gid, + token.file_system_id, token.file_system_node_id, token.device) + + elif bsm_type == 'BSM_TOKEN_GROUPS': + arguments = [] + for _ in range(token): + arguments.append(self._RawToUTF8( + self.BSM_TOKEN_DATA_INTEGER.parse_stream(file_object))) + return u'[{0}: {1:s}]'.format(bsm_type, u','.join(arguments)) + + elif bsm_type == 'BSM_TOKEN_AUT_SOCKINET32_EX': + if bsmtoken.BSM_PROTOCOLS.get(token.socket_domain, '') == 'INET6': + saddr = self._IPv6Format( + token.structure_addr_port.saddr_high, + token.structure_addr_port.saddr_low) + daddr = self._IPv6Format( + token.structure_addr_port.daddr_high, + token.structure_addr_port.daddr_low) + else: + saddr = self._IPv4Format(token.structure_addr_port.source_address) + daddr = self._IPv4Format(token.structure_addr_port.destination_address) + + return u'[{0}: from {1} port {2} to {3} port {4}]'.format( + bsm_type, saddr, token.structure_addr_port.source_port, + daddr, token.structure_addr_port.destination_port) + + elif bsm_type == 'BSM_TOKEN_IPC_PERM': + return ( + u'[{0}: user id {1}, group id {2}, create user id {3}, ' + u'create group id {4}, access {5}]').format( + bsm_type, token.user_id, token.group_id, + token.creator_user_id, token.creator_group_id, token.access_mode) + + elif bsm_type == 'BSM_TOKEN_SOCKET_UNIX': + string = self._CopyUtf8ByteArrayToString(token.path) + return u'[{0}: Family {1}, Path {2:s}]'.format( + bsm_type, token.family, string) + + elif bsm_type == 'BSM_TOKEN_OPAQUE': + string = self._CopyByteArrayToBase16String(token.text) + return u'[{0}: {1:s}]'.format(bsm_type, string) + + elif bsm_type == 'BSM_TOKEN_SEQUENCE': + return u'[{0}: {1}]'.format(bsm_type, token) + + def _IPv6Format(self, high, low): + """Provide a readable IPv6 IP having the high and low part in 2 integers. + + Args: + high: 64 bits integers number with the high part of the IPv6. + low: 64 bits integers number with the low part of the IPv6. + + Returns: + String with a well represented IPv6. + """ + ipv6_string = self.IPV6_STRUCT.build( + construct.Container(high=high, low=low)) + # socket.inet_ntop not supported in Windows. + if hasattr(socket, 'inet_ntop'): + return socket.inet_ntop(socket.AF_INET6, ipv6_string) + else: + # TODO: this approach returns double "::", illegal IPv6 addr. + str_address = binascii.hexlify(ipv6_string) + address = [] + blank = False + for pos in range(0, len(str_address), 4): + if str_address[pos:pos + 4] == '0000': + if not blank: + address.append('') + blank = True + else: + blank = False + address.append(str_address[pos:pos + 4].lstrip('0')) + return u':'.join(address) + + def _IPv4Format(self, address): + """Change an integer IPv4 address value for its 4 octets representation. + + Args: + address: integer with the IPv4 address. + + Returns: + IPv4 address in 4 octect representation (class A, B, C, D). + """ + ipv4_string = self.IPV4_STRUCT.build(address) + return socket.inet_ntoa(ipv4_string) + + def _RawToUTF8(self, byte_stream): + """Copies a UTF-8 byte stream into a Unicode string. + + Args: + byte_stream: A byte stream containing an UTF-8 encoded string. + + Returns: + A Unicode string. + """ + try: + string = byte_stream.decode('utf-8') + except UnicodeDecodeError: + logging.warning( + u'Decode UTF8 failed, the message string may be cut short.') + string = byte_stream.decode('utf-8', errors='ignore') + return string.partition('\x00')[0] + + def _CopyByteArrayToBase16String(self, byte_array): + """Copies a byte array into a base-16 encoded Unicode string. + + Args: + byte_array: A byte array. + + Returns: + A base-16 encoded Unicode string. + """ + return u''.join(['{0:02x}'.format(byte) for byte in byte_array]) + + def _CopyUtf8ByteArrayToString(self, byte_array): + """Copies a UTF-8 encoded byte array into a Unicode string. + + Args: + byte_array: A byte array containing an UTF-8 encoded string. + + Returns: + A Unicode string. + """ + byte_stream = ''.join(map(chr, byte_array)) + + try: + string = byte_stream.decode('utf-8') + except UnicodeDecodeError: + logging.warning(u'Unable to decode UTF-8 formatted byte array.') + string = byte_stream.decode('utf-8', errors='ignore') + + string, _, _ = string.partition('\x00') + return string + + +manager.ParsersManager.RegisterParser(BsmParser) diff --git a/plaso/parsers/bsm_test.py b/plaso/parsers/bsm_test.py new file mode 100644 index 0000000..78faf9d --- /dev/null +++ b/plaso/parsers/bsm_test.py @@ -0,0 +1,197 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for Basic Security Module (BSM) file parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import bsm as bsm_formatter +from plaso.lib import timelib_test +from plaso.parsers import bsm +from plaso.parsers import test_lib + + +class MacOSXBsmParserTest(test_lib.ParserTestCase): + """Tests for Basic Security Module (BSM) file parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = bsm.BsmParser() + + def testParse(self): + """Tests the Parse function on a Mac OS X BSM file.""" + knowledge_base_values = {'guessed_os': 'MacOSX'} + test_file = self._GetTestFilePath(['apple.bsm']) + event_queue_consumer = self._ParseFile( + self._parser, test_file, knowledge_base_values=knowledge_base_values) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEqual(len(event_objects), 54) + + event_object = event_objects[0] + + self.assertEqual(event_object.data_type, 'mac:bsm:event') + + expected_msg = ( + u'Type: audit crash recovery (45029) ' + u'Return: [BSM_TOKEN_RETURN32: Success (0), System call status: 0] ' + u'Information: [BSM_TOKEN_TEXT: launchctl::Audit recovery]. ' + u'[BSM_TOKEN_PATH: /var/audit/20131104171720.crash_recovery]') + + expected_msg_short = ( + u'Type: audit crash recovery (45029) ' + u'Return: [BSM_TOKEN_RETURN32: Success (0), ...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-04 18:36:20.000381') + self.assertEqual(event_object.timestamp, expected_timestamp) + self.assertEqual(event_object.event_type, u'audit crash recovery (45029)') + + expected_extra_tokens = ( + u'[BSM_TOKEN_TEXT: launchctl::Audit recovery]. ' + u'[BSM_TOKEN_PATH: /var/audit/20131104171720.crash_recovery]') + self.assertEqual(event_object.extra_tokens, expected_extra_tokens) + + expected_return_value = ( + u'[BSM_TOKEN_RETURN32: Success (0), System call status: 0]') + self.assertEqual(event_object.return_value, expected_return_value) + + event_object = event_objects[15] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-04 18:36:26.000171') + self.assertEqual(event_object.timestamp, expected_timestamp) + self.assertEqual(event_object.event_type, u'user authentication (45023)') + + expected_extra_tokens = ( + u'[BSM_TOKEN_SUBJECT32: aid(4294967295), euid(92), egid(92), uid(92), ' + u'gid(92), pid(143), session_id(100004), terminal_port(143), ' + u'terminal_ip(0.0.0.0)]. ' + u'[BSM_TOKEN_TEXT: Verify password for record type Users ' + u'\'moxilo\' node \'/Local/Default\']') + self.assertEqual(event_object.extra_tokens, expected_extra_tokens) + + expected_return_value = ( + u'[BSM_TOKEN_RETURN32: Unknown (255), System call status: 5000]') + self.assertEqual(event_object.return_value, expected_return_value) + + event_object = event_objects[31] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-04 18:36:26.000530') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.event_type, u'SecSrvr AuthEngine (45025)') + expected_extra_tokens = ( + u'[BSM_TOKEN_SUBJECT32: aid(4294967295), euid(0), egid(0), uid(0), ' + u'gid(0), pid(67), session_id(100004), terminal_port(67), ' + u'terminal_ip(0.0.0.0)]. ' + u'[BSM_TOKEN_TEXT: system.login.done]. ' + u'[BSM_TOKEN_TEXT: system.login.done]') + self.assertEqual(event_object.extra_tokens, expected_extra_tokens) + + expected_return_value = ( + u'[BSM_TOKEN_RETURN32: Success (0), System call status: 0]') + self.assertEqual(event_object.return_value, expected_return_value) + + event_object = event_objects[50] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-04 18:37:36.000399') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.event_type, u'session end (44903)') + + expected_extra_tokens = ( + u'[BSM_TOKEN_ARGUMENT64: sflags(1) is 0x0]. ' + u'[BSM_TOKEN_ARGUMENT32: am_success(2) is 0x3000]. ' + u'[BSM_TOKEN_ARGUMENT32: am_failure(3) is 0x3000]. ' + u'[BSM_TOKEN_SUBJECT32: aid(4294967295), euid(0), egid(0), uid(0), ' + u'gid(0), pid(0), session_id(100015), terminal_port(0), ' + u'terminal_ip(0.0.0.0)]') + self.assertEqual(event_object.extra_tokens, expected_extra_tokens) + + expected_return_value = ( + u'[BSM_TOKEN_RETURN32: Success (0), System call status: 0]') + self.assertEqual(event_object.return_value, expected_return_value) + + +class OpenBsmParserTest(test_lib.ParserTestCase): + """Tests for Basic Security Module (BSM) file parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = bsm.BsmParser() + + def testParse(self): + """Tests the Parse function on a "generic" BSM file.""" + knowledge_base_values = {'guessed_os': 'openbsm'} + test_file = self._GetTestFilePath(['openbsm.bsm']) + event_queue_consumer = self._ParseFile( + self._parser, test_file, knowledge_base_values=knowledge_base_values) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEqual(len(event_objects), 50) + + expected_extra_tokens = [ + u'[BSM_TOKEN_ARGUMENT32: test_arg32_token(3) is 0xABCDEF00]', + u'[BSM_TOKEN_DATA: Format data: String, Data: SomeData]', + u'[BSM_TOKEN_FILE: test, timestamp: 1970-01-01 20:42:45]', + u'[BSM_TOKEN_ADDR: 192.168.100.15]', + u'[IPv4_Header: 0x400000145478000040010000c0a8649bc0a86e30]', + u'[BSM_TOKEN_IPC: object type 1, object id 305419896]', + u'[BSM_TOKEN_PORT: 20480]', + u'[BSM_TOKEN_OPAQUE: aabbccdd]', + u'[BSM_TOKEN_PATH: /test/this/is/a/test]', + (u'[BSM_TOKEN_PROCESS32: aid(305419896), euid(19088743), ' + u'egid(591751049), uid(2557891634), gid(159868227), ' + u'pid(321140038), session_id(2542171492), ' + u'terminal_port(374945606), terminal_ip(127.0.0.1)]'), + (u'[BSM_TOKEN_PROCESS64: aid(305419896), euid(19088743), ' + u'egid(591751049), uid(2557891634), gid(159868227), ' + u'pid(321140038), session_id(2542171492), ' + u'terminal_port(374945606), terminal_ip(127.0.0.1)]'), + (u'[BSM_TOKEN_RETURN32: Invalid argument (22), ' + u'System call status: 305419896]'), + u'[BSM_TOKEN_SEQUENCE: 305419896]', + (u'[BSM_TOKEN_AUT_SOCKINET32_EX: ' + u'from 127.0.0.1 port 0 to 127.0.0.1 port 0]'), + (u'[BSM_TOKEN_SUBJECT32: aid(305419896), euid(19088743), ' + u'egid(591751049), uid(2557891634), gid(159868227), ' + u'pid(321140038), session_id(2542171492), ' + u'terminal_port(374945606), terminal_ip(127.0.0.1)]'), + (u'[BSM_TOKEN_SUBJECT32_EX: aid(305419896), euid(19088743), ' + u'egid(591751049), uid(2557891634), gid(159868227), ' + u'pid(321140038), session_id(2542171492), ' + u'terminal_port(374945606), terminal_ip(fe80::1)]'), + u'[BSM_TOKEN_TEXT: This is a test.]', + u'[BSM_TOKEN_ZONENAME: testzone]', + (u'[BSM_TOKEN_RETURN32: Argument list too long (7), ' + u'System call status: 4294967295]')] + + extra_tokens = [] + for event_object_index in range(0, 19): + extra_tokens.append(event_objects[event_object_index].extra_tokens) + + self.assertEqual(extra_tokens, expected_extra_tokens) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/chrome_cache.py b/plaso/parsers/chrome_cache.py new file mode 100644 index 0000000..7accdd1 --- /dev/null +++ b/plaso/parsers/chrome_cache.py @@ -0,0 +1,441 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for Google Chrome and Chromium Cache files.""" + +import logging +import os + +import construct + +from dfvfs.resolver import resolver as path_spec_resolver +from dfvfs.path import factory as path_spec_factory + +from plaso.events import time_events +from plaso.lib import errors +from plaso.lib import eventdata +from plaso.parsers import interface +from plaso.parsers import manager + + +class CacheAddress(object): + """Class that contains a cache address.""" + FILE_TYPE_SEPARATE = 0 + FILE_TYPE_BLOCK_RANKINGS = 1 + FILE_TYPE_BLOCK_256 = 2 + FILE_TYPE_BLOCK_1024 = 3 + FILE_TYPE_BLOCK_4096 = 4 + + _BLOCK_DATA_FILE_TYPES = [ + FILE_TYPE_BLOCK_RANKINGS, + FILE_TYPE_BLOCK_256, + FILE_TYPE_BLOCK_1024, + FILE_TYPE_BLOCK_4096] + + _FILE_TYPE_BLOCK_SIZES = [0, 36, 256, 1024, 4096] + + def __init__(self, cache_address): + """Initializes the cache address object. + + Args: + cache_address: the cache address value. + """ + super(CacheAddress, self).__init__() + self.block_number = None + self.block_offset = None + self.block_size = None + self.filename = None + self.value = cache_address + + if cache_address & 0x80000000: + self.is_initialized = u'True' + else: + self.is_initialized = u'False' + + self.file_type = (cache_address & 0x70000000) >> 28 + if not cache_address == 0x00000000: + if self.file_type == self.FILE_TYPE_SEPARATE: + file_selector = cache_address & 0x0fffffff + self.filename = u'f_{0:06x}'.format(file_selector) + + elif self.file_type in self._BLOCK_DATA_FILE_TYPES: + file_selector = (cache_address & 0x00ff0000) >> 16 + self.filename = u'data_{0:d}'.format(file_selector) + + file_block_size = self._FILE_TYPE_BLOCK_SIZES[self.file_type] + self.block_number = cache_address & 0x0000ffff + self.block_size = (cache_address & 0x03000000) >> 24 + self.block_size *= file_block_size + self.block_offset = 8192 + (self.block_number * file_block_size) + + +class CacheEntry(object): + """Class that contains a cache entry.""" + + def __init__(self): + """Initializes the cache entry object.""" + super(CacheEntry, self).__init__() + self.creation_time = None + self.hash = None + self.key = None + self.next = None + self.rankings_node = None + + +class IndexFile(object): + """Class that contains an index file.""" + + SIGNATURE = 0xc103cac3 + + _FILE_HEADER = construct.Struct( + 'chrome_cache_index_file_header', + construct.ULInt32('signature'), + construct.ULInt16('minor_version'), + construct.ULInt16('major_version'), + construct.ULInt32('number_of_entries'), + construct.ULInt32('stored_data_size'), + construct.ULInt32('last_created_file_number'), + construct.ULInt32('unknown1'), + construct.ULInt32('unknown2'), + construct.ULInt32('table_size'), + construct.ULInt32('unknown3'), + construct.ULInt32('unknown4'), + construct.ULInt64('creation_time'), + construct.Padding(208)) + + def __init__(self): + """Initializes the index file object.""" + super(IndexFile, self).__init__() + self._file_object = None + self.creation_time = None + self.version = None + self.index_table = [] + + def _ReadFileHeader(self): + """Reads the file header. + + Raises: + IOError: if the file header cannot be read. + """ + self._file_object.seek(0, os.SEEK_SET) + + try: + file_header = self._FILE_HEADER.parse_stream(self._file_object) + except construct.FieldError as exception: + raise IOError(u'Unable to parse file header with error: {0:s}'.format( + exception)) + + signature = file_header.get('signature') + + if signature != self.SIGNATURE: + raise IOError(u'Unsupported index file signature') + + self.version = u'{0:d}.{1:d}'.format( + file_header.get('major_version'), + file_header.get('minor_version')) + + if self.version not in [u'2.0', u'2.1']: + raise IOError(u'Unsupported index file version: {0:s}'.format( + self.version)) + + self.creation_time = file_header.get('creation_time') + + def _ReadIndexTable(self): + """Reads the index table.""" + cache_address_data = self._file_object.read(4) + + while len(cache_address_data) == 4: + value = construct.ULInt32('cache_address').parse(cache_address_data) + + if value: + cache_address = CacheAddress(value) + self.index_table.append(cache_address) + + cache_address_data = self._file_object.read(4) + + def Close(self): + """Closes the index file.""" + if self._file_object: + self._file_object.close() + self._file_object = None + + def Open(self, file_object): + """Opens the index file. + + Args: + file_object: the file object. + """ + self._file_object = file_object + self._ReadFileHeader() + # Skip over the LRU data, which is 112 bytes in size. + self._file_object.seek(112, os.SEEK_CUR) + self._ReadIndexTable() + + +class DataBlockFile(object): + """Class that contains a data block file.""" + + SIGNATURE = 0xc104cac3 + + _FILE_HEADER = construct.Struct( + 'chrome_cache_data_file_header', + construct.ULInt32('signature'), + construct.ULInt16('minor_version'), + construct.ULInt16('major_version'), + construct.ULInt16('file_number'), + construct.ULInt16('next_file_number'), + construct.ULInt32('block_size'), + construct.ULInt32('number_of_entries'), + construct.ULInt32('maximum_number_of_entries'), + construct.Array(4, construct.ULInt32('emtpy')), + construct.Array(4, construct.ULInt32('hints')), + construct.ULInt32('updating'), + construct.Array(5, construct.ULInt32('user'))) + + _CACHE_ENTRY = construct.Struct( + 'chrome_cache_entry', + construct.ULInt32('hash'), + construct.ULInt32('next_address'), + construct.ULInt32('rankings_node_address'), + construct.ULInt32('reuse_count'), + construct.ULInt32('refetch_count'), + construct.ULInt32('state'), + construct.ULInt64('creation_time'), + construct.ULInt32('key_size'), + construct.ULInt32('long_key_address'), + construct.Array(4, construct.ULInt32('data_stream_sizes')), + construct.Array(4, construct.ULInt32('data_stream_addresses')), + construct.ULInt32('flags'), + construct.Padding(16), + construct.ULInt32('self_hash'), + construct.Array(160, construct.UBInt8('key'))) + + def __init__(self): + """Initializes the data block file object.""" + super(DataBlockFile, self).__init__() + self._file_object = None + self.creation_time = None + self.block_size = None + self.number_of_entries = None + self.version = None + + def _ReadFileHeader(self): + """Reads the file header. + + Raises: + IOError: if the file header cannot be read. + """ + self._file_object.seek(0, os.SEEK_SET) + + try: + file_header = self._FILE_HEADER.parse_stream(self._file_object) + except construct.FieldError as exception: + raise IOError(u'Unable to parse file header with error: {0:s}'.format( + exception)) + + signature = file_header.get('signature') + + if signature != self.SIGNATURE: + raise IOError(u'Unsupported data block file signature') + + self.version = u'{0:d}.{1:d}'.format( + file_header.get('major_version'), + file_header.get('minor_version')) + + if self.version not in [u'2.0', u'2.1']: + raise IOError(u'Unsupported data block file version: {0:s}'.format( + self.version)) + + self.block_size = file_header.get('block_size') + self.number_of_entries = file_header.get('number_of_entries') + + def ReadCacheEntry(self, block_offset): + """Reads a cache entry.""" + self._file_object.seek(block_offset, os.SEEK_SET) + + try: + cache_entry_struct = self._CACHE_ENTRY.parse_stream(self._file_object) + except construct.FieldError as exception: + raise IOError(u'Unable to parse cache entry with error: {0:s}'.format( + exception)) + + cache_entry = CacheEntry() + + cache_entry.hash = cache_entry_struct.get('hash') + + cache_entry.next = CacheAddress(cache_entry_struct.get('next_address')) + cache_entry.rankings_node = CacheAddress(cache_entry_struct.get( + 'rankings_node_address')) + + cache_entry.creation_time = cache_entry_struct.get('creation_time') + + byte_array = cache_entry_struct.get('key') + string = u''.join(map(chr, byte_array)) + cache_entry.key, _, _ = string.partition(u'\x00') + + return cache_entry + + def Close(self): + """Closes the data block file.""" + if self._file_object: + self._file_object.close() + self._file_object = None + + def Open(self, file_object): + """Opens the data block file. + + Args: + file_object: the file object. + """ + self._file_object = file_object + self._ReadFileHeader() + + +class ChromeCacheEntryEvent(time_events.WebKitTimeEvent): + """Class that contains a Chrome Cache event.""" + + DATA_TYPE = 'chrome:cache:entry' + + def __init__(self, cache_entry): + """Initializes the event object. + + Args: + cache_entry: the cache entry (instance of CacheEntry). + """ + super(ChromeCacheEntryEvent, self).__init__( + cache_entry.creation_time, eventdata.EventTimestamp.CREATION_TIME) + self.original_url = cache_entry.key + + +class ChromeCacheParser(interface.BaseParser): + """Parses Chrome Cache files.""" + + NAME = 'chrome_cache' + DESCRIPTION = u'Parser for Chrome Cache files.' + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract event objects from Chrome Cache files. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + file_object = file_entry.GetFileObject() + index_file = IndexFile() + try: + index_file.Open(file_object) + except IOError as exception: + file_object.close() + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse index file {1:s} with error: {2:s}'.format( + self.NAME, file_entry.name, exception)) + + # Build a lookup table for the data block files. + file_system = file_entry.GetFileSystem() + path_segments = file_system.SplitPath(file_entry.path_spec.location) + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + data_block_files = {} + for cache_address in index_file.index_table: + if cache_address.filename not in data_block_files: + # Remove the previous filename from the path segments list and + # add one of the data block file. + path_segments.pop() + path_segments.append(cache_address.filename) + + # We need to pass only used arguments to the path specification + # factory otherwise it will raise. + kwargs = {} + if file_entry.path_spec.parent: + kwargs['parent'] = file_entry.path_spec.parent + kwargs['location'] = file_system.JoinPath(path_segments) + + data_block_file_path_spec = path_spec_factory.Factory.NewPathSpec( + file_entry.path_spec.TYPE_INDICATOR, **kwargs) + + try: + data_block_file_entry = path_spec_resolver.Resolver.OpenFileEntry( + data_block_file_path_spec) + except RuntimeError as exception: + logging.error(( + u'[{0:s}] Unable to open data block file: {1:s} while parsing ' + u'{2:s} with error: {3:s}').format( + parser_chain, kwargs['location'], + file_entry.path_spec.comparable, exception)) + data_block_file_entry = None + + if not data_block_file_entry: + logging.error(u'Missing data block file: {0:s}'.format( + cache_address.filename)) + data_block_file = None + + else: + data_block_file_object = data_block_file_entry.GetFileObject() + data_block_file = DataBlockFile() + + try: + data_block_file.Open(data_block_file_object) + except IOError as exception: + logging.error(( + u'Unable to open data block file: {0:s} with error: ' + u'{1:s}').format(cache_address.filename, exception)) + data_block_file = None + + data_block_files[cache_address.filename] = data_block_file + + # Parse the cache entries in the data block files. + for cache_address in index_file.index_table: + cache_address_chain_length = 0 + while cache_address.value != 0x00000000: + if cache_address_chain_length >= 64: + logging.error(u'Maximum allowed cache address chain length reached.') + break + + data_file = data_block_files.get(cache_address.filename, None) + if not data_file: + logging.debug(u'Cache address: 0x{0:08x} missing data file.'.format( + cache_address.value)) + break + + try: + cache_entry = data_file.ReadCacheEntry(cache_address.block_offset) + except (IOError, UnicodeDecodeError) as exception: + logging.error( + u'Unable to parse cache entry with error: {0:s}'.format( + exception)) + break + + event_object = ChromeCacheEntryEvent(cache_entry) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + cache_address = cache_entry.next + cache_address_chain_length += 1 + + for data_block_file in data_block_files.itervalues(): + if data_block_file: + data_block_file.Close() + + index_file.Close() + + +manager.ParsersManager.RegisterParser(ChromeCacheParser) diff --git a/plaso/parsers/chrome_cache_test.py b/plaso/parsers/chrome_cache_test.py new file mode 100644 index 0000000..1b14baa --- /dev/null +++ b/plaso/parsers/chrome_cache_test.py @@ -0,0 +1,60 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Chrome Cache files parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import chrome_cache as chrome_cache_formatter +from plaso.lib import timelib_test +from plaso.parsers import test_lib +from plaso.parsers import chrome_cache + + +class ChromeCacheParserTest(test_lib.ParserTestCase): + """Tests for the Chrome Cache files parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = chrome_cache.ChromeCacheParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['chrome_cache', 'index']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 217) + + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2014-04-30 16:44:36.226091') + self.assertEqual(event_object.timestamp, expected_timestamp) + + expected_original_url = ( + u'https://s.ytimg.com/yts/imgbin/player-common-vfliLfqPT.webp') + self.assertEqual(event_object.original_url, expected_original_url) + + expected_string = u'Original URL: {0:s}'.format(expected_original_url) + + self._TestGetMessageStrings(event_object, expected_string, expected_string) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/context.py b/plaso/parsers/context.py new file mode 100644 index 0000000..793cdcf --- /dev/null +++ b/plaso/parsers/context.py @@ -0,0 +1,289 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The parser context object.""" + +import os + +from dfvfs.lib import definitions as dfvfs_definitions + +from plaso.lib import event +from plaso.lib import utils + + +class ParserContext(object): + """Class that implements the parser context.""" + + def __init__( + self, event_queue_producer, parse_error_queue_producer, knowledge_base): + """Initializes a parser context object. + + Args: + event_queue_producer: the event object queue producer (instance of + ItemQueueProducer). + parse_error_queue_producer: the parse error queue producer (instance of + ItemQueueProducer). + knowledge_base: A knowledge base object (instance of KnowledgeBase), + which contains information from the source data needed + for parsing. + """ + super(ParserContext, self).__init__() + self._abort = False + self._event_queue_producer = event_queue_producer + self._filter_object = None + self._knowledge_base = knowledge_base + self._mount_path = None + self._parse_error_queue_producer = parse_error_queue_producer + self._text_prepend = None + + self.number_of_events = 0 + self.number_of_parse_errors = 0 + + @property + def abort(self): + """Read-only value to indicate the parsing should be aborted.""" + return self._abort + + @property + def codepage(self): + """The codepage.""" + return self._knowledge_base.codepage + + @property + def hostname(self): + """The hostname.""" + return self._knowledge_base.hostname + + @property + def knowledge_base(self): + """The knowledge base.""" + return self._knowledge_base + + @property + def platform(self): + """The platform.""" + return self._knowledge_base.platform + + @property + def timezone(self): + """The timezone object.""" + return self._knowledge_base.timezone + + @property + def year(self): + """The year.""" + return self._knowledge_base.year + + def GetDisplayName(self, file_entry): + """Retrieves the display name for the file entry. + + Args: + file_entry: a file entry object (instance of dfvfs.FileEntry). + + Returns: + A string containing the display name. + """ + relative_path = self.GetRelativePath(file_entry) + if not relative_path: + return file_entry.name + + return u'{0:s}:{1:s}'.format( + file_entry.path_spec.type_indicator, relative_path) + + def GetRelativePath(self, file_entry): + """Retrieves the relative path of the file entry. + + Args: + file_entry: a file entry object (instance of dfvfs.FileEntry). + + Returns: + A string containing the relative path or None. + """ + path_spec = getattr(file_entry, 'path_spec', None) + if not path_spec: + return + + # TODO: Solve this differently, quite possibly inside dfVFS using mount + # path spec. + file_path = getattr(path_spec, 'location', None) + + if path_spec.type_indicator != dfvfs_definitions.TYPE_INDICATOR_OS: + return file_path + + # If we are parsing a mount point we don't want to include the full + # path to file's location here, we are only interested in the relative + # path to the mount point. + if self._mount_path: + _, _, file_path = file_path.partition(self._mount_path) + + return file_path + + def MatchesFilter(self, event_object): + """Checks if the event object matces the filter. + + Args: + event_object: the event object (instance of EventObject). + + Returns: + A boolean value indicating if the event object matches the filter. + """ + return self._filter_object and self._filter_object.Matches(event_object) + + def ProcessEvent( + self, event_object, parser_chain=None, file_entry=None, query=None): + """Processes an event before it is emitted to the event queue. + + Args: + event_object: the event object (instance of EventObject). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + query: Optional query string. The default is None. + """ + if not getattr(event_object, 'parser', None) and parser_chain: + event_object.parser = parser_chain + + # TODO: deprecate text_prepend in favor of an event tag. + if not getattr(event_object, 'text_prepend', None) and self._text_prepend: + event_object.text_prepend = self._text_prepend + + display_name = None + if file_entry: + event_object.pathspec = file_entry.path_spec + + if not getattr(event_object, 'filename', None): + event_object.filename = self.GetRelativePath(file_entry) + + if not display_name: + # TODO: dfVFS refactor: move display name to output since the path + # specification contains the full information. + display_name = self.GetDisplayName(file_entry) + + stat_object = file_entry.GetStat() + inode_number = getattr(stat_object, 'ino', None) + if not hasattr(event_object, 'inode') and inode_number: + # TODO: clean up the GetInodeValue function. + event_object.inode = utils.GetInodeValue(inode_number) + + if not getattr(event_object, 'display_name', None) and display_name: + event_object.display_name = display_name + + if not getattr(event_object, 'hostname', None) and self.hostname: + event_object.hostname = self.hostname + + if not getattr(event_object, 'username', None): + user_sid = getattr(event_object, 'user_sid', None) + username = self._knowledge_base.GetUsernameByIdentifier(user_sid) + if username: + event_object.username = username + + if not getattr(event_object, 'query', None) and query: + event_object.query = query + + def ProduceEvent( + self, event_object, parser_chain=None, file_entry=None, query=None): + """Produces an event onto the queue. + + Args: + event_object: the event object (instance of EventObject). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + query: Optional query string. The default is None. + """ + self.ProcessEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry, + query=query) + + if self.MatchesFilter(event_object): + return + + self._event_queue_producer.ProduceItem(event_object) + self.number_of_events += 1 + + def ProduceEvents( + self, event_objects, parser_chain=None, file_entry=None, query=None): + """Produces events onto the queue. + + Args: + event_objects: a list or generator of event objects (instances of + EventObject). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + query: Optional query string. The default is None. + """ + for event_object in event_objects: + self.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry, + query=query) + + def ProduceParseError(self, name, description, file_entry=None): + """Produces a parse error. + + Args: + name: The parser or plugin name. + description: The description of the error. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + """ + if self._parse_error_queue_producer: + path_spec = getattr(file_entry, 'path_spec', None) + parse_error = event.ParseError(name, description, path_spec=path_spec) + self._parse_error_queue_producer.ProduceItem(parse_error) + self.number_of_parse_errors += 1 + + def ResetCounters(self): + """Resets the counters.""" + self.number_of_events = 0 + self.number_of_parse_errors = 0 + + def SetFilterObject(self, filter_object): + """Sets the filter object. + + Args: + filter_object: the filter object (instance of objectfilter.Filter). + """ + self._filter_object = filter_object + + def SetMountPath(self, mount_path): + """Sets the mount path. + + Args: + mount_path: string containing the mount path. + """ + # Remove a trailing path separator from the mount path so the relative + # paths will start with a path separator. + if mount_path and mount_path.endswith(os.sep): + mount_path = mount_path[:-1] + + self._mount_path = mount_path + + def SetTextPrepend(self, text_prepend): + """Sets the text prepend. + + Args: + text_prepend: string that contains the text to prepend to every event. + """ + self._text_prepend = text_prepend + + def SignalAbort(self): + """Signals the parsers to abort.""" + self._abort = True diff --git a/plaso/parsers/cookie_plugins/__init__.py b/plaso/parsers/cookie_plugins/__init__.py new file mode 100644 index 0000000..e014011 --- /dev/null +++ b/plaso/parsers/cookie_plugins/__init__.py @@ -0,0 +1,19 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains an import statement for each browser cookie plugin.""" + +from plaso.parsers.cookie_plugins import ganalytics diff --git a/plaso/parsers/cookie_plugins/ganalytics.py b/plaso/parsers/cookie_plugins/ganalytics.py new file mode 100644 index 0000000..175f0d8 --- /dev/null +++ b/plaso/parsers/cookie_plugins/ganalytics.py @@ -0,0 +1,221 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a plugin for parsing Google Analytics cookies.""" + +import urllib + +from plaso.events import time_events +from plaso.lib import errors +from plaso.lib import eventdata +from plaso.parsers.cookie_plugins import interface + + +class GoogleAnalyticsEvent(time_events.PosixTimeEvent): + """A simple placeholder for a Google Analytics event.""" + + DATA_TYPE = u'cookie:google:analytics' + + def __init__( + self, timestamp, timestamp_desc, url, data_type_append, cookie_name, + **kwargs): + """Initialize a Google Analytics event. + + Args: + timestamp: The timestamp in a POSIX format. + timestamp_desc: A string describing the timestamp. + url: The full URL where the cookie got set. + data_type_append: String to append to the data type. + cookie_name: The name of the cookie. + """ + super(GoogleAnalyticsEvent, self).__init__( + timestamp, timestamp_desc, u'{0:s}:{1:s}'.format( + self.DATA_TYPE, data_type_append)) + + self.url = url + self.cookie_name = cookie_name + + for key, value in kwargs.iteritems(): + setattr(self, key, value) + + +class GoogleAnalyticsUtmzPlugin(interface.CookiePlugin): + """A browser cookie plugin for Google Analytics cookies.""" + + NAME = 'google_analytics_utmz' + + COOKIE_NAME = u'__utmz' + + # Point to few sources for URL information. + URLS = [ + (u'http://www.dfinews.com/articles/2012/02/' + u'google-analytics-cookies-and-forensic-implications')] + + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, + cookie_data=None, url=None, **unused_kwargs): + """Extracts event objects from the cookie. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + cookie_data: The cookie data, as a byte string. + url: The full URL or path where the cookie got set. + """ + # The structure of the field: + # .... + fields = cookie_data.split('.') + + if len(fields) > 5: + variables = u'.'.join(fields[4:]) + fields = fields[0:4] + fields.append(variables) + + if len(fields) != 5: + raise errors.WrongPlugin(u'Wrong number of fields. [{0:d} vs. 5]'.format( + len(fields))) + + domain_hash, last, sessions, sources, variables = fields + extra_variables = variables.split(u'|') + + kwargs = {} + for variable in extra_variables: + key, _, value = variable.partition(u'=') + try: + value_line = unicode(urllib.unquote(str(value)), 'utf-8') + except UnicodeDecodeError: + value_line = repr(value) + + kwargs[key] = value_line + + event_object = GoogleAnalyticsEvent( + int(last, 10), eventdata.EventTimestamp.LAST_VISITED_TIME, + url, 'utmz', self.COOKIE_NAME, domain_hash=domain_hash, + sessions=int(sessions, 10), sources=int(sources, 10), + **kwargs) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +class GoogleAnalyticsUtmaPlugin(interface.CookiePlugin): + """A browser cookie plugin for Google Analytics cookies.""" + + NAME = 'google_analytics_utma' + + COOKIE_NAME = u'__utma' + + # Point to few sources for URL information. + URLS = [ + (u'http://www.dfinews.com/articles/2012/02/' + u'google-analytics-cookies-and-forensic-implications')] + + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, + cookie_data=None, url=None, **unused_kwargs): + """Extracts event objects from the cookie. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + cookie_data: The cookie data, as a byte string. + url: The full URL or path where the cookie got set. + """ + # Values has the structure of: + # .....<# of + # sessions> + fields = cookie_data.split(u'.') + + # Check for a valid record. + if len(fields) != 6: + raise errors.WrongPlugin(u'Wrong number of fields. [{0:d} vs. 6]'.format( + len(fields))) + + domain_hash, visitor_id, first_visit, previous, last, sessions = fields + + # TODO: Double check this time is stored in UTC and not local time. + first_epoch = int(first_visit, 10) + event_object = GoogleAnalyticsEvent( + first_epoch, 'Analytics Creation Time', url, 'utma', self.COOKIE_NAME, + domain_hash=domain_hash, visitor_id=visitor_id, + sessions=int(sessions, 10)) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + event_object = GoogleAnalyticsEvent( + int(previous, 10), 'Analytics Previous Time', url, 'utma', + self.COOKIE_NAME, domain_hash=domain_hash, visitor_id=visitor_id, + sessions=int(sessions, 10)) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + event_object = GoogleAnalyticsEvent( + int(last, 10), eventdata.EventTimestamp.LAST_VISITED_TIME, + url, 'utma', self.COOKIE_NAME, domain_hash=domain_hash, + visitor_id=visitor_id, sessions=int(sessions, 10)) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +class GoogleAnalyticsUtmbPlugin(interface.CookiePlugin): + """A browser cookie plugin for Google Analytics cookies.""" + + NAME = 'google_analytics_utmb' + + COOKIE_NAME = u'__utmb' + + # Point to few sources for URL information. + URLS = [ + (u'http://www.dfinews.com/articles/2012/02/' + u'google-analytics-cookies-and-forensic-implications')] + + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, + cookie_data=None, url=None, **unused_kwargs): + """Extracts event objects from the cookie. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + cookie_data: The cookie data, as a byte string. + url: The full URL or path where the cookie got set. + """ + # Values has the structure of: + # ..10. + fields = cookie_data.split(u'.') + + # Check for a valid record. + if len(fields) != 4: + raise errors.WrongPlugin(u'Wrong number of fields. [{0:d} vs. 4]'.format( + len(fields))) + + domain_hash, pages_viewed, _, last = fields + + event_object = GoogleAnalyticsEvent( + int(last, 10), eventdata.EventTimestamp.LAST_VISITED_TIME, + url, 'utmb', self.COOKIE_NAME, domain_hash=domain_hash, + pages_viewed=int(pages_viewed, 10)) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) diff --git a/plaso/parsers/cookie_plugins/ganalytics_test.py b/plaso/parsers/cookie_plugins/ganalytics_test.py new file mode 100644 index 0000000..e6a531c --- /dev/null +++ b/plaso/parsers/cookie_plugins/ganalytics_test.py @@ -0,0 +1,139 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Google Analytics cookies.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import ganalytics as ganalytics_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers.cookie_plugins import ganalytics +from plaso.parsers.sqlite_plugins import chrome_cookies +from plaso.parsers.sqlite_plugins import firefox_cookies +from plaso.parsers.sqlite_plugins import test_lib + + +class GoogleAnalyticsPluginTest(test_lib.SQLitePluginTestCase): + """Tests for the Google Analytics plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + + def _GetAnalyticsCookies(self, event_queue_consumer): + """Return a list of analytics cookies.""" + cookies = [] + for event_object in self._GetEventObjectsFromQueue(event_queue_consumer): + if isinstance(event_object, ganalytics.GoogleAnalyticsEvent): + cookies.append(event_object) + return cookies + + def testParsingFirefox29CookieDatabase(self): + """Tests the Process function on a Firefox 29 cookie database file.""" + plugin = firefox_cookies.FirefoxCookiePlugin() + test_file = self._GetTestFilePath(['firefox_cookies.sqlite']) + event_queue_consumer = self._ParseDatabaseFileWithPlugin(plugin, test_file) + event_objects = self._GetAnalyticsCookies(event_queue_consumer) + + self.assertEquals(len(event_objects), 25) + + event_object = event_objects[14] + + self.assertEquals( + event_object.utmcct, + u'/frettir/erlent/2013/10/30/maelt_med_kerfisbundnum_hydingum/') + self.assertEquals( + event_object.timestamp, timelib_test.CopyStringToTimestamp( + '2013-10-30 21:56:06')) + self.assertEquals(event_object.url, u'http://ads.aha.is/') + self.assertEquals(event_object.utmcsr, u'mbl.is') + + expected_msg = ( + u'http://ads.aha.is/ (__utmz) Sessions: 1 Domain Hash: 137167072 ' + u'Sources: 1 Last source used to access: mbl.is Ad campaign ' + u'information: (referral) Last type of visit: referral Path to ' + u'the page of referring link: /frettir/erlent/2013/10/30/' + u'maelt_med_kerfisbundnum_hydingum/') + + self._TestGetMessageStrings( + event_object, expected_msg, u'http://ads.aha.is/ (__utmz)') + + def testParsingChromeCookieDatabase(self): + """Test the process function on a Chrome cookie database.""" + plugin = chrome_cookies.ChromeCookiePlugin() + test_file = self._GetTestFilePath(['cookies.db']) + event_queue_consumer = self._ParseDatabaseFileWithPlugin(plugin, test_file) + event_objects = self._GetAnalyticsCookies(event_queue_consumer) + + # The cookie database contains 560 entries in total. Out of them + # there are 75 events created by the Google Analytics plugin. + self.assertEquals(len(event_objects), 75) + # Check few "random" events to verify. + + # Check an UTMZ Google Analytics event. + event_object = event_objects[39] + self.assertEquals(event_object.utmctr, u'enders game') + self.assertEquals(event_object.domain_hash, u'68898382') + self.assertEquals(event_object.sessions, 1) + + expected_msg = ( + u'http://imdb.com/ (__utmz) Sessions: 1 Domain Hash: 68898382 ' + u'Sources: 1 Last source used to access: google Ad campaign ' + u'information: (organic) Last type of visit: organic Keywords ' + u'used to find site: enders game') + self._TestGetMessageStrings( + event_object, expected_msg, u'http://imdb.com/ (__utmz)') + + # Check the UTMA Google Analytics event. + event_object = event_objects[41] + self.assertEquals(event_object.timestamp_desc, u'Analytics Previous Time') + self.assertEquals(event_object.cookie_name, u'__utma') + self.assertEquals(event_object.visitor_id, u'1827102436') + self.assertEquals(event_object.sessions, 2) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-03-22 01:55:29') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'http://assets.tumblr.com/ (__utma) Sessions: 2 Domain Hash: ' + u'151488169 Visitor ID: 151488169') + self._TestGetMessageStrings( + event_object, expected_msg, u'http://assets.tumblr.com/ (__utma)') + + # Check the UTMB Google Analytics event. + event_object = event_objects[34] + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.LAST_VISITED_TIME) + self.assertEquals(event_object.cookie_name, u'__utmb') + self.assertEquals(event_object.domain_hash, u'154523900') + self.assertEquals(event_object.pages_viewed, 1) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-03-22 01:48:30') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'http://upressonline.com/ (__utmb) Pages Viewed: 1 Domain Hash: ' + u'154523900') + self._TestGetMessageStrings( + event_object, expected_msg, u'http://upressonline.com/ (__utmb)') + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/cookie_plugins/interface.py b/plaso/parsers/cookie_plugins/interface.py new file mode 100644 index 0000000..3e03c53 --- /dev/null +++ b/plaso/parsers/cookie_plugins/interface.py @@ -0,0 +1,115 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains an interface for browser cookie plugins.""" + +import abc + +from plaso.lib import errors +from plaso.lib import registry +from plaso.parsers import plugins + + +# TODO: move this into the parsers and plugins manager. +def GetPlugins(): + """Returns a list of all cookie plugins.""" + plugins_list = [] + for plugin_cls in CookiePlugin.classes.itervalues(): + parent_name = getattr(plugin_cls, 'parent_class_name', 'NOTHERE') + if parent_name != 'cookie': + continue + + plugins_list.append(plugin_cls()) + + return plugins_list + + +class CookiePlugin(plugins.BasePlugin): + """A browser cookie plugin for Plaso. + + This is a generic cookie parsing interface that can handle parsing + cookies from all browsers. + """ + __metaclass__ = registry.MetaclassRegistry + __abstract = True + + NAME = 'cookie' + + # The name of the cookie value that this plugin is designed to parse. + # This value is used to evaluate whether the plugin is the correct one + # to parse the browser cookie. + COOKIE_NAME = u'' + + def __init__(self): + """Initialize the browser cookie plugin.""" + super(CookiePlugin, self).__init__() + self.cookie_data = '' + + @abc.abstractmethod + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, + cookie_data=None, url=None, **kwargs): + """Extract and return EventObjects from the data structure. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + cookie_data: Optional cookie data, as a byte string. + url: Optional URL or path where the cookie got set. + """ + + def Process( + self, parser_context, file_entry=None, parser_chain=None, + cookie_name=None, cookie_data=None, url=None, **kwargs): + """Determine if this is the right plugin for this cookie. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + cookie_name: The name of the cookie value. + cookie_data: The cookie data, as a byte string. + url: The full URL or path where the cookie got set. + + Raises: + errors.WrongPlugin: If the cookie name differs from the one + supplied in COOKIE_NAME. + ValueError: If cookie_name or cookie_data are not set. + """ + if cookie_name is None or cookie_data is None: + raise ValueError(u'Cookie name or data are not set.') + + if cookie_name != self.COOKIE_NAME: + raise errors.WrongPlugin( + u'Not the correct cookie plugin for: {0:s} [{1:s}]'.format( + cookie_name, self.NAME)) + + # This will raise if unhandled keyword arguments are passed. + super(CookiePlugin, self).Process(parser_context, **kwargs) + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + self.GetEntries( + parser_context, file_entry=file_entry, parser_chain=parser_chain, + cookie_data=cookie_data, url=url) diff --git a/plaso/parsers/cookie_plugins/test_lib.py b/plaso/parsers/cookie_plugins/test_lib.py new file mode 100644 index 0000000..76e50a7 --- /dev/null +++ b/plaso/parsers/cookie_plugins/test_lib.py @@ -0,0 +1,24 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Browser cookie plugin related functions and classes for testing.""" + +from plaso.parsers import test_lib + + +class CookiePluginTestCase(test_lib.ParserTestCase): + """The unit test case for a browser cookie plugin.""" diff --git a/plaso/parsers/cups_ipp.py b/plaso/parsers/cups_ipp.py new file mode 100644 index 0000000..dcea9a9 --- /dev/null +++ b/plaso/parsers/cups_ipp.py @@ -0,0 +1,345 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The CUPS IPP Control Files Parser. + +CUPS IPP version 1.0: +* http://tools.ietf.org/html/rfc2565 +* http://tools.ietf.org/html/rfc2566 +* http://tools.ietf.org/html/rfc2567 +* http://tools.ietf.org/html/rfc2568 +* http://tools.ietf.org/html/rfc2569 +* http://tools.ietf.org/html/rfc2639 + +CUPS IPP version 1.1: +* http://tools.ietf.org/html/rfc2910 +* http://tools.ietf.org/html/rfc2911 +* http://tools.ietf.org/html/rfc3196 +* http://tools.ietf.org/html/rfc3510 + +CUPS IPP version 2.0: +* N/A +""" + +import construct +import logging +import os + +from plaso.lib import errors +from plaso.lib import event +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import interface +from plaso.parsers import manager + + +__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)' + + +# TODO: RFC Pendings types: resolution, dateTime, rangeOfInteger. +# "dateTime" is not used by Mac OS, instead it uses integer types. +# TODO: Only tested against CUPS IPP Mac OS X. + + +class CupsIppEvent(event.EventObject): + """Convenience class for an cups ipp event.""" + + DATA_TYPE = 'cups:ipp:event' + + def __init__( + self, timestamp, timestamp_desc, data_dict): + """Initializes the event object. + + Args: + timestamp: Timestamp of the entry. + timestamp_desc: Description of the timestamp. + data_dict: Dictionary with all the pairs coming from IPP file. + user: String with the system user name. + owner: String with the real name of the user. + computer_name: String with the name of the computer. + printer_id: String with the identification name of the print. + uri: String with the URL of the CUPS service. + job_id: String with the identification id of the job. + job_name: String with the job name. + copies: Integer with the number of copies. + application: String with the application that prints the document. + doc_usingtype: String with the type of document. + data_dict: Dictionary with all the parsed data comming from the file. + """ + super(CupsIppEvent, self).__init__() + self.timestamp = timelib.Timestamp.FromPosixTime(timestamp) + self.timestamp_desc = timestamp_desc + # TODO: Find a better solution than to have join for each attribute. + self.user = self._ListToString(data_dict.get('user', None)) + self.owner = self._ListToString(data_dict.get('owner', None)) + self.computer_name = self._ListToString(data_dict.get( + 'computer_name', None)) + self.printer_id = self._ListToString(data_dict.get('printer_id', None)) + self.uri = self._ListToString(data_dict.get('uri', None)) + self.job_id = self._ListToString(data_dict.get('job_id', None)) + self.job_name = self._ListToString(data_dict.get('job_name', None)) + self.copies = data_dict.get('copies', 0)[0] + self.application = self._ListToString(data_dict.get('application', None)) + self.doc_type = self._ListToString(data_dict.get('doc_type', None)) + self.data_dict = data_dict + + def _ListToString(self, values): + """Returns a string from a list value using comma as a delimiter. + + If any value inside the list contains comma, which is the delimiter, + the entire field is surrounded with double quotes. + + Args: + values: A list or tuple containing the values. + + Returns: + A string containing all the values joined using comma as a delimiter + or None. + """ + if values is None: + return + + if type(values) not in (list, tuple): + return + + for index, value in enumerate(values): + if ',' in value: + values[index] = u'"{0:s}"'.format(value) + + try: + return u', '.join(values) + except UnicodeDecodeError as exception: + logging.error( + u'Unable to parse log line, with error: {0:s}'.format(exception)) + + +class CupsIppParser(interface.BaseParser): + """Parser for CUPS IPP files. """ + + NAME = 'cups_ipp' + DESCRIPTION = u'Parser for CUPS IPP files.' + + # INFO: + # For each file, we have only one document with three different timestamps: + # Created, process and finished. + # Format: + # [HEADER: MAGIC + KNOWN_TYPE][GROUP A]...[GROUP Z][GROUP_END: 0x03] + # GROUP: [GROUP ID][PAIR A]...[PAIR Z] where [PAIR: NAME + VALUE] + # GROUP ID: [1byte ID] + # PAIR: [TagID][\x00][Name][Value]) + # TagID: 1 byte integer with the type of "Value". + # Name: [Length][Text][\00] + # Name can be empty when the name has more than one value. + # Example: family name "lopez mata" with more than one surname. + # Type_Text + [0x06, family, 0x00] + [0x05, lopez, 0x00] + + # Type_Text + [0x00, 0x00] + [0x04, mata, 0x00] + # Value: can be integer, boolean, or text provided by TagID. + # If boolean, Value: [\x01][0x00(False)] or [\x01(True)] + # If integer, Value: [\x04][Integer] + # If text, Value: [Length text][Text][\00] + + # Magic number that identify the CUPS IPP supported version. + IPP_MAJOR_VERSION = 2 + IPP_MINOR_VERSION = 0 + # Supported Operation ID. + IPP_OP_ID = 5 + + # CUPS IPP File header. + CUPS_IPP_HEADER = construct.Struct( + 'cups_ipp_header_struct', + construct.UBInt8('major_version'), + construct.UBInt8('minor_version'), + construct.UBInt16('operation_id'), + construct.UBInt32('request_id')) + + # Group ID that indicates the end of the IPP Control file. + GROUP_END = 3 + # Identification Groups. + GROUP_LIST = [1, 2, 4, 5, 6, 7] + + # Type ID. + TYPE_GENERAL_INTEGER = 32 + TYPE_INTEGER = 33 + TYPE_ENUMERATION = 35 + TYPE_BOOL = 34 + + # Type of values that can be extracted. + INTEGER_8 = construct.UBInt8('integer') + INTEGER_32 = construct.UBInt32('integer') + TEXT = construct.PascalString( + 'text', + length_field=construct.UBInt8('length')) + BOOLEAN = construct.Struct( + 'boolean_value', + construct.Padding(1), + INTEGER_8) + INTEGER = construct.Struct( + 'integer_value', + construct.Padding(1), + INTEGER_32) + + # Name of the pair. + PAIR_NAME = construct.Struct( + 'pair_name', + TEXT, + construct.Padding(1)) + + # Specific CUPS IPP to generic name. + NAME_PAIR_TRANSLATION = { + 'printer-uri': u'uri', + 'job-uuid': u'job_id', + 'DestinationPrinterID': u'printer_id', + 'job-originating-user-name': u'user', + 'job-name': u'job_name', + 'document-format': u'doc_type', + 'job-originating-host-name': u'computer_name', + 'com.apple.print.JobInfo.PMApplicationName': u'application', + 'com.apple.print.JobInfo.PMJobOwner': u'owner'} + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract a entry from an CUPS IPP file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + parser_chain = self._BuildParserChain(parser_chain) + + file_object = file_entry.GetFileObject() + file_object.seek(0, os.SEEK_SET) + + try: + header = self.CUPS_IPP_HEADER.parse_stream(file_object) + except (IOError, construct.FieldError) as exception: + file_object.close() + raise errors.UnableToParseFile( + u'Unable to parse CUPS IPP Header with error: {0:s}'.format( + exception)) + + if (header.major_version != self.IPP_MAJOR_VERSION or + header.minor_version != self.IPP_MINOR_VERSION): + file_object.close() + raise errors.UnableToParseFile( + u'[{0:s}] Unsupported version number.'.format(self.NAME)) + + if header.operation_id != self.IPP_OP_ID: + # Warn if the operation ID differs from the standard one. We should be + # able to parse the file nonetheless. + logging.debug( + u'[{0:s}] Unsupported operation identifier in file: {1:s}.'.format( + self.NAME, parser_context.GetDisplayName(file_entry))) + + # Read the pairs extracting the name and the value. + data_dict = {} + name, value = self.ReadPair(parser_context, file_entry, file_object) + while name or value: + # Translate the known "name" CUPS IPP to a generic name value. + pretty_name = self.NAME_PAIR_TRANSLATION.get(name, name) + data_dict.setdefault(pretty_name, []).append(value) + name, value = self.ReadPair(parser_context, file_entry, file_object) + + if u'time-at-creation' in data_dict: + event_object = CupsIppEvent( + data_dict['time-at-creation'][0], + eventdata.EventTimestamp.CREATION_TIME, data_dict) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if u'time-at-processing' in data_dict: + event_object = CupsIppEvent( + data_dict['time-at-processing'][0], + eventdata.EventTimestamp.START_TIME, data_dict) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if u'time-at-completed' in data_dict: + event_object = CupsIppEvent( + data_dict['time-at-completed'][0], + eventdata.EventTimestamp.END_TIME, data_dict) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + file_object.close() + + def ReadPair(self, parser_context, file_entry, file_object): + """Reads an attribute name and value pair from a CUPS IPP event. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + file_object: a file-like object that points to a file. + + Returns: + A list of name and value. If name and value cannot be read both are + set to None. + """ + # Pair = Type ID + Name + Value. + try: + # Can be: + # Group ID + IDtag = Group ID (1byte) + Tag ID (1byte) + '0x00'. + # IDtag = Tag ID (1byte) + '0x00'. + type_id = self.INTEGER_8.parse_stream(file_object) + if type_id == self.GROUP_END: + return None, None + + elif type_id in self.GROUP_LIST: + # If it is a group ID we must read the next byte that contains + # the first TagID. + type_id = self.INTEGER_8.parse_stream(file_object) + + # 0x00 separator character. + _ = self.INTEGER_8.parse_stream(file_object) + + except (IOError, construct.FieldError): + logging.warning( + u'[{0:s}] Unsupported identifier in file: {1:s}.'.format( + self.NAME, parser_context.GetDisplayName(file_entry))) + return None, None + + # Name = Length name + name + 0x00 + try: + name = self.PAIR_NAME.parse_stream(file_object).text + except (IOError, construct.FieldError): + logging.warning( + u'[{0:s}] Unsupported name in file: {1:s}.'.format( + self.NAME, parser_context.GetDisplayName(file_entry))) + return None, None + + # Value: can be integer, boolean or text select by Type ID. + try: + if type_id in [ + self.TYPE_GENERAL_INTEGER, self.TYPE_INTEGER, self.TYPE_ENUMERATION]: + value = self.INTEGER.parse_stream(file_object).integer + + elif type_id == self.TYPE_BOOL: + value = bool(self.BOOLEAN.parse_stream(file_object).integer) + + else: + value = self.TEXT.parse_stream(file_object) + + except (IOError, construct.FieldError): + logging.warning( + u'[{0:s}] Unsupported value in file: {1:s}.'.format( + self.NAME, parser_context.GetDisplayName(file_entry))) + return None, None + + return name, value + + +manager.ParsersManager.RegisterParser(CupsIppParser) diff --git a/plaso/parsers/cups_ipp_test.py b/plaso/parsers/cups_ipp_test.py new file mode 100644 index 0000000..d614da2 --- /dev/null +++ b/plaso/parsers/cups_ipp_test.py @@ -0,0 +1,98 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser test for Mac Cups IPP Log files.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import cups_ipp as cups_ipp_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import cups_ipp +from plaso.parsers import test_lib + + +class CupsIppParserTest(test_lib.ParserTestCase): + """The unit test for Mac Cups IPP parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = cups_ipp.CupsIppParser() + + def testParse(self): + """Tests the Parse function.""" + # TODO: only tested against Mac OS X Cups IPP (Version 2.0) + test_file = self._GetTestFilePath(['mac_cups_ipp']) + events = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(events) + + self.assertEqual(len(event_objects), 3) + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-03 18:07:21') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual( + event_object.timestamp_desc, + eventdata.EventTimestamp.CREATION_TIME) + self.assertEqual(event_object.application, u'LibreOffice') + self.assertEqual(event_object.job_name, u'Assignament 1') + self.assertEqual(event_object.computer_name, u'localhost') + self.assertEqual(event_object.copies, 1) + self.assertEqual(event_object.doc_type, u'application/pdf') + expected_job = u'urn:uuid:d51116d9-143c-3863-62aa-6ef0202de49a' + self.assertEqual(event_object.job_id, expected_job) + self.assertEqual(event_object.owner, u'Joaquin Moreno Garijo') + self.assertEqual(event_object.user, u'moxilo') + self.assertEqual(event_object.printer_id, u'RHULBW') + expected_uri = u'ipp://localhost:631/printers/RHULBW' + self.assertEqual(event_object.uri, expected_uri) + expected_msg = ( + u'User: moxilo ' + u'Owner: Joaquin Moreno Garijo ' + u'Job Name: Assignament 1 ' + u'Application: LibreOffice ' + u'Printer: RHULBW') + expected_msg_short = ( + u'Job Name: Assignament 1') + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + event_object = event_objects[1] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-03 18:07:21') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual( + event_object.timestamp_desc, + eventdata.EventTimestamp.START_TIME) + + event_object = event_objects[2] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-03 18:07:32') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual( + event_object.timestamp_desc, + eventdata.EventTimestamp.END_TIME) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/custom_destinations.py b/plaso/parsers/custom_destinations.py new file mode 100644 index 0000000..730c285 --- /dev/null +++ b/plaso/parsers/custom_destinations.py @@ -0,0 +1,212 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for .customDestinations-ms files.""" + +import logging +import os + +import construct +from dfvfs.lib import definitions +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver import resolver + +from plaso.lib import errors +from plaso.parsers import interface +from plaso.parsers import manager +from plaso.parsers import winlnk + + +class CustomDestinationsParser(interface.BaseParser): + """Parses .customDestinations-ms files.""" + + NAME = 'custom_destinations' + DESCRIPTION = u'Parser for *.customDestinations-ms files.' + + # We cannot use the parser registry here since winlnk could be disabled. + # TODO: see if there is a more elegant solution for this. + _WINLNK_PARSER = winlnk.WinLnkParser() + + _LNK_GUID = '\x01\x14\x02\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00\x46' + + _FILE_HEADER = construct.Struct( + 'file_header', + construct.ULInt32('unknown1'), + construct.ULInt32('unknown2'), + construct.ULInt32('unknown3'), + construct.ULInt32('header_values_type')) + + _HEADER_VALUE_TYPE_0 = construct.Struct( + 'header_value_type_0', + construct.ULInt32('number_of_characters'), + construct.String('string', lambda ctx: ctx.number_of_characters * 2), + construct.ULInt32('unknown1')) + + _HEADER_VALUE_TYPE_1_OR_2 = construct.Struct( + 'header_value_type_1_or_2', + construct.ULInt32('unknown1')) + + _ENTRY_HEADER = construct.Struct( + 'entry_header', + construct.String('guid', 16)) + + _FILE_FOOTER = construct.Struct( + 'file_footer', + construct.ULInt32('signature')) + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract data from an *.customDestinations-ms file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + file_object = file_entry.GetFileObject() + self.ParseFileObject( + parser_context, file_object, file_entry=file_entry, + parser_chain=parser_chain) + file_object.close() + + def ParseFileObject( + self, parser_context, file_object, file_entry=None, parser_chain=None): + """Extract data from an *.customDestinations-ms file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_object: A file-like object. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Raises: + UnableToParseFile: when the file cannot be parsed. + """ + parser_chain = self._BuildParserChain(parser_chain) + + try: + file_header = self._FILE_HEADER.parse_stream(file_object) + except (IOError, construct.FieldError) as exception: + raise errors.UnableToParseFile(( + u'Unable to parse Custom Destination file header with error: ' + u'{0:s}').format(exception)) + + if file_header.unknown1 != 2: + raise errors.UnableToParseFile(( + u'Unsupported Custom Destination file - invalid unknown1: ' + u'{0:d}.').format(file_header.unknown1)) + + if file_header.header_values_type > 2: + raise errors.UnableToParseFile(( + u'Unsupported Custom Destination file - invalid header value type: ' + u'{0:d}.').format(file_header.header_values_type)) + + if file_header.header_values_type == 0: + data_structure = self._HEADER_VALUE_TYPE_0 + else: + data_structure = self._HEADER_VALUE_TYPE_1_OR_2 + + try: + _ = data_structure.parse_stream(file_object) + except (IOError, construct.FieldError) as exception: + raise errors.UnableToParseFile(( + u'Unable to parse Custom Destination file header value with error: ' + u'{0:s}').format(exception)) + + file_size = file_object.get_size() + file_offset = file_object.get_offset() + remaining_file_size = file_size - file_offset + + # The Custom Destination file does not have a unique signature in + # the file header that is why we use the first LNK class identifier (GUID) + # as a signature. + first_guid_checked = False + while remaining_file_size > 4: + try: + entry_header = self._ENTRY_HEADER.parse_stream(file_object) + except (IOError, construct.FieldError) as exception: + if not first_guid_checked: + raise errors.UnableToParseFile(( + u'Unable to parse Custom Destination file entry header with ' + u'error: {0:s}').format(exception)) + else: + logging.warning(( + u'Unable to parse Custom Destination file entry header with ' + u'error: {0:s}').format(exception)) + break + + if entry_header.guid != self._LNK_GUID: + if not first_guid_checked: + raise errors.UnableToParseFile( + u'Unsupported Custom Destination file - invalid entry header.') + else: + logging.warning( + u'Unsupported Custom Destination file - invalid entry header.') + break + + first_guid_checked = True + file_offset += 16 + remaining_file_size -= 16 + + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_DATA_RANGE, range_offset=file_offset, + range_size=remaining_file_size, parent=file_entry.path_spec) + + try: + lnk_file_object = resolver.Resolver.OpenFileObject(path_spec) + except RuntimeError as exception: + logging.error(( + u'[{0:s}] Unable to open LNK file from {1:s} with error: ' + u'{2:s}').format( + parser_chain, + file_entry.path_spec.comparable.replace(u'\n', u';'), + exception)) + return + + display_name = u'{0:s} # 0x{1:08x}'.format( + parser_context.GetDisplayName(file_entry), file_offset) + + self._WINLNK_PARSER.ParseFileObject( + parser_context, lnk_file_object, file_entry=file_entry, + parser_chain=parser_chain, display_name=display_name) + + # We cannot trust the file size in the LNK data so we get the last offset + # that was read instead. + lnk_file_size = lnk_file_object.get_offset() + + lnk_file_object.close() + + file_offset += lnk_file_size + remaining_file_size -= lnk_file_size + + file_object.seek(file_offset, os.SEEK_SET) + + try: + file_footer = self._FILE_FOOTER.parse_stream(file_object) + except (IOError, construct.FieldError) as exception: + logging.warning(( + u'Unable to parse Custom Destination file footer with error: ' + u'{0:s}').format(exception)) + + if file_footer.signature != 0xbabffbab: + logging.warning( + u'Unsupported Custom Destination file - invalid footer signature.') + + +manager.ParsersManager.RegisterParser(CustomDestinationsParser) diff --git a/plaso/parsers/custom_destinations_test.py b/plaso/parsers/custom_destinations_test.py new file mode 100644 index 0000000..c22572a --- /dev/null +++ b/plaso/parsers/custom_destinations_test.py @@ -0,0 +1,116 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the .customDestinations-ms file parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winlnk as winlnk_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import test_lib +from plaso.parsers import custom_destinations + + +class CustomDestinationsParserTest(test_lib.ParserTestCase): + """Tests for the .customDestinations-ms file parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = custom_destinations.CustomDestinationsParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath([ + u'5afe4de1b92fc382.customDestinations-ms']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEqual(len(event_objects), 108) + + # A shortcut event object. + # The last accessed timestamp. + event_object = event_objects[105] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2009-07-13 23:55:56.248103') + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.ACCESS_TIME) + self.assertEquals(event_object.timestamp, expected_timestamp) + + # The creation timestamp. + event_object = event_objects[106] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2009-07-13 23:55:56.248103') + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.CREATION_TIME) + self.assertEquals(event_object.timestamp, expected_timestamp) + + # The last modification timestamp. + event_object = event_objects[107] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2009-07-14 01:39:11.388000') + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.MODIFICATION_TIME) + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'[@%systemroot%\\system32\\oobefldr.dll,-1262] ' + u'File size: 11776 ' + u'File attribute flags: 0x00000020 ' + u'Drive type: 3 ' + u'Drive serial number: 0x24ba718b ' + u'Local path: C:\\Windows\\System32\\GettingStarted.exe ' + u'cmd arguments: {DE3895CB-077B-4C38-B6E3-F3DE1E0D84FC} ' + u'%systemroot%\\system32\\control.exe /name Microsoft.Display ' + u'env location: %SystemRoot%\\system32\\GettingStarted.exe ' + u'Icon location: %systemroot%\\system32\\display.dll ' + u'Link target: [My Computer, C:\\, Windows, System32, ' + u'GettingStarted.exe]') + + expected_msg_short = ( + u'[@%systemroot%\\system32\\oobefldr.dll,-1262] ' + u'C:\\Windows\\System32\\GettingStarte...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + # A shell item event object. + event_object = event_objects[16] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2010-11-10 07:41:04') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'Name: System32 ' + u'Long name: System32 ' + u'NTFS file reference: 2331-1 ' + u'Origin: 5afe4de1b92fc382.customDestinations-ms') + + expected_msg_short = ( + u'Name: System32 ' + u'NTFS file reference: 2331-1 ' + u'Origin: 5afe4de1b92fc382.customDes...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/esedb.py b/plaso/parsers/esedb.py new file mode 100644 index 0000000..6613795 --- /dev/null +++ b/plaso/parsers/esedb.py @@ -0,0 +1,98 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for Extensible Storage Engine (ESE) database files (EDB).""" + +import logging + +import pyesedb + +from plaso.lib import errors +from plaso.parsers import interface +from plaso.parsers import manager +from plaso.parsers import plugins + + +if pyesedb.get_version() < '20140301': + raise ImportWarning(u'EseDbParser requires at least pyesedb 20140301.') + + +class EseDbCache(plugins.BasePluginCache): + """A cache storing query results for ESEDB plugins.""" + + +class EseDbParser(interface.BasePluginsParser): + """Parses Extensible Storage Engine (ESE) database files (EDB).""" + + NAME = 'esedb' + DESCRIPTION = u'Parser for Extensible Storage Engine (ESE) database files.' + + _plugin_classes = {} + + def __init__(self): + """Initializes a parser object.""" + super(EseDbParser, self).__init__() + self._plugins = EseDbParser.GetPluginObjects() + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extracts data from an ESE database File. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Raises: + UnableToParseFile: when the file cannot be parsed. + """ + file_object = file_entry.GetFileObject() + esedb_file = pyesedb.file() + + try: + esedb_file.open_file_object(file_object) + except IOError as exception: + file_object.close() + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse file {1:s} with error: {2:s}'.format( + self.NAME, file_entry.name, exception)) + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + # Compare the list of available plugins. + cache = EseDbCache() + for plugin_object in self._plugins: + try: + plugin_object.Process( + parser_context, file_entry=file_entry, parser_chain=parser_chain, + database=esedb_file, cache=cache) + + except errors.WrongPlugin: + logging.debug(( + u'[{0:s}] plugin: {1:s} cannot parse the ESE database: ' + u'{2:s}').format( + self.NAME, plugin_object.NAME, file_entry.name)) + + # TODO: explicitly clean up cache. + + esedb_file.close() + file_object.close() + + +manager.ParsersManager.RegisterParser(EseDbParser) diff --git a/plaso/parsers/esedb_plugins/__init__.py b/plaso/parsers/esedb_plugins/__init__.py new file mode 100644 index 0000000..b0ac3cc --- /dev/null +++ b/plaso/parsers/esedb_plugins/__init__.py @@ -0,0 +1,20 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains import statements for the ESE database plugins.""" + +from plaso.parsers.esedb_plugins import msie_webcache diff --git a/plaso/parsers/esedb_plugins/interface.py b/plaso/parsers/esedb_plugins/interface.py new file mode 100644 index 0000000..8a26599 --- /dev/null +++ b/plaso/parsers/esedb_plugins/interface.py @@ -0,0 +1,303 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the interface for ESE database plugins.""" + +import construct +import logging + +import pyesedb + +from plaso.lib import errors +from plaso.parsers import plugins + + +class EseDbPlugin(plugins.BasePlugin): + """The ESE database plugin interface.""" + + NAME = 'esedb' + + BINARY_DATA_COLUMN_TYPES = frozenset([ + pyesedb.column_types.BINARY_DATA, + pyesedb.column_types.LARGE_BINARY_DATA]) + + FLOATING_POINT_COLUMN_TYPES = frozenset([ + pyesedb.column_types.FLOAT_32BIT, + pyesedb.column_types.DOUBLE_64BIT]) + + INTEGER_COLUMN_TYPES = frozenset([ + pyesedb.column_types.CURRENCY, + pyesedb.column_types.DATE_TIME, + pyesedb.column_types.INTEGER_8BIT_UNSIGNED, + pyesedb.column_types.INTEGER_16BIT_SIGNED, + pyesedb.column_types.INTEGER_16BIT_UNSIGNED, + pyesedb.column_types.INTEGER_32BIT_SIGNED, + pyesedb.column_types.INTEGER_32BIT_UNSIGNED, + pyesedb.column_types.INTEGER_64BIT_SIGNED]) + + STRING_COLUMN_TYPES = frozenset([ + pyesedb.column_types.TEXT, + pyesedb.column_types.LARGE_TEXT]) + + _UINT64_BIG_ENDIAN = construct.UBInt64('value') + _UINT64_LITTLE_ENDIAN = construct.ULInt64('value') + + # Dictionary containing a callback method per table name. + # E.g. 'SystemIndex_0A': 'ParseSystemIndex_0A' + REQUIRED_TABLES = {} + OPTIONAL_TABLES = {} + + def __init__(self): + """Initializes the ESE database plugin.""" + super(EseDbPlugin, self).__init__() + self._required_tables = frozenset(self.REQUIRED_TABLES.keys()) + self._tables = {} + self._tables.update(self.REQUIRED_TABLES) + self._tables.update(self.OPTIONAL_TABLES) + + def _ConvertValueBinaryDataToStringAscii(self, value): + """Converts a binary data value into a string. + + Args: + value: The binary data value containing an ASCII string or None. + + Returns: + A string or None if value is None. + """ + if value: + return value.decode('ascii') + + def _ConvertValueBinaryDataToStringBase16(self, value): + """Converts a binary data value into a base-16 (hexadecimal) string. + + Args: + value: The binary data value or None. + + Returns: + A string or None if value is None. + """ + if value: + return value.encode('hex') + + def _ConvertValueBinaryDataToUBInt64(self, value): + """Converts a binary data value into an integer. + + Args: + value: The binary data value containing an unsigned 64-bit big-endian + integer. + + Returns: + An integer or None if value is None. + """ + if value: + return self._UINT64_BIG_ENDIAN.parse(value) + + def _ConvertValueBinaryDataToULInt64(self, value): + """Converts a binary data value into an integer. + + Args: + value: The binary data value containing an unsigned 64-bit little-endian + integer. + + Returns: + An integer or None if value is None. + """ + if value: + return self._UINT64_LITTLE_ENDIAN.parse(value) + + def _GetRecordValue(self, record, value_entry): + """Retrieves a specific value from the record. + + Args: + record: The ESE record object (instance of pyesedb.record). + value_entry: The value entry. + + Returns: + An object containing the value. + """ + column_type = record.get_column_type(value_entry) + value_data_flags = record.get_value_data_flags(value_entry) + + if value_data_flags & pyesedb.value_flags.MULTI_VALUE: + # TODO: implement + pass + + elif column_type == pyesedb.column_types.NULL: + return + + elif column_type == pyesedb.column_types.BOOLEAN: + # TODO: implement + pass + + elif column_type in self.INTEGER_COLUMN_TYPES: + return record.get_value_data_as_integer(value_entry) + + elif column_type in self.FLOATING_POINT_COLUMN_TYPES: + return record.get_value_data_as_floating_point(value_entry) + + elif column_type in self.STRING_COLUMN_TYPES: + return record.get_value_data_as_string(value_entry) + + elif column_type == pyesedb.column_types.GUID: + # TODO: implement + pass + + return record.get_value_data(value_entry) + + def _GetRecordValues(self, table_name, record, value_mappings=None): + """Retrieves the values from the record. + + Args: + table_name: The name of the table. + record: The ESE record object (instance of pyesedb.record). + value_mappings: Optional dict of value mappings, which map the column + name to a callback method. The default is None. + + Returns: + An dict containing the values. + """ + record_values = {} + + for value_entry in range(0, record.number_of_values): + column_name = record.get_column_name(value_entry) + if column_name in record_values: + logging.warning( + u'[{0:s}] duplicate column: {1:s} in table: {2:s}'.format( + self.NAME, column_name, table_name)) + continue + + value_callback = None + if value_mappings and column_name in value_mappings: + value_callback_method = value_mappings.get(column_name) + if value_callback_method: + value_callback = getattr(self, value_callback_method, None) + if value_callback is None: + logging.warning(( + u'[{0:s}] missing value callback method: {1:s} for column: ' + u'{2:s} in table: {3:s}').format( + self.NAME, value_callback_method, column_name, table_name)) + + value = self._GetRecordValue(record, value_entry) + if value_callback: + value = value_callback(value) + + record_values[column_name] = value + + return record_values + + def _GetTableNames(self, database): + """Retrieves the table names in a database. + + Args: + database: The ESE database object (instance of pyesedb.file). + + Returns: + A list of the table names. + """ + table_names = [] + for esedb_table in database.tables: + table_names.append(esedb_table.name) + + return table_names + + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, database=None, + cache=None, **kwargs): + """Extracts event objects from the database. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + database: Optional ESE database object (instance of pyesedb.file). + The default is None. + cache: Optional cache object (instance of EseDbCache). The default is + None. + + Raises: + ValueError: If the database attribute is not valid. + """ + if database is None: + raise ValueError(u'Invalid database.') + + for table_name, callback_method in self._tables.iteritems(): + if not callback_method: + # Table names without a callback method are allowed to improve + # the detection of a database based on its table names. + continue + + callback = getattr(self, callback_method, None) + if callback is None: + logging.warning( + u'[{0:s}] missing callback method: {1:s} for table: {2:s}'.format( + self.NAME, callback_method, table_name)) + continue + + esedb_table = database.get_table_by_name(table_name) + if not esedb_table: + logging.warning(u'[{0:s}] missing table: {1:s}'.format( + self.NAME, table_name)) + continue + + # The database is passed in case the database contains table names + # that are assigned dynamically and cannot be defined by + # the table name-callback mechanism. + callback( + parser_context, file_entry=file_entry, parser_chain=parser_chain, + database=database, table=esedb_table, cache=cache, **kwargs) + + def Process( + self, parser_context, file_entry=None, parser_chain=None, database=None, + cache=None, **kwargs): + """Determines if this is the appropriate plugin for the database. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + database: Optional ESE database object (instance of pyesedb.file). + The default is None. + cache: Optional cache object (instance of EseDbCache). The default is + None. + + Raises: + errors.WrongPlugin: If the database does not contain all the tables + defined in the required_tables set. + ValueError: If the database attribute is not valid. + """ + if database is None: + raise ValueError(u'Invalid database.') + + table_names = frozenset(self._GetTableNames(database)) + if self._required_tables.difference(table_names): + raise errors.WrongPlugin( + u'[{0:s}] required tables not found.'.format(self.NAME)) + + # This will raise if unhandled keyword arguments are passed. + super(EseDbPlugin, self).Process(parser_context, **kwargs) + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + self.GetEntries( + parser_context, file_entry=file_entry, parser_chain=parser_chain, + database=database, cache=cache, **kwargs) diff --git a/plaso/parsers/esedb_plugins/msie_webcache.py b/plaso/parsers/esedb_plugins/msie_webcache.py new file mode 100644 index 0000000..040355c --- /dev/null +++ b/plaso/parsers/esedb_plugins/msie_webcache.py @@ -0,0 +1,366 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for the Microsoft Internet Explorer WebCache ESE database. + +The WebCache database (WebCacheV01.dat or WebCacheV24.dat) are used by MSIE +as of version 10. +""" + +import logging + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.parsers import esedb +from plaso.parsers.esedb_plugins import interface + + +class MsieWebCacheContainersEventObject(time_events.FiletimeEvent): + """Convenience class for a MSIE WebCache Containers table event.""" + + DATA_TYPE = 'msie:webcache:containers' + + def __init__(self, timestamp, usage, record_values): + """Initializes the event. + + Args: + timestamp: The FILETIME timestamp value. + usage: The usage string, describing the timestamp value. + record_values: A dict object containing the record values. + """ + super(MsieWebCacheContainersEventObject, self).__init__(timestamp, usage) + + self.container_identifier = record_values.get('ContainerId', 0) + self.set_identifier = record_values.get('SetId', 0) + self.name = record_values.get('Name', u'') + self.directory = record_values.get('Directory', u'') + + +class MsieWebCacheContainerEventObject(time_events.FiletimeEvent): + """Convenience class for a MSIE WebCache Container table event.""" + + DATA_TYPE = 'msie:webcache:container' + + def __init__(self, timestamp, usage, record_values): + """Initializes the event. + + Args: + timestamp: The FILETIME timestamp value. + usage: The usage string, describing the timestamp value. + record_values: A dict object containing the record values. + """ + super(MsieWebCacheContainerEventObject, self).__init__(timestamp, usage) + + self.entry_identifier = record_values.get(u'EntryId', 0) + self.container_identifier = record_values.get(u'ContainerId', 0) + self.cache_identifier = record_values.get(u'CacheId', 0) + + url = record_values.get(u'Url', u'') + # Ignore URL that start with a binary value. + if ord(url[0]) >= 0x20: + self.url = url + self.redirect_url = record_values.get(u'RedirectUrl', u'') + + self.access_count = record_values.get(u'AccessCount', 0) + self.sync_count = record_values.get(u'SyncCount', 0) + + self.cached_filename = record_values.get('Filename', u'') + self.file_extension = record_values.get(u'FileExtension', u'') + self.cached_file_size = record_values.get(u'FileSize', 0) + + # Ignore non-Unicode request headers values. + request_headers = record_values.get(u'RequestHeaders', u'') + if type(request_headers) == unicode and request_headers: + self.request_headers = request_headers + + # Ignore non-Unicode response headers values. + response_headers = record_values.get(u'ResponseHeaders', u'') + if type(response_headers) == unicode and response_headers: + self.response_headers = response_headers + + +class MsieWebCacheLeakFilesEventObject(time_events.FiletimeEvent): + """Convenience class for a MSIE WebCache LeakFiles table event.""" + + DATA_TYPE = 'msie:webcache:leak_file' + + def __init__(self, timestamp, usage, record_values): + """Initializes the event. + + Args: + timestamp: The FILETIME timestamp value. + usage: The usage string, describing the timestamp value. + record_values: A dict object containing the record values. + """ + super(MsieWebCacheLeakFilesEventObject, self).__init__(timestamp, usage) + + self.leak_identifier = record_values.get('LeakId', 0) + self.cached_filename = record_values.get('Filename', u'') + + +class MsieWebCachePartitionsEventObject(time_events.FiletimeEvent): + """Convenience class for a MSIE WebCache Partitions table event.""" + + DATA_TYPE = 'msie:webcache:partitions' + + def __init__(self, timestamp, usage, record_values): + """Initializes the event. + + Args: + timestamp: The FILETIME timestamp value. + usage: The usage string, describing the timestamp value. + record_values: A dict object containing the record values. + """ + super(MsieWebCachePartitionsEventObject, self).__init__(timestamp, usage) + + self.partition_identifier = record_values.get('PartitionId', 0) + self.partition_type = record_values.get('PartitionType', 0) + self.directory = record_values.get('Directory', u'') + self.table_identifier = record_values.get('TableId', 0) + + +class MsieWebCacheEseDbPlugin(interface.EseDbPlugin): + """Parses a MSIE WebCache ESE database file.""" + + NAME = 'msie_webcache' + DESCRIPTION = u'Parser for MSIE WebCache ESE database files.' + + # TODO: add support for AppCache_#, AppCacheEntry_#, DependencyEntry_# + + REQUIRED_TABLES = { + 'Containers': 'ParseContainersTable', + 'LeakFiles': 'ParseLeakFilesTable', + 'Partitions': 'ParsePartitionsTable'} + + _CONTAINER_TABLE_VALUE_MAPPINGS = { + 'RequestHeaders': '_ConvertValueBinaryDataToStringAscii', + 'ResponseHeaders': '_ConvertValueBinaryDataToStringAscii'} + + def _ParseContainerTable( + self, parser_context, file_entry=None, parser_chain=None, table=None, + container_name=u'Unknown'): + """Parses a Container_# table. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. + table: Optional table object (instance of pyesedb.table). + container_name: Optional string that contains the container name. + The container name indicates the table type. + The default is a string containing 'Unknown'. + """ + if table is None: + logging.warning(u'[{0:s}] invalid Container_# table'.format(self.NAME)) + return + + for esedb_record in table.records: + # TODO: add support for: + # wpnidm, iecompat, iecompatua, DNTException, DOMStore + if container_name == u'Content': + value_mappings = self._CONTAINER_TABLE_VALUE_MAPPINGS + else: + value_mappings = None + + try: + record_values = self._GetRecordValues( + table.name, esedb_record, value_mappings=value_mappings) + except UnicodeDecodeError as exception: + logging.error(( + u'[{0:s}] Unable to return record values for {1:s} with error: ' + u'{2:s}').format( + parser_chain, + file_entry.path_spec.comparable.replace(u'\n', u';'), + exception)) + continue + + if (container_name in [ + u'Content', u'Cookies', u'History', u'iedownload'] or + container_name.startswith(u'MSHist')): + timestamp = record_values.get(u'SyncTime', 0) + if timestamp: + event_object = MsieWebCacheContainerEventObject( + timestamp, u'Synchronization time', record_values) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + timestamp = record_values.get(u'CreationTime', 0) + if timestamp: + event_object = MsieWebCacheContainerEventObject( + timestamp, eventdata.EventTimestamp.CREATION_TIME, record_values) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + timestamp = record_values.get(u'ExpiryTime', 0) + if timestamp: + event_object = MsieWebCacheContainerEventObject( + timestamp, eventdata.EventTimestamp.EXPIRATION_TIME, + record_values) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + timestamp = record_values.get(u'ModifiedTime', 0) + if timestamp: + event_object = MsieWebCacheContainerEventObject( + timestamp, eventdata.EventTimestamp.MODIFICATION_TIME, + record_values) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + timestamp = record_values.get(u'AccessedTime', 0) + if timestamp: + event_object = MsieWebCacheContainerEventObject( + timestamp, eventdata.EventTimestamp.ACCESS_TIME, record_values) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + timestamp = record_values.get(u'PostCheckTime', 0) + if timestamp: + event_object = MsieWebCacheContainerEventObject( + timestamp, u'Post check time', record_values) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + def ParseContainersTable( + self, parser_context, file_entry=None, parser_chain=None, database=None, + table=None, **unused_kwargs): + """Parses the Containers table. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + database: Optional database object (instance of pyesedb.file). + The default is None. + table: Optional table object (instance of pyesedb.table). + The default is None. + """ + if database is None: + logging.warning(u'[{0:s}] invalid database'.format(self.NAME)) + return + + if table is None: + logging.warning(u'[{0:s}] invalid Containers table'.format(self.NAME)) + return + + for esedb_record in table.records: + record_values = self._GetRecordValues(table.name, esedb_record) + + timestamp = record_values.get(u'LastScavengeTime', 0) + if timestamp: + event_object = MsieWebCacheContainersEventObject( + timestamp, u'Last Scavenge Time', record_values) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + timestamp = record_values.get(u'LastAccessTime', 0) + if timestamp: + event_object = MsieWebCacheContainersEventObject( + timestamp, eventdata.EventTimestamp.ACCESS_TIME, record_values) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + container_identifier = record_values.get(u'ContainerId', None) + container_name = record_values.get(u'Name', None) + + if not container_identifier or not container_name: + continue + + table_name = u'Container_{0:d}'.format(container_identifier) + esedb_table = database.get_table_by_name(table_name) + if not esedb_table: + logging.warning( + u'[{0:s}] missing table: {1:s}'.format(self.NAME, table_name)) + continue + + self._ParseContainerTable( + parser_context, file_entry=file_entry, parser_chain=parser_chain, + table=esedb_table, container_name=container_name) + + def ParseLeakFilesTable( + self, parser_context, file_entry=None, parser_chain=None, database=None, + table=None, **unused_kwargs): + """Parses the LeakFiles table. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + database: Optional database object (instance of pyesedb.file). + The default is None. + table: Optional table object (instance of pyesedb.table). + The default is None. + """ + if database is None: + logging.warning(u'[{0:s}] invalid database'.format(self.NAME)) + return + + if table is None: + logging.warning(u'[{0:s}] invalid LeakFiles table'.format(self.NAME)) + return + + for esedb_record in table.records: + record_values = self._GetRecordValues(table.name, esedb_record) + + timestamp = record_values.get(u'CreationTime', 0) + if timestamp: + event_object = MsieWebCacheLeakFilesEventObject( + timestamp, eventdata.EventTimestamp.CREATION_TIME, record_values) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + def ParsePartitionsTable( + self, parser_context, file_entry=None, parser_chain=None, database=None, + table=None, **unused_kwargs): + """Parses the Partitions table. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + database: Optional database object (instance of pyesedb.file). + The default is None. + table: Optional table object (instance of pyesedb.table). + The default is None. + """ + if database is None: + logging.warning(u'[{0:s}] invalid database'.format(self.NAME)) + return + + if table is None: + logging.warning(u'[{0:s}] invalid Partitions table'.format(self.NAME)) + return + + for esedb_record in table.records: + record_values = self._GetRecordValues(table.name, esedb_record) + + timestamp = record_values.get(u'LastScavengeTime', 0) + if timestamp: + event_object = MsieWebCachePartitionsEventObject( + timestamp, u'Last Scavenge Time', record_values) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +esedb.EseDbParser.RegisterPlugin(MsieWebCacheEseDbPlugin) diff --git a/plaso/parsers/esedb_plugins/msie_webcache_test.py b/plaso/parsers/esedb_plugins/msie_webcache_test.py new file mode 100644 index 0000000..5535776 --- /dev/null +++ b/plaso/parsers/esedb_plugins/msie_webcache_test.py @@ -0,0 +1,71 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Microsoft Internet Explorer WebCache database.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import msie_webcache as msie_webcache_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers.esedb_plugins import msie_webcache +from plaso.parsers.esedb_plugins import test_lib + + +class MsieWebCacheEseDbPluginTest(test_lib.EseDbPluginTestCase): + """Tests for the MSIE WebCache ESE database plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = msie_webcache.MsieWebCacheEseDbPlugin() + + def testProcess(self): + """Tests the Process function.""" + test_file = self._GetTestFilePath(['WebCacheV01.dat']) + event_queue_consumer = self._ParseEseDbFileWithPlugin( + test_file, self._plugin) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 1354) + + event_object = event_objects[0] + + self.assertEquals(event_object.container_identifier, 1) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2014-05-12 07:30:25.486198') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.ACCESS_TIME) + + expected_msg = ( + u'Container identifier: 1 ' + u'Set identifier: 0 ' + u'Name: Content ' + u'Directory: C:\\Users\\test\\AppData\\Local\\Microsoft\\Windows\\' + u'INetCache\\IE\\ ' + u'Table: Container_1') + expected_msg_short = ( + u'Directory: C:\\Users\\test\\AppData\\Local\\Microsoft\\Windows\\' + u'INetCache\\IE\\') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/esedb_plugins/test_lib.py b/plaso/parsers/esedb_plugins/test_lib.py new file mode 100644 index 0000000..2924c07 --- /dev/null +++ b/plaso/parsers/esedb_plugins/test_lib.py @@ -0,0 +1,76 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""ESEDB plugin related functions and classes for testing.""" + +import pyesedb + +from dfvfs.lib import definitions +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver import resolver as path_spec_resolver + +from plaso.engine import single_process +from plaso.parsers import test_lib + + +class EseDbPluginTestCase(test_lib.ParserTestCase): + """The unit test case for ESE database based plugins.""" + + def _OpenEseDbFile(self, path): + """Opens an ESE database file and returns back a pyesedb.file object. + + Args: + path: The path to the ESE database test file. + """ + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=path) + file_entry = path_spec_resolver.Resolver.OpenFileEntry(path_spec) + + file_object = file_entry.GetFileObject() + esedb_file = pyesedb.file() + + esedb_file.open_file_object(file_object) + + return esedb_file + + def _ParseEseDbFileWithPlugin( + self, path, plugin_object, knowledge_base_values=None): + """Parses a file as an ESE database file and returns an event generator. + + Args: + path: The path to the ESE database test file. + plugin_object: The plugin object that is used to extract an event + generator. + knowledge_base_values: optional dict containing the knowledge base + values. The default is None. + + Returns: + An event object queue consumer object (instance of + TestEventObjectQueueConsumer). + """ + event_queue = single_process.SingleProcessQueue() + event_queue_consumer = test_lib.TestEventObjectQueueConsumer(event_queue) + + parse_error_queue = single_process.SingleProcessQueue() + + parser_context = self._GetParserContext( + event_queue, parse_error_queue, + knowledge_base_values=knowledge_base_values) + esedb_file = self._OpenEseDbFile(path) + plugin_object.Process(parser_context, database=esedb_file) + + return event_queue_consumer diff --git a/plaso/parsers/filestat.py b/plaso/parsers/filestat.py new file mode 100644 index 0000000..7cfa61a --- /dev/null +++ b/plaso/parsers/filestat.py @@ -0,0 +1,129 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""File system stat object parser.""" + +from dfvfs.lib import definitions as dfvfs_definitions + +from plaso.events import time_events +from plaso.lib import timelib +from plaso.parsers import interface +from plaso.parsers import manager + + +class FileStatEvent(time_events.TimestampEvent): + """File system stat event.""" + + DATA_TYPE = 'fs:stat' + + def __init__(self, timestamp, usage, allocated, size, fs_type): + """Initializes the event. + + Args: + timestamp: The timestamp value. + usage: The usage string describing the timestamp. + allocated: Boolean value to indicate the file entry is allocated. + size: The file size in bytes. + fs_type: The filesystem this timestamp is extracted from. + """ + super(FileStatEvent, self).__init__(timestamp, usage) + + self.offset = 0 + self.size = size + self.allocated = allocated + self.fs_type = fs_type + + +class FileStatParser(interface.BaseParser): + """Class that defines a file system stat object parser.""" + + NAME = 'filestat' + DESCRIPTION = u'Parser for file system stat information.' + + _TIME_ATTRIBUTES = frozenset([ + 'atime', 'bkup_time', 'ctime', 'crtime', 'dtime', 'mtime']) + + def _GetFileSystemTypeFromFileEntry(self, file_entry): + """Return a filesystem type string from a file entry object. + + Args: + file_entry: A file entry object (instance of vfs.file_entry.FileEntry). + + Returns: + A string indicating the file system type. + """ + file_system = file_entry.GetFileSystem() + type_indicator = file_system.type_indicator + + if type_indicator != dfvfs_definitions.TYPE_INDICATOR_TSK: + return type_indicator + + # TODO: Implement fs_type in dfVFS and remove this implementation + # once that is in place. + fs_info = file_system.GetFsInfo() + if fs_info.info: + type_string = unicode(fs_info.info.ftype) + if type_string.startswith('TSK_FS_TYPE'): + return type_string[12:] + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extracts event objects from a file system stat entry. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + stat_object = file_entry.GetStat() + if not stat_object: + return + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + file_system_type = self._GetFileSystemTypeFromFileEntry(file_entry) + + is_allocated = getattr(stat_object, 'allocated', True) + file_size = getattr(stat_object, 'size', None), + + for time_attribute in self._TIME_ATTRIBUTES: + timestamp = getattr(stat_object, time_attribute, None) + if timestamp is None: + continue + + nano_time_attribute = u'{0:s}_nano'.format(time_attribute) + nano_time_attribute = getattr(stat_object, nano_time_attribute, None) + + timestamp = timelib.Timestamp.FromPosixTime(timestamp) + if nano_time_attribute is not None: + # Note that the _nano values are in intervals of 100th nano seconds. + timestamp += nano_time_attribute / 10 + + # TODO: this also ignores any timestamp that equals 0. + # Is this the desired behavior? + if not timestamp: + continue + + event_object = FileStatEvent( + timestamp, time_attribute, is_allocated, file_size, file_system_type) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +manager.ParsersManager.RegisterParser(FileStatParser) diff --git a/plaso/parsers/filestat_test.py b/plaso/parsers/filestat_test.py new file mode 100644 index 0000000..6e42ce3 --- /dev/null +++ b/plaso/parsers/filestat_test.py @@ -0,0 +1,153 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for filestat parser.""" + +import unittest + +from dfvfs.lib import definitions +from dfvfs.path import factory as path_spec_factory + +# pylint: disable=unused-import +from plaso.formatters import filestat as filestat_formatter +from plaso.parsers import filestat +from plaso.parsers import test_lib + + +class FileStatTest(test_lib.ParserTestCase): + """Tests for filestat parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = filestat.FileStatParser() + + def testTSKFile(self): + """Read a file within an image file and make few tests.""" + test_file = self._GetTestFilePath([u'ímynd.dd']) + os_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=test_file) + tsk_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_TSK, inode=15, location=u'/passwords.txt', + parent=os_path_spec) + + event_queue_consumer = self._ParseFileByPathSpec( + self._parser, tsk_path_spec) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # The TSK file entry has 3 event objects. + self.assertEquals(len(event_objects), 3) + + def testZipFile(self): + """Test a ZIP file.""" + test_file = self._GetTestFilePath([u'syslog.zip']) + os_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=test_file) + zip_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_ZIP, location=u'/syslog', + parent=os_path_spec) + + event_queue_consumer = self._ParseFileByPathSpec( + self._parser, zip_path_spec) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # The ZIP file has 1 event object. + self.assertEquals(len(event_objects), 1) + + def testGzipFile(self): + """Test a GZIP file.""" + test_file = self._GetTestFilePath([u'syslog.gz']) + os_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=test_file) + gzip_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_GZIP, parent=os_path_spec) + + event_queue_consumer = self._ParseFileByPathSpec( + self._parser, gzip_path_spec) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # The gzip file has 1 event object. + self.assertEquals(len(event_objects), 1) + + def testTarFile(self): + """Test a TAR file.""" + test_file = self._GetTestFilePath([u'syslog.tar']) + os_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=test_file) + tar_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_TAR, location=u'/syslog', + parent=os_path_spec) + + event_queue_consumer = self._ParseFileByPathSpec( + self._parser, tar_path_spec) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # The tar file has 1 event object. + self.assertEquals(len(event_objects), 1) + + def testNestedFile(self): + """Test a nested file.""" + test_file = self._GetTestFilePath([u'syslog.tgz']) + os_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=test_file) + gzip_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_GZIP, parent=os_path_spec) + tar_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_TAR, location=u'/syslog', + parent=gzip_path_spec) + + event_queue_consumer = self._ParseFileByPathSpec( + self._parser, tar_path_spec) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # The tar file has 1 event object. + self.assertEquals(len(event_objects), 1) + + test_file = self._GetTestFilePath([u'syslog.tgz']) + os_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=test_file) + gzip_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_GZIP, parent=os_path_spec) + + event_queue_consumer = self._ParseFileByPathSpec( + self._parser, gzip_path_spec) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # The gzip file has 1 event object. + self.assertEquals(len(event_objects), 1) + + def testNestedTSK(self): + """Test a nested TSK file.""" + test_file = self._GetTestFilePath([u'syslog_image.dd']) + os_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=test_file) + tsk_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_TSK, inode=11, location=u'/logs/hidden.zip', + parent=os_path_spec) + zip_path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_ZIP, location=u'/syslog', + parent=tsk_path_spec) + + event_queue_consumer = self._ParseFileByPathSpec( + self._parser, zip_path_spec) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # The ZIP file has 1 event objects. + self.assertEquals(len(event_objects), 1) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/firefox_cache.py b/plaso/parsers/firefox_cache.py new file mode 100644 index 0000000..e8edb39 --- /dev/null +++ b/plaso/parsers/firefox_cache.py @@ -0,0 +1,246 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Implements a parser for Firefox cache files.""" + +import collections +import logging +import os + +import construct +import pyparsing + +from plaso.events import time_events +from plaso.lib import errors +from plaso.lib import eventdata +from plaso.parsers import interface +from plaso.parsers import manager + + +__author__ = 'Petter Bjelland (petter.bjelland@gmail.com)' + + +class FirefoxCacheEvent(time_events.PosixTimeEvent): + """Convenience class for a Firefox cache record event.""" + + DATA_TYPE = 'firefox:cache:record' + + def __init__(self, metadata, request_method, url, response_code): + super(FirefoxCacheEvent, self).__init__( + metadata.last_fetched, eventdata.EventTimestamp.ADDED_TIME) + + self.last_modified = metadata.last_modified + self.major = metadata.major + self.minor = metadata.minor + self.location = metadata.location + self.last_fetched = metadata.last_fetched + self.expire_time = metadata.expire_time + self.fetch_count = metadata.fetch_count + self.request_size = metadata.request_size + self.info_size = metadata.info_size + self.data_size = metadata.data_size + self.request_method = request_method + self.url = url + self.response_code = response_code + + +class FirefoxCacheParser(interface.BaseParser): + """Extract cached records from Firefox.""" + + NAME = 'firefox_cache' + DESCRIPTION = u'Parser for Firefox Cache files.' + + # Number of bytes allocated to a cache record metadata. + RECORD_HEADER_SIZE = 36 + + # Initial size of Firefox >= 4 cache files. + INITIAL_CACHE_FILE_SIZE = 1024 * 1024 * 4 + + # Smallest possible block size in Firefox cache files. + MIN_BLOCK_SIZE = 256 + + RECORD_HEADER_STRUCT = construct.Struct( + 'record_header', + construct.UBInt16('major'), + construct.UBInt16('minor'), + construct.UBInt32('location'), + construct.UBInt32('fetch_count'), + construct.UBInt32('last_fetched'), + construct.UBInt32('last_modified'), + construct.UBInt32('expire_time'), + construct.UBInt32('data_size'), + construct.UBInt32('request_size'), + construct.UBInt32('info_size')) + + ALTERNATIVE_CACHE_NAME = ( + pyparsing.Word(pyparsing.hexnums, exact=5) + pyparsing.Word('m', exact=1) + + pyparsing.Word(pyparsing.nums, exact=2)) + + FIREFOX_CACHE_CONFIG = collections.namedtuple( + u'firefox_cache_config', + u'block_size first_record_offset') + + REQUEST_METHODS = [ + u'GET', 'HEAD', 'POST', 'PUT', 'DELETE', + u'TRACE', 'OPTIONS', 'CONNECT', 'PATCH'] + + def _GetFirefoxConfig(self, file_entry): + """Determine cache file block size. Raises exception if not found.""" + + if file_entry.name[0:9] != '_CACHE_00': + try: + # Match alternative filename. Five hex characters + 'm' + two digit + # number, e.g. '01ABCm02'. 'm' is for metadata. Cache files with 'd' + # instead contain data only. + self.ALTERNATIVE_CACHE_NAME.parseString(file_entry.name) + except pyparsing.ParseException: + raise errors.UnableToParseFile(u'Not a Firefox cache file.') + + file_object = file_entry.GetFileObject() + + # There ought to be a valid record within the first 4MB. We use this + # limit to prevent reading large invalid files. + to_read = min(file_object.get_size(), self.INITIAL_CACHE_FILE_SIZE) + + while file_object.get_offset() < to_read: + offset = file_object.get_offset() + + try: + # We have not yet determined the block size, so we use the smallest + # possible size. + record = self.__NextRecord( + file_entry.name, file_object, self.MIN_BLOCK_SIZE) + + record_size = ( + self.RECORD_HEADER_SIZE + record.request_size + record.info_size) + + if record_size >= 4096: + # _CACHE_003_ + block_size = 4096 + elif record_size >= 1024: + # _CACHE_002_ + block_size = 1024 + else: + # _CACHE_001_ + block_size = 256 + + return self.FIREFOX_CACHE_CONFIG(block_size, offset) + + except IOError: + logging.debug(u'[{0:s}] {1:s}:{2:d}: Invalid record.'.format( + self.NAME, file_entry.name, offset)) + + raise errors.UnableToParseFile( + u'Could not find a valid cache record. ' + u'Not a Firefox cache file.') + + def __Accept(self, candidate, block_size): + """Determine whether the candidate is a valid cache record.""" + + record_size = ( + self.RECORD_HEADER_SIZE + candidate.request_size+ candidate.info_size) + + return ( + candidate.request_size > 0 and candidate.fetch_count > 0 and + candidate.major == 1 and record_size // block_size < 256) + + def __NextRecord(self, filename, file_object, block_size): + """Provide the next cache record.""" + + offset = file_object.get_offset() + + try: + candidate = self.RECORD_HEADER_STRUCT.parse_stream(file_object) + except (IOError, construct.FieldError): + raise IOError(u'Unable to parse stream.') + + if not self.__Accept(candidate, block_size): + # Move reader to next candidate block. + file_object.seek(block_size - self.RECORD_HEADER_SIZE, os.SEEK_CUR) + raise IOError(u'Not a valid Firefox cache record.') + + # The last byte in a request is null. + url = file_object.read(candidate.request_size)[:-1] + + # HTTP response header, even elements are keys, odd elements values. + headers = file_object.read(candidate.info_size) + + request_method, _, _ = ( + headers.partition('request-method\x00')[2].partition('\x00')) + + _, _, response_head = headers.partition('response-head\x00') + + response_code, _, _ = response_head.partition('\r\n') + + if request_method not in self.REQUEST_METHODS: + safe_headers = headers.decode('ascii', errors='replace') + logging.debug(( + u'[{0:s}] {1:s}:{2:d}: Unknown HTTP method \'{3:s}\'. Response ' + u'headers: \'{4:s}\'').format( + self.NAME, filename, offset, request_method, safe_headers)) + + if response_code[0:4] != 'HTTP': + safe_headers = headers.decode('ascii', errors='replace') + logging.debug(( + u'[{0:s}] {1:s}:{2:d}: Could not determine HTTP response code. ' + u'Response headers: \'{3:s}\'.').format( + self.NAME, filename, offset, safe_headers)) + + # A request can span multiple blocks, so we use modulo. + _, remainder = divmod(file_object.get_offset() - offset, block_size) + + # Move reader to next candidate block. Include the null-byte skipped above. + file_object.seek(block_size - remainder, os.SEEK_CUR) + + return FirefoxCacheEvent(candidate, request_method, url, response_code) + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract records from a Firefox cache file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + firefox_config = self._GetFirefoxConfig(file_entry) + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + file_object = file_entry.GetFileObject() + + file_object.seek(firefox_config.first_record_offset) + + while file_object.get_offset() < file_object.get_size(): + try: + event_object = self.__NextRecord( + file_entry.name, file_object, firefox_config.block_size) + + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + except IOError: + logging.debug(u'[{0:s}] {1:s}:{2:d}: Invalid cache record.'.format( + self.NAME, file_entry.name, + file_object.get_offset() - self.MIN_BLOCK_SIZE)) + + file_object.close() + + +manager.ParsersManager.RegisterParser(FirefoxCacheParser) diff --git a/plaso/parsers/firefox_cache_test.py b/plaso/parsers/firefox_cache_test.py new file mode 100644 index 0000000..603bb0a --- /dev/null +++ b/plaso/parsers/firefox_cache_test.py @@ -0,0 +1,188 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for Firefox cache files parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import firefox_cache as firefox_cache_formatter +from plaso.lib import errors +from plaso.lib import timelib_test +from plaso.parsers import firefox_cache +from plaso.parsers import test_lib + + +__author__ = 'Petter Bjelland (petter.bjelland@gmail.com)' + + +class FirefoxCacheTest(test_lib.ParserTestCase): + """A unit test for the FirefoxCacheParser.""" + + def setUp(self): + self._parser = firefox_cache.FirefoxCacheParser() + + def VerifyMajorMinor(self, events): + """Verify that valid Firefox cahce version is extracted.""" + for event_object in events: + self.assertEquals(event_object.major, 1) + self.assertEquals(event_object.minor, 19) + + def testParseCache_InvalidFile(self): + """Verify that parser do not accept small, invalid files.""" + + test_file = self._GetTestFilePath(['firefox_cache', 'invalid_file']) + + with self.assertRaises(errors.UnableToParseFile): + _ = self._ParseFile(self._parser, test_file) + + def testParseCache_001(self): + """Test Firefox 28 cache file _CACHE_001_ parsing.""" + + test_file = self._GetTestFilePath( + ['firefox_cache', 'firefox28', '_CACHE_001_']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(574, len(event_objects)) + self.assertEquals( + event_objects[1].url, 'HTTP:http://start.ubuntu.com/12.04/sprite.png') + + self.assertEquals(event_objects[1].timestamp, + timelib_test.CopyStringToTimestamp('2014-04-21 14:13:35')) + + self.VerifyMajorMinor(event_objects) + + expected_msg = ( + u'Fetched 2 time(s) ' + u'[HTTP/1.0 200 OK] GET ' + u'"HTTP:http://start.ubuntu.com/12.04/sprite.png"') + expected_msg_short = ( + u'[HTTP/1.0 200 OK] GET ' + u'"HTTP:http://start.ubuntu.com/12.04/sprite.png"') + + self._TestGetMessageStrings( + event_objects[1], expected_msg, expected_msg_short) + + def testParseCache_002(self): + """Test Firefox 28 cache file _CACHE_002_ parsing.""" + + test_file = self._GetTestFilePath( + ['firefox_cache', 'firefox28', '_CACHE_002_']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(58, len(event_objects)) + self.assertEquals( + event_objects[2].url, + ('HTTP:http://www.google-analytics.com/__utm.gif?utmwv=5.5.0&utms=' + '1&utmn=1106893631&utmhn=www.dagbladet.no&utmcs=windows-1252&ut' + 'msr=1920x1080&utmvp=1430x669&utmsc=24-bit&utmul=en-us&utmje=0&' + 'utmfl=-&utmdt=Dagbladet.no%20-%20forsiden&utmhid=460894302&utm' + 'r=-&utmp=%2F&utmht=1398089458997&utmac=UA-3072159-1&utmcc=__ut' + 'ma%3D68537988.718312608.1398089459.1398089459.1398089459.1%3B%' + '2B__utmz%3D68537988.1398089459.1.1.utmcsr%3D(direct)%7Cutmccn' + '%3D(direct)%7Cutmcmd%3D(none)%3B&aip=1&utmu=qBQ~')) + + self.assertEquals(event_objects[1].timestamp, + timelib_test.CopyStringToTimestamp('2014-04-21 14:10:58')) + + self.VerifyMajorMinor(event_objects) + + def testParseCache_003(self): + """Test Firefox 28 cache file _CACHE_003_ parsing.""" + + test_file = self._GetTestFilePath( + ['firefox_cache', 'firefox28', '_CACHE_003_']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(4, len(event_objects)) + + self.assertEquals( + event_objects[3].url, + 'HTTP:https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js') + + self.assertEquals( + event_objects[3].timestamp, + timelib_test.CopyStringToTimestamp('2014-04-21 14:11:07')) + + self.VerifyMajorMinor(event_objects) + + def testParseAlternativeFilename(self): + """Test Firefox 28 cache 003 file with alternative filename.""" + + test_file = self._GetTestFilePath( + ['firefox_cache', 'firefox28', 'E8D65m01']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(4, len(event_objects)) + + def testParseLegacyCache_001(self): + """Test Firefox 3 cache file _CACHE_001_ parsing.""" + + test_file = self._GetTestFilePath( + ['firefox_cache', 'firefox3', '_CACHE_001_']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(25, len(event_objects)) + + self.assertEquals(event_objects[0].timestamp, + timelib_test.CopyStringToTimestamp('2014-05-02 14:15:03')) + + expected_msg = ( + u'Fetched 1 time(s) ' + u'[HTTP/1.1 200 OK] GET ' + u'"HTTP:http://start.mozilla.org/en-US/"') + expected_msg_short = ( + u'[HTTP/1.1 200 OK] GET ' + u'"HTTP:http://start.mozilla.org/en-US/"') + + self._TestGetMessageStrings( + event_objects[0], expected_msg, expected_msg_short) + + def testParseLegacyCache_002(self): + """Test Firefox 3 cache file _CACHE_002_ parsing.""" + + test_file = self._GetTestFilePath( + ['firefox_cache', 'firefox3', '_CACHE_002_']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(3, len(event_objects)) + + self.assertEquals(event_objects[1].timestamp, + timelib_test.CopyStringToTimestamp('2014-05-02 14:25:55')) + + def testParseLegacyCache_003(self): + """Test Firefox 3 cache file _CACHE_003_ parsing.""" + + test_file = self._GetTestFilePath( + ['firefox_cache', 'firefox3', '_CACHE_003_']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(2, len(event_objects)) + + self.assertEquals(event_objects[1].timestamp, + timelib_test.CopyStringToTimestamp('2014-05-02 14:15:07')) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/hachoir.py b/plaso/parsers/hachoir.py new file mode 100644 index 0000000..e58e4bf --- /dev/null +++ b/plaso/parsers/hachoir.py @@ -0,0 +1,172 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a parser for extracting metadata.""" +# TODO: Add a unit test for this parser. + +import datetime + +import hachoir_core.config + +# This is necessary to do PRIOR to loading up other parts of hachoir +# framework, otherwise console does not work and other "weird" behavior +# is observed. +hachoir_core.config.unicode_stdout = False +hachoir_core.config.quiet = True + +import hachoir_core +import hachoir_parser +import hachoir_metadata + +from plaso.events import time_events +from plaso.lib import errors +from plaso.lib import timelib +from plaso.parsers import interface +from plaso.parsers import manager + + +__author__ = 'David Nides (david.nides@gmail.com)' + + +class HachoirEvent(time_events.TimestampEvent): + """Process timestamps from Hachoir Events.""" + + DATA_TYPE = 'metadata:hachoir' + + def __init__(self, dt_timestamp, usage, attributes): + """An EventObject created from a Hachoir entry. + + Args: + dt_timestamp: A python datetime.datetime object. + usage: The description of the usage of the time value. + attributes: A dict containing metadata for the event. + """ + timestamp = timelib.Timestamp.FromPythonDatetime(dt_timestamp) + super(HachoirEvent, self).__init__(timestamp, usage, self.DATA_TYPE) + self.metadata = attributes + + +class HachoirParser(interface.BaseParser): + """Parse meta data from files.""" + + NAME = 'hachoir' + DESCRIPTION = u'Parser that wraps Hachoir.' + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract data from a file using Hachoir. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + file_object = file_entry.GetFileObject() + + try: + fstream = hachoir_core.stream.InputIOStream(file_object, None, tags=[]) + except hachoir_core.error.HachoirError as exception: + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse file {1:s}: {2:s}'.format( + self.NAME, file_entry.name, exception)) + + if not fstream: + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse file {1:s}: {2:s}'.format( + self.NAME, file_entry.name, 'Not fstream')) + + try: + doc_parser = hachoir_parser.guessParser(fstream) + except hachoir_core.error.HachoirError as exception: + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse file {1:s}: {2:s}'.format( + self.NAME, file_entry.name, exception)) + + if not doc_parser: + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse file {1:s}: {2:s}'.format( + self.NAME, file_entry.name, 'Not parser')) + + try: + metadata = hachoir_metadata.extractMetadata(doc_parser) + except (AssertionError, AttributeError) as exception: + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse file {1:s}: {2:s}'.format( + self.NAME, file_entry.name, exception)) + + try: + metatext = metadata.exportPlaintext(human=False) + except AttributeError as exception: + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse file {1:s}: {2:s}'.format( + self.NAME, file_entry.name, exception)) + + if not metatext: + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse file {1:s}: No metadata'.format( + self.NAME, file_entry.name)) + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + attributes = {} + extracted_events = [] + for meta in metatext: + if not meta.startswith('-'): + continue + + if len(meta) < 3: + continue + + key, _, value = meta[2:].partition(': ') + + key2, _, value2 = value.partition(': ') + if key2 == 'LastPrinted' and value2 != 'False': + date_object = timelib.StringToDatetime( + value2, timezone=parser_context.timezone) + if isinstance(date_object, datetime.datetime): + extracted_events.append((date_object, key2)) + + try: + date = metadata.get(key) + if isinstance(date, datetime.datetime): + extracted_events.append((date, key)) + except ValueError: + pass + + if key in attributes: + if isinstance(attributes.get(key), list): + attributes[key].append(value) + else: + old_value = attributes.get(key) + attributes[key] = [old_value, value] + else: + attributes[key] = value + + if not extracted_events: + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse file {1:s}: {2:s}'.format( + self.NAME, file_entry.name, 'No events discovered')) + + for date, key in extracted_events: + event_object = HachoirEvent(date, key, attributes) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +manager.ParsersManager.RegisterParser(HachoirParser) diff --git a/plaso/parsers/iis.py b/plaso/parsers/iis.py new file mode 100644 index 0000000..80ac8d8 --- /dev/null +++ b/plaso/parsers/iis.py @@ -0,0 +1,234 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for Windows IIS Log file. + +More documentation on fields can be found here: +http://www.microsoft.com/technet/prodtechnol/WindowsServer2003/Library/ +IIS/676400bc-8969-4aa7-851a-9319490a9bbb.mspx?mfr=true + +""" + +import logging + +import pyparsing + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import manager +from plaso.parsers import text_parser + + +__author__ = 'Ashley Holtz (ashley.a.holtz@gmail.com)' + + +class IISEventObject(time_events.TimestampEvent): + """Convenience class to handle the IIS event object.""" + + DATA_TYPE = 'iis:log:line' + + def __init__(self, timestamp, structure): + """Initializes the IIS event object. + + Args: + timestamp: The timestamp time value, epoch. + structure: The structure with any parsed log values to iterate over. + """ + super(IISEventObject, self).__init__( + timestamp, eventdata.EventTimestamp.WRITTEN_TIME) + + for key, value in structure.iteritems(): + if key in ('time', 'date'): + continue + if value == u'-': + continue + if type(value) is pyparsing.ParseResults: + setattr(self, key, u''.join(value)) + else: + try: + save_value = int(value, 10) + except ValueError: + save_value = value + setattr(self, key, save_value) + + +class WinIISParser(text_parser.PyparsingSingleLineTextParser): + """Parses a Microsoft IIS log file.""" + + NAME = 'winiis' + DESCRIPTION = u'Parser for Microsoft IIS log files.' + + # Common Fields (6.0: date time s-sitename s-ip cs-method cs-uri-stem + # cs-uri-query s-port cs-username c-ip cs(User-Agent) sc-status + # sc-substatus sc-win32-status. + # Common Fields (7.5): date time s-ip cs-method cs-uri-stem cs-uri-query + # s-port cs-username c-ip cs(User-Agent) sc-status sc-substatus + # sc-win32-status time-taken + + # Define common structures. + BLANK = pyparsing.Literal(u'-') + WORD = pyparsing.Word(pyparsing.alphanums + u'-') | BLANK + INT = pyparsing.Word(pyparsing.nums, min=1) | BLANK + IP = ( + text_parser.PyparsingConstants.IPV4_ADDRESS | + text_parser.PyparsingConstants.IPV6_ADDRESS | BLANK) + PORT = pyparsing.Word(pyparsing.nums, min=1, max=6) | BLANK + URI = pyparsing.Word(pyparsing.alphanums + u'/.?&+;_=()-:,%') | BLANK + + # Define how a log line should look like for version 6.0. + LOG_LINE_6_0 = ( + text_parser.PyparsingConstants.DATE.setResultsName('date') + + text_parser.PyparsingConstants.TIME.setResultsName('time') + + WORD.setResultsName('s_sitename') + IP.setResultsName('dest_ip') + + WORD.setResultsName('http_method') + URI.setResultsName('cs_uri_stem') + + URI.setResultsName('cs_uri_query') + PORT.setResultsName('dest_port') + + WORD.setResultsName('cs_username') + IP.setResultsName('source_ip') + + URI.setResultsName('user_agent') + INT.setResultsName('sc_status') + + INT.setResultsName('sc_substatus') + + INT.setResultsName('sc_win32_status')) + + _LOG_LINE_STRUCTURES = {} + + # Common fields. Set results name with underscores, not hyphens because regex + # will not pick them up. + _LOG_LINE_STRUCTURES['date'] = ( + text_parser.PyparsingConstants.DATE.setResultsName('date')) + _LOG_LINE_STRUCTURES['time'] = ( + text_parser.PyparsingConstants.TIME.setResultsName('time')) + _LOG_LINE_STRUCTURES['s-sitename'] = WORD.setResultsName('s_sitename') + _LOG_LINE_STRUCTURES['s-ip'] = IP.setResultsName('dest_ip') + _LOG_LINE_STRUCTURES['cs-method'] = WORD.setResultsName('http_method') + _LOG_LINE_STRUCTURES['cs-uri-stem'] = URI.setResultsName('requested_uri_stem') + _LOG_LINE_STRUCTURES['cs-uri-query'] = URI.setResultsName('cs_uri_query') + _LOG_LINE_STRUCTURES['s-port'] = PORT.setResultsName('dest_port') + _LOG_LINE_STRUCTURES['cs-username'] = WORD.setResultsName('cs_username') + _LOG_LINE_STRUCTURES['c-ip'] = IP.setResultsName('source_ip') + _LOG_LINE_STRUCTURES['cs(User-Agent)'] = URI.setResultsName('user_agent') + _LOG_LINE_STRUCTURES['sc-status'] = INT.setResultsName('http_status') + _LOG_LINE_STRUCTURES['sc-substatus'] = INT.setResultsName('sc_substatus') + _LOG_LINE_STRUCTURES['sc-win32-status'] = ( + INT.setResultsName('sc_win32_status')) + + # Less common fields. + _LOG_LINE_STRUCTURES['s-computername'] = URI.setResultsName('s_computername') + _LOG_LINE_STRUCTURES['sc-bytes'] = INT.setResultsName('sent_bytes') + _LOG_LINE_STRUCTURES['cs-bytes'] = INT.setResultsName('received_bytes') + _LOG_LINE_STRUCTURES['time-taken'] = INT.setResultsName('time_taken') + _LOG_LINE_STRUCTURES['cs-version'] = WORD.setResultsName('protocol_version') + _LOG_LINE_STRUCTURES['cs-host'] = WORD.setResultsName('cs_host') + _LOG_LINE_STRUCTURES['cs(Cookie)'] = URI.setResultsName('cs_cookie') + _LOG_LINE_STRUCTURES['cs(Referrer)'] = URI.setResultsName('cs_referrer') + + # Define the available log line structures. Default to the IIS v. 6.0 + # common format. + LINE_STRUCTURES = [ + ('comment', text_parser.PyparsingConstants.COMMENT_LINE_HASH), + ('logline', LOG_LINE_6_0)] + + # Define a signature value for the log file. + SIGNATURE = '#Software: Microsoft Internet Information Services' + + def __init__(self): + """Initializes a parser object.""" + super(WinIISParser, self).__init__() + self.version = None + self.software = None + + def VerifyStructure(self, unused_parser_context, line): + """Verify that this file is an IIS log file. + + Args: + parser_context: A parser context object (instance of ParserContext). + line: A single line from the text file. + + Returns: + True if this is the correct parser, False otherwise. + """ + # TODO: Examine other versions of the file format and if this parser should + # support them. For now just checking if it contains the IIS header. + if self.SIGNATURE in line: + return True + + return False + + def ParseRecord(self, unused_parser_context, key, structure): + """Parse each record structure and return an event object if applicable. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: An identification string indicating the name of the parsed + structure. + structure: A pyparsing.ParseResults object from a line in the + log file. + + Returns: + An event object (instance of EventObject) or None. + """ + if key == 'comment': + self._ParseCommentRecord(structure) + elif key == 'logline': + return self._ParseLogLine(structure) + else: + logging.warning( + u'Unable to parse record, unknown structure: {0:s}'.format(key)) + + def _ParseCommentRecord(self, structure): + """Parse a comment and store appropriate attributes.""" + comment = structure[1] + if comment.startswith(u'Version'): + _, _, self.version = comment.partition(u':') + elif comment.startswith(u'Software'): + _, _, self.software = comment.partition(u':') + elif comment.startswith(u'Date'): + # TODO: fix this date is not used here. + _, _, unused_date = comment.partition(u':') + + # Check if there's a Fields line. If not, LOG_LINE defaults to IIS 6.0 + # common format. + elif comment.startswith(u'Fields'): + log_line = pyparsing.Empty() + for member in comment[7:].split(): + log_line += self._LOG_LINE_STRUCTURES.get(member, self.URI) + # TODO: self._line_structures is a work-around and this needs + # a structural fix. + self._line_structures[1] = ('logline', log_line) + + def _ParseLogLine(self, structure): + """Parse a single log line and return an EventObject.""" + date = structure.get('date', None) + time = structure.get('time', None) + + if not (date and time): + logging.warning(( + u'Unable to extract timestamp from IIS log line with structure: ' + u'{0:s}.').format(structure)) + return + + year, month, day = date + hour, minute, second = time + + timestamp = timelib.Timestamp.FromTimeParts( + year, month, day, hour, minute, second) + + if not timestamp: + return + + return IISEventObject(timestamp, structure) + + +manager.ParsersManager.RegisterParser(WinIISParser) diff --git a/plaso/parsers/iis_test.py b/plaso/parsers/iis_test.py new file mode 100644 index 0000000..1ef1ed3 --- /dev/null +++ b/plaso/parsers/iis_test.py @@ -0,0 +1,96 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Windows IIS log parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import iis as iis_formatter +from plaso.lib import timelib_test +from plaso.parsers import test_lib +from plaso.parsers import iis + + +__author__ = 'Ashley Holtz (ashley.a.holtz@gmail.com)' + + +class WinIISUnitTest(test_lib.ParserTestCase): + """Tests for the Windows IIS parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = iis.WinIISParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['iis.log']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 11) + + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-07-30 00:00:00') + self.assertEquals(event_object.timestamp, expected_timestamp) + + self.assertEquals(event_object.source_ip, u'10.10.10.100') + self.assertEquals(event_object.dest_ip, u'10.10.10.100') + self.assertEquals(event_object.dest_port, 80) + + expected_msg = ( + u'GET /some/image/path/something.jpg ' + u'[ 10.10.10.100 > 10.10.10.100 : 80 ] ' + u'Http Status: 200 ' + u'User Agent: Mozilla/4.0+(compatible;+Win32;' + u'+WinHttp.WinHttpRequest.5)') + expected_msg_short = ( + u'GET /some/image/path/something.jpg ' + u'[ 10.10.10.100 > 10.10.10.100 : 80 ]') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + event_object = event_objects[5] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-07-30 00:00:05') + self.assertEquals(event_object.timestamp, expected_timestamp) + + self.assertEquals(event_object.http_method, 'GET') + self.assertEquals(event_object.http_status, 200) + self.assertEquals( + event_object.requested_uri_stem, u'/some/image/path/something.jpg') + + event_object = event_objects[1] + + expected_msg = ( + u'GET /some/image/path/something.htm ' + u'[ 22.22.22.200 > 10.10.10.100 : 80 ] ' + u'Http Status: 404 ' + u'User Agent: Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_6_8)' + u'+AppleWebKit/534.57.2+(KHTML,+like+Gecko)+Version/5.1.7' + u'+Safari/534.57.2') + expected_msg_short = ( + u'GET /some/image/path/something.htm ' + u'[ 22.22.22.200 > 10.10.10.100 : 80 ]') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/interface.py b/plaso/parsers/interface.py new file mode 100644 index 0000000..e70a5ea --- /dev/null +++ b/plaso/parsers/interface.py @@ -0,0 +1,251 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a class to provide a parsing framework to plaso. + +This class contains a base framework class for parsing fileobjects, and +also some implementations that extend it to provide a more comprehensive +parser. +""" + +import abc + +from plaso.parsers import manager + + +class BaseParser(object): + """Class that implements the parser object interface.""" + + NAME = 'base_parser' + DESCRIPTION = u'' + + def _BuildParserChain(self, parser_chain=None): + """Return the parser chain with the addition of the current parser. + + Args: + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Returns: + The parser chain, with the addition of the current parser. + """ + if not parser_chain: + return self.NAME + + return u'/'.join([parser_chain, self.NAME]) + + @abc.abstractmethod + def Parse(self, parser_context, file_entry, parser_chain=None): + """Parsers the file entry and extracts event objects. + + This is the main function of the class, the one that actually + goes through the log file and parses each line of it to + produce a parsed line and a timestamp. + + It also tries to verify the file structure and see if the class is capable + of parsing the file passed to the module. It will do so with series of tests + that should determine if the file is of the correct structure. + + If the class is not capable of parsing the file passed to it an exception + should be raised, an exception of the type UnableToParseFile that indicates + the reason why the class does not parse it. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Raises: + NotImplementedError when not implemented. + """ + raise NotImplementedError + + @classmethod + def SupportsPlugins(cls): + """Determines if a parser supports plugins. + + Returns: + A boolean value indicating whether the parser supports plugins. + """ + return False + + +class BasePluginsParser(BaseParser): + """Class that implements the parser with plugins object interface.""" + + NAME = 'base_plugin_parser' + DESCRIPTION = u'' + + # Every child class should define its own _plugin_classes dict. + # We don't define it here to make sure the plugins of different + # classes don't end up in the same dict. + # _plugin_classes = {} + _plugin_classes = None + + @classmethod + def DeregisterPlugin(cls, plugin_class): + """Deregisters a plugin class. + + The plugin classes are identified based on their lower case name. + + Args: + plugin_class: the class object of the plugin. + + Raises: + KeyError: if plugin class is not set for the corresponding name. + """ + plugin_name = plugin_class.NAME.lower() + if plugin_name not in cls._plugin_classes: + raise KeyError( + u'Plugin class not set for name: {0:s}.'.format( + plugin_class.NAME)) + + del cls._plugin_classes[plugin_name] + + @classmethod + def GetPluginNames(cls, parser_filter_string=None): + """Retrieves the plugin names. + + Args: + parser_filter_string: Optional parser filter string. The default is None. + + Returns: + A list of plugin names. + """ + plugin_names = [] + + for plugin_name, _ in cls.GetPlugins( + parser_filter_string=parser_filter_string): + plugin_names.append(plugin_name) + + return plugin_names + + @classmethod + def GetPluginObjects(cls, parser_filter_string=None): + """Retrieves the plugin objects. + + Args: + parser_filter_string: Optional parser filter string. The default is None. + + Returns: + A list of plugin objects (instances of BasePlugin). + """ + plugin_objects = [] + + for _, plugin_class in cls.GetPlugins( + parser_filter_string=parser_filter_string): + plugin_object = plugin_class() + plugin_objects.append(plugin_object) + + return plugin_objects + + @classmethod + def GetPlugins(cls, parser_filter_string=None): + """Retrieves the registered plugins. + + Args: + parser_filter_string: Optional parser filter string. The default is None. + + Yields: + A tuple that contains the uniquely identifying name of the plugin + and the plugin class (subclass of BasePlugin). + """ + if parser_filter_string: + includes, excludes = manager.ParsersManager.GetFilterListsFromString( + parser_filter_string) + else: + includes = None + excludes = None + + for plugin_name, plugin_class in cls._plugin_classes.iteritems(): + if excludes and plugin_name in excludes: + continue + + if includes and plugin_name not in includes: + continue + + yield plugin_name, plugin_class + + @abc.abstractmethod + def Parse(self, parser_context, file_entry, parser_chain=None): + """Parsers the file entry and extracts event objects. + + This is the main function of the class, the one that actually + goes through the log file and parses each line of it to + produce a parsed line and a timestamp. + + It also tries to verify the file structure and see if the class is capable + of parsing the file passed to the module. It will do so with series of tests + that should determine if the file is of the correct structure. + + If the class is not capable of parsing the file passed to it an exception + should be raised, an exception of the type UnableToParseFile that indicates + the reason why the class does not parse it. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Raises: + NotImplementedError when not implemented. + """ + raise NotImplementedError + + @classmethod + def RegisterPlugin(cls, plugin_class): + """Registers a plugin class. + + The plugin classes are identified based on their lower case name. + + Args: + plugin_class: the class object of the plugin. + + Raises: + KeyError: if plugin class is already set for the corresponding name. + """ + plugin_name = plugin_class.NAME.lower() + if plugin_name in cls._plugin_classes: + raise KeyError(( + u'Plugin class already set for name: {0:s}.').format( + plugin_class.NAME)) + + cls._plugin_classes[plugin_name] = plugin_class + + @classmethod + def RegisterPlugins(cls, plugin_classes): + """Registers plugin classes. + + Args: + plugin_classes: a list of class objects of the plugins. + + Raises: + KeyError: if plugin class is already set for the corresponding name. + """ + for plugin_class in plugin_classes: + cls.RegisterPlugin(plugin_class) + + @classmethod + def SupportsPlugins(cls): + """Determines if a parser supports plugins. + + Returns: + A boolean value indicating whether the parser supports plugins. + """ + return True diff --git a/plaso/parsers/java_idx.py b/plaso/parsers/java_idx.py new file mode 100644 index 0000000..f3f754a --- /dev/null +++ b/plaso/parsers/java_idx.py @@ -0,0 +1,236 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for Java Cache IDX files.""" + +# TODO: +# * 6.02 files did not retain IP addresses. However, the +# deploy_resource_codebase header field may contain the host IP. +# This needs to be researched further, as that field may not always +# be present. 6.02 files will currently return 'Unknown'. +import construct + +from plaso.events import time_events +from plaso.lib import errors +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import interface +from plaso.parsers import manager + + +class JavaIDXEvent(time_events.TimestampEvent): + """Convenience class for a Java IDX cache file download event.""" + + DATA_TYPE = 'java:download:idx' + + def __init__( + self, timestamp, timestamp_description, idx_version, url, ip_address): + """Initializes the event object. + + Args: + timestamp: The timestamp value. + timestamp_description: The description of the usage of the time value. + idx_version: Version of IDX file. + url: URL of the downloaded file. + ip_address: IP address of the host in the URL. + """ + super(JavaIDXEvent, self).__init__(timestamp, timestamp_description) + self.idx_version = idx_version + self.url = url + self.ip_address = ip_address + + +class JavaIDXParser(interface.BaseParser): + """Parse Java IDX files for download events. + + There are five structures defined. 6.02 files had one generic section + that retained all data. From 6.03, the file went to a multi-section + format where later sections were optional and had variable-lengths. + 6.03, 6.04, and 6.05 files all have their main data section (#2) + begin at offset 128. The short structure is because 6.05 files + deviate after the 8th byte. So, grab the first 8 bytes to ensure it's + valid, get the file version, then continue on with the correct + structures. + """ + + NAME = 'java_idx' + DESCRIPTION = u'Parser for Java IDX files.' + + IDX_SHORT_STRUCT = construct.Struct( + 'magic', + construct.UBInt8('busy'), + construct.UBInt8('incomplete'), + construct.UBInt32('idx_version')) + + IDX_602_STRUCT = construct.Struct( + 'IDX_602_Full', + construct.UBInt16('null_space'), + construct.UBInt8('shortcut'), + construct.UBInt32('content_length'), + construct.UBInt64('last_modified_date'), + construct.UBInt64('expiration_date'), + construct.PascalString( + 'version_string', length_field=construct.UBInt16('length')), + construct.PascalString( + 'url', length_field=construct.UBInt16('length')), + construct.PascalString( + 'namespace', length_field=construct.UBInt16('length')), + construct.UBInt32('FieldCount')) + + IDX_605_SECTION_ONE_STRUCT = construct.Struct( + 'IDX_605_Section1', + construct.UBInt8('shortcut'), + construct.UBInt32('content_length'), + construct.UBInt64('last_modified_date'), + construct.UBInt64('expiration_date'), + construct.UBInt64('validation_date'), + construct.UBInt8('signed'), + construct.UBInt32('sec2len'), + construct.UBInt32('sec3len'), + construct.UBInt32('sec4len')) + + IDX_605_SECTION_TWO_STRUCT = construct.Struct( + 'IDX_605_Section2', + construct.PascalString( + 'version', length_field=construct.UBInt16('length')), + construct.PascalString( + 'url', length_field=construct.UBInt16('length')), + construct.PascalString( + 'namespec', length_field=construct.UBInt16('length')), + construct.PascalString( + 'ip_address', length_field=construct.UBInt16('length')), + construct.UBInt32('FieldCount')) + + # Java uses Pascal-style strings, but with a 2-byte length field. + JAVA_READUTF_STRING = construct.Struct( + 'Java.ReadUTF', + construct.PascalString( + 'string', length_field=construct.UBInt16('length'))) + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract data from a Java cache IDX file. + + This is the main parsing engine for the parser. It determines if + the selected file is a proper IDX file. It then checks the file + version to determine the correct structure to apply to extract + data. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + file_object = file_entry.GetFileObject() + try: + magic = self.IDX_SHORT_STRUCT.parse_stream(file_object) + except (IOError, construct.FieldError) as exception: + raise errors.UnableToParseFile( + u'Unable to parse Java IDX file with error: {0:s}.'.format(exception)) + + # Fields magic.busy and magic.incomplete are normally 0x00. They + # are set to 0x01 if the file is currently being downloaded. Logic + # checks for > 1 to avoid a race condition and still reject any + # file with other data. + # Field magic.idx_version is the file version, of which only + # certain versions are supported. + if magic.busy > 1 or magic.incomplete > 1: + raise errors.UnableToParseFile(u'Not a valid Java IDX file') + + if not magic.idx_version in [602, 603, 604, 605]: + raise errors.UnableToParseFile(u'Not a valid Java IDX file') + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + # Obtain the relevant values from the file. The last modified date + # denotes when the file was last modified on the HOST. For example, + # when the file was uploaded to a web server. + if magic.idx_version == 602: + section_one = self.IDX_602_STRUCT.parse_stream(file_object) + last_modified_date = section_one.last_modified_date + url = section_one.url + ip_address = 'Unknown' + http_header_count = section_one.FieldCount + elif magic.idx_version in [603, 604, 605]: + + # IDX 6.03 and 6.04 have two unused bytes before the structure. + if magic.idx_version in [603, 604]: + file_object.read(2) + + # IDX 6.03, 6.04, and 6.05 files use the same structures for the + # remaining data. + section_one = self.IDX_605_SECTION_ONE_STRUCT.parse_stream(file_object) + last_modified_date = section_one.last_modified_date + if file_object.get_size() > 128: + file_object.seek(128) # Static offset for section 2. + section_two = self.IDX_605_SECTION_TWO_STRUCT.parse_stream(file_object) + url = section_two.url + ip_address = section_two.ip_address + http_header_count = section_two.FieldCount + else: + url = 'Unknown' + ip_address = 'Unknown' + http_header_count = 0 + + # File offset is now just prior to HTTP headers. Make sure there + # are headers, and then parse them to retrieve the download date. + download_date = None + for field in range(0, http_header_count): + field = self.JAVA_READUTF_STRING.parse_stream(file_object) + value = self.JAVA_READUTF_STRING.parse_stream(file_object) + if field.string == 'date': + # Time string "should" be in UTC or have an associated time zone + # information in the string itself. If that is not the case then + # there is no reliable method for plaso to determine the proper + # timezone, so the assumption is that it is UTC. + download_date = timelib.Timestamp.FromTimeString( + value.string, gmt_as_timezone=False) + + if not url or not ip_address: + raise errors.UnableToParseFile( + u'Unexpected Error: URL or IP address not found in file.') + + last_modified_timestamp = timelib.Timestamp.FromJavaTime( + last_modified_date) + # TODO: Move the timestamp description fields into eventdata. + event_object = JavaIDXEvent( + last_modified_timestamp, 'File Hosted Date', magic.idx_version, url, + ip_address) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if section_one: + expiration_date = section_one.get('expiration_date', None) + if expiration_date: + expiration_timestamp = timelib.Timestamp.FromJavaTime(expiration_date) + event_object = JavaIDXEvent( + expiration_timestamp, 'File Expiration Date', magic.idx_version, + url, ip_address) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if download_date: + event_object = JavaIDXEvent( + download_date, eventdata.EventTimestamp.FILE_DOWNLOADED, + magic.idx_version, url, ip_address) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +manager.ParsersManager.RegisterParser(JavaIDXParser) diff --git a/plaso/parsers/java_idx_test.py b/plaso/parsers/java_idx_test.py new file mode 100644 index 0000000..0229a01 --- /dev/null +++ b/plaso/parsers/java_idx_test.py @@ -0,0 +1,124 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for Java Cache IDX file parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import java_idx as java_idx_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import java_idx +from plaso.parsers import test_lib + + +class IDXTest(test_lib.ParserTestCase): + """Tests for Java Cache IDX file parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = java_idx.JavaIDXParser() + + def testParse602(self): + """Tests the Parse function on a version 602 IDX file.""" + test_file = self._GetTestFilePath(['java_602.idx']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 2) + + event_object = event_objects[0] + + idx_version_expected = 602 + self.assertEqual(event_object.idx_version, idx_version_expected) + + ip_address_expected = u'Unknown' + self.assertEqual(event_object.ip_address, ip_address_expected) + + url_expected = u'http://www.gxxxxx.com/a/java/xxz.jar' + self.assertEqual(event_object.url, url_expected) + + description_expected = u'File Hosted Date' + self.assertEqual(event_object.timestamp_desc, description_expected) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2010-05-05 01:34:19.720') + self.assertEqual( + event_object.timestamp, expected_timestamp) + + # Parse second event. Same metadata; different timestamp event. + event_object = event_objects[1] + + self.assertEqual(event_object.idx_version, idx_version_expected) + self.assertEqual(event_object.ip_address, ip_address_expected) + self.assertEqual(event_object.url, url_expected) + + description_expected = eventdata.EventTimestamp.FILE_DOWNLOADED + self.assertEqual(event_object.timestamp_desc, description_expected) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2010-05-05 03:52:31') + self.assertEqual(event_object.timestamp, expected_timestamp) + + def testParse605(self): + """Tests the Parse function on a version 605 IDX file.""" + test_file = self._GetTestFilePath(['java.idx']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 2) + + event_object = event_objects[0] + + idx_version_expected = 605 + self.assertEqual(event_object.idx_version, idx_version_expected) + + ip_address_expected = '10.7.119.10' + self.assertEqual(event_object.ip_address, ip_address_expected) + + url_expected = ( + u'http://xxxxc146d3.gxhjxxwsf.xx:82/forum/dare.php?' + u'hsh=6&key=b30xxxx1c597xxxx15d593d3f0xxx1ab') + self.assertEqual(event_object.url, url_expected) + + description_expected = 'File Hosted Date' + self.assertEqual(event_object.timestamp_desc, description_expected) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2001-07-26 05:00:00' + ) + self.assertEqual(event_object.timestamp, expected_timestamp) + + # Parse second event. Same metadata; different timestamp event. + event_object = event_objects[1] + + self.assertEqual(event_object.idx_version, idx_version_expected) + self.assertEqual(event_object.ip_address, ip_address_expected) + self.assertEqual(event_object.url, url_expected) + + description_expected = eventdata.EventTimestamp.FILE_DOWNLOADED + self.assertEqual(event_object.timestamp_desc, description_expected) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-01-13 16:22:01' + ) + self.assertEqual(event_object.timestamp, expected_timestamp) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/mac_appfirewall.py b/plaso/parsers/mac_appfirewall.py new file mode 100644 index 0000000..291e0b3 --- /dev/null +++ b/plaso/parsers/mac_appfirewall.py @@ -0,0 +1,257 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a appfirewall.log (Mac OS X Firewall) parser.""" + +import datetime +import logging + +import pyparsing + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import manager +from plaso.parsers import text_parser + + +__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)' + + +class MacAppFirewallLogEvent(time_events.TimestampEvent): + """Convenience class for a Mac Wifi log line event.""" + + DATA_TYPE = 'mac:asl:appfirewall:line' + + def __init__(self, timestamp, structure, process_name, action): + """Initializes the event object. + + Args: + timestamp: The timestamp time value, epoch. + structure: structure with the parse fields. + computer_name: string with the name of the computer. + agent: string with the agent that save the log. + status: string with the saved status action. + process_name: string name of the entity that tried do the action. + action: string with the action + """ + super(MacAppFirewallLogEvent, self).__init__( + timestamp, eventdata.EventTimestamp.ADDED_TIME) + self.timestamp = timestamp + self.computer_name = structure.computer_name + self.agent = structure.agent + self.status = structure.status + self.process_name = process_name + self.action = action + + +class MacAppFirewallParser(text_parser.PyparsingSingleLineTextParser): + """Parse text based on appfirewall.log file.""" + + NAME = 'mac_appfirewall_log' + DESCRIPTION = u'Parser for appfirewall.log files.' + + ENCODING = u'utf-8' + + # Regular expressions for known actions. + + # Define how a log line should look like. + # Example: 'Nov 2 04:07:35 DarkTemplar-2.local socketfilterfw[112] ' + # ': Dropbox: Allow (in:0 out:2)' + # INFO: process_name is going to have a white space at the beginning. + FIREWALL_LINE = ( + text_parser.PyparsingConstants.MONTH.setResultsName('month') + + text_parser.PyparsingConstants.ONE_OR_TWO_DIGITS.setResultsName('day') + + text_parser.PyparsingConstants.TIME.setResultsName('time') + + pyparsing.Word(pyparsing.printables).setResultsName('computer_name') + + pyparsing.Word(pyparsing.printables).setResultsName('agent') + + pyparsing.Literal(u'<').suppress() + + pyparsing.CharsNotIn(u'>').setResultsName('status') + + pyparsing.Literal(u'>:').suppress() + + pyparsing.CharsNotIn(u':').setResultsName('process_name') + + pyparsing.Literal(u':') + + pyparsing.SkipTo(pyparsing.lineEnd).setResultsName('action')) + + # Repeated line. + # Example: Nov 29 22:18:29 --- last message repeated 1 time --- + REPEATED_LINE = ( + text_parser.PyparsingConstants.MONTH.setResultsName('month') + + text_parser.PyparsingConstants.ONE_OR_TWO_DIGITS.setResultsName('day') + + text_parser.PyparsingConstants.TIME.setResultsName('time') + + pyparsing.Literal(u'---').suppress() + + pyparsing.CharsNotIn(u'---').setResultsName('process_name') + + pyparsing.Literal(u'---').suppress()) + + # Define the available log line structures. + LINE_STRUCTURES = [ + ('logline', FIREWALL_LINE), + ('repeated', REPEATED_LINE)] + + def __init__(self): + """Initializes a parser object.""" + super(MacAppFirewallParser, self).__init__() + self._year_use = 0 + self._last_month = None + self.previous_structure = None + + def VerifyStructure(self, parser_context, line): + """Verify that this file is a Mac AppFirewall log file. + + Args: + parser_context: A parser context object (instance of ParserContext). + line: A single line from the text file. + + Returns: + True if this is the correct parser, False otherwise. + """ + try: + line = self.FIREWALL_LINE.parseString(line) + except pyparsing.ParseException: + logging.debug(u'Not a Mac AppFirewall log file') + return False + if (line.action != 'creating /var/log/appfirewall.log' or + line.status != 'Error'): + return False + return True + + def ParseRecord(self, parser_context, key, structure): + """Parses each record structure and return an event object if applicable. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: An identification string indicating the name of the parsed + structure. + structure: A pyparsing.ParseResults object from a line in the + log file. + + Returns: + An event object (instance of EventObject) or None. + """ + if key == 'logline' or key == 'repeated': + return self._ParseLogLine(parser_context, structure, key) + else: + logging.warning( + u'Unable to parse record, unknown structure: {0:s}'.format(key)) + + def _ParseLogLine(self, parser_context, structure, key): + """Parse a logline and store appropriate attributes. + + Args: + parser_context: A parser context object (instance of ParserContext). + structure: log line of structure. + key: type of line log (normal or repeated). + + Returns: + Return an object MacAppFirewallLogEvent. + """ + # TODO: improve this to get a valid year. + if not self._year_use: + self._year_use = parser_context.year + + if not self._year_use: + # Get from the creation time of the file. + self._year_use = self._GetYear( + self.file_entry.GetStat(), parser_context.timezone) + # If fail, get from the current time. + if not self._year_use: + self._year_use = timelib.GetCurrentYear() + + # Gap detected between years. + month = timelib.MONTH_DICT.get(structure.month.lower()) + if not self._last_month: + self._last_month = month + if month < self._last_month: + self._year_use += 1 + timestamp = self._GetTimestamp( + structure.day, + month, + self._year_use, + structure.time) + if not timestamp: + logging.debug(u'Invalid timestamp {0:s}'.format(structure.timestamp)) + return + self._last_month = month + + # If the actual entry is a repeated entry, we take the basic information + # from the previous entry, but using the timestmap from the actual entry. + if key == 'logline': + self.previous_structure = structure + else: + structure = self.previous_structure + + # Pyparsing reads in RAW, but the text is in UTF8. + try: + action = structure.action.decode('utf-8') + except UnicodeDecodeError: + logging.warning( + u'Decode UTF8 failed, the message string may be cut short.') + action = structure.action.decode('utf-8', 'ignore') + # Due to the use of CharsNotIn pyparsing structure contains whitespaces + # that need to be removed. + process_name = structure.process_name.strip() + + event_object = MacAppFirewallLogEvent( + timestamp, structure, process_name, action) + return event_object + + def _GetTimestamp(self, day, month, year, time): + """Gets a timestamp from a pyparsing ParseResults timestamp. + + This is a timestamp_string as returned by using + text_parser.PyparsingConstants structures: + 08, Nov, [20, 36, 37] + + Args: + timestamp_string: The pyparsing ParseResults object + + Returns: + day: An integer representing the day. + month: An integer representing the month. + year: An integer representing the year. + timestamp: A plaso timelib timestamp event or 0. + """ + try: + hour, minute, second = time + timestamp = timelib.Timestamp.FromTimeParts( + year, month, day, hour, minute, second) + except ValueError: + timestamp = 0 + return timestamp + + def _GetYear(self, stat, timezone): + """Retrieves the year either from the input file or from the settings.""" + time = getattr(stat, 'crtime', 0) + if not time: + time = getattr(stat, 'ctime', 0) + + if not time: + logging.error( + u'Unable to determine correct year of log file, defaulting to ' + u'current year.') + return timelib.GetCurrentYear() + + try: + timestamp = datetime.datetime.fromtimestamp(time, timezone) + except ValueError as exception: + logging.error(( + u'Unable to determine correct year of log file with error: {0:s}, ' + u'defaulting to current year.').format(exception)) + return timelib.GetCurrentYear() + return timestamp.year + + +manager.ParsersManager.RegisterParser(MacAppFirewallParser) diff --git a/plaso/parsers/mac_appfirewall_test.py b/plaso/parsers/mac_appfirewall_test.py new file mode 100644 index 0000000..54d4456 --- /dev/null +++ b/plaso/parsers/mac_appfirewall_test.py @@ -0,0 +1,118 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for Mac AppFirewall log file parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import mac_appfirewall as mac_appfirewall_formatter +from plaso.lib import timelib_test +from plaso.parsers import mac_appfirewall +from plaso.parsers import test_lib + + +class MacAppFirewallUnitTest(test_lib.ParserTestCase): + """Tests for Mac AppFirewall log file parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = mac_appfirewall.MacAppFirewallParser() + + def testParseFile(self): + """Test parsing of a Mac Wifi log file.""" + knowledge_base_values = {'year': 2013} + test_file = self._GetTestFilePath(['appfirewall.log']) + event_queue_consumer = self._ParseFile( + self._parser, test_file, knowledge_base_values=knowledge_base_values) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEqual(len(event_objects), 47) + + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-02 04:07:35') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.agent, u'socketfilterfw[112]') + self.assertEqual(event_object.computer_name, u'DarkTemplar-2.local') + self.assertEqual(event_object.status, u'Error') + self.assertEqual(event_object.process_name, u'Logging') + self.assertEqual(event_object.action, u'creating /var/log/appfirewall.log') + + expected_msg = ( + u'Computer: DarkTemplar-2.local ' + u'Agent: socketfilterfw[112] ' + u'Status: Error ' + u'Process name: Logging ' + u'Log: creating /var/log/appfirewall.log') + expected_msg_short = ( + u'Process name: Logging ' + u'Status: Error') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + event_object = event_objects[9] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-03 13:25:15') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.agent, u'socketfilterfw[87]') + self.assertEqual(event_object.computer_name, u'DarkTemplar-2.local') + self.assertEqual(event_object.status, u'Info') + self.assertEqual(event_object.process_name, u'Dropbox') + self.assertEqual(event_object.action, u'Allow TCP LISTEN (in:0 out:1)') + + expected_msg = ( + u'Computer: DarkTemplar-2.local ' + u'Agent: socketfilterfw[87] ' + u'Status: Info ' + u'Process name: Dropbox ' + u'Log: Allow TCP LISTEN (in:0 out:1)') + expected_msg_short = ( + u'Process name: Dropbox ' + u'Status: Info') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + # Check repeated lines. + event_object = event_objects[38] + repeated_event_object = event_objects[39] + self.assertEqual(event_object.agent, repeated_event_object.agent) + self.assertEqual( + event_object.computer_name, repeated_event_object.computer_name) + self.assertEqual(event_object.status, repeated_event_object.status) + self.assertEqual( + event_object.process_name, repeated_event_object.process_name) + self.assertEqual(event_object.action, repeated_event_object.action) + + # Year changes. + event_object = event_objects[45] + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-12-31 23:59:23') + self.assertEqual(event_object.timestamp, expected_timestamp) + + event_object = event_objects[46] + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2014-01-01 01:13:23') + self.assertEqual(event_object.timestamp, expected_timestamp) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/mac_keychain.py b/plaso/parsers/mac_keychain.py new file mode 100644 index 0000000..d875997 --- /dev/null +++ b/plaso/parsers/mac_keychain.py @@ -0,0 +1,507 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for Mac OS X Keychain files.""" + +# INFO: Only supports internet and application passwords, +# because it is the only data that contains timestamp events. +# Keychain can also store "secret notes". These notes are stored +# in the same type than the application format, then, they are already +# supported. The stored wifi are also application passwords. + +# TODO: the AccessControl for each entry has not been implemented. Until now, +# I know that the AccessControl from Internet and App password are stored +# using other tables (Symmetric, certificates, etc). Access Control +# indicates which specific tool, or all, is able to use this entry. + + +import binascii +import construct +import logging +import os + +from plaso.lib import errors +from plaso.lib import event +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import interface +from plaso.parsers import manager + + +__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)' + + +class KeychainInternetRecordEvent(event.EventObject): + """Convenience class for an keychain internet record event.""" + + DATA_TYPE = 'mac:keychain:internet' + + def __init__( + self, timestamp, timestamp_desc, entry_name, account_name, + text_description, comments, where, protocol, type_protocol, ssgp_hash): + """Initializes the event object. + + Args: + timestamp: Description of the timestamp value. + timestamp_desc: Timelib type of the timestamp. + entry_name: Name of the entry. + account_name: Name of the account. + text_description: Short description about the entry. + comments: String that contains the comments added by the user. + where: The domain name or IP where the password is used. + protocol: The internet protocol used (eg. https). + type_protocol: The sub-protocol used (eg. form). + ssgp_hash: String with hexadecimal values from the password / cert hash. + """ + super(KeychainInternetRecordEvent, self).__init__() + self.timestamp = timestamp + self.timestamp_desc = timestamp_desc + self.entry_name = entry_name + self.account_name = account_name + self.text_description = text_description + self.where = where + self.protocol = protocol + self.type_protocol = type_protocol + self.comments = comments + self.ssgp_hash = ssgp_hash + + +class KeychainApplicationRecordEvent(event.EventObject): + """Convenience class for an keychain application password record event.""" + DATA_TYPE = 'mac:keychain:application' + + def __init__( + self, timestamp, timestamp_desc, entry_name, + account_name, text_description, comments, ssgp_hash): + """Initializes the event object. + + Args: + timestamp: Description of the timestamp value. + timestamp_desc: Timelib type of the timestamp. + entry_name: Name of the entry. + account_name: Name of the account. + text_description: Short description about the entry. + comments: String that contains the comments added by the user. + ssgp_hash: String with hexadecimal values from the password / cert hash. + """ + super(KeychainApplicationRecordEvent, self).__init__() + self.timestamp = timestamp + self.timestamp_desc = timestamp_desc + self.entry_name = entry_name + self.account_name = account_name + self.text_description = text_description + self.comments = comments + self.ssgp_hash = ssgp_hash + + +class KeychainParser(interface.BaseParser): + """Parser for Keychain files.""" + + NAME = 'mac_keychain' + DESCRIPTION = u'Parser for Mac OS X Keychain files.' + + KEYCHAIN_MAGIC_HEADER = 'kych' + KEYCHAIN_MAJOR_VERSION = 1 + KEYCHAIN_MINOR_VERSION = 0 + + RECORD_TYPE_APPLICATION = 0x80000000 + RECORD_TYPE_INTERNET = 0x80000001 + + # DB HEADER. + KEYCHAIN_DB_HEADER = construct.Struct( + 'db_header', + construct.String('magic', 4), + construct.UBInt16('major_version'), + construct.UBInt16('minor_version'), + construct.UBInt32('header_size'), + construct.UBInt32('schema_offset'), + construct.Padding(4)) + + # DB SCHEMA. + KEYCHAIN_DB_SCHEMA = construct.Struct( + 'db_schema', + construct.UBInt32('size'), + construct.UBInt32('number_of_tables')) + # For each number_of_tables, the schema has a TABLE_OFFSET with the + # offset starting in the DB_SCHEMA. + TABLE_OFFSET = construct.UBInt32('table_offset') + + TABLE_HEADER = construct.Struct( + 'table_header', + construct.UBInt32('table_size'), + construct.UBInt32('record_type'), + construct.UBInt32('number_of_records'), + construct.UBInt32('first_record'), + construct.UBInt32('index_offset'), + construct.Padding(4), + construct.UBInt32('recordnumbercount')) + + RECORD_HEADER = construct.Struct( + 'record_header', + construct.UBInt32('entry_length'), + construct.Padding(12), + construct.UBInt32('ssgp_length'), + construct.Padding(4), + construct.UBInt32('creation_time'), + construct.UBInt32('last_mod_time'), + construct.UBInt32('text_description'), + construct.Padding(4), + construct.UBInt32('comments'), + construct.Padding(8), + construct.UBInt32('entry_name'), + construct.Padding(20), + construct.UBInt32('account_name'), + construct.Padding(4)) + RECORD_HEADER_APP = construct.Struct( + 'record_entry_app', + RECORD_HEADER, + construct.Padding(4)) + RECORD_HEADER_INET = construct.Struct( + 'record_entry_inet', + RECORD_HEADER, + construct.UBInt32('where'), + construct.UBInt32('protocol'), + construct.UBInt32('type'), + construct.Padding(4), + construct.UBInt32('url')) + + TEXT = construct.PascalString( + 'text', length_field=construct.UBInt32('length')) + TIME = construct.Struct( + 'timestamp', + construct.String('year', 4), + construct.String('month', 2), + construct.String('day', 2), + construct.String('hour', 2), + construct.String('minute', 2), + construct.String('second', 2), + construct.Padding(2)) + TYPE_TEXT = construct.String('type', 4) + + # TODO: add more protocols. + _PROTOCOL_TRANSLATION_DICT = { + u'htps': u'https', + u'smtp': u'smtp', + u'imap': u'imap', + u'http': u'http'} + + def _GetTimestampFromEntry(self, parser_context, file_entry, structure): + """Parse a time entry structure into a microseconds since Epoch in UTC. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + structure: TIME entry structure: + year: String with the number of the year. + month: String with the number of the month. + day: String with the number of the day. + hour: String with the number of the month. + minute: String with the number of the minute. + second: String with the number of the second. + + Returns: + Microseconds since Epoch in UTC. + """ + try: + return timelib.Timestamp.FromTimeParts( + int(structure.year, 10), int(structure.month, 10), + int(structure.day, 10), int(structure.hour, 10), + int(structure.minute, 10), int(structure.second, 10)) + except ValueError: + logging.warning( + u'[{0:s}] Invalid keychain time {1!s} in file: {2:s}'.format( + self.NAME, parser_context.GetDisplayName(file_entry), structure)) + return 0 + + def _ReadEntryApplication( + self, parser_context, file_object, file_entry=None, parser_chain=None): + """Extracts the information from an application password entry. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_object: A file-like object that points to an Keychain file. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + offset = file_object.tell() + try: + record = self.RECORD_HEADER_APP.parse_stream(file_object) + except (IOError, construct.FieldError): + logging.warning(( + u'[{0:s}] Unsupported record header at 0x{1:08x} in file: ' + u'{2:s}').format( + self.NAME, offset, parser_context.GetDisplayName(file_entry))) + return + + (ssgp_hash, creation_time, last_mod_time, text_description, + comments, entry_name, account_name) = self._ReadEntryHeader( + parser_context, file_entry, file_object, record.record_header, offset) + + # Move to the end of the record, and then, prepared for the next record. + file_object.seek( + record.record_header.entry_length + offset - file_object.tell(), + os.SEEK_CUR) + event_object = KeychainApplicationRecordEvent( + creation_time, eventdata.EventTimestamp.CREATION_TIME, + entry_name, account_name, text_description, comments, ssgp_hash) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if creation_time != last_mod_time: + event_object = KeychainApplicationRecordEvent( + last_mod_time, eventdata.EventTimestamp.MODIFICATION_TIME, + entry_name, account_name, text_description, comments, ssgp_hash) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + def _ReadEntryHeader( + self, parser_context, file_entry, file_object, record, offset): + """Read the common record attributes. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + file_object: A file-like object that points to an Keychain file. + record: Structure with the header of the record. + offset: First byte of the record. + + Returns: + A list of: + ssgp_hash: Hash of the encrypted data (passwd, cert, note). + creation_time: When the entry was created. + last_mod_time: Last time the entry was updated. + text_description: A brief description of the entry. + entry_name: Name of the entry + account_name: Name of the account. + """ + # Info: The hash header always start with the string ssgp follow by + # the hash. Furthermore The fields are always a multiple of four. + # Then if it is not multiple the value is padded by 0x00. + ssgp_hash = binascii.hexlify(file_object.read(record.ssgp_length)[4:]) + + file_object.seek( + record.creation_time - file_object.tell() + offset - 1, os.SEEK_CUR) + creation_time = self._GetTimestampFromEntry( + parser_context, file_entry, self.TIME.parse_stream(file_object)) + + file_object.seek( + record.last_mod_time - file_object.tell() + offset - 1, os.SEEK_CUR) + last_mod_time = self._GetTimestampFromEntry( + parser_context, file_entry, self.TIME.parse_stream(file_object)) + + # The comment field does not always contain data. + if record.text_description: + file_object.seek( + record.text_description - file_object.tell() + offset -1, + os.SEEK_CUR) + try: + text_description = self.TEXT.parse_stream(file_object) + except construct.FieldError: + text_description = u'N/A (error)' + else: + text_description = u'N/A' + + # The comment field does not always contain data. + if record.comments: + file_object.seek( + record.text_description - file_object.tell() + offset -1, + os.SEEK_CUR) + try: + comments = self.TEXT.parse_stream(file_object) + except construct.FieldError: + comments = u'N/A (error)' + else: + comments = u'N/A' + + file_object.seek( + record.entry_name - file_object.tell() + offset - 1, os.SEEK_CUR) + try: + entry_name = self.TEXT.parse_stream(file_object) + except construct.FieldError: + entry_name = u'N/A (error)' + + file_object.seek( + record.account_name - file_object.tell() + offset - 1, os.SEEK_CUR) + try: + account_name = self.TEXT.parse_stream(file_object) + except construct.FieldError: + account_name = u'N/A (error)' + + return ( + ssgp_hash, creation_time, last_mod_time, + text_description, comments, entry_name, account_name) + + def _ReadEntryInternet( + self, parser_context, file_object, file_entry=None, parser_chain=None): + """Extracts the information from an Internet password entry. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_object: A file-like object that points to an Keychain file. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + offset = file_object.tell() + try: + record = self.RECORD_HEADER_INET.parse_stream(file_object) + except (IOError, construct.FieldError): + logging.warning(( + u'[{0:s}] Unsupported record header at 0x{1:08x} in file: ' + u'{2:s}').format( + self.NAME, offset, parser_context.GetDisplayName(file_entry))) + return + + (ssgp_hash, creation_time, last_mod_time, text_description, + comments, entry_name, account_name) = self._ReadEntryHeader( + parser_context, file_entry, file_object, record.record_header, offset) + if not record.where: + where = u'N/A' + protocol = u'N/A' + type_protocol = u'N/A' + else: + file_object.seek( + record.where - file_object.tell() + offset - 1, os.SEEK_CUR) + where = self.TEXT.parse_stream(file_object) + file_object.seek( + record.protocol - file_object.tell() + offset - 1, os.SEEK_CUR) + protocol = self.TYPE_TEXT.parse_stream(file_object) + file_object.seek( + record.type - file_object.tell() + offset - 1, os.SEEK_CUR) + type_protocol = self.TEXT.parse_stream(file_object) + type_protocol = self._PROTOCOL_TRANSLATION_DICT.get( + type_protocol, type_protocol) + if record.url: + file_object.seek( + record.url - file_object.tell() + offset - 1, os.SEEK_CUR) + url = self.TEXT.parse_stream(file_object) + where = u'{0:s}{1:s}'.format(where, url) + + # Move to the end of the record, and then, prepared for the next record. + file_object.seek( + record.record_header.entry_length + offset - file_object.tell(), + os.SEEK_CUR) + + event_object = KeychainInternetRecordEvent( + creation_time, eventdata.EventTimestamp.CREATION_TIME, + entry_name, account_name, text_description, + comments, where, protocol, type_protocol, ssgp_hash) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if creation_time != last_mod_time: + event_object = KeychainInternetRecordEvent( + last_mod_time, eventdata.EventTimestamp.MODIFICATION_TIME, + entry_name, account_name, text_description, + comments, where, protocol, type_protocol, ssgp_hash) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + def _VerifyStructure(self, file_object): + """Verify that we are dealing with an Keychain entry. + + Args: + file_object: A file-like object that points to an Keychain file. + + Returns: + A list of table positions if it is a keychain, None otherwise. + """ + # INFO: The HEADER KEYCHAIN: + # [DBHEADER] + [DBSCHEMA] + [OFFSET TABLE A] + ... + [OFFSET TABLE Z] + # Where the table offset is relative to the first byte of the DB Schema, + # then we must add to this offset the size of the [DBHEADER]. + try: + db_header = self.KEYCHAIN_DB_HEADER.parse_stream(file_object) + except (IOError, construct.FieldError): + return + if (db_header.minor_version != self.KEYCHAIN_MINOR_VERSION or + db_header.major_version != self.KEYCHAIN_MAJOR_VERSION or + db_header.magic != self.KEYCHAIN_MAGIC_HEADER): + return + + # Read the database schema and extract the offset for all the tables. + # They are ordered by file position from the top to the bottom of the file. + try: + db_schema = self.KEYCHAIN_DB_SCHEMA.parse_stream(file_object) + except (IOError, construct.FieldError): + return + table_offsets = [] + for _ in range(db_schema.number_of_tables): + try: + table_offset = self.TABLE_OFFSET.parse_stream(file_object) + except (IOError, construct.FieldError): + return + table_offsets.append(table_offset + self.KEYCHAIN_DB_HEADER.sizeof()) + return table_offsets + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract data from a Keychain file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + file_object = file_entry.GetFileObject() + table_offsets = self._VerifyStructure(file_object) + if not table_offsets: + file_object.close() + raise errors.UnableToParseFile(u'The file is not a Keychain file.') + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + for table_offset in table_offsets: + # Skipping X bytes, unknown data at this point. + file_object.seek(table_offset - file_object.tell(), os.SEEK_CUR) + try: + table = self.TABLE_HEADER.parse_stream(file_object) + except construct.FieldError as exception: + logging.warning(( + u'[{0:s}] Unable to parse table header in file: {1:s} ' + u'with error: {2:s}.').format( + self.NAME, parser_context.GetDisplayName(file_entry), + exception)) + continue + + # Table_offset: absolute byte in the file where the table starts. + # table.first_record: first record in the table, relative to the + # first byte of the table. + file_object.seek( + table_offset + table.first_record - file_object.tell(), os.SEEK_CUR) + + if table.record_type == self.RECORD_TYPE_INTERNET: + for _ in range(table.number_of_records): + self._ReadEntryInternet( + parser_context, file_object, file_entry=file_entry, + parser_chain=parser_chain) + + elif table.record_type == self.RECORD_TYPE_APPLICATION: + for _ in range(table.number_of_records): + self._ReadEntryApplication( + parser_context, file_object, file_entry=file_entry, + parser_chain=parser_chain) + + file_object.close() + + +manager.ParsersManager.RegisterParser(KeychainParser) diff --git a/plaso/parsers/mac_keychain_test.py b/plaso/parsers/mac_keychain_test.py new file mode 100644 index 0000000..6fe352b --- /dev/null +++ b/plaso/parsers/mac_keychain_test.py @@ -0,0 +1,107 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for Keychain password database parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import mac_keychain as mac_keychain_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import test_lib +from plaso.parsers import mac_keychain + + +class MacKeychainParserTest(test_lib.ParserTestCase): + """Tests for keychain file parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = mac_keychain.KeychainParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['login.keychain']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEqual(len(event_objects), 5) + + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2014-01-26 14:51:48') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual( + event_object.timestamp_desc, + eventdata.EventTimestamp.CREATION_TIME) + self.assertEqual(event_object.entry_name, u'Secret Application') + self.assertEqual(event_object.account_name, u'moxilo') + expected_ssgp = (u'b8e44863af1cb0785b89681d22e2721997cc' + u'fb8adb8853e726aff94c8830b05a') + self.assertEqual(event_object.ssgp_hash, expected_ssgp) + self.assertEqual(event_object.text_description, u'N/A') + expected_msg = u'Name: Secret Application Account: moxilo' + expected_msg_short = u'Secret Application' + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + event_object = event_objects[1] + self.assertEqual( + event_object.timestamp_desc, + eventdata.EventTimestamp.MODIFICATION_TIME) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2014-01-26 14:52:29') + self.assertEqual(event_object.timestamp, expected_timestamp) + + event_object = event_objects[2] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2014-01-26 14:53:29') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.entry_name, u'Secret Note') + self.assertEqual(event_object.text_description, u'secure note') + self.assertEqual(len(event_object.ssgp_hash), 1696) + expected_msg = u'Name: Secret Note' + expected_msg_short = u'Secret Note' + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + event_object = event_objects[3] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2014-01-26 14:54:33') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.entry_name, u'plaso.kiddaland.net') + self.assertEqual(event_object.account_name, u'MrMoreno') + expected_ssgp = (u'83ccacf55a8cb656d340ec405e9d8b308f' + u'ac54bb79c5c9b0219bd0d700c3c521') + self.assertEqual(event_object.ssgp_hash, expected_ssgp) + self.assertEqual(event_object.where, u'plaso.kiddaland.net') + self.assertEqual(event_object.protocol, u'http') + self.assertEqual(event_object.type_protocol, u'dflt') + self.assertEqual(event_object.text_description, u'N/A') + expected_msg = (u'Name: plaso.kiddaland.net Account: MrMoreno Where: ' + u'plaso.kiddaland.net Protocol: http (dflt)') + expected_msg_short = u'plaso.kiddaland.net' + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/mac_securityd.py b/plaso/parsers/mac_securityd.py new file mode 100644 index 0000000..c274204 --- /dev/null +++ b/plaso/parsers/mac_securityd.py @@ -0,0 +1,276 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the ASL securityd log plaintext parser.""" + +import datetime +import logging + +import pyparsing + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import manager +from plaso.parsers import text_parser + + +__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)' + + +# INFO: +# http://opensource.apple.com/source/Security/Security-55471/sec/securityd/ + + +class MacSecuritydLogEvent(time_events.TimestampEvent): + """Convenience class for a ASL securityd line event.""" + + DATA_TYPE = 'mac:asl:securityd:line' + + def __init__( + self, timestamp, structure, sender, sender_pid, + security_api, caller, message): + """Initializes the event object. + + Args: + timestamp: The timestamp time value, epoch. + structure: Structure with the parse fields. + level: String with the text representation of the priority level. + facility: String with the ASL facility. + sender: String with the name of the sender. + sender_pid: Process id of the sender. + security_api: Securityd function name. + caller: The caller field, a string containing two hex numbers. + message: String with the ASL message. + """ + super(MacSecuritydLogEvent, self).__init__( + timestamp, + eventdata.EventTimestamp.ADDED_TIME) + self.timestamp = timestamp + self.level = structure.level + self.sender_pid = sender_pid + self.facility = structure.facility + self.sender = sender + self.security_api = security_api + self.caller = caller + self.message = message + + +class MacSecuritydLogParser(text_parser.PyparsingSingleLineTextParser): + """Parses the securityd file that contains logs from the security daemon.""" + + NAME = 'mac_securityd' + DESCRIPTION = u'Parser for Mac OS X securityd log files.' + + ENCODING = u'utf-8' + + # Default ASL Securityd log. + SECURITYD_LINE = ( + text_parser.PyparsingConstants.MONTH.setResultsName('month') + + text_parser.PyparsingConstants.ONE_OR_TWO_DIGITS.setResultsName('day') + + text_parser.PyparsingConstants.TIME.setResultsName('time') + + pyparsing.CharsNotIn(u'[').setResultsName('sender') + + pyparsing.Literal(u'[').suppress() + + text_parser.PyparsingConstants.PID.setResultsName('sender_pid') + + pyparsing.Literal(u']').suppress() + + pyparsing.Literal(u'<').suppress() + + pyparsing.CharsNotIn(u'>').setResultsName('level') + + pyparsing.Literal(u'>').suppress() + + pyparsing.Literal(u'[').suppress() + + pyparsing.CharsNotIn(u'{').setResultsName('facility') + + pyparsing.Literal(u'{').suppress() + + pyparsing.Optional(pyparsing.CharsNotIn( + u'}').setResultsName('security_api')) + + pyparsing.Literal(u'}').suppress() + + pyparsing.Optional(pyparsing.CharsNotIn(u']:').setResultsName('caller')) + + pyparsing.Literal(u']:').suppress() + + pyparsing.SkipTo(pyparsing.lineEnd).setResultsName('message')) + + # Repeated line. + REPEATED_LINE = ( + text_parser.PyparsingConstants.MONTH.setResultsName('month') + + text_parser.PyparsingConstants.ONE_OR_TWO_DIGITS.setResultsName('day') + + text_parser.PyparsingConstants.TIME.setResultsName('time') + + pyparsing.Literal(u'--- last message repeated').suppress() + + text_parser.PyparsingConstants.INTEGER.setResultsName('times') + + pyparsing.Literal(u'time ---').suppress()) + + # Define the available log line structures. + LINE_STRUCTURES = [ + ('logline', SECURITYD_LINE), + ('repeated', REPEATED_LINE)] + + def __init__(self): + """Initializes a parser object.""" + super(MacSecuritydLogParser, self).__init__() + self._year_use = 0 + self._last_month = None + self.previous_structure = None + + def VerifyStructure(self, parser_context, line): + """Verify that this file is a ASL securityd log file. + + Args: + parser_context: A parser context object (instance of ParserContext). + line: A single line from the text file. + + Returns: + True if this is the correct parser, False otherwise. + """ + try: + line = self.SECURITYD_LINE.parseString(line) + except pyparsing.ParseException: + logging.debug(u'Not a ASL securityd log file') + return False + # Check if the day, month and time is valid taking a random year. + month = timelib.MONTH_DICT.get(line.month.lower()) + if not month: + return False + if self._GetTimestamp(line.day, month, 2012, line.time) == 0: + return False + return True + + def ParseRecord(self, parser_context, key, structure): + """Parse each record structure and return an EventObject if applicable. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: An identification string indicating the name of the parsed + structure. + structure: A pyparsing.ParseResults object from a line in the + log file. + + Returns: + An event object (instance of EventObject) or None. + """ + if key == 'repeated' or key == 'logline': + return self._ParseLogLine(parser_context, structure, key) + else: + logging.warning( + u'Unable to parse record, unknown structure: {0:s}'.format(key)) + + def _ParseLogLine(self, parser_context, structure, key): + """Parse a logline and store appropriate attributes. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: An identification string indicating the name of the parsed + structure. + structure: A pyparsing.ParseResults object from a line in the + log file. + + Returns: + An event object (instance of EventObject) or None. + """ + # TODO: improving this to get a valid year. + if not self._year_use: + self._year_use = parser_context.year + + if not self._year_use: + # Get from the creation time of the file. + self._year_use = self._GetYear( + self.file_entry.GetStat(), parser_context.timezone) + # If fail, get from the current time. + if not self._year_use: + self._year_use = timelib.GetCurrentYear() + + # Gap detected between years. + month = timelib.MONTH_DICT.get(structure.month.lower()) + if not self._last_month: + self._last_month = month + if month < self._last_month: + self._year_use += 1 + timestamp = self._GetTimestamp( + structure.day, + month, + self._year_use, + structure.time) + if not timestamp: + logging.debug(u'Invalid timestamp {0:s}'.format(structure.timestamp)) + return + self._last_month = month + + if key == 'logline': + self.previous_structure = structure + message = structure.message + else: + times = structure.times + structure = self.previous_structure + message = u'Repeated {0:d} times: {1:s}'.format( + times, structure.message) + + # It uses CarsNotIn structure which leaves whitespaces + # at the beginning of the sender and the caller. + sender = structure.sender.strip() + caller = structure.caller.strip() + if not caller: + caller = 'unknown' + if not structure.security_api: + security_api = u'unknown' + else: + security_api = structure.security_api + + return MacSecuritydLogEvent( + timestamp, structure, sender, structure.sender_pid, security_api, + caller, message) + + def _GetTimestamp(self, day, month, year, time): + """Gets a timestamp from a pyparsing ParseResults timestamp. + + This is a timestamp_string as returned by using + text_parser.PyparsingConstants structures: + 08, Nov, [20, 36, 37] + + Args: + day: An integer representing the day. + month: An integer representing the month. + year: An integer representing the year. + time: A list containing the hours, minutes, seconds. + + Returns: + timestamp: A plaso timestamp. + """ + hours, minutes, seconds = time + return timelib.Timestamp.FromTimeParts( + year, month, day, hours, minutes, seconds) + + def _GetYear(self, stat, zone): + """Retrieves the year either from the input file or from the settings.""" + time = getattr(stat, 'crtime', 0) + if not time: + time = getattr(stat, 'ctime', 0) + + if not time: + current_year = timelib.GetCurrentYear() + logging.error(( + u'Unable to determine year of log file.\nDefaulting to: ' + u'{0:d}').format(current_year)) + return current_year + + try: + timestamp = datetime.datetime.fromtimestamp(time, zone) + except ValueError: + current_year = timelib.GetCurrentYear() + logging.error(( + u'Unable to determine year of log file.\nDefaulting to: ' + u'{0:d}').format(current_year)) + return current_year + + return timestamp.year + + +manager.ParsersManager.RegisterParser(MacSecuritydLogParser) diff --git a/plaso/parsers/mac_securityd_test.py b/plaso/parsers/mac_securityd_test.py new file mode 100644 index 0000000..4265b3c --- /dev/null +++ b/plaso/parsers/mac_securityd_test.py @@ -0,0 +1,159 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a unit test for ASL securityd log parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import mac_securityd as mac_securityd_formatter +from plaso.lib import timelib_test +from plaso.parsers import mac_securityd as mac_securityd_parser +from plaso.parsers import test_lib + + +class MacSecurityUnitTest(test_lib.ParserTestCase): + """A unit test for the ASL securityd log parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = mac_securityd_parser.MacSecuritydLogParser() + + def testParseFile(self): + """Test parsing of a ASL securityd log file.""" + knowledge_base_values = {'year': 2013} + test_file = self._GetTestFilePath(['security.log']) + events = self._ParseFile( + self._parser, test_file, knowledge_base_values=knowledge_base_values) + event_objects = self._GetEventObjectsFromQueue(events) + + self.assertEqual(len(event_objects), 9) + + event_object = event_objects[0] + expected_msg = ( + u'Sender: secd (1) Level: Error Facility: user ' + u'Text: securityd_xpc_dictionary_handler EscrowSecurityAl' + u'[3273] DeviceInCircle \xdeetta \xe6tti a\xf0 ' + u'virka l\xedka, setja \xedslensku inn.') + expected_msg_short = ( + u'Text: securityd_xpc_dictionary_handler ' + u'EscrowSecurityAl[3273] DeviceInCircle ...') + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-02-26 19:11:56') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.sender, u'secd') + self.assertEqual(event_object.sender_pid, 1) + self.assertEqual(event_object.facility, u'user') + self.assertEqual(event_object.security_api, u'unknown') + self.assertEqual(event_object.caller, u'unknown') + self.assertEqual(event_object.level, u'Error') + expected_msg = ( + u'securityd_xpc_dictionary_handler EscrowSecurityAl' + u'[3273] DeviceInCircle \xdeetta \xe6tti a\xf0 virka ' + u'l\xedka, setja \xedslensku inn.') + self.assertEqual(event_object.message, expected_msg) + + event_object = event_objects[1] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-12-26 19:11:57') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.sender, u'secd') + self.assertEqual(event_object.sender_pid, 11) + self.assertEqual(event_object.facility, u'serverxpc') + self.assertEqual(event_object.security_api, u'SOSCCThisDeviceIsInCircle') + self.assertEqual(event_object.caller, u'unknown') + self.assertEqual(event_object.level, u'Notice') + + event_object = event_objects[2] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-12-26 19:11:58') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.sender, u'secd') + self.assertEqual(event_object.sender_pid, 111) + self.assertEqual(event_object.facility, u'user') + self.assertEqual(event_object.security_api, u'unknown') + self.assertEqual(event_object.caller, u'unknown') + self.assertEqual(event_object.level, u'Debug') + + event_object = event_objects[3] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-12-26 19:11:59') + self.assertEqual(event_object.timestamp, 1388085119000000) + + self.assertEqual(event_object.sender, u'secd') + self.assertEqual(event_object.sender_pid, 1111) + self.assertEqual(event_object.facility, u'user') + self.assertEqual(event_object.security_api, u'SOSCCThisDeviceIsInCircle') + self.assertEqual(event_object.caller, u'C0x7fff872fa482') + self.assertEqual(event_object.level, u'Error') + + event_object = event_objects[4] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-12-06 19:11:01') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.sender, u'secd') + self.assertEqual(event_object.sender_pid, 1) + self.assertEqual(event_object.facility, u'user') + self.assertEqual(event_object.security_api, u'unknown') + self.assertEqual(event_object.caller, u'unknown') + self.assertEqual(event_object.level, u'Error') + self.assertEqual(event_object.message, u'') + + event_object = event_objects[5] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-12-06 19:11:02') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.sender, u'secd') + self.assertEqual(event_object.sender_pid, 11111) + self.assertEqual(event_object.facility, u'user') + self.assertEqual(event_object.security_api, u'SOSCCThisDeviceIsInCircle') + self.assertEqual(event_object.caller, u'C0x7fff872fa482 F0x106080db0') + self.assertEqual(event_object.level, u'Error') + self.assertEqual(event_object.message, u'') + + event_object = event_objects[6] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-12-31 23:59:59') + self.assertEqual(event_object.timestamp, expected_timestamp) + + event_object = event_objects[7] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2014-03-01 00:00:01') + self.assertEqual(event_object.timestamp, expected_timestamp) + + # Repeated line. + event_object = event_objects[8] + expected_msg = u'Repeated 3 times: Happy new year!' + self.assertEqual(event_object.message, expected_msg) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/mac_wifi.py b/plaso/parsers/mac_wifi.py new file mode 100644 index 0000000..99e1412 --- /dev/null +++ b/plaso/parsers/mac_wifi.py @@ -0,0 +1,280 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the wifi.log (Mac OS X) parser.""" + +import datetime +import logging +import re + +import pyparsing + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import manager +from plaso.parsers import text_parser + + +__author__ = 'Joaquin Moreno Garijo (bastionado@gmail.com)' + + +class MacWifiLogEvent(time_events.TimestampEvent): + """Convenience class for a Mac Wifi log line event.""" + + DATA_TYPE = 'mac:wifilog:line' + + def __init__(self, timestamp, agent, function, text, action): + """Initializes the event object. + + Args: + timestamp: The timestamp time value, epoch. + source_code: Details of the source code file generating the event. + log_level: The log level used for the event. + text: The log message + action: A string containing known WiFI actions, eg: connected to + an AP, configured, etc. If the action is not known, + the value is the message of the log (text variable). + """ + super(MacWifiLogEvent, self).__init__( + timestamp, eventdata.EventTimestamp.ADDED_TIME) + self.agent = agent + self.function = function + self.text = text + self.action = action + + +class MacWifiLogParser(text_parser.PyparsingSingleLineTextParser): + """Parse text based on wifi.log file.""" + + NAME = 'macwifi' + DESCRIPTION = u'Parser for Mac OS X wifi.log files.' + + ENCODING = u'utf-8' + + # Regular expressions for known actions. + RE_CONNECTED = re.compile(r'Already\sassociated\sto\s(.*)\.\sBailing') + RE_WIFI_PARAMETERS = re.compile( + r'\[ssid=(.*?), bssid=(.*?), security=(.*?), rssi=') + + # Define how a log line should look like. + WIFI_LINE = ( + text_parser.PyparsingConstants.MONTH.setResultsName('day_of_week') + + text_parser.PyparsingConstants.MONTH.setResultsName('month') + + text_parser.PyparsingConstants.ONE_OR_TWO_DIGITS.setResultsName('day') + + text_parser.PyparsingConstants.TIME_MSEC.setResultsName('time') + + pyparsing.Literal(u'<') + + pyparsing.CharsNotIn(u'>').setResultsName('agent') + + pyparsing.Literal(u'>') + + pyparsing.CharsNotIn(u':').setResultsName('function') + + pyparsing.Literal(u':') + + pyparsing.SkipTo(pyparsing.lineEnd).setResultsName('text')) + + WIFI_HEADER = ( + text_parser.PyparsingConstants.MONTH.setResultsName('day_of_week') + + text_parser.PyparsingConstants.MONTH.setResultsName('month') + + text_parser.PyparsingConstants.ONE_OR_TWO_DIGITS.setResultsName('day') + + text_parser.PyparsingConstants.TIME_MSEC.setResultsName('time') + + pyparsing.Literal(u'***Starting Up***')) + + # Define the available log line structures. + LINE_STRUCTURES = [ + ('logline', WIFI_LINE), + ('header', WIFI_HEADER)] + + def __init__(self): + """Initializes a parser object.""" + super(MacWifiLogParser, self).__init__() + self._year_use = 0 + self._last_month = None + + def _GetAction(self, agent, function, text): + """Parse the well know actions for easy reading. + + Args: + agent: The device that generate the entry. + function: The function or action called by the agent. + text: Mac Wifi log text. + + Returns: + know_action: A formatted string representing the known (or common) action. + """ + if not agent.startswith('airportd'): + return text + + if 'airportdProcessDLILEvent' in function: + interface = text.split()[0] + return u'Interface {0:s} turn up.'.format(interface) + + if 'doAutoJoin' in function: + match = re.match(self.RE_CONNECTED, text) + if match: + ssid = match.group(1)[1:-1] + else: + ssid = 'Unknown' + return u'Wifi connected to SSID {0:s}'.format(ssid) + + if 'processSystemPSKAssoc' in function: + wifi_parameters = self.RE_WIFI_PARAMETERS.search(text) + if wifi_parameters: + ssid = wifi_parameters.group(1) + bssid = wifi_parameters.group(2) + security = wifi_parameters.group(3) + if not ssid: + ssid = 'Unknown' + if not bssid: + bssid = 'Unknown' + if not security: + security = 'Unknown' + return ( + u'New wifi configured. BSSID: {0:s}, SSID: {1:s}, ' + u'Security: {2:s}.').format(bssid, ssid, security) + return text + + def _GetTimestamp(self, day, month, year, time): + """Gets a timestamp from a pyparsing ParseResults timestamp. + + This is a timestamp_string as returned by using + text_parser.PyparsingConstants structures: + 08, Nov, [20, 36, 37], 222] + + Args: + timestamp_string: The pyparsing ParseResults object + + Returns: + day: An integer representing the day. + month: An integer representing the month. + year: An integer representing the year. + timestamp: A plaso timelib timestamp event or 0. + """ + try: + time_part, millisecond = time + hour, minute, second = time_part + timestamp = timelib.Timestamp.FromTimeParts( + year, month, day, hour, minute, second, + microseconds=(millisecond * 1000)) + except ValueError: + timestamp = 0 + return timestamp + + def _GetYear(self, stat, zone): + """Retrieves the year either from the input file or from the settings.""" + time = getattr(stat, 'crtime', 0) + if not time: + time = getattr(stat, 'ctime', 0) + + if not time: + logging.error( + ('Unable to determine correct year of syslog file, using current ' + 'year')) + return timelib.GetCurrentYear() + + try: + timestamp = datetime.datetime.fromtimestamp(time, zone) + except ValueError as exception: + logging.error(( + u'Unable to determine correct year of syslog file, using current ' + u'one, with error: {0:s}').format(exception)) + return timelib.GetCurrentYear() + return timestamp.year + + def _ParseLogLine(self, parser_context, structure): + """Parse a logline and store appropriate attributes. + + Args: + parser_context: A parser context object (instance of ParserContext). + structure: A pyparsing.ParseResults object from a line in the + log file. + + Returns: + An event object (instance of EventObject) or None. + """ + # TODO: improving this to get a valid year. + if not self._year_use: + self._year_use = parser_context.year + + if not self._year_use: + # Get from the creation time of the file. + self._year_use = self._GetYear( + self.file_entry.GetStat(), parser_context.timezone) + # If fail, get from the current time. + if not self._year_use: + self._year_use = timelib.GetCurrentYear() + + # Gap detected between years. + month = timelib.MONTH_DICT.get(structure.month.lower()) + if not self._last_month: + self._last_month = month + if month < self._last_month: + self._year_use += 1 + timestamp = self._GetTimestamp( + structure.day, + month, + self._year_use, + structure.time) + if not timestamp: + logging.debug(u'Invalid timestamp {0:s}'.format(structure.timestamp)) + return + self._last_month = month + + text = structure.text + + # Due to the use of CharsNotIn pyparsing structure contains whitespaces + # that need to be removed. + function = structure.function.strip() + action = self._GetAction(structure.agent, function, text) + return MacWifiLogEvent( + timestamp, structure.agent, function, text, action) + + def ParseRecord(self, parser_context, key, structure): + """Parse each record structure and return an EventObject if applicable. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: An identification string indicating the name of the parsed + structure. + structure: A pyparsing.ParseResults object from a line in the + log file. + + Returns: + An event object (instance of EventObject) or None. + """ + if key == 'logline': + return self._ParseLogLine(parser_context, structure) + elif key != 'header': + logging.warning( + u'Unable to parse record, unknown structure: {0:s}'.format(key)) + + def VerifyStructure(self, parser_context, line): + """Verify that this file is a Mac Wifi log file. + + Args: + parser_context: A parser context object (instance of ParserContext). + line: A single line from the text file. + + Returns: + True if this is the correct parser, False otherwise. + """ + try: + _ = self.WIFI_HEADER.parseString(line) + except pyparsing.ParseException: + logging.debug(u'Not a Mac Wifi log file') + return False + return True + + +manager.ParsersManager.RegisterParser(MacWifiLogParser) diff --git a/plaso/parsers/mac_wifi_test.py b/plaso/parsers/mac_wifi_test.py new file mode 100644 index 0000000..762c233 --- /dev/null +++ b/plaso/parsers/mac_wifi_test.py @@ -0,0 +1,134 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Mac wifi.log parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import mac_wifi as mac_wifi_formatter +from plaso.lib import timelib_test +from plaso.parsers import mac_wifi +from plaso.parsers import test_lib + + +class MacWifiUnitTest(test_lib.ParserTestCase): + """Tests for the Mac wifi.log parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = mac_wifi.MacWifiLogParser() + + def testParse(self): + """Tests the Parse function.""" + knowledge_base_values = {'year': 2013} + test_file = self._GetTestFilePath(['wifi.log']) + event_queue_consumer = self._ParseFile( + self._parser, test_file, knowledge_base_values=knowledge_base_values) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEqual(len(event_objects), 9) + + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-14 20:36:37.222') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.agent, u'airportd[88]') + self.assertEqual(event_object.function, u'airportdProcessDLILEvent') + self.assertEqual(event_object.action, u'Interface en0 turn up.') + self.assertEqual(event_object.text, u'en0 attached (up)') + + expected_msg = ( + u'Action: Interface en0 turn up. ' + u'(airportdProcessDLILEvent) ' + u'Log: en0 attached (up)') + expected_msg_short = ( + u'Action: Interface en0 turn up.') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + event_object = event_objects[1] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-14 20:36:43.818') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.agent, u'airportd[88]') + self.assertEqual(event_object.function, u'_doAutoJoin') + self.assertEqual(event_object.action, u'Wifi connected to SSID CampusNet') + + expected_text = ( + u'Already associated to \u201cCampusNet\u201d. Bailing on auto-join.') + self.assertEqual(event_object.text, expected_text) + + event_object = event_objects[2] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-14 21:50:52.395') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.agent, u'airportd[88]') + self.assertEqual(event_object.function, u'_handleLinkEvent') + + expected_string = ( + u'Unable to process link event, op mode request returned -3903 ' + u'(Operation not supported)') + + self.assertEqual(event_object.action, expected_string) + self.assertEqual(event_object.text, expected_string) + + event_object = event_objects[5] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-14 21:52:09.883') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(u'airportd[88]', event_object.agent) + self.assertEqual(u'_processSystemPSKAssoc', event_object.function) + + expected_action = ( + u'New wifi configured. BSSID: 88:30:8a:7a:61:88, SSID: AndroidAP, ' + u'Security: WPA2 Personal.') + + self.assertEqual(event_object.action, expected_action) + + expected_text = ( + u'No password for network ' + u'[ssid=AndroidAP, bssid=88:30:8a:7a:61:88, security=WPA2 ' + u'Personal, rssi=-21, channel= ' + u'[channelNumber=11(2GHz), channelWidth={20MHz}], ibss=0] ' + u'in the system keychain') + + self.assertEqual(event_object.text, expected_text) + + event_object = event_objects[7] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-12-31 23:59:38.165') + self.assertEqual(event_object.timestamp, expected_timestamp) + + event_object = event_objects[8] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2014-01-01 01:12:17.311') + self.assertEqual(event_object.timestamp, expected_timestamp) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/mactime.py b/plaso/parsers/mactime.py new file mode 100644 index 0000000..9a2a882 --- /dev/null +++ b/plaso/parsers/mactime.py @@ -0,0 +1,153 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for the Sleuthkit (TSK) bodyfile or mactime format. + +The format specifications can be read here: + http://wiki.sleuthkit.org/index.php?title=Body_file +""" + +import re + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.parsers import manager +from plaso.parsers import text_parser + + +class MactimeEvent(time_events.PosixTimeEvent): + """Convenience class for a mactime-based event.""" + + DATA_TYPE = 'fs:mactime:line' + + def __init__(self, posix_time, usage, row_offset, data): + """Initializes a mactime-based event object. + + Args: + posix_time: The POSIX time value. + usage: The description of the usage of the time value. + row_offset: The offset of the row. + data: A dict object containing extracted data from the body file. + """ + super(MactimeEvent, self).__init__(posix_time, usage) + self.offset = row_offset + self.user_sid = unicode(data.get('uid', u'')) + self.user_gid = data.get('gid', None) + self.md5 = data.get('md5', None) + self.filename = data.get('name', 'N/A') + # Check if the filename field is not a string, eg in the instances where a + # filename only conists of numbers. In that case the self.filename field + # becomes an integer value instead of a string value. That causes issues + # later in the process, where we expect the filename value to be a string. + if not isinstance(self.filename, basestring): + self.filename = unicode(self.filename) + + self.mode_as_string = data.get('mode_as_string', None) + self.size = data.get('size', None) + + inode_number = data.get('inode', 0) + if isinstance(inode_number, basestring): + if '-' in inode_number: + inode_number, _, _ = inode_number.partition('-') + + try: + inode_number = int(inode_number, 10) + except ValueError: + inode_number = 0 + + self.inode = inode_number + + +class MactimeParser(text_parser.TextCSVParser): + """Parses SleuthKit's mactime bodyfiles.""" + + NAME = 'mactime' + DESCRIPTION = u'Parser for SleuthKit\'s mactime bodyfiles.' + + COLUMNS = [ + 'md5', 'name', 'inode', 'mode_as_string', 'uid', 'gid', 'size', + 'atime', 'mtime', 'ctime', 'crtime'] + VALUE_SEPARATOR = '|' + + MD5_RE = re.compile('^[0-9a-fA-F]+$') + + _TIMESTAMP_DESC_MAP = { + 'atime': eventdata.EventTimestamp.ACCESS_TIME, + 'crtime': eventdata.EventTimestamp.CREATION_TIME, + 'ctime': eventdata.EventTimestamp.CHANGE_TIME, + 'mtime': eventdata.EventTimestamp.MODIFICATION_TIME, + } + + def VerifyRow(self, unused_parser_context, row): + """Verify we are dealing with a mactime bodyfile. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: A single row from the CSV file. + + Returns: + True if this is the correct parser, False otherwise. + """ + if not self.MD5_RE.match(row['md5']): + return False + + try: + # Verify that the "size" field is an integer, thus cast it to int + # and then back to string so it can be compared, if the value is + # not a string representation of an integer, eg: '12a' then this + # conversion will fail and we return a False value. + if str(int(row.get('size', '0'), 10)) != row.get('size', None): + return False + except ValueError: + return False + + # TODO: Add additional verification. + return True + + def ParseRow( + self, parser_context, row_offset, row, file_entry=None, + parser_chain=None): + """Parses a row and extract event objects. + + Args: + parser_context: A parser context object (instance of ParserContext). + row_offset: The offset of the row. + row: A dictionary containing all the fields as denoted in the + COLUMNS class list. + file_entry: optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + for key, value in row.iteritems(): + if isinstance(row[key], basestring): + try: + row[key] = int(value, 10) + except ValueError: + pass + + for key, timestamp_description in self._TIMESTAMP_DESC_MAP.iteritems(): + value = row.get(key, None) + if not value: + continue + event_object = MactimeEvent( + value, timestamp_description, row_offset, row) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +manager.ParsersManager.RegisterParser(MactimeParser) diff --git a/plaso/parsers/mactime_test.py b/plaso/parsers/mactime_test.py new file mode 100644 index 0000000..c49f0ec --- /dev/null +++ b/plaso/parsers/mactime_test.py @@ -0,0 +1,105 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests the for mactime parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import mactime as mactime_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import mactime +from plaso.parsers import test_lib +from plaso.serializer import protobuf_serializer + + +class MactimeUnitTest(test_lib.ParserTestCase): + """Tests the for mactime parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = mactime.MactimeParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['mactime.body']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # The file contains 13 lines x 4 timestamps per line, which should be + # 52 events in total. However several of these events have an empty + # timestamp value and are omitted. + # Total entries: 11 * 3 + 2 * 4 = 41 + self.assertEquals(len(event_objects), 41) + + # Test this entry: + # 0|/a_directory/another_file|16|r/rrw-------|151107|5000|22|1337961583| + # 1337961584|1337961585|0 + event_object = event_objects[6] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + u'2012-05-25 15:59:43+00:00') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.ACCESS_TIME) + self.assertEquals(event_object.inode, 16) + + event_object = event_objects[6] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + u'2012-05-25 15:59:43+00:00') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.ACCESS_TIME) + + expected_string = u'/a_directory/another_file' + self._TestGetMessageStrings(event_object, expected_string, expected_string) + + event_object = event_objects[8] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + u'2012-05-25 15:59:44+00:00') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.MODIFICATION_TIME) + + event_object = event_objects[7] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + u'2012-05-25 15:59:45+00:00') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.CHANGE_TIME) + self.assertEquals(event_object.filename, u'/a_directory/another_file') + self.assertEquals(event_object.mode_as_string, u'r/rrw-------') + + event_object = event_objects[37] + + self.assertEquals(event_object.inode, 4) + + # Serialize the event objects. + serialized_events = [] + serializer = protobuf_serializer.ProtobufEventObjectSerializer + for event_object in event_objects: + serialized_events.append(serializer.WriteSerialized(event_object)) + + self.assertEquals(len(serialized_events), len(event_objects)) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/manager.py b/plaso/parsers/manager.py new file mode 100644 index 0000000..976cf2b --- /dev/null +++ b/plaso/parsers/manager.py @@ -0,0 +1,205 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The parsers and plugins manager objects.""" + +from plaso.frontend import presets + + +class ParsersManager(object): + """Class that implements the parsers manager.""" + + _parser_classes = {} + + @classmethod + def DeregisterParser(cls, parser_class): + """Deregisters a parser class. + + The parser classes are identified based on their lower case name. + + Args: + parser_class: the class object of the parser. + + Raises: + KeyError: if parser class is not set for the corresponding name. + """ + parser_name = parser_class.NAME.lower() + if parser_name not in cls._parser_classes: + raise KeyError( + u'Parser class not set for name: {0:s}.'.format( + parser_class.NAME)) + + del cls._parser_classes[parser_name] + + @classmethod + def GetFilterListsFromString(cls, parser_filter_string): + """Determines an include and exclude list of parser and plugin names. + + Takes a comma separated string and splits it up into two lists, + of parsers or plugins to include and to exclude from selection. + If a particular filter is prepended with a minus sign it will + be included in the exclude section, otherwise in the include. + + Args: + parser_filter_string: The parser filter string. + + Returns: + A tuple of two lists, include and exclude. + """ + includes = [] + excludes = [] + + preset_categories = presets.categories.keys() + + for filter_string in parser_filter_string.split(','): + filter_string = filter_string.strip() + if not filter_string: + continue + + if filter_string.startswith('-'): + active_list = excludes + filter_string = filter_string[1:] + else: + active_list = includes + + filter_string = filter_string.lower() + if filter_string in cls._parser_classes: + parser_class = cls._parser_classes[filter_string] + active_list.append(filter_string) + + if parser_class.SupportsPlugins(): + active_list.extend(parser_class.GetPluginNames()) + + elif filter_string in preset_categories: + active_list.extend( + presets.GetParsersFromCategory(filter_string)) + + else: + active_list.append(filter_string) + + return includes, excludes + + @classmethod + def GetParserNames(cls, parser_filter_string=None): + """Retrieves the parser names. + + Args: + parser_filter_string: Optional parser filter string. The default is None. + + Returns: + A list of parser names. + """ + parser_names = [] + + for parser_name, _ in cls.GetParsers( + parser_filter_string=parser_filter_string): + parser_names.append(parser_name) + + return parser_names + + @classmethod + def GetParserObjects(cls, parser_filter_string=None): + """Retrieves the parser objects. + + Args: + parser_filter_string: Optional parser filter string. The default is None. + + Returns: + A list of parser objects (instances of BaseParser). + """ + parser_objects = [] + + for _, parser_class in cls.GetParsers( + parser_filter_string=parser_filter_string): + parser_object = parser_class() + parser_objects.append(parser_object) + + return parser_objects + + @classmethod + def GetParsers(cls, parser_filter_string=None): + """Retrieves the registered parsers. + + Args: + parser_filter_string: Optional parser filter string. The default is None. + + Yields: + A tuple that contains the uniquely identifying name of the parser + and the parser class (subclass of BaseParser). + """ + if parser_filter_string: + includes, excludes = cls.GetFilterListsFromString(parser_filter_string) + else: + includes = None + excludes = None + + for parser_name, parser_class in cls._parser_classes.iteritems(): + if excludes and parser_name in excludes: + continue + + if includes and parser_name not in includes: + continue + + yield parser_name, parser_class + + @classmethod + def GetWindowsRegistryPlugins(cls): + """Build a list of all available Windows Registry plugins. + + Returns: + A plugins list (instance of PluginList). + """ + parser_class = cls._parser_classes.get('winreg', None) + if not parser_class: + return + + return parser_class.GetPluginList() + + @classmethod + def RegisterParser(cls, parser_class): + """Registers a parser class. + + The parser classes are identified based on their lower case name. + + Args: + parser_class: the class object of the parser. + + Raises: + KeyError: if parser class is already set for the corresponding name. + """ + parser_name = parser_class.NAME.lower() + if parser_name in cls._parser_classes: + raise KeyError(( + u'Parser class already set for name: {0:s}.').format( + parser_class.NAME)) + + cls._parser_classes[parser_name] = parser_class + + @classmethod + def RegisterParsers(cls, parser_classes): + """Registers parser classes. + + The parser classes are identified based on their lower case name. + + Args: + parser_classes: a list of class objects of the parsers. + + Raises: + KeyError: if parser class is already set for the corresponding name. + """ + for parser_class in parser_classes: + cls.RegisterParser(parser_class) diff --git a/plaso/parsers/manager_test.py b/plaso/parsers/manager_test.py new file mode 100644 index 0000000..755cd61 --- /dev/null +++ b/plaso/parsers/manager_test.py @@ -0,0 +1,152 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the parsers manager.""" + +import unittest + +from plaso.parsers import interface +from plaso.parsers import manager +from plaso.parsers import plugins + + +class TestParser(interface.BaseParser): + """Test parser.""" + + NAME = 'test_parser' + DESCRIPTION = u'Test parser.' + + def Parse(self, unused_parser_context, unused_file_entry, parser_chain=None): + """Parsers the file entry and extracts event objects. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + return + + +class TestParserWithPlugins(interface.BasePluginsParser): + """Test parser with plugins.""" + + NAME = 'test_parser_with_plugins' + DESCRIPTION = u'Test parser with plugins.' + + _plugin_classes = {} + + def Parse(self, unused_parser_context, unused_file_entry, parser_chain=None): + """Parsers the file entry and extracts event objects. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + return + + +class TestPlugin(plugins.BasePlugin): + """Test plugin.""" + + NAME = 'test_plugin' + DESCRIPTION = u'Test plugin.' + + def Process(self, unused_parser_context, unused_parser_chain=None, **kwargs): + """Evaluates if this is the correct plugin and processes data accordingly. + + Args: + parser_context: A parser context object (instance of ParserContext). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + kwargs: Depending on the plugin they may require different sets of + arguments to be able to evaluate whether or not this is + the correct plugin. + + Raises: + ValueError: When there are unused keyword arguments. + """ + return + + +class ParsersManagerTest(unittest.TestCase): + """Tests for the parsers manager.""" + + def testParserRegistration(self): + """Tests the RegisterParser and DeregisterParser functions.""" + # pylint: disable=protected-access + number_of_parsers = len(manager.ParsersManager._parser_classes) + + manager.ParsersManager.RegisterParser(TestParser) + self.assertEquals( + len(manager.ParsersManager._parser_classes), + number_of_parsers + 1) + + with self.assertRaises(KeyError): + manager.ParsersManager.RegisterParser(TestParser) + + manager.ParsersManager.DeregisterParser(TestParser) + self.assertEquals( + len(manager.ParsersManager._parser_classes), + number_of_parsers) + + def testPluginRegistration(self): + """Tests the RegisterPlugin and DeregisterPlugin functions.""" + TestParserWithPlugins.RegisterPlugin(TestPlugin) + # pylint: disable=protected-access + self.assertEquals( + len(TestParserWithPlugins._plugin_classes), 1) + + with self.assertRaises(KeyError): + TestParserWithPlugins.RegisterPlugin(TestPlugin) + + TestParserWithPlugins.DeregisterPlugin(TestPlugin) + self.assertEquals( + len(TestParserWithPlugins._plugin_classes), 0) + + def testGetFilterListsFromString(self): + """Tests the GetFilterListsFromString function.""" + TestParserWithPlugins.RegisterPlugin(TestPlugin) + manager.ParsersManager.RegisterParser(TestParserWithPlugins) + manager.ParsersManager.RegisterParser(TestParser) + + includes, excludes = manager.ParsersManager.GetFilterListsFromString( + 'test_parser') + + self.assertEquals(includes, ['test_parser']) + self.assertEquals(excludes, []) + + includes, excludes = manager.ParsersManager.GetFilterListsFromString( + '-test_parser') + + self.assertEquals(includes, []) + self.assertEquals(excludes, ['test_parser']) + + includes, excludes = manager.ParsersManager.GetFilterListsFromString( + 'test_parser_with_plugins') + + self.assertEquals(includes, ['test_parser_with_plugins', 'test_plugin']) + + TestParserWithPlugins.DeregisterPlugin(TestPlugin) + manager.ParsersManager.DeregisterParser(TestParserWithPlugins) + manager.ParsersManager.DeregisterParser(TestParser) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/mcafeeav.py b/plaso/parsers/mcafeeav.py new file mode 100644 index 0000000..fd29122 --- /dev/null +++ b/plaso/parsers/mcafeeav.py @@ -0,0 +1,141 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for McAfee Anti-Virus Logs. + +McAfee AV uses 4 logs to track when scans were run, when virus databases were +updated, and when files match the virus database.""" + +import logging + +from plaso.events import text_events +from plaso.lib import timelib +from plaso.parsers import manager +from plaso.parsers import text_parser + + +class McafeeAVEvent(text_events.TextEvent): + """Convenience class for McAfee AV Log events """ + DATA_TYPE = 'av:mcafee:accessprotectionlog' + + def __init__(self, timestamp, offset, attributes): + """Initializes a McAfee AV Log Event. + + Args: + timestamp: The timestamp time value. The timestamp contains the + number of seconds since Jan 1, 1970 00:00:00 UTC. + offset: The offset of the attributes. + attributes: Dict of elements from the AV log line. + """ + del attributes['time'] + del attributes['date'] + super(McafeeAVEvent, self).__init__(timestamp, offset, attributes) + self.full_path = attributes['filename'] + + +class McafeeAccessProtectionParser(text_parser.TextCSVParser): + """Parses the McAfee AV Access Protection Log.""" + + NAME = 'mcafee_protection' + DESCRIPTION = u'Parser for McAfee AV Access Protection log files.' + + VALUE_SEPARATOR = '\t' + # Define the columns of the McAfee AV Access Protection Log. + COLUMNS = ['date', 'time', 'status', 'username', 'filename', + 'trigger_location', 'rule', 'action'] + + def _GetTimestamp(self, date, time, timezone): + """Return a 64-bit signed timestamp in microseconds since Epoch. + + The timestamp is made up of two strings, the date and the time, separated + by a tab. The time is in local time. The month and day can be either 1 or 2 + characters long. E.g.: 7/30/2013\t10:22:48 AM + + Args: + date: The string representing the date. + time: The string representing the time. + timezone: The timezone object. + + Returns: + A plaso timestamp value, microseconds since Epoch in UTC or None. + """ + + if not (date and time): + logging.warning('Unable to extract timestamp from McAfee AV logline.') + return + + # TODO: Figure out how McAfee sets Day First and use that here. + # The in-file time format is '07/30/2013\t10:22:48 AM'. + return timelib.Timestamp.FromTimeString( + u'{0:s} {1:s}'.format(date, time), timezone=timezone) + + def VerifyRow(self, parser_context, row): + """Verify that this is a McAfee AV Access Protection Log file. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: A single row from the CSV file. + + Returns: + True if this is the correct parser, False otherwise. + """ + if len(row) != 8: + return False + + # This file can have a UTF-8 byte-order-marker at the beginning of + # the first row. + # TODO: Find out all the code pages this can have. Asked McAfee 10/31. + if row['date'][0:3] == '\xef\xbb\xbf': + row['date'] = row['date'][3:] + + # Check the date format! + # If it doesn't pass, then this isn't a McAfee AV Access Protection Log + try: + self._GetTimestamp(row['date'], row['time'], parser_context.timezone) + except (TypeError, ValueError): + return False + + # Use the presence of these strings as a backup or in case of partial file. + if (not 'Access Protection' in row['status'] and + not 'Would be blocked' in row['status']): + return False + + return True + + def ParseRow( + self, parser_context, row_offset, row, file_entry=None, + parser_chain=None): + """Parses a row and extract event objects. + + Args: + parser_context: A parser context object (instance of ParserContext). + row_offset: The offset of the row. + row: A dictionary containing all the fields as denoted in the + COLUMNS class list. + file_entry: optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + timestamp = self._GetTimestamp( + row['date'], row['time'], parser_context.timezone) + event_object = McafeeAVEvent(timestamp, row_offset, row) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +manager.ParsersManager.RegisterParser(McafeeAccessProtectionParser) diff --git a/plaso/parsers/mcafeeav_test.py b/plaso/parsers/mcafeeav_test.py new file mode 100644 index 0000000..24cb137 --- /dev/null +++ b/plaso/parsers/mcafeeav_test.py @@ -0,0 +1,79 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the McAfee AV Log parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import mcafeeav as mcafeeav_formatter +from plaso.parsers import mcafeeav +from plaso.parsers import test_lib + + +class McafeeAccessProtectionUnitTest(test_lib.ParserTestCase): + """Tests for the McAfee AV Log parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = mcafeeav.McafeeAccessProtectionParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['AccessProtectionLog.txt']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # The file contains 14 lines which results in 14 event objects. + self.assertEquals(len(event_objects), 14) + + # Test that the UTF-8 byte order mark gets removed from the first line. + event_object = event_objects[0] + + self.assertEquals(event_object.timestamp, 1380292946000000) + + # Test this entry: + # 9/27/2013 2:42:26 PM Blocked by Access Protection rule + # SOMEDOMAIN\someUser C:\Windows\System32\procexp64.exe C:\Program Files + # (x86)\McAfee\Common Framework\UdaterUI.exe Common Standard + # Protection:Prevent termination of McAfee processes Action blocked : + # Terminate + + event_object = event_objects[1] + + self.assertEquals(event_object.timestamp, 1380292959000000) + self.assertEquals(event_object.username, u'SOMEDOMAIN\\someUser') + self.assertEquals( + event_object.full_path, u'C:\\Windows\\System32\\procexp64.exe') + + expected_msg = ( + u'File Name: C:\\Windows\\System32\\procexp64.exe ' + u'User: SOMEDOMAIN\\someUser ' + u'C:\\Program Files (x86)\\McAfee\\Common Framework\\Frame' + u'workService.exe ' + u'Blocked by Access Protection rule ' + u'Common Standard Protection:Prevent termination of McAfee processes ' + u'Action blocked : Terminate') + expected_msg_short = ( + u'C:\\Windows\\System32\\procexp64.exe ' + u'Action blocked : Terminate') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/msiecf.py b/plaso/parsers/msiecf.py new file mode 100644 index 0000000..45224cb --- /dev/null +++ b/plaso/parsers/msiecf.py @@ -0,0 +1,233 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for Microsoft Internet Explorer (MSIE) Cache Files (CF).""" + +import logging + +import pymsiecf + +from plaso.events import time_events +from plaso.lib import errors +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import interface +from plaso.parsers import manager + + +if pymsiecf.get_version() < '20130317': + raise ImportWarning(u'MsiecfParser requires at least pymsiecf 20130317.') + + +class MsiecfUrlEvent(time_events.TimestampEvent): + """Convenience class for an MSIECF URL event.""" + + DATA_TYPE = 'msiecf:url' + + def __init__( + self, timestamp, timestamp_description, msiecf_item, recovered=False): + """Initializes the event. + + Args: + timestamp: The timestamp value. + timestamp_desc: The usage string describing the timestamp. + msiecf_item: The MSIECF item (pymsiecf.url). + recovered: Boolean value to indicate the item was recovered, False + by default. + """ + super(MsiecfUrlEvent, self).__init__(timestamp, timestamp_description) + + self.recovered = recovered + self.offset = msiecf_item.offset + + self.url = msiecf_item.location + self.number_of_hits = msiecf_item.number_of_hits + self.cache_directory_index = msiecf_item.cache_directory_index + self.filename = msiecf_item.filename + self.cached_file_size = msiecf_item.cached_file_size + + if msiecf_item.type and msiecf_item.data: + if msiecf_item.type == u'cache': + if msiecf_item.data[:4] == 'HTTP': + self.http_headers = msiecf_item.data[:-1] + # TODO: parse data of other URL item type like history which requires + # OLE VT parsing. + + +class MsiecfParser(interface.BaseParser): + """Parses MSIE Cache Files (MSIECF).""" + + NAME = 'msiecf' + DESCRIPTION = u'Parser for MSIE Cache Files (MSIECF) also known as index.dat.' + + def _ParseUrl( + self, parser_context, msiecf_item, file_entry=None, parser_chain=None, + recovered=False): + """Extract data from a MSIE Cache Files (MSIECF) URL item. + + Every item is stored as an event object, one for each timestamp. + + Args: + parser_context: A parser context object (instance of ParserContext). + msiecf_item: An item (pymsiecf.url). + file_entry: optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + recovered: Boolean value to indicate the item was recovered, False + by default. + """ + # The secondary timestamp can be stored in either UTC or local time + # this is dependent on what the index.dat file is used for. + # Either the file path of the location string can be used to distinguish + # between the different type of files. + primary_timestamp = timelib.Timestamp.FromFiletime( + msiecf_item.get_primary_time_as_integer()) + primary_timestamp_desc = 'Primary Time' + + # Need to convert the FILETIME to the internal timestamp here to + # do the from localtime conversion. + secondary_timestamp = timelib.Timestamp.FromFiletime( + msiecf_item.get_secondary_time_as_integer()) + secondary_timestamp_desc = 'Secondary Time' + + if msiecf_item.type: + if msiecf_item.type == u'cache': + primary_timestamp_desc = eventdata.EventTimestamp.ACCESS_TIME + secondary_timestamp_desc = eventdata.EventTimestamp.MODIFICATION_TIME + + elif msiecf_item.type == u'cookie': + primary_timestamp_desc = eventdata.EventTimestamp.ACCESS_TIME + secondary_timestamp_desc = eventdata.EventTimestamp.MODIFICATION_TIME + + elif msiecf_item.type == u'history': + primary_timestamp_desc = eventdata.EventTimestamp.LAST_VISITED_TIME + secondary_timestamp_desc = eventdata.EventTimestamp.LAST_VISITED_TIME + + elif msiecf_item.type == u'history-daily': + primary_timestamp_desc = eventdata.EventTimestamp.LAST_VISITED_TIME + secondary_timestamp_desc = eventdata.EventTimestamp.LAST_VISITED_TIME + # The secondary_timestamp is in localtime normalize it to be in UTC. + secondary_timestamp = timelib.Timestamp.LocaltimeToUTC( + secondary_timestamp, parser_context.timezone) + + elif msiecf_item.type == u'history-weekly': + primary_timestamp_desc = eventdata.EventTimestamp.CREATION_TIME + secondary_timestamp_desc = eventdata.EventTimestamp.LAST_VISITED_TIME + # The secondary_timestamp is in localtime normalize it to be in UTC. + secondary_timestamp = timelib.Timestamp.LocaltimeToUTC( + secondary_timestamp, parser_context.timezone) + + event_object = MsiecfUrlEvent( + primary_timestamp, primary_timestamp_desc, msiecf_item, recovered) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if secondary_timestamp > 0: + event_object = MsiecfUrlEvent( + secondary_timestamp, secondary_timestamp_desc, msiecf_item, + recovered) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + expiration_timestamp = msiecf_item.get_expiration_time_as_integer() + if expiration_timestamp > 0: + # The expiration time in MSIECF version 4.7 is stored as a FILETIME value + # in version 5.2 it is stored as a FAT date time value. + # Since the as_integer function returns the raw integer value we need to + # apply the right conversion here. + if self.version == u'4.7': + event_object = MsiecfUrlEvent( + timelib.Timestamp.FromFiletime(expiration_timestamp), + eventdata.EventTimestamp.EXPIRATION_TIME, msiecf_item, recovered) + else: + event_object = MsiecfUrlEvent( + timelib.Timestamp.FromFatDateTime(expiration_timestamp), + eventdata.EventTimestamp.EXPIRATION_TIME, msiecf_item, recovered) + + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + last_checked_timestamp = msiecf_item.get_last_checked_time_as_integer() + if last_checked_timestamp > 0: + event_object = MsiecfUrlEvent( + timelib.Timestamp.FromFatDateTime(last_checked_timestamp), + eventdata.EventTimestamp.LAST_CHECKED_TIME, msiecf_item, recovered) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract data from a MSIE Cache File (MSIECF). + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + file_object = file_entry.GetFileObject() + msiecf_file = pymsiecf.file() + msiecf_file.set_ascii_codepage(parser_context.codepage) + + try: + msiecf_file.open_file_object(file_object) + + self.version = msiecf_file.format_version + except IOError as exception: + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse file {1:s}: {2:s}'.format( + self.NAME, file_entry.name, exception)) + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + for item_index in range(0, msiecf_file.number_of_items): + try: + msiecf_item = msiecf_file.get_item(item_index) + if isinstance(msiecf_item, pymsiecf.url): + self._ParseUrl( + parser_context, msiecf_item, file_entry=file_entry, + parser_chain=parser_chain) + + # TODO: implement support for pymsiecf.leak, pymsiecf.redirected, + # pymsiecf.item. + except IOError as exception: + logging.warning( + u'[{0:s}] unable to parse item: {1:d} in file: {2:s}: {3:s}'.format( + self.NAME, item_index, file_entry.name, exception)) + + for item_index in range(0, msiecf_file.number_of_recovered_items): + try: + msiecf_item = msiecf_file.get_recovered_item(item_index) + if isinstance(msiecf_item, pymsiecf.url): + self._ParseUrl( + parser_context, msiecf_item, file_entry=file_entry, + parser_chain=parser_chain, recovered=True) + + # TODO: implement support for pymsiecf.leak, pymsiecf.redirected, + # pymsiecf.item. + except IOError as exception: + logging.info(( + u'[{0:s}] unable to parse recovered item: {1:d} in file: {2:s}: ' + u'{3:s}').format( + self.NAME, item_index, file_entry.name, exception)) + + file_object.close() + + +manager.ParsersManager.RegisterParser(MsiecfParser) diff --git a/plaso/parsers/msiecf_test.py b/plaso/parsers/msiecf_test.py new file mode 100644 index 0000000..00f8e07 --- /dev/null +++ b/plaso/parsers/msiecf_test.py @@ -0,0 +1,113 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Microsoft Internet Explorer (MSIE) Cache Files (CF) parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import msiecf as msiecf_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import msiecf +from plaso.parsers import test_lib + + +class MsiecfParserTest(test_lib.ParserTestCase): + """Tests for the MSIE Cache Files (MSIECF) parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = msiecf.MsiecfParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['index.dat']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # MSIE Cache File information: + # File size: 32768 bytes + # Number of items: 7 + # Number of recovered items: 11 + # 7 + 11 records, each with 4 records. + + self.assertEquals(len(event_objects), (7 + 11) * 4) + + # Record type : URL + # Offset range : 21376 - 21632 (256) + # Location : Visited: testing@http://www.trafficfusionx.com + # /download/tfscrn2/funnycats.exe + # Primary time : Jun 23, 2011 18:02:10.066000000 + # Secondary time : Jun 23, 2011 18:02:10.066000000 + # Expiration time : Jun 29, 2011 17:55:02 + # Last checked time : Jun 23, 2011 18:02:12 + # Cache directory index : -2 (0xfe) + + event_object = event_objects[8] + expected_location = ( + u'Visited: testing@http://www.trafficfusionx.com/download/tfscrn2' + u'/funnycats.exe') + + self.assertEquals(event_object.offset, 21376) + self.assertEquals(event_object.url, expected_location) + self.assertEquals(event_object.cache_directory_index, -2) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-06-23 18:02:10.066') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.LAST_VISITED_TIME) + + event_object = event_objects[9] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-06-23 18:02:10.066') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.LAST_VISITED_TIME) + + event_object = event_objects[10] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-06-29 17:55:02') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.EXPIRATION_TIME) + + event_object = event_objects[11] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-06-23 18:02:12') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.LAST_CHECKED_TIME) + + expected_msg = ( + u'Location: Visited: testing@http://www.trafficfusionx.com/download' + u'/tfscrn2/funnycats.exe ' + u'Number of hits: 6 ' + u'Cached file size: 0') + expected_msg_short = ( + u'Location: Visited: testing@http://www.trafficfusionx.com/download' + u'/tfscrn2/fun...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/olecf.py b/plaso/parsers/olecf.py new file mode 100644 index 0000000..be553c2 --- /dev/null +++ b/plaso/parsers/olecf.py @@ -0,0 +1,109 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for OLE Compound Files (OLECF).""" + +import logging + +import pyolecf + +from plaso.lib import errors +from plaso.parsers import interface +from plaso.parsers import manager + + +if pyolecf.get_version() < '20131012': + raise ImportWarning('OleCfParser requires at least pyolecf 20131012.') + + +class OleCfParser(interface.BasePluginsParser): + """Parses OLE Compound Files (OLECF).""" + + NAME = 'olecf' + DESCRIPTION = u'Parser for OLE Compound Files (OLECF).' + + _plugin_classes = {} + + def __init__(self): + """Initializes a parser object.""" + super(OleCfParser, self).__init__() + self._plugins = OleCfParser.GetPluginObjects() + + for list_index, plugin_object in enumerate(self._plugins): + if plugin_object.NAME == 'olecf_default': + self._default_plugin = self._plugins.pop(list_index) + break + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extracts data from an OLE Compound File (OLECF). + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Raises: + UnableToParseFile: when the file cannot be parsed. + """ + file_object = file_entry.GetFileObject() + olecf_file = pyolecf.file() + olecf_file.set_ascii_codepage(parser_context.codepage) + + try: + olecf_file.open_file_object(file_object) + except IOError as exception: + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse file {1:s}: {2:s}'.format( + self.NAME, file_entry.name, exception)) + + # Get a list of all root items from the OLE CF file. + root_item = olecf_file.root_item + item_names = [item.name for item in root_item.sub_items] + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + # Compare the list of available plugins. + # We will try to use every plugin against the file (except + # the default plugin) and run it. Only if none of the plugins + # works will we use the default plugin. + parsed = False + for plugin_object in self._plugins: + try: + plugin_object.Process( + parser_context, file_entry=file_entry, parser_chain=parser_chain, + root_item=root_item, item_names=item_names) + + except errors.WrongPlugin: + logging.debug( + u'[{0:s}] plugin: {1:s} cannot parse the OLECF file: {2:s}'.format( + self.NAME, plugin_object.NAME, file_entry.name)) + + # Check if we still haven't parsed the file, and if so we will use + # the default OLECF plugin. + if not parsed and self._default_plugin: + self._default_plugin.Process( + parser_context, file_entry=file_entry, parser_chain=parser_chain, + root_item=root_item, item_names=item_names) + + olecf_file.close() + file_object.close() + + +manager.ParsersManager.RegisterParser(OleCfParser) diff --git a/plaso/parsers/olecf_plugins/__init__.py b/plaso/parsers/olecf_plugins/__init__.py new file mode 100644 index 0000000..cf95c3e --- /dev/null +++ b/plaso/parsers/olecf_plugins/__init__.py @@ -0,0 +1,22 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains an import statement for each OLECF plugin.""" + +from plaso.parsers.olecf_plugins import automatic_destinations +from plaso.parsers.olecf_plugins import default +from plaso.parsers.olecf_plugins import summary diff --git a/plaso/parsers/olecf_plugins/automatic_destinations.py b/plaso/parsers/olecf_plugins/automatic_destinations.py new file mode 100644 index 0000000..f109b77 --- /dev/null +++ b/plaso/parsers/olecf_plugins/automatic_destinations.py @@ -0,0 +1,204 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Plugin to parse .automaticDestinations-ms OLECF files.""" + +import logging +import re + +import construct + +from plaso.events import time_events +from plaso.lib import binary +from plaso.lib import errors +from plaso.lib import eventdata +from plaso.parsers import olecf +from plaso.parsers import winlnk +from plaso.parsers.olecf_plugins import interface + + +class AutomaticDestinationsDestListEntryEvent(time_events.FiletimeEvent): + """Convenience class for an .automaticDestinations-ms DestList entry event.""" + + DATA_TYPE = 'olecf:dest_list:entry' + + def __init__( + self, timestamp, timestamp_description, entry_offset, dest_list_entry): + """Initializes the event object. + + Args: + timestamp: The FILETIME value for the timestamp. + timestamp_description: The usage string for the timestamp value. + entry_offset: The offset of the DestList entry relative to the start of + the DestList stream. + dest_list_entry: The DestList entry (instance of construct.Struct). + """ + super(AutomaticDestinationsDestListEntryEvent, self).__init__( + timestamp, timestamp_description) + + self.offset = entry_offset + self.entry_number = dest_list_entry.entry_number + + self.hostname = binary.ByteStreamCopyToString( + dest_list_entry.hostname, codepage='ascii') + self.path = binary.Ut16StreamCopyToString(dest_list_entry.path) + self.pin_status = dest_list_entry.pin_status + + self.droid_volume_identifier = binary.ByteStreamCopyToGuid( + dest_list_entry.droid_volume_identifier) + self.droid_file_identifier = binary.ByteStreamCopyToGuid( + dest_list_entry.droid_file_identifier) + self.birth_droid_volume_identifier = binary.ByteStreamCopyToGuid( + dest_list_entry.birth_droid_volume_identifier) + self.birth_droid_file_identifier = binary.ByteStreamCopyToGuid( + dest_list_entry.birth_droid_file_identifier) + + +class AutomaticDestinationsOlecfPlugin(interface.OlecfPlugin): + """Plugin that parses an .automaticDestinations-ms OLECF file.""" + + NAME = 'olecf_automatic_destinations' + DESCRIPTION = u'Parser for *.automaticDestinations-ms OLECF files.' + + REQUIRED_ITEMS = frozenset([u'DestList']) + + _RE_LNK_ITEM_NAME = re.compile(r'^[1-9a-f][0-9a-f]*$') + + # We cannot use the parser registry here since winlnk could be disabled. + # TODO: see if there is a more elegant solution for this. + _WINLNK_PARSER = winlnk.WinLnkParser() + + _DEST_LIST_STREAM_HEADER = construct.Struct( + 'dest_list_stream_header', + construct.ULInt32('unknown1'), + construct.ULInt32('number_of_entries'), + construct.ULInt32('number_of_pinned_entries'), + construct.LFloat32('unknown2'), + construct.ULInt32('last_entry_number'), + construct.Padding(4), + construct.ULInt32('last_revision_number'), + construct.Padding(4)) + + _DEST_LIST_STREAM_HEADER_SIZE = _DEST_LIST_STREAM_HEADER.sizeof() + + # Using Construct's utf-16 encoding here will create strings with their + # end-of-string characters exposed. Instead the strings are read as + # binary strings and converted using ReadUtf16(). + _DEST_LIST_STREAM_ENTRY = construct.Struct( + 'dest_list_stream_entry', + construct.ULInt64('unknown1'), + construct.Array(16, construct.Byte('droid_volume_identifier')), + construct.Array(16, construct.Byte('droid_file_identifier')), + construct.Array(16, construct.Byte('birth_droid_volume_identifier')), + construct.Array(16, construct.Byte('birth_droid_file_identifier')), + construct.String('hostname', 16), + construct.ULInt32('entry_number'), + construct.ULInt32('unknown2'), + construct.LFloat32('unknown3'), + construct.ULInt64('last_modification_time'), + construct.ULInt32('pin_status'), + construct.ULInt16('path_size'), + construct.String('path', lambda ctx: ctx.path_size * 2)) + + def ParseDestList( + self, parser_context, file_entry=None, parser_chain=None, + olecf_item=None): + """Parses the DestList OLECF item. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + olecf_item: An optional OLECF item (instance of pyolecf.item). + """ + if not olecf_item: + return + + try: + header = self._DEST_LIST_STREAM_HEADER.parse_stream(olecf_item) + except (IOError, construct.FieldError) as exception: + raise errors.UnableToParseFile( + u'Unable to parse DestList header with error: {0:s}'.format( + exception)) + + if header.unknown1 != 1: + # TODO: add format debugging notes to parser context. + logging.debug(u'[{0:s}] unknown1 value: {1:d}.'.format( + self.NAME, header.unknown1)) + + entry_offset = olecf_item.get_offset() + while entry_offset < olecf_item.size: + try: + entry = self._DEST_LIST_STREAM_ENTRY.parse_stream(olecf_item) + except (IOError, construct.FieldError) as exception: + raise errors.UnableToParseFile( + u'Unable to parse DestList entry with error: {0:s}'.format( + exception)) + + if not entry: + break + + event_object = AutomaticDestinationsDestListEntryEvent( + entry.last_modification_time, + eventdata.EventTimestamp.MODIFICATION_TIME, entry_offset, entry) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + entry_offset = olecf_item.get_offset() + + def ParseItems( + self, parser_context, file_entry=None, parser_chain=None, root_item=None, + **unused_kwargs): + """Parses OLECF items. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + root_item: Optional root item of the OLECF file. The default is None. + + Raises: + ValueError: If the root_item is not set. + """ + if root_item is None: + raise ValueError(u'Root item not set.') + + for item in root_item.sub_items: + if item.name == u'DestList': + self.ParseDestList( + parser_context, file_entry=file_entry, parser_chain=parser_chain, + olecf_item=item) + + elif self._RE_LNK_ITEM_NAME.match(item.name): + if file_entry: + display_name = u'{0:s} # {1:s}'.format( + parser_context.GetDisplayName(file_entry), item.name) + else: + display_name = u'# {0:s}'.format(item.name) + + self._WINLNK_PARSER.ParseFileObject( + parser_context, item, file_entry=file_entry, + parser_chain=parser_chain, display_name=display_name) + + # TODO: check for trailing data? + + +olecf.OleCfParser.RegisterPlugin(AutomaticDestinationsOlecfPlugin) diff --git a/plaso/parsers/olecf_plugins/automatic_destinations_test.py b/plaso/parsers/olecf_plugins/automatic_destinations_test.py new file mode 100644 index 0000000..ae93800 --- /dev/null +++ b/plaso/parsers/olecf_plugins/automatic_destinations_test.py @@ -0,0 +1,101 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the .automaticDestinations-ms OLECF file plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import olecf as olecf_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers.olecf_plugins import automatic_destinations +from plaso.parsers.olecf_plugins import test_lib + + +class TestAutomaticDestinationsOlecfPlugin(test_lib.OleCfPluginTestCase): + """Tests for the .automaticDestinations-ms OLECF file plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = automatic_destinations.AutomaticDestinationsOlecfPlugin() + + def testProcess(self): + """Tests the Process function.""" + test_file = self._GetTestFilePath([ + u'1b4dd67f29cb1962.automaticDestinations-ms']) + event_queue_consumer = self._ParseOleCfFileWithPlugin( + test_file, self._plugin) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 44) + + # Check a AutomaticDestinationsDestListEntryEvent. + event_object = event_objects[3] + + self.assertEquals(event_object.offset, 32) + + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.MODIFICATION_TIME) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-04-01 13:52:38.997538') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'Entry: 11 ' + u'Pin status: Unpinned ' + u'Hostname: wks-win764bitb ' + u'Path: C:\\Users\\nfury\\Pictures\\The SHIELD ' + u'Droid volume identifier: {cf6619c2-66a8-44a6-8849-1582fcd3a338} ' + u'Droid file identifier: {63eea867-7b85-11e1-8950-005056a50b40} ' + u'Birth droid volume identifier: ' + u'{cf6619c2-66a8-44a6-8849-1582fcd3a338} ' + u'Birth droid file identifier: {63eea867-7b85-11e1-8950-005056a50b40}') + + expected_msg_short = ( + u'Entry: 11 ' + u'Pin status: Unpinned ' + u'Path: C:\\Users\\nfury\\Pictures\\The SHIELD') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + # Check a WinLnkLinkEvent. + event_object = event_objects[1] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2010-11-10 07:51:16.749125') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'File size: 3545 ' + u'File attribute flags: 0x00002020 ' + u'Drive type: 3 ' + u'Drive serial number: 0x24ba718b ' + u'Local path: C:\\Users\\nfury\\AppData\\Roaming\\Microsoft\\Windows\\' + u'Libraries\\Documents.library-ms ' + u'Link target: [Users Libraries, UNKNOWN: 0x00]') + + expected_msg_short = ( + u'C:\\Users\\nfury\\AppData\\Roaming\\Microsoft\\Windows\\Libraries\\' + u'Documents.library-ms') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/olecf_plugins/default.py b/plaso/parsers/olecf_plugins/default.py new file mode 100644 index 0000000..0fbd8a9 --- /dev/null +++ b/plaso/parsers/olecf_plugins/default.py @@ -0,0 +1,162 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The default plugin for parsing OLE Compound Files (OLECF).""" + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.parsers import olecf +from plaso.parsers.olecf_plugins import interface + + +class OleCfItemEvent(time_events.FiletimeEvent): + """Convenience class for an OLECF item event.""" + + DATA_TYPE = 'olecf:item' + + def __init__(self, timestamp, usage, olecf_item): + """Initializes the event. + + Args: + timestamp: The FILETIME timestamp value. + usage: A string describing the timestamp value. + olecf_item: The OLECF item (pyolecf.item). + """ + super(OleCfItemEvent, self).__init__(timestamp, usage) + + # TODO: need a better way to express the original location of the + # original data. + self.offset = 0 + + self.name = olecf_item.name + # TODO: have pyolecf return the item type here. + # self.type = olecf_item.type + self.size = olecf_item.size + + +class DefaultOleCFPlugin(interface.OlecfPlugin): + """Class to define the default OLECF file plugin.""" + + NAME = 'olecf_default' + DESCRIPTION = u'Parser for a generic OLECF item.' + + def _ParseItem( + self, parser_context, file_entry=None, parser_chain=None, + olecf_item=None): + """Parses an OLECF item. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + olecf_item: An optional OLECF item (instance of pyolecf.item). + + Returns: + A boolean value indicating if an event object was produced. + """ + event_object = None + result = False + + creation_time, modification_time = self.GetTimestamps(olecf_item) + + if creation_time: + event_object = OleCfItemEvent( + creation_time, eventdata.EventTimestamp.CREATION_TIME, + olecf_item) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if modification_time: + event_object = OleCfItemEvent( + modification_time, eventdata.EventTimestamp.MODIFICATION_TIME, + olecf_item) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if event_object: + result = True + + for sub_item in olecf_item.sub_items: + if self._ParseItem( + parser_context, file_entry=file_entry, parser_chain=parser_chain, + olecf_item=sub_item): + result = True + + return result + + def ParseItems( + self, parser_context, file_entry=None, parser_chain=None, root_item=None, + **unused_kwargs): + """Parses OLECF items. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + root_item: Optional root item of the OLECF file. The default is None. + """ + if not self._ParseItem( + parser_context, file_entry=file_entry, parser_chain=parser_chain, + olecf_item=root_item): + # If no event object was produced, produce at least one for + # the root item. + event_object = OleCfItemEvent( + 0, eventdata.EventTimestamp.CREATION_TIME, root_item) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + def Process( + self, parser_context, file_entry=None, parser_chain=None, root_item=None, + item_names=None, **kwargs): + """Determine if this is the right plugin for this OLECF file. + + This function takes a list of sub items found in the root of a + OLECF file and compares that to a list of required items defined + in this plugin. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + root_item: Optional root item of the OLECF file. The default is None. + item_names: Optional list of all items discovered in the root. + The default is None. + + Raises: + errors.WrongPlugin: If the set of required items is not a subset + of the available items. + ValueError: If the root_item or items are not set. + """ + if root_item is None or item_names is None: + raise ValueError(u'Root item or items are not set.') + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + self.ParseItems( + parser_context, file_entry=file_entry, parser_chain=parser_chain, + root_item=root_item) + + +olecf.OleCfParser.RegisterPlugin(DefaultOleCFPlugin) diff --git a/plaso/parsers/olecf_plugins/default_test.py b/plaso/parsers/olecf_plugins/default_test.py new file mode 100644 index 0000000..97b0c42 --- /dev/null +++ b/plaso/parsers/olecf_plugins/default_test.py @@ -0,0 +1,75 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the OLE Compound File (OLECF) default plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import olecf as olecf_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers.olecf_plugins import default +from plaso.parsers.olecf_plugins import test_lib + + +class TestDefaultPluginOleCf(test_lib.OleCfPluginTestCase): + """Tests for the OLECF default plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = default.DefaultOleCFPlugin() + + def testProcess(self): + """Tests the Process function.""" + test_file = self._GetTestFilePath(['Document.doc']) + event_queue_consumer = self._ParseOleCfFileWithPlugin( + test_file, self._plugin) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 5) + + # Check the Root Entry event. + event_object = event_objects[0] + + self.assertEquals(event_object.name, u'Root Entry') + + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.MODIFICATION_TIME) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-05-16 02:29:49.795') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_string = ( + u'Name: Root Entry') + + self._TestGetMessageStrings(event_object, expected_string, expected_string) + + # Check one other entry. + event_object = event_objects[1] + + expected_string = u'Name: MsoDataStore' + self._TestGetMessageStrings(event_object, expected_string, expected_string) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-05-16 02:29:49.704') + self.assertEquals(event_object.timestamp, expected_timestamp) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/olecf_plugins/interface.py b/plaso/parsers/olecf_plugins/interface.py new file mode 100644 index 0000000..a93ac10 --- /dev/null +++ b/plaso/parsers/olecf_plugins/interface.py @@ -0,0 +1,150 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the necessary interface for OLECF plugins.""" + +import abc +import logging + +from plaso.lib import errors +from plaso.parsers import plugins + + +class OlecfPlugin(plugins.BasePlugin): + """An OLECF plugin for Plaso.""" + + NAME = 'olecf' + + # List of tables that should be present in the database, for verification. + REQUIRED_ITEMS = frozenset([]) + + def GetTimestamps(self, olecf_item): + """Takes an OLECF object and returns extracted timestamps. + + Args: + olecf_item: A OLECF item (instance of pyolecf.item). + + Returns: + A tuple of two timestamps: created and modified. + """ + if not olecf_item: + return None, None + + try: + creation_time = olecf_item.get_creation_time_as_integer() + except OverflowError as exception: + logging.warning( + u'Unable to read the creation time with error: {0:s}'.format( + exception)) + creation_time = 0 + + try: + modification_time = olecf_item.get_modification_time_as_integer() + except OverflowError as exception: + logging.warning( + u'Unable to read the modification time with error: {0:s}'.format( + exception)) + modification_time = 0 + + # If no useful events, return early. + if not creation_time and not modification_time: + return None, None + + # Office template documents sometimes contain a creation time + # of -1 (0xffffffffffffffff). + if creation_time == 0xffffffffffffffffL: + creation_time = 0 + + return creation_time, modification_time + + @abc.abstractmethod + def ParseItems( + self, parser_context, file_entry=None, parser_chain=None, root_item=None, + items=None, **kwargs): + """Parses OLECF items. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + root_item: Optional root item of the OLECF file. The default is None. + item_names: Optional list of all items discovered in the root. + The default is None. + """ + + def Process( + self, parser_context, file_entry=None, parser_chain=None, root_item=None, + item_names=None, **kwargs): + """Determine if this is the right plugin for this OLECF file. + + This function takes a list of sub items found in the root of a + OLECF file and compares that to a list of required items defined + in this plugin. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + root_item: Optional root item of the OLECF file. The default is None. + item_names: Optional list of all items discovered in the root. + The default is None. + + Raises: + errors.WrongPlugin: If the set of required items is not a subset + of the available items. + ValueError: If the root_item or items are not set. + """ + if root_item is None or item_names is None: + raise ValueError(u'Root item or items are not set.') + + if not frozenset(item_names) >= self.REQUIRED_ITEMS: + raise errors.WrongPlugin( + u'Not the correct items for: {0:s}'.format(self.NAME)) + + # This will raise if unhandled keyword arguments are passed. + super(OlecfPlugin, self).Process(parser_context, **kwargs) + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + items = [] + for item_string in self.REQUIRED_ITEMS: + item = root_item.get_sub_item_by_name(item_string) + + if item: + items.append(item) + + self.ParseItems( + parser_context, file_entry=file_entry, parser_chain=parser_chain, + root_item=root_item, items=items) + + +class OleDefinitions(object): + """Convenience class for OLE definitions.""" + + VT_I2 = 0x0002 + VT_I4 = 0x0003 + VT_BOOL = 0x000b + VT_LPSTR = 0x001e + VT_LPWSTR = 0x001e + VT_FILETIME = 0x0040 + VT_CF = 0x0047 diff --git a/plaso/parsers/olecf_plugins/summary.py b/plaso/parsers/olecf_plugins/summary.py new file mode 100644 index 0000000..d1b7e62 --- /dev/null +++ b/plaso/parsers/olecf_plugins/summary.py @@ -0,0 +1,433 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Plugin to parse the OLECF summary/document summary information items.""" + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.parsers import olecf +from plaso.parsers.olecf_plugins import interface + + +class OleCfSummaryInfoEvent(time_events.FiletimeEvent): + """Convenience class for an OLECF Summary info event.""" + + DATA_TYPE = 'olecf:summary_info' + + def __init__(self, timestamp, usage, attributes): + """Initializes the event. + + Args: + timestamp: The FILETIME timestamp value. + usage: The usage string, describing the timestamp value. + attributes: A dict object containing all extracted attributes. + """ + super(OleCfSummaryInfoEvent, self).__init__( + timestamp, usage) + + self.name = u'Summary Information' + + for attribute_name, attribute_value in attributes.iteritems(): + setattr(self, attribute_name, attribute_value) + + +# TODO: Move this class to a higher level (to the interface) +# so the these functions can be shared by other plugins. +class OleCfSummaryInfo(object): + """An OLECF Summary Info object.""" + + _CLASS_IDENTIFIER = 'f29f85e0-4ff9-1068-ab91-08002b27b3d9' + + _PROPERTY_NAMES_INT32 = { + 0x000e: 'number_of_pages', # PIDSI_PAGECOUNT + 0x000f: 'number_of_words', # PIDSI_WORDCOUNT + 0x0010: 'number_of_characters', # PIDSI_CHARCOUNT + 0x0013: 'security', # PIDSI_SECURITY + } + + _PROPERTY_NAMES_STRING = { + 0x0002: 'title', # PIDSI_TITLE + 0x0003: 'subject', # PIDSI_SUBJECT + 0x0004: 'author', # PIDSI_AUTHOR + 0x0005: 'keywords', # PIDSI_KEYWORDS + 0x0006: 'comments', # PIDSI_COMMENTS + 0x0007: 'template', # PIDSI_TEMPLATE + 0x0008: 'last_saved_by', # PIDSI_LASTAUTHOR + 0x0009: 'revision_number', # PIDSI_REVNUMBER + 0x0012: 'application', # PIDSI_APPNAME + } + + PIDSI_CODEPAGE = 0x0001 + PIDSI_EDITTIME = 0x000a + PIDSI_LASTPRINTED = 0x000b + PIDSI_CREATE_DTM = 0x000c + PIDSI_LASTSAVE_DTM = 0x000d + PIDSI_THUMBNAIL = 0x0011 + + def __init__(self, olecf_item): + """Initialize the OLECF summary object. + + Args: + olecf_item: The OLECF item (instance of pyolecf.property_set_stream). + """ + super(OleCfSummaryInfo, self).__init__() + self.attributes = {} + self.events = [] + + self._InitFromPropertySet(olecf_item.set) + + def _InitFromPropertySet(self, property_set): + """Initializes the object from a property set. + + Args: + property_set: The OLECF property set (pyolecf.property_set). + """ + # Combine the values of multiple property sections + # but do not override properties that are already set. + for property_section in property_set.sections: + if property_section.class_identifier != self._CLASS_IDENTIFIER: + continue + for property_value in property_section.properties: + self._InitFromPropertyValue(property_value) + + def _InitFromPropertyValue(self, property_value): + """Initializes the object from a property value. + + Args: + property_value: The OLECF property value (pyolecf.property_value). + """ + if property_value.type == interface.OleDefinitions.VT_I2: + self._InitFromPropertyValueTypeInt16(property_value) + + elif property_value.type == interface.OleDefinitions.VT_I4: + self._InitFromPropertyValueTypeInt32(property_value) + + elif (property_value.type == interface.OleDefinitions.VT_LPSTR or + property_value.type == interface.OleDefinitions.VT_LPWSTR): + self._InitFromPropertyValueTypeString(property_value) + + elif property_value.type == interface.OleDefinitions.VT_FILETIME: + self._InitFromPropertyValueTypeFiletime(property_value) + + def _InitFromPropertyValueTypeInt16(self, property_value): + """Initializes the object from a 16-bit int type property value. + + Args: + property_value: The OLECF property value (pyolecf.property_value + of type VT_I2). + """ + if property_value.identifier == self.PIDSI_CODEPAGE: + # TODO: can the codepage vary per property section? + # And is it needed to interpret the ASCII strings? + # codepage = property_value.data_as_integer + pass + + def _InitFromPropertyValueTypeInt32(self, property_value): + """Initializes the object from a 32-bit int type property value. + + Args: + property_value: The OLECF property value (pyolecf.property_value + of type VT_I4). + """ + property_name = self._PROPERTY_NAMES_INT32.get( + property_value.identifier, None) + + if property_name and not property_name in self.attributes: + self.attributes[property_name] = property_value.data_as_integer + + def _InitFromPropertyValueTypeString(self, property_value): + """Initializes the object from a string type property value. + + Args: + property_value: The OLECF property value (pyolecf.property_value + of type VT_LPSTR or VT_LPWSTR). + """ + property_name = self._PROPERTY_NAMES_STRING.get( + property_value.identifier, None) + + if property_name and not property_name in self.attributes: + self.attributes[property_name] = property_value.data_as_string + + def _InitFromPropertyValueTypeFiletime(self, property_value): + """Initializes the object from a filetime type property value. + + Args: + property_value: The OLECF property value (pyolecf.property_value + of type VT_FILETIME). + """ + if property_value.identifier == self.PIDSI_LASTPRINTED: + self.events.append( + (property_value.data_as_integer, 'Document Last Printed Time')) + + elif property_value.identifier == self.PIDSI_CREATE_DTM: + self.events.append( + (property_value.data_as_integer, 'Document Creation Time')) + + elif property_value.identifier == self.PIDSI_LASTSAVE_DTM: + self.events.append( + (property_value.data_as_integer, 'Document Last Save Time')) + + elif property_value.identifier == self.PIDSI_EDITTIME: + # property_name = 'total_edit_time' + # TODO: handle duration. + pass + + +class OleCfDocumentSummaryInfoEvent(time_events.FiletimeEvent): + """Convenience class for an OLECF Document Summary info event.""" + + DATA_TYPE = 'olecf:document_summary_info' + + _CLASS_IDENTIFIER = 'd5cdd502-2e9c-101b-9397-08002b2cf9ae' + + _PROPERTY_NAMES_BOOL = { + 0x0013: 'shared_document', # PIDDSI_SHAREDDOC + } + + _PROPERTY_NAMES_INT32 = { + 0x0004: 'number_of_bytes', # PIDDSI_BYTECOUNT + 0x0005: 'number_of_lines', # PIDDSI_LINECOUNT + 0x0006: 'number_of_paragraphs', # PIDDSI_PARCOUNT + 0x0007: 'number_of_slides', # PIDDSI_SLIDECOUNT + 0x0008: 'number_of_notes', # PIDDSI_NOTECOUNT + 0x0009: 'number_of_hidden_slides', # PIDDSI_HIDDENCOUNT + 0x000a: 'number_of_clips', # PIDDSI_MMCLIPCOUNT + 0x0011: 'number_of_characters_with_white_space', # PIDDSI_CCHWITHSPACES + 0x0017: 'application_version', # PIDDSI_VERSION + } + + _PROPERTY_NAMES_STRING = { + 0x000e: 'manager', # PIDDSI_MANAGER + 0x000f: 'company', # PIDDSI_COMPANY + 0x001a: 'content_type', # PIDDSI_CONTENTTYPE + 0x001b: 'content_status', # PIDDSI_CONTENTSTATUS + 0x001c: 'language', # PIDDSI_LANGUAGE + 0x001d: 'document_version', # PIDDSI_DOCVERSION + } + + PIDDSI_CODEPAGE = 0x0001 + PIDDSI_CATEGORY = 0x0002 + PIDDSI_PRESFORMAT = 0x0003 + PIDDSI_SCALE = 0x000b + PIDDSI_HEADINGPAIR = 0x000c + PIDDSI_DOCPARTS = 0x000d + PIDDSI_LINKSDIRTY = 0x0010 + PIDDSI_VERSION = 0x0017 + + def __init__(self, timestamp, usage, olecf_item): + """Initializes the event. + + Args: + timestamp: The FILETIME timestamp value. + usage: The usage string, describing the timestamp value. + olecf_item: The OLECF item (pyolecf.property_set_stream). + """ + super(OleCfDocumentSummaryInfoEvent, self).__init__( + timestamp, usage) + + self.name = u'Document Summary Information' + + self._InitFromPropertySet(olecf_item.set) + + def _InitFromPropertySet(self, property_set): + """Initializes the event from a property set. + + Args: + property_set: The OLECF property set (pyolecf.property_set). + """ + # Combine the values of multiple property sections + # but do not override properties that are already set. + for property_section in property_set.sections: + if property_section.class_identifier != self._CLASS_IDENTIFIER: + continue + for property_value in property_section.properties: + self._InitFromPropertyValue(property_value) + + def _InitFromPropertyValue(self, property_value): + """Initializes the event from a property value. + + Args: + property_value: The OLECF property value (pyolecf.property_value). + """ + if property_value.type == interface.OleDefinitions.VT_I2: + self._InitFromPropertyValueTypeInt16(property_value) + + elif property_value.type == interface.OleDefinitions.VT_I4: + self._InitFromPropertyValueTypeInt32(property_value) + + elif property_value.type == interface.OleDefinitions.VT_BOOL: + self._InitFromPropertyValueTypeBool(property_value) + + elif (property_value.type == interface.OleDefinitions.VT_LPSTR or + property_value.type == interface.OleDefinitions.VT_LPWSTR): + self._InitFromPropertyValueTypeString(property_value) + + def _InitFromPropertyValueTypeInt16(self, property_value): + """Initializes the event from a 16-bit int type property value. + + Args: + property_value: The OLECF property value (pyolecf.property_value + of type VT_I2). + """ + if property_value.identifier == self.PIDDSI_CODEPAGE: + # TODO: can the codepage vary per property section? + # And is it needed to interpret the ASCII strings? + # codepage = property_value.data_as_integer + pass + + def _InitFromPropertyValueTypeInt32(self, property_value): + """Initializes the event from a 32-bit int type property value. + + Args: + property_value: The OLECF property value (pyolecf.property_value + of type VT_I4). + """ + property_name = self._PROPERTY_NAMES_INT32.get( + property_value.identifier, None) + + # The application version consists of 2 16-bit values that make up + # the version number. Where the upper 16-bit is the major number + # and the lower 16-bit the minor number. + if property_value.identifier == self.PIDDSI_VERSION: + application_version = property_value.data_as_integer + setattr(self, property_name, u'{0:d}.{1:d}'.format( + application_version >> 16, application_version & 0xffff)) + + elif property_name and not hasattr(self, property_name): + setattr(self, property_name, property_value.data_as_integer) + + def _InitFromPropertyValueTypeBool(self, property_value): + """Initializes the event from a boolean type property value. + + Args: + property_value: The OLECF property value (pyolecf.property_value + of type VT_BOOL). + """ + property_name = self._PROPERTY_NAMES_BOOL.get( + property_value.identifier, None) + + if property_name and not hasattr(self, property_name): + setattr(self, property_name, property_value.data_as_boolean) + + def _InitFromPropertyValueTypeString(self, property_value): + """Initializes the event from a string type property value. + + Args: + property_value: The OLECF property value (pyolecf.property_value + of type VT_LPSTR or VT_LPWSTR). + """ + property_name = self._PROPERTY_NAMES_STRING.get( + property_value.identifier, None) + + if property_name and not hasattr(self, property_name): + setattr(self, property_name, property_value.data_as_string) + + +class DocumentSummaryOlecfPlugin(interface.OlecfPlugin): + """Plugin that parses DocumentSummaryInformation item from an OLECF file.""" + + NAME = 'olecf_document_summary' + DESCRIPTION = u'Parser for a DocumentSummaryInformation OLECF stream.' + + # pylint: disable=anomalous-backslash-in-string + REQUIRED_ITEMS = frozenset([u'\005DocumentSummaryInformation']) + + def ParseItems( + self, parser_context, file_entry=None, parser_chain=None, root_item=None, + items=None, **unused_kwargs): + """Parses a document summary information OLECF item. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + root_item: Optional root item of the OLECF file. The default is None. + item_names: Optional list of all items discovered in the root. + The default is None. + """ + root_creation_time, root_modification_time = self.GetTimestamps(root_item) + + for item in items: + if root_creation_time: + event_object = OleCfDocumentSummaryInfoEvent( + root_creation_time, eventdata.EventTimestamp.CREATION_TIME, item) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if root_modification_time: + event_object = OleCfDocumentSummaryInfoEvent( + root_modification_time, eventdata.EventTimestamp.MODIFICATION_TIME, + item) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +class SummaryInfoOlecfPlugin(interface.OlecfPlugin): + """Plugin that parses the SummaryInformation item from an OLECF file.""" + + NAME = 'olecf_summary' + DESCRIPTION = u'Parser for a SummaryInformation OLECF stream.' + + # pylint: disable=anomalous-backslash-in-string + REQUIRED_ITEMS = frozenset([u'\005SummaryInformation']) + + def ParseItems( + self, parser_context, file_entry=None, parser_chain=None, root_item=None, + items=None, **unused_kwargs): + """Parses a summary information OLECF item. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + root_item: Optional root item of the OLECF file. The default is None. + item_names: Optional list of all items discovered in the root. + The default is None. + """ + root_creation_time, root_modification_time = self.GetTimestamps(root_item) + + for item in items: + summary_information_object = OleCfSummaryInfo(item) + + for timestamp, timestamp_description in summary_information_object.events: + event_object = OleCfSummaryInfoEvent( + timestamp, timestamp_description, + summary_information_object.attributes) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if root_creation_time: + event_object = OleCfSummaryInfoEvent( + root_creation_time, eventdata.EventTimestamp.CREATION_TIME, + summary_information_object.attributes) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if root_modification_time: + event_object = OleCfSummaryInfoEvent( + root_modification_time, eventdata.EventTimestamp.MODIFICATION_TIME, + summary_information_object.attributes) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +olecf.OleCfParser.RegisterPlugins( + [DocumentSummaryOlecfPlugin, SummaryInfoOlecfPlugin]) diff --git a/plaso/parsers/olecf_plugins/summary_test.py b/plaso/parsers/olecf_plugins/summary_test.py new file mode 100644 index 0000000..edef6ca --- /dev/null +++ b/plaso/parsers/olecf_plugins/summary_test.py @@ -0,0 +1,129 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the OLE Compound File summary and document summary plugins.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import olecf as olecf_formatter +from plaso.lib import timelib_test +from plaso.parsers.olecf_plugins import summary +from plaso.parsers.olecf_plugins import test_lib + + +class TestSummaryInfoOlecfPlugin(test_lib.OleCfPluginTestCase): + """Tests for the OLECF summary information plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._summary_plugin = summary.SummaryInfoOlecfPlugin() + self._test_file = self._GetTestFilePath(['Document.doc']) + + def testProcess(self): + """Tests the Process function on a SummaryInformation stream.""" + event_queue_consumer = self._ParseOleCfFileWithPlugin( + self._test_file, self._summary_plugin) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # There is one summary info stream with three event objects. + self.assertEquals(len(event_objects), 3) + + event_object = event_objects[0] + self.assertEquals(event_object.name, u'Summary Information') + + self.assertEquals(event_object.title, u'Table of Context') + self.assertEquals(event_object.author, u'DAVID NIDES') + self.assertEquals(event_object.template, u'Normal.dotm') + self.assertEquals(event_object.last_saved_by, u'Nides') + self.assertEquals(event_object.revision_number, u'4') + self.assertEquals(event_object.number_of_characters, 18) + self.assertEquals(event_object.application, u'Microsoft Office Word') + self.assertEquals(event_object.security, 0) + + self.assertEquals(event_object.timestamp_desc, u'Document Creation Time') + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-12-10 18:38:00') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'Title: Table of Context ' + u'Author: DAVID NIDES ' + u'Template: Normal.dotm ' + u'Revision number: 4 ' + u'Last saved by: Nides ' + u'Number of pages: 1 ' + u'Number of words: 3 ' + u'Number of characters: 18 ' + u'Application: Microsoft Office Word ' + u'Security: 0') + + expected_msg_short = ( + u'Title: Table of Context ' + u'Author: DAVID NIDES ' + u'Revision number: 4') + + # TODO: add support for: + # u'Total edit time (secs): 0 ' + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +class TestDocumentSummaryInfoOlecfPlugin(test_lib.OleCfPluginTestCase): + """Tests for the OLECF document summary information plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._document_summary_plugin = summary.DocumentSummaryOlecfPlugin() + self._test_file = self._GetTestFilePath(['Document.doc']) + + def testProcess(self): + """Tests the Process function on a DocumentSummaryInformation stream.""" + event_queue_consumer = self._ParseOleCfFileWithPlugin( + self._test_file, self._document_summary_plugin) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # There should only be one summary info stream with one event. + self.assertEquals(len(event_objects), 1) + + event_object = event_objects[0] + self.assertEquals(event_object.name, u'Document Summary Information') + + self.assertEquals(event_object.number_of_lines, 1) + self.assertEquals(event_object.number_of_paragraphs, 1) + self.assertEquals(event_object.company, u'KPMG') + self.assertFalse(event_object.shared_document) + self.assertEquals(event_object.application_version, u'14.0') + + # TODO: add support for: + # self.assertEquals(event_object.is_shared, False) + + expected_msg = ( + u'Number of lines: 1 ' + u'Number of paragraphs: 1 ' + u'Company: KPMG ' + u'Shared document: False ' + u'Application version: 14.0') + + expected_msg_short = ( + u'Company: KPMG') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/olecf_plugins/test_lib.py b/plaso/parsers/olecf_plugins/test_lib.py new file mode 100644 index 0000000..6ae019b --- /dev/null +++ b/plaso/parsers/olecf_plugins/test_lib.py @@ -0,0 +1,84 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""OLECF plugin related functions and classes for testing.""" + +from dfvfs.lib import definitions +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver import resolver as path_spec_resolver + +import pyolecf + +from plaso.engine import single_process +from plaso.parsers import test_lib + + +class OleCfPluginTestCase(test_lib.ParserTestCase): + """The unit test case for OLE CF based plugins.""" + + def _OpenOleCfFile(self, path, codepage='cp1252'): + """Opens an OLE compound file and returns back a pyolecf.file object. + + Args: + path: The path to the OLE CF test file. + codepate: Optional codepage. The default is cp1252. + """ + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=path) + file_entry = path_spec_resolver.Resolver.OpenFileEntry(path_spec) + + file_object = file_entry.GetFileObject() + olecf_file = pyolecf.file() + olecf_file.set_ascii_codepage(codepage) + + olecf_file.open_file_object(file_object) + + return olecf_file + + def _ParseOleCfFileWithPlugin( + self, path, plugin_object, knowledge_base_values=None): + """Parses a file as an OLE compound file and returns an event generator. + + Args: + path: The path to the OLE CF test file. + plugin_object: The plugin object that is used to extract an event + generator. + knowledge_base_values: optional dict containing the knowledge base + values. The default is None. + + Returns: + An event object queue consumer object (instance of + TestEventObjectQueueConsumer). + """ + event_queue = single_process.SingleProcessQueue() + event_queue_consumer = test_lib.TestEventObjectQueueConsumer(event_queue) + + parse_error_queue = single_process.SingleProcessQueue() + + parser_context = self._GetParserContext( + event_queue, parse_error_queue, + knowledge_base_values=knowledge_base_values) + olecf_file = self._OpenOleCfFile(path) + + # Get a list of all root items from the OLE CF file. + root_item = olecf_file.root_item + item_names = [item.name for item in root_item.sub_items] + + plugin_object.Process( + parser_context, root_item=root_item, item_names=item_names) + + return event_queue_consumer diff --git a/plaso/parsers/opera.py b/plaso/parsers/opera.py new file mode 100644 index 0000000..92944f5 --- /dev/null +++ b/plaso/parsers/opera.py @@ -0,0 +1,326 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parsers for Opera Browser history files.""" + +import logging +import os +import urllib2 + +from dfvfs.helpers import text_file +from xml.etree import ElementTree + +from plaso.events import time_events +from plaso.lib import errors +from plaso.lib import event +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.lib import utils +from plaso.parsers import interface +from plaso.parsers import manager + + +class OperaTypedHistoryEvent(event.EventObject): + """An EventObject for an Opera typed history entry.""" + + DATA_TYPE = 'opera:history:typed_entry' + + def __init__(self, last_typed_time, url, entry_type): + """A constructor for the typed history event. + + Args: + last_typed_time: A ISO 8601 string denoting the last time + the URL was typed into a browser. + url: The url, or the typed hostname. + entry_type: A string indicating whether the URL was directly + typed in or the result of the user choosing from the + auto complete (based on prior history). + """ + super(OperaTypedHistoryEvent, self).__init__() + self.url = url + self.entry_type = entry_type + + if entry_type == 'selected': + self.entry_selection = 'Filled from autocomplete.' + elif entry_type == 'text': + self.entry_selection = 'Manually typed.' + + self.timestamp = timelib.Timestamp.FromTimeString(last_typed_time) + self.timestamp_desc = eventdata.EventTimestamp.LAST_VISITED_TIME + + +class OperaGlobalHistoryEvent(time_events.PosixTimeEvent): + """An EventObject for an Opera global history entry.""" + + DATA_TYPE = 'opera:history:entry' + + def __init__(self, timestamp, url, title, popularity_index): + """Initialize the event object.""" + super(OperaGlobalHistoryEvent, self).__init__( + timestamp, eventdata.EventTimestamp.PAGE_VISITED, self.DATA_TYPE) + + self.url = url + if title != url: + self.title = title + + self.popularity_index = popularity_index + + if popularity_index < 0: + self.description = 'First and Only Visit' + else: + self.description = 'Last Visit' + + +class OperaTypedHistoryParser(interface.BaseParser): + """Parses the Opera typed_history.xml file.""" + + NAME = 'opera_typed_history' + DESCRIPTION = u'Parser for Opera typed_history.xml files.' + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract data from an Opera typed history file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + file_object = file_entry.GetFileObject() + file_object.seek(0, os.SEEK_SET) + + text_file_object = text_file.TextFile(file_object) + + # Need to verify the first line to make sure this is a) XML and + # b) the right XML. + first_line = text_file_object.readline(90) + + if not first_line.startswith('': + file_object.close() + raise errors.UnableToParseFile( + u'Not an Opera typed history file [wrong XML root key]') + + # For ElementTree to work we need to work on a file object seeked + # to the beginning. + file_object.seek(0, os.SEEK_SET) + + xml = ElementTree.parse(file_object) + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + for history_item in xml.iterfind('typed_history_item'): + content = history_item.get('content', '') + last_typed = history_item.get('last_typed', '') + entry_type = history_item.get('type', '') + + event_object = OperaTypedHistoryEvent(last_typed, content, entry_type) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + file_object.close() + + +class OperaGlobalHistoryParser(interface.BaseParser): + """Parses the Opera global_history.dat file.""" + + NAME = 'opera_global' + DESCRIPTION = u'Parser for Opera global_history.dat files.' + + _SUPPORTED_URL_SCHEMES = frozenset(['file', 'http', 'https', 'ftp']) + + def _IsValidUrl(self, url): + """A simple test to see if an URL is considered valid.""" + parsed_url = urllib2.urlparse.urlparse(url) + + # Few supported first URL entries. + if parsed_url.scheme in self._SUPPORTED_URL_SCHEMES: + return True + + return False + + def _ReadRecord(self, text_file_object, max_line_length=0): + """Return a single record from an Opera global_history file. + + A single record consists of four lines, with each line as: + Title of page (or the URL if not there). + Website URL. + Timestamp in POSIX time. + Popularity index (-1 if first time visited). + + Args: + text_file_object: A text file object (instance of dfvfs.TextFile). + max_line_length: An integer that denotes the maximum byte + length for each line read. + + Returns: + A tuple of: title, url, timestamp, popularity_index. + + Raises: + errors.NotAText: If the file being read is not a text file. + """ + if max_line_length: + title_raw = text_file_object.readline(max_line_length) + if len(title_raw) == max_line_length and not title_raw.endswith('\n'): + return None, None, None, None + if not utils.IsText(title_raw): + raise errors.NotAText(u'Title line is not a text.') + title = title_raw.strip() + else: + title = text_file_object.readline().strip() + + if not title: + return None, None, None, None + + url = text_file_object.readline().strip() + + if not url: + return None, None, None, None + + timestamp_line = text_file_object.readline().strip() + popularity_line = text_file_object.readline().strip() + + try: + timestamp = int(timestamp_line, 10) + except ValueError: + if len(timestamp_line) > 30: + timestamp_line = timestamp_line[0:30] + logging.debug(u'Unable to read in timestamp [{!r}]'.format( + timestamp_line)) + return None, None, None, None + + try: + popularity_index = int(popularity_line, 10) + except ValueError: + try: + logging.debug(u'Unable to read in popularity index[{}]'.format( + popularity_line)) + except UnicodeDecodeError: + logging.debug( + u'Unable to read in popularity index [unable to print ' + u'bad line]') + return None, None, None, None + + # Try to get the data into unicode. + try: + title_unicode = title.decode('utf-8') + except UnicodeDecodeError: + partial_title = title.decode('utf-8', 'ignore') + title_unicode = u'Warning: partial line, starts with: {}'.format( + partial_title) + + return title_unicode, url, timestamp, popularity_index + + def _ReadRecords(self, text_file_object): + """Yield records read from an Opera global_history file. + + A single record consists of four lines, with each line as: + Title of page (or the URL if not there). + Website URL. + Timestamp in POSIX time. + Popularity index (-1 if first time visited). + + Args: + text_file_object: A text file object (instance of dfvfs.TextFile). + + Yields: + A tuple of: title, url, timestamp, popularity_index. + """ + while True: + title, url, timestamp, popularity_index = self._ReadRecord( + text_file_object) + + if not title: + raise StopIteration + if not url: + raise StopIteration + if not popularity_index: + raise StopIteration + + yield title, url, timestamp, popularity_index + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract data from an Opera global history file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + file_object = file_entry.GetFileObject() + file_object.seek(0, os.SEEK_SET) + + text_file_object = text_file.TextFile(file_object) + + try: + title, url, timestamp, popularity_index = self._ReadRecord( + text_file_object, 400) + except errors.NotAText: + file_object.close() + raise errors.UnableToParseFile( + u'Not an Opera history file [not a text file].') + + if not title: + file_object.close() + raise errors.UnableToParseFile( + u'Not an Opera history file [no title present].') + + if not self._IsValidUrl(url): + file_object.close() + raise errors.UnableToParseFile( + u'Not an Opera history file [not a valid URL].') + + if not timestamp: + file_object.close() + raise errors.UnableToParseFile( + u'Not an Opera history file [timestamp does not exist].') + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + event_object = OperaGlobalHistoryEvent( + timestamp, url, title, popularity_index) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + # Read in the rest of the history file. + for title, url, timestamp, popularity_index in self._ReadRecords( + text_file_object): + event_object = OperaGlobalHistoryEvent( + timestamp, url, title, popularity_index) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + file_object.close() + + +manager.ParsersManager.RegisterParsers([ + OperaTypedHistoryParser, OperaGlobalHistoryParser]) diff --git a/plaso/parsers/opera_test.py b/plaso/parsers/opera_test.py new file mode 100644 index 0000000..9b130a3 --- /dev/null +++ b/plaso/parsers/opera_test.py @@ -0,0 +1,118 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Opera browser history parsers.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import opera as opera_formatter +from plaso.lib import timelib_test +from plaso.parsers import opera +from plaso.parsers import test_lib + + +class OperaTypedParserTest(test_lib.ParserTestCase): + """Tests for the Opera Typed History parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = opera.OperaTypedHistoryParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['typed_history.xml']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 4) + + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-11 23:45:27') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals(event_object.entry_selection, 'Filled from autocomplete.') + + expected_string = u'plaso.kiddaland.net (Filled from autocomplete.)' + + self._TestGetMessageStrings(event_object, expected_string, expected_string) + + event_object = event_objects[3] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-11 22:46:07') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals(event_object.entry_selection, 'Manually typed.') + + expected_string = u'theonion.com (Manually typed.)' + + self._TestGetMessageStrings(event_object, expected_string, expected_string) + + +class OperaGlobalParserTest(test_lib.ParserTestCase): + """Tests for the Opera Global History parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = opera.OperaGlobalHistoryParser() + + def testParseFile(self): + """Read a history file and run a few tests.""" + test_file = self._GetTestFilePath(['global_history.dat']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 37) + + event_object = event_objects[4] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-11 22:45:46') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'http://www.mbl.is/frettir/erlent/2013/11/11/' + u'karl_bretaprins_faer_ellilifeyri/ (Karl Bretaprins fær ellilífeyri' + u' - mbl.is) [First and Only Visit]') + expected_msg_short = ( + u'http://www.mbl.is/frettir/erlent/2013/11/11/' + u'karl_bretaprins_faer_ellilifeyri/...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + event_object = event_objects[10] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-11 22:45:55') + self.assertEquals(event_object.timestamp, expected_timestamp) + + event_object = event_objects[16] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-11 22:46:16') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_title = ( + u'10 Celebrities You Never Knew Were Abducted And Murdered ' + u'By Andie MacDowell | The Onion - America\'s Finest News Source') + + self.assertEquals(event_object.title, expected_title) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/oxml.py b/plaso/parsers/oxml.py new file mode 100644 index 0000000..90e48fb --- /dev/null +++ b/plaso/parsers/oxml.py @@ -0,0 +1,183 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a parser for OXML files (i.e. MS Office 2007+).""" + +import logging +import re +import struct +import zipfile + +from xml.etree import ElementTree + +from plaso.events import time_events +from plaso.lib import errors +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import interface +from plaso.parsers import manager + + +__author__ = 'David Nides (david.nides@gmail.com)' + + +class OpenXMLParserEvent(time_events.TimestampEvent): + """Process timestamps from MS Office XML Events.""" + + DATA_TYPE = 'metadata:openxml' + + def __init__(self, timestamp_string, usage, metadata): + """Initializes the event object. + + Args: + timestamp_string: An ISO 8601 representation of a timestamp. + usage: The description of the usage of the time value. + metadata: A dict object containing extracted metadata. + """ + timestamp = timelib.Timestamp.FromTimeString(timestamp_string) + super(OpenXMLParserEvent, self).__init__(timestamp, usage, self.DATA_TYPE) + for key, value in metadata.iteritems(): + setattr(self, key, value) + + +class OpenXMLParser(interface.BaseParser): + """Parse metadata from OXML files.""" + + NAME = 'openxml' + DESCRIPTION = u'Parser for OpenXML (OXML) files.' + + _METAKEY_TRANSLATE = { + 'creator': 'author', + 'lastModifiedBy': 'last_saved_by', + 'Total_Time': 'total_edit_time', + 'Pages': 'num_pages', + 'Characters_with_spaces': 'num_chars_w_spaces', + 'Paragraphs': 'num_paragraphs', + 'Characters': 'num_chars', + 'Lines': 'num_lines', + 'revision': 'revision_num', + 'Words': 'num_words', + 'Application': 'creating_app', + 'Shared_Doc': 'shared', + } + + _FILES_REQUIRED = frozenset([ + '[Content_Types].xml', '_rels/.rels', 'docProps/core.xml']) + + def _FixString(self, key): + """Convert CamelCase to lower_with_underscore.""" + # TODO: Add unicode support. + fix_key = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', key) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', fix_key).lower() + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract data from an OXML file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + file_object = file_entry.GetFileObject() + + if not zipfile.is_zipfile(file_object): + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse file: {1:s} with error: {2:s}'.format( + self.NAME, file_entry.name, 'Not a Zip file.')) + + try: + zip_container = zipfile.ZipFile(file_object, 'r') + except (zipfile.BadZipfile, struct.error, zipfile.LargeZipFile): + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse file: {1:s} with error: {2:s}'.format( + self.NAME, file_entry.name, 'Bad Zip file.')) + + zip_name_list = set(zip_container.namelist()) + + if not self._FILES_REQUIRED.issubset(zip_name_list): + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse file: {1:s} with error: {2:s}'.format( + self.NAME, file_entry.name, 'OXML element(s) missing.')) + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + metadata = {} + timestamps = {} + + try: + rels_xml = zip_container.read('_rels/.rels') + except zipfile.BadZipfile as exception: + logging.error( + u'Unable to parse file {0:s} with error: {1:s}'.format( + file_entry.name, exception)) + return + + rels_root = ElementTree.fromstring(rels_xml) + + for properties in rels_root.iter(): + if 'properties' in repr(properties.get('Type')): + try: + xml = zip_container.read(properties.get('Target')) + root = ElementTree.fromstring(xml) + except ( + OverflowError, IndexError, KeyError, ValueError, + zipfile.BadZipfile) as exception: + logging.warning( + u'[{0:s}] unable to read property with error: {1:s}.'.format( + self.NAME, exception)) + continue + + for element in root.iter(): + if element.text: + _, _, tag = element.tag.partition('}') + # Not including the 'lpstr' attribute because it is + # very verbose. + if tag == 'lpstr': + continue + + if tag in ('created', 'modified', 'lastPrinted'): + timestamps[tag] = element.text + else: + tag_name = self._METAKEY_TRANSLATE.get(tag, self._FixString(tag)) + metadata[tag_name] = element.text + + if timestamps.get('created', None): + event_object = OpenXMLParserEvent( + timestamps.get('created'), eventdata.EventTimestamp.CREATION_TIME, + metadata) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if timestamps.get('modified', None): + event_object = OpenXMLParserEvent( + timestamps.get('modified'), + eventdata.EventTimestamp.MODIFICATION_TIME, metadata) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if timestamps.get('lastPrinted', None): + event_object = OpenXMLParserEvent( + timestamps.get('lastPrinted'), eventdata.EventTimestamp.LAST_PRINTED, + metadata) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +manager.ParsersManager.RegisterParser(OpenXMLParser) diff --git a/plaso/parsers/oxml_test.py b/plaso/parsers/oxml_test.py new file mode 100644 index 0000000..d8dc1a3 --- /dev/null +++ b/plaso/parsers/oxml_test.py @@ -0,0 +1,96 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the OXML parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import oxml as oxml_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import oxml +from plaso.parsers import test_lib + + +class OXMLTest(test_lib.ParserTestCase): + """Tests for the OXML parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = oxml.OpenXMLParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['Document.docx']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 2) + + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-11-07 23:29:00') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.CREATION_TIME) + + event_object = event_objects[1] + + self.assertEquals(event_object.num_chars, u'13') + self.assertEquals(event_object.total_time, u'1385') + self.assertEquals(event_object.characters_with_spaces, u'14') + self.assertEquals(event_object.i4, u'1') + self.assertEquals(event_object.app_version, u'14.0000') + self.assertEquals(event_object.num_lines, u'1') + self.assertEquals(event_object.scale_crop, u'false') + self.assertEquals(event_object.num_pages, u'1') + self.assertEquals(event_object.num_words, u'2') + self.assertEquals(event_object.links_up_to_date, u'false') + self.assertEquals(event_object.num_paragraphs, u'1') + self.assertEquals(event_object.doc_security, u'0') + self.assertEquals(event_object.hyperlinks_changed, u'false') + self.assertEquals(event_object.revision_num, u'3') + self.assertEquals(event_object.last_saved_by, u'Nides') + self.assertEquals(event_object.author, u'Nides') + self.assertEquals( + event_object.creating_app, u'Microsoft Office Word') + self.assertEquals(event_object.template, u'Normal.dotm') + + expected_msg = ( + u'Creating App: Microsoft Office Word ' + u'App version: 14.0000 ' + u'Last saved by: Nides ' + u'Author: Nides ' + u'Revision Num: 3 ' + u'Template: Normal.dotm ' + u'Num pages: 1 ' + u'Num words: 2 ' + u'Num chars: 13 ' + u'Num lines: 1 ' + u'Hyperlinks changed: false ' + u'Links up to date: false ' + u'Scale crop: false') + expected_msg_short = ( + u'Author: Nides') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/pcap.py b/plaso/parsers/pcap.py new file mode 100644 index 0000000..47df468 --- /dev/null +++ b/plaso/parsers/pcap.py @@ -0,0 +1,843 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for PCAP files.""" + +import binascii +import operator +import socket + +import dpkt + +from plaso.events import time_events +from plaso.lib import errors +from plaso.lib import eventdata +from plaso.parsers import interface +from plaso.parsers import manager + + +__author__ = 'Dominique Kilman (lexistar97@gmail.com)' + + +def ParseDNS(dns_packet_data): + """Parse DNS packets and return a string with relevant details. + + Args: + dns_packet_data: DNS packet data. + + Returns: + Formatted DNS details. + """ + dns_data = [] + + try: + dns = dpkt.dns.DNS(dns_packet_data) + if dns.rcode is dpkt.dns.DNS_RCODE_NOERR: + if dns.get_qr() == 1: + if not dns.an: + dns_data.append('DNS Response: No answer for ') + dns_data.append(dns.qd[0].name) + else: + # Type of DNS answer. + for answer in dns.an: + if answer.type == 5: + dns_data.append('DNS-CNAME request ') + dns_data.append(answer.name) + dns_data.append(' response: ') + dns_data.append(answer.cname) + elif answer.type == 1: + dns_data.append('DNS-A request ') + dns_data.append(answer.name) + dns_data.append(' response: ') + dns_data.append(socket.inet_ntoa(answer.rdata)) + elif answer.type == 12: + dns_data.append('DNS-PTR request ') + dns_data.append(answer.name) + dns_data.append(' response: ') + dns_data.append(answer.ptrname) + elif not dns.get_qr(): + dns_data.append('DNS Query for ') + dns_data.append(dns.qd[0].name) + else: + dns_data.append('DNS error code ') + dns_data.append(str(dns.rcode)) + + except dpkt.UnpackError as exception: + dns_data.append('DNS Unpack Error: {0:s}. First 20 of data {1:s}'.format( + exception, repr(dns_packet_data[:20]))) + except IndexError as exception: + dns_data.append('DNS Index Error: {0:s}'.format(exception)) + + return u' '.join(dns_data) + + +def ParseNetBios(netbios_packet): + """Parse the netBIOS stream details. + + Args: + netbios_packet: NetBIOS packet. + + Returns: + Formatted netBIOS details. + """ + netbios_data = [] + for query in netbios_packet.qd: + netbios_data.append('NETBIOS qd:') + netbios_data.append(repr(dpkt.netbios.decode_name(query.name))) + for answer in netbios_packet.an: + netbios_data.append('NETBIOS an:') + netbios_data.append(repr(dpkt.netbios.decode_name(answer.name))) + for name in netbios_packet.ns: + netbios_data.append('NETBIOS ns:') + netbios_data.append(repr(dpkt.netbios.decode_name(name.name))) + + return u' '.join(netbios_data) + + +def TCPFlags(flag): + """Check the tcp flags for a packet for future use. + + Args: + flag: Flag value from TCP packet. + + Returns: + String with printable flags for specific packet. + """ + res = [] + if flag & dpkt.tcp.TH_FIN: + res.append('FIN') + if flag & dpkt.tcp.TH_SYN: + res.append('SYN') + if flag & dpkt.tcp.TH_RST: + res.append('RST') + if flag & dpkt.tcp.TH_PUSH: + res.append('PUSH') + if flag & dpkt.tcp.TH_ACK: + res.append('ACK') + if flag & dpkt.tcp.TH_URG: + res.append('URG') + if flag & dpkt.tcp.TH_ECE: + res.append('ECN') + if flag & dpkt.tcp.TH_CWR: + res.append('CWR') + + return '|'.join(res) + + +def ICMPTypes(packet): + """Parse the type information for the icmp packets. + + Args: + packet: ICMP packet data. + + Returns: + Formatted ICMP details. + """ + icmp_type = packet.type + icmp_code = packet.code + icmp_data = [] + icmp_data.append('ICMP') + + # TODO: Make the below code more readable. + # Possible to use lookup dict? Or method + # calls? + if icmp_type is dpkt.icmp.ICMP_CODE_NONE: + icmp_data.append('ICMP without codes') + elif icmp_type is dpkt.icmp.ICMP_ECHOREPLY: + icmp_data.append('echo reply') + elif icmp_type is dpkt.icmp.ICMP_UNREACH: + icmp_data.append('ICMP dest unreachable') + if icmp_code is dpkt.icmp.ICMP_UNREACH_NET: + icmp_data.append(': bad net') + elif icmp_code is dpkt.icmp.ICMP_UNREACH_HOST: + icmp_data.append(': host unreachable') + elif icmp_code is dpkt.icmp.ICMP_UNREACH_PROTO: + icmp_data.append(': bad protocol') + elif icmp_code is dpkt.icmp.ICMP_UNREACH_PORT: + icmp_data.append(': port unreachable') + elif icmp_code is dpkt.icmp.ICMP_UNREACH_NEEDFRAG: + icmp_data.append(': IP_DF caused drop') + elif icmp_code is dpkt.icmp.ICMP_UNREACH_SRCFAIL: + icmp_data.append(': src route failed') + elif icmp_code is dpkt.icmp.ICMP_UNREACH_NET_UNKNOWN: + icmp_data.append(': unknown net') + elif icmp_code is dpkt.icmp.ICMP_UNREACH_HOST_UNKNOWN: + icmp_data.append(': unknown host') + elif icmp_code is dpkt.icmp.ICMP_UNREACH_ISOLATED: + icmp_data.append(': src host isolated') + elif icmp_code is dpkt.icmp.ICMP_UNREACH_NET_PROHIB: + icmp_data.append(': for crypto devs') + elif icmp_code is dpkt.icmp.ICMP_UNREACH_HOST_PROHIB: + icmp_data.append(': for cypto devs') + elif icmp_code is dpkt.icmp.ICMP_UNREACH_TOSNET: + icmp_data.append(': bad tos for net') + elif icmp_code is dpkt.icmp.ICMP_UNREACH_TOSHOST: + icmp_data.append(': bad tos for host') + elif icmp_code is dpkt.icmp.ICMP_UNREACH_FILTER_PROHIB: + icmp_data.append(': prohibited access') + elif icmp_code is dpkt.icmp.ICMP_UNREACH_HOST_PRECEDENCE: + icmp_data.append(': precedence error') + elif icmp_code is dpkt.icmp.ICMP_UNREACH_PRECEDENCE_CUTOFF: + icmp_data.append(': precedence cutoff') + elif icmp_type is dpkt.icmp.ICMP_SRCQUENCH: + icmp_data.append('ICMP source quench') + elif icmp_type is dpkt.icmp.ICMP_REDIRECT: + icmp_data.append('ICMP Redirect') + if icmp_code is dpkt.icmp.ICMP_REDIRECT_NET: + icmp_data.append(' for network') + elif icmp_code is dpkt.icmp.ICMP_REDIRECT_HOST: + icmp_data.append(' for host') + elif icmp_code is dpkt.icmp.ICMP_REDIRECT_TOSNET: + icmp_data.append(' for tos and net') + elif icmp_code is dpkt.icmp.ICMP_REDIRECT_TOSHOST: + icmp_data.append(' for tos and host') + elif icmp_type is dpkt.icmp.ICMP_ALTHOSTADDR: + icmp_data.append('ICMP alternate host address') + elif icmp_type is dpkt.icmp.ICMP_ECHO: + icmp_data.append('ICMP echo') + elif icmp_type is dpkt.icmp.ICMP_RTRADVERT: + icmp_data.append('ICMP Route advertisement') + if icmp_code is dpkt.icmp.ICMP_RTRADVERT_NORMAL: + icmp_data.append(': normal') + elif icmp_code is dpkt.icmp.ICMP_RTRADVERT_NOROUTE_COMMON: + icmp_data.append(': selective routing') + elif icmp_type is dpkt.icmp.ICMP_RTRSOLICIT: + icmp_data.append('ICMP Router solicitation') + elif icmp_type is dpkt.icmp.ICMP_TIMEXCEED: + icmp_data.append('ICMP time exceeded, code:') + if icmp_code is dpkt.icmp.ICMP_TIMEXCEED_INTRANS: + icmp_data.append(' ttl==0 in transit') + elif icmp_code is dpkt.icmp.ICMP_TIMEXCEED_REASS: + icmp_data.append('ttl==0 in reass') + elif icmp_type is dpkt.icmp.ICMP_PARAMPROB: + icmp_data.append('ICMP ip header bad') + if icmp_code is dpkt.icmp.ICMP_PARAMPROB_ERRATPTR: + icmp_data.append(':req. opt. absent') + elif icmp_code is dpkt.icmp.ICMP_PARAMPROB_OPTABSENT: + icmp_data.append(': req. opt. absent') + elif icmp_code is dpkt.icmp.ICMP_PARAMPROB_LENGTH: + icmp_data.append(': length') + elif icmp_type is dpkt.icmp.ICMP_TSTAMP: + icmp_data.append('ICMP timestamp request') + elif icmp_type is dpkt.icmp.ICMP_TSTAMPREPLY: + icmp_data.append('ICMP timestamp reply') + elif icmp_type is dpkt.icmp.ICMP_INFO: + icmp_data.append('ICMP information request') + elif icmp_type is dpkt.icmp.ICMP_INFOREPLY: + icmp_data.append('ICMP information reply') + elif icmp_type is dpkt.icmp.ICMP_MASK: + icmp_data.append('ICMP address mask request') + elif icmp_type is dpkt.icmp.ICMP_MASKREPLY: + icmp_data.append('ICMP address mask reply') + elif icmp_type is dpkt.icmp.ICMP_TRACEROUTE: + icmp_data.append('ICMP traceroute') + elif icmp_type is dpkt.icmp.ICMP_DATACONVERR: + icmp_data.append('ICMP data conversion error') + elif icmp_type is dpkt.icmp.ICMP_MOBILE_REDIRECT: + icmp_data.append('ICMP mobile host redirect') + elif icmp_type is dpkt.icmp.ICMP_IP6_WHEREAREYOU: + icmp_data.append('ICMP IPv6 where-are-you') + elif icmp_type is dpkt.icmp.ICMP_IP6_IAMHERE: + icmp_data.append('ICMP IPv6 i-am-here') + elif icmp_type is dpkt.icmp.ICMP_MOBILE_REG: + icmp_data.append('ICMP mobile registration req') + elif icmp_type is dpkt.icmp.ICMP_MOBILE_REGREPLY: + icmp_data.append('ICMP mobile registration reply') + elif icmp_type is dpkt.icmp.ICMP_DNS: + icmp_data.append('ICMP domain name request') + elif icmp_type is dpkt.icmp.ICMP_DNSREPLY: + icmp_data.append('ICMP domain name reply') + elif icmp_type is dpkt.icmp.ICMP_PHOTURIS: + icmp_data.append('ICMP Photuris') + if icmp_code is dpkt.icmp.ICMP_PHOTURIS_UNKNOWN_INDEX: + icmp_data.append(': unknown sec index') + elif icmp_code is dpkt.icmp.ICMP_PHOTURIS_AUTH_FAILED: + icmp_data.append(': auth failed') + elif icmp_code is dpkt.icmp.ICMP_PHOTURIS_DECOMPRESS_FAILED: + icmp_data.append(': decompress failed') + elif icmp_code is dpkt.icmp.ICMP_PHOTURIS_DECRYPT_FAILED: + icmp_data.append(': decrypt failed') + elif icmp_code is dpkt.icmp.ICMP_PHOTURIS_NEED_AUTHN: + icmp_data.append(': no authentication') + elif icmp_code is dpkt.icmp.ICMP_PHOTURIS_NEED_AUTHZ: + icmp_data.append(': no authorization') + elif icmp_type is dpkt.icmp.ICMP_TYPE_MAX: + icmp_data.append('ICMP Type Max') + + return u' '.join(icmp_data) + + +class Stream(object): + """Used to store packet details on network streams parsed from a pcap file.""" + + def __init__(self, packet, prot_data, source_ip, dest_ip, prot): + """Initialize new stream. + + Args: + packet: Packet data. + prot_data: Protocol level data for ARP, UDP, RCP, ICMP. + other types of ether packets, this is just the ether.data. + source_ip: Source IP. + dest_ip: Dest IP. + prot: Protocol (TCP, UDP, ICMP, ARP). + """ + self.packet_id = [packet[1]] + self.timestamps = [packet[0]] + self.size = packet[3] + self.start_time = packet[0] + self.all_data = [prot_data] + self.protocol_data = '' + self.stream_data = [] + + if prot == 'TCP' or prot == 'UDP': + self.source_port = prot_data.sport + self.dest_port = prot_data.dport + else: + self.source_port = '' + self.dest_port = '' + + self.source_ip = source_ip + self.dest_ip = dest_ip + self.protocol = prot + + def AddPacket(self, packet, prot_data): + """Add another packet to an existing stream. + + Args: + packet: Packet data. + prot_data: Protocol level data for ARP, UDP, RCP, ICMP. + other types of ether packets, this is just the ether.data + """ + self.packet_id.append(packet[1]) + self.timestamps.append(packet[0]) + self.all_data.append(prot_data) + self.size += packet[3] + + def SpecialTypes(self): + """Checks for some special types of packets. + + This method checks for some special packets and assembles usable data + currently works for: DNS (udp 53), http, netbios (udp 137), ICMP. + + Returns: + A tuple consisting of a basic desctiption of the stream + (i.e. HTTP Request) and the prettyfied string for the protocols. + """ + packet_details = [] + if self.stream_data[:4] == 'HTTP': + try: + http = dpkt.http.Response(self.stream_data) + packet_details.append('HTTP Response: status: ') + packet_details.append(http.status) + packet_details.append(' reason: ') + packet_details.append(http.reason) + packet_details.append(' version: ') + packet_details.append(http.version) + return 'HTTP Response', u' '.join(packet_details) + + except dpkt.UnpackError as exception: + packet_details = ( + u'HTTP Response Unpack Error: {0:s}. ' + u'First 20 of data {1:s}').format( + exception, repr(self.stream_data[:20])) + return 'HTTP Response', packet_details + + except IndexError as exception: + packet_details = ( + u'HTTP Response Index Error: {0:s}. First 20 of data {1:s}').format( + exception, repr(self.stream_data[:20])) + return 'HTTP Response', packet_details + + except ValueError as exception: + packet_details = ( + u'HTTP Response parsing error: {0:s}. ' + u'First 20 of data {1:s}').format( + exception, repr(self.stream_data[:20])) + return 'HTTP Response', packet_details + + elif self.stream_data[:3] == 'GET' or self.stream_data[:4] == 'POST': + try: + http = dpkt.http.Request(self.stream_data) + packet_details.append('HTTP Request: method: ') + packet_details.append(http.method) + packet_details.append(' uri: ') + packet_details.append(http.uri) + packet_details.append(' version: ') + packet_details.append(http.version) + packet_details.append(' headers: ') + packet_details.append(repr(http.headers)) + return 'HTTP Request', u' '.join(packet_details) + + except dpkt.UnpackError as exception: + packet_details = ( + u'HTTP Request unpack error: {0:s}. First 20 of data {1:s}').format( + exception, repr(self.stream_data[:20])) + return 'HTTP Request', packet_details + + except ValueError as exception: + packet_details = ( + u'HTTP Request parsing error: {0:s}. ' + u'First 20 of data {1:s}').format( + exception, repr(self.stream_data[:20])) + return 'HTTP Request', packet_details + + elif self.protocol == 'UDP' and ( + self.source_port == 53 or self.dest_port == 53): + # DNS request/replies. + # Check to see if the lengths are valid. + for packet in self.all_data: + if not packet.ulen == len(packet): + packet_details.append('Truncated DNS packets - unable to parse: ') + packet_details.append(repr(self.stream_data[15:40])) + return 'DNS', u' '.join(packet_details) + + return 'DNS', ParseDNS(self.stream_data) + + elif self.protocol == 'UDP' and ( + self.source_port == 137 or self.dest_port == 137): + return 'NetBIOS', ParseNetBios(dpkt.netbios.NS(self.stream_data)) + + elif self.protocol == 'ICMP': + # ICMP packets all end up as 1 stream, so they need to be + # processed 1 by 1. + return 'ICMP', ICMPTypes(self.all_data[0]) + + elif '\x03\x01' in self.stream_data[1:3]: + # Some form of ssl3 data. + try: + ssl = dpkt.ssl.SSL2(self.stream_data) + packet_details.append('SSL data. Length: ') + packet_details.append(str(ssl.len)) + return 'SSL', u' '.join(packet_details) + except dpkt.UnpackError as exception: + packet_details = ( + u'SSL unpack error: {0:s}. First 20 of data {1:s}').format( + exception, repr(self.stream_data[:20])) + return 'SSL', packet_details + + elif '\x03\x00' in self.stream_data[1:3]: + # Some form of ssl3 data. + try: + ssl = dpkt.ssl.SSL2(self.stream_data) + packet_details.append('SSL data. Length: ') + packet_details.append(str(ssl.len)) + return 'SSL', u' '.join(packet_details) + + except dpkt.UnpackError as exception: + packet_details = ( + u'SSL unpack error: {0:s}. First 20 of data {1:s}').format( + exception, repr(self.stream_data[:20])) + return 'SSL', packet_details + + return 'other', self.protocol_data + + def Clean(self): + """Clean up stream data.""" + clean_data = [] + for packet in self.all_data: + try: + clean_data.append(packet.data) + except AttributeError: + pass + + self.stream_data = ''.join(clean_data) + + +class PcapEvent(time_events.PosixTimeEvent): + """Convenience class for a PCAP record event.""" + + DATA_TYPE = 'metadata:pcap' + + def __init__(self, timestamp, usage, stream_object): + """Initializes the event. + + Args: + timestamp: The POSIX value of the timestamp. + usage: A usage description value. + stream_object: The stream object (instance of Stream). + """ + super(PcapEvent, self).__init__(timestamp, usage) + + self.source_ip = stream_object.source_ip + self.dest_ip = stream_object.dest_ip + self.source_port = stream_object.source_port + self.dest_port = stream_object.dest_port + self.protocol = stream_object.protocol + self.size = stream_object.size + self.stream_type, self.protocol_data = stream_object.SpecialTypes() + self.first_packet_id = min(stream_object.packet_id) + self.last_packet_id = max(stream_object.packet_id) + self.packet_count = len(stream_object.packet_id) + self.stream_data = repr(stream_object.stream_data[:50]) + + +class PcapParser(interface.BaseParser): + """Parses PCAP files.""" + + NAME = 'pcap' + DESCRIPTION = u'Parser for PCAP files.' + + def _ParseIPPacket( + self, connections, trunc_list, packet_number, timestamp, + packet_data_size, ip_packet): + """Parses an IP packet. + + Args: + connections: A dictionary object to track the IP connections. + trunc_list: A list of packets that truncated strangely and could + not be turned into a stream. + packet_number: The PCAP packet number, where 1 is the first packet. + timestamp: The PCAP packet timestamp. + packet_data_size: The packet data size. + ip_packet: The IP packet (instance of dpkt.ip.IP). + """ + packet_values = [timestamp, packet_number, ip_packet, packet_data_size] + + source_ip_address = socket.inet_ntoa(ip_packet.src) + destination_ip_address = socket.inet_ntoa(ip_packet.dst) + + if ip_packet.p == dpkt.ip.IP_PROTO_TCP: + # Later versions of dpkt seem to return a string instead of a TCP object. + if isinstance(ip_packet.data, str): + try: + tcp = dpkt.tcp.TCP(ip_packet.data) + except (dpkt.NeedData, dpkt.UnpackError): + trunc_list.append(packet_values) + return + + else: + tcp = ip_packet.data + + stream_key = 'tcp: {0:s}:{1:d} > {2:s}:{3:d}'.format( + source_ip_address, tcp.sport, destination_ip_address, tcp.dport) + + if stream_key in connections: + connections[stream_key].AddPacket(packet_values, tcp) + else: + connections[stream_key] = Stream( + packet_values, tcp, source_ip_address, destination_ip_address, + 'TCP') + + elif ip_packet.p == dpkt.ip.IP_PROTO_UDP: + # Later versions of dpkt seem to return a string instead of an UDP object. + if isinstance(ip_packet.data, str): + try: + udp = dpkt.udp.UDP(ip_packet.data) + except (dpkt.NeedData, dpkt.UnpackError): + trunc_list.append(packet_values) + return + + else: + udp = ip_packet.data + + stream_key = 'udp: {0:s}:{1:d} > {2:s}:{3:d}'.format( + source_ip_address, udp.sport, destination_ip_address, udp.dport) + + if stream_key in connections: + connections[stream_key].AddPacket(packet_values, udp) + else: + connections[stream_key] = Stream( + packet_values, udp, source_ip_address, destination_ip_address, + 'UDP') + + elif ip_packet.p == dpkt.ip.IP_PROTO_ICMP: + # Later versions of dpkt seem to return a string instead of + # an ICMP object. + if isinstance(ip_packet.data, str): + icmp = dpkt.icmp.ICMP(ip_packet.data) + else: + icmp = ip_packet.data + + stream_key = 'icmp: {0:d} {1:s} > {2:s}'.format( + timestamp, source_ip_address, destination_ip_address) + + if stream_key in connections: + connections[stream_key].AddPacket(packet_values, icmp) + else: + connections[stream_key] = Stream( + packet_values, icmp, source_ip_address, destination_ip_address, + 'ICMP') + + def _ParseOtherPacket(self, packet_values): + """Parses a non-IP packet. + + Args: + packet_values: list of packet values + + Returns: + A stream object (instance of Stream) or None if the packet data + is not supported. + """ + ether = packet_values[2] + stream_object = None + + if ether.type == dpkt.ethernet.ETH_TYPE_ARP: + arp = ether.data + arp_data = [] + stream_object = Stream( + packet_values, arp, binascii.hexlify(ether.src), + binascii.hexlify(ether.dst), 'ARP') + + if arp.op == dpkt.arp.ARP_OP_REQUEST: + arp_data.append('arp request: target IP = ') + arp_data.append(socket.inet_ntoa(arp.tpa)) + stream_object.protocol_data = u' '.join(arp_data) + + elif arp.op == dpkt.arp.ARP_OP_REPLY: + arp_data.append('arp reply: target IP = ') + arp_data.append(socket.inet_ntoa(arp.tpa)) + arp_data.append(' target MAC = ') + arp_data.append(binascii.hexlify(arp.tha)) + stream_object.protocol_data = u' '.join(arp_data) + + elif arp.op == dpkt.arp.ARP_OP_REVREQUEST: + arp_data.append('arp protocol address request: target IP = ') + arp_data.append(socket.inet_ntoa(arp.tpa)) + stream_object.protocol_data = u' '.join(arp_data) + + elif arp.op == dpkt.arp.ARP_OP_REVREPLY: + arp_data.append('arp protocol address reply: target IP = ') + arp_data.append(socket.inet_ntoa(arp.tpa)) + arp_data.append(' target MAC = ') + arp_data.append(binascii.hexlify(arp.tha)) + stream_object.protocol_data = u' '.join(arp_data) + + elif ether.type == dpkt.ethernet.ETH_TYPE_IP6: + ip6 = ether.data + stream_object = Stream( + packet_values, ether.data, binascii.hexlify(ip6.src), + binascii.hexlify(ip6.dst), 'IPv6') + stream_object.protocol_data = 'IPv6' + + elif ether.type == dpkt.ethernet.ETH_TYPE_CDP: + stream_object = Stream( + packet_values, ether.data, binascii.hexlify(ether.src), + binascii.hexlify(ether.dst), 'CDP') + stream_object.protocol_data = 'CDP' + + elif ether.type == dpkt.ethernet.ETH_TYPE_DTP: + stream_object = Stream( + packet_values, ether.data, binascii.hexlify(ether.src), + binascii.hexlify(ether.dst), 'DTP') + stream_object.protocol_data = 'DTP' + + elif ether.type == dpkt.ethernet.ETH_TYPE_REVARP: + stream_object = Stream( + packet_values, ether.data, binascii.hexlify(ether.src), + binascii.hexlify(ether.dst), 'RARP') + stream_object.protocol_data = 'Reverse ARP' + + elif ether.type == dpkt.ethernet.ETH_TYPE_8021Q: + stream_object = Stream( + packet_values, ether.data, binascii.hexlify(ether.src), + binascii.hexlify(ether.dst), '8021Q packet') + stream_object.protocol_data = '8021Q packet' + + elif ether.type == dpkt.ethernet.ETH_TYPE_IPX: + stream_object = Stream( + packet_values, ether.data, binascii.hexlify(ether.src), + binascii.hexlify(ether.dst), 'IPX') + stream_object.protocol_data = 'IPX' + + elif ether.type == dpkt.ethernet.ETH_TYPE_PPP: + stream_object = Stream( + packet_values, ether.data, binascii.hexlify(ether.src), + binascii.hexlify(ether.dst), 'PPP') + stream_object.protocol_data = 'PPP' + + elif ether.type == dpkt.ethernet.ETH_TYPE_MPLS: + stream_object = Stream( + packet_values, ether.data, binascii.hexlify(ether.src), + binascii.hexlify(ether.dst), 'MPLS') + stream_object.protocol_data = 'MPLS' + + elif ether.type == dpkt.ethernet.ETH_TYPE_MPLS_MCAST: + stream_object = Stream( + packet_values, ether.data, binascii.hexlify(ether.src), + binascii.hexlify(ether.dst), 'MPLS') + stream_object.protocol_data = 'MPLS MCAST' + + elif ether.type == dpkt.ethernet.ETH_TYPE_PPPoE_DISC: + stream_object = Stream( + packet_values, ether.data, binascii.hexlify(ether.src), + binascii.hexlify(ether.dst), 'PPOE') + stream_object.protocol_data = 'PPoE Disc packet' + + elif ether.type == dpkt.ethernet.ETH_TYPE_PPPoE: + stream_object = Stream( + packet_values, ether.data, binascii.hexlify(ether.src), + binascii.hexlify(ether.dst), 'PPPoE') + stream_object.protocol_data = 'PPPoE' + + elif str(hex(ether.type)) == '0x2452': + stream_object = Stream( + packet_values, ether.data, binascii.hexlify(ether.src), + binascii.hexlify(ether.dst), '802.11') + stream_object.protocol_data = '802.11' + + return stream_object + + def _ParseOtherStreams(self, other_list, trunc_list): + """Process PCAP packets that are not IP packets. + + For all packets that were not IP packets, create stream containers + depending on the type of packet. + + Args: + other_list: List of non-ip packets. + trunc_list: A list of packets that truncated strangely and could + not be turned into a stream. + + Returns: + A list of stream objects (instances of Stream). + """ + other_streams = [] + + for packet_values in other_list: + stream_object = self._ParseOtherPacket(packet_values) + if stream_object: + other_streams.append(stream_object) + + for packet_values in trunc_list: + ip_packet = packet_values[2] + + source_ip_address = socket.inet_ntoa(ip_packet.src) + destination_ip_address = socket.inet_ntoa(ip_packet.dst) + stream_object = Stream( + packet_values, ip_packet.data, source_ip_address, + destination_ip_address, 'BAD') + stream_object.protocolData = 'Bad truncated IP packet' + other_streams.append(stream_object) + + return other_streams + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Parses a PCAP file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + file_object = file_entry.GetFileObject() + self.ParseFileObject( + parser_context, file_object, file_entry=file_entry, + parser_chain=parser_chain) + file_object.close() + + def ParseFileObject( + self, parser_context, file_object, file_entry=None, parser_chain=None): + """Parses a PCAP file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_object: A file-like object. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Raises: + UnableToParseFile: when the file cannot be parsed. + """ + data = file_object.read(dpkt.pcap.FileHdr.__hdr_len__) + + try: + file_header = dpkt.pcap.FileHdr(data) + packet_header_class = dpkt.pcap.PktHdr + + except (dpkt.NeedData, dpkt.UnpackError) as exception: + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse file: {1:s} with error: {2:s}'.format( + self.NAME, file_entry.name, exception)) + + if file_header.magic == dpkt.pcap.PMUDPCT_MAGIC: + try: + file_header = dpkt.pcap.LEFileHdr(data) + packet_header_class = dpkt.pcap.LEPktHdr + + except (dpkt.NeedData, dpkt.UnpackError) as exception: + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse file: {1:s} with error: {2:s}'.format( + self.NAME, file_entry.name, exception)) + + elif file_header.magic != dpkt.pcap.TCPDUMP_MAGIC: + raise errors.UnableToParseFile(u'Unsupported file signature') + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + packet_number = 1 + connections = {} + other_list = [] + trunc_list = [] + + data = file_object.read(dpkt.pcap.PktHdr.__hdr_len__) + while data: + packet_header = packet_header_class(data) + timestamp = packet_header.tv_sec + (packet_header.tv_usec / 1000000.0) + packet_data = file_object.read(packet_header.caplen) + + ethernet_frame = dpkt.ethernet.Ethernet(packet_data) + + if ethernet_frame.type == dpkt.ethernet.ETH_TYPE_IP: + self._ParseIPPacket( + connections, trunc_list, packet_number, timestamp, + len(ethernet_frame), ethernet_frame.data) + + else: + packet_values = [ + timestamp, packet_number, ethernet_frame, len(ethernet_frame)] + other_list.append(packet_values) + + packet_number += 1 + data = file_object.read(dpkt.pcap.PktHdr.__hdr_len__) + + other_streams = self._ParseOtherStreams(other_list, trunc_list) + + for stream_object in sorted( + connections.values(), key=operator.attrgetter('start_time')): + + if not stream_object.protocol == 'ICMP': + stream_object.Clean() + + event_objects = [ + PcapEvent( + min(stream_object.timestamps), + eventdata.EventTimestamp.START_TIME, stream_object), + PcapEvent( + max(stream_object.timestamps), + eventdata.EventTimestamp.END_TIME, stream_object)] + + parser_context.ProduceEvents( + event_objects, parser_chain=parser_chain, file_entry=file_entry) + + for stream_object in other_streams: + event_objects = [ + PcapEvent( + min(stream_object.timestamps), + eventdata.EventTimestamp.START_TIME, stream_object), + PcapEvent( + max(stream_object.timestamps), + eventdata.EventTimestamp.END_TIME, stream_object)] + parser_context.ProduceEvents( + event_objects, parser_chain=parser_chain, file_entry=file_entry) + + +manager.ParsersManager.RegisterParser(PcapParser) diff --git a/plaso/parsers/pcap_test.py b/plaso/parsers/pcap_test.py new file mode 100644 index 0000000..7a03b03 --- /dev/null +++ b/plaso/parsers/pcap_test.py @@ -0,0 +1,117 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors.. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the PCAP parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import pcap as pcap_formatter +from plaso.parsers import pcap +from plaso.parsers import test_lib + + +class PcapParserTest(test_lib.ParserTestCase): + """Tests for the PCAP parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = pcap.PcapParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['test.pcap']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # PCAP information: + # Number of streams: 96 (TCP: 47, UDP: 39, ICMP: 0, Other: 10) + # + # For each stream 2 event objects are generated one for the start + # and one for the end time. + + self.assertEquals(len(event_objects), 192) + + # Test stream 3 (event object 6). + # Protocol: TCP + # Source IP: 192.168.195.130 + # Dest IP: 63.245.217.43 + # Source Port: 1038 + # Dest Port: 443 + # Stream Type: SSL + # Starting Packet: 4 + # Ending Packet: 6 + + event_object = event_objects[6] + self.assertEquals(event_object.packet_count, 3) + self.assertEquals(event_object.protocol, u'TCP') + self.assertEquals(event_object.source_ip, u'192.168.195.130') + self.assertEquals(event_object.dest_ip, u'63.245.217.43') + self.assertEquals(event_object.dest_port, 443) + self.assertEquals(event_object.source_port, 1038) + self.assertEquals(event_object.stream_type, u'SSL') + self.assertEquals(event_object.first_packet_id, 4) + self.assertEquals(event_object.last_packet_id, 6) + + # Test stream 6 (event object 12). + # Protocol: UDP + # Source IP: 192.168.195.130 + # Dest IP: 192.168.195.2 + # Source Port: 55679 + # Dest Port: 53 + # Stream Type: DNS + # Starting Packet: 4 + # Ending Packet: 6 + # Protocol Data: DNS Query for wpad.localdomain + + event_object = event_objects[12] + self.assertEquals(event_object.packet_count, 5) + self.assertEquals(event_object.protocol, u'UDP') + self.assertEquals(event_object.source_ip, u'192.168.195.130') + self.assertEquals(event_object.dest_ip, u'192.168.195.2') + self.assertEquals(event_object.dest_port, 53) + self.assertEquals(event_object.source_port, 55679) + self.assertEquals(event_object.stream_type, u'DNS') + self.assertEquals(event_object.first_packet_id, 11) + self.assertEquals(event_object.last_packet_id, 1307) + self.assertEquals( + event_object.protocol_data, u'DNS Query for wpad.localdomain') + + expected_msg = ( + u'Source IP: 192.168.195.130 ' + u'Destination IP: 192.168.195.2 ' + u'Source Port: 55679 ' + u'Destination Port: 53 ' + u'Protocol: UDP ' + u'Type: DNS ' + u'Size: 380 ' + u'Protocol Data: DNS Query for wpad.localdomain ' + u'Stream Data: \'\\xb8\\x9c\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00' + u'\\x00\\x00\\x04wpad\\x0blocaldomain\\x00\\x00\\x01\\x00\\x01\\xb8' + u'\\x9c\\x01\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x04wpa\' ' + u'First Packet ID: 11 ' + u'Last Packet ID: 1307 ' + u'Packet Count: 5') + expected_msg_short = ( + u'Type: DNS ' + u'First Packet ID: 11') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/plist.py b/plaso/parsers/plist.py new file mode 100644 index 0000000..5a07431 --- /dev/null +++ b/plaso/parsers/plist.py @@ -0,0 +1,159 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the Property List (Plist) Parser. + +Plaso's engine calls PlistParser when it encounters Plist files to be processed. +""" + +import binascii +import logging + +from binplist import binplist + +from plaso.lib import errors +from plaso.lib import utils +from plaso.parsers import interface +from plaso.parsers import manager + + +class PlistParser(interface.BasePluginsParser): + """De-serializes and parses plists the event objects are generated by plist. + + The Plaso engine calls parsers by their Parse() method. This parser's + Parse() has GetTopLevel() which deserializes plist files using the binplist + library and calls plugins (PlistPlugin) registered through the + interface by their Process() to produce event objects. + + Plugins are how this parser understands the content inside a plist file, + each plugin holds logic specific to a particular plist file. See the + interface and plist_plugins/ directory for examples of how plist plugins are + implemented. + """ + + NAME = 'plist' + DESCRIPTION = u'Parser for binary and text plist files.' + + _plugin_classes = {} + + def __init__(self): + """Initializes a parser object.""" + super(PlistParser, self).__init__() + self._plugins = PlistParser.GetPluginObjects() + + def GetTopLevel(self, file_object, file_name=''): + """Returns the deserialized content of a plist as a dictionary object. + + Args: + file_object: A file-like object to parse. + file_name: The name of the file-like object. + + Returns: + A dictionary object representing the contents of the plist. + """ + try: + top_level_object = binplist.readPlist(file_object) + except binplist.FormatError as exception: + raise errors.UnableToParseFile( + u'[{0:s}] File is not a plist file: {1:s}'.format( + self.NAME, utils.GetUnicodeString(exception))) + except ( + LookupError, binascii.Error, ValueError, AttributeError) as exception: + raise errors.UnableToParseFile( + u'[{0:s}] Unable to parse XML file, reason: {1:s}'.format( + self.NAME, exception)) + except OverflowError as exception: + raise errors.UnableToParseFile( + u'[{0:s}] Unable to parse: {1:s} with error: {2:s}'.format( + self.NAME, file_name, exception)) + + if not top_level_object: + raise errors.UnableToParseFile( + u'[{0:s}] File is not a plist: {1:s}'.format( + self.NAME, utils.GetUnicodeString(file_name))) + + # Since we are using readPlist from binplist now instead of manually + # opening up the BinarPlist file we loose this option. Keep it commented + # out for now but this needs to be tested a bit more. + # TODO: Re-evaluate if we can delete this or still require it. + #if bpl.is_corrupt: + # logging.warning( + # u'[{0:s}] corruption detected in binary plist: {1:s}'.format( + # self.NAME, file_name)) + + return top_level_object + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Parse and extract values from a plist file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + # TODO: Should we rather query the stats object to get the size here? + file_object = file_entry.GetFileObject() + file_size = file_object.get_size() + + if file_size <= 0: + file_object.close() + raise errors.UnableToParseFile( + u'[{0:s}] file size: {1:d} bytes is less equal 0.'.format( + self.NAME, file_size)) + + # 50MB is 10x larger than any plist seen to date. + if file_size > 50000000: + file_object.close() + raise errors.UnableToParseFile( + u'[{0:s}] file size: {1:d} bytes is larger than 50 MB.'.format( + self.NAME, file_size)) + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + top_level_object = None + try: + top_level_object = self.GetTopLevel(file_object, file_entry.name) + except errors.UnableToParseFile: + file_object.close() + raise + + if not top_level_object: + file_object.close() + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse: {1:s} skipping.'.format( + self.NAME, file_entry.name)) + + file_system = file_entry.GetFileSystem() + plist_name = file_system.BasenamePath(file_entry.name) + + for plugin_object in self._plugins: + try: + plugin_object.Process( + parser_context, file_entry=file_entry, parser_chain=parser_chain, + plist_name=plist_name, top_level=top_level_object) + + except errors.WrongPlistPlugin as exception: + logging.debug(u'[{0:s}] Wrong plugin: {1:s} for: {2:s}'.format( + self.NAME, exception[0], exception[1])) + + file_object.close() + + +manager.ParsersManager.RegisterParser(PlistParser) diff --git a/plaso/parsers/plist_plugins/__init__.py b/plaso/parsers/plist_plugins/__init__.py new file mode 100644 index 0000000..14d448a --- /dev/null +++ b/plaso/parsers/plist_plugins/__init__.py @@ -0,0 +1,30 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains an import statement for each plist related plugin.""" + +from plaso.parsers.plist_plugins import airport +from plaso.parsers.plist_plugins import appleaccount +from plaso.parsers.plist_plugins import bluetooth +from plaso.parsers.plist_plugins import ipod +from plaso.parsers.plist_plugins import install_history +from plaso.parsers.plist_plugins import macuser +from plaso.parsers.plist_plugins import safari +from plaso.parsers.plist_plugins import softwareupdate +from plaso.parsers.plist_plugins import spotlight +from plaso.parsers.plist_plugins import spotlight_volume +from plaso.parsers.plist_plugins import timemachine +from plaso.parsers.plist_plugins import default diff --git a/plaso/parsers/plist_plugins/airport.py b/plaso/parsers/plist_plugins/airport.py new file mode 100644 index 0000000..d2c9ecd --- /dev/null +++ b/plaso/parsers/plist_plugins/airport.py @@ -0,0 +1,63 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the airport plist plugin in Plaso.""" + +from plaso.events import plist_event +from plaso.parsers import plist +from plaso.parsers.plist_plugins import interface + + +__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)' + + +class AirportPlugin(interface.PlistPlugin): + """Plist plugin that extracts WiFi information.""" + + NAME = 'plist_airport' + DESCRIPTION = u'Parser for Airport plist files.' + + PLIST_PATH = 'com.apple.airport.preferences.plist' + PLIST_KEYS = frozenset(['RememberedNetworks']) + + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, match=None, + **unused_kwargs): + """Extracts relevant Airport entries. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + match: Optional dictionary containing keys extracted from PLIST_KEYS. + The default is None. + """ + for wifi in match['RememberedNetworks']: + description = ( + u'[WiFi] Connected to network: <{0:s}> using security {1:s}').format( + wifi.get('SSIDString', u'no SSID string'), + wifi.get('SecurityType', u'N/A')) + last_connected = wifi.get('LastConnected') + event_object = plist_event.PlistEvent( + u'/RememberedNetworks', u'item', last_connected, description) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +plist.PlistParser.RegisterPlugin(AirportPlugin) diff --git a/plaso/parsers/plist_plugins/airport_test.py b/plaso/parsers/plist_plugins/airport_test.py new file mode 100644 index 0000000..0f602cc --- /dev/null +++ b/plaso/parsers/plist_plugins/airport_test.py @@ -0,0 +1,69 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the airport plist plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import plist as plist_formatter +from plaso.parsers import plist +from plaso.parsers.plist_plugins import airport +from plaso.parsers.plist_plugins import test_lib + + +class AirportPluginTest(test_lib.PlistPluginTestCase): + """Tests for the airport plist plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = airport.AirportPlugin() + self._parser = plist.PlistParser() + + def testProcess(self): + """Tests the Process function.""" + test_file = self._GetTestFilePath(['com.apple.airport.preferences.plist']) + plist_name = 'com.apple.airport.preferences.plist' + event_queue_consumer = self._ParsePlistFileWithPlugin( + self._parser, self._plugin, test_file, plist_name) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 4) + + timestamps = [] + for event_object in event_objects: + timestamps.append(event_object.timestamp) + expected_timestamps = frozenset([ + 1375144166000000, 1386874984000000, 1386949546000000, + 1386950747000000]) + self.assertTrue(set(timestamps) == expected_timestamps) + + event_object = event_objects[0] + self.assertEqual(event_object.key, u'item') + self.assertEqual(event_object.root, u'/RememberedNetworks') + expected_desc = ( + u'[WiFi] Connected to network: using security ' + u'WPA/WPA2 Personal') + self.assertEqual(event_object.desc, expected_desc) + expected_string = u'/RememberedNetworks/item {0:s}'.format(expected_desc) + expected_short = expected_string[:77] + u'...' + self._TestGetMessageStrings( + event_object, expected_string, expected_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/plist_plugins/appleaccount.py b/plaso/parsers/plist_plugins/appleaccount.py new file mode 100644 index 0000000..f881aa1 --- /dev/null +++ b/plaso/parsers/plist_plugins/appleaccount.py @@ -0,0 +1,111 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a Apple Account plist plugin in Plaso.""" + +from plaso.events import plist_event +from plaso.lib import errors +from plaso.parsers import plist +from plaso.parsers.plist_plugins import interface + + +__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)' + + +class AppleAccountPlugin(interface.PlistPlugin): + """Basic plugin to extract the apple account information.""" + + NAME = 'plist_appleaccount' + DESCRIPTION = u'Parser for Apple account information plist files.' + + PLIST_PATH = u'com.apple.coreservices.appleidauthenticationinfo' + PLIST_KEYS = frozenset(['AuthCertificates', 'AccessorVersions', 'Accounts']) + + def Process( + self, parser_context, file_entry=None, parser_chain=None, plist_name=None, + top_level=None, **kwargs): + """Check if it is a valid Apple account plist file name. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + plist_name: name of the plist file. + top_level: dictionary with the plist file parsed. + """ + if not plist_name.startswith(self.PLIST_PATH): + raise errors.WrongPlistPlugin(self.NAME, plist_name) + super(AppleAccountPlugin, self).Process( + parser_context, file_entry=file_entry, parser_chain=parser_chain, + plist_name=self.PLIST_PATH, top_level=top_level, **kwargs) + + # Generated events: + # Accounts: account name. + # FirstName: first name associated with the account. + # LastName: family name associate with the account. + # CreationDate: timestamp when the account was configured in the system. + # LastSuccessfulConnect: last time when the account was connected. + # ValidationDate: last time when the account was validated. + + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, match=None, + **unused_kwargs): + """Extracts relevant Apple Account entries. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + match: Optional dictionary containing keys extracted from PLIST_KEYS. + The default is None. + """ + root = '/Accounts' + + for name_account, account in match['Accounts'].iteritems(): + general_description = u'{0:s} ({1:s} {2:s})'.format( + name_account, account.get('FirstName', ''), + account.get('LastName', '')) + key = name_account + description = u'Configured Apple account {0:s}'.format( + general_description) + event_object = plist_event.PlistEvent( + root, key, account['CreationDate'], description) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if 'LastSuccessfulConnect' in account: + description = u'Connected Apple account {0:s}'.format( + general_description) + event_object = plist_event.PlistEvent( + root, key, account['LastSuccessfulConnect'], description) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if 'ValidationDate' in account: + description = u'Last validation Apple account {0:s}'.format( + general_description) + event_object = plist_event.PlistEvent( + root, key, account['ValidationDate'], description) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +plist.PlistParser.RegisterPlugin(AppleAccountPlugin) diff --git a/plaso/parsers/plist_plugins/appleaccount_test.py b/plaso/parsers/plist_plugins/appleaccount_test.py new file mode 100644 index 0000000..bd5072f --- /dev/null +++ b/plaso/parsers/plist_plugins/appleaccount_test.py @@ -0,0 +1,79 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Apple account plist plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import plist as plist_formatter +from plaso.parsers import plist +from plaso.parsers.plist_plugins import appleaccount +from plaso.parsers.plist_plugins import test_lib + + +class AppleAccountPluginTest(test_lib.PlistPluginTestCase): + """Tests for the Apple account plist plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = appleaccount.AppleAccountPlugin() + self._parser = plist.PlistParser() + + def testProcess(self): + """Tests the Process function.""" + plist_file = (u'com.apple.coreservices.appleidauthenticationinfo.' + u'ABC0ABC1-ABC0-ABC0-ABC0-ABC0ABC1ABC2.plist') + test_file = self._GetTestFilePath([plist_file]) + plist_name = plist_file + event_queue_consumer = self._ParsePlistFileWithPlugin( + self._parser, self._plugin, test_file, plist_name) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 3) + + timestamps = [] + for event_object in event_objects: + timestamps.append(event_object.timestamp) + expected_timestamps = frozenset([ + 1372106802000000, 1387980032000000, 1387980032000000]) + self.assertTrue(set(timestamps) == expected_timestamps) + + event_object = event_objects[0] + self.assertEqual(event_object.root, u'/Accounts') + self.assertEqual(event_object.key, u'email@domain.com') + expected_desc = ( + u'Configured Apple account email@domain.com (Joaquin Moreno Garijo)') + self.assertEqual(event_object.desc, expected_desc) + expected_string = u'/Accounts/email@domain.com {0:s}'.format(expected_desc) + expected_short = expected_string[:77] + u'...' + self._TestGetMessageStrings( + event_object, expected_string, expected_short) + + event_object = event_objects[1] + expected_desc = (u'Connected Apple account ' + u'email@domain.com (Joaquin Moreno Garijo)') + self.assertEqual(event_object.desc, expected_desc) + + event_object = event_objects[2] + expected_desc = (u'Last validation Apple account ' + u'email@domain.com (Joaquin Moreno Garijo)') + self.assertEqual(event_object.desc, expected_desc) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/plist_plugins/bluetooth.py b/plaso/parsers/plist_plugins/bluetooth.py new file mode 100644 index 0000000..dff51ae --- /dev/null +++ b/plaso/parsers/plist_plugins/bluetooth.py @@ -0,0 +1,99 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a default plist plugin in Plaso.""" + +from plaso.events import plist_event +from plaso.parsers import plist +from plaso.parsers.plist_plugins import interface + + +class BluetoothPlugin(interface.PlistPlugin): + """Basic plugin to extract interesting Bluetooth related keys.""" + + NAME = 'plist_bluetooth' + DESCRIPTION = u'Parser for Bluetooth plist files.' + + PLIST_PATH = 'com.apple.bluetooth.plist' + PLIST_KEYS = frozenset(['DeviceCache', 'PairedDevices']) + + # LastInquiryUpdate = Device connected via Bluetooth Discovery. Updated + # when a device is detected in discovery mode. E.g. BT headphone power + # on. Pairing is not required for a device to be discovered and cached. + # + # LastNameUpdate = When the human name was last set. Usually done only once + # during initial setup. + # + # LastServicesUpdate = Time set when device was polled to determine what it + # is. Usually done at setup or manually requested via advanced menu. + + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, match=None, + **unused_kwargs): + """Extracts relevant BT entries. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + match: Optional dictionary containing extracted keys from PLIST_KEYS. + The default is None. + """ + root = '/DeviceCache' + + for device, value in match['DeviceCache'].items(): + name = value.get('Name', '') + if name: + name = u''.join(('Name:', name)) + + if device in match['PairedDevices']: + desc = 'Paired:True {0:s}'.format(name) + key = device + if 'LastInquiryUpdate' in value: + event_object = plist_event.PlistEvent( + root, key, value['LastInquiryUpdate'], desc) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if value.get('LastInquiryUpdate'): + desc = u' '.join(filter(None, ('Bluetooth Discovery', name))) + key = u''.join((device, '/LastInquiryUpdate')) + event_object = plist_event.PlistEvent( + root, key, value['LastInquiryUpdate'], desc) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if value.get('LastNameUpdate'): + desc = u' '.join(filter(None, ('Device Name Set', name))) + key = u''.join((device, '/LastNameUpdate')) + event_object = plist_event.PlistEvent( + root, key, value['LastNameUpdate'], desc) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if value.get('LastServicesUpdate'): + desc = desc = u' '.join(filter(None, ('Services Updated', name))) + key = ''.join((device, '/LastServicesUpdate')) + event_object = plist_event.PlistEvent( + root, key, value['LastServicesUpdate'], desc) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +plist.PlistParser.RegisterPlugin(BluetoothPlugin) diff --git a/plaso/parsers/plist_plugins/bluetooth_test.py b/plaso/parsers/plist_plugins/bluetooth_test.py new file mode 100644 index 0000000..fd79baa --- /dev/null +++ b/plaso/parsers/plist_plugins/bluetooth_test.py @@ -0,0 +1,85 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Bluetooth plist plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import plist as plist_formatter +from plaso.parsers import plist +from plaso.parsers.plist_plugins import bluetooth +from plaso.parsers.plist_plugins import test_lib + + +class TestBtPlugin(test_lib.PlistPluginTestCase): + """Tests for the Bluetooth plist plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = bluetooth.BluetoothPlugin() + self._parser = plist.PlistParser() + + def testProcess(self): + """Tests the Process function.""" + test_file = self._GetTestFilePath(['plist_binary']) + plist_name = 'com.apple.bluetooth.plist' + event_queue_consumer = self._ParsePlistFileWithPlugin( + self._parser, self._plugin, test_file, plist_name) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 14) + + paired_event_objects = [] + timestamps = [] + for event_object in event_objects: + timestamps.append(event_object.timestamp) + if event_object.desc.startswith(u'Paired'): + paired_event_objects.append(event_object) + + # Ensure all 14 events and times from the plist are parsed correctly. + self.assertEquals(len(timestamps), 14) + + expected_timestamps = frozenset([ + 1341957896010535, 1341957896010535, 1350666385239661, 1350666391557044, + 1341957900020116, 1302199013524275, 1301012201414766, 1351818797324095, + 1351818797324095, 1351819298997672, 1351818803000000, 1351827808261762, + 1345251268370453, 1345251192528750]) + + self.assertTrue(set(timestamps) == expected_timestamps) + + # Ensure two paired devices are matched. + self.assertEquals(len(paired_event_objects), 2) + + # One of the paired event object descriptions should contain the string: + # Paired:True Name:Apple Magic Trackpad 2. + paired_descriptions = [ + event_object.desc for event_object in paired_event_objects] + + self.assertTrue( + 'Paired:True Name:Apple Magic Trackpad 2' in paired_descriptions) + + expected_string = ( + u'/DeviceCache/44-00-00-00-00-04 ' + u'Paired:True ' + u'Name:Apple Magic Trackpad 2') + + self._TestGetMessageStrings(event_object, expected_string, expected_string) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/plist_plugins/default.py b/plaso/parsers/plist_plugins/default.py new file mode 100644 index 0000000..23dac2b --- /dev/null +++ b/plaso/parsers/plist_plugins/default.py @@ -0,0 +1,86 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a default plist plugin in Plaso.""" + +import datetime +import logging + +from plaso.events import plist_event +from plaso.parsers import plist +from plaso.parsers.plist_plugins import interface + + +class DefaultPlugin(interface.PlistPlugin): + """Basic plugin to extract keys with timestamps as values from plists.""" + + NAME = 'plist_default' + DESCRIPTION = u'Parser for plist files.' + + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, top_level=None, + **unused_kwargs): + """Simple method to exact date values from a Plist. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + top_level: Plist in dictionary form. + """ + for root, key, value in interface.RecurseKey(top_level): + if isinstance(value, datetime.datetime): + event_object = plist_event.PlistEvent(root, key, value) + parser_context.ProduceEvent( + event_object, file_entry=file_entry, parser_chain=parser_chain) + + # TODO: Binplist keeps a list of offsets but not mapped to a key. + # adjust code when there is a way to map keys to offsets. + + # TODO: move this into the parser as with the olecf plugins. + def Process( + self, parser_context, file_entry=None, parser_chain=None, plist_name=None, + top_level=None, **kwargs): + """Overwrite the default Process function so it always triggers. + + Process() checks if the current plist being processed is a match for a + plugin by comparing the PATH and KEY requirements defined by a plugin. If + both match processing continues; else raise WrongPlistPlugin. + + The purpose of the default plugin is to always trigger on any given plist + file, thus it needs to overwrite the default behavior of comparing PATH + and KEY. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + plist_name: Name of the plist file. + top_level: Plist in dictionary form. + """ + logging.debug(u'Plist {0:s} plugin used for: {1:s}'.format( + self.NAME, plist_name)) + self.GetEntries( + parser_context, file_entry=file_entry, parser_chain=parser_chain, + top_level=top_level, **kwargs) + + +plist.PlistParser.RegisterPlugin(DefaultPlugin) diff --git a/plaso/parsers/plist_plugins/default_test.py b/plaso/parsers/plist_plugins/default_test.py new file mode 100644 index 0000000..cb595c3 --- /dev/null +++ b/plaso/parsers/plist_plugins/default_test.py @@ -0,0 +1,110 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the default plist plugin.""" + +import datetime +import unittest + +# pylint: disable=unused-import +from plaso.formatters import plist as plist_formatter +from plaso.lib import timelib_test +from plaso.parsers.plist_plugins import default +from plaso.parsers.plist_plugins import test_lib + +import pytz + + +class TestDefaultPlist(test_lib.PlistPluginTestCase): + """Tests for the default plist plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = default.DefaultPlugin() + + def testProcessSingle(self): + """Tests Process on a plist containing a root, value and timestamp.""" + top_level_dict_single = { + 'DE-00-AD-00-BE-EF': { + 'Name': 'DBF Industries Slideshow Lazer', 'LastUsed': + datetime.datetime( + 2012, 11, 2, 1, 21, 38, 997672, tzinfo=pytz.utc)}} + + event_object_generator = self._ParsePlistWithPlugin( + self._plugin, 'single', top_level_dict_single) + event_objects = self._GetEventObjectsFromQueue(event_object_generator) + + self.assertEquals(len(event_objects), 1) + + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-11-02 01:21:38.997672') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals(event_object.root, u'/DE-00-AD-00-BE-EF') + self.assertEquals(event_object.key, u'LastUsed') + + expected_string = ( + u'/DE-00-AD-00-BE-EF/LastUsed') + + self._TestGetMessageStrings(event_object, expected_string, expected_string) + + def testProcessMulti(self): + """Tests Process on a plist containing five keys with date values.""" + top_level_dict_many_keys = { + 'DeviceCache': { + '44-00-00-00-00-04': { + 'Name': 'Apple Magic Trackpad 2', 'LMPSubversion': 796, + 'LMPVersion': 3, 'PageScanMode': 0, 'ClassOfDevice': 9620, + 'SupportedFeatures': '\x00\x00\x00\x00', 'Manufacturer': 76, + 'PageScanPeriod': 0, 'ClockOffset': 17981, 'LastNameUpdate': + datetime.datetime( + 2012, 11, 2, 1, 21, 38, 997672, tzinfo=pytz.utc), + 'InquiryRSSI': 198, 'PageScanRepetitionMode': 1, + 'LastServicesUpdate': + datetime.datetime(2012, 11, 2, 1, 13, 23, tzinfo=pytz.utc), + 'displayName': 'Apple Magic Trackpad 2', 'LastInquiryUpdate': + datetime.datetime( + 2012, 11, 2, 1, 13, 17, 324095, tzinfo=pytz.utc), + 'Services': '', 'BatteryPercent': 0.61}, + '44-00-00-00-00-02': { + 'Name': 'test-macpro', 'ClockOffset': 28180, 'ClassOfDevice': + 3670276, 'PageScanMode': 0, 'LastNameUpdate': + datetime.datetime( + 2011, 4, 7, 17, 56, 53, 524275, tzinfo=pytz.utc), + 'PageScanPeriod': 2, 'PageScanRepetitionMode': 1, + 'LastInquiryUpdate': + datetime.datetime( + 2012, 7, 10, 22, 5, 0, 20116, tzinfo=pytz.utc)}}} + + event_queue_consumer = self._ParsePlistWithPlugin( + self._plugin, 'nested', top_level_dict_many_keys) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 5) + + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-04-07 17:56:53.524275') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals(event_object.root, u'/DeviceCache/44-00-00-00-00-02') + self.assertEquals(event_object.key, u'LastNameUpdate') + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/plist_plugins/install_history.py b/plaso/parsers/plist_plugins/install_history.py new file mode 100644 index 0000000..c66f450 --- /dev/null +++ b/plaso/parsers/plist_plugins/install_history.py @@ -0,0 +1,67 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the install history plist plugin in Plaso.""" + +from plaso.events import plist_event +from plaso.parsers import plist +from plaso.parsers.plist_plugins import interface + + +__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)' + + +class InstallHistoryPlugin(interface.PlistPlugin): + """Plist plugin that extracts the installation history.""" + + NAME = 'plist_install_history' + DESCRIPTION = u'Parser for installation history plist files.' + + PLIST_PATH = 'InstallHistory.plist' + PLIST_KEYS = frozenset([ + 'date', 'displayName', 'displayVersion', + 'processName', 'packageIdentifiers']) + + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, top_level=None, + **unused_kwargs): + """Extracts relevant install history entries. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + top_level: Optional plist in dictionary form. The default is None. + """ + for entry in top_level: + packages = [] + for package in entry.get('packageIdentifiers'): + packages.append(package) + description = ( + u'Installation of [{0:s} {1:s}] using [{2:s}]. ' + u'Packages: {3:s}.').format( + entry.get('displayName'), entry.get('displayVersion'), + entry.get('processName'), u', '.join(packages)) + event_object = plist_event.PlistEvent( + u'/item', u'', entry.get('date'), description) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +plist.PlistParser.RegisterPlugin(InstallHistoryPlugin) diff --git a/plaso/parsers/plist_plugins/install_history_test.py b/plaso/parsers/plist_plugins/install_history_test.py new file mode 100644 index 0000000..fd0a6e7 --- /dev/null +++ b/plaso/parsers/plist_plugins/install_history_test.py @@ -0,0 +1,77 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the install history plist plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import plist as plist_formatter +from plaso.parsers import plist +from plaso.parsers.plist_plugins import install_history +from plaso.parsers.plist_plugins import test_lib + + +class InstallHistoryPluginTest(test_lib.PlistPluginTestCase): + """Tests for the install history plist plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = install_history.InstallHistoryPlugin() + self._parser = plist.PlistParser() + + def testProcess(self): + """Tests the Process function.""" + test_file = self._GetTestFilePath(['InstallHistory.plist']) + plist_name = 'InstallHistory.plist' + event_queue_consumer = self._ParsePlistFileWithPlugin( + self._parser, self._plugin, test_file, plist_name) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 7) + + timestamps = [] + for event_object in event_objects: + timestamps.append(event_object.timestamp) + expected_timestamps = frozenset([ + 1384225175000000, 1388205491000000, 1388232883000000, 1388232883000000, + 1388232883000000, 1388232883000000, 1390941528000000]) + self.assertTrue(set(timestamps) == expected_timestamps) + + event_object = event_objects[0] + self.assertEqual(event_object.key, u'') + self.assertEqual(event_object.root, u'/item') + expected_desc = ( + u'Installation of [OS X 10.9 (13A603)] using [OS X Installer]. ' + u'Packages: com.apple.pkg.BaseSystemBinaries, ' + u'com.apple.pkg.BaseSystemResources, ' + u'com.apple.pkg.Essentials, com.apple.pkg.BSD, ' + u'com.apple.pkg.JavaTools, com.apple.pkg.AdditionalEssentials, ' + u'com.apple.pkg.AdditionalSpeechVoices, ' + u'com.apple.pkg.AsianLanguagesSupport, com.apple.pkg.MediaFiles, ' + u'com.apple.pkg.JavaEssentials, com.apple.pkg.OxfordDictionaries, ' + u'com.apple.pkg.X11redirect, com.apple.pkg.OSInstall, ' + u'com.apple.pkg.update.compatibility.2013.001.') + self.assertEqual(event_object.desc, expected_desc) + expected_string = u'/item/ {}'.format(expected_desc) + expected_short = expected_string[:77] + u'...' + self._TestGetMessageStrings( + event_object, expected_string, expected_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/plist_plugins/interface.py b/plaso/parsers/plist_plugins/interface.py new file mode 100644 index 0000000..1dd20ec --- /dev/null +++ b/plaso/parsers/plist_plugins/interface.py @@ -0,0 +1,323 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Plist_interface contains basic interface for plist plugins within Plaso. + +Plist files are only one example of a type of object that the Plaso tool is +expected to encounter and process. There can be and are many other parsers +which are designed to process specific data types. + +PlistPlugin defines the attributes necessary for registration, discovery +and operation of plugins for plist files which will be used by PlistParser. +""" + +import abc +import logging + +from plaso.lib import errors +from plaso.parsers import plugins + + +class PlistPlugin(plugins.BasePlugin): + """This is an abstract class from which plugins should be based. + + The following are the attributes and methods expected to be overridden by a + plugin. + + Attributes: + PLIST_PATH - string of the filename the plugin is designed to process. + PLIST_KEY - list of keys holding values that are necessary for processing. + + Please note, PLIST_KEY is cAse sensitive and for a plugin to match a + plist file needs to contain at minimum the number of keys needed for + processing or WrongPlistPlugin is raised. + + For example if a Plist file contains the following keys, + {'foo': 1, 'bar': 2, 'opt': 3} with 'foo' and 'bar' being keys critical to + processing define PLIST_KEY as ['foo', 'bar']. If 'opt' is only optionally + defined it can still be accessed by manually processing self.top_level from + the plugin. + + Methods: + GetEntries() - extract and format info from keys and yields event.PlistEvent. + """ + + NAME = 'plist_plugin' + + # PLIST_PATH is a string for the filename this parser is designed to process. + # This is expected to be overriden by the processing plugin. + # Ex. 'com.apple.bluetooth.plist' + PLIST_PATH = 'any' + + # PLIST_KEYS is a list of keys required by a plugin. + # This is expected to be overriden by the processing plugin. + # Ex. frozenset(['DeviceCache', 'PairedDevices']) + PLIST_KEYS = frozenset(['any']) + + # This is expected to be overriden by the processing plugin. + # URLS should contain a list of URLs with additional information about + # this key or value. + # Ex. ['http://www.forensicswiki.org/wiki/Property_list_(plist)'] + URLS = [] + + @abc.abstractmethod + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, top_level=None, + match=None, **unused_kwargs): + """Extracts event objects from the values of entries within a plist. + + This is the main method that a plist plugin needs to implement. + + The contents of the plist keys defined in PLIST_KEYS will be made available + to the plugin as self.matched{'KEY': 'value'}. The plugin should implement + logic to parse this into a useful event for incorporation into the Plaso + timeline. + + For example if you want to note the timestamps of when devices were + LastInquiryUpdated you would need to examine the bluetooth config file + called 'com.apple.bluetooth' and need to look at devices under the key + 'DeviceCache'. To do this the plugin needs to define + PLIST_PATH = 'com.apple.bluetooth' and PLIST_KEYS = + frozenset(['DeviceCache']). IMPORTANT: this interface requires exact names + and is case sensitive. A unit test based on a real world file is expected + for each plist plugin. + + When a file with this key is encountered during processing self.matched is + populated and the plugin's GetEntries() is called. The plugin would have + self.matched = {'DeviceCache': [{'DE:AD:BE:EF:01': {'LastInquiryUpdate': + DateTime_Object}, 'DE:AD:BE:EF:01': {'LastInquiryUpdate': + DateTime_Object}'...}]} and needs to implement logic here to extract + values, format, and produce the data as a event.PlistEvent. + + The attributes for a PlistEvent should include the following: + root = Root key this event was extracted from. E.g. DeviceCache/ + key = Key the value resided in. E.g. 'DE:AD:BE:EF:01' + time = Date this artifact was created in microseconds(usec) from epoch. + desc = Short description. E.g. 'Device LastInquiryUpdated' + + See plist/bluetooth.py for the implemented example plugin. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + top_level: Optional plist in dictionary form. The default is None. + match: Optional dictionary containing extracted keys from PLIST_KEYS. + The default is None. + """ + + def Process( + self, parser_context, file_entry=None, parser_chain=None, plist_name=None, + top_level=None, **kwargs): + """Determine if this is the correct plugin; if so proceed with processing. + + Process() checks if the current plist being processed is a match for a + plugin by comparing the PATH and KEY requirements defined by a plugin. If + both match processing continues; else raise WrongPlistPlugin. + + This function also extracts the required keys as defined in self.PLIST_KEYS + from the plist and stores the result in self.match[key] and calls + self.GetEntries() which holds the processing logic implemented by the + plugin. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + plist_name: Name of the plist file. + top_level: Plist in dictionary form. + + Raises: + WrongPlistPlugin: If this plugin is not able to process the given file. + ValueError: If top_level or plist_name are not set. + """ + if plist_name is None or top_level is None: + raise ValueError(u'Top level or plist name are not set.') + + if plist_name.lower() != self.PLIST_PATH.lower(): + raise errors.WrongPlistPlugin(self.NAME, plist_name) + + if isinstance(top_level, dict): + if not set(top_level.keys()).issuperset(self.PLIST_KEYS): + raise errors.WrongPlistPlugin(self.NAME, plist_name) + + else: + # Make sure we are getting back an object that has an iterator. + if not hasattr(top_level, '__iter__'): + raise errors.WrongPlistPlugin(self.NAME, plist_name) + + # This is a list and we need to just look at the first level + # of keys there. + keys = [] + for top_level_entry in top_level: + if isinstance(top_level_entry, dict): + keys.extend(top_level_entry.keys()) + + # Compare this is a set, which removes possible duplicate entries + # in the list. + if not set(keys).issuperset(self.PLIST_KEYS): + raise errors.WrongPlistPlugin(self.NAME, plist_name) + + # This will raise if unhandled keyword arguments are passed. + super(PlistPlugin, self).Process(parser_context, **kwargs) + + logging.debug(u'Plist Plugin Used: {0:s} for: {1:s}'.format( + self.NAME, plist_name)) + match = GetKeys(top_level, self.PLIST_KEYS) + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + self.GetEntries( + parser_context, file_entry=file_entry, parser_chain=parser_chain, + top_level=top_level, match=match) + + +def RecurseKey(recur_item, root='', depth=15): + """Flattens nested dictionaries and lists by yielding it's values. + + The hierarchy of a plist file is a series of nested dictionaries and lists. + This is a helper function helps plugins navigate the structure without + having to reimplement their own recursive methods. + + This method implements an overridable depth limit to prevent processing + extremely deeply nested plists. If the limit is reached a debug message is + logged indicating which key processing stopped on. + + Example Input Plist: + recur_item = { DeviceRoot: { DeviceMAC1: [Value1, Value2, Value3], + DeviceMAC2: [Value1, Value2, Value3]}} + + Example Output: + ('', DeviceRoot, {DeviceMACs...}) + (DeviceRoot, DeviceMAC1, [Value1, Value2, Value3]) + (DeviceRoot, DeviceMAC2, [Value1, Value2, Value3]) + + Args: + recur_item: An object to be checked for additional nested items. + root: The pathname of the current working key. + depth: A counter to ensure we stop at the maximum recursion depth. + + Yields: + A tuple of the root, key, and value from a plist. + """ + if depth < 1: + logging.debug(u'Recursion limit hit for key: {0:s}'.format(root)) + return + + if type(recur_item) in (list, tuple): + for recur in recur_item: + for key in RecurseKey(recur, root, depth): + yield key + return + + if not hasattr(recur_item, 'iteritems'): + return + + for key, value in recur_item.iteritems(): + yield root, key, value + if isinstance(value, dict): + value = [value] + if isinstance(value, list): + for item in value: + if isinstance(item, dict): + for keyval in RecurseKey( + item, root=root + u'/' + key, depth=depth - 1): + yield keyval + + +def GetKeys(top_level, keys, depth=1): + """Helper function to return keys nested in a plist dict. + + By default this function will return the values for the named keys requested + by a plugin in match dictonary objecte. The default setting is to look + a single layer down from the root (same as the check for plugin + applicability). This level is suitable for most cases. + + For cases where there is varability in the name at the first level + (e.g. it is the MAC addresses of a device, or a UUID) it is possible to + override the depth limit and use GetKeys to fetch from a deeper level. + + E.g. + Top_Level (root): # depth = 0 + |-- Key_Name_is_UUID_Generated_At_Install 1234-5678-8 # depth = 1 + | |-- Interesting_SubKey_with_value_to_Process: [Values, ...] # depth = 2 + + Args: + top_level: Plist in dictionary form. + keys: A list of keys that should be returned. + depth: Defines how many levels deep to check for a match. + + Returns: + A dictionary with just the keys requested or an empty dict if the plist + is flat, eg. top_level is a list instead of a dict object. + """ + match = {} + if not isinstance(top_level, dict): + # Return an empty dict here if top_level is a list object, which happens + # if the plist file is flat. + return match + keys = set(keys) + + if depth == 1: + for key in keys: + match[key] = top_level.get(key, None) + else: + for _, parsed_key, parsed_value in RecurseKey(top_level, depth=depth): + if parsed_key in keys: + match[parsed_key] = parsed_value + if set(match.keys()) == keys: + return match + return match + + +def GetKeysDefaultEmpty(top_level, keys, depth=1): + """Return keys nested in a plist dict, defaulting to an empty value. + + The method GetKeys fails if the supplied key does not exist within the + plist object. This alternate method behaves the same way as GetKeys + except that instead of raising an error if the key doesn't exist it will + assign a default empty value ('') to the field. + + Args: + top_level: Plist in dictionary form. + keys: A list of keys that should be returned. + depth: Defines how many levels deep to check for a match. + + Returns: + A dictionary with just the keys requested. + """ + keys = set(keys) + match = {} + + if depth == 1: + for key in keys: + value = top_level.get(key, None) + if value is not None: + match[key] = value + else: + for _, parsed_key, parsed_value in RecurseKey(top_level, depth=depth): + if parsed_key in keys: + match[parsed_key] = parsed_value + if set(match.keys()) == keys: + return match + return match diff --git a/plaso/parsers/plist_plugins/interface_test.py b/plaso/parsers/plist_plugins/interface_test.py new file mode 100644 index 0000000..8355055 --- /dev/null +++ b/plaso/parsers/plist_plugins/interface_test.py @@ -0,0 +1,135 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the plist plugin interface.""" + +import unittest + +from plaso.events import plist_event +from plaso.lib import errors +from plaso.parsers import plist +from plaso.parsers.plist_plugins import interface +from plaso.parsers.plist_plugins import test_lib + + +class MockPlugin(interface.PlistPlugin): + """Mock plugin.""" + + NAME = 'mock_plist_plugin' + DESCRIPTION = u'Parser for testing parsing plist files.' + + PLIST_PATH = 'plist_binary' + PLIST_KEYS = frozenset(['DeviceCache', 'PairedDevices']) + + def GetEntries(self, parser_context, **unused_kwargs): + event_object = plist_event.PlistEvent( + u'/DeviceCache/44-00-00-00-00-00', u'LastInquiryUpdate', + 1351827808261762) + parser_context.ProduceEvent(event_object, parser_chain=self.NAME) + + +class TestPlistPlugin(test_lib.PlistPluginTestCase): + """Tests for the plist plugin interface.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._top_level_dict = { + 'DeviceCache': { + '44-00-00-00-00-04': { + 'Name': 'Apple Magic Trackpad 2', 'LMPSubversion': 796, + 'Services': '', 'BatteryPercent': 0.61}, + '44-00-00-00-00-02': { + 'Name': 'test-macpro', 'ClockOffset': 28180, + 'PageScanPeriod': 2, 'PageScanRepetitionMode': 1}}} + + def testGetPluginNames(self): + """Tests the GetPluginNames function.""" + plugin_names = plist.PlistParser.GetPluginNames() + + self.assertNotEquals(plugin_names, []) + + self.assertTrue('plist_default' in plugin_names) + + def testProcess(self): + """Tests the Process function.""" + # Ensure the plugin only processes if both filename and keys exist. + plugin_object = MockPlugin() + + # Test correct filename and keys. + top_level = {'DeviceCache': 1, 'PairedDevices': 1} + event_object_generator = self._ParsePlistWithPlugin( + plugin_object, 'plist_binary', top_level) + event_objects = self._GetEventObjectsFromQueue(event_object_generator) + + self.assertEquals(len(event_objects), 1) + + # Correct filename with odd filename cAsinG. Adding an extra useless key. + top_level = {'DeviceCache': 1, 'PairedDevices': 1, 'R@ndomExtraKey': 1} + event_object_generator = self._ParsePlistWithPlugin( + plugin_object, 'pLiSt_BinAry', top_level) + event_objects = self._GetEventObjectsFromQueue(event_object_generator) + + self.assertEquals(len(event_objects), 1) + + # Test wrong filename. + top_level = {'DeviceCache': 1, 'PairedDevices': 1} + with self.assertRaises(errors.WrongPlistPlugin): + _ = self._ParsePlistWithPlugin( + plugin_object, 'wrong_file.plist', top_level) + + # Test not enough required keys. + top_level = {'Useless_Key': 0, 'PairedDevices': 1} + with self.assertRaises(errors.WrongPlistPlugin): + _ = self._ParsePlistWithPlugin( + plugin_object, 'plist_binary.plist', top_level) + + def testRecurseKey(self): + """Tests the RecurseKey function.""" + # Ensure with a depth of 1 we only return the root key. + result = list(interface.RecurseKey(self._top_level_dict, depth=1)) + self.assertEquals(len(result), 1) + + # Trying again with depth limit of 2 this time. + result = list(interface.RecurseKey(self._top_level_dict, depth=2)) + self.assertEquals(len(result), 3) + + # A depth of two should gives us root plus the two devices. Let's check. + my_keys = [] + for unused_root, key, unused_value in result: + my_keys.append(key) + expected = set(['DeviceCache', '44-00-00-00-00-04', '44-00-00-00-00-02']) + self.assertTrue(expected == set(my_keys)) + + def testGetKeys(self): + """Tests the GetKeys function.""" + # Match DeviceCache from the root level. + key = ['DeviceCache'] + result = interface.GetKeys(self._top_level_dict, key) + self.assertEquals(len(result), 1) + + # Look for a key nested a layer beneath DeviceCache from root level. + # Note: overriding the default depth to look deeper. + key = ['44-00-00-00-00-02'] + result = interface.GetKeys(self._top_level_dict, key, depth=2) + self.assertEquals(len(result), 1) + + # Check the value of the result was extracted as expected. + self.assertTrue('test-macpro' == result[key[0]]['Name']) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/plist_plugins/ipod.py b/plaso/parsers/plist_plugins/ipod.py new file mode 100644 index 0000000..4daa1c1 --- /dev/null +++ b/plaso/parsers/plist_plugins/ipod.py @@ -0,0 +1,91 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a plist plugin for the iPod/iPhone storage plist.""" + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.parsers import plist +from plaso.parsers.plist_plugins import interface + + +class IPodPlistEvent(time_events.PythonDatetimeEvent): + """An event object for an entry in the iPod plist file.""" + + DATA_TYPE = 'ipod:device:entry' + + def __init__(self, datetime_timestamp, device_id, device_info): + """Initialize the event. + + Args: + datetime_timestamp: The timestamp for the event as a datetime object. + device_id: The device ID. + device_info: A dict that contains extracted information from the plist. + """ + super(IPodPlistEvent, self).__init__( + datetime_timestamp, eventdata.EventTimestamp.LAST_CONNECTED) + + self.device_id = device_id + + # Save the other attributes. + for key, value in device_info.iteritems(): + if key == 'Connected': + continue + attribute_name = key.lower().replace(u' ', u'_') + setattr(self, attribute_name, value) + + +class IPodPlugin(interface.PlistPlugin): + """Plugin to extract iPod/iPad/iPhone device information.""" + + NAME = 'ipod_device' + DESCRIPTION = u'Parser for iPod, iPad and iPhone plist files.' + + PLIST_PATH = 'com.apple.iPod.plist' + PLIST_KEYS = frozenset(['Devices']) + + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, match=None, + **unused_kwargs): + """Extract device information from the iPod plist. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + match: Optional dictionary containing keys extracted from PLIST_KEYS. + The default is None. + """ + if not 'Devices' in match: + return + + devices = match['Devices'] + if not devices: + return + + for device, device_info in devices.iteritems(): + if 'Connected' not in device_info: + continue + event_object = IPodPlistEvent( + device_info.get('Connected'), device, device_info) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +plist.PlistParser.RegisterPlugin(IPodPlugin) diff --git a/plaso/parsers/plist_plugins/ipod_test.py b/plaso/parsers/plist_plugins/ipod_test.py new file mode 100644 index 0000000..addd128 --- /dev/null +++ b/plaso/parsers/plist_plugins/ipod_test.py @@ -0,0 +1,77 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the iPod plist plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import ipod as ipod_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import plist +from plaso.parsers.plist_plugins import ipod +from plaso.parsers.plist_plugins import test_lib + + +class TestIPodPlugin(test_lib.PlistPluginTestCase): + """Tests for the iPod plist plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = ipod.IPodPlugin() + self._parser = plist.PlistParser() + + def testProcess(self): + """Tests the Process function.""" + plist_name = 'com.apple.iPod.plist' + test_file = self._GetTestFilePath([plist_name]) + event_queue_consumer = self._ParsePlistFileWithPlugin( + self._parser, self._plugin, test_file, plist_name) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 4) + + event_object = event_objects[1] + + timestamp = timelib_test.CopyStringToTimestamp('2013-10-09 19:27:54') + self.assertEquals(event_object.timestamp, timestamp) + + expected_string = ( + u'Device ID: 4C6F6F6E65000000 Type: iPhone [10016] Connected 1 times ' + u'Serial nr: 526F676572 IMEI [012345678901234]') + + self._TestGetMessageStrings( + event_object, expected_string, expected_string[0:77] + '...') + + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.LAST_CONNECTED) + + self.assertEquals(event_object.device_class, u'iPhone') + self.assertEquals(event_object.device_id, u'4C6F6F6E65000000') + self.assertEquals(event_object.firmware_version, 256) + self.assertEquals(event_object.imei, u'012345678901234') + self.assertEquals(event_object.use_count, 1) + + event_object = event_objects[3] + timestamp = timelib_test.CopyStringToTimestamp('1995-11-22 18:25:07') + self.assertEquals(event_object.timestamp, timestamp) + self.assertEquals(event_object.device_id, u'0000A11300000000') + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/plist_plugins/macuser.py b/plaso/parsers/plist_plugins/macuser.py new file mode 100644 index 0000000..c557678 --- /dev/null +++ b/plaso/parsers/plist_plugins/macuser.py @@ -0,0 +1,172 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the Mac OS X user plist plugin.""" + +# TODO: Only plists from Mac OS X 10.8 and 10.9 were tested. Look at other +# versions as well. + +import binascii + +from binplist import binplist +from dfvfs.file_io import fake_file_io +from dfvfs.path import fake_path_spec +from dfvfs.resolver import context +from xml.etree import ElementTree + +from plaso.events import plist_event +from plaso.lib import timelib +from plaso.parsers import plist +from plaso.parsers.plist_plugins import interface + + +__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)' + + +class MacUserPlugin(interface.PlistPlugin): + """Basic plugin to extract timestamp Mac user information.""" + + NAME = 'plist_macuser' + DESCRIPTION = u'Parser for Mac OS X user plist files.' + + # The PLIST_PATH is dynamic, "user".plist is the name of the + # Mac OS X user. + PLIST_KEYS = frozenset([ + 'name', 'uid', 'home', + 'passwordpolicyoptions', 'ShadowHashData']) + + _ROOT = u'/' + + def Process( + self, parser_context, file_entry=None, parser_chain=None, plist_name=None, + top_level=None, **kwargs): + """Check if it is a valid Mac OS X system account plist file name. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + plist_name: name of the plist file. + top_level: dictionary with the plist file parsed. + """ + super(MacUserPlugin, self).Process( + parser_context, file_entry=file_entry, parser_chain=parser_chain, + plist_name=self.PLIST_PATH, top_level=top_level, **kwargs) + + # Generated events: + # name: string with the system user. + # uid: user ID. + # passwordpolicyoptions: XML Plist structures with the timestamp. + # passwordLastSetTime: last time the password was changed. + # lastLoginTimestamp: last time the user was authenticated (*). + # failedLoginTimestamp: last time the user passwd was incorrectly(*). + # failedLoginCount: times of incorrect passwords. + # (*): depending on the situation, these timestamps are reset (0 value). + # It is translated by the library as a 2001-01-01 00:00:00 (COCAO + # zero time representation). If this happens, the event is not yield. + + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, match=None, + **unused_kwargs): + """Extracts relevant user timestamp entries. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + match: Optional dictionary containing keys extracted from PLIST_KEYS. + The default is None. + """ + account = match['name'][0] + uid = match['uid'][0] + cocoa_zero = ( + timelib.Timestamp.COCOA_TIME_TO_POSIX_BASE * + timelib.Timestamp.MICRO_SECONDS_PER_SECOND) + # INFO: binplist return a string with the Plist XML. + for policy in match['passwordpolicyoptions']: + xml_policy = ElementTree.fromstring(policy) + for dict_elements in xml_policy.iterfind('dict'): + key_values = [value.text for value in dict_elements.getchildren()] + policy_dict = dict(zip(key_values[0::2], key_values[1::2])) + + if policy_dict.get('passwordLastSetTime', 0): + timestamp = timelib.Timestamp.FromTimeString( + policy_dict.get('passwordLastSetTime', '0')) + if timestamp > cocoa_zero: + # Extract the hash password information. + # It is store in the attribure ShadowHasData which is + # a binary plist data; However binplist only extract one + # level of binary plist, then it returns this information + # as a string. + + # TODO: change this into a DataRange instead. For this we + # need the file offset and size of the ShadowHashData value data. + resolver_context = context.Context() + fake_file = fake_file_io.FakeFile( + resolver_context, match['ShadowHashData'][0]) + fake_file.open(path_spec=fake_path_spec.FakePathSpec( + location=u'ShadowHashData')) + + try: + plist_file = binplist.BinaryPlist(file_obj=fake_file) + top_level = plist_file.Parse() + except binplist.FormatError: + top_level = dict() + salted_hash = top_level.get('SALTED-SHA512-PBKDF2', None) + if salted_hash: + password_hash = u'$ml${0:d}${1:s}${2:s}'.format( + salted_hash['iterations'], + binascii.hexlify(salted_hash['salt']), + binascii.hexlify(salted_hash['entropy'])) + else: + password_hash = u'N/A' + description = ( + u'Last time {0:s} ({1!s}) changed the password: {2!s}').format( + account, uid, password_hash) + event_object = plist_event.PlistTimeEvent( + self._ROOT, u'passwordLastSetTime', timestamp, description) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if policy_dict.get('lastLoginTimestamp', 0): + timestamp = timelib.Timestamp.FromTimeString( + policy_dict.get('lastLoginTimestamp', '0')) + description = u'Last login from {0:s} ({1!s})'.format(account, uid) + if timestamp > cocoa_zero: + event_object = plist_event.PlistTimeEvent( + self._ROOT, u'lastLoginTimestamp', timestamp, description) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if policy_dict.get('failedLoginTimestamp', 0): + timestamp = timelib.Timestamp.FromTimeString( + policy_dict.get('failedLoginTimestamp', '0')) + description = ( + u'Last failed login from {0:s} ({1!s}) ({2!s} times)').format( + account, uid, policy_dict['failedLoginCount']) + if timestamp > cocoa_zero: + event_object = plist_event.PlistTimeEvent( + self._ROOT, u'failedLoginTimestamp', timestamp, description) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +plist.PlistParser.RegisterPlugin(MacUserPlugin) diff --git a/plaso/parsers/plist_plugins/macuser_test.py b/plaso/parsers/plist_plugins/macuser_test.py new file mode 100644 index 0000000..f056513 --- /dev/null +++ b/plaso/parsers/plist_plugins/macuser_test.py @@ -0,0 +1,73 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Mac OS X local users plist plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import plist as plist_formatter +from plaso.lib import timelib_test +from plaso.parsers import plist +from plaso.parsers.plist_plugins import macuser +from plaso.parsers.plist_plugins import test_lib + + +class MacUserPluginTest(test_lib.PlistPluginTestCase): + """Tests for the Mac OS X local user plist plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = macuser.MacUserPlugin() + self._parser = plist.PlistParser() + + def testProcess(self): + """Tests the Process function.""" + plist_name = u'user.plist' + test_file = self._GetTestFilePath([plist_name]) + event_queue_consumer = self._ParsePlistFileWithPlugin( + self._parser, self._plugin, test_file, plist_name) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 1) + + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-12-28 04:35:47') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.key, u'passwordLastSetTime') + self.assertEqual(event_object.root, u'/') + expected_desc = ( + u'Last time user (501) changed the password: ' + u'$ml$37313$fa6cac1869263baa85cffc5e77a3d4ee164b7' + u'5536cae26ce8547108f60e3f554$a731dbb0e386b169af8' + u'9fbb33c255ceafc083c6bc5194853f72f11c550c42e4625' + u'ef113b66f3f8b51fc3cd39106bad5067db3f7f1491758ff' + u'e0d819a1b0aba20646fd61345d98c0c9a411bfd1144dd4b' + u'3c40ec0f148b66d5b9ab014449f9b2e103928ef21db6e25' + u'b536a60ff17a84e985be3aa7ba3a4c16b34e0d1d2066ae178') + self.assertEqual(event_object.desc, expected_desc) + expected_string = u'//passwordLastSetTime {}'.format(expected_desc) + expected_short = u'{}...'.format(expected_string[:77]) + self._TestGetMessageStrings( + event_object, expected_string, expected_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/plist_plugins/safari.py b/plaso/parsers/plist_plugins/safari.py new file mode 100644 index 0000000..f268771 --- /dev/null +++ b/plaso/parsers/plist_plugins/safari.py @@ -0,0 +1,97 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a default plist plugin in Plaso.""" + +import logging + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import plist +from plaso.parsers.plist_plugins import interface + + +class SafariHistoryEvent(time_events.TimestampEvent): + """An EventObject for Safari history entries.""" + + def __init__(self, timestamp, history_entry): + """Initialize the event. + + Args: + timestamp: The timestamp of the Event, in microseconds since Unix Epoch. + history_entry: A dict object read from the Safari history plist. + """ + super(SafariHistoryEvent, self).__init__( + timestamp, eventdata.EventTimestamp.LAST_VISITED_TIME) + self.data_type = 'safari:history:visit' + self.url = history_entry.get('', None) + self.title = history_entry.get('title', None) + display_title = history_entry.get('displayTitle', None) + if display_title != self.title: + self.display_title = display_title + self.visit_count = history_entry.get('visitCount', None) + self.was_http_non_get = history_entry.get('lastVisitWasHTTPNonGet', None) + + +class SafariHistoryPlugin(interface.PlistPlugin): + """Plugin to extract Safari history timestamps.""" + + NAME = 'safari_history' + DESCRIPTION = u'Parser for Safari history plist files.' + + PLIST_PATH = 'History.plist' + PLIST_KEYS = frozenset(['WebHistoryDates', 'WebHistoryFileVersion']) + + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, match=None, + **unused_kwargs): + """Extracts Safari history items. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + match: Optional dictionary containing keys extracted from PLIST_KEYS. + The default is None. + """ + if match.get('WebHistoryFileVersion', 0) != 1: + logging.warning(u'Unable to parse Safari version: {0:s}'.format( + match.get('WebHistoryFileVersion', 0))) + return + + for history_entry in match.get('WebHistoryDates', {}): + try: + time = timelib.Timestamp.FromCocoaTime(float( + history_entry.get('lastVisitedDate', 0))) + except ValueError: + logging.warning(u'Unable to translate timestamp: {0:s}'.format( + history_entry.get('lastVisitedDate', 0))) + continue + + if not time: + logging.debug('No timestamp set, skipping record.') + continue + + event_object = SafariHistoryEvent(time, history_entry) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +plist.PlistParser.RegisterPlugin(SafariHistoryPlugin) diff --git a/plaso/parsers/plist_plugins/safari_test.py b/plaso/parsers/plist_plugins/safari_test.py new file mode 100644 index 0000000..701834f --- /dev/null +++ b/plaso/parsers/plist_plugins/safari_test.py @@ -0,0 +1,65 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Safari history plist plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import plist as plist_formatter +from plaso.lib import timelib_test +from plaso.parsers import plist +from plaso.parsers.plist_plugins import safari +from plaso.parsers.plist_plugins import test_lib + + +class SafariPluginTest(test_lib.PlistPluginTestCase): + """Tests for the Safari history plist plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = safari.SafariHistoryPlugin() + self._parser = plist.PlistParser() + + def testProcess(self): + """Tests the Process function.""" + test_file = self._GetTestFilePath(['History.plist']) + plist_name = 'History.plist' + event_queue_consumer = self._ParsePlistFileWithPlugin( + self._parser, self._plugin, test_file, plist_name) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # 18 entries in timeline. + self.assertEquals(len(event_objects), 18) + + event_object = event_objects[8] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-07-08 17:31:00') + self.assertEquals(event_objects[10].timestamp, expected_timestamp) + expected_url = u'http://netverslun.sci-mx.is/aminosyrur' + self.assertEquals(event_object.url, expected_url) + + expected_string = ( + u'Visited: {0:s} (Am\xedn\xf3s\xfdrur ) Visit Count: 1').format( + expected_url) + + self._TestGetMessageStrings(event_object, expected_string, expected_string) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/plist_plugins/softwareupdate.py b/plaso/parsers/plist_plugins/softwareupdate.py new file mode 100644 index 0000000..fa25af9 --- /dev/null +++ b/plaso/parsers/plist_plugins/softwareupdate.py @@ -0,0 +1,83 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a default plist plugin in Plaso.""" + +from plaso.events import plist_event +from plaso.parsers import plist +from plaso.parsers.plist_plugins import interface + + +__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)' + + +class SoftwareUpdatePlugin(interface.PlistPlugin): + """Basic plugin to extract the Mac OS X update status.""" + + NAME = 'plist_softwareupdate' + DESCRIPTION = u'Parser for Mac OS X software update plist files.' + + PLIST_PATH = 'com.apple.SoftwareUpdate.plist' + PLIST_KEYS = frozenset([ + 'LastFullSuccessfulDate', 'LastSuccessfulDate', + 'LastAttemptSystemVersion', 'LastUpdatesAvailable', + 'LastRecommendedUpdatesAvailable', 'RecommendedUpdates']) + + # Generated events: + # LastFullSuccessfulDate: timestamp when Mac OS X was full update. + # LastSuccessfulDate: timestamp when Mac OS X was partially update. + + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, match=None, + **unused_kwargs): + """Extracts relevant Mac OS X update entries. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + match: Optional dictionary containing keys extracted from PLIST_KEYS. + The default is None. + """ + root = '/' + key = '' + version = match.get('LastAttemptSystemVersion', u'N/A') + pending = match['LastUpdatesAvailable'] + + description = u'Last Mac OS X {0:s} full update.'.format(version) + event_object = plist_event.PlistEvent( + root, key, match['LastFullSuccessfulDate'], description) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if pending: + software = [] + for update in match['RecommendedUpdates']: + software.append(u'{0:s}({1:s})'.format( + update['Identifier'], update['Product Key'])) + description = ( + u'Last Mac OS {0!s} partially update, pending {1!s}: {2:s}.').format( + version, pending, u','.join(software)) + event_object = plist_event.PlistEvent( + root, key, match['LastSuccessfulDate'], description) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +plist.PlistParser.RegisterPlugin(SoftwareUpdatePlugin) diff --git a/plaso/parsers/plist_plugins/softwareupdate_test.py b/plaso/parsers/plist_plugins/softwareupdate_test.py new file mode 100644 index 0000000..3b182d9 --- /dev/null +++ b/plaso/parsers/plist_plugins/softwareupdate_test.py @@ -0,0 +1,65 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Software Update plist plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import plist as plist_formatter +from plaso.parsers import plist +from plaso.parsers.plist_plugins import softwareupdate +from plaso.parsers.plist_plugins import test_lib + + +class SoftwareUpdatePluginTest(test_lib.PlistPluginTestCase): + """Tests for the SoftwareUpdate plist plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = softwareupdate.SoftwareUpdatePlugin() + self._parser = plist.PlistParser() + + def testProcess(self): + """Tests the Process function.""" + plist_name = u'com.apple.SoftwareUpdate.plist' + test_file = self._GetTestFilePath([plist_name]) + event_queue_consumer = self._ParsePlistFileWithPlugin( + self._parser, self._plugin, test_file, plist_name) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 2) + event_object = event_objects[0] + self.assertEqual(event_object.key, u'') + self.assertEqual(event_object.root, u'/') + expected_desc = u'Last Mac OS X 10.9.1 (13B42) full update.' + self.assertEqual(event_object.desc, expected_desc) + expected_string = u'// {}'.format(expected_desc) + self._TestGetMessageStrings( + event_object, expected_string, expected_string) + + event_object = event_objects[1] + self.assertEqual(event_object.key, u'') + self.assertEqual(event_object.root, u'/') + expected_desc = ( + u'Last Mac OS 10.9.1 (13B42) partially ' + u'update, pending 1: RAWCameraUpdate5.03(031-2664).') + self.assertEqual(event_object.desc, expected_desc) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/plist_plugins/spotlight.py b/plaso/parsers/plist_plugins/spotlight.py new file mode 100644 index 0000000..4e511ec --- /dev/null +++ b/plaso/parsers/plist_plugins/spotlight.py @@ -0,0 +1,67 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the Spotlight searched terms plugin in Plaso.""" + +from plaso.events import plist_event +from plaso.parsers import plist +from plaso.parsers.plist_plugins import interface + + +__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)' + + +class SpotlightPlugin(interface.PlistPlugin): + """Basic plugin to extract Spotlight.""" + + NAME = 'plist_spotlight' + DESCRIPTION = u'Parser for Spotlight plist files.' + + PLIST_PATH = 'com.apple.spotlight.plist' + PLIST_KEYS = frozenset(['UserShortcuts']) + + # Generated events: + # name of the item: searched term. + # PATH: path of the program associated to the term. + # LAST_USED: last time when it was executed. + # DISPLAY_NAME: the display name of the program associated. + + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, match=None, + **unused_kwargs): + """Extracts relevant Spotlight entries. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + match: Optional dictionary containing keys extracted from PLIST_KEYS. + The default is None. + """ + for search_text, data in match['UserShortcuts'].iteritems(): + description = ( + u'Spotlight term searched "{0:s}" associate to {1:s} ' + u'({2:s})').format(search_text, data['DISPLAY_NAME'], data['PATH']) + event_object = plist_event.PlistEvent( + u'/UserShortcuts', search_text, data['LAST_USED'], description) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +plist.PlistParser.RegisterPlugin(SpotlightPlugin) diff --git a/plaso/parsers/plist_plugins/spotlight_test.py b/plaso/parsers/plist_plugins/spotlight_test.py new file mode 100644 index 0000000..565d9cb --- /dev/null +++ b/plaso/parsers/plist_plugins/spotlight_test.py @@ -0,0 +1,69 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the spotlight plist plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import plist as plist_formatter +from plaso.parsers import plist +from plaso.parsers.plist_plugins import spotlight +from plaso.parsers.plist_plugins import test_lib + + +class SpotlightPluginTest(test_lib.PlistPluginTestCase): + """Tests for the spotlight plist plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = spotlight.SpotlightPlugin() + self._parser = plist.PlistParser() + + def testProcess(self): + """Tests the Process function.""" + test_file = self._GetTestFilePath(['com.apple.spotlight.plist']) + plist_name = 'com.apple.spotlight.plist' + event_queue_consumer = self._ParsePlistFileWithPlugin( + self._parser, self._plugin, test_file, plist_name) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 9) + + timestamps = [] + for event_object in event_objects: + timestamps.append(event_object.timestamp) + expected_timestamps = frozenset([ + 1379937262090906, 1387822901900937, 1375236414408299, 1388331212005129, + 1376696381196456, 1386951868185477, 1380942616952359, 1389056477460443, + 1386111811136093]) + self.assertTrue(set(timestamps) == expected_timestamps) + + event_object = event_objects[1] + self.assertEqual(event_object.key, u'gr') + self.assertEqual(event_object.root, u'/UserShortcuts') + expected_desc = (u'Spotlight term searched "gr" associate to ' + u'Grab (/Applications/Utilities/Grab.app)') + self.assertEqual(event_object.desc, expected_desc) + expected_string = u'/UserShortcuts/gr {}'.format(expected_desc) + expected_short = expected_string[:77] + u'...' + self._TestGetMessageStrings( + event_object, expected_string, expected_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/plist_plugins/spotlight_volume.py b/plaso/parsers/plist_plugins/spotlight_volume.py new file mode 100644 index 0000000..9773d76 --- /dev/null +++ b/plaso/parsers/plist_plugins/spotlight_volume.py @@ -0,0 +1,60 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the Spotlight Volume Configuration plist in Plaso.""" + +from plaso.events import plist_event +from plaso.parsers import plist +from plaso.parsers.plist_plugins import interface + + +__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)' + + +class SpotlightVolumePlugin(interface.PlistPlugin): + """Basic plugin to extract the Spotlight Volume Configuration.""" + + NAME = 'plist_spotlight_volume' + DESCRIPTION = u'Parser for Spotlight volume configuration plist files.' + + PLIST_PATH = 'VolumeConfiguration.plist' + PLIST_KEYS = frozenset(['Stores']) + + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, match=None, + **unused_kwargs): + """Extracts relevant VolumeConfiguration Spotlight entries. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + match: Optional dictionary containing keys extracted from PLIST_KEYS. + The default is None. + """ + for volume_name, volume in match['Stores'].iteritems(): + description = u'Spotlight Volume {0:s} ({1:s}) activated.'.format( + volume_name, volume['PartialPath']) + event_object = plist_event.PlistEvent( + u'/Stores', '', volume['CreationDate'], description) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +plist.PlistParser.RegisterPlugin(SpotlightVolumePlugin) diff --git a/plaso/parsers/plist_plugins/spotlight_volume_test.py b/plaso/parsers/plist_plugins/spotlight_volume_test.py new file mode 100644 index 0000000..311db84 --- /dev/null +++ b/plaso/parsers/plist_plugins/spotlight_volume_test.py @@ -0,0 +1,67 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Spotlight Volume configuration plist plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import plist as plist_formatter +from plaso.parsers import plist +from plaso.parsers.plist_plugins import spotlight_volume +from plaso.parsers.plist_plugins import test_lib + + +class SpotlightVolumePluginTest(test_lib.PlistPluginTestCase): + """Tests for the Spotlight Volume configuration plist plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = spotlight_volume.SpotlightVolumePlugin() + self._parser = plist.PlistParser() + + def testProcess(self): + """Tests the Process function.""" + test_file = self._GetTestFilePath(['VolumeConfiguration.plist']) + plist_name = 'VolumeConfiguration.plist' + event_queue_consumer = self._ParsePlistFileWithPlugin( + self._parser, self._plugin, test_file, plist_name) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 2) + + timestamps = [] + for event_object in event_objects: + timestamps.append(event_object.timestamp) + expected_timestamps = frozenset([ + 1372139683000000, 1369657656000000]) + self.assertTrue(set(timestamps) == expected_timestamps) + + event_object = event_objects[0] + self.assertEqual(event_object.key, u'') + self.assertEqual(event_object.root, u'/Stores') + expected_desc = (u'Spotlight Volume 4D4BFEB5-7FE6-4033-AAAA-' + u'AAAABBBBCCCCDDDD (/.MobileBackups) activated.') + self.assertEqual(event_object.desc, expected_desc) + expected_string = u'/Stores/ {}'.format(expected_desc) + expected_short = expected_string[:77] + u'...' + self._TestGetMessageStrings( + event_object, expected_string, expected_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/plist_plugins/test_lib.py b/plaso/parsers/plist_plugins/test_lib.py new file mode 100644 index 0000000..a3f7a39 --- /dev/null +++ b/plaso/parsers/plist_plugins/test_lib.py @@ -0,0 +1,87 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Plist plugin related functions and classes for testing.""" + +from dfvfs.lib import definitions +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver import resolver as path_spec_resolver + +from plaso.engine import single_process +from plaso.parsers import test_lib + + +class PlistPluginTestCase(test_lib.ParserTestCase): + """The unit test case for a plist plugin.""" + + def _ParsePlistFileWithPlugin( + self, parser_object, plugin_object, path, plist_name, + knowledge_base_values=None): + """Parses a file using the parser and plugin object. + + Args: + parser_object: the parser object. + plugin_object: the plugin object. + path: the path of the file to parse. + plist_name: the name of the plist to parse. + knowledge_base_values: optional dict containing the knowledge base + values. The default is None. + + Returns: + An event object queue consumer object (instance of + TestEventObjectQueueConsumer). + """ + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=path) + file_entry = path_spec_resolver.Resolver.OpenFileEntry(path_spec) + + file_object = file_entry.GetFileObject() + top_level_object = parser_object.GetTopLevel(file_object) + self.assertNotEquals(top_level_object, None) + + return self._ParsePlistWithPlugin( + plugin_object, plist_name, top_level_object, + knowledge_base_values=knowledge_base_values) + + def _ParsePlistWithPlugin( + self, plugin_object, plist_name, top_level_object, + knowledge_base_values=None): + """Parses a plist using the plugin object. + + Args: + plugin_object: the plugin object. + plist_name: the name of the plist to parse. + top_level_object: the top-level plist object. + knowledge_base_values: optional dict containing the knowledge base + values. The default is None. + + Returns: + An event object queue consumer object (instance of + TestEventObjectQueueConsumer). + """ + event_queue = single_process.SingleProcessQueue() + event_queue_consumer = test_lib.TestEventObjectQueueConsumer(event_queue) + + parse_error_queue = single_process.SingleProcessQueue() + + parser_context = self._GetParserContext( + event_queue, parse_error_queue, + knowledge_base_values=knowledge_base_values) + plugin_object.Process( + parser_context, plist_name=plist_name, top_level=top_level_object) + + return event_queue_consumer diff --git a/plaso/parsers/plist_plugins/timemachine.py b/plaso/parsers/plist_plugins/timemachine.py new file mode 100644 index 0000000..903f30f --- /dev/null +++ b/plaso/parsers/plist_plugins/timemachine.py @@ -0,0 +1,86 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a TimeMachine plist plugin in Plaso.""" + +import construct + +from plaso.events import plist_event +from plaso.parsers import plist +from plaso.parsers.plist_plugins import interface + + +__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)' + + +class TimeMachinePlugin(interface.PlistPlugin): + """Basic plugin to extract time machine hardisk and the backups.""" + + NAME = 'plist_timemachine' + DESCRIPTION = u'Parser for TimeMachine plist files.' + + PLIST_PATH = 'com.apple.TimeMachine.plist' + PLIST_KEYS = frozenset(['Destinations', 'RootVolumeUUID']) + + # Generated events: + # DestinationID: remote UUID hard disk where the backup is done. + # BackupAlias: structure that contains the extra information from the + # destinationID. + # SnapshotDates: list of the backup dates. + + TM_BACKUP_ALIAS = construct.Struct( + 'tm_backup_alias', + construct.Padding(10), + construct.PascalString('value', length_field=construct.UBInt8('length'))) + + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, match=None, + **unused_kwargs): + """Extracts relevant TimeMachine entries. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + match: Optional dictionary containing keys extracted from PLIST_KEYS. + The default is None. + """ + root = '/Destinations' + key = 'item/SnapshotDates' + + # For each TimeMachine devices. + for destination in match['Destinations']: + hd_uuid = destination['DestinationID'] + if not hd_uuid: + hd_uuid = u'Unknown device' + alias = destination['BackupAlias'] + try: + alias = self.TM_BACKUP_ALIAS.parse(alias).value + except construct.FieldError: + alias = u'Unknown alias' + # For each Backup. + for timestamp in destination['SnapshotDates']: + description = u'TimeMachine Backup in {0:s} ({1:s})'.format( + alias, hd_uuid) + event_object = plist_event.PlistEvent(root, key, timestamp, description) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +plist.PlistParser.RegisterPlugin(TimeMachinePlugin) diff --git a/plaso/parsers/plist_plugins/timemachine_test.py b/plaso/parsers/plist_plugins/timemachine_test.py new file mode 100644 index 0000000..570fecb --- /dev/null +++ b/plaso/parsers/plist_plugins/timemachine_test.py @@ -0,0 +1,72 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the timemachine plist plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import plist as plist_formatter +from plaso.parsers import plist +from plaso.parsers.plist_plugins import timemachine +from plaso.parsers.plist_plugins import test_lib + + +class TimeMachinePluginTest(test_lib.PlistPluginTestCase): + """Tests for the timemachine plist plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = timemachine.TimeMachinePlugin() + self._parser = plist.PlistParser() + + def testProcess(self): + """Tests the Process function.""" + test_file = self._GetTestFilePath(['com.apple.TimeMachine.plist']) + plist_name = 'com.apple.timemachine.plist' + event_object_generator = self._ParsePlistFileWithPlugin( + self._parser, self._plugin, test_file, plist_name) + event_objects = self._GetEventObjectsFromQueue(event_object_generator) + + self.assertEquals(len(event_objects), 13) + + timestamps = [] + for event_object in event_objects: + timestamps.append(event_object.timestamp) + expected_timestamps = frozenset([ + 1379165051000000, 1380098455000000, 1380810276000000, 1381883538000000, + 1382647890000000, 1383351739000000, 1384090020000000, 1385130914000000, + 1386265911000000, 1386689852000000, 1387723091000000, 1388840950000000, + 1388842718000000]) + self.assertTrue(set(timestamps) == expected_timestamps) + + event_object = event_objects[0] + self.assertEqual(event_object.root, u'/Destinations') + self.assertEqual(event_object.key, u'item/SnapshotDates') + expected_desc = ( + u'TimeMachine Backup in BackUpFast ' + u'(5B33C22B-A4A1-4024-A2F5-C9979C4AAAAA)') + self.assertEqual(event_object.desc, expected_desc) + expected_string = u'/Destinations/item/SnapshotDates {}'.format( + expected_desc) + expected_short = expected_string[:77] + u'...' + self._TestGetMessageStrings( + event_object, expected_string, expected_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/plist_test.py b/plaso/parsers/plist_test.py new file mode 100644 index 0000000..4613022 --- /dev/null +++ b/plaso/parsers/plist_test.py @@ -0,0 +1,74 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests the plist parser.""" + +import unittest + +from plaso.parsers import plist +# Register all plugins. +from plaso.parsers import plist_plugins # pylint: disable=unused-import +from plaso.parsers import test_lib + + +class PlistParserTest(test_lib.ParserTestCase): + """Tests the plist parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = plist.PlistParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['plist_binary']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 12) + + timestamps, roots, keys = zip( + *[(evt.timestamp, evt.root, evt.key) for evt in event_objects]) + + expected_timestamps = frozenset([ + 1345251192528750, 1351827808261762, 1345251268370453, + 1351818803000000, 1351819298997672, 1351818797324095, + 1301012201414766, 1302199013524275, 1341957900020116, + 1350666391557044, 1350666385239661, 1341957896010535]) + + self.assertTrue(set(expected_timestamps) == set(timestamps)) + self.assertEquals(12, len(set(timestamps))) + + expected_roots = frozenset([ + '/DeviceCache/00-0d-fd-00-00-00', + '/DeviceCache/44-00-00-00-00-00', + '/DeviceCache/44-00-00-00-00-01', + '/DeviceCache/44-00-00-00-00-02', + '/DeviceCache/44-00-00-00-00-03', + '/DeviceCache/44-00-00-00-00-04']) + self.assertTrue(expected_roots == set(roots)) + self.assertEquals(6, len(set(roots))) + + expected_keys = frozenset([ + u'LastInquiryUpdate', + u'LastServicesUpdate', + u'LastNameUpdate']) + self.assertTrue(expected_keys == set(keys)) + self.assertEquals(3, len(set(keys))) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/pls_recall.py b/plaso/parsers/pls_recall.py new file mode 100644 index 0000000..d31cce3 --- /dev/null +++ b/plaso/parsers/pls_recall.py @@ -0,0 +1,173 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specifiSc language governing permissions and +# limitations under the License. +"""Parser for PL-SQL Developer Recall files.""" + +import construct +import os + +from plaso.lib import errors +from plaso.lib import event +from plaso.lib import timelib +from plaso.lib import utils +from plaso.parsers import interface +from plaso.parsers import manager + + +class PlsRecallEvent(event.EventObject): + """Convenience class for a PL-SQL Recall file container.""" + + DATA_TYPE = 'PLSRecall:event' + + def __init__(self, timestamp, sequence, user, database, query): + """Initializes the event object. + + Args: + timestamp: The timestamp when the entry was created. + sequence: Sequence indicates the order of execution. + username: The username that made the query. + database_name: String containing the databe name. + query: String containing the PL-SQL query. + """ + super(PlsRecallEvent, self).__init__() + self.timestamp = timestamp + self.sequence = sequence + self.username = user + self.database_name = database + self.query = query + + +class PlsRecallParser(interface.BaseParser): + """Parse PL-SQL Recall files. + + Parser is based on a: + TRecallRecord = packed record + Sequence: Integer; + TimeStamp: TDateTime; + Username: array[0..30] of Char; + Database: array[0..80] of Char; + Text: array[0..4000] of Char; + end; + + Delphi TDateTime is a little endian 64-bit + floating point without any time zone information + """ + + NAME = 'pls_recall' + DESCRIPTION = u'Parser for PL-SQL Recall files.' + + PLS_STRUCT = construct.Struct( + 'PL-SQL_Recall', + construct.ULInt32('Sequence'), + construct.LFloat64('TimeStamp'), + construct.String('Username', 31, None, '\x00'), + construct.String('Database', 81, None, '\x00'), + construct.String('Query', 4001, None, '\x00')) + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract entries from a PLSRecall.dat file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + file_object = file_entry.GetFileObject() + + try: + is_pls = self.VerifyFile(file_object) + except (IOError, construct.FieldError) as exception: + file_object.close() + raise errors.UnableToParseFile(( + u'Not a PLSrecall File, unable to parse.' + u'with error: {0:s}').format(exception)) + + if not is_pls: + file_object.close() + raise errors.UnableToParseFile( + u'Not a PLSRecall File, unable to parse.') + + file_object.seek(0, os.SEEK_SET) + pls_record = self.PLS_STRUCT.parse_stream(file_object) + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + while pls_record: + event_object = PlsRecallEvent( + timelib.Timestamp.FromDelphiTime(pls_record.TimeStamp), + pls_record.Sequence, pls_record.Username, + pls_record.Database, pls_record.Query) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + try: + pls_record = self.PLS_STRUCT.parse_stream(file_object) + except construct.FieldError as exception: + # The code has reached the end of file (EOF). + break + + file_object.close() + + def VerifyFile(self, file_object): + """Check if the file is a PLSRecall.dat file. + + Args: + file_object: file that we want to check. + + Returns: + True if this is a valid PLSRecall.dat file, otherwise False. + """ + file_object.seek(0, os.SEEK_SET) + + # The file consists of PL-SQL structures that are equal + # size (4125 bytes) TRecallRecord records. It should be + # noted that the query value is free form. + try: + structure = self.PLS_STRUCT.parse_stream(file_object) + except (IOError, construct.FieldError): + return False + + # Verify few entries inside the structure. + try: + timestamp = timelib.Timestamp.FromDelphiTime(structure.TimeStamp) + except ValueError: + return False + + if timestamp <= 0: + return False + + # TODO: Add other verification checks here. For instance make sure + # that the query actually looks like a SQL query. This structure produces a + # lot of false positives and thus we need to add additional verification to + # make sure we are not parsing non-PLSRecall files. + # Another check might be to make sure the username looks legitimate, or the + # sequence number, or the database name. + # For now we just check if all three fields pass our "is this a text" test. + if not utils.IsText(structure.Username): + return False + if not utils.IsText(structure.Query): + return False + if not utils.IsText(structure.Database): + return False + + return True + + +manager.ParsersManager.RegisterParser(PlsRecallParser) diff --git a/plaso/parsers/pls_recall_test.py b/plaso/parsers/pls_recall_test.py new file mode 100644 index 0000000..3fc56da --- /dev/null +++ b/plaso/parsers/pls_recall_test.py @@ -0,0 +1,78 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for PL-SQL recall file parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import pls_recall as pls_recall_formatter +from plaso.lib import timelib_test +from plaso.parsers import pls_recall +from plaso.parsers import test_lib + + +class PlsRecallTest(test_lib.ParserTestCase): + """Tests for PL-SQL recall file parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = pls_recall.PlsRecallParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['PLSRecall_Test.dat']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # There are two events in test file. + self.assertEquals(len(event_objects), 2) + + event_object = event_objects[0] + + timestamp_expected = timelib_test.CopyStringToTimestamp( + '2013-06-18 19:50:00:00:00') + self.assertEqual(event_object.timestamp, timestamp_expected) + + sequence_expected = 206 + self.assertEqual(event_object.sequence, sequence_expected) + + username_expected = u'tsltmp' + self.assertEqual(event_object.username, username_expected) + + database_name_expected = u'DB11' + self.assertEqual(event_object.database_name, database_name_expected) + + # The test file actually has 'test_databae' in the SQL string. + query_expected = u'SELECT * from test_databae where date > \'01/01/2012\'' + self.assertEqual(event_object.query, query_expected) + + expected_msg = ( + u'Sequence #206 ' + u'User: tsltmp ' + u'Database Name: DB11 ' + u'Query: SELECT * from test_databae where date > \'01/01/2012\'') + + expected_msg_short = ( + u'206 tsltmp DB11 ' + u'SELECT * from test_databae where date > \'01/01/2012\'') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/plugins.py b/plaso/parsers/plugins.py new file mode 100644 index 0000000..c7c3a5d --- /dev/null +++ b/plaso/parsers/plugins.py @@ -0,0 +1,118 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains basic interface for plugins within Plaso. + +This library serves a basis for all plugins in Plaso, whether that are +Windows registry plugins, SQLite plugins or any other parsing plugins. + +This is provided as a separate file to make it easier to inherit in other +projects that may want to use the Plaso plugin system. +""" + + +class BasePlugin(object): + """A plugin is a lightweight parser that makes use of a common data structure. + + When a data structure is common amongst several artifacts or files a plugin + infrastructure can be written to make writing parsers simpler. The goal of a + plugin is have only a single parser that understands the data structure that + can call plugins that have specialized knowledge of certain structures. + + An example of this is a SQLite database. A plugin can be written that has + knowledge of certain database, such as Chrome history, or Skype history, etc. + This can be done without needing to write a full fledged parser that needs + to re-implement the data structure knowledge. A single parser can be created + that calls the plugins to see if it knows that particular database. + + Another example is Windows registry, there a single parser that can parse + the registry can be made and the job of a single plugin is to parse a + particular registry key. The parser can then read a registry key and compare + it to a list of available plugins to see if it can be parsed. + """ + + # The name of the plugin. This is the name that is used in the registration + # and used for parser/plugin selection, so this needs to be concise and unique + # for all plugins/parsers, eg: 'Chrome', 'Safari', 'UserAssist', etc. + NAME = 'base_plugin' + + DESCRIPTION = u'' + + # The URLS should contain a list of URLs with additional information about + # the plugin, for instance some additional reading material. That can be + # a description of the data structure, or how to read the data that comes + # out of the parser, etc. So in essence this is a field to define pointers + # to additional resources to assist the practitioner reading the output of + # the plugin. + URLS = [] + + # TODO: remove. + @property + def plugin_name(self): + """Return the name of the plugin.""" + return self.NAME + + def _BuildParserChain(self, parser_chain=None): + """Return the parser chain with the addition of the current parser. + + Args: + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Returns: + The parser chain, with the addition of the current parser. + """ + if not parser_chain: + return self.NAME + + return u'/'.join([parser_chain, self.NAME]) + + def Process(self, unused_parser_context, unused_parser_chain=None, **kwargs): + """Evaluates if this is the correct plugin and processes data accordingly. + + The purpose of the process function is to evaluate if this particular + plugin is the correct one for the particular data structure at hand. + This function accepts one value to use for evaluation, that could be + a registry key, list of table names for a database or any other criteria + that can be used to evaluate if the plugin should be run or not. + + Args: + parser_context: A parser context object (instance of ParserContext). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + kwargs: Depending on the plugin they may require different sets of + arguments to be able to evaluate whether or not this is + the correct plugin. + + Raises: + ValueError: When there are unused keyword arguments. + """ + if kwargs: + raise ValueError(u'Unused keyword arguments: {0:s}.'.format( + kwargs.keys())) + + +class BasePluginCache(object): + """A generic cache object for plugins. + + This cache object can be used to store various information that needs + to be cached to speed up code execution. + """ + + def GetResults(self, attribute): + """Return back a cached attribute if it exists.""" + return getattr(self, attribute, None) diff --git a/plaso/parsers/popcontest.py b/plaso/parsers/popcontest.py new file mode 100644 index 0000000..673ae6c --- /dev/null +++ b/plaso/parsers/popcontest.py @@ -0,0 +1,275 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains Popularity Contest log file parser in plaso. + + Information updated 20 january 2014. + From Debian Package Popularity Contest + Avery Pennarun + + From 'http://www.unix.com/man-page/Linux/8/popularity-contest/': + ' + The popularity-contest command gathers information about Debian pack- + ages installed on the system, and prints the name of the most recently + used executable program in that package as well as its last-accessed + time (atime) and last-attribute-changed time (ctime) to stdout. + + When aggregated with the output of popularity-contest from many other + systems, this information is valuable because it can be used to deter- + mine which Debian packages are commonly installed, used, or installed + and never used. This helps Debian maintainers make decisions such as + which packages should be installed by default on new systems. + + The resulting statistic is available from the project home page + http://popcon.debian.org/. + + Normally, popularity-contest is run from a cron(8) job, + /etc/cron.daily/popularity-contest, which automatically submits the + results to Debian package maintainers (only once a week) according to + the settings in /etc/popularity-contest.conf and /usr/share/popularity- + contest/default.conf. + ' + + From 'http://popcon.ubuntu.com/README': + ' + The popularity-contest output looks like this: + + POPULARITY-CONTEST-0 TIME:914183330 ID:b92a5fc1809d8a95a12eb3a3c8445 + 914183333 909868335 grep /bin/fgrep + 914183333 909868280 findutils /usr/bin/find + 914183330 909885698 dpkg-awk /usr/bin/dpkg-awk + 914183330 909868577 gawk /usr/bin/gawk + [...more lines...] + END-POPULARITY-CONTEST-0 TIME:914183335 + + The first and last lines allow you to put more than one set of + popularity-contest results into a single file and then split them up + easily later. + + The rest of the lines are package entries, one line for each package + installed on your system. They have the format: + + + + is the name of the Debian package that contains + . is the most recently used program, + static library, or header (.h) file in the package. + + and are the access time and creation time of the + on your disk, respectively, represented as the number of + seconds since midnight GMT on January 1, 1970 (i.e. in Unix time_t format). + Linux updates whenever you open the file; was set when you + first installed the package. + + is determined by popularity-contest depending on , , and + the current date. can be RECENT-CTIME, OLD, or NOFILES. + + RECENT-CTIME means that atime is very close to ctime; it's impossible to + tell whether the package was used recently or not, since is also + updated when is set. Normally, this happens because you have + recently upgraded the package to a new version, resetting the . + + OLD means that the is more than a month ago; you haven't used the + package for more than a month. + + NOFILES means that no files in the package seemed to be programs, so + , , and are invalid.' + + REMARKS. The parser will generate events solely based on the field + and not using , to reduce the generation of (possibly many) useless + events all with the same . Indeed, that will be probably + get from file system and/or package management logs. The will be + reported in the log line. +""" + +import logging + +import pyparsing + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import manager +from plaso.parsers import text_parser + + +__author__ = 'Francesco Picasso (francesco.picasso@gmail.com)' + + +class PopularityContestSessionEvent(time_events.PosixTimeEvent): + """Convenience class for a Popularity Contest start/end event.""" + + DATA_TYPE = 'popularity_contest:session:event' + + def __init__(self, timestamp, session, status, hostid=None, details=None): + """Initializes the event object. + + Args: + timestamp: microseconds since epoch in UTC, it's the start/end time. + session: the session number. + status: start or end of the session. + hostid: the host uuid. + details: the popularity contest version and host architecture. + """ + super(PopularityContestSessionEvent, self).__init__( + timestamp, eventdata.EventTimestamp.ADDED_TIME) + self.session = session + self.status = status + self.hostid = hostid + self.details = details + + +class PopularityContestEvent(time_events.PosixTimeEvent): + """Convenience class for a Popularity Contest line event.""" + + DATA_TYPE = 'popularity_contest:log:event' + + def __init__(self, timestamp, ctime, package, mru, tag=None): + """Initializes the event object. + + Args: + timestamp: microseconds since epoch in UTC, it's the . + ctime: seconds since epoch in UTC, it's the . + package: the installed packaged name, whom mru belongs to. + mru: the recently used app/library from package. + tag: the popularity context tag. + """ + super(PopularityContestEvent, self).__init__( + timestamp, eventdata.EventTimestamp.ACCESS_TIME) + # TODO: adding ctime as is, reconsider a conversion to human readable form. + self.ctime = ctime + self.package = package + self.mru = mru + self.record_tag = tag + + +class PopularityContestParser(text_parser.PyparsingSingleLineTextParser): + """Parse popularity contest log files.""" + + NAME = 'popularity_contest' + DESCRIPTION = u'Parser for popularity contest log files.' + + EPOCH = text_parser.PyparsingConstants.INTEGER.setResultsName('epoch') + PACKAGE = pyparsing.Word(pyparsing.printables).setResultsName('package') + MRU = pyparsing.Word(pyparsing.printables).setResultsName('mru') + TAG = pyparsing.QuotedString('<', endQuoteChar='>').setResultsName('tag') + + HEADER = ( + pyparsing.Literal(u'POPULARITY-CONTEST-').suppress() + + text_parser.PyparsingConstants.INTEGER.setResultsName('session') + + pyparsing.Literal(u'TIME:').suppress() + EPOCH + + pyparsing.Literal('ID:').suppress() + + pyparsing.Word(pyparsing.alphanums, exact=32).setResultsName('id') + + pyparsing.SkipTo(pyparsing.LineEnd()).setResultsName('details')) + + FOOTER = ( + pyparsing.Literal(u'END-POPULARITY-CONTEST-').suppress() + + text_parser.PyparsingConstants.INTEGER.setResultsName('session') + + pyparsing.Literal(u'TIME:').suppress() + EPOCH) + + LOG_LINE = ( + EPOCH.setResultsName('atime') + EPOCH.setResultsName('ctime') + + (PACKAGE + TAG | PACKAGE + MRU + pyparsing.Optional(TAG))) + + LINE_STRUCTURES = [ + ('logline', LOG_LINE), + ('header', HEADER), + ('footer', FOOTER), + ] + + def VerifyStructure(self, parser_context, line): + """Verify that this file is a Popularity Contest log file. + + Args: + parser_context: A parser context object (instance of ParserContext). + line: A single line from the text file. + + Returns: + True if this is the correct parser, False otherwise. + """ + try: + header_struct = self.HEADER.parseString(line) + except pyparsing.ParseException: + logging.debug(u'Not a Popularity Contest log file, invalid header') + return False + if not timelib.Timestamp.FromPosixTime(header_struct.epoch): + logging.debug(u'Invalid Popularity Contest log file header timestamp.') + return False + return True + + def ParseRecord(self, parser_context, key, structure): + """Parse each record structure and return an EventObject if applicable. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: An identification string indicating the name of the parsed + structure. + structure: A pyparsing.ParseResults object from a line in the + log file. + + Returns: + An event object (instance of EventObject) or None. + """ + # TODO: Add anomaly objects for abnormal timestamps, such as when the log + # timestamp is greater than the session start. + if key == 'logline': + return self._ParseLogLine(structure) + elif key == 'header': + if not structure.epoch: + logging.debug(u'PopularityContestParser, header with invalid epoch.') + return + return PopularityContestSessionEvent( + structure.epoch, unicode(structure.session), u'start', structure.id, + structure.details) + elif key == 'footer': + if not structure.epoch: + logging.debug(u'PopularityContestParser, footer with invalid epoch.') + return + return PopularityContestSessionEvent( + structure.epoch, unicode(structure.session), u'end') + else: + logging.warning( + u'PopularityContestParser, unknown structure: {}.'.format(key)) + + def _ParseLogLine(self, structure): + """Gets an event_object or None from the pyparsing ParseResults. + + Args: + structure: the pyparsing ParseResults object. + + Returns: + event_object: a plaso event or None. + """ + # Required fields are and and we are not interested in + # log lines without . + if not structure.mru: + return + # The field (as ) is always present but could be 0. + # In case of equal to 0, we are in case, safely return + # without logging. + if not structure.atime: + return + # TODO: not doing any check on fields, even if only informative + # probably it could be better to check for the expected values. + # TODO: ctime is a numeric string representing seconds since epoch UTC, + # reconsider a conversion to integer together with microseconds usage. + return PopularityContestEvent( + structure.atime, structure.ctime, structure.package, structure.mru, + structure.tag) + + +manager.ParsersManager.RegisterParser(PopularityContestParser) diff --git a/plaso/parsers/popcontest_test.py b/plaso/parsers/popcontest_test.py new file mode 100644 index 0000000..c979fdf --- /dev/null +++ b/plaso/parsers/popcontest_test.py @@ -0,0 +1,145 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Popularity Contest (popcontest) parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import popcontest as popcontest_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import popcontest +from plaso.parsers import test_lib + + +__author__ = 'Francesco Picasso (francesco.picasso@gmail.com)' + + +class PopularityContestUnitTest(test_lib.ParserTestCase): + """Tests for the popcontest parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = popcontest.PopularityContestParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['popcontest1.log']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 13) + + self.assertEquals( + event_objects[0].timestamp, + timelib_test.CopyStringToTimestamp('2010-06-22 05:41:41.000')) + expected_string = ( + u'Session 0 start ' + u'ID 12345678901234567890123456789012 [ARCH:i386 POPCONVER:1.38]') + expected_short_string = u'Session 0 start' + self._TestGetMessageStrings( + event_objects[0], expected_string, expected_short_string) + + self.assertEquals( + event_objects[1].timestamp, + timelib_test.CopyStringToTimestamp('2010-06-22 07:34:42.000')) + expected_string = u'mru [/usr/sbin/atd] package [at]' + expected_short_string = u'/usr/sbin/atd' + self._TestGetMessageStrings( + event_objects[1], expected_string, expected_short_string) + + self.assertEquals( + event_objects[2].timestamp, + timelib_test.CopyStringToTimestamp('2010-06-22 07:34:43.000')) + expected_string = ( + u'mru [/usr/lib/python2.5/lib-dynload/_struct.so] ' + u'package [python2.5-minimal]') + expected_short_string = u'/usr/lib/python2.5/lib-dynload/_struct.so' + self._TestGetMessageStrings( + event_objects[2], expected_string, expected_short_string) + + self.assertEquals( + event_objects[3].timestamp, + timelib_test.CopyStringToTimestamp('2010-05-30 05:26:20.000')) + expected_string = ( + u'mru [/usr/bin/empathy] package [empathy] tag [RECENT-CTIME]') + expected_short_string = u'/usr/bin/empathy' + self._TestGetMessageStrings( + event_objects[3], expected_string, expected_short_string) + + self.assertEquals( + event_objects[6].timestamp, + timelib_test.CopyStringToTimestamp('2010-05-12 07:58:33.000')) + expected_string = u'mru [/usr/bin/orca] package [gnome-orca] tag [OLD]' + expected_short_string = u'/usr/bin/orca' + self._TestGetMessageStrings( + event_objects[6], expected_string, expected_short_string) + + self.assertEquals( + event_objects[7].timestamp, + timelib_test.CopyStringToTimestamp('2010-06-22 05:41:41.000')) + expected_string = u'Session 0 end' + expected_short_string = expected_string + self._TestGetMessageStrings( + event_objects[7], expected_string, expected_short_string) + + self.assertEquals( + event_objects[8].timestamp, + timelib_test.CopyStringToTimestamp('2010-06-22 05:41:41.000')) + expected_string = ( + u'Session 1 start ' + u'ID 12345678901234567890123456789012 [ARCH:i386 POPCONVER:1.38]') + expected_short_string = u'Session 1 start' + self._TestGetMessageStrings( + event_objects[8], expected_string, expected_short_string) + + self.assertEquals( + event_objects[9].timestamp, + timelib_test.CopyStringToTimestamp('2010-06-22 07:34:42.000')) + expected_string = u'mru [/super/cool/plasuz] package [plaso]' + expected_short_string = u'/super/cool/plasuz' + self._TestGetMessageStrings( + event_objects[9], expected_string, expected_short_string) + + self.assertEquals( + event_objects[10].timestamp, + timelib_test.CopyStringToTimestamp('2010-04-06 12:25:42.000')) + expected_string = u'mru [/super/cool/plasuz] package [miss_ctime]' + expected_short_string = u'/super/cool/plasuz' + self._TestGetMessageStrings( + event_objects[10], expected_string, expected_short_string) + + self.assertEquals( + event_objects[11].timestamp, + timelib_test.CopyStringToTimestamp('2010-05-12 07:58:33.000')) + expected_string = u'mru [/super/cool] package [plaso] tag [WRONG_TAG]' + expected_short_string = u'/super/cool' + self._TestGetMessageStrings( + event_objects[11], expected_string, expected_short_string) + + self.assertEquals( + event_objects[12].timestamp, + timelib_test.CopyStringToTimestamp('2010-06-22 05:41:41.000')) + expected_string = u'Session 1 end' + expected_short_string = expected_string + self._TestGetMessageStrings( + event_objects[12], expected_string, expected_short_string) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/recycler.py b/plaso/parsers/recycler.py new file mode 100644 index 0000000..a1f0976 --- /dev/null +++ b/plaso/parsers/recycler.py @@ -0,0 +1,222 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for Windows Recycle files, INFO2 and $I/$R pairs.""" + +import logging + +import construct + +from plaso.events import time_events +from plaso.lib import binary +from plaso.lib import errors +from plaso.lib import eventdata +from plaso.lib import utils +from plaso.parsers import interface +from plaso.parsers import manager + + +class WinRecycleEvent(time_events.FiletimeEvent): + """Convenience class for a Windows Recycle bin EventObject.""" + + DATA_TYPE = 'windows:metadata:deleted_item' + + def __init__( + self, filename_ascii, filename_utf, record_information, record_size): + """Initializes the event object.""" + timestamp = record_information.get('filetime', 0) + + super(WinRecycleEvent, self).__init__( + timestamp, eventdata.EventTimestamp.DELETED_TIME) + + if 'index' in record_information: + self.index = record_information.get('index', 0) + self.offset = record_size * self.index + else: + self.offset = 0 + + self.drive_number = record_information.get('drive', None) + self.file_size = record_information.get('filesize', 0) + + if filename_utf: + self.orig_filename = filename_utf + else: + self.orig_filename = filename_ascii + + # The unicode cast is done on the ASCII string to make + # comparison work better (sometimes a warning that a comparison + # could not be made due to the objects being of different type). + if filename_ascii and unicode(filename_ascii) != filename_utf: + self.orig_filename_legacy = filename_ascii + + +class WinRecycleBinParser(interface.BaseParser): + """Parses the Windows $Recycle.Bin $I files.""" + + NAME = 'recycle_bin' + DESCRIPTION = u'Parser for Windows $Recycle.Bin $I files.' + + # Define a list of all structs needed. + # Struct read from: + # https://code.google.com/p/rifiuti2/source/browse/trunk/src/rifiuti-vista.h + RECORD_STRUCT = construct.Struct( + 'record', + construct.ULInt64('filesize'), + construct.ULInt64('filetime')) + + MAGIC_STRUCT = construct.ULInt64('magic') + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract entries from a Windows RecycleBin $Ixx file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + file_object = file_entry.GetFileObject() + try: + magic_header = self.MAGIC_STRUCT.parse_stream(file_object) + except (construct.FieldError, IOError) as exception: + raise errors.UnableToParseFile( + u'Unable to parse $Ixxx file with error: {0:s}'.format(exception)) + + if magic_header is not 1: + raise errors.UnableToParseFile( + u'Not an $Ixxx file, wrong magic header.') + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + # We may have to rely on filenames since this header is very generic. + # TODO: Rethink this and potentially make a better test. + base_filename = utils.GetBaseName(file_entry.name) + if not base_filename.startswith('$I'): + raise errors.UnableToParseFile( + u'Not an $Ixxx file, filename doesn\'t start with $I.') + + record = self.RECORD_STRUCT.parse_stream(file_object) + filename_utf = binary.ReadUtf16Stream(file_object) + + file_object.close() + event_object = WinRecycleEvent(u'', filename_utf, record, 0) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +class WinRecycleInfo2Parser(interface.BaseParser): + """Parses the Windows Recycler INFO2 file.""" + + NAME = 'recycle_bin_info2' + DESCRIPTION = u'Parser for Windows Recycler INFO2 files.' + + # Define a list of all structs used. + INT32_LE = construct.ULInt32('my_int') + + FILE_HEADER_STRUCT = construct.Struct( + 'file_header', + construct.Padding(8), + construct.ULInt32('record_size')) + + # Struct based on (-both unicode and legacy string): + # https://code.google.com/p/rifiuti2/source/browse/trunk/src/rifiuti.h + RECORD_STRUCT = construct.Struct( + 'record', + construct.ULInt32('index'), + construct.ULInt32('drive'), + construct.ULInt64('filetime'), + construct.ULInt32('filesize')) + + STRING_STRUCT = construct.CString('legacy_filename') + + # Define a list of needed variables. + UNICODE_FILENAME_OFFSET = 0x11C + RECORD_INDEX_OFFSET = 0x108 + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract entries from Windows Recycler INFO2 file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + file_object = file_entry.GetFileObject() + try: + magic_header = self.INT32_LE.parse_stream(file_object) + except (construct.FieldError, IOError) as exception: + raise errors.UnableToParseFile( + u'Unable to parse INFO2 file with error: {0:s}'.format(exception)) + + if magic_header is not 5: + raise errors.UnableToParseFile( + u'Not an INFO2 file, wrong magic header.') + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + # Since this header value is really generic it is hard not to use filename + # as an indicator too. + # TODO: Rethink this and potentially make a better test. + base_filename = utils.GetBaseName(file_entry.name) + if not base_filename.startswith('INFO2'): + raise errors.UnableToParseFile( + u'Not an INFO2 file, filename isn\'t INFO2.') + + file_header = self.FILE_HEADER_STRUCT.parse_stream(file_object) + + # Limit recrodsize to 65536 to be on the safe side. + record_size = file_header['record_size'] + if record_size > 65536: + logging.error(( + u'Record size: {0:d} is too large for INFO2 reducing to: ' + u'65535').format(record_size)) + record_size = 65535 + + # If recordsize is 0x320 then we have UTF/unicode names as well. + read_unicode_names = False + if record_size is 0x320: + read_unicode_names = True + + data = file_object.read(record_size) + while data: + if len(data) != record_size: + break + filename_ascii = self.STRING_STRUCT.parse(data[4:]) + record_information = self.RECORD_STRUCT.parse( + data[self.RECORD_INDEX_OFFSET:]) + if read_unicode_names: + filename_utf = binary.ReadUtf16( + data[self.UNICODE_FILENAME_OFFSET:]) + else: + filename_utf = u'' + + event_object = WinRecycleEvent( + filename_ascii, filename_utf, record_information, record_size) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + data = file_object.read(record_size) + + file_object.close() + + +manager.ParsersManager.RegisterParser(WinRecycleBinParser) diff --git a/plaso/parsers/recycler_test.py b/plaso/parsers/recycler_test.py new file mode 100644 index 0000000..aba7358 --- /dev/null +++ b/plaso/parsers/recycler_test.py @@ -0,0 +1,111 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Windows recycler parsers.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import recycler as recycler_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import recycler +from plaso.parsers import test_lib + + +class WinRecycleBinParserTest(test_lib.ParserTestCase): + """Tests for the Windows Recycle Bin parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = recycler.WinRecycleBinParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['$II3DF3L.zip']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 1) + + event_object = event_objects[0] + + self.assertEquals(event_object.orig_filename, ( + u'C:\\Users\\nfury\\Documents\\Alloy Research\\StarFury.zip')) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-03-12 20:49:58.633') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals(event_object.file_size, 724919) + + expected_msg = ( + u'C:\\Users\\nfury\\Documents\\Alloy Research\\StarFury.zip ' + u'(from drive C?)') + expected_msg_short = ( + u'Deleted file: C:\\Users\\nfury\\Documents\\Alloy Research\\' + u'StarFury.zip') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +class WinRecyclerInfo2ParserTest(test_lib.ParserTestCase): + """Tests for the Windows Recycler INFO2 parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = recycler.WinRecycleInfo2Parser() + + def testParse(self): + """Reads an INFO2 file and run a few tests.""" + test_file = self._GetTestFilePath(['INFO2']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 4) + + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2004-08-25 16:18:25.237') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals(event_object.timestamp_desc, + eventdata.EventTimestamp.DELETED_TIME) + + self.assertEquals(event_object.index, 1) + self.assertEquals(event_object.orig_filename, ( + u'C:\\Documents and Settings\\Mr. Evil\\Desktop\\lalsetup250.exe')) + + event_object = event_objects[1] + + expected_msg = ( + u'DC2 -> C:\\Documents and Settings\\Mr. Evil\\Desktop' + u'\\netstumblerinstaller_0_4_0.exe [C:\\Documents and ' + u'Settings\\Mr. Evil\\Desktop\\netstumblerinstaller_0_4_0.exe] ' + u'(from drive C)') + expected_msg_short = ( + u'Deleted file: C:\\Documents and Settings\\Mr. Evil\\Desktop' + u'\\netstumblerinstaller...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + event_object = event_objects[2] + + self._TestGetSourceStrings(event_object, u'Recycle Bin', u'RECBIN') + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/rubanetra.py b/plaso/parsers/rubanetra.py new file mode 100755 index 0000000..ac002a5 --- /dev/null +++ b/plaso/parsers/rubanetra.py @@ -0,0 +1,754 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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 logging +from plaso.lib import errors, event, timelib +from plaso.events.time_events import TimestampEvent +from plaso.lib.eventdata import EventTimestamp +from plaso.parsers import interface +from plaso.parsers import manager + +try: + import xml.etree.cElementTree as ElementTree +except ImportError: + import xml.etree.ElementTree as ElementTree + +__author__ = 'Stefan Swerk (stefan_rubanetra@swerk.priv.at)' + + +class RubanetraXmlParser(interface.BaseParser): + """ This class represents the python parser-component of the Rubanetra + project. Currently, it is only capable of parsing files adhering to the + XML standard and depends on the 'xml.etree' library. + """ + NAME = 'rubanetra_xml_parser' + DESCRIPTION = u'Rubanetra XML file parser' + VERSION = u'0.0.6' + + RUBANETRA_METADATA_FIELDS = frozenset(['implementationVersion', 'implementationTitle', 'implementationVendor']) + + def Parse(self, parser_context, file_entry, parser_chain=None): + """ Parses a XML file containing Rubanetra produced content. + + :param parser_context: A parser context object (instance of ParserContext). + :param file_entry: A file entry object (instance of dfvfs.FileEntry). + :param parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + rubanetra_metadata_fields = set(self.RUBANETRA_METADATA_FIELDS) + rubanetra_metadata_dict = dict() + + file_handle = None + try: + if file_entry is not None and file_entry.IsFile(): + # Open the file read-only. + file_handle = file_entry.GetFileObject() + else: + raise errors.UnableToParseFile(u'Not a valid Rubanetra file.') + + file_size = file_handle.get_size() + + if file_size <= 0: + raise errors.UnableToParseFile(u'File size: {0:d} bytes is less or equal than 0.'.format(file_size)) + + # read from the beginning and check whether this is a XML file + file_handle.seek(0, os.SEEK_SET) + # get an iterable xml parser context + xml_parser_context = ElementTree.iterparse(file_handle, events=('start', 'end')) + # turn it into an iterator + xml_parser_context = iter(xml_parser_context) + # check whether this is a valid XML file + try: + xml_parser_context.next() + except ElementTree.ParseError: + raise errors.UnableToParseFile(u'Not a valid Rubanetra file (not XML).') + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + # read from the beginning to process metadata + file_handle.seek(0, os.SEEK_SET) + # get an iterable xml parser context + xml_parser_context = ElementTree.iterparse(file_handle, events=('start', 'end')) + # turn it into an iterator + xml_parser_context = iter(xml_parser_context) + # get the root element + xml_event, xml_root = xml_parser_context.next() + + for xml_event, xml_elem in xml_parser_context: + if xml_event == 'end': + # ... process metadata ... + if xml_elem.tag in rubanetra_metadata_fields: + rubanetra_metadata_dict[xml_elem.tag] = xml_elem.text + rubanetra_metadata_fields.discard(xml_elem.tag) + elif len(rubanetra_metadata_fields) == 0: + xml_elem.clear() + xml_root.clear() + break + + xml_elem.clear() + xml_root.clear() + + if len(rubanetra_metadata_fields) != 0: + raise errors.UnableToParseFile( + u'Unable to verify metadata, required fields {0:s} could not be parsed'.format(rubanetra_metadata_fields)) + + self.validate_metadata(rubanetra_metadata_dict) + + # reopen the file handle + file_handle.seek(0, os.SEEK_SET) + + # get an iterable xml parser context + xml_parser_context = ElementTree.iterparse(file_handle, events=('start', 'end')) + # turn it into an iterator + xml_parser_context = iter(xml_parser_context) + # get the root element + xml_event, xml_root = xml_parser_context.next() + + for xml_event, xml_elem in xml_parser_context: + if xml_event == 'end': + # ... process activities ... + if xml_elem.tag == 'activity': + activity_dict_list = self.element_to_dict(xml_elem).pop('activity') + event_objects = self.parse_activity(merge_list_of_dicts_to_dict(activity_dict_list)) + parser_context.ProduceEvents(event_objects, parser_chain=parser_chain, file_entry=file_entry) + xml_root.clear() + finally: + if file_handle is not None: + file_handle.close() + + + def parse_activity(self, activity_dict_merged): + """ Takes a dictionary resembling an arbitrary activity and parses all fields in a recursive manner in order + to process nested activities as well as ordinary leaf values. + The following cases are currently handled: + - Nested and split activities may occur as a list of dictionaries, as long as each activity dict contains + an 'activityType' + - Nested activities are allowed to occur under arbitrary key values + - EventObjects are constructed using reflective access to the event class, i.e. each occurring 'activityType' must + be mapped via 'activity_type_to_class_dict' and the mapped class must have a constructor that can handle the + parsing process of a single flat activity using a dict. + - If there is no mapping of an occurring 'activityType' within 'activity_type_to_class_dict', a BaseActivityEvent + will be constructed instead. + - Leaf values that do not represent an entire Activity will not be modified but instead passed as constructor + argument to the corresponding EventObject implementation. + + :param activity_dict_merged: an arbitrary activity as dictionary, conforming to a certain 'activityType'. + :return: a list of EventObjects that could be parsed from the given dictionary, corresponding to the + value of 'activityType', if possible. Otherwise, either an empty list, in case the given dict does not + contain a valid activity, or None, in case the given argument is not a dict, will be returned. + """ + if activity_dict_merged is None: + return None + + event_objects = list() + if isinstance(activity_dict_merged, list): + for v in activity_dict_merged: + if isinstance(v, dict) and 'activityType' in v: + if len(v) == 1: + # It is safe to assume that this is a nested and split activity. Therefore break the loop + # and parse it. This assumption does no longer hold true in case of a scenario where + # an activity_dict_list contains both, a split activity and another nested activity_dict. + # Currently the xml parser handles this case by wrapping the nested activity_dict in another + # list. + return self.parse_activity(merge_list_of_dicts_to_dict(activity_dict_merged)) + else: + # this is a nested activity, parse and save objects + child_evt_objs = self.parse_activity(v) + if child_evt_objs is not None: + for e in child_evt_objs: + if isinstance(e, event.EventObject): + event_objects += [e] + + else: + event_objects += self.parse_activity(v) + + return event_objects + + # is it a leaf value or an actual activity? + if not isinstance(activity_dict_merged, dict): + return None + + + # it is at least a dict, however, is it an activity or a value? + activity_type = activity_dict_merged.get('activityType', None) + if activity_type is not None: # it is an activity + event_object_class = activity_type_to_class_dict.get(activity_type, BaseActivityEvent) + event_objects = [event_object_class(activity_dict_merged)] # TODO check whether it is an actual class + + # everything that remains may be another activity or an unconsumed leaf value + for k, v in activity_dict_merged.items(): + child_evt_objs = self.parse_activity(v) + # currently, the key attribute is not used -> TODO: find a way to link the child-events to its parent? + if child_evt_objs is not None: + for e in child_evt_objs: + if isinstance(e, event.EventObject): + event_objects += [e] + + return event_objects + + def parse_timestamp_events(self, activity_dict): + """ Takes a dictionary of dictionaries that must contain at least two keys: + - 'startInstant', a dictionary that corresponds to a serialized Java Instant object + - 'endInstant', as above + + This method will produce a dictionary consisting of either one or two JavaInstantEvent objects. + + :param activity_dict: containing the 'startInstant' and 'endInstant' dictionaries + :return: either one, iff 'startInstant' == 'endInstant', or two JavaInstantEvent-objects inside a dictionary. + """ + start_instant_dict = merge_list_of_dicts_to_dict(activity_dict.get('startInstant', None)) + end_instant_dict = merge_list_of_dicts_to_dict(activity_dict.get('endInstant', None)) + + if start_instant_dict != end_instant_dict: + instant_dict = dict(startInstant=None, endInstant=None) + # interval + start_instant_evt = JavaInstantEvent.from_java_instant_dict(start_instant_dict, + EventTimestamp.FIRST_CONNECTED) + end_instant_evt = JavaInstantEvent.from_java_instant_dict(end_instant_dict, + EventTimestamp.LAST_CONNECTED) + instant_dict['startInstant'] = start_instant_evt + instant_dict['endInstant'] = end_instant_evt + return instant_dict + else: + return dict(startInstant=JavaInstantEvent.from_java_instant_dict(start_instant_dict, u'Pcap time stamp')) + + def element_to_dict(self, elem): + """ Internal method to transform a XML node to a dictionary. + + :param elem: the XML node + :return: a dictionary containing the values below 'elem', using 'elem.tag' as respective keys + """ + return {elem.tag: map(self.element_to_dict, list(elem)) or elem.text} + + def validate_metadata(self, rubanetra_metadata_dict): + """ Tries to verify that the parsed XML document corresponds to a known version to prevent potential + issues due to version incompatibility. An exception will be raised if such a case is encountered. + + :param rubanetra_metadata_dict: a dictionary containing the basic Rubanetra metadata values + """ + if rubanetra_metadata_dict.get('implementationTitle') != u'Rubanetra': + raise errors.UnableToParseFile(u'Unknown Rubanetra implementation title encountered.') + + version = rubanetra_metadata_dict.get('implementationVersion') + if version != self.VERSION: + logging.warning(u'Rubanetra version number mismatch, expected:{0:s}, actual:{1:s}'.format(self.VERSION, version)) + + + def link_activity(self, event_object_from, event_object_to): + """ This method is currently unused, however, in case it is necessary to group multiple events, + a link between those events must be established. Whether a backtracking chain or a forward-chain should be + established depends entirely on the caller. + Currently, the UUID of 'event_object_to' will be appended to the list 'related_activity_uuids' of + 'event_object_from'. + """ + if isinstance(event_object_to, BaseActivityEvent): + event_object_from.related_activity_uuids.append(event_object_to.uuid) + # TODO: else error + + +class BaseActivityEvent(event.EventObject): + def __init__(self, activity_dict, + data_type='java:rubanetra:base_activity'): + """Initializes the base event object. + + Args: + activity_dict: A dictionary containing all related BaseActivity key/value pairs. + """ + super(BaseActivityEvent, self).__init__() + + if activity_dict is None: + raise errors.UnableToParseFile + + self.data_type = data_type + self.activity_type = activity_dict.pop('activityType', 'BaseActivity') + self.description = activity_dict.pop('description', None) + compound_frame_number_dict_list = activity_dict.pop('compoundFrameNumbers', None) + self.compound_frame_number_list = list() + if compound_frame_number_dict_list is not None: + for d in compound_frame_number_dict_list: + for k, v in d.items(): + self.compound_frame_number_list.append(long(v)) # TODO checks + + self.optional_field_dict = merge_list_of_dicts_to_dict(activity_dict.pop('optionalFields', None)) + self.replaced = string_to_boolean(activity_dict.pop('replaced', None)) + self.source_address = activity_dict.pop('sourceAddressAsString', None) + self.destination_address = activity_dict.pop('destinationAddressAsString', None) + + start_instant_dict = merge_list_of_dicts_to_dict(activity_dict.get('startInstant', None)) + end_instant_dict = merge_list_of_dicts_to_dict(activity_dict.get('endInstant', None)) + + start_instant_evt = None + if start_instant_dict != end_instant_dict: + start_instant_evt = JavaInstantEvent.from_java_instant_dict(start_instant_dict, + EventTimestamp.FIRST_CONNECTED) + # interval + end_instant_evt = JavaInstantEvent.from_java_instant_dict(end_instant_dict, + EventTimestamp.LAST_CONNECTED) + self.last_timestamp = end_instant_evt.timestamp if end_instant_evt is not None else None + else: + start_instant_evt = JavaInstantEvent.from_java_instant_dict(start_instant_dict, u'Pcap time stamp') + self.timestamp = self.first_timestamp = start_instant_evt.timestamp if start_instant_evt is not None else None + self.timestamp_desc = start_instant_evt.timestamp_desc + self.related_activity_uuids = list() + + +class PcapActivityEvent(BaseActivityEvent): + def __init__(self, pcap_activity_dict): + super(PcapActivityEvent, self).__init__(pcap_activity_dict, + data_type='java:rubanetra:pcap_activity') + pcap_packet = merge_list_of_dicts_to_dict(pcap_activity_dict.pop('pcapPacket', None)) + if pcap_packet is not None: + self.pcap_total_size = pcap_packet.pop('totalSize', None) + self.pcap_frame_number = pcap_packet.pop('frameNumber', None) + self.pcap_packet_wirelen = pcap_packet.pop('packetWirelen', None) + self.pcap_header_count = pcap_packet.pop('headerCount', None) + + +class HttpRequestActivityEvent(BaseActivityEvent): + def __init__(self, http_request_activity_dict): + super(HttpRequestActivityEvent, self).__init__(http_request_activity_dict, + data_type='java:rubanetra:http_request_activity') + self.http_version = http_request_activity_dict.pop('httpVersion', None) + self.server_address = http_request_activity_dict.pop('serverAddress', None) + self.client_address = http_request_activity_dict.pop('clientAddress', None) + self.http_method = http_request_activity_dict.pop('httpMethod', None) + self.http_query_string = http_request_activity_dict.pop('httpQueryString', None) + self.http_query_parameters = http_request_activity_dict.pop('httpQueryParameters', None) + self.http_request_header_dict = http_request_activity_dict.pop('requestHeaderMap', None) + self.url = http_request_activity_dict.pop('url', None) + + http_request = merge_list_of_dicts_to_dict(http_request_activity_dict.pop('httpRequest', None)) + if http_request is not None: + self.orig_http_header = http_request.pop('header', None) + self.content_type = http_request.pop('contentType', None) + self.is_response = http_request.pop('response', None) + self.jnetpcap_http_string = http_request.pop('JNetPcap-HTTP-String', None) + + self.source_address = self.client_address + self.destination_address = self.server_address + + +class HttpResponseActivityEvent(BaseActivityEvent): + def __init__(self, http_response_activity_dict): + super(HttpResponseActivityEvent, self).__init__(http_response_activity_dict, + data_type='java:rubanetra:http_response_activity') + self.http_version = http_response_activity_dict.pop('httpVersion', None) + self.response_status_code = http_response_activity_dict.pop('responseStatusCode', None) + self.response_status_line = http_response_activity_dict.pop('responseStatusLine', None) + self.response_header_dict = http_response_activity_dict.pop('responseHeaderMap', None) + + http_response = merge_list_of_dicts_to_dict(http_response_activity_dict.pop('httpResponse', None)) + + if http_response is not None: + self.orig_http_header = http_response.pop('header', None) + self.content_type = http_response.pop('contentType', None) + self.is_response = http_response.pop('response', None) + self.jnetpcap_http_string = http_response.pop('JNetPcap-HTTP-String', None) + + +class HttpImageActivityEvent(BaseActivityEvent): + def __init__(self, http_image_activity_dict): + super(HttpImageActivityEvent, self).__init__(http_image_activity_dict, + data_type='java:rubanetra:http_image_activity') + + self.image_type = http_image_activity_dict.pop('imageType', None) + self.image_path = http_image_activity_dict.pop('imagePath', None) + + +class DnsActivityEvent(BaseActivityEvent): + def __init__(self, dns_activity_dict): + super(DnsActivityEvent, self).__init__(dns_activity_dict, + data_type='java:rubanetra:dns_activity') + self.question_record_list = dns_activity_dict.pop('questionRecords', None) + self.answer_record_list = dns_activity_dict.pop('answerRecords', None) + self.authority_record_list = dns_activity_dict.pop('authorityRecords', None) + self.additional_record_list = dns_activity_dict.pop('additionalRecords', None) + self.dns_message_header = merge_list_of_dicts_to_dict(dns_activity_dict.pop('dnsMessageHeader', None)) + self.is_response_bool = string_to_boolean(dns_activity_dict.pop('response', None)) + + +class ArpActivityEvent(BaseActivityEvent): + def __init__(self, arp_activity_dict): + super(ArpActivityEvent, self).__init__(arp_activity_dict, + data_type='java:rubanetra:arp_activity') + self.hardware_type = arp_activity_dict.pop('hardwareType', None) + self.protocol_type = arp_activity_dict.pop('protocolType', None) + self.hardware_address_length = arp_activity_dict.pop('hardwareAddressLength', None) + self.protocol_address_length = arp_activity_dict.pop('protocolAddressLength', None) + self.sender_mac_address = arp_activity_dict.pop('senderHardwareAddress', None) + self.target_mac_address = arp_activity_dict.pop('targetHardwareAddress', None) + self.sender_protocol_address = arp_activity_dict.pop('senderProtocolAddress', None) + self.target_protocol_address = arp_activity_dict.pop('targetProtocolAddress', None) + self.jnetpcap_arp = arp_activity_dict.pop('arp', None) + + +class DhcpActivityEvent(BaseActivityEvent): + def __init__(self, dhcp_activity_dict): + super(DhcpActivityEvent, self).__init__(dhcp_activity_dict, + data_type='java:rubanetra:dhcp_activity') + self.dhcp_message = dhcp_activity_dict.pop('dhcpMessage', None) + + +class EthernetActivityEvent(BaseActivityEvent): + def __init__(self, ethernet_activity_dict): + super(EthernetActivityEvent, self).__init__(ethernet_activity_dict, + data_type='java:rubanetra:ethernet_activity') + self.source_mac_address = ethernet_activity_dict.pop('sourceMacAddress', None) + self.destination_mac_address = ethernet_activity_dict.pop('destinationMacAddress', None) + self.ethernet_type = ethernet_activity_dict.pop('ethernetType', None) + self.ethernet_type_enum = ethernet_activity_dict.pop('ethernetTypeEnum', None) + self.jnetpcap_ethernet = ethernet_activity_dict.pop('ethernet', None) + + +class FtpActivityEvent(BaseActivityEvent): + def __init__(self, ftp_activity_dict): + super(FtpActivityEvent, self).__init__(ftp_activity_dict, + data_type='java:rubanetra:ftp_activity') + self.ftp_type = ftp_activity_dict.pop('ftpActivityType', None) + self.command = ftp_activity_dict.pop('command', None) + self.reply = ftp_activity_dict.pop('reply', None) + self.list = ftp_activity_dict.pop('list', None) + + +class Icmpv4ActivityEvent(BaseActivityEvent): + def __init__(self, icmpv4_activity_dict): + super(Icmpv4ActivityEvent, self).__init__(icmpv4_activity_dict, + data_type='java:rubanetra:icmpv4_activity') + self.icmp_subtype = icmpv4_activity_dict.pop('icmpSubType', None) + self.icmp_packet = icmpv4_activity_dict.pop('icmpPacket', None) + self.icmp_message = icmpv4_activity_dict.pop('icmpMessage', None) + self.icmp_type = icmpv4_activity_dict.pop('icmpType', None) + self.icmp_code = icmpv4_activity_dict.pop('icmpCode', None) + self.source_address = icmpv4_activity_dict.pop('sourceAddress', None) + self.destination_address = icmpv4_activity_dict.pop('destinationAddress', None) + self.identifier = icmpv4_activity_dict.pop('identifier', None) + self.sequence = icmpv4_activity_dict.pop('sequence', None) + self.jnetpcap_icmp = icmpv4_activity_dict.pop('icmp', None) + + +class Icmpv6ActivityEvent(BaseActivityEvent): + def __init__(self, icmpv6_activity_dict): + super(Icmpv6ActivityEvent, self).__init__(icmpv6_activity_dict, + data_type='java:rubanetra:icmpv6_activity') + self.icmp_subtype = icmpv6_activity_dict.pop('icmpSubType', None) + self.icmp_packet = icmpv6_activity_dict.pop('icmpPacket', None) + self.icmp_message = icmpv6_activity_dict.pop('icmpMessage', None) + self.icmp_type = icmpv6_activity_dict.pop('icmpType', None) + self.jnetpcap_icmp = icmpv6_activity_dict.pop('icmp', None) + + +class IpActivityEvent(BaseActivityEvent): + def __init__(self, ip_activity_dict): + super(IpActivityEvent, self).__init__(ip_activity_dict, + data_type='java:rubanetra:ip_activity') + self.version = ip_activity_dict.pop('version', None) + self.protocol = ip_activity_dict.pop('protocol', None) + self.source_address = ip_activity_dict.pop('sourceAddress', None) + self.destination_address = ip_activity_dict.pop('destinationAddress', None) + + +class Ipv4ActivityEvent(BaseActivityEvent): + def __init__(self, ip_activity_dict): + super(Ipv4ActivityEvent, self).__init__(ip_activity_dict, + data_type='java:rubanetra:ipv4_activity') + self.internet_header_length = ip_activity_dict.pop('internetHeaderLength', None) + self.differentiated_services_code_point = ip_activity_dict.pop('differentiatedServicesCodePoint', None) + self.total_length = ip_activity_dict.pop('totalLength', None) + self.identification = ip_activity_dict.pop('identification', None) + self.flags = ip_activity_dict.pop('flags', None) + self.fragment_offset = ip_activity_dict.pop('fragmentOffset', None) + self.time_to_live = ip_activity_dict.pop('timeToLive', None) + self.header_checksum = ip_activity_dict.pop('headerChecksum', None) + self.options = ip_activity_dict.pop('options', None) + self.jnetpcap_ip4 = ip_activity_dict.pop('ipv4', None) + + +class Ipv6ActivityEvent(BaseActivityEvent): + def __init__(self, ip_activity_dict): + super(Ipv6ActivityEvent, self).__init__(ip_activity_dict, + data_type='java:rubanetra:ipv6_activity') + self.traffic_class = ip_activity_dict.pop('trafficClass', None) + self.flow_label = ip_activity_dict.pop('flowLabel', None) + self.payload_length = ip_activity_dict.pop('payloadLength', None) + self.next_header = ip_activity_dict.pop('nextHeader', None) + self.hop_limit = ip_activity_dict.pop('hopLimit', None) + self.jnetpcap_ip6 = ip_activity_dict.pop('ipv6', None) + self.kraken_ip6 = ip_activity_dict.pop('ipv6Packet', None) + + +class MsnActivityEvent(BaseActivityEvent): + def __init__(self, activity_dict): + super(MsnActivityEvent, self).__init__(activity_dict, + data_type='java:rubanetra:msn_activity') + self.account = activity_dict.pop('account', None) + self.chat = activity_dict.pop('chat', None) + + +class NetbiosActivityEvent(BaseActivityEvent): + def __init__(self, activity_dict): + super(NetbiosActivityEvent, self).__init__(activity_dict, + data_type='java:rubanetra:netbios_activity') + self.datagram_packet = activity_dict.pop('datagramPacket', None) + self.name_packet = activity_dict.pop('namePacket', None) + + +class Pop3ActivityEvent(BaseActivityEvent): + def __init__(self, activity_dict): + super(Pop3ActivityEvent, self).__init__(activity_dict, + data_type='java:rubanetra:pop3_activity') + self.sub_type = activity_dict.pop('subType', None) + self.header = activity_dict.pop('header', None) + self.data = activity_dict.pop('data', None) + self.command = activity_dict.pop('command', None) + self.response = activity_dict.pop('response', None) + + +class SmtpCommandActivityEvent(BaseActivityEvent): + def __init__(self, activity_dict): + super(SmtpCommandActivityEvent, self).__init__(activity_dict, + data_type='java:rubanetra:smtp_command_activity') + self.command = activity_dict.pop('command', None) + self.parameter = activity_dict.pop('parameter', None) + + +class SmtpReplyActivityEvent(BaseActivityEvent): + def __init__(self, activity_dict): + super(SmtpReplyActivityEvent, self).__init__(activity_dict, + data_type='java:rubanetra:smtp_reply_activity') + self.code = activity_dict.pop('code', None) + self.message = activity_dict.pop('message', None) + + +class SmtpSendActivityEvent(BaseActivityEvent): + def __init__(self, activity_dict): + super(SmtpSendActivityEvent, self).__init__(activity_dict, + data_type='java:rubanetra:smtp_send_activity') + self.header = activity_dict.pop('header', None) + self.data = activity_dict.pop('data', None) + + +class Snmpv1ActivityEvent(BaseActivityEvent): + def __init__(self, activity_dict): + super(Snmpv1ActivityEvent, self).__init__(activity_dict, + data_type='java:rubanetra:snmpv1_activity') + self.pdu = activity_dict.pop('pdu', None) + self.source_socket_address = activity_dict.pop('sourceSocketAddress', None) + self.destination_socket_address = activity_dict.pop('destinationSocketAddress', None) + + +class Snmpv2ActivityEvent(BaseActivityEvent): + def __init__(self, activity_dict): + super(Snmpv2ActivityEvent, self).__init__(activity_dict, + data_type='java:rubanetra:snmpv2_activity') + self.pdu = activity_dict.pop('pdu', None) + self.source_socket_address = activity_dict.pop('sourceSocketAddress', None) + self.destination_socket_address = activity_dict.pop('destinationSocketAddress', None) + + +class TcpActivityEvent(BaseActivityEvent): + def __init__(self, activity_dict): + super(TcpActivityEvent, self).__init__(activity_dict, + data_type='java:rubanetra:tcp_activity') + self.source_port = activity_dict.pop('sourcePort', None) + self.destination_port = activity_dict.pop('destinationPort', None) + self.sequence_number = activity_dict.pop('sequenceNumber', None) + self.acknowledge_number = activity_dict.pop('acknowledgeNumber', None) + self.relative_sequence_number = activity_dict.pop('relativeSequenceNumber', None) + self.relative_acknowledge_number = activity_dict.pop('relativeAcknowledgeNumber', None) + self.data_offset = activity_dict.pop('dataOffset', None) + self.control_bits = activity_dict.pop('controlBits', None) + self.window_size = activity_dict.pop('windowSize', None) + self.checksum = activity_dict.pop('checksum', None) + self.urgent_pointer = activity_dict.pop('urgentPointer', None) + self.tcp_length = activity_dict.pop('tcpLength', None) + self.options = activity_dict.pop('options', None) + self.padding = activity_dict.pop('padding', None) + self.syn = activity_dict.pop('syn', None) + self.ack = activity_dict.pop('ack', None) + self.psh = activity_dict.pop('psh', None) + self.fin = activity_dict.pop('fin', None) + self.rst = activity_dict.pop('rst', None) + self.urg = activity_dict.pop('urg', None) + self.direction = activity_dict.pop('direction', None) + self.client_state = activity_dict.pop('clientState', None) + self.server_state = activity_dict.pop('serverState', None) + self.jnetpcap_tcp = activity_dict.pop('tcp', None) + self.source_address = activity_dict.pop('sourceAddress', None) + self.destination_address = activity_dict.pop('destinationAddress', None) + self.source_socket_address = activity_dict.pop('sourceSocketAddress', None) + self.destination_socket_address = activity_dict.pop('destinationSocketAddress', None) + + +class TelnetActivityEvent(BaseActivityEvent): + def __init__(self, activity_dict): + super(TelnetActivityEvent, self).__init__(activity_dict, + data_type='java:rubanetra:telnet_activity') + self.sub_type = activity_dict.pop('subType', None) + self.command = activity_dict.pop('command', None) + self.option = activity_dict.pop('option', None) + self.ansi_mode = activity_dict.pop('ansiMode', None) + self.arguments = activity_dict.pop('arguments', None) + self.text = activity_dict.pop('text', None) + self.title = activity_dict.pop('title', None) + + +class TlsActivityEvent(BaseActivityEvent): + def __init__(self, activity_dict): + super(TlsActivityEvent, self).__init__(activity_dict, + data_type='java:rubanetra:tls_activity') + self.client_to_server_traffic = activity_dict.pop('clientToServerTraffic', None) + self.server_to_client_traffic = activity_dict.pop('serverToClientTraffic', None) + + +class UdpActivityEvent(BaseActivityEvent): + def __init__(self, activity_dict): + super(UdpActivityEvent, self).__init__(activity_dict, + data_type='java:rubanetra:udp_activity') + self.source_port = activity_dict.pop('sourcePort', None) + self.destination_port = activity_dict.pop('destinationPort', None) + self.length = activity_dict.pop('length', None) + self.checksum = activity_dict.pop('checksum', None) + self.jnetpcap_udp = activity_dict.pop('udp', None) + self.source_socket_address = activity_dict.pop('sourceSocketAddress', None) + self.destination_socket_address = activity_dict.pop('destinationSocketAddress', None) + + +class OpenSSHActivityEvent(BaseActivityEvent): + def __init__(self, activity_dict): + super(OpenSSHActivityEvent, self).__init__(activity_dict, + data_type='java:rubanetra:open_ssh_activity') + self.client_to_server_traffic = activity_dict.pop('clientToServerTraffic', None) + self.server_to_client_traffic = activity_dict.pop('serverToClientTraffic', None) + + +class DropboxTlsActivityEvent(BaseActivityEvent): + def __init__(self, activity_dict): + super(DropboxTlsActivityEvent, self).__init__(activity_dict, + data_type='java:rubanetra:dropbox_tls_activity') + self.client_address = activity_dict.pop('clientAddress', None) + self.server_address = activity_dict.pop('serverAddress', None) + + +class SpiderOakActivityEvent(BaseActivityEvent): + def __init__(self, activity_dict): + super(SpiderOakActivityEvent, self).__init__(activity_dict, + data_type='java:rubanetra:spideroak_activity') + self.client_address = activity_dict.pop('clientAddress', None) + self.server_address = activity_dict.pop('serverAddress', None) + + +class SkypePayloadActivityEvent(BaseActivityEvent): + def __init__(self, activity_dict): + super(SkypePayloadActivityEvent, self).__init__(activity_dict, + data_type='java:rubanetra:skype_payload_activity') + self.source_object_id = activity_dict.pop('sourceObjectId', None) + self.destination_object_id = activity_dict.pop('destinationObjectId', None) + self.source_host = activity_dict.pop('sourceHost', None) + self.destination_host = activity_dict.pop('destinationHost', None) + + +class JavaInstantEvent(TimestampEvent): + """Convenience class for a Java Instant-based event.""" + + def __init__(self, instant_epoch_seconds, instant_nano, usage, data_type='java:time:Instant'): + """Initializes a Java instant-based event object. + + Args: + java_java_instant_epoch_seconds: The Java epoch seconds value (long). + java_java_instant_nano: The Java nano seconds value (long), will be reduced to microsecond precision. + usage: The description of the usage of the instant value. + data_type: The event data type. If not set data_type is derived + from DATA_TYPE. + """ + super(JavaInstantEvent, self).__init__( + timelib.Timestamp.FromPosixTimeWithMicrosecond(instant_epoch_seconds, instant_nano / 1000), + usage, data_type) + self.instant_epoch_seconds = instant_epoch_seconds + self.instant_nano = instant_nano + self.related_activity_uuids = list() + + @classmethod + def from_java_instant_dict(cls, java_instant_as_dict, usage, data_type='java:time:Instant'): + # TODO: validate fields + instant_epoch_seconds = long(java_instant_as_dict.pop('epochSecond', -1)) + instant_nano = long(java_instant_as_dict.pop('nano', -1)) + + return cls(instant_epoch_seconds, instant_nano, usage, data_type) + +""" FIXME: This method is ineffective for now, because + it is apparently not possible to specify a filter expression that + is based on a boolean value. +""" +def string_to_boolean(s): + """ Returns true, iff s.lower() in ('true', '1') + + :param s: a String representation of a boolean value + :return:true, iff s.lower() in ('true', '1'), false otherwise + """ + #return s.lower() in ('true', '1') + return s + + +def merge_list_of_dicts_to_dict(list_of_dicts): + """ Takes a list of dictionaries and transforms it to a flat dictionary, overwriting duplicate keys in the process. + + :param list_of_dicts: a list of dictionaries + :return: a flat dictionary containing the keys and values of all dictionaries that were previously located inside the + list. If two dictionaries contained the same key, the mapping of the last dictionary that contained that key + will be included, while the older value is discarded. + """ + if list_of_dicts is None or not isinstance(list_of_dicts, list) or isinstance(list_of_dicts, dict): + return list_of_dicts + + return {k: v for d in list_of_dicts for k, v in d.items()} + +# A dictionary of 'activityType' to class mappings. +activity_type_to_class_dict = { + 'ArpActivity': ArpActivityEvent, + 'DhcpActivity': DhcpActivityEvent, + 'DnsActivity': DnsActivityEvent, + 'EthernetActivity': EthernetActivityEvent, + 'FtpActivity': FtpActivityEvent, + 'HttpImageActivity': HttpImageActivityEvent, + 'HttpRequestActivity': HttpRequestActivityEvent, + 'HttpResponseActivity': HttpResponseActivityEvent, + 'Icmpv4Activity': Icmpv4ActivityEvent, + 'Icmpv6Activity': Icmpv6ActivityEvent, + 'IpActivity': IpActivityEvent, + 'Ipv4Activity': Ipv4ActivityEvent, + 'Ipv6Activity': Ipv6ActivityEvent, + 'MsnActivity': MsnActivityEvent, + 'NetbiosActivity': NetbiosActivityEvent, + 'PcapActivity': PcapActivityEvent, + 'Pop3Activity': Pop3ActivityEvent, + 'SmtpCommandActivity': SmtpCommandActivityEvent, + 'SmtpReplyActivity': SmtpReplyActivityEvent, + 'SmtpSendActivity': SmtpSendActivityEvent, + 'TcpActivity': TcpActivityEvent, + 'TelnetActivity': TelnetActivityEvent, + 'TlsActivity': TlsActivityEvent, + 'UdpActivity': UdpActivityEvent, + 'OpenSSHActivity': OpenSSHActivityEvent, + 'DropboxTlsActivity': DropboxTlsActivityEvent, + 'SpiderOakActivity': SpiderOakActivityEvent, + 'SkypePayloadActivity': SkypePayloadActivityEvent} + +manager.ParsersManager.RegisterParser(RubanetraXmlParser) diff --git a/plaso/parsers/selinux.py b/plaso/parsers/selinux.py new file mode 100644 index 0000000..c963338 --- /dev/null +++ b/plaso/parsers/selinux.py @@ -0,0 +1,175 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains SELinux log file parser in plaso. + + Information updated 16 january 2013. + + The parser applies to SELinux 'audit.log' file. + An entry log file example is the following: + + type=AVC msg=audit(1105758604.519:420): avc: denied { getattr } for pid=5962 + comm="httpd" path="/home/auser/public_html" dev=sdb2 ino=921135 + + The Parser will extract the 'type' value, the timestamp abd the 'pid'. + In the previous example, the timestamp is '1105758604.519', and it + represents the EPOCH time (seconds since Jan 1, 1970) plus the + milliseconds past current time (epoch: 1105758604, milliseconds: 519). + + The number after the timestamp (420 in the example) is a 'serial number' + that can be used to correlate multiple logs generated from the same event. + + References + http://selinuxproject.org/page/NB_AL + http://blog.commandlinekungfu.com/2010/08/episode-106-epoch-fail.html + http://www.redhat.com/promo/summit/2010/presentations/ + taste_of_training/Summit_2010_SELinux.pdf +""" + +import logging +import re + +from plaso.events import text_events +from plaso.lib import errors +from plaso.lib import lexer +from plaso.lib import timelib +from plaso.parsers import manager +from plaso.parsers import text_parser + + +__author__ = 'Francesco Picasso (francesco.picasso@gmail.com)' + + +class SELinuxLineEvent(text_events.TextEvent): + """Convenience class for a SELinux log line event.""" + DATA_TYPE = 'selinux:line' + + +class SELinuxParser(text_parser.SlowLexicalTextParser): + """Parse SELinux audit log files.""" + + NAME = 'selinux' + DESCRIPTION = u'Parser for SELinux audit log files.' + + PID_RE = re.compile(r'pid=([0-9]+)[\s]+', re.DOTALL) + + tokens = [ + # Skipping empty lines, both EOLs are considered here and in other states. + lexer.Token('INITIAL', r'^\r?\n', '', ''), + # FSM entry point ('type=anything msg=audit'), critical to recognize a + # SELinux audit file and used to retrieve the audit type. From there two + # next states are possible: TIME or failure, since TIME state is required. + # An empty type is not accepted and it will cause a failure. + # Examples: + # type=SYSCALL msg=audit(...): ... + # type=UNKNOWN[1323] msg=audit(...): ... + lexer.Token( + 'INITIAL', r'^type=([\w]+(\[[0-9]+\])?)[ \t]+msg=audit', 'ParseType', + 'TIMESTAMP'), + lexer.Token( + 'TIMESTAMP', r'\(([0-9]+)\.([0-9]+):([0-9]*)\):', 'ParseTime', + 'STRING'), + # Get the log entry description and stay in the same state. + lexer.Token('STRING', r'[ \t]*([^\r\n]+)', 'ParseString', ''), + # Entry parsed. Note that an empty description is managed and it will not + # raise a parsing failure. + lexer.Token('STRING', r'[ \t]*\r?\n', 'ParseMessage', 'INITIAL'), + # The entry is not formatted as expected, so the parsing failed. + lexer.Token('.', '([^\r\n]+)\r?\n', 'ParseFailed', 'INITIAL') + ] + + def __init__(self): + """Initializes a parser object.""" + # Set local_zone to false, since timestamps are UTC. + super(SELinuxParser, self).__init__(local_zone=False) + self.attributes = {'audit_type': '', 'pid': '', 'body': ''} + self.timestamp = 0 + + def ParseType(self, match=None, **unused_kwargs): + """Parse the audit event type. + + Args: + match: The regular expression match object. + """ + self.attributes['audit_type'] = match.group(1) + + def ParseTime(self, match=None, **unused_kwargs): + """Parse the log timestamp. + + Args: + match: The regular expression match object. + """ + # TODO: do something with match.group(3) ? + try: + number_of_seconds = int(match.group(1), 10) + timestamp = timelib.Timestamp.FromPosixTime(number_of_seconds) + timestamp += int(match.group(2), 10) * 1000 + self.timestamp = timestamp + except ValueError as exception: + logging.error( + u'Unable to retrieve timestamp with error: {0:s}'.format(exception)) + self.timestamp = 0 + raise lexer.ParseError(u'Not a valid timestamp.') + + def ParseString(self, match=None, **unused_kwargs): + """Add a string to the body attribute. + + This method extends the one from TextParser slightly, + searching for the 'pid=[0-9]+' value inside the message body. + + Args: + match: The regular expression match object. + """ + try: + self.attributes['body'] += match.group(1) + # TODO: fix it using lexer or remove pid parsing. + # Indeed this is something that lexer is able to manage, but 'pid' field + # is non positional: so, by doing the following step, the FSM is kept + # simpler. Left the 'to do' as a reminder of possible refactoring. + pid_search = self.PID_RE.search(self.attributes['body']) + if pid_search: + self.attributes['pid'] = pid_search.group(1) + except IndexError: + self.attributes['body'] += match.group(0).strip('\n') + + def ParseFailed(self, **unused_kwargs): + """Entry parsing failed callback.""" + raise lexer.ParseError(u'Unable to parse SELinux log line.') + + def ParseLine(self, parser_context): + """Parse a single line from the SELinux audit file. + + This method extends the one from TextParser slightly, creating a + SELinux event with the timestamp (UTC) taken from log entries. + + Args: + parser_context: A parser context object (instance of ParserContext). + + Returns: + An event object (instance of EventObject) that is constructed + from the selinux entry. + """ + if not self.timestamp: + raise errors.TimestampNotCorrectlyFormed( + u'Unable to parse entry, timestamp not defined.') + offset = getattr(self, 'entry_offset', 0) + event_object = SELinuxLineEvent(self.timestamp, offset, self.attributes) + self.timestamp = 0 + return event_object + + +manager.ParsersManager.RegisterParser(SELinuxParser) diff --git a/plaso/parsers/selinux_test.py b/plaso/parsers/selinux_test.py new file mode 100644 index 0000000..52cff16 --- /dev/null +++ b/plaso/parsers/selinux_test.py @@ -0,0 +1,96 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the selinux log file parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import selinux as selinux_formatter +from plaso.parsers import selinux +from plaso.parsers import test_lib + + +__author__ = 'Francesco Picasso (francesco.picasso@gmail.com)' + + +class SELinuxUnitTest(test_lib.ParserTestCase): + """Tests for the selinux log file parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = selinux.SELinuxParser() + + def testParse(self): + """Tests the Parse function.""" + knowledge_base_values = {'year': 2013} + test_file = self._GetTestFilePath(['selinux.log']) + event_queue_consumer = self._ParseFile( + self._parser, test_file, knowledge_base_values=knowledge_base_values) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 5) + + # Test case: normal entry. + event_object = event_objects[0] + + self.assertEquals(event_object.timestamp, 1337845201174000) + + expected_msg = ( + u'[audit_type: LOGIN, pid: 25443] pid=25443 uid=0 old ' + u'auid=4294967295 new auid=0 old ses=4294967295 new ses=1165') + expected_msg_short = ( + u'[audit_type: LOGIN, pid: 25443] pid=25443 uid=0 old ' + u'auid=4294967295 new auid=...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + # Test case: short date. + event_object = event_objects[1] + + self.assertEquals(event_object.timestamp, 1337845201000000) + + expected_string = u'[audit_type: SHORTDATE] check rounding' + + self._TestGetMessageStrings(event_object, expected_string, expected_string) + + # Test case: no msg. + event_object = event_objects[2] + + self.assertEquals(event_object.timestamp, 1337845222174000) + + expected_string = u'[audit_type: NOMSG]' + + self._TestGetMessageStrings(event_object, expected_string, expected_string) + + # Test case: under score. + event_object = event_objects[3] + + self.assertEquals(event_object.timestamp, 1337845666174000) + + expected_msg = ( + u'[audit_type: UNDER_SCORE, pid: 25444] pid=25444 uid=0 old ' + u'auid=4294967295 new auid=54321 old ses=4294967295 new ses=1166') + expected_msg_short = ( + u'[audit_type: UNDER_SCORE, pid: 25444] pid=25444 uid=0 old ' + u'auid=4294967295 new...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/shared/__init__.py b/plaso/parsers/shared/__init__.py new file mode 100644 index 0000000..f462564 --- /dev/null +++ b/plaso/parsers/shared/__init__.py @@ -0,0 +1,17 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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/plaso/parsers/shared/shell_items.py b/plaso/parsers/shared/shell_items.py new file mode 100644 index 0000000..f3166cb --- /dev/null +++ b/plaso/parsers/shared/shell_items.py @@ -0,0 +1,188 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for Windows NT shell items.""" + +import pyfwsi + +from plaso.events import shell_item_events +from plaso.lib import eventdata +from plaso.winnt import shell_folder_ids + + +if pyfwsi.get_version() < '20140714': + raise ImportWarning( + u'Shell item support functions require at least pyfwsi 20140714.') + + +class ShellItemsParser(object): + """Parses for Windows NT shell items.""" + + NAME = 'shell_items' + + def __init__(self, origin): + """Initializes the parser. + + Args: + origin: A string containing the origin of the event (event source). + """ + super(ShellItemsParser, self).__init__() + self._origin = origin + self._path_segments = [] + + def _BuildParserChain(self, parser_chain=None): + """Return the parser chain with the addition of the current parser. + + Args: + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Returns: + The parser chain, with the addition of the current parser. + """ + if not parser_chain: + return self.NAME + + return u'/'.join([parser_chain, self.NAME]) + + + def _ParseShellItem( + self, parser_context, shell_item, file_entry=None, parser_chain=None): + """Parses a shell item. + + Args: + parser_context: A parser context object (instance of ParserContext). + shell_item: the shell item (instance of pyfwsi.item). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + path_segment = None + + if isinstance(shell_item, pyfwsi.root_folder): + description = shell_folder_ids.DESCRIPTIONS.get( + shell_item.shell_folder_identifier, None) + + if description: + path_segment = description + else: + path_segment = u'{{{0:s}}}'.format(shell_item.shell_folder_identifier) + + elif isinstance(shell_item, pyfwsi.volume): + if shell_item.name: + path_segment = shell_item.name + elif shell_item.identifier: + path_segment = u'{{{0:s}}}'.format(shell_item.identifier) + + elif isinstance(shell_item, pyfwsi.file_entry): + long_name = u'' + localized_name = u'' + file_reference = u'' + for extension_block in shell_item.extension_blocks: + if isinstance(extension_block, pyfwsi.file_entry_extension): + long_name = extension_block.long_name + localized_name = extension_block.localized_name + file_reference = extension_block.file_reference + if file_reference: + file_reference = u'{0:d}-{1:d}'.format( + file_reference & 0xffffffffffff, file_reference >> 48) + + fat_date_time = extension_block.get_creation_time_as_integer() + if fat_date_time: + event_object = shell_item_events.ShellItemFileEntryEvent( + fat_date_time, eventdata.EventTimestamp.CREATION_TIME, + shell_item.name, long_name, localized_name, file_reference, + self._origin) + parser_context.ProduceEvent( + event_object, file_entry=file_entry, + parser_chain=parser_chain) + + fat_date_time = extension_block.get_access_time_as_integer() + if fat_date_time: + event_object = shell_item_events.ShellItemFileEntryEvent( + fat_date_time, eventdata.EventTimestamp.ACCESS_TIME, + shell_item.name, long_name, localized_name, file_reference, + self._origin) + parser_context.ProduceEvent( + event_object, file_entry=file_entry, + parser_chain=parser_chain) + + fat_date_time = shell_item.get_modification_time_as_integer() + if fat_date_time: + event_object = shell_item_events.ShellItemFileEntryEvent( + fat_date_time, eventdata.EventTimestamp.MODIFICATION_TIME, + shell_item.name, long_name, localized_name, file_reference, + self._origin) + parser_context.ProduceEvent( + event_object, file_entry=file_entry, + parser_chain=parser_chain) + + if long_name: + path_segment = long_name + elif shell_item.name: + path_segment = shell_item.name + + elif isinstance(shell_item, pyfwsi.network_location): + if shell_item.location: + path_segment = shell_item.location + + if path_segment is None and shell_item.class_type == 0x00: + # TODO: check for signature 0x23febbee + pass + + if path_segment is None: + path_segment = u'UNKNOWN: 0x{0:02x}'.format(shell_item.class_type) + + self._path_segments.append(path_segment) + + def CopyToPath(self): + """Copies the shell items to a path. + + Returns: + A Unicode string containing the converted shell item list path or None. + """ + if not self._path_segments: + return + + return u', '.join(self._path_segments) + + def Parse( + self, parser_context, byte_stream, codepage='cp1252', + file_entry=None, parser_chain=None): + """Parses the shell items from the byte stream. + + Args: + parser_context: A parser context object (instance of ParserContext). + byte_stream: a string holding the shell items data. + codepage: Optional byte stream codepage. The default is cp1252. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + self._path_segments = [] + shell_item_list = pyfwsi.item_list() + shell_item_list.copy_from_byte_stream(byte_stream, ascii_codepage=codepage) + # Add ourselves to the parser chain, so it is used for subsequent object + # creation. + parser_chain = self._BuildParserChain(parser_chain) + + for shell_item in shell_item_list.items: + self._ParseShellItem( + parser_context, shell_item, file_entry=file_entry, + parser_chain=parser_chain) diff --git a/plaso/parsers/skydrivelog.py b/plaso/parsers/skydrivelog.py new file mode 100644 index 0000000..f00cd57 --- /dev/null +++ b/plaso/parsers/skydrivelog.py @@ -0,0 +1,209 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains SkyDrive log file parser in plaso.""" + +import logging + +import pyparsing + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import manager +from plaso.parsers import text_parser + + +__author__ = 'Francesco Picasso (francesco.picasso@gmail.com)' + + +class SkyDriveLogEvent(time_events.TimestampEvent): + """Convenience class for a SkyDrive log line event.""" + DATA_TYPE = 'skydrive:log:line' + + def __init__(self, timestamp, offset, source_code, log_level, text): + """Initializes the event object. + + Args: + timestamp: The timestamp time value, epoch. + source_code: Details of the source code file generating the event. + log_level: The log level used for the event. + text: The log message. + """ + super(SkyDriveLogEvent, self).__init__( + timestamp, eventdata.EventTimestamp.ADDED_TIME) + self.offset = offset + self.source_code = source_code + self.log_level = log_level + self.text = text + + +class SkyDriveLogParser(text_parser.PyparsingSingleLineTextParser): + """Parse SkyDrive log files.""" + + NAME = 'skydrive_log' + DESCRIPTION = u'Parser for OneDrive (or SkyDrive) log files.' + + ENCODING = 'UTF-8-SIG' + + # Common SDL (SkyDriveLog) pyparsing objects. + SDL_COLON = pyparsing.Literal(u':') + SDL_EXCLAMATION = pyparsing.Literal(u'!') + + # Timestamp (08-01-2013 21:22:28.999). + SDL_TIMESTAMP = ( + text_parser.PyparsingConstants.DATE_REV + + text_parser.PyparsingConstants.TIME_MSEC).setResultsName('timestamp') + + # SkyDrive source code pyparsing structures. + SDL_SOURCE_CODE = pyparsing.Combine( + pyparsing.CharsNotIn(u':') + + SDL_COLON + + text_parser.PyparsingConstants.INTEGER + + SDL_EXCLAMATION + + pyparsing.Word(pyparsing.printables)).setResultsName('source_code') + + # SkyDriveLogLevel pyparsing structures. + SDL_LOG_LEVEL = ( + pyparsing.Literal(u'(').suppress() + + pyparsing.SkipTo(u')').setResultsName('log_level') + + pyparsing.Literal(u')').suppress()) + + # SkyDrive line pyparsing structure. + SDL_LINE = ( + SDL_TIMESTAMP + SDL_SOURCE_CODE + SDL_LOG_LEVEL + + SDL_COLON + pyparsing.SkipTo(pyparsing.lineEnd).setResultsName('text')) + + # Sometimes the timestamped log line is followed by an empy line, + # then by a file name plus other data and finally by another empty + # line. It could happen that a logline is split in two parts. + # These lines will not be discarded and an event will be generated + # ad-hoc (see source), based on the last one if available. + SDL_NO_HEADER_SINGLE_LINE = ( + pyparsing.Optional(pyparsing.Literal(u'->').suppress()) + + pyparsing.SkipTo(pyparsing.lineEnd).setResultsName('text')) + + # Define the available log line structures. + LINE_STRUCTURES = [ + ('logline', SDL_LINE), + ('no_header_single_line', SDL_NO_HEADER_SINGLE_LINE), + ] + + def __init__(self): + """Initializes a parser object.""" + super(SkyDriveLogParser, self).__init__() + self.offset = 0 + self.last_event = None + + def VerifyStructure(self, parser_context, line): + """Verify that this file is a SkyDrive log file. + + Args: + parser_context: A parser context object (instance of ParserContext). + line: A single line from the text file. + + Returns: + True if this is the correct parser, False otherwise. + """ + structure = self.SDL_LINE + parsed_structure = None + timestamp = None + try: + parsed_structure = structure.parseString(line) + except pyparsing.ParseException: + logging.debug(u'Not a SkyDrive log file') + return False + else: + timestamp = self._GetTimestamp(parsed_structure.timestamp) + if not timestamp: + logging.debug(u'Not a SkyDrive log file, invalid timestamp {0:s}'.format( + parsed_structure.timestamp)) + return False + return True + + def ParseRecord(self, parser_context, key, structure): + """Parse each record structure and return an EventObject if applicable. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: An identification string indicating the name of the parsed + structure. + structure: A pyparsing.ParseResults object from a line in the + log file. + + Returns: + An event object (instance of EventObject) or None. + """ + if key == 'logline': + return self._ParseLogline(structure) + elif key == 'no_header_single_line': + return self._ParseNoHeaderSingleLine(structure) + else: + logging.warning( + u'Unable to parse record, unknown structure: {0:s}'.format(key)) + + def _ParseLogline(self, structure): + """Parse a logline and store appropriate attributes.""" + timestamp = self._GetTimestamp(structure.timestamp) + if not timestamp: + logging.debug(u'Invalid timestamp {0:s}'.format(structure.timestamp)) + return + evt = SkyDriveLogEvent( + timestamp, self.offset, structure.source_code, structure.log_level, + structure.text) + self.last_event = evt + return evt + + def _ParseNoHeaderSingleLine(self, structure): + """Parse an isolated line and store appropriate attributes.""" + if not self.last_event: + logging.debug(u'SkyDrive, found isolated line with no previous events') + return + evt = SkyDriveLogEvent( + self.last_event.timestamp, self.last_event.offset, None, None, + structure.text) + # TODO think to a possible refactoring for the non-header lines. + self.last_event = None + return evt + + def _GetTimestamp(self, timestamp_pypr): + """Gets a timestamp from a pyparsing ParseResults timestamp. + + This is a timestamp_string as returned by using + text_parser.PyparsingConstants structures: + [[8, 1, 2013], [21, 22, 28], 999] + + Args: + timestamp_string: The pyparsing ParseResults object + + Returns: + timestamp: A plaso timelib timestamp event or 0. + """ + timestamp = 0 + try: + month, day, year = timestamp_pypr[0] + hour, minute, second = timestamp_pypr[1] + millisecond = timestamp_pypr[2] + timestamp = timelib.Timestamp.FromTimeParts( + year, month, day, hour, minute, second, + microseconds=(millisecond * 1000)) + except ValueError: + timestamp = 0 + return timestamp + + +manager.ParsersManager.RegisterParser(SkyDriveLogParser) diff --git a/plaso/parsers/skydrivelog_test.py b/plaso/parsers/skydrivelog_test.py new file mode 100644 index 0000000..2e7ef4b --- /dev/null +++ b/plaso/parsers/skydrivelog_test.py @@ -0,0 +1,85 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the skydrivelog parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import skydrivelog as skydrivelog_formatter +from plaso.lib import timelib_test +from plaso.parsers import skydrivelog +from plaso.parsers import test_lib + + +__author__ = 'Francesco Picasso (francesco.picasso@gmail.com)' + + +class SkyDriveLogUnitTest(test_lib.ParserTestCase): + """Tests for the skydrivelog parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = skydrivelog.SkyDriveLogParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['skydrive.log']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 18) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-08-01 21:22:28.999') + self.assertEquals(event_objects[0].timestamp, expected_timestamp) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-08-01 21:22:29.702') + self.assertEquals(event_objects[1].timestamp, expected_timestamp) + self.assertEquals(event_objects[2].timestamp, expected_timestamp) + self.assertEquals(event_objects[3].timestamp, expected_timestamp) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-08-01 21:22:58.344') + self.assertEquals(event_objects[4].timestamp, expected_timestamp) + self.assertEquals(event_objects[5].timestamp, expected_timestamp) + + expected_msg = ( + u'[global.cpp:626!logVersionInfo] (DETAIL) 17.0.2011.0627 (Ship)') + expected_msg_short = ( + u'17.0.2011.0627 (Ship)') + self._TestGetMessageStrings( + event_objects[0], expected_msg, expected_msg_short) + + expected_msg = ( + u'SyncToken = LM%3d12345678905670%3bID%3d1234567890E059C0!' + u'103%3bLR%3d12345678905623%3aEP%3d2') + expected_msg_short = ( + u'SyncToken = LM%3d12345678905670%3bID%3d1234567890E059C0!' + u'103%3bLR%3d1234567890...') + self._TestGetMessageStrings( + event_objects[3], expected_msg, expected_msg_short) + + expected_string = ( + u'SyncToken = Not a sync token (\xe0\xe8\xec\xf2\xf9)!') + self._TestGetMessageStrings( + event_objects[17], expected_string, expected_string) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/skydrivelogerr.py b/plaso/parsers/skydrivelogerr.py new file mode 100644 index 0000000..3bde927 --- /dev/null +++ b/plaso/parsers/skydrivelogerr.py @@ -0,0 +1,257 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains SkyDrive error log file parser in plaso.""" + +import logging + +import pyparsing + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import manager +from plaso.parsers import text_parser + + +__author__ = 'Francesco Picasso (francesco.picasso@gmail.com)' + + +class SkyDriveLogErrorEvent(time_events.TimestampEvent): + """Convenience class for a SkyDrive error log line event.""" + DATA_TYPE = 'skydrive:error:line' + + def __init__(self, timestamp, module, source_code, text, detail): + """Initializes the event object. + + Args: + timestamp: Milliseconds since epoch in UTC. + module: The module name that generated the log line. + source_code: Logging source file and line number. + text: The error text message. + detail: The error details. + """ + super(SkyDriveLogErrorEvent, self).__init__( + timestamp, eventdata.EventTimestamp.ADDED_TIME) + self.module = module + self.source_code = source_code + self.text = text + self.detail = detail + + +class SkyDriveLogErrorParser(text_parser.PyparsingMultiLineTextParser): + """Parse SkyDrive error log files.""" + + NAME = 'skydrive_log_error' + DESCRIPTION = u'Parser for OneDrive (or SkyDrive) error log files.' + + ENCODING = 'utf-8' + + # Common SDE (SkyDriveError) structures. + INTEGER_CAST = text_parser.PyParseIntCast + HYPHEN = text_parser.PyparsingConstants.HYPHEN + TWO_DIGITS = text_parser.PyparsingConstants.TWO_DIGITS + TIME_MSEC = text_parser.PyparsingConstants.TIME_MSEC + MSEC = pyparsing.Word(pyparsing.nums, max=3).setParseAction(INTEGER_CAST) + COMMA = pyparsing.Literal(u',').suppress() + DOT = pyparsing.Literal(u'.').suppress() + IGNORE_FIELD = pyparsing.CharsNotIn(u',').suppress() + + # Header line timestamp (2013-07-25-160323.291). + SDE_HEADER_TIMESTAMP = pyparsing.Group( + text_parser.PyparsingConstants.DATE.setResultsName('date') + HYPHEN + + TWO_DIGITS.setResultsName('hh') + TWO_DIGITS.setResultsName('mm') + + TWO_DIGITS.setResultsName('ss') + DOT + + MSEC.setResultsName('ms')).setResultsName('hdr_timestamp') + + # Line timestamp (07-25-13,16:06:31.820). + SDE_TIMESTAMP = ( + TWO_DIGITS.setResultsName('month') + HYPHEN + + TWO_DIGITS.setResultsName('day') + HYPHEN + + TWO_DIGITS.setResultsName('year_short') + COMMA + + TIME_MSEC.setResultsName('time')).setResultsName('timestamp') + + # Header start. + SDE_HEADER_START = ( + pyparsing.Literal(u'######').suppress() + + pyparsing.Literal(u'Logging started.').setResultsName('log_start')) + + # Multiline entry end marker, matched from right to left. + SDE_ENTRY_END = pyparsing.StringEnd() | SDE_HEADER_START | SDE_TIMESTAMP + + # SkyDriveError line pyparsing structure. + SDE_LINE = ( + SDE_TIMESTAMP + COMMA + + IGNORE_FIELD + COMMA + IGNORE_FIELD + COMMA + IGNORE_FIELD + COMMA + + pyparsing.CharsNotIn(u',').setResultsName('module') + COMMA + + pyparsing.CharsNotIn(u',').setResultsName('source_code') + COMMA + + IGNORE_FIELD + COMMA + IGNORE_FIELD + COMMA + IGNORE_FIELD + COMMA + + pyparsing.Optional(pyparsing.CharsNotIn(u',').setResultsName('text')) + + COMMA + pyparsing.SkipTo(SDE_ENTRY_END).setResultsName('detail') + + pyparsing.lineEnd()) + + # SkyDriveError header pyparsing structure. + SDE_HEADER = ( + SDE_HEADER_START + + pyparsing.Literal(u'Version=').setResultsName('ver_str') + + pyparsing.Word(pyparsing.nums + u'.').setResultsName('ver_num') + + pyparsing.Literal(u'StartSystemTime:').suppress() + + SDE_HEADER_TIMESTAMP + + pyparsing.Literal(u'StartLocalTime:').setResultsName('lt_str') + + pyparsing.SkipTo(pyparsing.lineEnd()).setResultsName('details') + + pyparsing.lineEnd()) + + # Define the available log line structures. + LINE_STRUCTURES = [ + ('logline', SDE_LINE), + ('header', SDE_HEADER) + ] + + def __init__(self): + """Initializes a parser object.""" + super(SkyDriveLogErrorParser, self).__init__() + self.use_local_zone = False + + def VerifyStructure(self, parser_context, line): + """Verify that this file is a SkyDrive Error log file. + + Args: + parser_context: A parser context object (instance of ParserContext). + line: A single line from the text file. + + Returns: + True if this is the correct parser, False otherwise. + """ + try: + parsed_structure = self.SDE_HEADER.parseString(line) + except pyparsing.ParseException: + logging.debug(u'Not a SkyDrive Error log file') + return False + timestamp = self._GetTimestampFromHeader(parsed_structure.hdr_timestamp) + if not timestamp: + logging.debug( + u'Not a SkyDrive Error log file, invalid timestamp {0:s}'.format( + parsed_structure.timestamp)) + return False + return True + + def ParseRecord(self, parser_context, key, structure): + """Parse each record structure and return an EventObject if applicable. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: An identification string indicating the name of the parsed + structure. + structure: A pyparsing.ParseResults object from a line in the + log file. + + Returns: + An event object (instance of EventObject) or None. + """ + if key == 'logline': + return self._ParseLine(structure) + elif key == 'header': + return self._ParseHeader(structure) + else: + logging.warning( + u'Unable to parse record, unknown structure: {0:s}'.format(key)) + + def _ParseLine(self, structure): + """Parse a logline and store appropriate attributes.""" + timestamp = self._GetTimestampFromLine(structure.timestamp) + if not timestamp: + logging.debug(u'SkyDriveLogError invalid timestamp {0:s}'.format( + structure.timestamp)) + return + # Replace newlines with spaces in structure.detail to preserve output. + return SkyDriveLogErrorEvent( + timestamp, structure.module, structure.source_code, + structure.text, structure.detail.replace(u'\n', u' ')) + + def _ParseHeader(self, structure): + """Parse header lines and store appropriate attributes. + + [u'Logging started.', u'Version=', u'17.0.2011.0627', + [2013, 7, 25], 16, 3, 23, 291, u'StartLocalTime', u'
'] + + Args: + structure: The parsed structure. + + Returns: + timestamp: The event or none. + """ + timestamp = self._GetTimestampFromHeader(structure.hdr_timestamp) + if not timestamp: + logging.debug( + u'SkyDriveLogError invalid timestamp {0:d}'.format( + structure.hdr_timestamp)) + return + text = u'{0:s} {1:s} {2:s}'.format( + structure.log_start, structure.ver_str, structure.ver_num) + detail = u'{0:s} {1:s}'.format(structure.lt_str, structure.details) + return SkyDriveLogErrorEvent( + timestamp, None, None, text, detail) + + def _GetTimestampFromHeader(self, structure): + """Gets a timestamp from the structure. + + The following is an example of the timestamp structure expected + [[2013, 7, 25], 16, 3, 23, 291] + + Args: + structure: The parsed structure, which should be a timestamp. + + Returns: + timestamp: A plaso timelib timestamp event or 0. + """ + year, month, day = structure.date + hour = structure.get('hh', 0) + minute = structure.get('mm', 0) + second = structure.get('ss', 0) + microsecond = structure.get('ms', 0) * 1000 + + return timelib.Timestamp.FromTimeParts( + year, month, day, hour, minute, second, microseconds=microsecond) + + def _GetTimestampFromLine(self, structure): + """Gets a timestamp from string from the structure + + The following is an example of the timestamp structure expected + [7, 25, 13, [16, 3, 24], 649] + + Args: + structure: The parsed structure. + + Returns: + timestamp: A plaso timelib timestamp event or 0. + """ + hour, minute, second = structure.time[0] + microsecond = structure.time[1] * 1000 + # TODO: Verify if timestamps are locale dependent. + year = structure.get('year_short', 0) + month = structure.get('month', 0) + day = structure.get('day', 0) + if year < 0 or not month or not day: + return 0 + + year += 2000 + + return timelib.Timestamp.FromTimeParts( + year, month, day, hour, minute, second, microseconds=microsecond) + + +manager.ParsersManager.RegisterParser(SkyDriveLogErrorParser) diff --git a/plaso/parsers/skydrivelogerr_test.py b/plaso/parsers/skydrivelogerr_test.py new file mode 100644 index 0000000..7cb14d6 --- /dev/null +++ b/plaso/parsers/skydrivelogerr_test.py @@ -0,0 +1,103 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the SkyDriveLogErr log parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import skydrivelogerr as skydrivelogerr_formatter +from plaso.lib import timelib_test +from plaso.parsers import skydrivelogerr as skydrivelogerr_parser +from plaso.parsers import test_lib + + +__author__ = 'Francesco Picasso (francesco.picasso@gmail.com)' + + +class SkyDriveLogErrorUnitTest(test_lib.ParserTestCase): + """A unit test for the SkyDriveLogErr parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = skydrivelogerr_parser.SkyDriveLogErrorParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath([u'skydriveerr.log']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 19) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-07-25 16:03:23.291') + self.assertEquals(event_objects[0].timestamp, expected_timestamp) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-07-25 16:03:24.649') + self.assertEquals(event_objects[1].timestamp, expected_timestamp) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-08-01 21:27:44.124') + self.assertEquals(event_objects[18].timestamp, expected_timestamp) + + expected_detail = ( + u'StartLocalTime: 2013-07-25-180323.291 PID=0x8f4 TID=0x718 ' + u'ContinuedFrom=') + self.assertEquals(event_objects[0].detail, expected_detail) + + expected_string = ( + u'Logging started. Version= 17.0.2011.0627 ({0:s})').format( + expected_detail) + + expected_string_short = u'Logging started. Version= 17.0.2011.0627' + self._TestGetMessageStrings( + event_objects[0], expected_string, expected_string_short) + + expected_string = ( + u'[AUTH authapi.cpp(280)] Sign in failed : ' + 'DRX_E_AUTH_NO_VALID_CREDENTIALS') + expected_string_short = u'Sign in failed : DRX_E_AUTH_NO_VALID_CREDENTIALS' + self._TestGetMessageStrings( + event_objects[1], expected_string, expected_string_short) + + expected_string = ( + u'[WNS absconn.cpp(177)] Received data from server ' + '(dwID=0x0;dwSize=0x3e;pbData=PNG 9 CON 48 ' + '44)') + expected_string_short = u'Received data from server' + self._TestGetMessageStrings( + event_objects[18], expected_string, expected_string_short) + + def testParseUnicode(self): + """Tests the Parse function on Unicode data.""" + test_file = self._GetTestFilePath([u'skydriveerr-unicode.log']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 19) + + # TODO: check if this test passes because the encoding on my system + # is UTF-8. + expected_text = ( + u'No node found named Passport-Jméno-člena') + self.assertEquals(event_objects[3].text, expected_text) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/sqlite.py b/plaso/parsers/sqlite.py new file mode 100644 index 0000000..40d3e53 --- /dev/null +++ b/plaso/parsers/sqlite.py @@ -0,0 +1,279 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a SQLite parser.""" + +import logging +import os +import tempfile + +import sqlite3 + +from plaso.lib import errors +from plaso.parsers import interface +from plaso.parsers import manager +from plaso.parsers import plugins + + +class SQLiteCache(plugins.BasePluginCache): + """A cache storing query results for SQLite plugins.""" + + def CacheQueryResults( + self, sql_results, attribute_name, key_name, values): + """Build a dict object based on a SQL command. + + This function will take a SQL command, execute it and for + each resulting row it will store a key in a dictionary. + + An example: + sql_results = A SQL result object after executing the + SQL command: 'SELECT foo, bla, bar FROM my_table' + attribute_name = 'all_the_things' + key_name = 'foo' + values = ['bla', 'bar'] + + Results from running this against the database: + 'first', 'stuff', 'things' + 'second', 'another stuff', 'another thing' + + This will result in a dict object being created in the + cache, called 'all_the_things' and it will contain the following value: + + all_the_things = { + 'first': ['stuff', 'things'], + 'second': ['another_stuff', 'another_thing']} + + Args: + sql_results: The SQL result object (sqlite.Cursor) after executing + a SQL command on the database. + attribute_name: The attribute name in the cache to store + results to. This will be the name of the + dict attribute. + key_name: The name of the result field that should be used + as a key in the resulting dict that is created. + values: A list of result fields that are stored as values + to the dict. If this list has only one value in it + the value will be stored directly, otherwise the value + will be a list containing the extracted results based + on the names provided in this list. + """ + setattr(self, attribute_name, {}) + attribute = getattr(self, attribute_name) + + row = sql_results.fetchone() + while row: + if len(values) == 1: + attribute[row[key_name]] = row[values[0]] + else: + attribute[row[key_name]] = [] + for value in values: + attribute[row[key_name]].append(row[value]) + + row = sql_results.fetchone() + + +class SQLiteDatabase(object): + """A simple wrapper for opening up a SQLite database.""" + + # Magic value for a SQLite database. + MAGIC = 'SQLite format 3' + + _READ_BUFFER_SIZE = 65536 + + def __init__(self, file_entry): + """Initializes the database object. + + Args: + file_entry: the file entry object. + """ + self._cursor = None + self._database = None + self._file_entry = file_entry + self._open = False + self._tables = [] + self._temp_file_name = '' + + def __exit__(self, unused_type, unused_value, unused_traceback): + """Make usable with "with" statement.""" + self.Close() + + def __enter__(self): + """Make usable with "with" statement.""" + return self + + @property + def cursor(self): + """Returns a cursor object from the database.""" + if not self._open: + self.Open() + + return self._database.cursor() + + @property + def tables(self): + """Returns a list of all the tables in the database.""" + if not self._open: + self.Open() + + return self._tables + + def Close(self): + """Close the database connection and clean up the temporary file.""" + if not self._open: + return + + self._database.close() + + try: + os.remove(self._temp_file_name) + except (OSError, IOError) as exception: + logging.warning(( + u'Unable to remove temporary copy: {0:s} of SQLite database: {1:s} ' + u'with error: {2:s}').format( + self._temp_file_name, self._file_entry.name, exception)) + + self._tables = [] + self._database = None + self._temp_file_name = '' + self._open = False + + def Open(self): + """Opens up a database connection and build a list of table names.""" + file_object = self._file_entry.GetFileObject() + + # TODO: Remove this when the classifier gets implemented + # and used. As of now, there is no check made against the file + # to verify it's signature, thus all files are sent here, meaning + # that this method assumes everything is a SQLite file and starts + # copying the content of the file into memory, which is not good + # for very large files. + file_object.seek(0, os.SEEK_SET) + + data = file_object.read(len(self.MAGIC)) + + if data != self.MAGIC: + file_object.close() + raise IOError( + u'File {0:s} not a SQLite database. (invalid signature)'.format( + self._file_entry.name)) + + # TODO: Current design copies the entire file into a buffer + # that is parsed by each SQLite parser. This is not very efficient, + # especially when many SQLite parsers are ran against a relatively + # large SQLite database. This temporary file that is created should + # be usable by all SQLite parsers so the file should only be read + # once in memory and then deleted when all SQLite parsers have completed. + + # TODO: Change this into a proper implementation using APSW + # and virtual filesystems when that will be available. + # Info: http://apidoc.apsw.googlecode.com/hg/vfs.html#vfs and + # http://apidoc.apsw.googlecode.com/hg/example.html#example-vfs + # Until then, just copy the file into a tempfile and parse it. + + # Note that data is filled here with the file header data and + # that with will explicitly close the temporary files and thus + # making sure it is available for sqlite3.connect(). + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + self._temp_file_name = temp_file.name + while data: + temp_file.write(data) + data = file_object.read(self._READ_BUFFER_SIZE) + + self._database = sqlite3.connect(self._temp_file_name) + try: + self._database.row_factory = sqlite3.Row + self._cursor = self._database.cursor() + except sqlite3.DatabaseError as exception: + logging.debug( + u'Unable to parse SQLite database: {0:s} with error: {1:s}'.format( + self._file_entry.name, exception)) + raise + + # Verify the table by reading in all table names and compare it to + # the list of required tables. + try: + sql_results = self._cursor.execute( + 'SELECT name FROM sqlite_master WHERE type="table"') + except sqlite3.DatabaseError as exception: + logging.debug( + u'Unable to parse SQLite database: {0:s} with error: {1:s}'.format( + self._file_entry.name, exception)) + raise + + self._tables = [] + for row in sql_results: + self._tables.append(row[0]) + + self._open = True + + +class SQLiteParser(interface.BasePluginsParser): + """A SQLite parser for Plaso.""" + + # Name of the parser, which enables all plugins by default. + NAME = 'sqlite' + DESCRIPTION = u'Parser for SQLite database files.' + + _plugin_classes = {} + + def __init__(self): + """Initializes a parser object.""" + super(SQLiteParser, self).__init__() + self._local_zone = False + self._plugins = SQLiteParser.GetPluginObjects() + self.db = None + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Parses an SQLite database. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Returns: + A event object generator (EventObjects) extracted from the database. + """ + with SQLiteDatabase(file_entry) as database: + try: + database.Open() + except IOError as exception: + raise errors.UnableToParseFile( + u'Unable to open database with error: {0:s}'.format( + repr(exception))) + except sqlite3.DatabaseError as exception: + raise errors.UnableToParseFile( + u'Unable to parse SQLite database with error: {0:s}.'.format( + repr(exception))) + + parser_chain = self._BuildParserChain(parser_chain) + # Create a cache in which the resulting tables are cached. + cache = SQLiteCache() + for plugin_object in self._plugins: + try: + plugin_object.Process( + parser_context, file_entry=file_entry, parser_chain=parser_chain, + cache=cache, database=database) + + except errors.WrongPlugin: + logging.debug( + u'Plugin: {0:s} cannot parse database: {1:s}'.format( + plugin_object.NAME, file_entry.name)) + + +manager.ParsersManager.RegisterParser(SQLiteParser) diff --git a/plaso/parsers/sqlite_plugins/__init__.py b/plaso/parsers/sqlite_plugins/__init__.py new file mode 100644 index 0000000..ad7d9ae --- /dev/null +++ b/plaso/parsers/sqlite_plugins/__init__.py @@ -0,0 +1,32 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains an import statement for each SQLite plugin.""" + +from plaso.parsers.sqlite_plugins import android_calls +from plaso.parsers.sqlite_plugins import android_sms +from plaso.parsers.sqlite_plugins import appusage +from plaso.parsers.sqlite_plugins import chrome +from plaso.parsers.sqlite_plugins import chrome_cookies +from plaso.parsers.sqlite_plugins import chrome_extension_activity +from plaso.parsers.sqlite_plugins import firefox +from plaso.parsers.sqlite_plugins import firefox_cookies +from plaso.parsers.sqlite_plugins import gdrive +from plaso.parsers.sqlite_plugins import ls_quarantine +from plaso.parsers.sqlite_plugins import mac_document_versions +from plaso.parsers.sqlite_plugins import mackeeper_cache +from plaso.parsers.sqlite_plugins import skype +from plaso.parsers.sqlite_plugins import zeitgeist diff --git a/plaso/parsers/sqlite_plugins/android_calls.py b/plaso/parsers/sqlite_plugins/android_calls.py new file mode 100644 index 0000000..d8294b1 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/android_calls.py @@ -0,0 +1,111 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a parser for the Android contacts2 Call History. + +Android Call History is stored in SQLite database files named contacts2.db. +""" + +from plaso.events import time_events +from plaso.parsers import sqlite +from plaso.parsers.sqlite_plugins import interface + + +class AndroidCallEvent(time_events.JavaTimeEvent): + """Convenience class for an Android Call History event.""" + + DATA_TYPE = 'android:event:call' + + def __init__( + self, java_time, usage, identifier, number, name, duration, call_type): + """Initializes the event object. + + Args: + java_time: The Java time value. + usage: The description of the usage of the time value. + identifier: The row identifier. + number: The phone number associated to the remote party. + duration: The number of seconds the call lasted. + call_type: Incoming, Outgoing, or Missed. + """ + super(AndroidCallEvent, self).__init__(java_time, usage) + self.offset = identifier + self.number = number + self.name = name + self.duration = duration + self.call_type = call_type + + +class AndroidCallPlugin(interface.SQLitePlugin): + """Parse Android contacts2 database.""" + + NAME = 'android_calls' + DESCRIPTION = u'Parser for Android calls SQLite database files.' + + # Define the needed queries. + QUERIES = [ + ('SELECT _id AS id, date, number, name, duration, type FROM calls', + 'ParseCallsRow')] + + CALL_TYPE = { + 1: u'INCOMING', + 2: u'OUTGOING', + 3: u'MISSED'} + + def ParseCallsRow( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + **unused_kwargs): + """Parses a Call record row. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + """ + # Extract and lookup the call type. + call_type = self.CALL_TYPE.get(row['type'], u'UNKNOWN') + + event_object = AndroidCallEvent( + row['date'], u'Call Started', row['id'], row['number'], row['name'], + row['duration'], call_type) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry, + query=query) + + duration = row['duration'] + if isinstance(duration, basestring): + try: + duration = int(duration, 10) + except ValueError: + duration = 0 + + if duration: + # The duration is in seconds and the date value in milli seconds. + duration *= 1000 + event_object = AndroidCallEvent( + row['date'] + duration, u'Call Ended', row['id'], row['number'], + row['name'], row['duration'], call_type) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + +sqlite.SQLiteParser.RegisterPlugin(AndroidCallPlugin) diff --git a/plaso/parsers/sqlite_plugins/android_calls_test.py b/plaso/parsers/sqlite_plugins/android_calls_test.py new file mode 100644 index 0000000..dac6baf --- /dev/null +++ b/plaso/parsers/sqlite_plugins/android_calls_test.py @@ -0,0 +1,91 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Android SMS call history plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import android_calls as android_calls_formatter +from plaso.lib import timelib_test +from plaso.parsers.sqlite_plugins import android_calls +from plaso.parsers.sqlite_plugins import test_lib + + +class AndroidCallSQLitePluginTest(test_lib.SQLitePluginTestCase): + """Tests for the Android Call History database plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = android_calls.AndroidCallPlugin() + + def testProcess(self): + """Test the Process function on an Android contacts2.db file.""" + test_file = self._GetTestFilePath(['contacts2.db']) + event_queue_consumer = self._ParseDatabaseFileWithPlugin( + self._plugin, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # The contacts2 database file contains 5 events (MISSED/OUTGOING/INCOMING). + self.assertEquals(len(event_objects), 5) + + # Check the first event. + event_object = event_objects[0] + + self.assertEquals(event_object.timestamp_desc, u'Call Started') + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-06 21:17:16.690') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_number = u'5404561685' + self.assertEquals(event_object.number, expected_number) + + expected_type = u'MISSED' + self.assertEquals(event_object.call_type, expected_type) + + expected_call = ( + u'MISSED ' + u'Number: 5404561685 ' + u'Name: Barney ' + u'Duration: 0 seconds') + expected_short = u'MISSED Call' + self._TestGetMessageStrings(event_object, expected_call, expected_short) + + # Run some tests on the last 2 events. + event_object_3 = event_objects[3] + event_object_4 = event_objects[4] + + # Check the timestamp_desc of the last event. + self.assertEquals(event_object_4.timestamp_desc, u'Call Ended') + + expected_timestamp3 = timelib_test.CopyStringToTimestamp( + '2013-11-07 00:03:36.690') + self.assertEquals(event_object_3.timestamp, expected_timestamp3) + + expected_timestamp4 = timelib_test.CopyStringToTimestamp( + '2013-11-07 00:14:15.690') + self.assertEquals(event_object_4.timestamp, expected_timestamp4) + + # Ensure the difference in btw. events 3 and 4 equals the duration. + expected_duration = ( + (expected_timestamp4 - expected_timestamp3) / 1000000) + self.assertEquals(event_object_4.duration, expected_duration) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/sqlite_plugins/android_sms.py b/plaso/parsers/sqlite_plugins/android_sms.py new file mode 100644 index 0000000..b87824f --- /dev/null +++ b/plaso/parsers/sqlite_plugins/android_sms.py @@ -0,0 +1,100 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a parser for the Android SMS database. + +Android SMS messages are stored in SQLite database files named mmssms.dbs. +""" + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.parsers import sqlite +from plaso.parsers.sqlite_plugins import interface + + +class AndroidSmsEvent(time_events.JavaTimeEvent): + """Convenience class for an Android SMS event.""" + + DATA_TYPE = 'android:messaging:sms' + + def __init__(self, java_time, identifier, address, sms_read, sms_type, body): + """Initializes the event object. + + Args: + java_time: The Java time value. + identifier: The row identifier. + address: The phone number associated to the sender/receiver. + status: Read or Unread. + type: Sent or Received. + body: Content of the SMS text message. + """ + super(AndroidSmsEvent, self).__init__( + java_time, eventdata.EventTimestamp.CREATION_TIME) + self.offset = identifier + self.address = address + self.sms_read = sms_read + self.sms_type = sms_type + self.body = body + + +class AndroidSmsPlugin(interface.SQLitePlugin): + """Parse Android SMS database.""" + + NAME = 'android_sms' + DESCRIPTION = u'Parser for Android text messages SQLite database files.' + + # Define the needed queries. + QUERIES = [ + ('SELECT _id AS id, address, date, read, type, body FROM sms', + 'ParseSmsRow')] + + # The required tables. + REQUIRED_TABLES = frozenset(['sms']) + + SMS_TYPE = { + 1: u'RECEIVED', + 2: u'SENT'} + SMS_READ = { + 0: u'UNREAD', + 1: u'READ'} + + def ParseSmsRow( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + **unused_kwargs): + """Parses an SMS row. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + """ + # Extract and lookup the SMS type and read status. + sms_type = self.SMS_TYPE.get(row['type'], u'UNKNOWN') + sms_read = self.SMS_READ.get(row['read'], u'UNKNOWN') + + event_object = AndroidSmsEvent( + row['date'], row['id'], row['address'], sms_read, sms_type, row['body']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + +sqlite.SQLiteParser.RegisterPlugin(AndroidSmsPlugin) diff --git a/plaso/parsers/sqlite_plugins/android_sms_test.py b/plaso/parsers/sqlite_plugins/android_sms_test.py new file mode 100644 index 0000000..44f727d --- /dev/null +++ b/plaso/parsers/sqlite_plugins/android_sms_test.py @@ -0,0 +1,73 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Android SMS plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import android_sms as android_sms_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers.sqlite_plugins import android_sms +from plaso.parsers.sqlite_plugins import test_lib + + +class AndroidSmsTest(test_lib.SQLitePluginTestCase): + """Tests for the Android SMS database plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = android_sms.AndroidSmsPlugin() + + def testProcess(self): + """Test the Process function on an Android SMS mmssms.db file.""" + test_file = self._GetTestFilePath(['mmssms.db']) + event_queue_consumer = self._ParseDatabaseFileWithPlugin( + self._plugin, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # The SMS database file contains 9 events (5 SENT, 4 RECEIVED messages). + self.assertEquals(len(event_objects), 9) + + # Check the first SMS sent. + event_object = event_objects[0] + + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.CREATION_TIME) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-10-29 16:56:28.038') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_address = u'1 555-521-5554' + self.assertEquals(event_object.address, expected_address) + + expected_body = u'Yo Fred this is my new number.' + self.assertEquals(event_object.body, expected_body) + + expected_msg = ( + u'Type: SENT ' + u'Address: 1 555-521-5554 ' + u'Status: READ ' + u'Message: Yo Fred this is my new number.') + expected_short = u'Yo Fred this is my new number.' + self._TestGetMessageStrings(event_object, expected_msg, expected_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/sqlite_plugins/appusage.py b/plaso/parsers/sqlite_plugins/appusage.py new file mode 100644 index 0000000..fa53c30 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/appusage.py @@ -0,0 +1,108 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a parser for the Mac OS X application usage. + + The application usage is stored in SQLite database files named + /var/db/application_usage.sqlite +""" + +from plaso.events import time_events +from plaso.parsers import sqlite +from plaso.parsers.sqlite_plugins import interface + + +class MacOSXApplicationUsageEvent(time_events.PosixTimeEvent): + """Convenience class for a Mac OS X application usage event.""" + + DATA_TYPE = 'macosx:application_usage' + + def __init__( + self, posix_time, usage, application_name, application_version, + bundle_id, number_of_times): + """Initializes the event object. + + Args: + posix_time: The POSIX time value. + usage: The description of the usage of the time value. + application_name: The name of the application. + application_version: The version of the application. + bundle_id: The bundle identifier of the application. + number_of_times: TODO: number of times what? + """ + super(MacOSXApplicationUsageEvent, self).__init__(posix_time, usage) + + self.application = application_name + self.app_version = application_version + self.bundle_id = bundle_id + self.count = number_of_times + + +class ApplicationUsagePlugin(interface.SQLitePlugin): + """Parse Application Usage history files. + + Application usage is a SQLite database that logs down entries + triggered by NSWorkspaceWillLaunchApplicationNotification and + NSWorkspaceDidTerminateApplicationNotification NSWorkspace notifications by + crankd. + + See the code here: + http://code.google.com/p/google-macops/source/browse/trunk/crankd/\ + ApplicationUsage.py + + Default installation: /var/db/application_usage.sqlite + """ + + NAME = 'appusage' + DESCRIPTION = u'Parser for Mac OS X application usage SQLite database files.' + + # Define the needed queries. + QUERIES = [( + ('SELECT last_time, event, bundle_id, app_version, app_path, ' + 'number_times FROM application_usage ORDER BY last_time'), + 'ParseApplicationUsageRow')] + + # The required tables. + REQUIRED_TABLES = frozenset(['application_usage']) + + def ParseApplicationUsageRow( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + **unused_kwargs): + """Parses an application usage row. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + """ + # TODO: replace usage by definition(s) in eventdata. Not sure which values + # it will hold here. + usage = u'Application {0:s}'.format(row['event']) + + event_object = MacOSXApplicationUsageEvent( + row['last_time'], usage, row['app_path'], row['app_version'], + row['bundle_id'], row['number_times']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + +sqlite.SQLiteParser.RegisterPlugin(ApplicationUsagePlugin) diff --git a/plaso/parsers/sqlite_plugins/appusage_test.py b/plaso/parsers/sqlite_plugins/appusage_test.py new file mode 100644 index 0000000..e714238 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/appusage_test.py @@ -0,0 +1,69 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Mac OS X application usage database plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import appusage as appusage_formatter +from plaso.lib import timelib_test +from plaso.parsers.sqlite_plugins import test_lib +from plaso.parsers.sqlite_plugins import appusage + + +class ApplicationUsagePluginTest(test_lib.SQLitePluginTestCase): + """Tests for the Mac OS X application usage activity database plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = appusage.ApplicationUsagePlugin() + + def testProcess(self): + """Tests the Process function.""" + test_file = self._GetTestFilePath(['application_usage.sqlite']) + event_queue_consumer = self._ParseDatabaseFileWithPlugin( + self._plugin, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # The sqlite database contains 5 events. + self.assertEquals(len(event_objects), 5) + + # Check the first event. + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2014-05-07 18:52:02') + self.assertEquals(event_object.timestamp, expected_timestamp) + + self.assertEquals(event_object.application, u'/Applications/Safari.app') + self.assertEquals(event_object.app_version, u'9537.75.14') + self.assertEquals(event_object.bundle_id, u'com.apple.Safari') + self.assertEquals(event_object.count, 1) + + expected_msg = ( + u'/Applications/Safari.app v.9537.75.14 ' + u'(bundle: com.apple.Safari). ' + u'Launched: 1 time(s)') + + expected_msg_short = u'/Applications/Safari.app (1 time(s))' + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/sqlite_plugins/chrome.py b/plaso/parsers/sqlite_plugins/chrome.py new file mode 100644 index 0000000..04abc82 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/chrome.py @@ -0,0 +1,337 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for the Google Chrome History files. + + The Chrome History is stored in SQLite database files named History + and Archived History. Where the Archived History does not contain + the downloads table. +""" + +from plaso.events import time_events +from plaso.lib import timelib +from plaso.lib import eventdata +from plaso.parsers import sqlite +from plaso.parsers.sqlite_plugins import interface + + +class ChromeHistoryFileDownloadedEvent(time_events.TimestampEvent): + """Convenience class for a Chrome History file downloaded event.""" + DATA_TYPE = 'chrome:history:file_downloaded' + + def __init__( + self, timestamp, row_id, url, full_path, received_bytes, total_bytes): + """Initializes the event object. + + Args: + timestamp: The timestamp value. + row_id: The identifier of the corresponding row. + url: The URL of the downloaded file. + full_path: The full path where the file was downloaded to. + received_bytes: The number of bytes received while downloading. + total_bytes: The total number of bytes to download. + """ + super(ChromeHistoryFileDownloadedEvent, self).__init__( + timestamp, eventdata.EventTimestamp.FILE_DOWNLOADED) + + self.offset = row_id + self.url = url + self.full_path = full_path + self.received_bytes = received_bytes + self.total_bytes = total_bytes + + +class ChromeHistoryPageVisitedEvent(time_events.WebKitTimeEvent): + """Convenience class for a Chrome History page visited event.""" + DATA_TYPE = 'chrome:history:page_visited' + + # TODO: refactor extra to be conditional arguments. + def __init__( + self, webkit_time, row_id, url, title, hostname, typed_count, from_visit, + extra, visit_source): + """Initializes the event object. + + Args: + webkit_time: The WebKit time value. + row_id: The identifier of the corresponding row. + url: The URL of the visited page. + title: The title of the visited page. + hostname: The visited hostname. + typed_count: The number of charcters of the URL that were typed. + from_visit: The URL where the visit originated from. + extra: String containing extra event data. + visit_source: The source of the page visit, if defined. + """ + super(ChromeHistoryPageVisitedEvent, self).__init__( + webkit_time, eventdata.EventTimestamp.PAGE_VISITED) + + self.offset = row_id + self.url = url + self.title = title + self.host = hostname + self.typed_count = typed_count + self.from_visit = from_visit + self.extra = extra + if visit_source is not None: + self.visit_source = visit_source + + +class ChromeHistoryPlugin(interface.SQLitePlugin): + """Parse Chrome Archived History and History files.""" + + NAME = 'chrome_history' + DESCRIPTION = u'Parser for Chrome history SQLite database files.' + + # Define the needed queries. + QUERIES = [ + (('SELECT urls.id, urls.url, urls.title, urls.visit_count, ' + 'urls.typed_count, urls.last_visit_time, urls.hidden, visits.' + 'visit_time, visits.from_visit, visits.transition, visits.id ' + 'AS visit_id FROM urls, visits WHERE urls.id = visits.url ORDER ' + 'BY visits.visit_time'), 'ParseLastVisitedRow'), + (('SELECT downloads.id AS id, downloads.start_time,' + 'downloads.target_path, downloads_url_chains.url, ' + 'downloads.received_bytes, downloads.total_bytes FROM downloads,' + ' downloads_url_chains WHERE downloads.id = ' + 'downloads_url_chains.id'), 'ParseNewFileDownloadedRow'), + (('SELECT id, full_path, url, start_time, received_bytes, ' + 'total_bytes,state FROM downloads'), 'ParseFileDownloadedRow')] + + # The required tables common to Archived History and History. + REQUIRED_TABLES = frozenset([ + 'keyword_search_terms', 'meta', 'urls', 'visits', 'visit_source']) + + # Queries for cache building. + URL_CACHE_QUERY = ( + 'SELECT visits.id AS id, urls.url, urls.title FROM ' + 'visits, urls WHERE urls.id = visits.url') + SYNC_CACHE_QUERY = 'SELECT id, source FROM visit_source' + + # The following definition for values can be found here: + # http://src.chromium.org/svn/trunk/src/content/public/common/ \ + # page_transition_types_list.h + PAGE_TRANSITION = { + 0: u'LINK', + 1: u'TYPED', + 2: u'AUTO_BOOKMARK', + 3: u'AUTO_SUBFRAME', + 4: u'MANUAL_SUBFRAME', + 5: u'GENERATED', + 6: u'START_PAGE', + 7: u'FORM_SUBMIT', + 8: u'RELOAD', + 9: u'KEYWORD', + 10: u'KEYWORD_GENERATED ' + } + + TRANSITION_LONGER = { + 0: u'User clicked a link', + 1: u'User typed the URL in the URL bar', + 2: u'Got through a suggestion in the UI', + 3: (u'Content automatically loaded in a non-toplevel frame - user may ' + u'not realize'), + 4: u'Subframe explicitly requested by the user', + 5: (u'User typed in the URL bar and selected an entry from the list - ' + u'such as a search bar'), + 6: u'The start page of the browser', + 7: u'A form the user has submitted values to', + 8: (u'The user reloaded the page, eg by hitting the reload button or ' + u'restored a session'), + 9: (u'URL what was generated from a replaceable keyword other than the ' + u'default search provider'), + 10: u'Corresponds to a visit generated from a KEYWORD' + } + + # The following is the values for the source enum found in the visit_source + # table and describes where a record originated from (if it originates from a + # different storage than locally generated). + # The source can be found here: + # http://src.chromium.org/svn/trunk/src/chrome/browser/history/\ + # history_types.h + VISIT_SOURCE = { + 0: u'SOURCE_SYNCED', + 1: u'SOURCE_BROWSED', + 2: u'SOURCE_EXTENSION', + 3: u'SOURCE_FIREFOX_IMPORTED', + 4: u'SOURCE_IE_IMPORTED', + 5: u'SOURCE_SAFARI_IMPORTED' + } + + CORE_MASK = 0xff + + def _GetHostname(self, hostname): + """Return a hostname from a full URL.""" + if hostname.startswith('http') or hostname.startswith('ftp'): + _, _, uri = hostname.partition('//') + hostname, _, _ = uri.partition('/') + + return hostname + + if hostname.startswith('about') or hostname.startswith('chrome'): + site, _, _ = hostname.partition('/') + return site + + return hostname + + def _GetUrl(self, url, cache, database): + """Return an URL from a reference to an entry in the from_visit table.""" + if not url: + return u'' + + url_cache_results = cache.GetResults('url') + if not url_cache_results: + cursor = database.cursor + result_set = cursor.execute(self.URL_CACHE_QUERY) + cache.CacheQueryResults( + result_set, 'url', 'id', ('url', 'title')) + url_cache_results = cache.GetResults('url') + + reference_url, reference_title = url_cache_results.get(url, [u'', u'']) + + if not reference_url: + return u'' + + return u'{0:s} ({1:s})'.format(reference_url, reference_title) + + def _GetVisitSource(self, visit_id, cache, database): + """Return a string denoting the visit source type if possible. + + Args: + visit_id: The ID from the visits table for the particular record. + cache: A cache object (instance of SQLiteCache). + database: A database object (instance of SQLiteDatabase). + + Returns: + A string with the visit source, None if not found. + """ + if not visit_id: + return + + sync_cache_results = cache.GetResults('sync') + if not sync_cache_results: + cursor = database.cursor + result_set = cursor.execute(self.SYNC_CACHE_QUERY) + cache.CacheQueryResults( + result_set, 'sync', 'id', ('source',)) + sync_cache_results = cache.GetResults('sync') + + results = sync_cache_results.get(visit_id, None) + if results is None: + return + + return self.VISIT_SOURCE.get(results, None) + + def ParseFileDownloadedRow( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + **unused_kwargs): + """Parses a file downloaded row. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + """ + timestamp = timelib.Timestamp.FromPosixTime(row['start_time']) + event_object = ChromeHistoryFileDownloadedEvent( + timestamp, row['id'], row['url'], row['full_path'], + row['received_bytes'], row['total_bytes']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + def ParseNewFileDownloadedRow( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + **unused_kwargs): + """Parses a file downloaded row. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + """ + timestamp = timelib.Timestamp.FromWebKitTime(row['start_time']) + event_object = ChromeHistoryFileDownloadedEvent( + timestamp, row['id'], row['url'], row['target_path'], + row['received_bytes'], row['total_bytes']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + def ParseLastVisitedRow( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + cache=None, database=None, + **unused_kwargs): + """Parses a last visited row. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + cache: Optional cache object (instance of SQLiteCache). + The default is None. + database: Optional database object (instance of SQLiteDatabase). + The default is None. + """ + extras = [] + + transition_nr = row['transition'] & self.CORE_MASK + page_transition = self.PAGE_TRANSITION.get(transition_nr, '') + if page_transition: + extras.append(u'Type: [{0:s} - {1:s}]'.format( + page_transition, self.TRANSITION_LONGER.get(transition_nr, ''))) + + if row['hidden'] == '1': + extras.append(u'(url hidden)') + + # TODO: move to formatter. + count = row['typed_count'] + if count >= 1: + if count > 1: + multi = u's' + else: + multi = u'' + + extras.append(u'(type count {1:d} time{0:s})'.format(multi, count)) + else: + extras.append(u'(URL not typed directly - no typed count)') + + visit_source = self._GetVisitSource(row['visit_id'], cache, database) + + # TODO: replace extras by conditional formatting. + event_object = ChromeHistoryPageVisitedEvent( + row['visit_time'], row['id'], row['url'], row['title'], + self._GetHostname(row['url']), row['typed_count'], + self._GetUrl(row['from_visit'], cache, database), u' '.join(extras), + visit_source) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + +sqlite.SQLiteParser.RegisterPlugin(ChromeHistoryPlugin) diff --git a/plaso/parsers/sqlite_plugins/chrome_cookies.py b/plaso/parsers/sqlite_plugins/chrome_cookies.py new file mode 100644 index 0000000..481d7a9 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/chrome_cookies.py @@ -0,0 +1,166 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for the Google Chrome Cookie database.""" + +from plaso.events import time_events +from plaso.lib import errors +from plaso.lib import eventdata +# Register the cookie plugins. +from plaso.parsers import cookie_plugins # pylint: disable=unused-import +from plaso.parsers import sqlite +from plaso.parsers.cookie_plugins import interface as cookie_interface +from plaso.parsers.sqlite_plugins import interface + + +class ChromeCookieEvent(time_events.WebKitTimeEvent): + """Convenience class for a Chrome Cookie event.""" + + DATA_TYPE = 'chrome:cookie:entry' + + def __init__( + self, timestamp, usage, hostname, cookie_name, value, path, secure, + httponly, persistent): + """Initializes the event. + + Args: + timestamp: The timestamp value in WebKit format.. + usage: Timestamp description string. + hostname: The hostname of host that set the cookie value. + cookie_name: The name field of the cookie. + value: The value of the cookie. + path: An URI of the page that set the cookie. + secure: Indication if this cookie should only be transmitted over a secure + channel. + httponly: An indication that the cookie cannot be accessed through client + side script. + persistent: A flag indicating cookies persistent value. + """ + super(ChromeCookieEvent, self).__init__(timestamp, usage) + if hostname.startswith('.'): + hostname = hostname[1:] + + self.host = hostname + self.cookie_name = cookie_name + self.data = value + self.path = path + self.secure = True if secure else False + self.httponly = True if httponly else False + self.persistent = True if persistent else False + + if self.secure: + scheme = u'https' + else: + scheme = u'http' + + self.url = u'{0:s}://{1:s}{2:s}'.format(scheme, hostname, path) + + +class ChromeCookiePlugin(interface.SQLitePlugin): + """Parse Chrome Cookies file.""" + + NAME = 'chrome_cookies' + DESCRIPTION = u'Parser for Chrome cookies SQLite database files.' + + # Define the needed queries. + QUERIES = [ + (('SELECT creation_utc, host_key, name, value, path, expires_utc, ' + 'secure, httponly, last_access_utc, has_expires, persistent ' + 'FROM cookies'), 'ParseCookieRow')] + + # The required tables common to Archived History and History. + REQUIRED_TABLES = frozenset(['cookies', 'meta']) + + # Point to few sources for URL information. + URLS = [ + u'http://src.chromium.org/svn/trunk/src/net/cookies/', + (u'http://www.dfinews.com/articles/2012/02/' + u'google-analytics-cookies-and-forensic-implications')] + + # Google Analytics __utmz variable translation. + # Taken from: + # http://www.dfinews.com/sites/dfinews.com/files/u739/Tab2Cookies020312.jpg + GA_UTMZ_TRANSLATION = { + 'utmcsr': 'Last source used to access.', + 'utmccn': 'Ad campaign information.', + 'utmcmd': 'Last type of visit.', + 'utmctr': 'Keywords used to find site.', + 'utmcct': 'Path to the page of referring link.'} + + def __init__(self): + """Initializes a plugin object.""" + super(ChromeCookiePlugin, self).__init__() + self._cookie_plugins = cookie_interface.GetPlugins() + + def ParseCookieRow( + self, parser_context, row, file_entry=None, parser_chain=None, + query=None, **unused_kwargs): + """Parses a cookie row. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + """ + event_object = ChromeCookieEvent( + row['creation_utc'], eventdata.EventTimestamp.CREATION_TIME, + row['host_key'], row['name'], row['value'], row['path'], row['secure'], + row['httponly'], row['persistent']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + event_object = ChromeCookieEvent( + row['last_access_utc'], eventdata.EventTimestamp.ACCESS_TIME, + row['host_key'], row['name'], row['value'], row['path'], row['secure'], + row['httponly'], row['persistent']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + if row['has_expires']: + event_object = ChromeCookieEvent( + row['expires_utc'], 'Cookie Expires', + row['host_key'], row['name'], row['value'], row['path'], + row['secure'], row['httponly'], row['persistent']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + # Go through all cookie plugins to see if there are is any specific parsing + # needed. + hostname = row['host_key'] + if hostname.startswith('.'): + hostname = hostname[1:] + + url = u'http{0:s}://{1:s}{2:s}'.format( + u's' if row['secure'] else u'', hostname, row['path']) + + for cookie_plugin in self._cookie_plugins: + try: + cookie_plugin.Process( + parser_context, cookie_name=row['name'], cookie_data=row['value'], + url=url, parser_chain=parser_chain, file_entry=file_entry) + except errors.WrongPlugin: + pass + + +sqlite.SQLiteParser.RegisterPlugin(ChromeCookiePlugin) diff --git a/plaso/parsers/sqlite_plugins/chrome_cookies_test.py b/plaso/parsers/sqlite_plugins/chrome_cookies_test.py new file mode 100644 index 0000000..8fd0b38 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/chrome_cookies_test.py @@ -0,0 +1,135 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Google Chrome cookie database plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import chrome_cookies as chrome_cookies_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers.sqlite_plugins import chrome_cookies +from plaso.parsers.sqlite_plugins import test_lib + + +class ChromeCookiesPluginTest(test_lib.SQLitePluginTestCase): + """Tests for the Google Chrome cookie database plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = chrome_cookies.ChromeCookiePlugin() + + def testProcess(self): + """Tests the Process function on a Chrome cookie database file.""" + test_file = self._GetTestFilePath(['cookies.db']) + event_queue_consumer = self._ParseDatabaseFileWithPlugin( + self._plugin, test_file) + + event_objects = [] + extra_objects = [] + + # Since we've got both events generated by cookie plugins and the Chrome + # cookie plugin we need to separate them. + for event_object in self._GetEventObjectsFromQueue(event_queue_consumer): + if isinstance(event_object, chrome_cookies.ChromeCookieEvent): + event_objects.append(event_object) + else: + extra_objects.append(event_object) + + # The cookie database contains 560 entries: + # 560 creation timestamps. + # 560 last access timestamps. + # 560 expired timestamps. + # Then there are extra events created by plugins: + # 75 events created by Google Analytics cookies. + # In total: 1755 events. + self.assertEquals(len(event_objects), 3 * 560) + + # Double check that we've got at least the 75 Google Analytics sessions. + self.assertGreaterEqual(len(extra_objects), 75) + + # Check few "random" events to verify. + + # Check one linkedin cookie. + event_object = event_objects[124] + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.ACCESS_TIME) + self.assertEquals(event_object.host, u'www.linkedin.com') + self.assertEquals(event_object.cookie_name, u'leo_auth_token') + self.assertFalse(event_object.httponly) + self.assertEquals(event_object.url, u'http://www.linkedin.com/') + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-08-25 21:50:27.292367') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'http://www.linkedin.com/ (leo_auth_token) Flags: [HTTP only] = False ' + u'[Persistent] = True') + expected_short = u'www.linkedin.com (leo_auth_token)' + self._TestGetMessageStrings(event_object, expected_msg, expected_short) + + # Check one of the visits to rubiconproject.com. + event_object = event_objects[379] + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.ACCESS_TIME) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-04-01 13:54:34.949210') + self.assertEquals(event_object.timestamp, expected_timestamp) + + self.assertEquals(event_object.url, u'http://rubiconproject.com/') + self.assertEquals(event_object.path, u'/') + self.assertFalse(event_object.secure) + self.assertTrue(event_object.persistent) + + expected_msg = ( + u'http://rubiconproject.com/ (put_2249) Flags: [HTTP only] = False ' + u'[Persistent] = True') + self._TestGetMessageStrings( + event_object, expected_msg, u'rubiconproject.com (put_2249)') + + # Examine an event for a visit to a political blog site. + event_object = event_objects[444] + self.assertEquals( + event_object.path, + u'/2012/03/21/romney-tries-to-clean-up-etch-a-sketch-mess/') + self.assertEquals(event_object.host, u'politicalticker.blogs.cnn.com') + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-03-22 01:47:21.012022') + self.assertEquals(event_object.timestamp, expected_timestamp) + + # Examine a cookie that has an autologin entry. + event_object = event_objects[1425] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-04-01 13:52:56.189444') + self.assertEquals(event_object.timestamp, expected_timestamp) + + self.assertEquals(event_object.host, u'marvel.com') + self.assertEquals(event_object.cookie_name, u'autologin[timeout]') + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.CREATION_TIME) + # This particular cookie value represents a timeout value that corresponds + # to the expiration date of the cookie. + self.assertEquals(event_object.data, u'1364824322') + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/sqlite_plugins/chrome_extension_activity.py b/plaso/parsers/sqlite_plugins/chrome_extension_activity.py new file mode 100644 index 0000000..53e1feb --- /dev/null +++ b/plaso/parsers/sqlite_plugins/chrome_extension_activity.py @@ -0,0 +1,91 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for the Google Chrome extension activity database files. + + The Chrome extension activity is stored in SQLite database files named + Extension Activity. +""" + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.parsers import sqlite +from plaso.parsers.sqlite_plugins import interface + + +class ChromeExtensionActivityEvent(time_events.WebKitTimeEvent): + """Convenience class for a Chrome Extension Activity event.""" + DATA_TYPE = 'chrome:extension_activity:activity_log' + + def __init__(self, row): + """Initializes the event object. + + Args: + row: The row resulting from the query (instance of sqlite3.Row). + """ + # TODO: change the timestamp usage from unknown to something else. + super(ChromeExtensionActivityEvent, self).__init__( + row['time'], eventdata.EventTimestamp.UNKNOWN) + + self.extension_id = row['extension_id'] + self.action_type = row['action_type'] + self.api_name = row['api_name'] + self.args = row['args'] + self.page_url = row['page_url'] + self.page_title = row['page_title'] + self.arg_url = row['arg_url'] + self.other = row['other'] + self.activity_id = row['activity_id'] + + +class ChromeExtensionActivityPlugin(interface.SQLitePlugin): + """Plugin to parse Chrome extension activity database files.""" + + NAME = 'chrome_extension_activity' + DESCRIPTION = u'Parser for Chrome exention activitiy SQLite database files.' + + # Define the needed queries. + QUERIES = [ + (('SELECT time, extension_id, action_type, api_name, args, page_url, ' + 'page_title, arg_url, other, activity_id ' + 'FROM activitylog_uncompressed ORDER BY time'), + 'ParseActivityLogUncompressedRow')] + + REQUIRED_TABLES = frozenset([ + 'activitylog_compressed', 'string_ids', 'url_ids']) + + def ParseActivityLogUncompressedRow( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + **unused_kwargs): + """Parses a file downloaded row. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query (instance of sqlite3.Row). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + """ + event_object = ChromeExtensionActivityEvent(row) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + +sqlite.SQLiteParser.RegisterPlugin(ChromeExtensionActivityPlugin) diff --git a/plaso/parsers/sqlite_plugins/chrome_extension_activity_test.py b/plaso/parsers/sqlite_plugins/chrome_extension_activity_test.py new file mode 100644 index 0000000..8c807e6 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/chrome_extension_activity_test.py @@ -0,0 +1,76 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Google Chrome extension activity database plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import chrome_extension_activity as chrome_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import sqlite +from plaso.parsers.sqlite_plugins import chrome_extension_activity +from plaso.parsers.sqlite_plugins import test_lib + + +class ChromeExtensionActivityPluginTest(test_lib.SQLitePluginTestCase): + """Tests for the Google Chrome extension activity database plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = chrome_extension_activity.ChromeExtensionActivityPlugin() + + def testProcess(self): + """Tests the Process function on a Chrome extension activity database.""" + test_file = self._GetTestFilePath(['Extension Activity']) + cache = sqlite.SQLiteCache() + event_queue_consumer = self._ParseDatabaseFileWithPlugin( + self._plugin, test_file, cache) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 56) + + event_object = event_objects[0] + + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.UNKNOWN) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2014-11-25 21:08:23.698737') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_extension_id = u'ognampngfcbddbfemdapefohjiobgbdl' + self.assertEquals(event_object.extension_id, expected_extension_id) + + self.assertEquals(event_object.action_type, 1) + self.assertEquals(event_object.activity_id, 48) + self.assertEquals(event_object.api_name, u'browserAction.onClicked') + + expected_msg = ( + u'Chrome extension: ognampngfcbddbfemdapefohjiobgbdl ' + u'Action type: 1 ' + u'Activity identifier: 48 ' + u'API name: browserAction.onClicked') + expected_short = ( + u'ognampngfcbddbfemdapefohjiobgbdl browserAction.onClicked') + + self._TestGetMessageStrings(event_object, expected_msg, expected_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/sqlite_plugins/chrome_test.py b/plaso/parsers/sqlite_plugins/chrome_test.py new file mode 100644 index 0000000..a473889 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/chrome_test.py @@ -0,0 +1,102 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Google Chrome History database plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import chrome as chrome_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import sqlite +from plaso.parsers.sqlite_plugins import chrome +from plaso.parsers.sqlite_plugins import test_lib + + +class ChromeHistoryPluginTest(test_lib.SQLitePluginTestCase): + """Tests for the Google Chrome History database plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = chrome.ChromeHistoryPlugin() + + def testProcess(self): + """Tests the Process function on a Chrome History database file.""" + test_file = self._GetTestFilePath(['History']) + cache = sqlite.SQLiteCache() + event_queue_consumer = self._ParseDatabaseFileWithPlugin( + self._plugin, test_file, cache) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # The History file contains 71 events (69 page visits, 1 file downloads). + self.assertEquals(len(event_objects), 71) + + # Check the first page visited entry. + event_object = event_objects[0] + + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.PAGE_VISITED) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-04-07 12:03:11') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_url = u'http://start.ubuntu.com/10.04/Google/' + self.assertEquals(event_object.url, expected_url) + + expected_title = u'Ubuntu Start Page' + self.assertEquals(event_object.title, expected_title) + + expected_msg = ( + u'{0:s} ({1:s}) [count: 0] Host: start.ubuntu.com ' + u'Visit Source: [SOURCE_FIREFOX_IMPORTED] Type: [LINK - User clicked ' + u'a link] (URL not typed directly - no typed count)').format( + expected_url, expected_title) + expected_short = u'{0:s} ({1:s})'.format(expected_url, expected_title) + + self._TestGetMessageStrings(event_object, expected_msg, expected_short) + + # Check the first file downloaded entry. + event_object = event_objects[69] + + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.FILE_DOWNLOADED) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-05-23 08:35:30') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_url = ( + u'http://fatloss4idiotsx.com/download/funcats/' + u'funcats_scr.exe') + self.assertEquals(event_object.url, expected_url) + + expected_full_path = u'/home/john/Downloads/funcats_scr.exe' + self.assertEquals(event_object.full_path, expected_full_path) + + expected_msg = ( + u'{0:s} ({1:s}). Received: 1132155 bytes out of: ' + u'1132155 bytes.').format( + expected_url, expected_full_path) + expected_short = u'{0:s} downloaded (1132155 bytes)'.format( + expected_full_path) + self._TestGetMessageStrings(event_object, expected_msg, expected_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/sqlite_plugins/firefox.py b/plaso/parsers/sqlite_plugins/firefox.py new file mode 100644 index 0000000..558faed --- /dev/null +++ b/plaso/parsers/sqlite_plugins/firefox.py @@ -0,0 +1,476 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a parser for the Mozilla Firefox history.""" + +import sqlite3 + +from plaso.events import time_events +from plaso.lib import event +from plaso.lib import eventdata +from plaso.parsers import sqlite +from plaso.parsers.sqlite_plugins import interface + + +# Check SQlite version, bail out early if too old. +if sqlite3.sqlite_version_info < (3, 7, 8): + raise ImportWarning( + 'FirefoxHistoryParser requires at least SQLite version 3.7.8.') + + +class FirefoxPlacesBookmarkAnnotation(time_events.TimestampEvent): + """Convenience class for a Firefox bookmark annotation event.""" + + DATA_TYPE = 'firefox:places:bookmark_annotation' + + def __init__(self, timestamp, usage, row_id, title, url, content): + """Initializes the event object. + + Args: + timestamp: The timestamp value. + usage: Timestamp description string. + row_id: The identifier of the corresponding row. + title: The title of the bookmark folder. + url: The bookmarked URL. + content: The content of the annotation. + """ + super(FirefoxPlacesBookmarkAnnotation, self).__init__( + timestamp, usage) + + self.offset = row_id + self.title = title + self.url = url + self.content = content + + +class FirefoxPlacesBookmarkFolder(time_events.TimestampEvent): + """Convenience class for a Firefox bookmark folder event.""" + + DATA_TYPE = 'firefox:places:bookmark_folder' + + def __init__(self, timestamp, usage, row_id, title): + """Initializes the event object. + + Args: + timestamp: The timestamp value. + usage: Timestamp description string. + row_id: The identifier of the corresponding row. + title: The title of the bookmark folder. + """ + super(FirefoxPlacesBookmarkFolder, self).__init__( + timestamp, usage) + + self.offset = row_id + self.title = title + + +class FirefoxPlacesBookmark(time_events.TimestampEvent): + """Convenience class for a Firefox bookmark event.""" + + DATA_TYPE = 'firefox:places:bookmark' + + # TODO: move to formatter. + _TYPES = { + 1: 'URL', + 2: 'Folder', + 3: 'Separator', + } + _TYPES.setdefault('N/A') + + # pylint: disable=redefined-builtin + def __init__(self, timestamp, usage, row_id, type, title, url, places_title, + hostname, visit_count): + """Initializes the event object. + + Args: + timestamp: The timestamp value. + usage: Timestamp description string. + row_id: The identifier of the corresponding row. + type: Integer value containing the bookmark type. + title: The title of the bookmark folder. + url: The bookmarked URL. + places_title: The places title. + hostname: The hostname. + visit_count: The visit count. + """ + super(FirefoxPlacesBookmark, self).__init__(timestamp, usage) + + self.offset = row_id + self.type = self._TYPES[type] + self.title = title + self.url = url + self.places_title = places_title + self.host = hostname + self.visit_count = visit_count + + +class FirefoxPlacesPageVisitedEvent(event.EventObject): + """Convenience class for a Firefox page visited event.""" + + DATA_TYPE = 'firefox:places:page_visited' + + def __init__(self, timestamp, row_id, url, title, hostname, visit_count, + visit_type, extra): + """Initializes the event object. + + Args: + timestamp: The timestamp time value. The timestamp contains the + number of microseconds since Jan 1, 1970 00:00:00 UTC. + row_id: The identifier of the corresponding row. + url: The URL of the visited page. + title: The title of the visited page. + hostname: The visited hostname. + visit_count: The visit count. + visit_type: The transition type for the event. + extra: A list containing extra event data (TODO refactor). + """ + super(FirefoxPlacesPageVisitedEvent, self).__init__() + + self.timestamp = timestamp + self.timestamp_desc = eventdata.EventTimestamp.PAGE_VISITED + + self.offset = row_id + self.url = url + self.title = title + self.host = hostname + self.visit_count = visit_count + self.visit_type = visit_type + if extra: + self.extra = extra + + +class FirefoxDownload(time_events.TimestampEvent): + """Convenience class for a Firefox download event.""" + + DATA_TYPE = 'firefox:downloads:download' + + def __init__(self, timestamp, usage, row_id, name, url, referrer, full_path, + temporary_location, received_bytes, total_bytes, mime_type): + """Initializes the event object. + + Args: + timestamp: The timestamp value. + usage: Timestamp description string. + row_id: The identifier of the corresponding row. + name: The name of the download. + url: The source URL of the download. + referrer: The referrer URL of the download. + full_path: The full path of the target of the download. + temporary_location: The temporary location of the download. + received_bytes: The number of bytes received. + total_bytes: The total number of bytes of the download. + mime_type: The mime type of the download. + """ + super(FirefoxDownload, self).__init__(timestamp, usage) + + self.offset = row_id + self.name = name + self.url = url + self.referrer = referrer + self.full_path = full_path + self.temporary_location = temporary_location + self.received_bytes = received_bytes + self.total_bytes = total_bytes + self.mime_type = mime_type + + +class FirefoxHistoryPlugin(interface.SQLitePlugin): + """Parses a Firefox history file. + + The Firefox history is stored in a SQLite database file named + places.sqlite. + """ + + NAME = 'firefox_history' + DESCRIPTION = u'Parser for Firefox history SQLite database files.' + + # Define the needed queries. + QUERIES = [ + (('SELECT moz_historyvisits.id, moz_places.url, moz_places.title, ' + 'moz_places.visit_count, moz_historyvisits.visit_date, ' + 'moz_historyvisits.from_visit, moz_places.rev_host, ' + 'moz_places.hidden, moz_places.typed, moz_historyvisits.visit_type ' + 'FROM moz_places, moz_historyvisits ' + 'WHERE moz_places.id = moz_historyvisits.place_id'), + 'ParsePageVisitedRow'), + (('SELECT moz_bookmarks.type, moz_bookmarks.title AS bookmark_title, ' + 'moz_bookmarks.dateAdded, moz_bookmarks.lastModified, ' + 'moz_places.url, moz_places.title AS places_title, ' + 'moz_places.rev_host, moz_places.visit_count, moz_bookmarks.id ' + 'FROM moz_places, moz_bookmarks WHERE moz_bookmarks.fk = moz_places.id ' + 'AND moz_bookmarks.type <> 3'), + 'ParseBookmarkRow'), + (('SELECT moz_items_annos.content, moz_items_annos.dateAdded, ' + 'moz_items_annos.lastModified, moz_bookmarks.title, ' + 'moz_places.url, moz_places.rev_host, moz_items_annos.id ' + 'FROM moz_items_annos, moz_bookmarks, moz_places ' + 'WHERE moz_items_annos.item_id = moz_bookmarks.id ' + 'AND moz_bookmarks.fk = moz_places.id'), + 'ParseBookmarkAnnotationRow'), + (('SELECT moz_bookmarks.id, moz_bookmarks.title,' + 'moz_bookmarks.dateAdded, moz_bookmarks.lastModified ' + 'FROM moz_bookmarks WHERE moz_bookmarks.type = 2'), + 'ParseBookmarkFolderRow')] + + # The required tables. + REQUIRED_TABLES = frozenset([ + 'moz_places', 'moz_historyvisits', 'moz_bookmarks', 'moz_items_annos']) + + # Cache queries. + URL_CACHE_QUERY = ( + 'SELECT h.id AS id, p.url, p.rev_host FROM moz_places p, ' + 'moz_historyvisits h WHERE p.id = h.place_id') + + def ParseBookmarkAnnotationRow( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + **unused_kwargs): + """Parses a bookmark annotation row. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + """ + if row['dateAdded']: + event_object = FirefoxPlacesBookmarkAnnotation( + row['dateAdded'], eventdata.EventTimestamp.ADDED_TIME, + row['id'], row['title'], row['url'], row['content']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + if row['lastModified']: + event_object = FirefoxPlacesBookmarkAnnotation( + row['lastModified'], eventdata.EventTimestamp.MODIFICATION_TIME, + row['id'], row['title'], row['url'], row['content']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + def ParseBookmarkFolderRow( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + **unused_kwargs): + """Parses a bookmark folder row. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + """ + if not row['title']: + title = 'N/A' + else: + title = row['title'] + + if row['dateAdded']: + event_object = FirefoxPlacesBookmarkFolder( + row['dateAdded'], eventdata.EventTimestamp.ADDED_TIME, + row['id'], title) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + if row['lastModified']: + event_object = FirefoxPlacesBookmarkFolder( + row['lastModified'], eventdata.EventTimestamp.MODIFICATION_TIME, + row['id'], title) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + def ParseBookmarkRow( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + **unused_kwargs): + """Parses a bookmark row. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + """ + if row['dateAdded']: + event_object = FirefoxPlacesBookmark( + row['dateAdded'], eventdata.EventTimestamp.ADDED_TIME, + row['id'], row['type'], row['bookmark_title'], row['url'], + row['places_title'], getattr(row, 'rev_host', 'N/A'), + row['visit_count']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + if row['lastModified']: + event_object = FirefoxPlacesBookmark( + row['lastModified'], eventdata.EventTimestamp.MODIFICATION_TIME, + row['id'], row['type'], row['bookmark_title'], row['url'], + row['places_title'], getattr(row, 'rev_host', 'N/A'), + row['visit_count']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + def ParsePageVisitedRow( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + cache=None, database=None, **unused_kwargs): + """Parses a page visited row. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + cache: A cache object (instance of SQLiteCache). + database: A database object (instance of SQLiteDatabase). + """ + # TODO: make extra conditional formatting. + extras = [] + if row['from_visit']: + extras.append(u'visited from: {0}'.format( + self._GetUrl(row['from_visit'], cache, database))) + + if row['hidden'] == '1': + extras.append('(url hidden)') + + if row['typed'] == '1': + extras.append('(directly typed)') + else: + extras.append('(URL not typed directly)') + + if row['visit_date']: + event_object = FirefoxPlacesPageVisitedEvent( + row['visit_date'], row['id'], row['url'], row['title'], + self._ReverseHostname(row['rev_host']), row['visit_count'], + row['visit_type'], extras) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + def _ReverseHostname(self, hostname): + """Reverses the hostname and strips the leading dot. + + The hostname entry is reversed: + moc.elgoog.www. + Should be: + www.google.com + + Args: + hostname: The reversed hostname. + + Returns: + Reversed string without a leading dot. + """ + if not hostname: + return '' + + if len(hostname) > 1: + if hostname[-1] == '.': + return hostname[::-1][1:] + else: + return hostname[::-1][0:] + return hostname + + def _GetUrl(self, url_id, cache, database): + """Return an URL from a reference to an entry in the from_visit table.""" + url_cache_results = cache.GetResults('url') + if not url_cache_results: + cursor = database.cursor + result_set = cursor.execute(self.URL_CACHE_QUERY) + cache.CacheQueryResults( + result_set, 'url', 'id', ('url', 'rev_host')) + url_cache_results = cache.GetResults('url') + + url, reverse_host = url_cache_results.get(url_id, [u'', u'']) + + if not url: + return u'' + + hostname = self._ReverseHostname(reverse_host) + return u'{:s} ({:s})'.format(url, hostname) + + +class FirefoxDownloadsPlugin(interface.SQLitePlugin): + """Parses a Firefox downloads file. + + The Firefox downloads history is stored in a SQLite database file named + downloads.sqlite. + """ + + NAME = 'firefox_downloads' + DESCRIPTION = u'Parser for Firefox downloads SQLite database files.' + + # Define the needed queries. + QUERIES = [ + (('SELECT moz_downloads.id, moz_downloads.name, moz_downloads.source, ' + 'moz_downloads.target, moz_downloads.tempPath, ' + 'moz_downloads.startTime, moz_downloads.endTime, moz_downloads.state, ' + 'moz_downloads.referrer, moz_downloads.currBytes, ' + 'moz_downloads.maxBytes, moz_downloads.mimeType ' + 'FROM moz_downloads'), + 'ParseDownloadsRow')] + + # The required tables. + REQUIRED_TABLES = frozenset(['moz_downloads']) + + def ParseDownloadsRow( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + **unused_kwargs): + """Parses a downloads row. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + """ + if row['startTime']: + event_object = FirefoxDownload( + row['startTime'], eventdata.EventTimestamp.START_TIME, + row['id'], row['name'], row['source'], row['referrer'], row['target'], + row['tempPath'], row['currBytes'], row['maxBytes'], row['mimeType']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + if row['endTime']: + event_object = FirefoxDownload( + row['endTime'], eventdata.EventTimestamp.END_TIME, + row['id'], row['name'], row['source'], row['referrer'], row['target'], + row['tempPath'], row['currBytes'], row['maxBytes'], row['mimeType']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + +sqlite.SQLiteParser.RegisterPlugins( + [FirefoxHistoryPlugin, FirefoxDownloadsPlugin]) diff --git a/plaso/parsers/sqlite_plugins/firefox_cookies.py b/plaso/parsers/sqlite_plugins/firefox_cookies.py new file mode 100644 index 0000000..b9ee8c9 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/firefox_cookies.py @@ -0,0 +1,163 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for the Firefox Cookie database.""" + +from plaso.events import time_events +from plaso.lib import errors +from plaso.lib import eventdata +from plaso.lib import timelib +# Register the cookie plugins. +from plaso.parsers import cookie_plugins # pylint: disable=unused-import +from plaso.parsers import sqlite +from plaso.parsers.cookie_plugins import interface as cookie_interface +from plaso.parsers.sqlite_plugins import interface + + +class FirefoxCookieEvent(time_events.TimestampEvent): + """Convenience class for a Firefox Cookie event.""" + + DATA_TYPE = 'firefox:cookie:entry' + + def __init__( + self, timestamp, usage, identifier, hostname, cookie_name, value, path, + secure, httponly): + """Initializes the event. + + Args: + timestamp: The timestamp value in WebKit format.. + usage: Timestamp description string. + identifier: The row identifier. + hostname: The hostname of host that set the cookie value. + cookie_name: The name field of the cookie. + value: The value of the cookie. + path: An URI of the page that set the cookie. + secure: Indication if this cookie should only be transmitted over a secure + channel. + httponly: An indication that the cookie cannot be accessed through client + side script. + """ + super(FirefoxCookieEvent, self).__init__(timestamp, usage) + if hostname.startswith('.'): + hostname = hostname[1:] + + self.offset = identifier + self.host = hostname + self.cookie_name = cookie_name + self.data = value + self.path = path + self.secure = True if secure else False + self.httponly = True if httponly else False + + if self.secure: + scheme = u'https' + else: + scheme = u'http' + + self.url = u'{0:s}://{1:s}{2:s}'.format(scheme, hostname, path) + + +class FirefoxCookiePlugin(interface.SQLitePlugin): + """Parse Firefox Cookies file.""" + + NAME = 'firefox_cookies' + DESCRIPTION = u'Parser for Firefox cookies SQLite database files.' + + # Define the needed queries. + QUERIES = [ + (('SELECT id, baseDomain, name, value, host, path, expiry, lastAccessed, ' + 'creationTime, isSecure, isHttpOnly FROM moz_cookies'), + 'ParseCookieRow')] + + # The required tables common to Archived History and History. + REQUIRED_TABLES = frozenset(['moz_cookies']) + + # Point to few sources for URL information. + URLS = [ + (u'https://hg.mozilla.org/mozilla-central/file/349a2f003529/netwerk/' + u'cookie/nsCookie.h')] + + def __init__(self): + """Initializes a plugin object.""" + super(FirefoxCookiePlugin, self).__init__() + self._cookie_plugins = cookie_interface.GetPlugins() + + def ParseCookieRow( + self, parser_context, row, file_entry=None, parser_chain=None, + query=None, **unused_kwargs): + """Parses a cookie row. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + """ + if row['creationTime']: + event_object = FirefoxCookieEvent( + row['creationTime'], eventdata.EventTimestamp.CREATION_TIME, + row['id'], row['host'], row['name'], row['value'], row['path'], + row['isSecure'], row['isHttpOnly']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + if row['lastAccessed']: + event_object = FirefoxCookieEvent( + row['lastAccessed'], eventdata.EventTimestamp.ACCESS_TIME, row['id'], + row['host'], row['name'], row['value'], row['path'], row['isSecure'], + row['isHttpOnly']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + if row['expiry']: + # Expiry time (nsCookieService::GetExpiry in + # netwerk/cookie/nsCookieService.cpp). + # It's calculated as the difference between the server time and the time + # the server wants the cookie to expire and adding that difference to the + # client time. This localizes the client time regardless of whether or not + # the TZ environment variable was set on the client. + timestamp = timelib.Timestamp.FromPosixTime(row['expiry']) + event_object = FirefoxCookieEvent( + timestamp, u'Cookie Expires', row['id'], row['host'], row['name'], + row['value'], row['path'], row['isSecure'], row['isHttpOnly']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + # Go through all cookie plugins to see if there are is any specific parsing + # needed. + hostname = row['host'] + if hostname.startswith('.'): + hostname = hostname[1:] + url = u'http{0:s}://{1:s}{2:s}'.format( + u's' if row['isSecure'] else u'', hostname, row['path']) + + for cookie_plugin in self._cookie_plugins: + try: + cookie_plugin.Process( + parser_context, cookie_name=row['name'], cookie_data=row['value'], + url=url, file_entry=file_entry, parser_chain=parser_chain) + except errors.WrongPlugin: + pass + + +sqlite.SQLiteParser.RegisterPlugin(FirefoxCookiePlugin) diff --git a/plaso/parsers/sqlite_plugins/firefox_cookies_test.py b/plaso/parsers/sqlite_plugins/firefox_cookies_test.py new file mode 100644 index 0000000..77397cd --- /dev/null +++ b/plaso/parsers/sqlite_plugins/firefox_cookies_test.py @@ -0,0 +1,107 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Firefox cookie database plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import firefox_cookies as firefox_cookies_formatter +from plaso.lib import timelib_test +from plaso.parsers.sqlite_plugins import firefox_cookies +from plaso.parsers.sqlite_plugins import test_lib + + +class FirefoxCookiesPluginTest(test_lib.SQLitePluginTestCase): + """Tests for the Firefox cookie database plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = firefox_cookies.FirefoxCookiePlugin() + + def testProcess(self): + """Tests the Process function on a Firefox 29 cookie database file.""" + test_file = self._GetTestFilePath(['firefox_cookies.sqlite']) + event_queue_consumer = self._ParseDatabaseFileWithPlugin( + self._plugin, test_file) + + event_objects = [] + extra_objects = [] + + # sqlite> SELECT COUNT(id) FROM moz_cookies; + # 90 + # Thus the cookie database contains 93 entries: + # 90 Last Access Time + # 90 Cookie Expires + # 90 Creation Time + # + # And then in addition the following entries are added due to cookie + # plugins (TODO filter these out since adding new cookie plugin will + # change this number and thus affect this test): + # 15 Last Visited Time + # 5 Analytics Previous Time + # 5 Analytics Creation Time + # + # In total: 93 * 3 + 15 + 5 + 5 = 304 events. + for event_object in self._GetEventObjectsFromQueue(event_queue_consumer): + if isinstance(event_object, firefox_cookies.FirefoxCookieEvent): + event_objects.append(event_object) + else: + extra_objects.append(event_object) + + self.assertEquals(len(event_objects), 90 * 3) + self.assertGreaterEqual(len(extra_objects), 25) + + # Check one greenqloud.com event + event_object = event_objects[32] + self.assertEquals( + event_object.timestamp_desc, 'Cookie Expires') + self.assertEquals(event_object.host, u's.greenqloud.com') + self.assertEquals(event_object.cookie_name, u'__utma') + self.assertFalse(event_object.httponly) + self.assertEquals(event_object.url, u'http://s.greenqloud.com/') + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2015-10-30 21:56:03') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'http://s.greenqloud.com/ (__utma) Flags: [HTTP only]: False') + expected_short = u's.greenqloud.com (__utma)' + self._TestGetMessageStrings(event_object, expected_msg, expected_short) + + # Check one of the visits to pubmatic.com. + event_object = event_objects[62] + self.assertEquals( + event_object.timestamp_desc, u'Cookie Expires') + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-29 21:56:04') + self.assertEquals(event_object.timestamp, expected_timestamp) + + self.assertEquals(event_object.url, u'http://pubmatic.com/') + self.assertEquals(event_object.path, u'/') + self.assertFalse(event_object.secure) + + expected_msg = ( + u'http://pubmatic.com/ (KRTBCOOKIE_391) Flags: [HTTP only]: False') + self._TestGetMessageStrings( + event_object, expected_msg, u'pubmatic.com (KRTBCOOKIE_391)') + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/sqlite_plugins/firefox_test.py b/plaso/parsers/sqlite_plugins/firefox_test.py new file mode 100644 index 0000000..1a57889 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/firefox_test.py @@ -0,0 +1,277 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Mozilla Firefox history database plugin.""" + +import collections +import unittest + +# pylint: disable=unused-import +from plaso.formatters import firefox as firefox_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import sqlite +from plaso.parsers.sqlite_plugins import firefox +from plaso.parsers.sqlite_plugins import test_lib + + +class FirefoxHistoryPluginTest(test_lib.SQLitePluginTestCase): + """Tests for the Mozilla Firefox history database plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = firefox.FirefoxHistoryPlugin() + + def testProcessPriorTo24(self): + """Tests the Process function on a Firefox History database file.""" + # This is probably version 23 but potentially an older version. + test_file = self._GetTestFilePath(['places.sqlite']) + cache = sqlite.SQLiteCache() + event_queue_consumer = self._ParseDatabaseFileWithPlugin( + self._plugin, test_file, cache) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # The places.sqlite file contains 205 events (1 page visit, + # 2 x 91 bookmark records, 2 x 3 bookmark annotations, + # 2 x 8 bookmark folders). + # However there are three events that do not have a timestamp + # so the test file will show 202 extracted events. + self.assertEquals(len(event_objects), 202) + + # Check the first page visited event. + event_object = event_objects[0] + + self.assertEquals(event_object.data_type, 'firefox:places:page_visited') + + self.assertEquals(event_object.timestamp_desc, + eventdata.EventTimestamp.PAGE_VISITED) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-07-01 11:16:21.371935') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_url = u'http://news.google.com/' + self.assertEquals(event_object.url, expected_url) + + expected_title = u'Google News' + self.assertEquals(event_object.title, expected_title) + + expected_msg = ( + u'{0:s} ({1:s}) [count: 1] Host: news.google.com ' + u'(URL not typed directly) Transition: TYPED').format( + expected_url, expected_title) + expected_short = u'URL: {}'.format(expected_url) + + self._TestGetMessageStrings(event_object, expected_msg, expected_short) + + # Check the first bookmark event. + event_object = event_objects[1] + + self.assertEquals(event_object.data_type, 'firefox:places:bookmark') + + self.assertEquals(event_object.timestamp_desc, + eventdata.EventTimestamp.ADDED_TIME) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + u'2011-07-01 11:13:59.266344+00:00') + self.assertEquals(event_object.timestamp, expected_timestamp) + + # Check the second bookmark event. + event_object = event_objects[2] + + self.assertEquals(event_object.data_type, 'firefox:places:bookmark') + + self.assertEquals(event_object.timestamp_desc, + eventdata.EventTimestamp.MODIFICATION_TIME) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + u'2011-07-01 11:13:59.267198+00:00') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_url = ( + u'place:folder=BOOKMARKS_MENU&folder=UNFILED_BOOKMARKS&folder=TOOLBAR&' + u'sort=12&excludeQueries=1&excludeItemIfParentHasAnnotation=livemark%2F' + u'feedURI&maxResults=10&queryType=1') + self.assertEquals(event_object.url, expected_url) + + expected_title = u'Recently Bookmarked' + self.assertEquals(event_object.title, expected_title) + + expected_msg = ( + u'Bookmark URL {0:s} ({1:s}) [folder=BOOKMARKS_MENU&' + u'folder=UNFILED_BOOKMARKS&folder=TOOLBAR&sort=12&excludeQueries=1&' + u'excludeItemIfParentHasAnnotation=livemark%2FfeedURI&maxResults=10&' + u'queryType=1] visit count 0').format( + expected_title, expected_url) + expected_short = ( + u'Bookmarked Recently Bookmarked ' + u'(place:folder=BOOKMARKS_MENU&folder=UNFILED_BO...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_short) + + # Check the first bookmark annotation event. + event_object = event_objects[183] + + self.assertEquals( + event_object.data_type, 'firefox:places:bookmark_annotation') + + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.CREATION_TIME) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + u'2011-07-01 11:13:59.267146+00:00') + self.assertEquals(event_object.timestamp, expected_timestamp) + + # Check another bookmark annotation event. + event_object = event_objects[184] + + self.assertEquals( + event_object.data_type, 'firefox:places:bookmark_annotation') + + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.CREATION_TIME) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + u'2011-07-01 11:13:59.267605+00:00') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_url = u'place:sort=14&type=6&maxResults=10&queryType=1' + self.assertEquals(event_object.url, expected_url) + + expected_title = u'Recent Tags' + self.assertEquals(event_object.title, expected_title) + + expected_msg = ( + u'Bookmark Annotation: [RecentTags] to bookmark ' + u'[{0:s}] ({1:s})').format( + expected_title, expected_url) + expected_short = u'Bookmark Annotation: Recent Tags' + self._TestGetMessageStrings(event_object, expected_msg, expected_short) + + # Check the second last bookmark folder event. + event_object = event_objects[200] + + self.assertEquals(event_object.data_type, 'firefox:places:bookmark_folder') + + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.ADDED_TIME) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + u'2011-03-21 10:05:01.553774+00:00') + self.assertEquals(event_object.timestamp, expected_timestamp) + # Check the last bookmark folder event. + event_object = event_objects[201] + + self.assertEquals( + event_object.data_type, 'firefox:places:bookmark_folder') + + self.assertEquals( + event_object.timestamp_desc, + eventdata.EventTimestamp.MODIFICATION_TIME) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + u'2011-07-01 11:14:11.766851+00:00') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_title = u'Latest Headlines' + self.assertEquals(event_object.title, expected_title) + + expected_msg = expected_title + expected_short = expected_title + self._TestGetMessageStrings(event_object, expected_msg, expected_short) + + def testProcessVersion25(self): + """Tests the Process function on a Firefox History database file v 25.""" + test_file = self._GetTestFilePath(['places_new.sqlite']) + cache = sqlite.SQLiteCache() + event_queue_consumer = self._ParseDatabaseFileWithPlugin( + self._plugin, test_file, cache) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # The places.sqlite file contains 84 events: + # 34 page visits. + # 28 bookmarks + # 14 bookmark folders + # 8 annotations + self.assertEquals(len(event_objects), 84) + counter = collections.Counter() + for event_object in event_objects: + counter[event_object.data_type] += 1 + + self.assertEquals(counter['firefox:places:bookmark'], 28) + self.assertEquals(counter['firefox:places:page_visited'], 34) + self.assertEquals(counter['firefox:places:bookmark_folder'], 14) + self.assertEquals(counter['firefox:places:bookmark_annotation'], 8) + + random_event = event_objects[10] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-10-30 21:57:11.281942') + self.assertEquals(random_event.timestamp, expected_timestamp) + + expected_short = u'URL: http://code.google.com/p/plaso' + expected_msg = ( + u'http://code.google.com/p/plaso [count: 1] Host: code.google.com ' + u'(URL not typed directly) Transition: TYPED') + + self._TestGetMessageStrings(random_event, expected_msg, expected_short) + + +class FirefoxDownloadsPluginTest(test_lib.SQLitePluginTestCase): + """Tests for the Mozilla Firefox downloads database plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = firefox.FirefoxDownloadsPlugin() + + def testProcessVersion25(self): + """Tests the Process function on a Firefox Downloads database file.""" + test_file = self._GetTestFilePath(['downloads.sqlite']) + cache = sqlite.SQLiteCache() + event_queue_consumer = self._ParseDatabaseFileWithPlugin( + self._plugin, test_file, cache) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # The downloads.sqlite file contains 2 events (1 download). + self.assertEquals(len(event_objects), 2) + + # Check the first page visited event. + event_object = event_objects[0] + + self.assertEquals(event_object.data_type, 'firefox:downloads:download') + + self.assertEquals(event_object.timestamp_desc, + eventdata.EventTimestamp.START_TIME) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + u'2013-07-18 18:59:59.312000+00:00') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_url = ( + u'https://plaso.googlecode.com/files/' + u'plaso-static-1.0.1-win32-vs2008.zip') + self.assertEquals(event_object.url, expected_url) + + expected_full_path = u'file:///D:/plaso-static-1.0.1-win32-vs2008.zip' + self.assertEquals(event_object.full_path, expected_full_path) + + self.assertEquals(event_object.received_bytes, 15974599) + self.assertEquals(event_object.total_bytes, 15974599) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/sqlite_plugins/gdrive.py b/plaso/parsers/sqlite_plugins/gdrive.py new file mode 100644 index 0000000..ac435d1 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/gdrive.py @@ -0,0 +1,268 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a parser for the Google Drive snaphots. + +The Google Drive snapshots are stored in SQLite database files named +snapshot.db. +""" + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.parsers import sqlite +from plaso.parsers.sqlite_plugins import interface + + +__author__ = 'David Nides (david.nides@gmail.com)' + + +class GoogleDriveSnapshotCloudEntryEvent(time_events.PosixTimeEvent): + """Convenience class for a Google Drive snapshot cloud entry.""" + + DATA_TYPE = 'gdrive:snapshot:cloud_entry' + + # TODO: this could be moved to the formatter. + # The following definition for values can be found on Patrick Olson's blog: + # http://www.sysforensics.org/2012/05/google-drive-forensics-notes.html + _DOC_TYPES = { + 0: u'FOLDER', + 1: u'FILE', + 2: u'PRESENTATION', + 3: u'UNKNOWN', + 4: u'SPREADSHEET', + 5: u'DRAWING', + 6: u'DOCUMENT', + 7: u'TABLE', + } + + def __init__(self, posix_time, usage, url, path, size, doc_type, shared): + """Initializes the event. + + Args: + posix_time: The POSIX time value. + usage: The description of the usage of the time value. + url: The URL of the file as in the cloud. + path: The path of the file. + size: The size of the file. + doc_type: Integer value containing the document type. + shared: A string indicating whether or not this is a shared document. + """ + super(GoogleDriveSnapshotCloudEntryEvent, self).__init__( + posix_time, usage) + + self.url = url + self.path = path + self.size = size + self.document_type = self._DOC_TYPES.get(doc_type, u'UNKNOWN') + self.shared = shared + + +class GoogleDriveSnapshotLocalEntryEvent(time_events.PosixTimeEvent): + """Convenience class for a Google Drive snapshot local entry event.""" + + DATA_TYPE = 'gdrive:snapshot:local_entry' + + def __init__(self, posix_time, local_path, size): + """Initializes the event object. + + Args: + posix_time: The POSIX time value. + local_path: The local path of the file. + size: The size of the file. + """ + super(GoogleDriveSnapshotLocalEntryEvent, self).__init__( + posix_time, eventdata.EventTimestamp.MODIFICATION_TIME) + + self.path = local_path + self.size = size + + +class GoogleDrivePlugin(interface.SQLitePlugin): + """SQLite plugin for Google Drive snapshot.db files.""" + + NAME = 'google_drive' + DESCRIPTION = u'Parser for Google Drive SQLite database files.' + + # Define the needed queries. + QUERIES = [ + ((u'SELECT e.resource_id, e.filename, e.modified, e.created, e.size, ' + u'e.doc_type, e.shared, e.checksum, e.url, r.parent_resource_id FROM ' + u'cloud_entry AS e, cloud_relations AS r WHERE r.child_resource_id = ' + u'e.resource_id AND e.modified IS NOT NULL;'), 'ParseCloudEntryRow'), + ((u'SELECT inode_number, filename, modified, checksum, size FROM ' + u'local_entry WHERE modified IS NOT NULL;'), 'ParseLocalEntryRow')] + + # The required tables. + REQUIRED_TABLES = frozenset([ + 'cloud_entry', 'cloud_relations', 'local_entry', 'local_relations', + 'mapping', 'overlay_status']) + + # Queries used to build cache. + LOCAL_PATH_CACHE_QUERY = ( + u'SELECT r.child_inode_number, r.parent_inode_number, e.filename FROM ' + u'local_relations AS r, local_entry AS e WHERE r.child_inode_number = ' + u'e.inode_number') + CLOUD_PATH_CACHE_QUERY = ( + u'SELECT e.filename, e.resource_id, r.parent_resource_id AS parent ' + u'FROM cloud_entry AS e, cloud_relations AS r WHERE e.doc_type = 0 ' + u'AND e.resource_id = r.child_resource_id') + + def GetLocalPath(self, inode, cache, database): + """Return local path for a given inode. + + Args: + inode: The inode number for the file. + cache: A cache object (instance of SQLiteCache). + database: A database object (instance of SQLiteDatabase). + + Returns: + A full path, including the filename of the given inode value. + """ + local_path = cache.GetResults('local_path') + if not local_path: + cursor = database.cursor + results = cursor.execute(self.LOCAL_PATH_CACHE_QUERY) + cache.CacheQueryResults( + results, 'local_path', 'child_inode_number', + ('parent_inode_number', 'filename')) + local_path = cache.GetResults('local_path') + + parent, path = local_path.get(inode, [None, None]) + + # TODO: Read the local_sync_root from the sync_config.db and use that + # for a root value. + root_value = u'%local_sync_root%/' + + if not path: + return root_value + + paths = [] + while path: + paths.append(path) + parent, path = local_path.get(parent, [None, None]) + + if not paths: + return root_value + + # Paths are built top level to root so we need to reverse the list to + # represent them in the traditional order. + paths.reverse() + return root_value + u'/'.join(paths) + + def GetCloudPath(self, resource_id, cache, database): + """Return cloud path given a resource id. + + Args: + resource_id: The resource_id for the file. + cache: The local cache object. + database: A database object (instance of SQLiteDatabase). + + Returns: + A full path to the resource value. + """ + cloud_path = cache.GetResults('cloud_path') + if not cloud_path: + cursor = database.cursor + results = cursor.execute(self.CLOUD_PATH_CACHE_QUERY) + cache.CacheQueryResults( + results, 'cloud_path', 'resource_id', ('filename', 'parent')) + cloud_path = cache.GetResults('cloud_path') + + if resource_id == u'folder:root': + return u'/' + + paths = [] + parent_path, parent_id = cloud_path.get(resource_id, [u'', u'']) + while parent_path: + if parent_path == u'folder:root': + break + paths.append(parent_path) + parent_path, parent_id = cloud_path.get(parent_id, [u'', u'']) + + if not paths: + return u'/' + + # Paths are built top level to root so we need to reverse the list to + # represent them in the traditional order. + paths.reverse() + return u'/{0:s}/'.format(u'/'.join(paths)) + + def ParseCloudEntryRow( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + cache=None, database=None, **unused_kwargs): + """Parses a cloud entry row. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + cache: The local cache object. + database: The database object. + """ + cloud_path = self.GetCloudPath(row['parent_resource_id'], cache, database) + cloud_filename = u'{0:s}{1:s}'.format(cloud_path, row['filename']) + + if row['shared']: + shared = 'Shared' + else: + shared = 'Private' + + event_object = GoogleDriveSnapshotCloudEntryEvent( + row['modified'], eventdata.EventTimestamp.MODIFICATION_TIME, + row['url'], cloud_filename, row['size'], row['doc_type'], shared) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + if row['created']: + event_object = GoogleDriveSnapshotCloudEntryEvent( + row['created'], eventdata.EventTimestamp.CREATION_TIME, + row['url'], cloud_filename, row['size'], row['doc_type'], shared) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + def ParseLocalEntryRow( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + cache=None, database=None, **unused_kwargs): + """Parses a local entry row. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + cache: The local cache object (instance of SQLiteCache). + database: A database object (instance of SQLiteDatabase). + """ + local_path = self.GetLocalPath(row['inode_number'], cache, database) + + event_object = GoogleDriveSnapshotLocalEntryEvent( + row['modified'], local_path, row['size']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + +sqlite.SQLiteParser.RegisterPlugin(GoogleDrivePlugin) diff --git a/plaso/parsers/sqlite_plugins/gdrive_test.py b/plaso/parsers/sqlite_plugins/gdrive_test.py new file mode 100644 index 0000000..e6889b0 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/gdrive_test.py @@ -0,0 +1,104 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Google Drive database plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import gdrive as gdrive_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import sqlite +from plaso.parsers.sqlite_plugins import gdrive +from plaso.parsers.sqlite_plugins import test_lib + + +class GoogleDrivePluginTest(test_lib.SQLitePluginTestCase): + """Tests for the Google Drive database plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = gdrive.GoogleDrivePlugin() + + def testProcess(self): + """Tests the Process function on a Google Drive database file.""" + test_file = self._GetTestFilePath(['snapshot.db']) + cache = sqlite.SQLiteCache() + event_queue_consumer = self._ParseDatabaseFileWithPlugin( + self._plugin, test_file, cache=cache) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 30) + + # Let's verify that we've got the correct balance of cloud and local + # entry events. + # 10 files mounting to: + # 20 Cloud Entries (two timestamps per file). + # 10 Local Entries (one timestamp per file). + local_entries = [] + cloud_entries = [] + for event_object in event_objects: + if event_object.data_type == 'gdrive:snapshot:local_entry': + local_entries.append(event_object) + else: + cloud_entries.append(event_object) + self.assertEquals(len(local_entries), 10) + self.assertEquals(len(cloud_entries), 20) + + # Test one local and one cloud entry. + event_object = local_entries[5] + + file_path = ( + u'%local_sync_root%/Top Secret/Enn meiri ' + u'leyndarm\xe1l/S\xfdnileiki - \xd6rverpi.gdoc') + self.assertEquals(event_object.path, file_path) + + expected_msg = u'File Path: {} Size: 184'.format(file_path) + + self._TestGetMessageStrings(event_object, expected_msg, file_path) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2014-01-28 00:11:25') + self.assertEquals(event_object.timestamp, expected_timestamp) + + event_object = cloud_entries[16] + + self.assertEquals(event_object.document_type, u'DOCUMENT') + self.assertEquals( + event_object.timestamp_desc, + eventdata.EventTimestamp.MODIFICATION_TIME) + self.assertEquals(event_object.url, ( + u'https://docs.google.com/document/d/' + u'1ypXwXhQWliiMSQN9S5M0K6Wh39XF4Uz4GmY-njMf-Z0/edit?usp=docslist_api')) + + expected_msg = ( + u'File Path: /Almenningur/Saklausa hli\xf0in [Private] Size: 0 URL: ' + u'https://docs.google.com/document/d/' + u'1ypXwXhQWliiMSQN9S5M0K6Wh39XF4Uz4GmY-njMf-Z0/edit?usp=docslist_api ' + u'Type: DOCUMENT') + expected_short = u'/Almenningur/Saklausa hli\xf0in' + + self._TestGetMessageStrings(event_object, expected_msg, expected_short) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2014-01-28 00:12:27') + self.assertEquals(event_object.timestamp, expected_timestamp) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/sqlite_plugins/interface.py b/plaso/parsers/sqlite_plugins/interface.py new file mode 100644 index 0000000..b77003b --- /dev/null +++ b/plaso/parsers/sqlite_plugins/interface.py @@ -0,0 +1,121 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a SQLite parser.""" + +import logging + +import sqlite3 + +from plaso.lib import errors +from plaso.parsers import plugins + + +class SQLitePlugin(plugins.BasePlugin): + """A SQLite plugin for Plaso.""" + + NAME = 'sqlite' + DESCRIPTION = u'Parser for SQLite database files.' + + # Queries to be executed. + # Should be a list of tuples with two entries, SQLCommand and callback + # function name. + QUERIES = [] + + # List of tables that should be present in the database, for verification. + REQUIRED_TABLES = frozenset([]) + + def GetEntries( + self, parser_context, file_entry=None, parser_chain=None, cache=None, + database=None, **kwargs): + """Extracts event objects from a SQLite database. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + cache: A SQLiteCache object. + database: A database object (instance of SQLiteDatabase). + """ + for query, callback_method in self.QUERIES: + try: + callback = getattr(self, callback_method, None) + if callback is None: + logging.warning( + u'[{0:s}] missing callback method: {1:s} for query: {2:s}'.format( + self.NAME, callback_method, query)) + continue + + cursor = database.cursor + sql_results = cursor.execute(query) + row = sql_results.fetchone() + + while row: + callback( + parser_context, row, query=query, cache=cache, database=database, + file_entry=file_entry, parser_chain=parser_chain) + + row = sql_results.fetchone() + + except sqlite3.DatabaseError as exception: + logging.debug(u'SQLite error occured: {0:s}'.format(exception)) + + def Process( + self, parser_context, file_entry=None, parser_chain=None, cache=None, + database=None, **kwargs): + """Determine if this is the right plugin for this database. + + This function takes a SQLiteDatabase object and compares the list + of required tables against the available tables in the database. + If all the tables defined in REQUIRED_TABLES are present in the + database then this plugin is considered to be the correct plugin + and the function will return back a generator that yields event + objects. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + cache: A SQLiteCache object. + database: A database object (instance of SQLiteDatabase). + + Raises: + errors.WrongPlugin: If the database does not contain all the tables + defined in the REQUIRED_TABLES set. + ValueError: If the database attribute is not passed in. + """ + if database is None: + raise ValueError(u'Database is not set.') + + if not frozenset(database.tables) >= self.REQUIRED_TABLES: + raise errors.WrongPlugin( + u'Not the correct database tables for: {0:s}'.format(self.NAME)) + + # This will raise if unhandled keyword arguments are passed. + super(SQLitePlugin, self).Process(parser_context, **kwargs) + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + self.GetEntries( + parser_context, cache=cache, database=database, file_entry=file_entry, + parser_chain=parser_chain) diff --git a/plaso/parsers/sqlite_plugins/ls_quarantine.py b/plaso/parsers/sqlite_plugins/ls_quarantine.py new file mode 100644 index 0000000..9e4bad1 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/ls_quarantine.py @@ -0,0 +1,90 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Plugin for the Mac OS X launch services quarantine events.""" + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.parsers import sqlite +from plaso.parsers.sqlite_plugins import interface + + +class LsQuarantineEvent(time_events.CocoaTimeEvent): + """Convenience class for a Mac OS X launch services quarantine event.""" + DATA_TYPE = 'macosx:lsquarantine' + + # TODO: describe more clearly what the data value contains. + def __init__(self, cocoa_time, url, user_agent, data): + """Initializes the event object. + + Args: + cocoa_time: The Cocoa time value. + url: The original URL of the file. + user_agent: The user agent that was used to download the file. + data: The data. + """ + super(LsQuarantineEvent, self).__init__( + cocoa_time, eventdata.EventTimestamp.FILE_DOWNLOADED) + + self.url = url + self.agent = user_agent + self.data = data + + +class LsQuarantinePlugin(interface.SQLitePlugin): + """Parses the launch services quarantine events database. + + The LS quarantine events are stored in SQLite database files named + /Users//Library/Preferences/\ + QuarantineEvents.com.apple.LaunchServices + """ + + NAME = 'ls_quarantine' + DESCRIPTION = u'Parser for LS quarantine events SQLite database files.' + + # Define the needed queries. + QUERIES = [ + (('SELECT LSQuarantineTimestamp AS Time, LSQuarantine' + 'AgentName AS Agent, LSQuarantineOriginURLString AS URL, ' + 'LSQuarantineDataURLString AS Data FROM LSQuarantineEvent ' + 'ORDER BY Time'), 'ParseLSQuarantineRow')] + + # The required tables. + REQUIRED_TABLES = frozenset(['LSQuarantineEvent']) + + def ParseLSQuarantineRow( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + **unused_kwargs): + """Parses a launch services quarantine event row. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + """ + event_object = LsQuarantineEvent( + row['Time'], row['URL'], row['Agent'], row['Data']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + +sqlite.SQLiteParser.RegisterPlugin(LsQuarantinePlugin) diff --git a/plaso/parsers/sqlite_plugins/ls_quarantine_test.py b/plaso/parsers/sqlite_plugins/ls_quarantine_test.py new file mode 100644 index 0000000..6c56807 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/ls_quarantine_test.py @@ -0,0 +1,90 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the LS Quarantine database plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import ls_quarantine as ls_quarantine_formatter +from plaso.lib import timelib_test +from plaso.parsers.sqlite_plugins import ls_quarantine +from plaso.parsers.sqlite_plugins import test_lib + + +class LSQuarantinePluginTest(test_lib.SQLitePluginTestCase): + """Tests for the LS Quarantine database plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = ls_quarantine.LsQuarantinePlugin() + + def testProcess(self): + """Tests the Process function on a LS Quarantine database file.""" + test_file = self._GetTestFilePath(['quarantine.db']) + event_queue_consumer = self._ParseDatabaseFileWithPlugin( + self._plugin, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # The quarantine database contains 14 event_objects. + self.assertEquals(len(event_objects), 14) + + # Examine a VLC event. + event_object = event_objects[3] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-07-08 21:12:03') + self.assertEquals(event_object.timestamp, expected_timestamp) + + self.assertEquals(event_object.agent, u'Google Chrome') + vlc_url = ( + u'http://download.cnet.com/VLC-Media-Player/3001-2139_4-10210434.html' + u'?spi=40ab24d3c71594a5017d74be3b0c946c') + self.assertEquals(event_object.url, vlc_url) + + self.assertTrue(u'vlc-2.0.7-intel64.dmg' in event_object.data) + + # Examine a MacKeeper event. + event_object = event_objects[9] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-07-12 19:28:58') + self.assertEquals(event_object.timestamp, expected_timestamp) + + # Examine a SpeedTest event. + event_object = event_objects[10] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-07-12 19:30:16') + self.assertEquals(event_object.timestamp, expected_timestamp) + + speedtest_message = ( + u'[Google Chrome] Downloaded: http://mackeeperapp.zeobit.com/aff/' + u'speedtest.net.6/download.php?affid=460245286&trt=5&utm_campaign=' + u'3ES&tid_ext=P107fSKcSfqpMbcP3sI4fhKmeMchEB3dkAGpX4YIsvM;US;L;1 ' + u'') + speedtest_short = ( + u'http://mackeeperapp.zeobit.com/aff/speedtest.net.6/download.php?' + u'affid=4602452...') + + self._TestGetMessageStrings( + event_object, speedtest_message, speedtest_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/sqlite_plugins/mac_document_versions.py b/plaso/parsers/sqlite_plugins/mac_document_versions.py new file mode 100644 index 0000000..48336e5 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/mac_document_versions.py @@ -0,0 +1,114 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for the Mac OS X Document Versions files.""" + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.parsers import sqlite +from plaso.parsers.sqlite_plugins import interface + + +__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)' + + +class MacDocumentVersionsEvent(time_events.PosixTimeEvent): + """Convenience class for a entry from the Document Versions database.""" + + DATA_TYPE = 'mac:document_versions:file' + + def __init__(self, posix_time, name, path, version_path, last_time, user_sid): + """Initializes the event object. + + Args: + posix_time: The POSIX time value. + name: name of the original file. + path: path from the original file. + version_path: path to the version copy of the original file. + last_time: the system user ID of the user that opened the file. + user_sid: identification user ID that open the file. + """ + super(MacDocumentVersionsEvent, self).__init__( + posix_time, eventdata.EventTimestamp.CREATION_TIME) + + self.name = name + self.path = path + self.version_path = version_path + # TODO: shouldn't this be a separate event? + self.last_time = last_time + self.user_sid = unicode(user_sid) + + +class MacDocumentVersionsPlugin(interface.SQLitePlugin): + """Parse the Mac OS X Document Versions SQLite database..""" + + NAME = 'mac_document_versions' + DESCRIPTION = u'Parser for document revisions SQLite database files.' + + # Define the needed queries. + # name: name from the original file. + # path: path from the original file (include the file) + # last_time: last time when the file was replicated. + # version_path: path where the version is stored. + # version_time: the timestamp when the version was created. + QUERIES = [ + (('SELECT f.file_name AS name, f.file_path AS path, ' + 'f.file_last_seen AS last_time, g.generation_path AS version_path, ' + 'g.generation_add_time AS version_time FROM files f, generations g ' + 'WHERE f.file_storage_id = g.generation_storage_id;'), + 'DocumentVersionsRow')] + + # The required tables for the query. + REQUIRED_TABLES = frozenset(['files', 'generations']) + + # The SQL field path is the relative path from DocumentRevisions. + # For this reason the Path to the program has to be added at the beginning. + ROOT_VERSION_PATH = u'/.DocumentRevisions-V100/' + + def DocumentVersionsRow( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + **unused_kwargs): + """Parses a document versions row. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + """ + # version_path = "PerUser/UserID/xx/client_id/version_file" + # where PerUser and UserID are a real directories. + paths = row['version_path'].split(u'/') + if len(paths) < 2 or not paths[1].isdigit(): + user_sid = None + else: + user_sid = paths[1] + version_path = self.ROOT_VERSION_PATH + row['version_path'] + path, _, _ = row['path'].rpartition(u'/') + + event_object = MacDocumentVersionsEvent( + row['version_time'], row['name'], path, version_path, + row['last_time'], user_sid) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + +sqlite.SQLiteParser.RegisterPlugin(MacDocumentVersionsPlugin) diff --git a/plaso/parsers/sqlite_plugins/mac_document_versions_test.py b/plaso/parsers/sqlite_plugins/mac_document_versions_test.py new file mode 100644 index 0000000..0d2ed97 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/mac_document_versions_test.py @@ -0,0 +1,74 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Mac OS X Document Versions plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import mac_document_versions as mac_doc_rev_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers.sqlite_plugins import mac_document_versions +from plaso.parsers.sqlite_plugins import test_lib + + +class MacDocumentVersionsTest(test_lib.SQLitePluginTestCase): + """Tests for the Mac OS X Document Versions plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = mac_document_versions.MacDocumentVersionsPlugin() + + def testProcess(self): + """Tests the Process function on a Mac OS X Document Versions file.""" + test_file = self._GetTestFilePath(['document_versions.sql']) + event_queue_consumer = self._ParseDatabaseFileWithPlugin( + self._plugin, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 4) + + # Check the first page visited entry. + event_object = event_objects[0] + + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.CREATION_TIME) + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2014-01-21 02:03:00') + self.assertEquals(event_object.timestamp, expected_timestamp) + + self.assertEquals(event_object.name, u'Spain is beautiful.rtf') + self.assertEquals(event_object.path, u'/Users/moxilo/Documents') + self.assertEquals(event_object.user_sid, u'501') + expected_version_path = ( + u'/.DocumentRevisions-V100/PerUID/501/1/' + u'com.apple.documentVersions/' + u'08CFEB5A-5CDA-486F-AED5-EA35BF3EE4C2.rtf') + self.assertEquals(event_object.version_path, expected_version_path) + + expected_msg = ( + u'Version of [{0:s}] ({1:s}) stored in {2:s} by {3:s}'.format( + event_object.name, event_object.path, + event_object.version_path, event_object.user_sid)) + expected_short = u'Stored a document version of [{0:s}]'.format( + event_object.name) + self._TestGetMessageStrings(event_object, expected_msg, expected_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/sqlite_plugins/mackeeper_cache.py b/plaso/parsers/sqlite_plugins/mackeeper_cache.py new file mode 100644 index 0000000..ec06290 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/mackeeper_cache.py @@ -0,0 +1,229 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a parser for the Mac OS X MacKeeper cache database.""" + +import json + +from plaso.lib import event +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import sqlite +from plaso.parsers.sqlite_plugins import interface + + +def DictToList(data_dict): + """Take a dict object and return a list of strings back.""" + ret_list = [] + for key, value in data_dict.iteritems(): + if key in ('body', 'datetime', 'type', 'room', 'rooms', 'id'): + continue + ret_list.append(u'{0:s} = {1!s}'.format(key, value)) + + return ret_list + + +def ExtractJQuery(jquery_raw): + """Extract and return the data inside a JQuery as a dict object.""" + data_part = u'' + if not jquery_raw: + return {} + + if '[' in jquery_raw: + _, _, first_part = jquery_raw.partition('[') + data_part, _, _ = first_part.partition(']') + elif jquery_raw.startswith('//'): + _, _, first_part = jquery_raw.partition('{') + data_part = u'{{{0:s}'.format(first_part) + elif '({' in jquery_raw: + _, _, first_part = jquery_raw.partition('(') + data_part, _, _ = first_part.rpartition(')') + + if not data_part: + return {} + + try: + data_dict = json.loads(data_part) + except ValueError: + return {} + + return data_dict + + +def ParseChatData(data): + """Parse a chat comment data dict and return a parsed one back. + + Args: + data: A dict object that is parsed from the record. + + Returns: + A dict object to store the results in. + """ + data_store = {} + + if 'body' in data: + body = data.get('body', '').replace('\n', ' ') + if body.startswith('//') and '{' in body: + body_dict = ExtractJQuery(body) + title, _, _ = body.partition('{') + body = u'{0:s} <{1!s}>'.format(title[2:], DictToList(body_dict)) + else: + body = 'No text.' + + data_store['text'] = body + + room = data.get('rooms', None) + if not room: + room = data.get('room', None) + if room: + data_store['room'] = room + + data_store['id'] = data.get('id', None) + user = data.get('user', None) + if user: + try: + user_sid = int(user) + data_store['sid'] = user_sid + except (ValueError, TypeError): + data_store['user'] = user + + return data_store + + +class MacKeeperCacheEvent(event.EventObject): + """Convenience class for a MacKeeper Cache event.""" + DATA_TYPE = 'mackeeper:cache' + + def __init__(self, timestamp, description, identifier, url, data_dict): + """Initializes the event object. + + Args: + timestamp: A timestamp as a number of milliseconds since Epoch + or as a UTC string. + description: The description of the cache entry. + identifier: The row identifier. + url: The MacKeeper URL value that is stored in every event. + data_dict: A dict object with the descriptive information. + """ + super(MacKeeperCacheEvent, self).__init__() + + # Two different types of timestamps stored in log files. + if type(timestamp) in (int, long): + self.timestamp = timelib.Timestamp.FromJavaTime(timestamp) + else: + self.timestamp = timelib.Timestamp.FromTimeString(timestamp) + + self.timestamp_desc = eventdata.EventTimestamp.ADDED_TIME + self.description = description + self.offset = identifier + self.text = data_dict.get('text', None) + self.user_sid = data_dict.get('sid', None) + self.user_name = data_dict.get('user', None) + self.event_type = data_dict.get('event_type', None) + self.room = data_dict.get('room', None) + self.record_id = data_dict.get('id', None) + self.url = url + + +class MacKeeperCachePlugin(interface.SQLitePlugin): + """Plugin for the MacKeeper Cache database file.""" + + NAME = 'mackeeper_cache' + DESCRIPTION = u'Parser for MacKeeper Cache SQLite database files.' + + # Define the needed queries. + QUERIES = [(( + 'SELECT d.entry_ID AS id, d.receiver_data AS data, r.request_key, ' + 'r.time_stamp AS time_string FROM cfurl_cache_receiver_data d, ' + 'cfurl_cache_response r WHERE r.entry_ID = ' + 'd.entry_ID'), 'ParseReceiverData')] + + # The required tables. + REQUIRED_TABLES = frozenset([ + 'cfurl_cache_blob_data', 'cfurl_cache_receiver_data', + 'cfurl_cache_response']) + + def ParseReceiverData( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + **unused_kwargs): + """Parses a single row from the receiver and cache response table. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + """ + data = {} + key_url = row['request_key'] + + data_dict = {} + description = 'MacKeeper Entry' + # Check the URL, since that contains vital information about the type of + # event we are dealing with. + if key_url.endswith('plist'): + description = 'Configuration Definition' + data['text'] = 'Plist content added to cache.' + elif key_url.startswith('http://event.zeobit.com'): + description = 'MacKeeper Event' + try: + _, _, part = key_url.partition('?') + data['text'] = part.replace('&', ' ') + except UnicodeDecodeError: + data['text'] = 'N/A' + elif key_url.startswith('http://account.zeobit.com'): + description = 'Account Activity' + _, _, activity = key_url.partition('#') + if activity: + data['text'] = u'Action started: {0:s}'.format(activity) + else: + data['text'] = u'Unknown activity.' + elif key_url.startswith('http://support.') and 'chat' in key_url: + description = 'Chat ' + try: + jquery = unicode(row['data']) + except UnicodeDecodeError: + jquery = '' + + data_dict = ExtractJQuery(jquery) + data = ParseChatData(data_dict) + + data['entry_type'] = data_dict.get('type', '') + if data['entry_type'] == 'comment': + description += 'Comment' + elif data['entry_type'] == 'outgoing': + description += 'Outgoing Message' + elif data['entry_type'] == 'incoming': + description += 'Incoming Message' + else: + # Empty or not known entry type, generic status message. + description += 'Entry' + data['text'] = u';'.join(DictToList(data_dict)) + if not data['text']: + data['text'] = 'No additional data.' + + event_object = MacKeeperCacheEvent( + row['time_string'], description, row['id'], key_url, data) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + +sqlite.SQLiteParser.RegisterPlugin(MacKeeperCachePlugin) diff --git a/plaso/parsers/sqlite_plugins/mackeeper_cache_test.py b/plaso/parsers/sqlite_plugins/mackeeper_cache_test.py new file mode 100644 index 0000000..a139e43 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/mackeeper_cache_test.py @@ -0,0 +1,68 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the MacKeeper Cache database plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import mackeeper_cache as mackeeper_cache_formatter +from plaso.lib import timelib_test +from plaso.parsers.sqlite_plugins import mackeeper_cache +from plaso.parsers.sqlite_plugins import test_lib + + +class MacKeeperCachePluginTest(test_lib.SQLitePluginTestCase): + """Tests for the MacKeeper Cache database plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = mackeeper_cache.MacKeeperCachePlugin() + + def testProcess(self): + """Tests the Process function on a MacKeeper Cache database file.""" + test_file = self._GetTestFilePath(['mackeeper_cache.db']) + event_queue_consumer = self._ParseDatabaseFileWithPlugin( + self._plugin, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # The cache file contains 198 entries. + self.assertEquals(len(event_objects), 198) + + event_object = event_objects[41] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-07-12 19:30:31') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'Chat Outgoing Message : I have received your system scan report and ' + u'I will start analyzing it right now. [ URL: http://support.kromtech.' + u'net/chat/listen/12828340738351e0593f987450z40787/?client-id=51e0593f' + u'a1a24468673655&callback=jQuery183013571173651143909_1373657420912&_=' + u'1373657423647 Event ID: 16059074 Room: ' + u'12828340738351e0593f987450z40787 ]') + + expected_short = ( + u'I have received your system scan report and I will start analyzing ' + u'it right now.') + + self._TestGetMessageStrings(event_object, expected_msg, expected_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/sqlite_plugins/skype.py b/plaso/parsers/sqlite_plugins/skype.py new file mode 100644 index 0000000..de7701c --- /dev/null +++ b/plaso/parsers/sqlite_plugins/skype.py @@ -0,0 +1,492 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a basic Skype SQLite parser.""" + +import logging + +from plaso.events import time_events +from plaso.parsers import sqlite +from plaso.parsers.sqlite_plugins import interface + + +__author__ = 'Joaquin Moreno Garijo (bastionado@gmail.com)' + + +class SkypeChatEvent(time_events.PosixTimeEvent): + """Convenience class for a Skype event.""" + + DATA_TYPE = 'skype:event:chat' + + def __init__(self, row, to_account): + """Build a Skype Event from a single row. + + Args: + row: A row object (instance of sqlite3.Row) that contains the + extracted data from a single row in the database. + to_account: A string containing the accounts (excluding the + author) of the conversation. + """ + super(SkypeChatEvent, self).__init__( + row['timestamp'], 'Chat from Skype', self.DATA_TYPE) + + self.title = row['title'] + self.text = row['body_xml'] + self.from_account = u'{0:s} <{1:s}>'.format( + row['from_displayname'], row['author']) + self.to_account = to_account + + +class SkypeAccountEvent(time_events.PosixTimeEvent): + """Convenience class for account information.""" + + DATA_TYPE = 'skype:event:account' + + def __init__( + self, timestamp, usage, identifier, full_name, display_name, email, + country): + """Initialize the event. + + Args: + timestamp: The POSIX timestamp value. + usage: A string containing the description string of the timestamp. + identifier: The row identifier. + full_name: A string containing the full name of the Skype account holder. + display_name: A string containing the chosen display name of the account + holder. + email: A string containing the registered email address of the account + holder. + country: A string containing the chosen home country of the account + holder. + """ + super(SkypeAccountEvent, self).__init__(timestamp, usage) + + self.offset = identifier + self.username = u'{0:s} <{1:s}>'.format(full_name, display_name) + self.display_name = display_name + self.email = email + self.country = country + self.data_type = self.DATA_TYPE + + +class SkypeSMSEvent(time_events.PosixTimeEvent): + """Convenience EventObject for SMS.""" + + DATA_TYPE = 'skype:event:sms' + + def __init__(self, row, dst_number): + """Read the information related with the SMS. + + Args: + row: row form the sql query. + row['time_sms']: timestamp when the sms was send. + row['dstnum_sms']: number which receives the sms. + row['msg_sms']: text send to this sms. + dst_number: phone number where the user send the sms. + """ + super(SkypeSMSEvent, self).__init__( + row['time_sms'], 'SMS from Skype', self.DATA_TYPE) + + self.number = dst_number + self.text = row['msg_sms'] + + +class SkypeCallEvent(time_events.PosixTimeEvent): + """Convenience EventObject for the calls.""" + + DATA_TYPE = 'skype:event:call' + + def __init__(self, timestamp, call_type, user_start_call, + source, destination, video_conference): + """Contains information if the call was cancelled, accepted or finished. + + Args: + timestamp: the timestamp of the event. + call_type: WAITING, STARTED, FINISHED. + user_start_call: boolean, true indicates that the owner + account started the call. + source: the account which started the call. + destination: the account which gets the call. + video_conference: boolean, if is true it was a videoconference. + """ + + super(SkypeCallEvent, self).__init__( + timestamp, 'Call from Skype', self.DATA_TYPE) + + self.call_type = call_type + self.user_start_call = user_start_call + self.src_call = source + self.dst_call = destination + self.video_conference = video_conference + + +class SkypeTransferFileEvent(time_events.PosixTimeEvent): + """Evaluate the action of send a file.""" + + DATA_TYPE = 'skype:event:transferfile' + + def __init__(self, row, timestamp, action_type, source, destination): + """Actions related with sending files. + + Args: + row: + filepath: path from the file. + filename: name of the file. + filesize: size of the file. + timestamp: when the action happens. + action_type: GETSOLICITUDE, SENDSOLICITUDE, ACCEPTED, FINISHED. + source: The account that sent the file. + destination: The account that received the file. + """ + + super(SkypeTransferFileEvent, self).__init__( + timestamp, 'File transfer from Skype', self.DATA_TYPE) + + self.offset = row['id'] + self.action_type = action_type + self.source = source + self.destination = destination + self.transferred_filepath = row['filepath'] + self.transferred_filename = row['filename'] + try: + self.transferred_filesize = int(row['filesize']) + except ValueError: + logging.debug(u'Unknown filesize {0:s}'.format( + self.transferred_filename)) + self.transferred_filesize = 0 + + +class SkypePlugin(interface.SQLitePlugin): + """SQLite plugin for Skype main.db SQlite database file.""" + + NAME = 'skype' + DESCRIPTION = u'Parser for Skype SQLite database files.' + + # Queries for building cache. + QUERY_DEST_FROM_TRANSFER = ( + u'SELECT parent_id, partner_handle AS skypeid, ' + u'partner_dispname AS skypename FROM transfers') + QUERY_SOURCE_FROM_TRANSFER = ( + u'SELECT pk_id, partner_handle AS skypeid, ' + u'partner_dispname AS skypename FROM transfers') + + # Define the needed queries. + QUERIES = [ + (('SELECT c.id, c.participants, c.friendlyname AS title, ' + 'm.author AS author, m.from_dispname AS from_displayname, ' + 'm.body_xml, m.timestamp, c.dialog_partner FROM Chats c, Messages m ' + 'WHERE c.name = m.chatname'), 'ParseChat'), + (('SELECT id, fullname, given_displayname, emails, ' + 'country, profile_timestamp, authreq_timestamp, ' + 'lastonline_timestamp, mood_timestamp, sent_authrequest_time, ' + 'lastused_timestamp FROM Accounts'), 'ParseAccountInformation'), + (('SELECT id, target_numbers AS dstnum_sms, timestamp AS time_sms, ' + 'body AS msg_sms FROM SMSes'), 'ParseSMS'), + (('SELECT id, partner_handle, partner_dispname, offer_send_list, ' + 'starttime, accepttime, finishtime, filepath, filename, filesize, ' + 'status, parent_id, pk_id FROM Transfers'), 'ParseFileTransfer'), + (('SELECT c.id, cm.guid, c.is_incoming, ' + 'cm.call_db_id, cm.videostatus, c.begin_timestamp AS try_call, ' + 'cm.start_timestamp AS accept_call, cm.call_duration ' + 'FROM Calls c, CallMembers cm ' + 'WHERE c.id = cm.call_db_id;'), 'ParseCall')] + + # The required tables. + REQUIRED_TABLES = frozenset([ + 'Chats', 'Accounts', 'Conversations', 'Contacts', 'SMSes', 'Transfers', + 'CallMembers', 'Calls']) + + def ParseAccountInformation( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + **unused_kwargs): + """Parses the Accounts database. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + """ + if row['profile_timestamp']: + event_object = SkypeAccountEvent( + row['profile_timestamp'], u'Profile Changed', row['id'], + row['fullname'], row['given_displayname'], row['emails'], + row['country']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + if row['authreq_timestamp']: + event_object = SkypeAccountEvent( + row['authreq_timestamp'], u'Authenticate Request', row['id'], + row['fullname'], row['given_displayname'], row['emails'], + row['country']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + if row['lastonline_timestamp']: + event_object = SkypeAccountEvent( + row['lastonline_timestamp'], u'Last Online', row['id'], + row['fullname'], row['given_displayname'], row['emails'], + row['country']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + if row['mood_timestamp']: + event_object = SkypeAccountEvent( + row['mood_timestamp'], u'Mood Event', row['id'], + row['fullname'], row['given_displayname'], row['emails'], + row['country']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + if row['sent_authrequest_time']: + event_object = SkypeAccountEvent( + row['sent_authrequest_time'], u'Auth Request Sent', row['id'], + row['fullname'], row['given_displayname'], row['emails'], + row['country']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + if row['lastused_timestamp']: + event_object = SkypeAccountEvent( + row['lastused_timestamp'], u'Last Used', row['id'], + row['fullname'], row['given_displayname'], row['emails'], + row['country']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + def ParseChat( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + **unused_kwargs): + """Parses a chat message row. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + """ + to_account = '' + accounts = [] + participants = row['participants'].split(' ') + for participant in participants: + if participant != row['author']: + accounts.append(participant) + to_account = u', '.join(accounts) + + if not to_account: + if row['dialog_partner']: + to_account = row['dialog_partner'] + else: + to_account = u'Unknown User' + + event_object = SkypeChatEvent(row, to_account) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + def ParseSMS( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + **unused_kwargs): + """Parse SMS. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + """ + dst_number = row['dstnum_sms'].replace(' ', '') + + event_object = SkypeSMSEvent(row, dst_number) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + def ParseCall( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + **unused_kwargs): + """Parse the calls taking into accounts some rows. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + """ + try: + aux = row['guid'] + if aux: + aux_list = aux.split('-') + src_aux = aux_list[0] + dst_aux = aux_list[1] + else: + src_aux = u'Unknown [no GUID]' + dst_aux = u'Unknown [no GUID]' + except IndexError: + src_aux = u'Unknown [{0:s}]'.format(row['guid']) + dst_aux = u'Unknown [{0:s}]'.format(row['guid']) + + if row['is_incoming'] == '0': + user_start_call = True + source = src_aux + if row['ip_address']: + destination = u'{0:s} <{1:s}>'.format(dst_aux, row['ip_address']) + else: + destination = dst_aux + else: + user_start_call = False + source = src_aux + destination = dst_aux + + if row['videostatus'] == '3': + video_conference = True + else: + video_conference = False + + event_object = SkypeCallEvent( + row['try_call'], 'WAITING', user_start_call, source, destination, + video_conference) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + if row['accept_call']: + event_object = SkypeCallEvent( + row['accept_call'], 'ACCEPTED', user_start_call, source, destination, + video_conference) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + if row['call_duration']: + try: + timestamp = int(row['accept_call']) + int(row['call_duration']) + event_object = SkypeCallEvent( + timestamp, 'FINISHED', user_start_call, source, destination, + video_conference) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + except ValueError: + logging.debug(( + u'[{0:s}] Unable to determine when the call {1:s} was ' + u'finished.').format(self.NAME, row['id'])) + + def ParseFileTransfer( + self, parser_context, row, file_entry=None, parser_chain=None, cache=None, + database=None, query=None, **unused_kwargs): + """Parse the transfer files. + + There is no direct relationship between who sends the file and + who accepts the file. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: the row with all information related with the file transfers. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + cache: a cache object (instance of SQLiteCache). + database: A database object (instance of SQLiteDatabase). + """ + source_dict = cache.GetResults('source') + if not source_dict: + cursor = database.cursor + results = cursor.execute(self.QUERY_SOURCE_FROM_TRANSFER) + cache.CacheQueryResults( + results, 'source', 'pk_id', ('skypeid', 'skypename')) + source_dict = cache.GetResults('source') + + dest_dict = cache.GetResults('destination') + if not dest_dict: + cursor = database.cursor + results = cursor.execute(self.QUERY_DEST_FROM_TRANSFER) + cache.CacheQueryResults( + results, 'destination', 'parent_id', ('skypeid', 'skypename')) + dest_dict = cache.GetResults('destination') + + source = u'Unknown' + destination = u'Unknown' + + if row['parent_id']: + destination = u'{0:s} <{1:s}>'.format( + row['partner_handle'], row['partner_dispname']) + skype_id, skype_name = source_dict.get(row['parent_id'], [None, None]) + if skype_name: + source = u'{0:s} <{1:s}>'.format(skype_id, skype_name) + else: + source = u'{0:s} <{1:s}>'.format( + row['partner_handle'], row['partner_dispname']) + + if row['pk_id']: + skype_id, skype_name = dest_dict.get(row['pk_id'], [None, None]) + if skype_name: + destination = u'{0:s} <{1:s}>'.format(skype_id, skype_name) + + if row['status'] == 8: + if row['starttime']: + event_object = SkypeTransferFileEvent( + row, row['starttime'], 'GETSOLICITUDE', source, destination) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + if row['accepttime']: + event_object = SkypeTransferFileEvent( + row, row['accepttime'], 'ACCEPTED', source, destination) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + if row['finishtime']: + event_object = SkypeTransferFileEvent( + row, row['finishtime'], 'FINISHED', source, destination) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + elif row['status'] == 2 and row['starttime']: + event_object = SkypeTransferFileEvent( + row, row['starttime'], 'SENDSOLICITUDE', source, destination) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + +sqlite.SQLiteParser.RegisterPlugin(SkypePlugin) diff --git a/plaso/parsers/sqlite_plugins/skype_test.py b/plaso/parsers/sqlite_plugins/skype_test.py new file mode 100644 index 0000000..e183748 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/skype_test.py @@ -0,0 +1,158 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Skype main.db history database plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import skype as skype_formatter +from plaso.lib import timelib_test +from plaso.parsers import sqlite +from plaso.parsers.sqlite_plugins import skype +from plaso.parsers.sqlite_plugins import test_lib + + +class SkypePluginTest(test_lib.SQLitePluginTestCase): + """Tests for the Skype main.db history database plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = skype.SkypePlugin() + + def testProcess(self): + """Tests the Process function on a Skype History database file. + + The History file contains 24 events: + 4 call events + 4 transfers file events + 1 sms events + 15 chat events + + Events used: + id = 16 -> SMS + id = 22 -> Call + id = 18 -> File + id = 1 -> Chat + id = 14 -> ChatRoom + """ + test_file = self._GetTestFilePath(['skype_main.db']) + cache = sqlite.SQLiteCache() + event_queue_consumer = self._ParseDatabaseFileWithPlugin( + self._plugin, test_file, cache) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + calls = 0 + files = 0 + sms = 0 + chats = 0 + for event_object in event_objects: + if event_object.data_type == 'skype:event:call': + calls += 1 + if event_object.data_type == 'skype:event:transferfile': + files += 1 + if event_object.data_type == 'skype:event:sms': + sms += 1 + if event_object.data_type == 'skype:event:chat': + chats += 1 + + self.assertEquals(len(event_objects), 24) + self.assertEquals(files, 4) + self.assertEquals(sms, 1) + self.assertEquals(chats, 15) + self.assertEquals(calls, 3) + + # TODO: Split this up into separate functions for testing each type of + # event, eg: testSMS, etc. + sms_event_object = event_objects[16] + call_event_object = event_objects[22] + event_file = event_objects[18] + chat_event_object = event_objects[1] + chat_room_event_object = event_objects[14] + + # Test cache processing and format strings. + expected_msg = ( + u'Source: gen.beringer Destination: ' + u'european.bbq.competitor File: secret-project.pdf ' + u'[SENDSOLICITUDE]') + + self._TestGetMessageStrings( + event_objects[17], expected_msg, expected_msg[0:77] + '...') + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-07-01 22:14:22') + self.assertEquals(sms_event_object.timestamp, expected_timestamp) + text_sms = (u'If you want I can copy ' + u'some documents for you, ' + u'if you can pay it... ;)') + self.assertEquals(sms_event_object.text, text_sms) + number = u'+34123456789' + self.assertEquals(sms_event_object.number, number) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-10-24 21:49:35') + self.assertEquals(event_file.timestamp, expected_timestamp) + + action_type = u'GETSOLICITUDE' + self.assertEquals(event_file.action_type, action_type) + source = u'gen.beringer ' + self.assertEquals(event_file.source, source) + destination = u'european.bbq.competitor ' + self.assertEquals(event_file.destination, destination) + transferred_filename = u'secret-project.pdf' + self.assertEquals(event_file.transferred_filename, transferred_filename) + filepath = u'/Users/gberinger/Desktop/secret-project.pdf' + self.assertEquals(event_file.transferred_filepath, filepath) + self.assertEquals(event_file.transferred_filesize, 69986) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-07-30 21:27:11') + self.assertEquals(chat_event_object.timestamp, expected_timestamp) + + title = u'European Competitor | need to know if you got it..' + self.assertEquals(chat_event_object.title, title) + expected_msg = u'need to know if you got it this time.' + self.assertEquals(chat_event_object.text, expected_msg) + from_account = u'Gen Beringer ' + self.assertEquals(chat_event_object.from_account, from_account) + self.assertEquals(chat_event_object.to_account, u'european.bbq.competitor') + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-10-27 15:29:19') + self.assertEquals(chat_room_event_object.timestamp, expected_timestamp) + + title = u'European Competitor, Echo123' + self.assertEquals(chat_room_event_object.title, title) + expected_msg = u'He is our new employee' + self.assertEquals(chat_room_event_object.text, expected_msg) + from_account = u'European Competitor ' + self.assertEquals(chat_room_event_object.from_account, from_account) + to_account = u'gen.beringer, echo123' + self.assertEquals(chat_room_event_object.to_account, to_account) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-07-01 22:12:17') + self.assertEquals(call_event_object.timestamp, expected_timestamp) + + self.assertEquals(call_event_object.dst_call, u'european.bbq.competitor') + self.assertEquals(call_event_object.src_call, u'gen.beringer') + self.assertEquals(call_event_object.user_start_call, False) + self.assertEquals(call_event_object.video_conference, False) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/sqlite_plugins/test_lib.py b/plaso/parsers/sqlite_plugins/test_lib.py new file mode 100644 index 0000000..2720942 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/test_lib.py @@ -0,0 +1,63 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""SQLite database plugin related functions and classes for testing.""" + +from dfvfs.lib import definitions +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver import resolver as path_spec_resolver + +from plaso.engine import single_process +from plaso.parsers import sqlite +from plaso.parsers import test_lib + + +class SQLitePluginTestCase(test_lib.ParserTestCase): + """The unit test case for SQLite database plugins.""" + + def _ParseDatabaseFileWithPlugin( + self, plugin_object, path, cache=None, knowledge_base_values=None): + """Parses a file as a SQLite database with a specific plugin. + + Args: + plugin_object: The plugin object that is used to extract an event + generator. + path: The path to the SQLite database file. + cache: A cache object (instance of SQLiteCache). + knowledge_base_values: optional dict containing the knowledge base + values. The default is None. + + Returns: + An event object queue consumer object (instance of + TestEventObjectQueueConsumer). + """ + event_queue = single_process.SingleProcessQueue() + event_queue_consumer = test_lib.TestEventObjectQueueConsumer(event_queue) + + parse_error_queue = single_process.SingleProcessQueue() + + parser_context = self._GetParserContext( + event_queue, parse_error_queue, + knowledge_base_values=knowledge_base_values) + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=path) + file_entry = path_spec_resolver.Resolver.OpenFileEntry(path_spec) + + with sqlite.SQLiteDatabase(file_entry) as database: + plugin_object.Process(parser_context, cache=cache, database=database) + + return event_queue_consumer diff --git a/plaso/parsers/sqlite_plugins/zeitgeist.py b/plaso/parsers/sqlite_plugins/zeitgeist.py new file mode 100644 index 0000000..2ec998d --- /dev/null +++ b/plaso/parsers/sqlite_plugins/zeitgeist.py @@ -0,0 +1,84 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Plugin for the Zeitgeist SQLite database. + + Zeitgeist is a service which logs the user activities and events, anywhere + from files opened to websites visited and conversations. +""" + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.parsers import sqlite +from plaso.parsers.sqlite_plugins import interface + + +class ZeitgeistEvent(time_events.JavaTimeEvent): + """Convenience class for a Zeitgeist event.""" + + DATA_TYPE = 'zeitgeist:activity' + + def __init__(self, java_time, row_id, subject_uri): + """Initializes the event object. + + Args: + java_time: The Java time value. + row_id: The identifier of the corresponding row. + subject_uri: The Zeitgeist event. + """ + super(ZeitgeistEvent, self).__init__( + java_time, eventdata.EventTimestamp.UNKNOWN) + + self.offset = row_id + self.subject_uri = subject_uri + + +class ZeitgeistPlugin(interface.SQLitePlugin): + """SQLite plugin for Zeitgeist activity database.""" + + NAME = 'zeitgeist' + DESCRIPTION = u'Parser for Zeitgeist activity SQLite database files.' + + # TODO: Explore the database more and make this parser cover new findings. + + QUERIES = [ + ('SELECT id, timestamp, subj_uri FROM event_view', + 'ParseZeitgeistEventRow')] + + REQUIRED_TABLES = frozenset(['event', 'actor']) + + def ParseZeitgeistEventRow( + self, parser_context, row, file_entry=None, parser_chain=None, query=None, + **unused_kwargs): + """Parses zeitgeist event row. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: The row resulting from the query. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + query: Optional query string. The default is None. + """ + event_object = ZeitgeistEvent(row['timestamp'], row['id'], row['subj_uri']) + parser_context.ProduceEvent( + event_object, query=query, parser_chain=parser_chain, + file_entry=file_entry) + + +sqlite.SQLiteParser.RegisterPlugin(ZeitgeistPlugin) diff --git a/plaso/parsers/sqlite_plugins/zeitgeist_test.py b/plaso/parsers/sqlite_plugins/zeitgeist_test.py new file mode 100644 index 0000000..50fc454 --- /dev/null +++ b/plaso/parsers/sqlite_plugins/zeitgeist_test.py @@ -0,0 +1,61 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Zeitgeist activity database plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import zeitgeist as zeitgeist_formatter +from plaso.lib import timelib_test +from plaso.parsers.sqlite_plugins import test_lib +from plaso.parsers.sqlite_plugins import zeitgeist + + +class ZeitgeistPluginTest(test_lib.SQLitePluginTestCase): + """Tests for the Zeitgeist activity database plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = zeitgeist.ZeitgeistPlugin() + + def testProcess(self): + """Tests the Process function.""" + test_file = self._GetTestFilePath(['activity.sqlite']) + event_queue_consumer = self._ParseDatabaseFileWithPlugin( + self._plugin, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # The sqlite database contains 44 events. + self.assertEquals(len(event_objects), 44) + + # Check the first event. + event_object = event_objects[0] + + expected_subject_uri = u'application://rhythmbox.desktop' + self.assertEquals(event_object.subject_uri, expected_subject_uri) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-10-22 08:53:19.477') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = u'application://rhythmbox.desktop' + self._TestGetMessageStrings(event_object, expected_msg, expected_msg) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/sqlite_test.py b/plaso/parsers/sqlite_test.py new file mode 100644 index 0000000..3150618 --- /dev/null +++ b/plaso/parsers/sqlite_test.py @@ -0,0 +1,63 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the SQLite database parser.""" + +import unittest + +from plaso.parsers import sqlite +# Register plugins. +from plaso.parsers import sqlite_plugins # pylint: disable=unused-import + + +class SQLiteParserTest(unittest.TestCase): + """Tests for the SQLite database parser.""" + + def testGetPluginNames(self): + """Tests the GetPluginNames function.""" + all_plugin_names = sqlite.SQLiteParser.GetPluginNames() + + self.assertNotEquals(all_plugin_names, []) + + self.assertTrue('skype' in all_plugin_names) + self.assertTrue('chrome_history' in all_plugin_names) + self.assertTrue('firefox_history' in all_plugin_names) + + # Change the calculations of the parsers. + parser_filter_string = 'chrome_history, firefox_history, -skype' + plugin_names = sqlite.SQLiteParser.GetPluginNames( + parser_filter_string=parser_filter_string) + + self.assertEquals(len(plugin_names), 2) + self.assertFalse('skype' in plugin_names) + self.assertTrue('chrome_history' in plugin_names) + self.assertTrue('firefox_history' in plugin_names) + + # Test with a different plugin selection. + parser_filter_string = 'sqlite, -skype' + plugin_names = sqlite.SQLiteParser.GetPluginNames( + parser_filter_string=parser_filter_string) + + # This should result in all plugins EXCEPT the skype one. + self.assertEquals(len(plugin_names), len(all_plugin_names) - 1) + self.assertFalse('skype' in plugin_names) + self.assertTrue('chrome_history' in plugin_names) + self.assertTrue('firefox_history' in plugin_names) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/symantec.py b/plaso/parsers/symantec.py new file mode 100644 index 0000000..21cffa6 --- /dev/null +++ b/plaso/parsers/symantec.py @@ -0,0 +1,153 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a Symantec parser in plaso.""" + +from plaso.events import text_events +from plaso.lib import timelib +from plaso.parsers import manager +from plaso.parsers import text_parser + +import pytz + + +__author__ = 'David Nides (david.nides@gmail.com)' + + +class SymantecEvent(text_events.TextEvent): + """Convenience class for a Symantec line event.""" + DATA_TYPE = 'av:symantec:scanlog' + + +class SymantecParser(text_parser.TextCSVParser): + """Parse Symantec AV Corporate Edition and Endpoint Protection log files.""" + + NAME = 'symantec_scanlog' + DESCRIPTION = u'Parser for Symantec Anti-Virus log files.' + + # Define the columns that make up the structure of a Symantec log file. + # http://www.symantec.com/docs/TECH100099 + COLUMNS = [ + 'time', 'event', 'cat', 'logger', 'computer', 'user', + 'virus', 'file', 'action1', 'action2', 'action0', 'virustype', + 'flags', 'description', 'scanid', 'new_ext', 'groupid', + 'event_data', 'vbin_id', 'virus_id', 'quarfwd_status', + 'access', 'snd_status', 'compressed', 'depth', 'still_infected', + 'definfo', 'defseqnumber', 'cleaninfo', 'deleteinfo', + 'backup_id', 'parent', 'guid', 'clientgroup', 'address', + 'domainname', 'ntdomain', 'macaddr', 'version:', + 'remote_machine', 'remote_machine_ip', 'action1_status', + 'action2_status', 'license_feature_name', 'license_feature_ver', + 'license_serial_num', 'license_fulfillment_id', 'license_start_dt', + 'license_expiration_dt', 'license_lifecycle', 'license_seats_total', + 'license_seats', 'err_code', 'license_seats_delta', 'status', + 'domain_guid', 'log_session_guid', 'vbin_session_id', + 'login_domain', 'extra'] + + def _GetTimestamp(self, timestamp_raw, timezone=pytz.utc): + """Return a 64-bit signed timestamp value in micro seconds since Epoch. + + The timestamp consists of six hexadecimal octets. + They represent the following: + First octet: Number of years since 1970 + Second octet: Month, where January = 0 + Third octet: Day + Fourth octet: Hour + Fifth octet: Minute + Sixth octet: Second + + For example, 200A13080122 represents November 19, 2002, 8:01:34 AM. + + Args: + timestamp_raw: The hexadecimal encoded timestamp value. + timezone: Optional timezone (instance of pytz.timezone). + The default is UTC. + + Returns: + A plaso timestamp value, micro seconds since Epoch in UTC. + """ + if timestamp_raw == '': + return 0 + + year, month, day, hours, minutes, seconds = ( + int(x[0] + x[1], 16) for x in zip( + timestamp_raw[::2], timestamp_raw[1::2])) + + return timelib.Timestamp.FromTimeParts( + year + 1970, month + 1, day, hours, minutes, seconds, timezone=timezone) + + def VerifyRow(self, parser_context, row): + """Verify a single line of a Symantec log file. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: A single row from the CSV file. + + Returns: + True if this is the correct parser, False otherwise. + """ + try: + timestamp = self._GetTimestamp(row['time'], parser_context.timezone) + except (TypeError, ValueError): + return False + + if not timestamp: + return False + + # Check few entries. + try: + my_event = int(row['event']) + except TypeError: + return False + + if my_event < 1 or my_event > 77: + return False + + try: + category = int(row['cat']) + except TypeError: + return False + + if category < 1 or category > 4: + return False + + return True + + def ParseRow( + self, parser_context, row_offset, row, file_entry=None, + parser_chain=None): + """Parses a row and extract event objects. + + Args: + parser_context: A parser context object (instance of ParserContext). + row_offset: The offset of the row. + row: A dictionary containing all the fields as denoted in the + COLUMNS class list. + file_entry: optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + timestamp = self._GetTimestamp(row['time'], parser_context.timezone) + + # TODO: Create new dict object that only contains valuable attributes. + event_object = SymantecEvent(timestamp, row_offset, row) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +manager.ParsersManager.RegisterParser(SymantecParser) diff --git a/plaso/parsers/symantec_test.py b/plaso/parsers/symantec_test.py new file mode 100644 index 0000000..aef99f1 --- /dev/null +++ b/plaso/parsers/symantec_test.py @@ -0,0 +1,93 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Symantec AV Log parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import symantec as symantec_formatter +from plaso.lib import timelib_test +from plaso.parsers import symantec +from plaso.parsers import test_lib + +import pytz + + +class SymantecAccessProtectionUnitTest(test_lib.ParserTestCase): + """Tests for the Symantec AV Log parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = symantec.SymantecParser() + + def testGetTimestamp(self): + """Tests the _GetTimestamp function.""" + # pylint: disable=protected-access + timestamp = self._parser._GetTimestamp('200A13080122', timezone=pytz.UTC) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2002-11-19 08:01:34') + self.assertEquals(timestamp, expected_timestamp) + + timestamp = self._parser._GetTimestamp('2A0A1E0A2F1D', timezone=pytz.UTC) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-11-30 10:47:29') + self.assertEquals(timestamp, expected_timestamp) + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['Symantec.Log']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # The file contains 8 lines which should result in 8 event objects. + self.assertEquals(len(event_objects), 8) + + # Test the second entry: + event_object = event_objects[1] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-11-30 10:47:29') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals(event_object.user, u'davnads') + expected_file = ( + u'D:\\Twinkle_Prod$\\VM11 XXX\\outside\\test.exe.txt') + self.assertEquals(event_object.file, expected_file) + + expected_msg = ( + u'Event Name: GL_EVENT_INFECTION; ' + u'Category Name: GL_CAT_INFECTION; ' + u'Malware Name: W32.Changeup!gen33; ' + u'Malware Path: ' + u'D:\\Twinkle_Prod$\\VM11 XXX\\outside\\test.exe.txt; ' + u'Action0: Unknown; ' + u'Action1: Clean virus from file; ' + u'Action2: Delete infected file; ' + u'Scan ID: 0; ' + u'Event Data: 201\t4\t6\t1\t65542\t0\t0\t0\t0\t0\t0') + expected_msg_short = ( + u'D:\\Twinkle_Prod$\\VM11 XXX\\outside\\test.exe.txt; ' + u'W32.Changeup!gen33; ' + u'Unknown; ...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/syslog.py b/plaso/parsers/syslog.py new file mode 100644 index 0000000..cf0c9df --- /dev/null +++ b/plaso/parsers/syslog.py @@ -0,0 +1,209 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a syslog parser in plaso.""" + +import datetime +import logging + +from plaso.events import text_events +from plaso.lib import lexer +from plaso.lib import timelib +from plaso.lib import utils +from plaso.parsers import manager +from plaso.parsers import text_parser + + +class SyslogLineEvent(text_events.TextEvent): + """Convenience class for a syslog line event.""" + DATA_TYPE = 'syslog:line' + + +class SyslogParser(text_parser.SlowLexicalTextParser): + """Parse text based syslog files.""" + + NAME = 'syslog' + DESCRIPTION = u'Parser for syslog files.' + + # TODO: can we change this similar to SQLite where create an + # event specific object for different lines using a callback function. + # Define the tokens that make up the structure of a syslog file. + tokens = [ + lexer.Token('INITIAL', + '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ', + 'SetMonth', 'DAY'), + lexer.Token('DAY', r'\s?(\d{1,2})\s+', 'SetDay', 'TIME'), + lexer.Token('TIME', r'([0-9:\.]+) ', 'SetTime', 'STRING_HOST'), + lexer.Token('STRING_HOST', r'^--(-)', 'ParseHostname', 'STRING'), + lexer.Token('STRING_HOST', r'([^\s]+) ', 'ParseHostname', 'STRING_PID'), + lexer.Token('STRING_PID', r'([^\:\n]+)', 'ParsePid', 'STRING'), + lexer.Token('STRING', r'([^\n]+)', 'ParseString', ''), + lexer.Token('STRING', r'\n\t', None, ''), + lexer.Token('STRING', r'\t', None, ''), + lexer.Token('STRING', r'\n', 'ParseMessage', 'INITIAL'), + lexer.Token('.', '([^\n]+)\n', 'ParseIncomplete', 'INITIAL'), + lexer.Token('.', '\n[^\t]', 'ParseIncomplete', 'INITIAL'), + lexer.Token('S[.]+', '(.+)', 'ParseString', ''), + ] + + def __init__(self): + """Initializes a syslog parser object.""" + super(SyslogParser, self).__init__(local_zone=True) + # Set the initial year to 0 (fixed in the actual Parse method) + self._year_use = 0 + self._last_month = 0 + + # Set some additional attributes. + self.attributes['reporter'] = '' + self.attributes['pid'] = '' + + def _GetYear(self, stat, timezone): + """Retrieves the year either from the input file or from the settings.""" + time = getattr(stat, 'crtime', 0) + if not time: + time = getattr(stat, 'ctime', 0) + + if not time: + current_year = timelib.GetCurrentYear() + logging.error(( + u'Unable to determine year of syslog file.\nDefautling to: ' + u'{0:d}').format(current_year)) + return current_year + + try: + timestamp = datetime.datetime.fromtimestamp(time, timezone) + except ValueError as exception: + current_year = timelib.GetCurrentYear() + logging.error( + u'Unable to determine year of syslog file with error: {0:s}\n' + u'Defaulting to: {1:d}'.format(exception, current_year)) + return current_year + + return timestamp.year + + def ParseLine(self, parser_context): + """Parse a single line from the syslog file. + + This method extends the one from TextParser slightly, adding + the context of the reporter and pid values found inside syslog + files. + + Args: + parser_context: A parser context object (instance of ParserContext). + + Returns: + An event object (instance of TextEvent). + """ + # Note this an older comment applying to a similar approach previously + # the init function. + # TODO: this is a HACK to get the tests working let's discuss this. + if not self._year_use: + self._year_use = parser_context.year + + if not self._year_use: + # TODO: Find a decent way to actually calculate the correct year + # from the syslog file, instead of relying on stats object. + stat = self.file_entry.GetStat() + self._year_use = self._GetYear(stat, parser_context.timezone) + + if not self._year_use: + # TODO: Make this sensible, not have the year permanent. + self._year_use = 2012 + + month_compare = int(self.attributes['imonth']) + if month_compare and self._last_month > month_compare: + self._year_use += 1 + + self._last_month = int(self.attributes['imonth']) + + self.attributes['iyear'] = self._year_use + + return super(SyslogParser, self).ParseLine(parser_context) + + def ParseHostname(self, match=None, **unused_kwargs): + """Parses the hostname. + + This is a callback function for the text parser (lexer) and is + called by the STRING_HOST lexer state. + + Args: + match: The regular expression match object. + """ + self.attributes['hostname'] = match.group(1) + + def ParsePid(self, match=None, **unused_kwargs): + """Parses the process identifier (PID). + + This is a callback function for the text parser (lexer) and is + called by the STRING_PID lexer state. + + Args: + match: The regular expression match object. + """ + # TODO: Change this logic and rather add more Tokens that + # fully cover all variations of the various PID stages. + line = match.group(1) + if line[-1] == ']': + splits = line.split('[') + if len(splits) == 2: + self.attributes['reporter'], pid = splits + else: + pid = splits[-1] + self.attributes['reporter'] = '['.join(splits[:-1]) + try: + self.attributes['pid'] = int(pid[:-1]) + except ValueError: + self.attributes['pid'] = 0 + else: + self.attributes['reporter'] = line + + def ParseString(self, match=None, **unused_kwargs): + """Parses a (body text) string. + + This is a callback function for the text parser (lexer) and is + called by the STRING lexer state. + + Args: + match: The regular expression match object. + """ + self.attributes['body'] += utils.GetUnicodeString(match.group(1)) + + def PrintLine(self): + """Prints a log line.""" + self.attributes['iyear'] = 2012 + return super(SyslogParser, self).PrintLine() + + # TODO: this is a rough initial implementation to get this working. + def CreateEvent(self, timestamp, offset, attributes): + """Creates a syslog line event. + + This overrides the default function in TextParser to create + syslog line events instead of text events. + + Args: + timestamp: The timestamp time value. The timestamp contains the + number of microseconds since Jan 1, 1970 00:00:00 UTC. + offset: The offset of the event. + attributes: A dict that contains the events attributes. + + Returns: + A text event (SyslogLineEvent). + """ + return SyslogLineEvent(timestamp, offset, attributes) + + +manager.ParsersManager.RegisterParser(SyslogParser) diff --git a/plaso/parsers/syslog_test.py b/plaso/parsers/syslog_test.py new file mode 100644 index 0000000..dd6f884 --- /dev/null +++ b/plaso/parsers/syslog_test.py @@ -0,0 +1,76 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the syslog parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import syslog as syslog_formatter +from plaso.lib import timelib_test +from plaso.parsers import syslog +from plaso.parsers import test_lib + + +class SyslogUnitTest(test_lib.ParserTestCase): + """Tests for the syslog parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = syslog.SyslogParser() + + def testParse(self): + """Tests the Parse function.""" + knowledge_base_values = {'year': 2012} + test_file = self._GetTestFilePath(['syslog']) + event_queue_consumer = self._ParseFile( + self._parser, test_file, knowledge_base_values=knowledge_base_values) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 13) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-01-22 07:52:33') + self.assertEquals(event_objects[0].timestamp, expected_timestamp) + self.assertEquals(event_objects[0].hostname, 'myhostname.myhost.com') + + expected_string = ( + u'[client, pid: 30840] : INFO No new content.') + self._TestGetMessageStrings( + event_objects[0], expected_string, expected_string) + + expected_msg = ( + '[aprocess, pid: 101001] : This is a multi-line message that screws up' + 'many syslog parsers.') + expected_msg_short = ( + '[aprocess, pid: 101001] : This is a multi-line message that screws up' + 'many sys...') + self._TestGetMessageStrings( + event_objects[11], expected_msg, expected_msg_short) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-02-29 01:15:43') + self.assertEquals(event_objects[6].timestamp, expected_timestamp) + + # Testing year increment. + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-03-23 23:01:18') + self.assertEquals(event_objects[8].timestamp, expected_timestamp) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/test_lib.py b/plaso/parsers/test_lib.py new file mode 100644 index 0000000..1fc7f9d --- /dev/null +++ b/plaso/parsers/test_lib.py @@ -0,0 +1,234 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser related functions and classes for testing.""" + +import os +import unittest + +from dfvfs.lib import definitions +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver import resolver as path_spec_resolver + +from plaso.artifacts import knowledge_base +from plaso.engine import queue +from plaso.engine import single_process +from plaso.formatters import manager as formatters_manager +from plaso.lib import event +from plaso.parsers import context + + +class TestEventObjectQueueConsumer(queue.EventObjectQueueConsumer): + """Class that implements a list event object queue consumer.""" + + def __init__(self, event_queue): + """Initializes the list event object queue consumer. + + Args: + event_queue: the event object queue (instance of Queue). + """ + super(TestEventObjectQueueConsumer, self).__init__(event_queue) + self.event_objects = [] + + def _ConsumeEventObject(self, event_object, **unused_kwargs): + """Consumes an event object callback for ConsumeEventObjects.""" + self.event_objects.append(event_object) + + +class ParserTestCase(unittest.TestCase): + """The unit test case for a parser.""" + + _TEST_DATA_PATH = os.path.join(os.getcwd(), 'test_data') + + # Show full diff results, part of TestCase so does not follow our naming + # conventions. + maxDiff = None + + def _GetEventObjects(self, event_generator): + """Retrieves the event objects from the event generator. + + This function will extract event objects from a generator. + + Args: + event_generator: the event generator as returned by the parser. + + Returns: + A list of event objects (instances of EventObject). + """ + event_objects = [] + + for event_object in event_generator: + self.assertIsInstance(event_object, event.EventObject) + event_objects.append(event_object) + + return event_objects + + def _GetEventObjectsFromQueue(self, event_queue_consumer): + """Retrieves the event objects from the queue consumer. + + Args: + event_queue_consumer: the event object queue consumer object (instance of + TestEventObjectQueueConsumer). + + Returns: + A list of event objects (instances of EventObject). + """ + event_queue_consumer.ConsumeEventObjects() + + event_objects = [] + for event_object in event_queue_consumer.event_objects: + self.assertIsInstance(event_object, event.EventObject) + event_objects.append(event_object) + + return event_objects + + def _GetParserContext( + self, event_queue, parse_error_queue, knowledge_base_values=None): + """Retrieves a parser context object. + + Args: + event_queue: the event queue (instance of Queue). + parse_error_queue: the parse error queue (instance of Queue). + knowledge_base_values: optional dict containing the knowledge base + values. The default is None. + + Returns: + A parser context object (instance of ParserContext). + """ + event_queue_producer = queue.ItemQueueProducer(event_queue) + parse_error_queue_producer = queue.ItemQueueProducer(parse_error_queue) + + knowledge_base_object = knowledge_base.KnowledgeBase() + if knowledge_base_values: + for identifier, value in knowledge_base_values.iteritems(): + knowledge_base_object.SetValue(identifier, value) + + return context.ParserContext( + event_queue_producer, parse_error_queue_producer, + knowledge_base_object) + + def _GetTestFilePath(self, path_segments): + """Retrieves the path of a test file relative to the test data directory. + + Args: + path_segments: the path segments inside the test data directory. + + Returns: + A path of the test file. + """ + # Note that we need to pass the individual path segments to os.path.join + # and not a list. + return os.path.join(self._TEST_DATA_PATH, *path_segments) + + def _GetTestFileEntryFromPath(self, path_segments): + """Creates a dfVFS file_entry that references a file in the test dir. + + Args: + path_segments: the path segments inside the test data directory. + + Returns: + A dfVFS file_entry object. + """ + path = self._GetTestFilePath(path_segments) + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=path) + file_entry = path_spec_resolver.Resolver.OpenFileEntry(path_spec) + return file_entry + + + def _ParseFile(self, parser_object, path, knowledge_base_values=None): + """Parses a file using the parser object. + + Args: + parser_object: the parser object. + path: the path of the file to parse. + knowledge_base_values: optional dict containing the knowledge base + values. The default is None. + + Returns: + An event object queue consumer object (instance of + TestEventObjectQueueConsumer). + """ + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=path) + return self._ParseFileByPathSpec( + parser_object, path_spec, knowledge_base_values=knowledge_base_values) + + def _ParseFileByPathSpec( + self, parser_object, path_spec, knowledge_base_values=None): + """Parses a file using the parser object. + + Args: + parser_object: the parser object. + path_spec: the path specification of the file to parse. + knowledge_base_values: optional dict containing the knowledge base + values. The default is None. + + Returns: + An event object queue consumer object (instance of + TestEventObjectQueueConsumer). + """ + event_queue = single_process.SingleProcessQueue() + event_queue_consumer = TestEventObjectQueueConsumer(event_queue) + + parse_error_queue = single_process.SingleProcessQueue() + + parser_context = self._GetParserContext( + event_queue, parse_error_queue, + knowledge_base_values=knowledge_base_values) + file_entry = path_spec_resolver.Resolver.OpenFileEntry(path_spec) + parser_object.Parse(parser_context, file_entry) + + return event_queue_consumer + + def _TestGetMessageStrings( + self, event_object, expected_message, expected_message_short): + """Tests the formatting of the message strings. + + This function invokes the GetMessageStrings function of the event + formatter on the event object and compares the resulting messages + strings with those expected. + + Args: + event_object: the event object (instance of EventObject). + expected_message: the expected message string. + expected_message_short: the expected short message string. + """ + manager_object = formatters_manager.EventFormatterManager + message, message_short = manager_object.GetMessageStrings(event_object) + self.assertEquals(message, expected_message) + self.assertEquals(message_short, expected_message_short) + + def _TestGetSourceStrings( + self, event_object, expected_source, expected_source_short): + """Tests the formatting of the source strings. + + This function invokes the GetSourceStrings function of the event + formatter on the event object and compares the resulting source + strings with those expected. + + Args: + event_object: the event object (instance of EventObject). + expected_source: the expected source string. + expected_source_short: the expected short source string. + """ + manager_object = formatters_manager.EventFormatterManager + # TODO: change this to return the long variant first so it is consistent + # with GetMessageStrings. + source_short, source = manager_object.GetSourceStrings(event_object) + self.assertEquals(source, expected_source) + self.assertEquals(source_short, expected_source_short) diff --git a/plaso/parsers/text_parser.py b/plaso/parsers/text_parser.py new file mode 100644 index 0000000..3f1ee70 --- /dev/null +++ b/plaso/parsers/text_parser.py @@ -0,0 +1,1099 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a class to provide a parsing framework to plaso. + +This class contains a base framework class for parsing fileobjects, and +also some implementations that extend it to provide a more comprehensive +parser. +""" + +import abc +import csv +import logging +import os + +from dfvfs.helpers import text_file +import pyparsing + +from plaso.events import text_events +from plaso.lib import errors +from plaso.lib import event +from plaso.lib import lexer +from plaso.lib import timelib +from plaso.lib import utils +from plaso.parsers import interface + +import pytz + +# Pylint complains about some functions not being implemented that shouldn't +# be since they need to be implemented by children. +# pylint: disable=abstract-method + + +class SlowLexicalTextParser(interface.BaseParser, lexer.SelfFeederMixIn): + """Generic text based parser that uses lexer to assist with parsing. + + This text parser is based on a rather slow lexer, which makes the + use of this interface highly discouraged. Parsers that already + implement it will most likely all be rewritten to support faster + text parsing implementations. + + This text based parser needs to be extended to provide an accurate + list of tokens that define the structure of the log file that the + parser is designed for. + """ + + # Define the max number of lines before we determine this is + # not the correct parser. + MAX_LINES = 15 + + # List of tokens that describe the structure of the log file. + tokens = [ + lexer.Token('INITIAL', '(.+)\n', 'ParseString', ''), + ] + + def __init__(self, local_zone=True): + """Constructor for the SlowLexicalTextParser. + + Args: + local_zone: A boolean value that determines if the entries + in the log file are stored in the local time + zone of the computer that stored it or in a fixed + timezone, like UTC. + """ + # TODO: remove the multiple inheritance. + lexer.SelfFeederMixIn.__init__(self) + interface.BaseParser.__init__(self) + self.line_ready = False + self.attributes = { + 'body': '', + 'iyear': 0, + 'imonth': 0, + 'iday': 0, + 'time': '', + 'hostname': '', + 'username': '', + } + self.local_zone = local_zone + self.file_entry = None + + def ClearValues(self): + """Clears all the values inside the attributes dict. + + All values that start with the letter 'i' are considered + to be an integer, otherwise string value is assumed. + """ + self.line_ready = False + for attr in self.attributes: + if attr[0] == 'i': + self.attributes[attr] = 0 + else: + self.attributes[attr] = '' + + def ParseIncomplete(self, match=None, **unused_kwargs): + """Indication that we've got a partial line to match against. + + Args: + match: The regular expression match object. + """ + self.attributes['body'] += match.group(0) + self.line_ready = True + + def ParseMessage(self, **unused_kwargs): + """Signal that a line is ready to be parsed.""" + self.line_ready = True + + def SetMonth(self, match=None, **unused_kwargs): + """Parses the month. + + This is a callback function for the text parser (lexer) and is + called by the corresponding lexer state. + + Args: + match: The regular expression match object. + """ + self.attributes['imonth'] = int( + timelib.MONTH_DICT.get(match.group(1).lower(), 1)) + + def SetDay(self, match=None, **unused_kwargs): + """Parses the day of the month. + + This is a callback function for the text parser (lexer) and is + called by the corresponding lexer state. + + Args: + match: The regular expression match object. + """ + self.attributes['iday'] = int(match.group(1)) + + def SetTime(self, match=None, **unused_kwargs): + """Set the time attribute. + + Args: + match: The regular expression match object. + """ + self.attributes['time'] = match.group(1) + + def SetYear(self, match=None, **unused_kwargs): + """Parses the year. + + This is a callback function for the text parser (lexer) and is + called by the corresponding lexer state. + + Args: + match: The regular expression match object. + """ + self.attributes['iyear'] = int(match.group(1)) + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract data from a text file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Raises: + UnableToParseFile: when the file cannot be parsed. + """ + path_spec_printable = u'{0:s}:{1:s}'.format( + file_entry.path_spec.type_indicator, file_entry.name) + file_object = file_entry.GetFileObject() + + self.file_entry = file_entry + # TODO: this is necessary since we inherit from lexer.SelfFeederMixIn. + self.file_object = file_object + + # Start by checking, is this a text file or not? Before we proceed + # any further. + file_object.seek(0, os.SEEK_SET) + if not utils.IsText(file_object.read(40)): + raise errors.UnableToParseFile(u'Not a text file, unable to proceed.') + + file_object.seek(0, os.SEEK_SET) + + error_count = 0 + file_verified = False + # We need to clear out few values in the Lexer before continuing. + # There might be some leftovers from previous run. + self.error = 0 + self.buffer = '' + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + while True: + _ = self.NextToken() + + if self.state == 'INITIAL': + self.entry_offset = getattr(self, 'next_entry_offset', 0) + self.next_entry_offset = file_object.tell() - len(self.buffer) + + if not file_verified and self.error >= self.MAX_LINES * 2: + logging.debug( + u'Lexer error count: {0:d} and current state {1:s}'.format( + self.error, self.state)) + file_object.close() + raise errors.UnableToParseFile( + u'[{0:s}] unsupported file: {1:s}.'.format( + self.NAME, path_spec_printable)) + + if self.line_ready: + try: + event_object = self.ParseLine(parser_context) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + file_verified = True + + except errors.TimestampNotCorrectlyFormed as exception: + error_count += 1 + if file_verified: + logging.debug( + u'[{0:s} VERIFIED] Error count: {1:d} and ERROR: {2:d}'.format( + path_spec_printable, error_count, self.error)) + logging.warning( + u'[{0:s}] Unable to parse timestamp with error: {1:s}'.format( + self.NAME, exception)) + + else: + logging.debug(( + u'[{0:s} EVALUATING] Error count: {1:d} and ERROR: ' + u'{2:d})').format(path_spec_printable, error_count, self.error)) + + if error_count >= self.MAX_LINES: + file_object.close() + raise errors.UnableToParseFile( + u'[{0:s}] unsupported file: {1:s}.'.format( + self.NAME, path_spec_printable)) + + finally: + self.ClearValues() + + if self.Empty(): + # Try to fill the buffer to prevent the parser from ending prematurely. + self.Feed() + + if self.Empty(): + break + + if not file_verified: + file_object.close() + raise errors.UnableToParseFile( + u'[{0:s}] unable to parser file: {1:s}.'.format( + self.NAME, path_spec_printable)) + + file_offset = file_object.get_offset() + if file_offset < file_object.get_size(): + logging.error(( + u'{0:s} prematurely terminated parsing: {1:s} at offset: ' + u'0x{2:08x}.').format( + self.NAME, path_spec_printable, file_offset)) + file_object.close() + + def ParseString(self, match=None, **unused_kwargs): + """Return a string with combined values from the lexer. + + Args: + match: The regular expression match object. + + Returns: + A string that combines the values that are so far + saved from the lexer. + """ + try: + self.attributes['body'] += match.group(1).strip('\n') + except IndexError: + self.attributes['body'] += match.group(0).strip('\n') + + def PrintLine(self): + """"Return a string with combined values from the lexer.""" + year = getattr(self.attributes, 'iyear', None) + month = getattr(self.attributes, 'imonth', None) + day = getattr(self.attributes, 'iday', None) + + if None in [year, month, day]: + date_string = u'[DATE NOT SET]' + else: + try: + year = int(year, 10) + month = int(month, 10) + day = int(day, 10) + + date_string = u'{0:04d}-{1:02d}-{2:02d}'.format(year, month, day) + except ValueError: + date_string = u'[DATE INVALID]' + + time_string = getattr(self.attributes, 'time', u'[TIME NOT SET]') + hostname_string = getattr(self.attributes, 'hostname', u'HOSTNAME NOT SET') + reporter_string = getattr( + self.attributes, 'reporter', u'[REPORTER NOT SET]') + body_string = getattr(self.attributes, 'body', u'[BODY NOT SET]') + + # TODO: this is a work in progress. The reason for the try-catch is that + # the text parser is handed a non-text file and must deal with converting + # arbitrary binary data. + try: + line = u'{0:s} {1:s} [{2:s}] {3:s} => {4:s}'.format( + date_string, time_string, hostname_string, reporter_string, + body_string) + except UnicodeError: + line = 'Unable to print line - due to encoding error.' + + return line + + def ParseLine(self, parser_context): + """Return an event object extracted from the current line. + + Args: + parser_context: A parser context object (instance of ParserContext). + + Returns: + An event object (instance of TextEvent). + """ + if not self.attributes['time']: + raise errors.TimestampNotCorrectlyFormed( + u'Unable to parse timestamp, time not set.') + + if not self.attributes['iyear']: + raise errors.TimestampNotCorrectlyFormed( + u'Unable to parse timestamp, year not set.') + + times = self.attributes['time'].split(':') + if self.local_zone: + timezone = parser_context.timezone + else: + timezone = pytz.UTC + + if len(times) < 3: + raise errors.TimestampNotCorrectlyFormed(( + u'Unable to parse timestamp, not of the format HH:MM:SS ' + u'[{0:s}]').format(self.PrintLine())) + try: + secs = times[2].split('.') + if len(secs) == 2: + sec, us = secs + else: + sec = times[2] + us = 0 + + timestamp = timelib.Timestamp.FromTimeParts( + int(self.attributes['iyear']), self.attributes['imonth'], + self.attributes['iday'], int(times[0]), int(times[1]), + int(sec), microseconds=int(us), timezone=timezone) + + except ValueError as exception: + raise errors.TimestampNotCorrectlyFormed( + u'Unable to parse: {0:s} with error: {1:s}'.format( + self.PrintLine(), exception)) + + return self.CreateEvent( + timestamp, getattr(self, 'entry_offset', 0), self.attributes) + + # TODO: this is a rough initial implementation to get this working. + def CreateEvent(self, timestamp, offset, attributes): + """Creates an event. + + This function should be overwritten by text parsers that require + to generate specific event object type, the default is TextEvent. + + Args: + timestamp: The timestamp time value. The timestamp contains the + number of microseconds since Jan 1, 1970 00:00:00 UTC. + offset: The offset of the event. + attributes: A dict that contains the events attributes. + + Returns: + An event object (instance of TextEvent). + """ + return text_events.TextEvent(timestamp, offset, attributes) + + +class TextCSVParser(interface.BaseParser): + """An implementation of a simple CSV line-per-entry log files.""" + + # A list that contains the names of all the fields in the log file. + COLUMNS = [] + + # A CSV file is comma separated, but this can be overwritten to include + # tab, pipe or other character separation. + VALUE_SEPARATOR = ',' + + # If there is a header before the lines start it can be defined here, and + # the number of header lines that need to be skipped before the parsing + # starts. + NUMBER_OF_HEADER_LINES = 0 + + # If there is a special quote character used inside the structured text + # it can be defined here. + QUOTE_CHAR = '"' + + # Value that should not appear inside the file, made to test the actual + # file to see if it confirms to standards. + MAGIC_TEST_STRING = 'RegnThvotturMeistarans' + + def VerifyRow(self, unused_parser_context, unused_row): + """Return a bool indicating whether or not this is the correct parser. + + Args: + parser_context: A parser context object (instance of ParserContext). + row: A single row from the CSV file. + + Returns: + True if this is the correct parser, False otherwise. + """ + pass + + def ParseRow( + self, parser_context, row_offset, row, file_entry=None, + parser_chain=None): + """Parse a line of the log file and extract event objects. + + Args: + parser_context: A parser context object (instance of ParserContext). + row_offset: The offset of the row. + row: A dictionary containing all the fields as denoted in the + COLUMNS class list. + file_entry: optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + event_object = event.EventObject() + if row_offset is not None: + event_object.offset = row_offset + event_object.row_dict = row + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract data from a CVS file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + path_spec_printable = file_entry.path_spec.comparable.replace(u'\n', u';') + file_object = file_entry.GetFileObject() + file_object.seek(0, os.SEEK_SET) + + text_file_object = text_file.TextFile(file_object) + + # If we specifically define a number of lines we should skip do that here. + for _ in range(0, self.NUMBER_OF_HEADER_LINES): + _ = text_file_object.readline() + + reader = csv.DictReader( + text_file_object, fieldnames=self.COLUMNS, + restkey=self.MAGIC_TEST_STRING, restval=self.MAGIC_TEST_STRING, + delimiter=self.VALUE_SEPARATOR, quotechar=self.QUOTE_CHAR) + + try: + row = reader.next() + except (csv.Error, StopIteration): + file_object.close() + raise errors.UnableToParseFile( + u'[{0:s}] Unable to parse CSV file: {1:s}.'.format( + self.NAME, path_spec_printable)) + + number_of_columns = len(self.COLUMNS) + number_of_records = len(row) + + if number_of_records != number_of_columns: + file_object.close() + raise errors.UnableToParseFile(( + u'[{0:s}] Unable to parse CSV file: {1:s}. Wrong number of ' + u'records (expected: {2:d}, got: {3:d})').format( + self.NAME, path_spec_printable, number_of_columns, + number_of_records)) + + for key, value in row.items(): + if key == self.MAGIC_TEST_STRING or value == self.MAGIC_TEST_STRING: + file_object.close() + raise errors.UnableToParseFile(( + u'[{0:s}] Unable to parse CSV file: {1:s}. Signature ' + u'mismatch.').format(self.NAME, path_spec_printable)) + + if not self.VerifyRow(parser_context, row): + file_object.close() + raise errors.UnableToParseFile(( + u'[{0:s}] Unable to parse CSV file: {1:s}. Verification ' + u'failed.').format(self.NAME, path_spec_printable)) + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + self.ParseRow( + parser_context, text_file_object.tell(), row, file_entry=file_entry, + parser_chain=parser_chain) + + for row in reader: + self.ParseRow( + parser_context, text_file_object.tell(), row, file_entry=file_entry, + parser_chain=parser_chain) + + file_object.close() + + +def PyParseRangeCheck(lower_bound, upper_bound): + """Verify that a number is within a defined range. + + This is a callback method for pyparsing setParseAction + that verifies that a read number is within a certain range. + + To use this method it needs to be defined as a callback method + in setParseAction with the upper and lower bound set as parameters. + + Args: + lower_bound: An integer representing the lower bound of the range. + upper_bound: An integer representing the upper bound of the range. + + Returns: + A callback method that can be used by pyparsing setParseAction. + """ + def CheckRange(unused_string, unused_location, tokens): + """Parse the arguments.""" + try: + check_number = tokens[0] + except IndexError: + check_number = -1 + + if check_number < lower_bound: + raise pyparsing.ParseException( + u'Value: {0:d} precedes lower bound: {1:d}'.format( + check_number, lower_bound)) + + if check_number > upper_bound: + raise pyparsing.ParseException( + u'Value: {0:d} exceeds upper bound: {1:d}'.format( + check_number, upper_bound)) + + # Since callback methods for pyparsing need to accept certain parameters + # and there is no way to define conditions, like upper and lower bounds + # we need to return here a method that accepts those pyparsing parameters. + return CheckRange + + +def PyParseIntCast(unused_string, unused_location, tokens): + """Return an integer from a string. + + This is a pyparsing callback method that converts the matched + string into an integer. + + The method modifies the content of the tokens list and converts + them all to an integer value. + + Args: + unused_string: The original parsed string. + unused_location: The location within the string where the match was made. + tokens: A list of extracted tokens (where the string to be converted is + stored). + """ + # Cast the regular tokens. + for index, token in enumerate(tokens): + try: + tokens[index] = int(token) + except ValueError: + logging.error(u'Unable to cast [{0:s}] to an int, setting to 0'.format( + token)) + tokens[index] = 0 + + # We also need to cast the dictionary built tokens. + for key in tokens.keys(): + try: + tokens[key] = int(tokens[key], 10) + except ValueError: + logging.error( + u'Unable to cast [{0:s} = {1:d}] to an int, setting to 0'.format( + key, tokens[key])) + tokens[key] = 0 + + +def PyParseJoinList(unused_string, unused_location, tokens): + """Return a joined token from a list of tokens. + + This is a callback method for pyparsing setParseAction that modifies + the returned token list to join all the elements in the list to a single + token. + + Args: + unused_string: The original parsed string. + unused_location: The location within the string where the match was made. + tokens: A list of extracted tokens. This is the list that should be joined + together and stored as a single token. + """ + join_list = [] + for token in tokens: + try: + join_list.append(str(token)) + except UnicodeDecodeError: + join_list.append(repr(token)) + + tokens[0] = u''.join(join_list) + del tokens[1:] + + +class PyparsingConstants(object): + """A class that maintains constants for pyparsing.""" + + # Numbers. + INTEGER = pyparsing.Word(pyparsing.nums).setParseAction(PyParseIntCast) + IPV4_OCTET = pyparsing.Word(pyparsing.nums, min=1, max=3).setParseAction( + PyParseIntCast, PyParseRangeCheck(0, 255)) + IPV4_ADDRESS = (IPV4_OCTET + ('.' + IPV4_OCTET) * 3).setParseAction( + PyParseJoinList) + + # TODO: Fix the IPv6 address specification to be more accurate (8 :, correct + # size, etc). + IPV6_ADDRESS = pyparsing.Word(':' + pyparsing.hexnums).setParseAction( + PyParseJoinList) + + # Common words. + MONTH = pyparsing.Word( + pyparsing.string.uppercase, pyparsing.string.lowercase, + exact=3) + + # Define date structures. + HYPHEN = pyparsing.Literal('-').suppress() + YEAR = pyparsing.Word(pyparsing.nums, exact=4).setParseAction( + PyParseIntCast) + TWO_DIGITS = pyparsing.Word(pyparsing.nums, exact=2).setParseAction( + PyParseIntCast) + ONE_OR_TWO_DIGITS = pyparsing.Word( + pyparsing.nums, min=1, max=2).setParseAction(PyParseIntCast) + DATE = pyparsing.Group( + YEAR + pyparsing.Suppress('-') + TWO_DIGITS + + pyparsing.Suppress('-') + TWO_DIGITS) + DATE_REV = pyparsing.Group( + TWO_DIGITS + pyparsing.Suppress('-') + TWO_DIGITS + + pyparsing.Suppress('-') + YEAR) + TIME = pyparsing.Group( + TWO_DIGITS + pyparsing.Suppress(':') + TWO_DIGITS + + pyparsing.Suppress(':') + TWO_DIGITS) + TIME_MSEC = TIME + pyparsing.Suppress('.') + INTEGER + DATE_TIME = DATE + TIME + DATE_TIME_MSEC = DATE + TIME_MSEC + + COMMENT_LINE_HASH = pyparsing.Literal('#') + pyparsing.SkipTo( + pyparsing.LineEnd()) + # TODO: Add more commonly used structs that can be used by parsers. + PID = pyparsing.Word( + pyparsing.nums, min=1, max=5).setParseAction(PyParseIntCast) + + +class PyparsingSingleLineTextParser(interface.BaseParser): + """Single line text parser based on the pyparsing library.""" + + # The actual structure, this needs to be defined by each parser. + # This is defined as a list of tuples so that more then a single line + # structure can be defined. That way the parser can support more than a + # single type of log entry, despite them all having in common the constraint + # that each log entry is a single line. + # The tuple should have two entries, a key and a structure. This is done to + # keep the structures in an order of priority/preference. + # The key is a comment or an identification that is passed to the ParseRecord + # function so that the developer can identify which structure got parsed. + # The value is the actual pyparsing structure. + LINE_STRUCTURES = [] + + # In order for the tool to not read too much data into a buffer to evaluate + # whether or not the parser is the right one for this file or not we + # specifically define a maximum amount of bytes a single line can occupy. This + # constant can be overwritten by implementations if their format might have a + # longer line than 400 bytes. + MAX_LINE_LENGTH = 400 + + # Define an encoding. If a file is encoded using specific encoding it is + # advised to include it here. If this class constant is set all lines wil be + # decoded prior to being sent to parsing by pyparsing, if not properly set it + # could negatively affect parsing of the file. + # If this value needs to be calculated on the fly (not a fixed constant for + # this particular file type) it can be done by modifying the self.encoding + # attribute. + ENCODING = '' + + def __init__(self): + """Initializes the pyparsing single-line text parser object.""" + super(PyparsingSingleLineTextParser, self).__init__() + self.encoding = self.ENCODING + self._current_offset = 0 + # TODO: self._line_structures is a work-around and this needs + # a structural fix. + self._line_structures = self.LINE_STRUCTURES + + def _ReadLine( + self, parser_context, file_entry, text_file_object, max_len=0, + quiet=False, depth=0): + """Read a single line from a text file and return it back. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + text_file_object: A text file object (instance of dfvfs.TextFile). + max_len: If defined determines the maximum number of bytes a single line + can take. + quiet: If True then a decode warning is not displayed. + depth: A threshold of how many newlines we can encounter before bailing + out. + + Returns: + A single line read from the file-like object, or the maximum number of + characters (if max_len defined and line longer than the defined size). + """ + if max_len: + line = text_file_object.readline(max_len) + else: + line = text_file_object.readline() + + if not line: + return + + # If line is empty, skip it and go on. + if line == '\n' or line == '\r\n': + # Max 40 new lines in a row before we bail out. + if depth == 40: + return '' + + return self._ReadLine( + parser_context, file_entry, text_file_object, max_len=max_len, + depth=depth + 1) + + if not self.encoding: + return line.strip() + + try: + decoded_line = line.decode(self.encoding) + return decoded_line.strip() + except UnicodeDecodeError: + if not quiet: + logging.warning(( + u'Unable to decode line [{0:s}...] with encoding: {1:s} in ' + u'file: {2:s}').format( + repr(line[1:30]), self.encoding, + parser_context.GetDisplayName(file_entry))) + return line.strip() + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract data from a text file using a pyparsing definition. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Raises: + UnableToParseFile: when the file cannot be parsed. + """ + # TODO: find a more elegant way for this; currently the mac_wifi and + # syslog parser seem to rely on this member. + self.file_entry = file_entry + + file_object = file_entry.GetFileObject() + + # TODO: self._line_structures is a work-around and this needs + # a structural fix. + if not self._line_structures: + raise errors.UnableToParseFile( + u'Line structure undeclared, unable to proceed.') + + file_object.seek(0, os.SEEK_SET) + text_file_object = text_file.TextFile(file_object) + + line = self._ReadLine( + parser_context, file_entry, text_file_object, + max_len=self.MAX_LINE_LENGTH, quiet=True) + if not line: + raise errors.UnableToParseFile(u'Not a text file.') + + if len(line) == self.MAX_LINE_LENGTH or len( + line) == self.MAX_LINE_LENGTH - 1: + logging.debug(( + u'Trying to read a line and reached the maximum allowed length of ' + u'{0:d}. The last few bytes of the line are: {1:s} [parser ' + u'{2:s}]').format( + self.MAX_LINE_LENGTH, repr(line[-10:]), self.NAME)) + + if not utils.IsText(line): + raise errors.UnableToParseFile(u'Not a text file, unable to proceed.') + + if not self.VerifyStructure(parser_context, line): + raise errors.UnableToParseFile('Wrong file structure.') + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + # Set the offset to the beginning of the file. + self._current_offset = 0 + # Read every line in the text file. + while line: + parsed_structure = None + use_key = None + # Try to parse the line using all the line structures. + for key, structure in self.LINE_STRUCTURES: + try: + parsed_structure = structure.parseString(line) + except pyparsing.ParseException: + pass + if parsed_structure: + use_key = key + break + + if parsed_structure: + parsed_event = self.ParseRecord( + parser_context, use_key, parsed_structure) + if parsed_event: + parsed_event.offset = self._current_offset + parser_context.ProduceEvent( + parsed_event, parser_chain=parser_chain, file_entry=file_entry) + else: + logging.warning(u'Unable to parse log line: {0:s}'.format(line)) + + self._current_offset = text_file_object.get_offset() + line = self._ReadLine(parser_context, file_entry, text_file_object) + + file_object.close() + + @abc.abstractmethod + def ParseRecord(self, parser_context, key, structure): + """Parse a single extracted pyparsing structure. + + This function takes as an input a parsed pyparsing structure + and produces an EventObject if possible from that structure. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: An identification string indicating the name of the parsed + structure. + structure: A pyparsing.ParseResults object from a line in the + log file. + + Returns: + An event object (instance of EventObject) or None. + """ + + @abc.abstractmethod + def VerifyStructure(self, parser_context, line): + """Verify the structure of the file and return boolean based on that check. + + This function should read enough text from the text file to confirm + that the file is the correct one for this particular parser. + + Args: + parser_context: A parser context object (instance of ParserContext). + line: A single line from the text file. + + Returns: + True if this is the correct parser, False otherwise. + """ + + +class EncodedTextReader(object): + """Class to read simple encoded text.""" + + def __init__(self, buffer_size=2048, encoding=None): + """Initializes the encoded test reader object. + + Args: + buffer_size: optional buffer size. The default is 2048. + encoding: optional encoding. The default is None. + """ + super(EncodedTextReader, self).__init__() + self._buffer = '' + self._buffer_size = buffer_size + self._current_offset = 0 + self._encoding = encoding + + if self._encoding: + self._new_line = u'\n'.encode(self._encoding) + self._carriage_return = u'\r'.encode(self._encoding) + else: + self._new_line = '\n' + self._carriage_return = '\r' + + self._new_line_length = len(self._new_line) + self._carriage_return_length = len(self._carriage_return) + + self.lines = u'' + + def _ReadLine(self, file_object): + """Reads a line from the file object. + + Args: + file_object: the file-like object. + + Returns: + A string containing the line. + """ + if len(self._buffer) < self._buffer_size: + self._buffer = ''.join([ + self._buffer, file_object.read(self._buffer_size)]) + + line, new_line, self._buffer = self._buffer.partition(self._new_line) + if not line and not new_line: + line = self._buffer + self._buffer = '' + + self._current_offset += len(line) + + # Strip carriage returns from the text. + if line.endswith(self._carriage_return): + line = line[:-self._carriage_return_length] + + if new_line: + line = ''.join([line, self._new_line]) + self._current_offset += self._new_line_length + + # If a parser specifically indicates specific encoding we need + # to handle the buffer as it is an encoded string. + # If it fails we fail back to the original raw string. + if self._encoding: + try: + line = line.decode(self._encoding) + except UnicodeDecodeError: + # TODO: it might be better to raise here. + pass + + return line + + def ReadLine(self, file_object): + """Reads a line. + + Args: + file_object: the file-like object. + + Returns: + A single line read from the lines buffer. + """ + line, _, self.lines = self.lines.partition('\n') + if not line: + self.ReadLines(file_object) + line, _, self.lines = self.lines.partition('\n') + + return line + + def ReadLines(self, file_object): + """Reads lines into the lines buffer. + + Args: + file_object: the file-like object. + """ + lines_size = len(self.lines) + if lines_size < self._buffer_size: + lines_size = self._buffer_size - lines_size + while lines_size > 0: + line = self._ReadLine(file_object) + if not line: + break + + self.lines = u''.join([self.lines, line]) + lines_size -= len(line) + + def Reset(self): + """Resets the encoded text reader.""" + self._buffer = '' + self._current_offset = 0 + + self.lines = u'' + + def SkipAhead(self, file_object, number_of_characters): + """Skips ahead a number of characters. + + Args: + file_object: the file-like object. + number_of_characters: the number of characters. + """ + lines_size = len(self.lines) + while number_of_characters >= lines_size: + number_of_characters -= lines_size + + self.lines = u'' + self.ReadLines(file_object) + lines_size = len(self.lines) + if lines_size == 0: + return + + self.lines = self.lines[number_of_characters:] + + +class PyparsingMultiLineTextParser(PyparsingSingleLineTextParser): + """Multi line text parser based on the pyparsing library.""" + + BUFFER_SIZE = 2048 + + def __init__(self): + """Initializes the pyparsing multi-line text parser object.""" + super(PyparsingMultiLineTextParser, self).__init__() + self._buffer_size = self.BUFFER_SIZE + self._text_reader = EncodedTextReader( + buffer_size=self.BUFFER_SIZE, encoding=self.ENCODING) + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Parse a text file using a pyparsing definition. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Raises: + UnableToParseFile: if the line structures are missing. + """ + if not self.LINE_STRUCTURES: + raise errors.UnableToParseFile(u'Missing line structures.') + + self._text_reader.Reset() + + file_object = file_entry.GetFileObject() + file_object.seek(0, os.SEEK_SET) + + try: + self._text_reader.ReadLines(file_object) + except UnicodeDecodeError as exception: + raise errors.UnableToParseFile( + u'Not a text file, with error: {0:s}'.format(exception)) + + if not utils.IsText(self._text_reader.lines): + raise errors.UnableToParseFile(u'Not a text file, unable to proceed.') + + if not self.VerifyStructure(parser_context, self._text_reader.lines): + raise errors.UnableToParseFile(u'Wrong file structure.') + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + # Read every line in the text file. + while self._text_reader.lines: + # Initialize pyparsing objects. + tokens = None + start = 0 + end = 0 + + key = None + + # Try to parse the line using all the line structures. + for key, structure in self.LINE_STRUCTURES: + try: + parsed_structure = next( + structure.scanString(self._text_reader.lines, maxMatches=1), None) + except pyparsing.ParseException: + continue + + if not parsed_structure: + continue + + tokens, start, end = parsed_structure + + # Only want to parse the structure if it starts + # at the beginning of the buffer. + if start == 0: + break + + if tokens and start == 0: + parsed_event = self.ParseRecord(parser_context, key, tokens) + if parsed_event: + # TODO: need a reliable way to handle this. + # parsed_event.offset = self._text_reader.line_offset + parser_context.ProduceEvent( + parsed_event, parser_chain=parser_chain, file_entry=file_entry) + + self._text_reader.SkipAhead(file_object, end) + + else: + odd_line = self._text_reader.ReadLine(file_object) + if odd_line: + logging.warning( + u'Unable to parse log line: {0:s}'.format(repr(odd_line))) + + try: + self._text_reader.ReadLines(file_object) + except UnicodeDecodeError as exception: + logging.error( + u'[{0:s}] Unable to read lines from file: {1:s} with error: ' + u'{2:s}'.format( + parser_chain, + file_entry.path_spec.comparable.replace(u'\n', u';'), + exception)) diff --git a/plaso/parsers/text_parser_test.py b/plaso/parsers/text_parser_test.py new file mode 100644 index 0000000..96c2b11 --- /dev/null +++ b/plaso/parsers/text_parser_test.py @@ -0,0 +1,181 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the tests for the generic text parser.""" + +import unittest + +import pyparsing + +from plaso.events import text_events +from plaso.formatters import interface as formatters_interface +from plaso.formatters import manager as formatters_manager +from plaso.lib import errors +from plaso.lib import lexer +from plaso.lib import timelib_test +from plaso.parsers import test_lib +from plaso.parsers import text_parser + + +class TestTextEvent(text_events.TextEvent): + """Test text event.""" + DATA_TYPE = 'test:parser:text' + + +class TestTextEventFormatter(formatters_interface.EventFormatter): + """Test text event formatter.""" + DATA_TYPE = 'test:parser:text' + FORMAT_STRING = u'{body}' + + SOURCE_LONG = 'Test Text Parser' + + +class TestTextParser(text_parser.SlowLexicalTextParser): + """Implement a text parser object that can successfully parse a text file. + + To be able to achieve that one function has to be implemented, the ParseDate + one. + """ + NAME = 'test_text' + + tokens = [ + lexer.Token('INITIAL', + r'^([\d\/]+) ', 'SetDate', 'TIME'), + lexer.Token('TIME', r'([0-9:\.]+) ', 'SetTime', 'STRING_HOST'), + lexer.Token('STRING_HOST', r'([^\-]+)- ', 'ParseStringHost', 'STRING'), + lexer.Token('STRING', '([^\n]+)', 'ParseString', ''), + lexer.Token('STRING', '\n', 'ParseMessage', 'INITIAL')] + + def ParseStringHost(self, match, **_): + user, host = match.group(1).split(':') + self.attributes['hostname'] = host + self.attributes['username'] = user + + def SetDate(self, match, **_): + month, day, year = match.group(1).split('/') + self.attributes['imonth'] = int(month) + self.attributes['iyear'] = int(year) + self.attributes['iday'] = int(day) + + def Scan(self, unused_file_entry): + pass + + def CreateEvent(self, timestamp, offset, attributes): + event_object = TestTextEvent(timestamp, 0, attributes) + event_object.offset = offset + return event_object + + +class TextParserTest(test_lib.ParserTestCase): + """An unit test for the plaso parser library.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = TestTextParser() + + def testTextParserFail(self): + """Test a text parser that will not match against content.""" + test_file = self._GetTestFilePath(['text_parser', 'test1.txt']) + + with self.assertRaises(errors.UnableToParseFile): + _ = self._ParseFile(self._parser, test_file) + + def testTextParserSuccess(self): + """Test a text parser that will match against content.""" + test_file = self._GetTestFilePath(['text_parser', 'test2.txt']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + event_object = event_objects[0] + + msg1, _ = formatters_manager.EventFormatterManager.GetMessageStrings( + event_object) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-01-01 05:23:15') + self.assertEquals(event_object.timestamp, expected_timestamp) + + self.assertEquals(msg1, 'first line.') + self.assertEquals(event_object.hostname, 'myhost') + self.assertEquals(event_object.username, 'myuser') + + event_object = event_objects[1] + + msg2, _ = formatters_manager.EventFormatterManager.GetMessageStrings( + event_object) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '1991-12-24 19:58:06') + self.assertEquals(event_object.timestamp, expected_timestamp) + + self.assertEquals(msg2, 'second line.') + self.assertEquals(event_object.hostname, 'myhost') + self.assertEquals(event_object.username, 'myuser') + + +class PyParserTest(test_lib.ParserTestCase): + """Few unit tests for the pyparsing unit.""" + + def _CheckIPv4(self, ip_address): + # TODO: Add a similar IPv6 check. + try: + text_parser.PyparsingConstants.IPV4_ADDRESS.parseString(ip_address) + return True + except pyparsing.ParseException: + return False + + def testPyConstantIPv4(self): + """Run few tests to make sure the constants are working.""" + self.assertTrue(self._CheckIPv4('123.51.234.52')) + self.assertTrue(self._CheckIPv4('255.254.23.1')) + self.assertTrue(self._CheckIPv4('1.1.34.2')) + self.assertFalse(self._CheckIPv4('1.1.34.258')) + self.assertFalse(self._CheckIPv4('a.1.34.258')) + self.assertFalse(self._CheckIPv4('.34.258')) + self.assertFalse(self._CheckIPv4('34.258')) + self.assertFalse(self._CheckIPv4('10.52.34.258')) + + def testPyConstantOctet(self): + with self.assertRaises(pyparsing.ParseException): + text_parser.PyparsingConstants.IPV4_OCTET.parseString('526') + + with self.assertRaises(pyparsing.ParseException): + text_parser.PyparsingConstants.IPV4_OCTET.parseString('1026') + + with self.assertRaises(pyparsing.ParseException): + text_parser.PyparsingConstants.IPV4_OCTET.parseString( + 'a9', parseAll=True) + + def testPyConstantOthers(self): + with self.assertRaises(pyparsing.ParseException): + text_parser.PyparsingConstants.MONTH.parseString('MMo') + with self.assertRaises(pyparsing.ParseException): + text_parser.PyparsingConstants.MONTH.parseString('M') + with self.assertRaises(pyparsing.ParseException): + text_parser.PyparsingConstants.MONTH.parseString('March', parseAll=True) + + self.assertTrue(text_parser.PyparsingConstants.MONTH.parseString('Jan')) + + line = '# This is a comment.' + parsed_line = text_parser.PyparsingConstants.COMMENT_LINE_HASH.parseString( + line) + self.assertEquals(parsed_line[-1], 'This is a comment.') + self.assertEquals(len(parsed_line), 2) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/utmp.py b/plaso/parsers/utmp.py new file mode 100644 index 0000000..0515b1e --- /dev/null +++ b/plaso/parsers/utmp.py @@ -0,0 +1,273 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for Linux UTMP files.""" + +import construct +import logging +import os +import socket + +from plaso.lib import errors +from plaso.lib import event +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import interface +from plaso.parsers import manager + + +__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)' + + +class UtmpEvent(event.EventObject): + """Convenience class for an UTMP event.""" + + DATA_TYPE = 'linux:utmp:event' + + def __init__( + self, timestamp, microsecond, user, computer_name, + terminal, status, ip_address, structure): + """Initializes the event object. + + Args: + timestamp: Epoch when the terminal was started. + microsecond: number of microseconds related with timestamp. + user: active user name. + computer_name: name of the computer. + terminal: type of terminal. + status: login status. + ip_address: ip_address from the connection is done. + structure: entry structure parsed. + exit: integer that represents the exit status. + pid: integer with the process ID. + terminal_id: integer with the Inittab ID. + """ + super(UtmpEvent, self).__init__() + self.timestamp = timelib.Timestamp.FromPosixTimeWithMicrosecond( + timestamp, microsecond) + self.timestamp_desc = eventdata.EventTimestamp.START_TIME + self.user = user + self.computer_name = computer_name + self.terminal = terminal + self.status = status + self.ip_address = ip_address + self.exit = structure.exit + self.pid = structure.pid + self.terminal_id = structure.terminal_id + + +class UtmpParser(interface.BaseParser): + """Parser for Linux/Unix UTMP files.""" + + NAME = 'utmp' + DESCRIPTION = u'Parser for Linux/Unix UTMP files.' + + LINUX_UTMP_ENTRY = construct.Struct( + 'utmp_linux', + construct.ULInt32('type'), + construct.ULInt32('pid'), + construct.String('terminal', 32), + construct.ULInt32('terminal_id'), + construct.String('username', 32), + construct.String('hostname', 256), + construct.ULInt16('termination'), + construct.ULInt16('exit'), + construct.ULInt32('session'), + construct.ULInt32('timestamp'), + construct.ULInt32('microsecond'), + construct.ULInt32('address_a'), + construct.ULInt32('address_b'), + construct.ULInt32('address_c'), + construct.ULInt32('address_d'), + construct.Padding(20)) + + LINUX_UTMP_ENTRY_SIZE = LINUX_UTMP_ENTRY.sizeof() + + STATUS_TYPE = { + 0: 'EMPTY', + 1: 'RUN_LVL', + 2: 'BOOT_TIME', + 3: 'NEW_TIME', + 4: 'OLD_TIME', + 5: 'INIT_PROCESS', + 6: 'LOGIN_PROCESS', + 7: 'USER_PROCESS', + 8: 'DEAD_PROCESS', + 9: 'ACCOUNTING'} + + # Set a default test value for few fields, this is supposed to be a text + # that is highly unlikely to be seen in a terminal field, or a username field. + # It is important that this value does show up in such fields, but otherwise + # it can be a free flowing text field. + _DEFAULT_TEST_VALUE = u'Ekki Fraedilegur Moguleiki, thetta er bull ! = + _<>' + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract data from an UTMP file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + file_object = file_entry.GetFileObject() + try: + structure = self.LINUX_UTMP_ENTRY.parse_stream(file_object) + except (IOError, construct.FieldError) as exception: + file_object.close() + raise errors.UnableToParseFile( + u'Unable to parse UTMP Header with error: {0:s}'.format(exception)) + + if structure.type not in self.STATUS_TYPE: + file_object.close() + raise errors.UnableToParseFile(( + u'Not an UTMP file, unknown type ' + u'[{0:d}].').format(structure.type)) + + if not self._VerifyTextField(structure.terminal): + file_object.close() + raise errors.UnableToParseFile( + u'Not an UTMP file, unknown terminal.') + + if not self._VerifyTextField(structure.username): + file_object.close() + raise errors.UnableToParseFile( + u'Not an UTMP file, unknown username.') + + if not self._VerifyTextField(structure.hostname): + file_object.close() + raise errors.UnableToParseFile( + u'Not an UTMP file, unknown hostname.') + + # Check few values. + terminal = self._GetTextFromNullTerminatedString( + structure.terminal, self._DEFAULT_TEST_VALUE) + if terminal == self._DEFAULT_TEST_VALUE: + raise errors.UnableToParseFile( + u'Not an UTMP file, no terminal set.') + + username = self._GetTextFromNullTerminatedString( + structure.username, self._DEFAULT_TEST_VALUE) + + if username == self._DEFAULT_TEST_VALUE: + raise errors.UnableToParseFile( + u'Not an UTMP file, no username set.') + + if not structure.timestamp: + raise errors.UnableToParseFile( + u'Not an UTMP file, no timestamp set in the first record.') + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + file_object.seek(0, os.SEEK_SET) + event_object = self._ReadUtmpEvent(file_object) + while event_object: + event_object.offset = file_object.tell() + parser_context.ProduceEvent( + event_object, file_entry=file_entry, parser_chain=None) + + event_object = self._ReadUtmpEvent(file_object) + + file_object.close() + + def _VerifyTextField(self, text): + """Check if a bytestream is a null terminated string. + + Args: + event_object: text field from the structure. + + Return: + True if it is a null terminated string, False otherwise. + """ + _, _, null_chars = text.partition('\x00') + if not null_chars: + return False + return len(null_chars) == null_chars.count('\x00') + + def _ReadUtmpEvent(self, file_object): + """Returns an UtmpEvent from a single UTMP entry. + + Args: + file_object: a file-like object that points to an UTMP file. + + Returns: + An event object constructed from a single UTMP record or None if we + have reached the end of the file (or EOF). + """ + offset = file_object.tell() + data = file_object.read(self.LINUX_UTMP_ENTRY_SIZE) + if not data or len(data) != self.LINUX_UTMP_ENTRY_SIZE: + return + try: + entry = self.LINUX_UTMP_ENTRY.parse(data) + except (IOError, construct.FieldError): + logging.warning(( + u'UTMP entry at 0x{:x} couldn\'t be parsed.').format(offset)) + return self.__ReadUtmpEvent(file_object) + + user = self._GetTextFromNullTerminatedString(entry.username) + terminal = self._GetTextFromNullTerminatedString(entry.terminal) + if terminal == '~': + terminal = u'system boot' + computer_name = self._GetTextFromNullTerminatedString(entry.hostname) + if computer_name == u'N/A' or computer_name == u':0': + computer_name = u'localhost' + status = self.STATUS_TYPE.get(entry.type, u'N/A') + + if not entry.address_b: + try: + ip_address = socket.inet_ntoa( + construct.ULInt32('int').build(entry.address_a)) + if ip_address == '0.0.0.0': + ip_address = u'localhost' + except (IOError, construct.FieldError, socket.error): + ip_address = u'N/A' + else: + ip_address = u'{0:d}.{1:d}.{2:d}.{3:d}'.format( + entry.address_a, entry.address_b, entry.address_c, entry.address_d) + + return UtmpEvent( + entry.timestamp, entry.microsecond, user, computer_name, terminal, + status, ip_address, entry) + + def _GetTextFromNullTerminatedString( + self, null_terminated_string, default_string=u'N/A'): + """Get a UTF-8 text from a raw null terminated string. + + Args: + null_terminated_string: Raw string terminated with null character. + default_string: The default string returned if the parser fails. + + Returns: + A decoded UTF-8 string or if unable to decode, the supplied default + string. + """ + text, _, _ = null_terminated_string.partition('\x00') + try: + text = text.decode('utf-8') + except UnicodeDecodeError: + logging.warning( + u'[UTMP] Decode UTF8 failed, the message string may be cut short.') + text = text.decode('utf-8', 'ignore') + if not text: + return default_string + return text + + +manager.ParsersManager.RegisterParser(UtmpParser) diff --git a/plaso/parsers/utmp_test.py b/plaso/parsers/utmp_test.py new file mode 100644 index 0000000..49985bb --- /dev/null +++ b/plaso/parsers/utmp_test.py @@ -0,0 +1,136 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser test for utmp files.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import utmp as utmp_formatter +from plaso.lib import timelib_test +from plaso.parsers import test_lib +from plaso.parsers import utmp + + +class UtmpParserTest(test_lib.ParserTestCase): + """The unit test for UTMP parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = utmp.UtmpParser() + + def testParseUtmpFile(self): + """Tests the Parse function for an UTMP file.""" + test_file = self._GetTestFilePath(['utmp']) + events = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(events) + self.assertEqual(len(event_objects), 14) + event_object = event_objects[0] + self.assertEqual(event_object.terminal, u'system boot') + self.assertEqual(event_object.status, u'BOOT_TIME') + event_object = event_objects[1] + self.assertEqual(event_object.status, u'RUN_LVL') + + event_object = event_objects[2] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-12-13 14:45:09') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.user, u'LOGIN') + self.assertEqual(event_object.computer_name, u'localhost') + self.assertEqual(event_object.terminal, u'tty4') + self.assertEqual(event_object.status, u'LOGIN_PROCESS') + self.assertEqual(event_object.exit, 0) + self.assertEqual(event_object.pid, 1115) + self.assertEqual(event_object.terminal_id, 52) + expected_msg = ( + u'User: LOGIN ' + u'Computer Name: localhost ' + u'Terminal: tty4 ' + u'PID: 1115 ' + u'Terminal_ID: 52 ' + u'Status: LOGIN_PROCESS ' + u'IP Address: localhost ' + u'Exit: 0') + expected_msg_short = ( + u'User: LOGIN') + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + event_object = event_objects[12] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-12-18 22:46:56.305504') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.user, u'moxilo') + self.assertEqual(event_object.computer_name, u'localhost') + self.assertEqual(event_object.terminal, u'pts/4') + self.assertEqual(event_object.status, u'USER_PROCESS') + self.assertEqual(event_object.exit, 0) + self.assertEqual(event_object.pid, 2684) + self.assertEqual(event_object.terminal_id, 13359) + expected_msg = ( + u'User: moxilo ' + u'Computer Name: localhost ' + u'Terminal: pts/4 ' + u'PID: 2684 ' + u'Terminal_ID: 13359 ' + u'Status: USER_PROCESS ' + u'IP Address: localhost ' + u'Exit: 0') + expected_msg_short = ( + u'User: moxilo') + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + def testParseWtmpFile(self): + """Tests the Parse function for an WTMP file.""" + test_file = self._GetTestFilePath(['wtmp.1']) + events = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(events) + self.assertEqual(len(event_objects), 4) + + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-12-01 17:36:38.432935') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.user, u'userA') + self.assertEqual(event_object.computer_name, u'10.10.122.1') + self.assertEqual(event_object.terminal, u'pts/32') + self.assertEqual(event_object.status, u'USER_PROCESS') + self.assertEqual(event_object.ip_address, u'10.10.122.1') + self.assertEqual(event_object.exit, 0) + self.assertEqual(event_object.pid, 20060) + self.assertEqual(event_object.terminal_id, 842084211) + expected_msg = ( + u'User: userA ' + u'Computer Name: 10.10.122.1 ' + u'Terminal: pts/32 ' + u'PID: 20060 ' + u'Terminal_ID: 842084211 ' + u'Status: USER_PROCESS ' + u'IP Address: 10.10.122.1 ' + u'Exit: 0') + expected_msg_short = ( + u'User: userA') + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/utmpx.py b/plaso/parsers/utmpx.py new file mode 100644 index 0000000..e48cd1c --- /dev/null +++ b/plaso/parsers/utmpx.py @@ -0,0 +1,207 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for utmpx files.""" + +# TODO: Add support for other implementations than Mac OS X. +# The parser should be checked against IOS UTMPX file. + +import construct +import logging + +from plaso.lib import errors +from plaso.lib import event +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import interface +from plaso.parsers import manager + + +__author__ = 'Joaquin Moreno Garijo (Joaquin.MorenoGarijo.2013@live.rhul.ac.uk)' + + +class UtmpxMacOsXEvent(event.EventObject): + """Convenience class for an event utmpx.""" + DATA_TYPE = 'mac:utmpx:event' + + def __init__(self, timestamp, user, terminal, status, computer_name): + """Initializes the event object. + + Args: + timestamp: when the terminal was started + user: active user name + terminal: name of the terminal + status: terminal status + computer_name: name of the host or IP. + """ + super(UtmpxMacOsXEvent, self).__init__() + self.timestamp = timestamp + self.timestamp_desc = eventdata.EventTimestamp.START_TIME + self.user = user + self.terminal = terminal + self.status = status + self.computer_name = computer_name + + +class UtmpxParser(interface.BaseParser): + """Parser for UTMPX files.""" + + NAME = 'utmpx' + DESCRIPTION = u'Parser for UTMPX files.' + + # INFO: Type is suppose to be a short (2 bytes), + # however if we analyze the file it is always + # byte follow by 3 bytes with \x00 value. + MAC_UTMPX_ENTRY = construct.Struct( + 'utmpx_mac', + construct.String('user', 256), + construct.ULInt32('id'), + construct.String('tty_name', 32), + construct.ULInt32('pid'), + construct.ULInt16('status_type'), + construct.ULInt16('unknown'), + construct.ULInt32('timestamp'), + construct.ULInt32('microsecond'), + construct.String('hostname', 256), + construct.Padding(64)) + + MAC_UTMPX_ENTRY_SIZE = MAC_UTMPX_ENTRY.sizeof() + + # 9, 10 and 11 are only for Darwin and IOS. + MAC_STATUS_TYPE = { + 0: 'EMPTY', + 1: 'RUN_LVL', + 2: 'BOOT_TIME', + 3: 'OLD_TIME', + 4: 'NEW_TIME', + 5: 'INIT_PROCESS', + 6: 'LOGIN_PROCESS', + 7: 'USER_PROCESS', + 8: 'DEAD_PROCESS', + 9: 'ACCOUNTING', + 10: 'SIGNATURE', + 11: 'SHUTDOWN_TIME'} + + def _ReadEntry(self, file_object): + """Reads an UTMPX entry. + + Args: + file_object: a file-like object that points to an UTMPX file. + + Returns: + An event object constructed from the UTMPX entry. + """ + data = file_object.read(self.MAC_UTMPX_ENTRY_SIZE) + if len(data) != self.MAC_UTMPX_ENTRY_SIZE: + return + + try: + entry = self.MAC_UTMPX_ENTRY.parse(data) + except (IOError, construct.FieldError) as exception: + logging.warning( + u'Unable to parse Mac OS X UTMPX entry with error: {0:s}'.format( + exception)) + return + + user, _, _ = entry.user.partition('\x00') + if not user: + user = u'N/A' + terminal, _, _ = entry.tty_name.partition('\x00') + if not terminal: + terminal = u'N/A' + computer_name, _, _ = entry.hostname.partition('\x00') + if not computer_name: + computer_name = u'localhost' + + value_status = self.MAC_STATUS_TYPE.get(entry.status_type, u'N/A') + status = u'{0}'.format(value_status) + + timestamp = timelib.Timestamp.FromPosixTimeWithMicrosecond( + entry.timestamp, entry.microsecond) + + return UtmpxMacOsXEvent(timestamp, user, terminal, status, computer_name) + + def _VerifyStructure(self, file_object): + """Verify that we are dealing with an UTMPX entry. + + Args: + file_object: a file-like object that points to an UTMPX file. + + Returns: + True if it is a UTMPX entry or False otherwise. + """ + # First entry is a SIGNAL entry of the file ("header"). + try: + header = self.MAC_UTMPX_ENTRY.parse_stream(file_object) + except (IOError, construct.FieldError): + return False + user, _, _ = header.user.partition('\x00') + + # The UTMPX_ENTRY structure will often successfully compile on various + # structures, such as binary plist files, and thus we need to do some + # additional validation. The first one is to check if the user name + # can be converted into a unicode string, otherwise we can assume + # we are dealing with non UTMPX data. + try: + _ = unicode(user) + except UnicodeDecodeError: + return False + + if user != u'utmpx-1.00': + return False + if self.MAC_STATUS_TYPE[header.status_type] != 'SIGNATURE': + return False + if header.timestamp != 0 or header.microsecond != 0 or header.pid != 0: + return False + tty_name, _, _ = header.tty_name.partition('\x00') + hostname, _, _ = header.hostname.partition('\x00') + if tty_name or hostname: + return False + + return True + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract data from a UTMPX file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + file_object = file_entry.GetFileObject() + if not self._VerifyStructure(file_object): + file_object.close() + raise errors.UnableToParseFile( + u'The file is not an UTMPX file.') + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + event_object = self._ReadEntry(file_object) + while event_object: + event_object.offset = file_object.tell() + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + event_object = self._ReadEntry(file_object) + + file_object.close() + + +manager.ParsersManager.RegisterParser(UtmpxParser) diff --git a/plaso/parsers/utmpx_test.py b/plaso/parsers/utmpx_test.py new file mode 100644 index 0000000..3ff07bd --- /dev/null +++ b/plaso/parsers/utmpx_test.py @@ -0,0 +1,94 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for UTMPX file parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import utmpx as utmpx_formatter +from plaso.lib import timelib_test +from plaso.parsers import test_lib +from plaso.parsers import utmpx + + +class UtmpxParserTest(test_lib.ParserTestCase): + """Tests for utmpx file parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = utmpx.UtmpxParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['utmpx_mac']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEqual(len(event_objects), 6) + + event_object = event_objects[0] + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-13 17:52:34') + self.assertEqual(event_object.timestamp, expected_timestamp) + + expected_msg_short = u'User: N/A' + expected_msg = ( + u'User: N/A Status: BOOT_TIME ' + u'Computer Name: localhost Terminal: N/A') + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + event_object = event_objects[1] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-13 17:52:41.736713') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.user, u'moxilo') + self.assertEqual(event_object.terminal, u'console', ) + self.assertEqual(event_object.status, u'USER_PROCESS') + self.assertEqual(event_object.computer_name, u'localhost') + expected_msg = ( + u'User: moxilo Status: ' + u'USER_PROCESS ' + u'Computer Name: localhost ' + u'Terminal: console') + expected_msg_short = ( + u'User: moxilo') + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + event_object = event_objects[4] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-11-14 04:32:56.641464') + self.assertEqual(event_object.timestamp, expected_timestamp) + + self.assertEqual(event_object.user, u'moxilo') + self.assertEqual(event_object.terminal, u'ttys002') + self.assertEqual(event_object.status, u'DEAD_PROCESS') + expected_msg = ( + u'User: moxilo Status: ' + u'DEAD_PROCESS ' + u'Computer Name: localhost ' + u'Terminal: ttys002') + expected_msg_short = ( + u'User: moxilo') + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winevt.py b/plaso/parsers/winevt.py new file mode 100644 index 0000000..49fb492 --- /dev/null +++ b/plaso/parsers/winevt.py @@ -0,0 +1,192 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for Windows EventLog (EVT) files.""" + +import logging + +import pyevt + +from plaso.events import time_events +from plaso.lib import errors +from plaso.lib import eventdata +from plaso.parsers import interface +from plaso.parsers import manager + + +class WinEvtRecordEvent(time_events.PosixTimeEvent): + """Convenience class for a Windows EventLog (EVT) record event.""" + + DATA_TYPE = 'windows:evt:record' + + def __init__( + self, timestamp, timestamp_description, evt_record, recovered=False): + """Initializes the event. + + Args: + timestamp: The POSIX timestamp value. + timestamp_description: A description string for the timestamp value. + evt_record: The EVT record (pyevt.record). + recovered: Boolean value to indicate the record was recovered, False + by default. + """ + super(WinEvtRecordEvent, self).__init__(timestamp, timestamp_description) + + self.recovered = recovered + self.offset = evt_record.offset + + try: + self.record_number = evt_record.identifier + except OverflowError as exception: + logging.warning( + u'Unable to assign the record identifier with error: {0:s}.'.format( + exception)) + try: + event_identifier = evt_record.event_identifier + except OverflowError as exception: + event_identifier = None + logging.warning( + u'Unable to assign the event identifier with error: {0:s}.'.format( + exception)) + + # We are only interest in the event identifier code to match the behavior + # of EVTX event records. + if event_identifier is not None: + self.event_identifier = event_identifier & 0xffff + self.facility = (event_identifier >> 16) & 0x0fff + self.severity = event_identifier >> 30 + self.message_identifier = event_identifier + + self.event_type = evt_record.event_type + self.event_category = evt_record.event_category + self.source_name = evt_record.source_name + + # Computer name is the value stored in the event record and does not + # necessarily corresponds with the actual hostname. + self.computer_name = evt_record.computer_name + self.user_sid = evt_record.user_security_identifier + + self.strings = list(evt_record.strings) + + +class WinEvtParser(interface.BaseParser): + """Parses Windows EventLog (EVT) files.""" + + NAME = 'winevt' + DESCRIPTION = u'Parser for Windows EventLog (EVT) files.' + + def _ParseRecord( + self, parser_context, evt_record, file_entry=None, parser_chain=None, + recovered=False): + """Extract data from a Windows EventLog (EVT) record. + + Args: + parser_context: A parser context object (instance of ParserContext). + evt_record: An event record (pyevt.record). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + recovered: Boolean value to indicate the record was recovered, False + by default. + """ + try: + creation_time = evt_record.get_creation_time_as_integer() + except OverflowError as exception: + logging.warning( + u'Unable to read the timestamp from record with error: {0:s}'.format( + exception)) + creation_time = 0 + + if creation_time: + event_object = WinEvtRecordEvent( + creation_time, eventdata.EventTimestamp.CREATION_TIME, + evt_record, recovered) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + try: + written_time = evt_record.get_written_time_as_integer() + except OverflowError as exception: + logging.warning( + u'Unable to read the timestamp from record with error: {0:s}'.format( + exception)) + written_time = 0 + + if written_time: + event_object = WinEvtRecordEvent( + written_time, eventdata.EventTimestamp.WRITTEN_TIME, + evt_record, recovered) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract data from a Windows EventLog (EVT) file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Raises: + UnableToParseFile: when the file cannot be parsed. + """ + parser_chain = self._BuildParserChain(parser_chain) + + file_object = file_entry.GetFileObject() + evt_file = pyevt.file() + evt_file.set_ascii_codepage(parser_context.codepage) + + try: + evt_file.open_file_object(file_object) + except IOError as exception: + evt_file.close() + file_object.close() + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse file {1:s} with error: {2:s}'.format( + self.NAME, file_entry.name, exception)) + + for record_index in range(0, evt_file.number_of_records): + try: + evt_record = evt_file.get_record(record_index) + self._ParseRecord( + parser_context, evt_record, file_entry=file_entry, + parser_chain=parser_chain) + except IOError as exception: + logging.warning(( + u'[{0:s}] unable to parse event record: {1:d} in file: {2:s} ' + u'with error: {3:s}').format( + self.NAME, record_index, file_entry.name, exception)) + + for record_index in range(0, evt_file.number_of_recovered_records): + try: + evt_record = evt_file.get_recovered_record(record_index) + self._ParseRecord( + parser_context, evt_record, file_entry=file_entry, + parser_chain=parser_chain, recovered=True) + except IOError as exception: + logging.info(( + u'[{0:s}] unable to parse recovered event record: {1:d} in file: ' + u'{2:s} with error: {3:s}').format( + self.NAME, record_index, file_entry.name, exception)) + + evt_file.close() + file_object.close() + + +manager.ParsersManager.RegisterParser(WinEvtParser) diff --git a/plaso/parsers/winevt_test.py b/plaso/parsers/winevt_test.py new file mode 100644 index 0000000..4972ecf --- /dev/null +++ b/plaso/parsers/winevt_test.py @@ -0,0 +1,120 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Windows EventLog (EVT) parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winevt as winevt_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import test_lib +from plaso.parsers import winevt + + +class WinEvtParserTest(test_lib.ParserTestCase): + """Tests for the Windows EventLog (EVT) parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = winevt.WinEvtParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['SysEvent.Evt']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # Windows Event Log (EVT) information: + # Version : 1.1 + # Number of records : 6063 + # Number of recovered records : 437 + # Log type : System + + self.assertEquals(len(event_objects), (6063 + 437) * 2) + + # Event number : 1392 + # Creation time : Jul 27, 2011 06:41:47 UTC + # Written time : Jul 27, 2011 06:41:47 UTC + # Event type : Warning event (2) + # Computer name : WKS-WINXP32BIT + # Source name : LSASRV + # Event category : 3 + # Event identifier : 0x8000a001 (2147524609) + # Number of strings : 2 + # String: 1 : cifs/CONTROLLER + # String: 2 : "The system detected a possible attempt to compromise + # security. Please ensure that you can contact the + # server that authenticated you.\r\n (0xc0000388)" + event_object = event_objects[1] + self.assertEquals(event_object.record_number, 1392) + self.assertEquals(event_object.event_type, 2) + self.assertEquals(event_object.computer_name, u'WKS-WINXP32BIT') + self.assertEquals(event_object.source_name, u'LSASRV') + self.assertEquals(event_object.event_category, 3) + self.assertEquals(event_object.event_identifier, 40961) + self.assertEquals(event_object.strings[0], u'cifs/CONTROLLER') + + expected_string = ( + u'"The system detected a possible attempt to compromise security. ' + u'Please ensure that you can contact the server that authenticated you.' + u'\r\n (0xc0000388)"') + + self.assertEquals(event_object.strings[1], expected_string) + + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-07-27 06:41:47') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.CREATION_TIME) + + event_object = event_objects[1] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-07-27 06:41:47') + self.assertEquals(event_object.timestamp, expected_timestamp) + + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.WRITTEN_TIME) + + expected_msg = ( + u'[40961 / 0xa001] ' + u'Severity: Warning ' + u'Record Number: 1392 ' + u'Event Type: Information event ' + u'Event Category: 3 ' + u'Source Name: LSASRV ' + u'Computer Name: WKS-WINXP32BIT ' + u'Strings: [u\'cifs/CONTROLLER\', ' + u'u\'"The system detected a possible attempt to ' + u'compromise security. Please ensure that you can ' + u'contact the server that authenticated you.\\r\\n ' + u'(0xc0000388)"\']') + + expected_msg_short = ( + u'[40961 / 0xa001] ' + u'Strings: [u\'cifs/CONTROLLER\', ' + u'u\'"The system detected a possi...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winevtx.py b/plaso/parsers/winevtx.py new file mode 100644 index 0000000..5f59731 --- /dev/null +++ b/plaso/parsers/winevtx.py @@ -0,0 +1,165 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for Windows XML EventLog (EVTX) files.""" + +import logging + +import pyevtx + +from plaso.events import time_events +from plaso.lib import errors +from plaso.lib import eventdata +from plaso.parsers import interface +from plaso.parsers import manager + + +if pyevtx.get_version() < '20141112': + raise ImportWarning('WinEvtxParser requires at least pyevtx 20141112.') + + +class WinEvtxRecordEvent(time_events.FiletimeEvent): + """Convenience class for a Windows XML EventLog (EVTX) record event.""" + DATA_TYPE = 'windows:evtx:record' + + def __init__(self, evtx_record, recovered=False): + """Initializes the event. + + Args: + evtx_record: The EVTX record (pyevtx.record). + recovered: Boolean value to indicate the record was recovered, False + by default. + """ + try: + timestamp = evtx_record.get_written_time_as_integer() + except OverflowError as exception: + logging.warning( + u'Unable to read the timestamp from record with error: {0:s}'.format( + exception)) + timestamp = 0 + + super(WinEvtxRecordEvent, self).__init__( + timestamp, eventdata.EventTimestamp.WRITTEN_TIME) + + self.recovered = recovered + self.offset = evtx_record.offset + + try: + self.record_number = evtx_record.identifier + except OverflowError as exception: + logging.warning( + u'Unable to assign the record number with error: {0:s}.'.format( + exception)) + + try: + event_identifier = evtx_record.event_identifier + except OverflowError as exception: + event_identifier = None + logging.warning( + u'Unable to assign the event identifier with error: {0:s}.'.format( + exception)) + + try: + event_identifier_qualifiers = evtx_record.event_identifier_qualifiers + except OverflowError as exception: + event_identifier_qualifiers = None + logging.warning(( + u'Unable to assign the event identifier qualifiers with error: ' + u'{0:s}.').format(exception)) + + if event_identifier is not None: + self.event_identifier = event_identifier + + if event_identifier_qualifiers is not None: + self.message_identifier = ( + (event_identifier_qualifiers << 16) | event_identifier) + + self.event_level = evtx_record.event_level + self.source_name = evtx_record.source_name + + # Computer name is the value stored in the event record and does not + # necessarily corresponds with the actual hostname. + self.computer_name = evtx_record.computer_name + self.user_sid = evtx_record.user_security_identifier + + self.strings = list(evtx_record.strings) + + self.xml_string = evtx_record.xml_string + + +class WinEvtxParser(interface.BaseParser): + """Parses Windows XML EventLog (EVTX) files.""" + + NAME = 'winevtx' + DESCRIPTION = u'Parser for Windows XML EventLog (EVTX) files.' + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract data from a Windows XML EventLog (EVTX) file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Raises: + UnableToParseFile: when the file cannot be parsed. + """ + parser_chain = self._BuildParserChain(parser_chain) + + file_object = file_entry.GetFileObject() + evtx_file = pyevtx.file() + evtx_file.set_ascii_codepage(parser_context.codepage) + + try: + evtx_file.open_file_object(file_object) + except IOError as exception: + evtx_file.close() + file_object.close() + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse file {1:s} with error: {2:s}'.format( + self.NAME, file_entry.name, exception)) + + for record_index in range(0, evtx_file.number_of_records): + try: + evtx_record = evtx_file.get_record(record_index) + event_object = WinEvtxRecordEvent(evtx_record) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + except IOError as exception: + logging.warning(( + u'[{0:s}] unable to parse event record: {1:d} in file: {2:s} ' + u'with error: {3:s}').format( + self.NAME, record_index, file_entry.name, exception)) + + for record_index in range(0, evtx_file.number_of_recovered_records): + try: + evtx_record = evtx_file.get_recovered_record(record_index) + event_object = WinEvtxRecordEvent(evtx_record, recovered=True) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + except IOError as exception: + logging.debug(( + u'[{0:s}] unable to parse recovered event record: {1:d} in file: ' + u'{2:s} with error: {3:s}').format( + self.NAME, record_index, file_entry.name, exception)) + + evtx_file.close() + file_object.close() + + +manager.ParsersManager.RegisterParser(WinEvtxParser) diff --git a/plaso/parsers/winevtx_test.py b/plaso/parsers/winevtx_test.py new file mode 100644 index 0000000..a980893 --- /dev/null +++ b/plaso/parsers/winevtx_test.py @@ -0,0 +1,138 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Windows XML EventLog (EVTX) parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winevtx as winevtx_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import test_lib +from plaso.parsers import winevtx + + +class WinEvtxParserTest(test_lib.ParserTestCase): + """Tests for the Windows XML EventLog (EVTX) parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = winevtx.WinEvtxParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['System.evtx']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # Windows Event Viewer Log (EVTX) information: + # Version : 3.1 + # Number of records : 1601 + # Number of recovered records : 0 + # Log type : System + + self.assertEquals(len(event_objects), 1601) + + # Event number : 12049 + # Written time : Mar 14, 2012 04:17:43.354562700 UTC + # Event level : Information (4) + # Computer name : WKS-WIN764BITB.shieldbase.local + # Provider identifier : {fc65ddd8-d6ef-4962-83d5-6e5cfe9ce148} + # Source name : Microsoft-Windows-Eventlog + # Event identifier : 0x00000069 (105) + # Number of strings : 2 + # String: 1 : System + # String: 2 : C:\Windows\System32\Winevt\Logs\ + # : Archive-System-2012-03-14-04-17-39-932.evtx + + event_object = event_objects[0] + + self.assertEquals(event_object.record_number, 12049) + expected_computer_name = u'WKS-WIN764BITB.shieldbase.local' + self.assertEquals(event_object.computer_name, expected_computer_name) + self.assertEquals(event_object.source_name, u'Microsoft-Windows-Eventlog') + self.assertEquals(event_object.event_level, 4) + self.assertEquals(event_object.event_identifier, 105) + + self.assertEquals(event_object.strings[0], u'System') + + expected_string = ( + u'C:\\Windows\\System32\\Winevt\\Logs\\' + u'Archive-System-2012-03-14-04-17-39-932.evtx') + + self.assertEquals(event_object.strings[1], expected_string) + + event_object = event_objects[1] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-03-14 04:17:38.276340') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.WRITTEN_TIME) + + expected_xml_string = ( + u'\n' + u' \n' + u' \n' + u' 7036\n' + u' 0\n' + u' 4\n' + u' 0\n' + u' 0\n' + u' 0x8080000000000000\n' + u' \n' + u' 12050\n' + u' \n' + u' \n' + u' System\n' + u' WKS-WIN764BITB.shieldbase.local\n' + u' \n' + u' \n' + u' \n' + u' Windows Modules Installer\n' + u' stopped\n' + u' 540072007500730074006500640049006E007300740061006C006C00' + u'650072002F0031000000\n' + u' \n' + u'\n') + + self.assertEquals(event_object.xml_string, expected_xml_string) + + expected_msg = ( + u'[7036 / 0x1b7c] ' + u'Record Number: 12050 ' + u'Event Level: 4 ' + u'Source Name: Service Control Manager ' + u'Computer Name: WKS-WIN764BITB.shieldbase.local ' + u'Strings: [u\'Windows Modules Installer\', ' + u'u\'stopped\', u\'540072007500730074006500640049006E00' + u'7300740061006C006C00650072002F0031000000\']') + + expected_msg_short = ( + u'[7036 / 0x1b7c] ' + u'Strings: [u\'Windows Modules Installer\', ' + u'u\'stopped\', u\'5400720...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winfirewall.py b/plaso/parsers/winfirewall.py new file mode 100644 index 0000000..90adb1b --- /dev/null +++ b/plaso/parsers/winfirewall.py @@ -0,0 +1,197 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for Windows Firewall Log file.""" + +import logging + +import pyparsing + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import manager +from plaso.parsers import text_parser + +import pytz + + +class WinFirewallParser(text_parser.PyparsingSingleLineTextParser): + """Parses the Windows Firewall Log file. + + More information can be read here: + http://technet.microsoft.com/en-us/library/cc758040(v=ws.10).aspx + """ + + NAME = 'winfirewall' + DESCRIPTION = u'Parser for Windows Firewall Log files.' + + # TODO: Add support for custom field names. Currently this parser only + # supports the default fields, which are: + # date time action protocol src-ip dst-ip src-port dst-port size + # tcpflags tcpsyn tcpack tcpwin icmptype icmpcode info path + + # Define common structures. + BLANK = pyparsing.Literal('-') + WORD = pyparsing.Word(pyparsing.alphanums + '-') | BLANK + INT = pyparsing.Word(pyparsing.nums, min=1) | BLANK + IP = ( + text_parser.PyparsingConstants.IPV4_ADDRESS | + text_parser.PyparsingConstants.IPV6_ADDRESS | BLANK) + PORT = pyparsing.Word(pyparsing.nums, min=1, max=6) | BLANK + + # Define how a log line should look like. + LOG_LINE = ( + text_parser.PyparsingConstants.DATE.setResultsName('date') + + text_parser.PyparsingConstants.TIME.setResultsName('time') + + WORD.setResultsName('action') + WORD.setResultsName('protocol') + + IP.setResultsName('source_ip') + IP.setResultsName('dest_ip') + + PORT.setResultsName('source_port') + INT.setResultsName('dest_port') + + INT.setResultsName('size') + WORD.setResultsName('flags') + + INT.setResultsName('tcp_seq') + INT.setResultsName('tcp_ack') + + INT.setResultsName('tcp_win') + INT.setResultsName('icmp_type') + + INT.setResultsName('icmp_code') + WORD.setResultsName('info') + + WORD.setResultsName('path')) + + # Define the available log line structures. + LINE_STRUCTURES = [ + ('comment', text_parser.PyparsingConstants.COMMENT_LINE_HASH), + ('logline', LOG_LINE), + ] + + DATA_TYPE = 'windows:firewall:log_entry' + + def __init__(self): + """Initializes a parser object.""" + super(WinFirewallParser, self).__init__() + self.version = None + self.use_local_zone = False + self.software = None + + def VerifyStructure(self, parser_context, line): + """Verify that this file is a firewall log file. + + Args: + parser_context: A parser context object (instance of ParserContext). + line: A single line from the text file. + + Returns: + True if this is the correct parser, False otherwise. + """ + # TODO: Examine other versions of the file format and if this parser should + # support them. + if line == '#Version: 1.5': + return True + + return False + + def ParseRecord(self, parser_context, key, structure): + """Parse each record structure and return an event object if applicable. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: An identification string indicating the name of the parsed + structure. + structure: A pyparsing.ParseResults object from a line in the + log file. + + Returns: + An event object (instance of EventObject) or None. + """ + if key == 'comment': + self._ParseCommentRecord(structure) + elif key == 'logline': + return self._ParseLogLine(parser_context, structure) + else: + logging.warning( + u'Unable to parse record, unknown structure: {0:s}'.format(key)) + + def _ParseCommentRecord(self, structure): + """Parse a comment and store appropriate attributes. + + Args: + structure: A pyparsing.ParseResults object from a line in the + log file. + """ + comment = structure[1] + if comment.startswith('Version'): + _, _, self.version = comment.partition(':') + elif comment.startswith('Software'): + _, _, self.software = comment.partition(':') + elif comment.startswith('Time'): + _, _, time_format = comment.partition(':') + if 'local' in time_format.lower(): + self.use_local_zone = True + + def _ParseLogLine(self, parser_context, structure): + """Parse a single log line and return an event object. + + Args: + parser_context: A parser context object (instance of ParserContext). + structure: A pyparsing.ParseResults object from a line in the + log file. + + Returns: + An event object (instance of EventObject) or None. + """ + log_dict = structure.asDict() + + date = log_dict.get('date', None) + time = log_dict.get('time', None) + + if not (date and time): + logging.warning(u'Unable to extract timestamp from Winfirewall logline.') + return + + year, month, day = date + hour, minute, second = time + if self.use_local_zone: + zone = parser_context.timezone + else: + zone = pytz.utc + + timestamp = timelib.Timestamp.FromTimeParts( + year, month, day, hour, minute, second, timezone=zone) + + if not timestamp: + return + + # TODO: refactor this into a WinFirewall specific event object. + event_object = time_events.TimestampEvent( + timestamp, eventdata.EventTimestamp.WRITTEN_TIME, self.DATA_TYPE) + + for key, value in log_dict.items(): + if key in ('time', 'date'): + continue + if value == '-': + continue + + if type(value) is pyparsing.ParseResults: + print value + setattr(event_object, key, ''.join(value)) + else: + try: + save_value = int(value) + except ValueError: + save_value = value + + setattr(event_object, key, save_value) + + return event_object + + +manager.ParsersManager.RegisterParser(WinFirewallParser) diff --git a/plaso/parsers/winfirewall_test.py b/plaso/parsers/winfirewall_test.py new file mode 100644 index 0000000..382c281 --- /dev/null +++ b/plaso/parsers/winfirewall_test.py @@ -0,0 +1,83 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Windows firewall log parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winfirewall as winfirewall_formatter +from plaso.lib import timelib_test +from plaso.parsers import test_lib +from plaso.parsers import winfirewall + + +class WinFirewallParserTest(test_lib.ParserTestCase): + """Tests for the Windows firewall log parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = winfirewall.WinFirewallParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['firewall.log']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 15) + + event_object = event_objects[4] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2005-04-11 08:06:02') + self.assertEquals(event_object.timestamp, expected_timestamp) + + self.assertEquals(event_object.source_ip, '123.45.78.90') + self.assertEquals(event_object.dest_ip, '123.156.78.90') + + event_object = event_objects[7] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2005-04-11 08:06:26') + self.assertEquals(event_object.timestamp, expected_timestamp) + + self.assertEquals(event_object.size, 576) + self.assertEquals(event_object.flags, 'A') + self.assertEquals(event_object.tcp_ack, 987654321) + + expected_msg = ( + u'DROP [ TCP RECEIVE ] ' + u'From: 123.45.78.90 :80 > 123.156.78.90 :1774 ' + u'Size (bytes): 576 ' + u'Flags [A] ' + u'TCP Seq Number: 123456789 ' + u'TCP ACK Number: 987654321 ' + u'TCP Window Size (bytes): 12345') + expected_msg_short = ( + u'DROP [TCP] 123.45.78.90 : 80 > 123.156.78.90 : 1774') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + event_object = event_objects[9] + + self.assertEquals(event_object.icmp_type, 8) + self.assertEquals(event_object.icmp_code, 0) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winjob.py b/plaso/parsers/winjob.py new file mode 100644 index 0000000..d9e3ef5 --- /dev/null +++ b/plaso/parsers/winjob.py @@ -0,0 +1,270 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for Windows Scheduled Task job files.""" + +import construct + +from plaso.events import time_events +from plaso.lib import binary +from plaso.lib import errors +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import interface +from plaso.parsers import manager + + +__author__ = 'Brian Baskin (brian@thebaskins.com)' + + +class WinJobEvent(time_events.TimestampEvent): + """Convenience class for a Windows Scheduled Task event.""" + + DATA_TYPE = 'windows:tasks:job' + + def __init__( + self, timestamp, timestamp_description, application, parameter, + working_dir, username, trigger, comment): + """Initializes the event object. + + Args: + timestamp: The timestamp value. + timestamp_description: The usage string for the timestamp value. + application: Path to job executable. + parameter: Application command line parameters. + working_dir: Working path for task. + username: User job was scheduled from. + trigger: Trigger event that runs the task, e.g. DAILY. + comment: Optional description about the job. + """ + super(WinJobEvent, self).__init__(timestamp, timestamp_description) + self.application = binary.ReadUtf16(application) + self.parameter = binary.ReadUtf16(parameter) + self.working_dir = binary.ReadUtf16(working_dir) + self.username = binary.ReadUtf16(username) + self.trigger = trigger + self.comment = binary.ReadUtf16(comment) + + +class WinJobParser(interface.BaseParser): + """Parse Windows Scheduled Task files for job events.""" + + NAME = 'winjob' + DESCRIPTION = u'Parser for Windows Scheduled Task job (or At-job) files.' + + PRODUCT_VERSIONS = { + 0x0400:'Windows NT 4.0', + 0x0500:'Windows 2000', + 0x0501:'Windows XP', + 0x0600:'Windows Vista', + 0x0601:'Windows 7', + 0x0602:'Windows 8', + 0x0603:'Windows 8.1' + } + + TRIGGER_TYPES = { + 0x0000:'ONCE', + 0x0001:'DAILY', + 0x0002:'WEEKLY', + 0x0003:'MONTHLYDATE', + 0x0004:'MONTHLYDOW', + 0x0005:'EVENT_ON_IDLE', + 0x0006:'EVENT_AT_SYSTEMSTART', + 0x0007:'EVENT_AT_LOGON' + } + + JOB_FIXED_STRUCT = construct.Struct( + 'job_fixed', + construct.ULInt16('product_version'), + construct.ULInt16('file_version'), + construct.Bytes('job_uuid', 16), + construct.ULInt16('app_name_len_offset'), + construct.ULInt16('trigger_offset'), + construct.ULInt16('error_retry_count'), + construct.ULInt16('error_retry_interval'), + construct.ULInt16('idle_deadline'), + construct.ULInt16('idle_wait'), + construct.ULInt32('priority'), + construct.ULInt32('max_run_time'), + construct.ULInt32('exit_code'), + construct.ULInt32('status'), + construct.ULInt32('flags'), + construct.ULInt16('ran_year'), + construct.ULInt16('ran_month'), + construct.ULInt16('ran_weekday'), + construct.ULInt16('ran_day'), + construct.ULInt16('ran_hour'), + construct.ULInt16('ran_minute'), + construct.ULInt16('ran_second'), + construct.ULInt16('ran_millisecond'), + ) + + # Using Construct's utf-16 encoding here will create strings with their + # null terminators exposed. Instead, we'll read these variables raw and + # convert them using Plaso's ReadUtf16() for proper formatting. + JOB_VARIABLE_STRUCT = construct.Struct( + 'job_variable', + construct.ULInt16('running_instance_count'), + construct.ULInt16('app_name_len'), + construct.String( + 'app_name', + lambda ctx: ctx.app_name_len * 2), + construct.ULInt16('parameter_len'), + construct.String( + 'parameter', + lambda ctx: ctx.parameter_len * 2), + construct.ULInt16('working_dir_len'), + construct.String( + 'working_dir', + lambda ctx: ctx.working_dir_len * 2), + construct.ULInt16('username_len'), + construct.String( + 'username', + lambda ctx: ctx.username_len * 2), + construct.ULInt16('comment_len'), + construct.String( + 'comment', + lambda ctx: ctx.comment_len * 2), + construct.ULInt16('userdata_len'), + construct.String( + 'userdata', + lambda ctx: ctx.userdata_len), + construct.ULInt16('reserved_len'), + construct.String( + 'reserved', + lambda ctx: ctx.reserved_len), + construct.ULInt16('test'), + construct.ULInt16('trigger_size'), + construct.ULInt16('trigger_reserved1'), + construct.ULInt16('sched_start_year'), + construct.ULInt16('sched_start_month'), + construct.ULInt16('sched_start_day'), + construct.ULInt16('sched_end_year'), + construct.ULInt16('sched_end_month'), + construct.ULInt16('sched_end_day'), + construct.ULInt16('sched_start_hour'), + construct.ULInt16('sched_start_minute'), + construct.ULInt32('sched_duration'), + construct.ULInt32('sched_interval'), + construct.ULInt32('trigger_flags'), + construct.ULInt32('trigger_type'), + construct.ULInt16('trigger_arg0'), + construct.ULInt16('trigger_arg1'), + construct.ULInt16('trigger_arg2'), + construct.ULInt16('trigger_padding'), + construct.ULInt16('trigger_reserved2'), + construct.ULInt16('trigger_reserved3')) + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract data from a Windows job file. + + This is the main parsing engine for the parser. It determines if + the selected file is a proper Scheduled task job file and extracts + the scheduled task data. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Raises: + UnableToParseFile: when the file cannot be parsed. + """ + parser_chain = self._BuildParserChain(parser_chain) + + file_object = file_entry.GetFileObject() + try: + header = self.JOB_FIXED_STRUCT.parse_stream(file_object) + except (IOError, construct.FieldError) as exception: + raise errors.UnableToParseFile( + u'Unable to parse Windows Task Job file with error: {0:s}'.format( + exception)) + + if not header.product_version in self.PRODUCT_VERSIONS: + raise errors.UnableToParseFile(u'Not a valid Scheduled Task file') + + if not header.file_version == 1: + raise errors.UnableToParseFile(u'Not a valid Scheduled Task file') + + # Obtain the relevant values from the file. + try: + data = self.JOB_VARIABLE_STRUCT.parse_stream(file_object) + except (IOError, construct.FieldError) as exception: + raise errors.UnableToParseFile( + u'Unable to parse Windows Task Job file with error: {0:s}'.format( + exception)) + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + trigger_type = self.TRIGGER_TYPES.get(data.trigger_type, u'Unknown') + + last_run_date = timelib.Timestamp.FromTimeParts( + header.ran_year, + header.ran_month, + header.ran_day, + header.ran_hour, + header.ran_minute, + header.ran_second, + microseconds=(header.ran_millisecond * 1000), + timezone=parser_context.timezone) + + scheduled_date = timelib.Timestamp.FromTimeParts( + data.sched_start_year, + data.sched_start_month, + data.sched_start_day, + data.sched_start_hour, + data.sched_start_minute, + 0, # Seconds are not stored. + timezone=parser_context.timezone) + + # Create two timeline events, one for created date and the other for last + # run. + parser_context.ProduceEvents( + [WinJobEvent( + last_run_date, eventdata.EventTimestamp.LAST_RUNTIME, data.app_name, + data.parameter, data.working_dir, data.username, trigger_type, + data.comment), + WinJobEvent( + scheduled_date, u'Scheduled To Start', data.app_name, + data.parameter, data.working_dir, data.username, trigger_type, + data.comment)], + parser_chain=parser_chain, file_entry=file_entry) + + # A scheduled end date is optional. + if data.sched_end_year: + scheduled_end_date = timelib.Timestamp.FromTimeParts( + data.sched_end_year, + data.sched_end_month, + data.sched_end_day, + 0, # Hours are not stored. + 0, # Minutes are not stored. + 0, # Seconds are not stored. + timezone=parser_context.timezone) + + event_object = WinJobEvent( + scheduled_end_date, 'Scheduled To End', data.app_name, data.parameter, + data.working_dir, data.username, trigger_type, data.comment) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + file_object.close() + + +manager.ParsersManager.RegisterParser(WinJobParser) diff --git a/plaso/parsers/winjob_test.py b/plaso/parsers/winjob_test.py new file mode 100644 index 0000000..c5af40b --- /dev/null +++ b/plaso/parsers/winjob_test.py @@ -0,0 +1,101 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Windows Scheduled Task job file parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winjob as winjob_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import test_lib +from plaso.parsers import winjob + + +class WinJobTest(test_lib.ParserTestCase): + """Tests for the Windows Scheduled Task job file parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = winjob.WinJobParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['wintask.job']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 2) + + event_object = event_objects[0] + + application_expected = ( + u'C:\\Program Files (x86)\\Google\\Update\\GoogleUpdate.exe') + self.assertEqual(event_object.application, application_expected) + + username_expected = u'Brian' + self.assertEqual(event_object.username, username_expected) + + description_expected = eventdata.EventTimestamp.LAST_RUNTIME + self.assertEqual(event_object.timestamp_desc, description_expected) + + trigger_expected = u'DAILY' + self.assertEqual(event_object.trigger, trigger_expected) + + comment_expected = ( + u'Keeps your Google software up to date. If this task is disabled or ' + u'stopped, your Google software will not be kept up to date, meaning ' + u'security vulnerabilities that may arise cannot be fixed and ' + u'features may not work. This task uninstalls itself when there is ' + u'no Google software using it.') + self.assertEqual(event_object.comment, comment_expected) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-08-24 12:42:00.112') + self.assertEqual(event_object.timestamp, expected_timestamp) + + # Parse second event. Same metadata; different timestamp event. + event_object = event_objects[1] + + self.assertEqual(event_object.application, application_expected) + self.assertEqual(event_object.username, username_expected) + self.assertEqual(event_object.trigger, trigger_expected) + self.assertEqual(event_object.comment, comment_expected) + + description_expected = u'Scheduled To Start' + self.assertEqual(event_object.timestamp_desc, description_expected) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-07-12 15:42:00') + self.assertEqual(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'Application: C:\\Program Files (x86)\\Google\\Update\\' + u'GoogleUpdate.exe /ua /installsource scheduler ' + u'Scheduled by: Brian ' + u'Run Iteration: DAILY') + + expected_msg_short = ( + u'Application: C:\\Program Files (x86)\\Google\\Update\\' + u'GoogleUpdate.exe /ua /insta...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winlnk.py b/plaso/parsers/winlnk.py new file mode 100644 index 0000000..0e4d4c3 --- /dev/null +++ b/plaso/parsers/winlnk.py @@ -0,0 +1,158 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for Windows Shortcut (LNK) files.""" + +import pylnk + +from plaso.events import time_events +from plaso.lib import errors +from plaso.lib import eventdata +from plaso.parsers import interface +from plaso.parsers import manager +from plaso.parsers.shared import shell_items + + +if pylnk.get_version() < '20141026': + raise ImportWarning('WinLnkParser requires at least pylnk 20141026.') + + +class WinLnkLinkEvent(time_events.FiletimeEvent): + """Convenience class for a Windows Shortcut (LNK) link event.""" + + DATA_TYPE = 'windows:lnk:link' + + def __init__(self, timestamp, timestamp_description, lnk_file, link_target): + """Initializes the event object. + + Args: + timestamp: The FILETIME value for the timestamp. + timestamp_description: The usage string for the timestamp value. + lnk_file: The LNK file (instance of pylnk.file). + link_target: String representation of the link target shell item list + or None. + """ + super(WinLnkLinkEvent, self).__init__(timestamp, timestamp_description) + + self.offset = 0 + self.file_size = lnk_file.file_size + self.file_attribute_flags = lnk_file.file_attribute_flags + self.drive_type = lnk_file.drive_type + self.drive_serial_number = lnk_file.drive_serial_number + self.description = lnk_file.description + self.volume_label = lnk_file.volume_label + self.local_path = lnk_file.local_path + self.network_path = lnk_file.network_path + self.command_line_arguments = lnk_file.command_line_arguments + self.env_var_location = lnk_file.environment_variables_location + self.relative_path = lnk_file.relative_path + self.working_directory = lnk_file.working_directory + self.icon_location = lnk_file.icon_location + + if link_target: + self.link_target = link_target + + +class WinLnkParser(interface.BaseParser): + """Parses Windows Shortcut (LNK) files.""" + + NAME = 'lnk' + DESCRIPTION = u'Parser for Windows Shortcut (LNK) files.' + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract data from a Windows Shortcut (LNK) file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + file_object = file_entry.GetFileObject() + self.ParseFileObject( + parser_context, file_object, file_entry=file_entry, + parser_chain=parser_chain) + file_object.close() + + def ParseFileObject( + self, parser_context, file_object, file_entry=None, parser_chain=None, + display_name=None): + """Parses a Windows Shortcut (LNK) file. + + The file entry is used to determine the display name if it was not provided. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_object: A file-like object. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + display_name: Optional display name. + + Raises: + UnableToParseFile: when the file cannot be parsed. + """ + parser_chain = self._BuildParserChain(parser_chain) + + if not display_name and file_entry: + display_name = parser_context.GetDisplayName(file_entry) + + lnk_file = pylnk.file() + lnk_file.set_ascii_codepage(parser_context.codepage) + + try: + lnk_file.open_file_object(file_object) + except IOError as exception: + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse file {1:s} with error: {2:s}'.format( + self.NAME, display_name, exception)) + + link_target = None + if lnk_file.link_target_identifier_data: + # TODO: change file_entry.name to display name once it is generated + # correctly. + if file_entry: + display_name = file_entry.name + + shell_items_parser = shell_items.ShellItemsParser(display_name) + shell_items_parser.Parse( + parser_context, lnk_file.link_target_identifier_data, + codepage=parser_context.codepage, file_entry=file_entry, + parser_chain=parser_chain) + + link_target = shell_items_parser.CopyToPath() + + parser_context.ProduceEvents( + [WinLnkLinkEvent( + lnk_file.get_file_access_time_as_integer(), + eventdata.EventTimestamp.ACCESS_TIME, lnk_file, link_target), + WinLnkLinkEvent( + lnk_file.get_file_creation_time_as_integer(), + eventdata.EventTimestamp.CREATION_TIME, lnk_file, link_target), + WinLnkLinkEvent( + lnk_file.get_file_modification_time_as_integer(), + eventdata.EventTimestamp.MODIFICATION_TIME, lnk_file, + link_target)], + parser_chain=parser_chain, file_entry=file_entry) + + # TODO: add support for the distributed link tracker. + + lnk_file.close() + + +manager.ParsersManager.RegisterParser(WinLnkParser) diff --git a/plaso/parsers/winlnk_test.py b/plaso/parsers/winlnk_test.py new file mode 100644 index 0000000..14c4017 --- /dev/null +++ b/plaso/parsers/winlnk_test.py @@ -0,0 +1,170 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Windows Shortcut (LNK) parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winlnk as winlnk_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import test_lib +from plaso.parsers import winlnk + + +class WinLnkParserTest(test_lib.ParserTestCase): + """Tests for the Windows Shortcut (LNK) parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = winlnk.WinLnkParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['example.lnk']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + # Link information: + # Creation time : Jul 13, 2009 23:29:02.849131000 UTC + # Modification time : Jul 14, 2009 01:39:18.220000000 UTC + # Access time : Jul 13, 2009 23:29:02.849131000 UTC + # Description : @%windir%\system32\migwiz\wet.dll,-590 + # Relative path : .\migwiz\migwiz.exe + # Working directory : %windir%\system32\migwiz + # Icon location : %windir%\system32\migwiz\migwiz.exe + # Environment variables location : %windir%\system32\migwiz\migwiz.exe + + self.assertEqual(len(event_objects), 3) + + # A shortcut event object. + event_object = event_objects[0] + + expected_string = u'@%windir%\\system32\\migwiz\\wet.dll,-590' + self.assertEquals(event_object.description, expected_string) + + expected_string = u'.\\migwiz\\migwiz.exe' + self.assertEquals(event_object.relative_path, expected_string) + + expected_string = u'%windir%\\system32\\migwiz' + self.assertEquals(event_object.working_directory, expected_string) + + expected_string = u'%windir%\\system32\\migwiz\\migwiz.exe' + self.assertEquals(event_object.icon_location, expected_string) + self.assertEquals(event_object.env_var_location, expected_string) + + # The last accessed timestamp. + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2009-07-13 23:29:02.849131') + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.ACCESS_TIME) + self.assertEquals(event_object.timestamp, expected_timestamp) + + # The creation timestamp. + event_object = event_objects[1] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2009-07-13 23:29:02.849131') + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.CREATION_TIME) + self.assertEquals(event_object.timestamp, expected_timestamp) + + # The last modification timestamp. + event_object = event_objects[2] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2009-07-14 01:39:18.220000') + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.MODIFICATION_TIME) + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'[@%windir%\\system32\\migwiz\\wet.dll,-590] ' + u'File size: 544768 ' + u'File attribute flags: 0x00000020 ' + u'env location: %windir%\\system32\\migwiz\\migwiz.exe ' + u'Relative path: .\\migwiz\\migwiz.exe ' + u'Working dir: %windir%\\system32\\migwiz ' + u'Icon location: %windir%\\system32\\migwiz\\migwiz.exe') + + expected_msg_short = ( + u'[@%windir%\\system32\\migwiz\\wet.dll,-590]') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + def testParseLinkTargetIdentifier(self): + """Tests the Parse function on an LNK with a link target identifier.""" + test_file = self._GetTestFilePath(['NeroInfoTool.lnk']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEqual(len(event_objects), 18) + + # A shortcut event object. + event_object = event_objects[16] + + expected_msg = ( + u'[Nero InfoTool provides you with information about the most ' + u'important features of installed drives, inserted discs, installed ' + u'software and much more. With Nero InfoTool you can find out all ' + u'about your drive and your system configuration.] ' + u'File size: 4635160 ' + u'File attribute flags: 0x00000020 ' + u'Drive type: 3 ' + u'Drive serial number: 0x70ecfa33 ' + u'Volume label: OS ' + u'Local path: C:\\Program Files (x86)\\Nero\\Nero 9\\Nero InfoTool\\' + u'InfoTool.exe ' + u'cmd arguments: -ScParameter=30002 ' + u'Relative path: ..\\..\\..\\..\\..\\..\\..\\..\\Program Files (x86)\\' + u'Nero\\Nero 9\\Nero InfoTool\\InfoTool.exe ' + u'Working dir: C:\\Program Files (x86)\\Nero\\Nero 9\\Nero InfoTool ' + u'Icon location: %ProgramFiles%\\Nero\\Nero 9\\Nero InfoTool\\' + u'InfoTool.exe ' + u'Link target: [My Computer, C:\\, Program Files (x86), Nero, Nero 9, ' + u'Nero InfoTool, InfoTool.exe]') + + expected_msg_short = ( + u'[Nero InfoTool provides you with information about the most ' + u'important feature...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + # A shell item event object. + event_object = event_objects[12] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2009-06-05 20:13:20') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'Name: InfoTool.exe ' + u'Long name: InfoTool.exe ' + u'NTFS file reference: 81349-1 ' + u'Origin: NeroInfoTool.lnk') + + expected_msg_short = ( + u'Name: InfoTool.exe ' + u'NTFS file reference: 81349-1 ' + u'Origin: NeroInfoTool.lnk') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winprefetch.py b/plaso/parsers/winprefetch.py new file mode 100644 index 0000000..d5bf21a --- /dev/null +++ b/plaso/parsers/winprefetch.py @@ -0,0 +1,504 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for Windows Prefetch files.""" + +import logging +import os + +import construct + +from plaso.events import time_events +from plaso.events import windows_events +from plaso.lib import binary +from plaso.lib import errors +from plaso.lib import eventdata +from plaso.parsers import interface +from plaso.parsers import manager + + +class WinPrefetchExecutionEvent(time_events.FiletimeEvent): + """Class that defines a Windows Prefetch execution event.""" + + DATA_TYPE = 'windows:prefetch:execution' + + def __init__( + self, timestamp, timestamp_description, file_header, file_information, + mapped_files, path, volume_serial_numbers, volume_device_paths): + """Initializes the event. + + Args: + timestamp: The FILETIME timestamp value. + timestamp_description: The usage string for the timestamp value. + file_header: The file header construct object. + file_information: The file information construct object. + mapped_files: A list of the mapped filenames. + path: A path to the executable. + volume_serial_numbers: A list of volume serial number strings. + volume_device_paths: A list of volume device path strings. + """ + super(WinPrefetchExecutionEvent, self).__init__( + timestamp, timestamp_description) + + self.offset = 0 + + self.version = file_header.get('version', None) + self.executable = binary.Ut16StreamCopyToString( + file_header.get('executable', '')) + self.prefetch_hash = file_header.get('prefetch_hash', None) + + self.run_count = file_information.get('run_count', None) + self.mapped_files = mapped_files + self.path = path + + self.number_of_volumes = file_information.get('number_of_volumes', 0) + self.volume_serial_numbers = volume_serial_numbers + self.volume_device_paths = volume_device_paths + + +class WinPrefetchParser(interface.BaseParser): + """A parser for Windows Prefetch files.""" + + NAME = 'prefetch' + DESCRIPTION = u'Parser for Windows Prefetch files.' + + FILE_SIGNATURE = 'SCCA' + + FILE_HEADER_STRUCT = construct.Struct( + 'file_header', + construct.ULInt32('version'), + construct.String('signature', 4), + construct.Padding(4), + construct.ULInt32('file_size'), + construct.String('executable', 60), + construct.ULInt32('prefetch_hash'), + construct.ULInt32('flags')) + + FILE_INFORMATION_V17 = construct.Struct( + 'file_information_v17', + construct.ULInt32('metrics_array_offset'), + construct.ULInt32('number_of_metrics_array_entries'), + construct.ULInt32('trace_chains_array_offset'), + construct.ULInt32('number_of_trace_chains_array_entries'), + construct.ULInt32('filename_strings_offset'), + construct.ULInt32('filename_strings_size'), + construct.ULInt32('volumes_information_offset'), + construct.ULInt32('number_of_volumes'), + construct.ULInt32('volumes_information_size'), + construct.ULInt64('last_run_time'), + construct.Padding(16), + construct.ULInt32('run_count'), + construct.Padding(4)) + + FILE_INFORMATION_V23 = construct.Struct( + 'file_information_v23', + construct.ULInt32('metrics_array_offset'), + construct.ULInt32('number_of_metrics_array_entries'), + construct.ULInt32('trace_chains_array_offset'), + construct.ULInt32('number_of_trace_chains_array_entries'), + construct.ULInt32('filename_strings_offset'), + construct.ULInt32('filename_strings_size'), + construct.ULInt32('volumes_information_offset'), + construct.ULInt32('number_of_volumes'), + construct.ULInt32('volumes_information_size'), + construct.Padding(8), + construct.ULInt64('last_run_time'), + construct.Padding(16), + construct.ULInt32('run_count'), + construct.Padding(84)) + + FILE_INFORMATION_V26 = construct.Struct( + 'file_information_v26', + construct.ULInt32('metrics_array_offset'), + construct.ULInt32('number_of_metrics_array_entries'), + construct.ULInt32('trace_chains_array_offset'), + construct.ULInt32('number_of_trace_chains_array_entries'), + construct.ULInt32('filename_strings_offset'), + construct.ULInt32('filename_strings_size'), + construct.ULInt32('volumes_information_offset'), + construct.ULInt32('number_of_volumes'), + construct.ULInt32('volumes_information_size'), + construct.Padding(8), + construct.ULInt64('last_run_time'), + construct.ULInt64('last_run_time1'), + construct.ULInt64('last_run_time2'), + construct.ULInt64('last_run_time3'), + construct.ULInt64('last_run_time4'), + construct.ULInt64('last_run_time5'), + construct.ULInt64('last_run_time6'), + construct.ULInt64('last_run_time7'), + construct.Padding(16), + construct.ULInt32('run_count'), + construct.Padding(96)) + + METRICS_ARRAY_ENTRY_V17 = construct.Struct( + 'metrics_array_entry_v17', + construct.ULInt32('start_time'), + construct.ULInt32('duration'), + construct.ULInt32('filename_string_offset'), + construct.ULInt32('filename_string_number_of_characters'), + construct.Padding(4)) + + # Note that at the moment for the purpose of this parser + # the v23 and v26 metrics array entry structures are the same. + METRICS_ARRAY_ENTRY_V23 = construct.Struct( + 'metrics_array_entry_v23', + construct.ULInt32('start_time'), + construct.ULInt32('duration'), + construct.ULInt32('average_duration'), + construct.ULInt32('filename_string_offset'), + construct.ULInt32('filename_string_number_of_characters'), + construct.Padding(4), + construct.ULInt64('file_reference')) + + VOLUME_INFORMATION_V17 = construct.Struct( + 'volume_information_v17', + construct.ULInt32('device_path_offset'), + construct.ULInt32('device_path_number_of_characters'), + construct.ULInt64('creation_time'), + construct.ULInt32('serial_number'), + construct.Padding(8), + construct.ULInt32('directory_strings_offset'), + construct.ULInt32('number_of_directory_strings'), + construct.Padding(4)) + + # Note that at the moment for the purpose of this parser + # the v23 and v26 volume information structures are the same. + VOLUME_INFORMATION_V23 = construct.Struct( + 'volume_information_v23', + construct.ULInt32('device_path_offset'), + construct.ULInt32('device_path_number_of_characters'), + construct.ULInt64('creation_time'), + construct.ULInt32('serial_number'), + construct.Padding(8), + construct.ULInt32('directory_strings_offset'), + construct.ULInt32('number_of_directory_strings'), + construct.Padding(68)) + + def _ParseFileHeader(self, file_object): + """Parses the file header. + + Args: + file_object: A file-like object to read data from. + + Returns: + The file header construct object. + """ + try: + file_header = self.FILE_HEADER_STRUCT.parse_stream(file_object) + except (IOError, construct.FieldError) as exception: + raise errors.UnableToParseFile( + u'Unable to parse file header with error: {0:s}'.format(exception)) + + if not file_header: + raise errors.UnableToParseFile(u'Unable to read file header') + + if file_header.get('signature', None) != self.FILE_SIGNATURE: + raise errors.UnableToParseFile(u'Unsupported file signature') + + return file_header + + def _ParseFileInformation(self, file_object, format_version): + """Parses the file information. + + Args: + file_object: A file-like object to read data from. + format_version: The format version. + + Returns: + The file information construct object. + """ + try: + if format_version == 17: + file_information = self.FILE_INFORMATION_V17.parse_stream(file_object) + elif format_version == 23: + file_information = self.FILE_INFORMATION_V23.parse_stream(file_object) + elif format_version == 26: + file_information = self.FILE_INFORMATION_V26.parse_stream(file_object) + else: + file_information = None + except (IOError, construct.FieldError) as exception: + raise errors.UnableToParseFile( + u'Unable to parse v{0:d} file information with error: {1:s}'.format( + format_version, exception)) + + if not file_information: + raise errors.UnableToParseFile( + u'Unable to read v{0:d} file information'.format(format_version)) + return file_information + + def _ParseMetricsArray(self, file_object, format_version, file_information): + """Parses the metrics array. + + Args: + file_object: A file-like object to read data from. + format_version: The format version. + file_information: The file information construct object. + + Returns: + A list of metrics array entry construct objects. + """ + metrics_array = [] + + metrics_array_offset = file_information.get('metrics_array_offset', 0) + number_of_metrics_array_entries = file_information.get( + 'number_of_metrics_array_entries', 0) + + if metrics_array_offset > 0 and number_of_metrics_array_entries > 0: + file_object.seek(metrics_array_offset, os.SEEK_SET) + + for entry_index in range(0, number_of_metrics_array_entries): + try: + if format_version == 17: + metrics_array_entry = self.METRICS_ARRAY_ENTRY_V17.parse_stream( + file_object) + elif format_version in [23, 26]: + metrics_array_entry = self.METRICS_ARRAY_ENTRY_V23.parse_stream( + file_object) + else: + metrics_array_entry = None + except (IOError, construct.FieldError) as exception: + raise errors.UnableToParseFile(( + u'Unable to parse v{0:d} metrics array entry: {1:d} with error: ' + u'{2:s}').format(format_version, entry_index, exception)) + + if not metrics_array_entry: + raise errors.UnableToParseFile( + u'Unable to read v{0:d} metrics array entry: {1:d}'.format( + format_version, entry_index)) + + metrics_array.append(metrics_array_entry) + + return metrics_array + + def _ParseFilenameStrings(self, file_object, file_information): + """Parses the filename strings. + + Args: + file_object: A file-like object to read data from. + file_information: The file information construct object. + + Returns: + A dict of filename strings with their byte offset as the key. + """ + filename_strings_offset = file_information.get('filename_strings_offset', 0) + filename_strings_size = file_information.get('filename_strings_size', 0) + + if filename_strings_offset > 0 and filename_strings_size > 0: + file_object.seek(filename_strings_offset, os.SEEK_SET) + filename_strings_data = file_object.read(filename_strings_size) + filename_strings = binary.ArrayOfUt16StreamCopyToStringTable( + filename_strings_data) + + else: + filename_strings = {} + + return filename_strings + + def _ParseVolumesInformationSection( + self, file_object, format_version, file_information): + """Parses the volumes information section. + + Args: + file_object: A file-like object to read data from. + format_version: The format version. + file_information: The file information construct object. + + Yields: + A volume information construct object. + """ + volumes_information_offset = file_information.get( + 'volumes_information_offset', 0) + + if volumes_information_offset > 0: + number_of_volumes = file_information.get('number_of_volumes', 0) + file_object.seek(volumes_information_offset, os.SEEK_SET) + + while number_of_volumes > 0: + try: + if format_version == 17: + yield self.VOLUME_INFORMATION_V17.parse_stream(file_object) + else: + yield self.VOLUME_INFORMATION_V23.parse_stream(file_object) + except (IOError, construct.FieldError) as exception: + raise errors.UnableToParseFile(( + u'Unable to parse v{0:d} volume information with error: ' + u'{1:s}').format(format_version, exception)) + + number_of_volumes -= 1 + + def _ParseVolumeDevicePath( + self, file_object, file_information, volume_information): + """Parses the volume device path. + + This function expects the current offset of the file-like object to point + as the end of the volume information structure. + + Args: + file_object: A file-like object to read data from. + file_information: The file information construct object. + volume_information: The volume information construct object. + + Returns: + A Unicode string containing the device path or None if not available. + """ + volumes_information_offset = file_information.get( + 'volumes_information_offset', 0) + + device_path = None + if volumes_information_offset > 0: + device_path_offset = volume_information.get('device_path_offset', 0) + device_path_size = 2 * volume_information.get( + 'device_path_number_of_characters', 0) + + if device_path_offset >= 36 and device_path_size > 0: + device_path_offset += volumes_information_offset + current_offset = file_object.tell() + + file_object.seek(device_path_offset, os.SEEK_SET) + device_path = binary.ReadUtf16Stream( + file_object, byte_size=device_path_size) + + file_object.seek(current_offset, os.SEEK_SET) + + return device_path + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extracts events from a Windows Prefetch file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Raises: + UnableToParseFile: when the file cannot be parsed. + """ + file_object = file_entry.GetFileObject() + file_header = self._ParseFileHeader(file_object) + + format_version = file_header.get('version', None) + if format_version not in [17, 23, 26]: + raise errors.UnableToParseFile( + u'Unsupported format version: {0:d}'.format(format_version)) + + # Add ourselves to the parser chain, which will be used in all subsequent + # event creation in this parser. + parser_chain = self._BuildParserChain(parser_chain) + + file_information = self._ParseFileInformation(file_object, format_version) + metrics_array = self._ParseMetricsArray( + file_object, format_version, file_information) + try: + filename_strings = self._ParseFilenameStrings( + file_object, file_information) + except UnicodeDecodeError as exception: + logging.warning(( + u'[{0:s}] Unable to parse filename information from file {1:s} ' + u'with error: {2:s}').format( + parser_chain, + file_entry.path_spec.comparable.replace(u'\n', u';'), + exception)) + filename_strings = {} + + if len(metrics_array) != len(filename_strings): + logging.debug( + u'Mismatch in number of metrics and filename strings array entries.') + + executable = binary.Ut16StreamCopyToString( + file_header.get('executable', u'')) + + volume_serial_numbers = [] + volume_device_paths = [] + path = u'' + + for volume_information in self._ParseVolumesInformationSection( + file_object, format_version, file_information): + volume_serial_number = volume_information.get('serial_number', 0) + volume_device_path = self._ParseVolumeDevicePath( + file_object, file_information, volume_information) + + volume_serial_numbers.append(volume_serial_number) + volume_device_paths.append(volume_device_path) + + timestamp = volume_information.get('creation_time', 0) + if timestamp: + event_object = windows_events.WindowsVolumeCreationEvent( + timestamp, volume_device_path, volume_serial_number, + file_entry.name) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + for filename in filename_strings.itervalues(): + if not filename: + continue + if (filename.startswith(volume_device_path) and + filename.endswith(executable)): + _, _, path = filename.partition(volume_device_path) + + mapped_files = [] + for metrics_array_entry in metrics_array: + file_reference = metrics_array_entry.get('file_reference', 0) + filename_string_offset = metrics_array_entry.get( + 'filename_string_offset', 0) + + filename = filename_strings.get(filename_string_offset, u'') + if not filename: + logging.debug(u'Missing filename string for offset: {0:d}.'.format( + filename_string_offset)) + continue + + if file_reference: + mapped_file_string = ( + u'{0:s} [MFT entry: {1:d}, sequence: {2:d}]').format( + filename, file_reference & 0xffffffffffffL, + file_reference >> 48) + else: + mapped_file_string = filename + + mapped_files.append(mapped_file_string) + + timestamp = file_information.get('last_run_time', 0) + if timestamp: + event_object = WinPrefetchExecutionEvent( + timestamp, eventdata.EventTimestamp.LAST_RUNTIME, file_header, + file_information, mapped_files, path, volume_serial_numbers, + volume_device_paths) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + # Check for the 7 older last run time values available in v26. + if format_version == 26: + for last_run_time_index in range(1, 8): + last_run_time_identifier = 'last_run_time{0:d}'.format( + last_run_time_index) + + timestamp = file_information.get(last_run_time_identifier, 0) + if timestamp: + event_object = WinPrefetchExecutionEvent( + timestamp, + u'Previous {0:s}'.format(eventdata.EventTimestamp.LAST_RUNTIME), + file_header, file_information, mapped_files, path, + volume_serial_numbers, volume_device_paths) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + file_object.close() + + +manager.ParsersManager.RegisterParser(WinPrefetchParser) diff --git a/plaso/parsers/winprefetch_test.py b/plaso/parsers/winprefetch_test.py new file mode 100644 index 0000000..8cd1256 --- /dev/null +++ b/plaso/parsers/winprefetch_test.py @@ -0,0 +1,378 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Windows prefetch parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winprefetch as winprefetch_formatter +from plaso.lib import eventdata +from plaso.lib import timelib_test +from plaso.parsers import test_lib +from plaso.parsers import winprefetch + + +class WinPrefetchParserTest(test_lib.ParserTestCase): + """Tests for the Windows prefetch parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = winprefetch.WinPrefetchParser() + + def testParse17(self): + """Tests the Parse function on a version 17 Prefetch file.""" + test_file = self._GetTestFilePath(['CMD.EXE-087B4001.pf']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 2) + + # The prefetch last run event. + event_object = event_objects[1] + + self.assertEquals(event_object.version, 17) + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-03-10 10:11:49.281250') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.LAST_RUNTIME) + self.assertEquals(event_object.executable, u'CMD.EXE') + self.assertEquals(event_object.prefetch_hash, 0x087b4001) + self.assertEquals(event_object.volume_serial_numbers[0], 0x24cb074b) + + expected_mapped_files = [ + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\NTDLL.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\KERNEL32.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\UNICODE.NLS', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\LOCALE.NLS', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\SORTTBLS.NLS', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\MSVCRT.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\CMD.EXE', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\USER32.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\GDI32.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\SHIMENG.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\APPPATCH\\SYSMAIN.SDB', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\APPPATCH\\ACGENRAL.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\ADVAPI32.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\RPCRT4.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\WINMM.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\OLE32.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\OLEAUT32.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\MSACM32.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\VERSION.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\SHELL32.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\SHLWAPI.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\USERENV.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\UXTHEME.DLL', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\CTYPE.NLS', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\SORTKEY.NLS', + (u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\WINSXS\\X86_MICROSOFT.WINDOWS.' + u'COMMON-CONTROLS_6595B64144CCF1DF_6.0.2600.2180_X-WW_A84F1FF9\\' + u'COMCTL32.DLL'), + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\WINDOWSSHELL.MANIFEST', + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\SYSTEM32\\COMCTL32.DLL', + (u'\\DEVICE\\HARDDISKVOLUME1\\D50FF1E628137B1A251B47AB9466\\UPDATE\\' + u'UPDATE.EXE.MANIFEST'), + u'\\DEVICE\\HARDDISKVOLUME1\\$MFT', + (u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\IE7\\SPUNINST\\SPUNINST.EXE.' + u'MANIFEST'), + (u'\\DEVICE\\HARDDISKVOLUME1\\D50FF1E628137B1A251B47AB9466\\UPDATE\\' + u'IERESETICONS.EXE'), + u'\\DEVICE\\HARDDISKVOLUME1\\WINDOWS\\IE7\\SPUNINST\\IERESETICONS.EXE'] + + self.assertEquals(event_object.mapped_files, expected_mapped_files) + + # The volume creation event. + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-03-10 10:19:46.234375') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.CREATION_TIME) + + expected_msg = ( + u'\\DEVICE\\HARDDISKVOLUME1 ' + u'Serial number: 0x24CB074B ' + u'Origin: CMD.EXE-087B4001.pf') + + expected_msg_short = ( + u'\\DEVICE\\HARDDISKVOLUME1 ' + u'Origin: CMD.EXE-087B4001.pf') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + def testParse23(self): + """Tests the Parse function on a version 23 Prefetch file.""" + test_file = self._GetTestFilePath(['PING.EXE-B29F6629.pf']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 2) + + # The prefetch last run event. + event_object = event_objects[1] + self.assertEquals(event_object.version, 23) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-04-06 19:00:55.932955') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.LAST_RUNTIME) + + self.assertEquals(event_object.executable, u'PING.EXE') + self.assertEquals(event_object.prefetch_hash, 0xb29f6629) + self.assertEquals( + event_object.path, u'\\WINDOWS\\SYSTEM32\\PING.EXE') + self.assertEquals(event_object.run_count, 14) + self.assertEquals( + event_object.volume_device_paths[0], u'\\DEVICE\\HARDDISKVOLUME1') + self.assertEquals(event_object.volume_serial_numbers[0], 0xac036525) + + expected_msg = ( + u'Prefetch [PING.EXE] was executed - run count 14 path: ' + u'\\WINDOWS\\SYSTEM32\\PING.EXE ' + u'hash: 0xB29F6629 ' + u'volume: 1 [serial number: 0xAC036525, ' + u'device path: \\DEVICE\\HARDDISKVOLUME1]') + + expected_msg_short = u'PING.EXE was run 14 time(s)' + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + # The volume creation event. + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2010-11-10 17:37:26.484375') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.CREATION_TIME) + + def testParse23MultiVolume(self): + """Tests the Parse function on a mulit volume version 23 Prefetch file.""" + test_file = self._GetTestFilePath(['WUAUCLT.EXE-830BCC14.pf']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 6) + + # The prefetch last run event. + event_object = event_objects[5] + self.assertEquals(event_object.version, 23) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-03-15 21:17:39.807996') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.LAST_RUNTIME) + + self.assertEquals(event_object.executable, u'WUAUCLT.EXE') + self.assertEquals(event_object.prefetch_hash, 0x830bcc14) + self.assertEquals( + event_object.path, u'\\WINDOWS\\SYSTEM32\\WUAUCLT.EXE') + self.assertEquals(event_object.run_count, 25) + self.assertEquals( + event_object.volume_device_paths[0], u'\\DEVICE\\HARDDISKVOLUME1') + self.assertEquals(event_object.volume_serial_numbers[0], 0xac036525) + + expected_msg = ( + u'Prefetch [WUAUCLT.EXE] was executed - run count 25 path: ' + u'\\WINDOWS\\SYSTEM32\\WUAUCLT.EXE ' + u'hash: 0x830BCC14 ' + u'volume: 1 [serial number: 0xAC036525, ' + u'device path: \\DEVICE\\HARDDISKVOLUME1], ' + u'volume: 2 [serial number: 0xAC036525, ' + u'device path: \\DEVICE\\HARDDISKVOLUMESHADOWCOPY2], ' + u'volume: 3 [serial number: 0xAC036525, ' + u'device path: \\DEVICE\\HARDDISKVOLUMESHADOWCOPY4], ' + u'volume: 4 [serial number: 0xAC036525, ' + u'device path: \\DEVICE\\HARDDISKVOLUMESHADOWCOPY7], ' + u'volume: 5 [serial number: 0xAC036525, ' + u'device path: \\DEVICE\\HARDDISKVOLUMESHADOWCOPY8]') + + expected_msg_short = u'WUAUCLT.EXE was run 25 time(s)' + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + # The volume creation event. + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2010-11-10 17:37:26.484375') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.CREATION_TIME) + + expected_msg = ( + u'\\DEVICE\\HARDDISKVOLUME1 ' + u'Serial number: 0xAC036525 ' + u'Origin: WUAUCLT.EXE-830BCC14.pf') + + expected_msg_short = ( + u'\\DEVICE\\HARDDISKVOLUME1 ' + u'Origin: WUAUCLT.EXE-830BCC14.pf') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + def testParse26(self): + """Tests the Parse function on a version 26 Prefetch file.""" + test_file = self._GetTestFilePath(['TASKHOST.EXE-3AE259FC.pf']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 5) + + # The prefetch last run event. + event_object = event_objects[1] + self.assertEquals(event_object.version, 26) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-10-04 15:40:09.037833') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.LAST_RUNTIME) + self.assertEquals(event_object.executable, u'TASKHOST.EXE') + self.assertEquals(event_object.prefetch_hash, 0x3ae259fc) + + # The prefetch previous last run event. + event_object = event_objects[2] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-10-04 15:28:09.010356') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, + u'Previous {0:s}'.format(eventdata.EventTimestamp.LAST_RUNTIME)) + + expected_mapped_files = [ + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\NTDLL.DLL ' + u'[MFT entry: 46299, sequence: 1]'), + u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\TASKHOST.EXE', + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\KERNEL32.DLL ' + u'[MFT entry: 45747, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\KERNELBASE.DLL ' + u'[MFT entry: 45734, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\LOCALE.NLS ' + u'[MFT entry: 45777, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\MSVCRT.DLL ' + u'[MFT entry: 46033, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\RPCRT4.DLL ' + u'[MFT entry: 46668, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\COMBASE.DLL ' + u'[MFT entry: 44616, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\OLEAUT32.DLL ' + u'[MFT entry: 46309, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\OLE32.DLL ' + u'[MFT entry: 46348, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\RPCSS.DLL ' + u'[MFT entry: 46654, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\KERNEL.APPCORE.DLL ' + u'[MFT entry: 45698, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\CRYPTBASE.DLL ' + u'[MFT entry: 44560, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\BCRYPTPRIMITIVES.DLL ' + u'[MFT entry: 44355, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\USER32.DLL ' + u'[MFT entry: 47130, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\GDI32.DLL ' + u'[MFT entry: 45344, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\EN-US\\' + u'TASKHOST.EXE.MUI'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\SECHOST.DLL ' + u'[MFT entry: 46699, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\CLBCATQ.DLL ' + u'[MFT entry: 44511, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\RACENGN.DLL ' + u'[MFT entry: 46549, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\NTMARTA.DLL ' + u'[MFT entry: 46262, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\WEVTAPI.DLL ' + u'[MFT entry: 47223, sequence: 1]'), + u'\\DEVICE\\HARDDISKVOLUME2\\$MFT', + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\SQMAPI.DLL ' + u'[MFT entry: 46832, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\AEPIC.DLL ' + u'[MFT entry: 43991, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\WINTRUST.DLL ' + u'[MFT entry: 47372, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\SLWGA.DLL ' + u'[MFT entry: 46762, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\DXGI.DLL ' + u'[MFT entry: 44935, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\ESENT.DLL ' + u'[MFT entry: 45256, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\WMICLNT.DLL ' + u'[MFT entry: 47413, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\ADVAPI32.DLL ' + u'[MFT entry: 43994, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\SFC_OS.DLL ' + u'[MFT entry: 46729, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\VERSION.DLL ' + u'[MFT entry: 47120, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\CRYPT32.DLL ' + u'[MFT entry: 44645, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\MSASN1.DLL ' + u'[MFT entry: 45909, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\WTSAPI32.DLL ' + u'[MFT entry: 47527, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\SPPC.DLL ' + u'[MFT entry: 46803, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\POWRPROF.DLL ' + u'[MFT entry: 46413, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\PROFAPI.DLL ' + u'[MFT entry: 46441, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\PROGRAMDATA\\MICROSOFT\\RAC\\STATEDATA\\' + u'RACMETADATA.DAT [MFT entry: 39345, sequence: 2]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\GLOBALIZATION\\SORTING\\' + u'SORTDEFAULT.NLS [MFT entry: 37452, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\RACRULES.XML ' + u'[MFT entry: 46509, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\TASKSCHD.DLL ' + u'[MFT entry: 47043, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\SSPICLI.DLL ' + u'[MFT entry: 46856, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\XMLLITE.DLL ' + u'[MFT entry: 47569, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\PROGRAMDATA\\MICROSOFT\\RAC\\STATEDATA\\' + u'RACWMIEVENTDATA.DAT [MFT entry: 23870, sequence: 3]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\PROGRAMDATA\\MICROSOFT\\RAC\\STATEDATA\\' + u'RACWMIDATABOOKMARKS.DAT [MFT entry: 23871, sequence: 2]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\TPMTASKS.DLL ' + u'[MFT entry: 47003, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\NCRYPT.DLL ' + u'[MFT entry: 46073, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\BCRYPT.DLL ' + u'[MFT entry: 44346, sequence: 1]'), + (u'\\DEVICE\\HARDDISKVOLUME2\\WINDOWS\\SYSTEM32\\NTASN1.DLL ' + u'[MFT entry: 46261, sequence: 1]')] + + self.assertEquals(event_object.mapped_files, expected_mapped_files) + + # The volume creation event. + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-10-04 15:57:26.146547') + self.assertEquals(event_object.timestamp, expected_timestamp) + self.assertEquals( + event_object.timestamp_desc, eventdata.EventTimestamp.CREATION_TIME) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg.py b/plaso/parsers/winreg.py new file mode 100644 index 0000000..4cb488e --- /dev/null +++ b/plaso/parsers/winreg.py @@ -0,0 +1,336 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for Windows NT Registry (REGF) files.""" + +import logging +import os + +from plaso.lib import errors +from plaso.parsers import interface +from plaso.parsers import manager +from plaso.winreg import cache +from plaso.winreg import winregistry + + +# TODO: add tests for this class. +class PluginList(object): + """A simple class that stores information about Windows Registry plugins.""" + + def __init__(self): + """Initializes the plugin list object.""" + super(PluginList, self).__init__() + self._key_plugins = {} + self._value_plugins = {} + + def __iter__(self): + """Return an iterator of all Windows Registry plugins.""" + ret = [] + _ = map(ret.extend, self._key_plugins.values()) + _ = map(ret.extend, self._value_plugins.values()) + for item in ret: + yield item + + def _GetPluginsByType(self, plugins_dict, plugin_type): + """Retrieves the Windows Registry plugins of a specific type. + + Args: + plugins_dict: Dictionary containing the Windows Registry plugins + by plugin type. + plugin_type: String containing the Windows Registry type, + e.g. NTUSER, SOFTWARE. + + Returns: + A list containing the Windows Registry plugins (instances of + RegistryPlugin) for the specific plugin type. + """ + return plugins_dict.get(plugin_type, []) + plugins_dict.get('any', []) + + def AddPlugin(self, plugin_type, plugin_class): + """Add a Windows Registry plugin to the plugin list. + + Args: + plugin_type: String containing the Windows Registry type, + e.g. NTUSER, SOFTWARE. + plugin_class: The plugin class that is being registered. + """ + # Cannot import the interface here otherwise this will create a cyclic + # dependency. + if hasattr(plugin_class, 'REG_VALUES'): + self._value_plugins.setdefault(plugin_type, []).append(plugin_class) + + else: + self._key_plugins.setdefault(plugin_type, []).append(plugin_class) + + def GetAllKeyPlugins(self): + """Return all key plugins as a list.""" + ret = [] + _ = map(ret.extend, self._key_plugins.values()) + return ret + + def GetAllValuePlugins(self): + """Return a list of a all classes that implement value-based plugins.""" + ret = [] + _ = map(ret.extend, self._value_plugins.values()) + return ret + + def GetExpandedKeyPaths( + self, parser_context, reg_cache=None, plugin_names=None): + """Retrieves a list of expanded Windows Registry key paths. + + Args: + parser_context: A parser context object (instance of ParserContext). + reg_cache: Optional Windows Registry objects cache (instance of + WinRegistryCache). The default is None. + plugin_names: Optional list of plugin names, if defined only keys from + these plugins will be expanded. The default is None which + means all key plugins will get expanded keys. + + Returns: + A list of expanded Windows Registry key paths. + """ + key_paths = [] + for key_plugin_cls in self.GetAllKeyPlugins(): + key_plugin = key_plugin_cls(reg_cache=reg_cache) + + if plugin_names and key_plugin.NAME not in plugin_names: + continue + key_plugin.ExpandKeys(parser_context) + if not key_plugin.expanded_keys: + continue + + for key_path in key_plugin.expanded_keys: + if key_path not in key_paths: + key_paths.append(key_path) + + return key_paths + + def GetKeyPlugins(self, plugin_type): + """Retrieves the Windows Registry key-based plugins of a specific type. + + Args: + plugin_type: String containing the Windows Registry type, + e.g. NTUSER, SOFTWARE. + + Returns: + A list containing the Windows Registry plugins (instances of + RegistryPlugin) for the specific plugin type. + """ + return self._GetPluginsByType(self._key_plugins, plugin_type) + + def GetTypes(self): + """Return a set of all plugins supported.""" + return set(self._key_plugins).union(self._value_plugins) + + def GetValuePlugins(self, plugin_type): + """Retrieves the Windows Registry value-based plugins of a specific type. + + Args: + plugin_type: String containing the Windows Registry type, + e.g. NTUSER, SOFTWARE. + + Returns: + A list containing the Windows Registry plugins (instances of + RegistryPlugin) for the specific plugin type. + """ + return self._GetPluginsByType(self._value_plugins, plugin_type) + + def GetWeights(self): + """Return a set of all weights/priority of the loaded plugins.""" + return set(plugin.WEIGHT for plugin in self.GetAllValuePlugins()).union( + plugin.WEIGHT for plugin in self.GetAllKeyPlugins()) + + def GetWeightPlugins(self, weight, plugin_type=''): + """Return a list of all plugins for a given weight or priority. + + Each plugin defines a weight or a priority that defines in which order + it should be processed in the case of a parser that applies priority. + + This method returns all plugins, whether they are key or value based + that use a defined weight or priority and are defined to parse keys + or values found in a certain Windows Registry type. + + Args: + weight: An integer representing the weight or priority (usually a + number from 1 to 3). + plugin_type: A string that defines the Windows Registry type, eg. NTUSER, + SOFTWARE, etc. + + Returns: + A list that contains all the plugins that fit the defined criteria. + """ + ret = [] + for reg_plugin in self.GetKeyPlugins(plugin_type): + if reg_plugin.WEIGHT == weight: + ret.append(reg_plugin) + + for reg_plugin in self.GetValuePlugins(plugin_type): + if reg_plugin.WEIGHT == weight: + ret.append(reg_plugin) + + return ret + + +class WinRegistryParser(interface.BasePluginsParser): + """Parses Windows NT Registry (REGF) files.""" + + NAME = 'winreg' + DESCRIPTION = u'Parser for Windows NT Registry (REGF) files.' + + _plugin_classes = {} + + # List of types Windows Registry types and required keys to identify each of + # these types. + REG_TYPES = { + 'NTUSER': ('\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer',), + 'SOFTWARE': ('\\Microsoft\\Windows\\CurrentVersion\\App Paths',), + 'SECURITY': ('\\Policy\\PolAdtEv',), + 'SYSTEM': ('\\Select',), + 'SAM': ('\\SAM\\Domains\\Account\\Users',), + 'UNKNOWN': (), + } + + def __init__(self): + """Initializes a parser object.""" + super(WinRegistryParser, self).__init__() + self._plugins = WinRegistryParser.GetPluginList() + + def _RecurseKey(self, key): + """A generator that takes a key and yields every subkey of it.""" + # In the case of a Registry file not having a root key we will not be able + # to traverse the Registry, in which case we need to return here. + if not key: + return + + yield key + + for subkey in key.GetSubkeys(): + for recursed_key in self._RecurseKey(subkey): + yield recursed_key + + @classmethod + def GetPluginList(cls): + """Build a list of all available plugins. + + Returns: + A plugins list (instance of PluginList). + """ + plugins_list = PluginList() + for _, plugin_class in cls.GetPlugins(): + plugins_list.AddPlugin(plugin_class.REG_TYPE, plugin_class) + return plugins_list + + def Parse(self, parser_context, file_entry, parser_chain=None): + """Extract data from a Windows Registry file. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: A file entry object (instance of dfvfs.FileEntry). + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + # TODO: Remove this magic reads when the classifier has been + # implemented, until then we need to make sure we are dealing with + # a Windows NT Registry file before proceeding. + magic = 'regf' + + file_object = file_entry.GetFileObject() + file_object.seek(0, os.SEEK_SET) + data = file_object.read(len(magic)) + file_object.close() + + if data != magic: + raise errors.UnableToParseFile(( + u'[{0:s}] unable to parse file: {1:s} with error: invalid ' + u'signature.').format(self.NAME, file_entry.name)) + + registry = winregistry.WinRegistry( + winregistry.WinRegistry.BACKEND_PYREGF) + + # Determine type, find all parsers + try: + winreg_file = registry.OpenFile( + file_entry, codepage=parser_context.codepage) + except IOError as exception: + raise errors.UnableToParseFile( + u'[{0:s}] unable to parse file: {1:s} with error: {2:s}'.format( + self.NAME, file_entry.name, exception)) + + # Detect the Windows Registry file type. + registry_type = 'UNKNOWN' + for reg_type in self.REG_TYPES: + if reg_type == 'UNKNOWN': + continue + + # Check if all the known keys for a certain Registry file exist. + known_keys_found = True + for known_key_path in self.REG_TYPES[reg_type]: + if not winreg_file.GetKeyByPath(known_key_path): + known_keys_found = False + break + + if known_keys_found: + registry_type = reg_type + break + + self._registry_type = registry_type + logging.debug( + u'Windows Registry file {0:s}: detected as: {1:s}'.format( + file_entry.name, registry_type)) + + registry_cache = cache.WinRegistryCache() + registry_cache.BuildCache(winreg_file, registry_type) + + plugins = {} + number_of_plugins = 0 + for weight in self._plugins.GetWeights(): + plist = self._plugins.GetWeightPlugins(weight, registry_type) + plugins[weight] = [] + for plugin in plist: + plugins[weight].append(plugin(reg_cache=registry_cache)) + number_of_plugins += 1 + + logging.debug( + u'Number of plugins for this Windows Registry file: {0:d}.'.format( + number_of_plugins)) + + # Recurse through keys in the file and apply the plugins in the order: + # 1. file type specific key-based plugins. + # 2. generic key-based plugins. + # 3. file type specific value-based plugins. + # 4. generic value-based plugins. + root_key = winreg_file.GetKeyByPath(u'\\') + + parser_chain = self._BuildParserChain(parser_chain) + + for key in self._RecurseKey(root_key): + for weight in plugins.iterkeys(): + # TODO: determine if the plugin matches the key and continue + # to the next key. + for plugin in plugins[weight]: + if parser_context.abort: + break + + plugin.Process( + parser_context, file_entry=file_entry, key=key, + registry_type=self._registry_type, + codepage=parser_context.codepage, parser_chain=parser_chain) + + winreg_file.Close() + + +manager.ParsersManager.RegisterParser(WinRegistryParser) diff --git a/plaso/parsers/winreg_plugins/__init__.py b/plaso/parsers/winreg_plugins/__init__.py new file mode 100644 index 0000000..9464d3a --- /dev/null +++ b/plaso/parsers/winreg_plugins/__init__.py @@ -0,0 +1,42 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the import statements for the Registry plugins.""" + +from plaso.parsers.winreg_plugins import appcompatcache +from plaso.parsers.winreg_plugins import bagmru +from plaso.parsers.winreg_plugins import ccleaner +from plaso.parsers.winreg_plugins import default +from plaso.parsers.winreg_plugins import lfu +from plaso.parsers.winreg_plugins import mountpoints +from plaso.parsers.winreg_plugins import mrulist +from plaso.parsers.winreg_plugins import mrulistex +from plaso.parsers.winreg_plugins import msie_zones +from plaso.parsers.winreg_plugins import officemru +from plaso.parsers.winreg_plugins import outlook +from plaso.parsers.winreg_plugins import run +from plaso.parsers.winreg_plugins import sam_users +from plaso.parsers.winreg_plugins import services +from plaso.parsers.winreg_plugins import shutdown +from plaso.parsers.winreg_plugins import task_scheduler +from plaso.parsers.winreg_plugins import terminal_server +from plaso.parsers.winreg_plugins import typedurls +from plaso.parsers.winreg_plugins import userassist +from plaso.parsers.winreg_plugins import usb +from plaso.parsers.winreg_plugins import usbstor +from plaso.parsers.winreg_plugins import winrar +from plaso.parsers.winreg_plugins import winver diff --git a/plaso/parsers/winreg_plugins/appcompatcache.py b/plaso/parsers/winreg_plugins/appcompatcache.py new file mode 100644 index 0000000..49b9566 --- /dev/null +++ b/plaso/parsers/winreg_plugins/appcompatcache.py @@ -0,0 +1,624 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Windows Registry plugin to parse the Application Compatibility Cache key.""" + +import construct +import logging + +from plaso.events import time_events +from plaso.lib import binary +from plaso.lib import eventdata +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface + + +class AppCompatCacheEvent(time_events.FiletimeEvent): + """Class that contains the event object for AppCompatCache entries.""" + + DATA_TYPE = 'windows:registry:appcompatcache' + + def __init__( + self, filetime, usage, key, entry_index, path, offset): + """Initializes a Windows Registry event. + + Args: + filetime: The FILETIME timestamp value. + usage: The description of the usage of the time value. + key: Name of the Registry key being parsed. + entry_index: The cache entry index number for the record. + path: The full path to the executable. + offset: The (data) offset of the Registry key or value. + """ + super(AppCompatCacheEvent, self).__init__(filetime, usage) + + self.keyname = key + self.offset = offset + self.entry_index = entry_index + self.path = path + + +class AppCompatCacheHeader(object): + """Class that contains the Application Compatibility Cache header.""" + + def __init__(self): + """Initializes the header object.""" + super(AppCompatCacheHeader, self).__init__() + self.number_of_cached_entries = 0 + self.header_size = 0 + + +class AppCompatCacheCachedEntry(object): + """Class that contains the Application Compatibility Cache cached entry.""" + + def __init__(self): + """Initializes the cached entry object.""" + super(AppCompatCacheCachedEntry, self).__init__() + self.cached_entry_size = 0 + self.data = None + self.file_size = None + self.insertion_flags = None + self.last_modification_time = None + self.last_update_time = None + self.shim_flags = None + self.path = None + + +class AppCompatCachePlugin(interface.KeyPlugin): + """Class that parses the Application Compatibility Cache Registry data.""" + + NAME = 'winreg_appcompatcache' + DESCRIPTION = u'Parser for Application Compatibility Cache Registry data.' + + REG_KEYS = [ + u'\\{current_control_set}\\Control\\Session Manager\\AppCompatibility', + u'\\{current_control_set}\\Control\\Session Manager\\AppCompatCache'] + REG_TYPE = 'SYSTEM' + URL = [ + (u'https://code.google.com/p/winreg-kb/wiki/' + u'ApplicationCompatibilityCacheKey')] + + _FORMAT_TYPE_2000 = 1 + _FORMAT_TYPE_XP = 2 + _FORMAT_TYPE_2003 = 3 + _FORMAT_TYPE_VISTA = 4 + _FORMAT_TYPE_7 = 5 + _FORMAT_TYPE_8 = 6 + + # AppCompatCache format signature used in Windows XP. + _HEADER_SIGNATURE_XP = 0xdeadbeef + + # AppCompatCache format used in Windows XP. + _HEADER_XP_32BIT_STRUCT = construct.Struct( + 'appcompatcache_header_xp', + construct.ULInt32('signature'), + construct.ULInt32('number_of_cached_entries'), + construct.ULInt32('unknown1'), + construct.ULInt32('unknown2'), + construct.Padding(384)) + + _CACHED_ENTRY_XP_32BIT_STRUCT = construct.Struct( + 'appcompatcache_cached_entry_xp_32bit', + construct.Array(528, construct.Byte('path')), + construct.ULInt64('last_modification_time'), + construct.ULInt64('file_size'), + construct.ULInt64('last_update_time')) + + # AppCompatCache format signature used in Windows 2003, Vista and 2008. + _HEADER_SIGNATURE_2003 = 0xbadc0ffe + + # AppCompatCache format used in Windows 2003. + _HEADER_2003_STRUCT = construct.Struct( + 'appcompatcache_header_2003', + construct.ULInt32('signature'), + construct.ULInt32('number_of_cached_entries')) + + _CACHED_ENTRY_2003_32BIT_STRUCT = construct.Struct( + 'appcompatcache_cached_entry_2003_32bit', + construct.ULInt16('path_size'), + construct.ULInt16('maximum_path_size'), + construct.ULInt32('path_offset'), + construct.ULInt64('last_modification_time'), + construct.ULInt64('file_size')) + + _CACHED_ENTRY_2003_64BIT_STRUCT = construct.Struct( + 'appcompatcache_cached_entry_2003_64bit', + construct.ULInt16('path_size'), + construct.ULInt16('maximum_path_size'), + construct.ULInt32('unknown1'), + construct.ULInt64('path_offset'), + construct.ULInt64('last_modification_time'), + construct.ULInt64('file_size')) + + # AppCompatCache format used in Windows Vista and 2008. + _CACHED_ENTRY_VISTA_32BIT_STRUCT = construct.Struct( + 'appcompatcache_cached_entry_vista_32bit', + construct.ULInt16('path_size'), + construct.ULInt16('maximum_path_size'), + construct.ULInt32('path_offset'), + construct.ULInt64('last_modification_time'), + construct.ULInt32('insertion_flags'), + construct.ULInt32('shim_flags')) + + _CACHED_ENTRY_VISTA_64BIT_STRUCT = construct.Struct( + 'appcompatcache_cached_entry_vista_64bit', + construct.ULInt16('path_size'), + construct.ULInt16('maximum_path_size'), + construct.ULInt32('unknown1'), + construct.ULInt64('path_offset'), + construct.ULInt64('last_modification_time'), + construct.ULInt32('insertion_flags'), + construct.ULInt32('shim_flags')) + + # AppCompatCache format signature used in Windows 7 and 2008 R2. + _HEADER_SIGNATURE_7 = 0xbadc0fee + + # AppCompatCache format used in Windows 7 and 2008 R2. + _HEADER_7_STRUCT = construct.Struct( + 'appcompatcache_header_7', + construct.ULInt32('signature'), + construct.ULInt32('number_of_cached_entries'), + construct.Padding(120)) + + _CACHED_ENTRY_7_32BIT_STRUCT = construct.Struct( + 'appcompatcache_cached_entry_7_32bit', + construct.ULInt16('path_size'), + construct.ULInt16('maximum_path_size'), + construct.ULInt32('path_offset'), + construct.ULInt64('last_modification_time'), + construct.ULInt32('insertion_flags'), + construct.ULInt32('shim_flags'), + construct.ULInt32('data_size'), + construct.ULInt32('data_offset')) + + _CACHED_ENTRY_7_64BIT_STRUCT = construct.Struct( + 'appcompatcache_cached_entry_7_64bit', + construct.ULInt16('path_size'), + construct.ULInt16('maximum_path_size'), + construct.ULInt32('unknown1'), + construct.ULInt64('path_offset'), + construct.ULInt64('last_modification_time'), + construct.ULInt32('insertion_flags'), + construct.ULInt32('shim_flags'), + construct.ULInt64('data_size'), + construct.ULInt64('data_offset')) + + # AppCompatCache format used in Windows 8.0 and 8.1. + _HEADER_SIGNATURE_8 = 0x00000080 + + _HEADER_8_STRUCT = construct.Struct( + 'appcompatcache_header_8', + construct.ULInt32('signature'), + construct.Padding(124)) + + _CACHED_ENTRY_HEADER_8_STRUCT = construct.Struct( + 'appcompatcache_cached_entry_header_8', + construct.ULInt32('signature'), + construct.ULInt32('unknown1'), + construct.ULInt32('cached_entry_data_size'), + construct.ULInt16('path_size')) + + # AppCompatCache format used in Windows 8.0. + _CACHED_ENTRY_SIGNATURE_8_0 = '00ts' + + # AppCompatCache format used in Windows 8.1. + _CACHED_ENTRY_SIGNATURE_8_1 = '10ts' + + def _CheckSignature(self, value_data): + """Parses and validates the signature. + + Args: + value_data: a binary string containing the value data. + + Returns: + The format type if successful or None otherwise. + """ + signature = construct.ULInt32('signature').parse(value_data) + if signature == self._HEADER_SIGNATURE_XP: + return self._FORMAT_TYPE_XP + + elif signature == self._HEADER_SIGNATURE_2003: + # TODO: determine which format version is used (2003 or Vista). + return self._FORMAT_TYPE_2003 + + elif signature == self._HEADER_SIGNATURE_7: + return self._FORMAT_TYPE_7 + + elif signature == self._HEADER_SIGNATURE_8: + if value_data[signature:signature + 4] in [ + self._CACHED_ENTRY_SIGNATURE_8_0, self._CACHED_ENTRY_SIGNATURE_8_1]: + return self._FORMAT_TYPE_8 + + def _DetermineCacheEntrySize( + self, format_type, value_data, cached_entry_offset): + """Determines the size of a cached entry. + + Args: + format_type: integer value that contains the format type. + value_data: a binary string containing the value data. + cached_entry_offset: integer value that contains the offset of + the first cached entry data relative to the start of + the value data. + + Returns: + The cached entry size if successful or None otherwise. + + Raises: + RuntimeError: if the format type is not supported. + """ + if format_type not in [ + self._FORMAT_TYPE_XP, self._FORMAT_TYPE_2003, self._FORMAT_TYPE_VISTA, + self._FORMAT_TYPE_7, self._FORMAT_TYPE_8]: + raise RuntimeError( + u'[{0:s}] Unsupported format type: {1:d}'.format( + self.NAME, format_type)) + + cached_entry_data = value_data[cached_entry_offset:] + cached_entry_size = 0 + + if format_type == self._FORMAT_TYPE_XP: + cached_entry_size = self._CACHED_ENTRY_XP_32BIT_STRUCT.sizeof() + + elif format_type in [ + self._FORMAT_TYPE_2003, self._FORMAT_TYPE_VISTA, self._FORMAT_TYPE_7]: + path_size = construct.ULInt16('path_size').parse(cached_entry_data[0:2]) + maximum_path_size = construct.ULInt16('maximum_path_size').parse( + cached_entry_data[2:4]) + path_offset_32bit = construct.ULInt32('path_offset').parse( + cached_entry_data[4:8]) + path_offset_64bit = construct.ULInt32('path_offset').parse( + cached_entry_data[8:16]) + + if maximum_path_size < path_size: + logging.error( + u'[{0:s}] Path size value out of bounds.'.format(self.NAME)) + return + + path_end_of_string_size = maximum_path_size - path_size + if path_size == 0 or path_end_of_string_size != 2: + logging.error( + u'[{0:s}] Unsupported path size values.'.format(self.NAME)) + return + + # Assume the entry is 64-bit if the 32-bit path offset is 0 and + # the 64-bit path offset is set. + if path_offset_32bit == 0 and path_offset_64bit != 0: + if format_type == self._FORMAT_TYPE_2003: + cached_entry_size = self._CACHED_ENTRY_2003_64BIT_STRUCT.sizeof() + elif format_type == self._FORMAT_TYPE_VISTA: + cached_entry_size = self._CACHED_ENTRY_VISTA_64BIT_STRUCT.sizeof() + elif format_type == self._FORMAT_TYPE_7: + cached_entry_size = self._CACHED_ENTRY_7_64BIT_STRUCT.sizeof() + + else: + if format_type == self._FORMAT_TYPE_2003: + cached_entry_size = self._CACHED_ENTRY_2003_32BIT_STRUCT.sizeof() + elif format_type == self._FORMAT_TYPE_VISTA: + cached_entry_size = self._CACHED_ENTRY_VISTA_32BIT_STRUCT.sizeof() + elif format_type == self._FORMAT_TYPE_7: + cached_entry_size = self._CACHED_ENTRY_7_32BIT_STRUCT.sizeof() + + elif format_type == self._FORMAT_TYPE_8: + cached_entry_size = self._CACHED_ENTRY_HEADER_8_STRUCT.sizeof() + + return cached_entry_size + + def _ParseHeader(self, format_type, value_data): + """Parses the header. + + Args: + format_type: integer value that contains the format type. + value_data: a binary string containing the value data. + + Returns: + A header object (instance of AppCompatCacheHeader). + + Raises: + RuntimeError: if the format type is not supported. + """ + if format_type not in [ + self._FORMAT_TYPE_XP, self._FORMAT_TYPE_2003, self._FORMAT_TYPE_VISTA, + self._FORMAT_TYPE_7, self._FORMAT_TYPE_8]: + raise RuntimeError( + u'[{0:s}] Unsupported format type: {1:d}'.format( + self.NAME, format_type)) + + # TODO: change to collections.namedtuple or use __slots__ if the overhead + # of a regular object becomes a problem. + header_object = AppCompatCacheHeader() + + if format_type == self._FORMAT_TYPE_XP: + header_object.header_size = self._HEADER_XP_32BIT_STRUCT.sizeof() + header_struct = self._HEADER_XP_32BIT_STRUCT.parse(value_data) + + elif format_type == self._FORMAT_TYPE_2003: + header_object.header_size = self._HEADER_2003_STRUCT.sizeof() + header_struct = self._HEADER_2003_STRUCT.parse(value_data) + + elif format_type == self._FORMAT_TYPE_VISTA: + header_object.header_size = self._HEADER_VISTA_STRUCT.sizeof() + header_struct = self._HEADER_VISTA_STRUCT.parse(value_data) + + elif format_type == self._FORMAT_TYPE_7: + header_object.header_size = self._HEADER_7_STRUCT.sizeof() + header_struct = self._HEADER_7_STRUCT.parse(value_data) + + elif format_type == self._FORMAT_TYPE_8: + header_object.header_size = self._HEADER_8_STRUCT.sizeof() + header_struct = self._HEADER_8_STRUCT.parse(value_data) + + if format_type in [ + self._FORMAT_TYPE_XP, self._FORMAT_TYPE_2003, self._FORMAT_TYPE_VISTA, + self._FORMAT_TYPE_7]: + header_object.number_of_cached_entries = header_struct.get( + 'number_of_cached_entries') + + return header_object + + def _ParseCachedEntry( + self, format_type, value_data, cached_entry_offset, cached_entry_size): + """Parses a cached entry. + + Args: + format_type: integer value that contains the format type. + value_data: a binary string containing the value data. + cached_entry_offset: integer value that contains the offset of + the cached entry data relative to the start of + the value data. + cached_entry_size: integer value that contains the cached entry data size. + + Returns: + A cached entry object (instance of AppCompatCacheCachedEntry). + + Raises: + RuntimeError: if the format type is not supported. + """ + if format_type not in [ + self._FORMAT_TYPE_XP, self._FORMAT_TYPE_2003, self._FORMAT_TYPE_VISTA, + self._FORMAT_TYPE_7, self._FORMAT_TYPE_8]: + raise RuntimeError( + u'[{0:s}] Unsupported format type: {1:d}'.format( + self.NAME, format_type)) + + cached_entry_data = value_data[ + cached_entry_offset:cached_entry_offset + cached_entry_size] + + cached_entry_struct = None + + if format_type == self._FORMAT_TYPE_XP: + if cached_entry_size == self._CACHED_ENTRY_XP_32BIT_STRUCT.sizeof(): + cached_entry_struct = self._CACHED_ENTRY_XP_32BIT_STRUCT.parse( + cached_entry_data) + + elif format_type == self._FORMAT_TYPE_2003: + if cached_entry_size == self._CACHED_ENTRY_2003_32BIT_STRUCT.sizeof(): + cached_entry_struct = self._CACHED_ENTRY_2003_32BIT_STRUCT.parse( + cached_entry_data) + + elif cached_entry_size == self._CACHED_ENTRY_2003_64BIT_STRUCT.sizeof(): + cached_entry_struct = self._CACHED_ENTRY_2003_64BIT_STRUCT.parse( + cached_entry_data) + + elif format_type == self._FORMAT_TYPE_VISTA: + if cached_entry_size == self._CACHED_ENTRY_VISTA_32BIT_STRUCT.sizeof(): + cached_entry_struct = self._CACHED_ENTRY_VISTA_32BIT_STRUCT.parse( + cached_entry_data) + + elif cached_entry_size == self._CACHED_ENTRY_VISTA_64BIT_STRUCT.sizeof(): + cached_entry_struct = self._CACHED_ENTRY_VISTA_64BIT_STRUCT.parse( + cached_entry_data) + + elif format_type == self._FORMAT_TYPE_7: + if cached_entry_size == self._CACHED_ENTRY_7_32BIT_STRUCT.sizeof(): + cached_entry_struct = self._CACHED_ENTRY_7_32BIT_STRUCT.parse( + cached_entry_data) + + elif cached_entry_size == self._CACHED_ENTRY_7_64BIT_STRUCT.sizeof(): + cached_entry_struct = self._CACHED_ENTRY_7_64BIT_STRUCT.parse( + cached_entry_data) + + elif format_type == self._FORMAT_TYPE_8: + if cached_entry_data[0:4] not in [ + self._CACHED_ENTRY_SIGNATURE_8_0, self._CACHED_ENTRY_SIGNATURE_8_1]: + raise RuntimeError( + u'[{0:s}] Unsupported cache entry signature'.format(self.NAME)) + + if cached_entry_size == self._CACHED_ENTRY_HEADER_8_STRUCT.sizeof(): + cached_entry_struct = self._CACHED_ENTRY_HEADER_8_STRUCT.parse( + cached_entry_data) + + cached_entry_data_size = cached_entry_struct.get( + 'cached_entry_data_size') + cached_entry_size = 12 + cached_entry_data_size + + cached_entry_data = value_data[ + cached_entry_offset:cached_entry_offset + cached_entry_size] + + if not cached_entry_struct: + raise RuntimeError( + u'[{0:s}] Unsupported cache entry size: {1:d}'.format( + self.NAME, cached_entry_size)) + + cached_entry_object = AppCompatCacheCachedEntry() + cached_entry_object.cached_entry_size = cached_entry_size + + path_offset = 0 + data_size = 0 + + if format_type == self._FORMAT_TYPE_XP: + string_size = 0 + for string_index in xrange(0, 528, 2): + if (ord(cached_entry_data[string_index]) == 0 and + ord(cached_entry_data[string_index + 1]) == 0): + break + string_size += 2 + + cached_entry_object.path = binary.Ut16StreamCopyToString( + cached_entry_data[0:string_size]) + + elif format_type in [ + self._FORMAT_TYPE_2003, self._FORMAT_TYPE_VISTA, self._FORMAT_TYPE_7]: + path_size = cached_entry_struct.get('path_size') + path_offset = cached_entry_struct.get('path_offset') + + elif format_type == self._FORMAT_TYPE_8: + path_size = cached_entry_struct.get('path_size') + + cached_entry_data_offset = 14 + path_size + cached_entry_object.path = binary.Ut16StreamCopyToString( + cached_entry_data[14:cached_entry_data_offset]) + + remaining_data = cached_entry_data[cached_entry_data_offset:] + + cached_entry_object.insertion_flags = construct.ULInt32( + 'insertion_flags').parse(remaining_data[0:4]) + cached_entry_object.shim_flags = construct.ULInt32( + 'shim_flags').parse(remaining_data[4:8]) + + if cached_entry_data[0:4] == self._CACHED_ENTRY_SIGNATURE_8_0: + cached_entry_data_offset += 8 + + elif cached_entry_data[0:4] == self._CACHED_ENTRY_SIGNATURE_8_1: + cached_entry_data_offset += 10 + + remaining_data = cached_entry_data[cached_entry_data_offset:] + + if format_type in [ + self._FORMAT_TYPE_XP, self._FORMAT_TYPE_2003, self._FORMAT_TYPE_VISTA, + self._FORMAT_TYPE_7]: + cached_entry_object.last_modification_time = cached_entry_struct.get( + 'last_modification_time') + + elif format_type == self._FORMAT_TYPE_8: + cached_entry_object.last_modification_time = construct.ULInt64( + 'last_modification_time').parse(remaining_data[0:8]) + + if format_type in [self._FORMAT_TYPE_XP, self._FORMAT_TYPE_2003]: + cached_entry_object.file_size = cached_entry_struct.get('file_size') + + elif format_type in [self._FORMAT_TYPE_VISTA, self._FORMAT_TYPE_7]: + cached_entry_object.insertion_flags = cached_entry_struct.get( + 'insertion_flags') + cached_entry_object.shim_flags = cached_entry_struct.get('shim_flags') + + if format_type == self._FORMAT_TYPE_XP: + cached_entry_object.last_update_time = cached_entry_struct.get( + 'last_update_time') + + if format_type == self._FORMAT_TYPE_7: + data_offset = cached_entry_struct.get('data_offset') + data_size = cached_entry_struct.get('data_size') + + elif format_type == self._FORMAT_TYPE_8: + data_offset = cached_entry_offset + cached_entry_data_offset + 12 + data_size = construct.ULInt32('data_size').parse(remaining_data[8:12]) + + if path_offset > 0 and path_size > 0: + path_size += path_offset + + cached_entry_object.path = binary.Ut16StreamCopyToString( + value_data[path_offset:path_size]) + + if data_size > 0: + data_size += data_offset + + cached_entry_object.data = value_data[data_offset:data_size] + + return cached_entry_object + + def GetEntries(self, parser_context, key=None, file_entry=None, + parser_chain=None, **unused_kwargs): + """Extracts event objects from a Application Compatibility Cache key. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + """ + value = key.GetValue('AppCompatCache') + if not value: + return + + value_data = value.data + value_data_size = len(value.data) + + format_type = self._CheckSignature(value_data) + if not format_type: + # TODO: Instead of logging emit a parser error object that once that + # mechanism is implemented. + logging.error( + u'AppCompatCache format error: [{0:s}] Unsupported signature'.format( + key.path)) + return + + header_object = self._ParseHeader(format_type, value_data) + + # On Windows Vista and 2008 when the cache is empty it will + # only consist of the header. + if value_data_size <= header_object.header_size: + return + + cached_entry_offset = header_object.header_size + cached_entry_size = self._DetermineCacheEntrySize( + format_type, value_data, cached_entry_offset) + + if not cached_entry_size: + # TODO: Instead of logging emit a parser error object that once that + # mechanism is implemented. + logging.error( + u'AppCompatCache format error: [{0:s}] Unsupported cached entry ' + u'size.'.format(key.path)) + return + + cached_entry_index = 0 + while cached_entry_offset < value_data_size: + cached_entry_object = self._ParseCachedEntry( + format_type, value_data, cached_entry_offset, cached_entry_size) + + if cached_entry_object.last_modification_time is not None: + # TODO: refactor to file modification event. + event_object = AppCompatCacheEvent( + cached_entry_object.last_modification_time, + u'File Last Modification Time', key.path, + cached_entry_index + 1, cached_entry_object.path, + cached_entry_offset) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if cached_entry_object.last_update_time is not None: + # TODO: refactor to process run event. + event_object = AppCompatCacheEvent( + cached_entry_object.last_update_time, + eventdata.EventTimestamp.LAST_RUNTIME, key.path, + cached_entry_index + 1, cached_entry_object.path, + cached_entry_offset) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + cached_entry_offset += cached_entry_object.cached_entry_size + cached_entry_index += 1 + + if (header_object.number_of_cached_entries != 0 and + cached_entry_index >= header_object.number_of_cached_entries): + break + + +winreg.WinRegistryParser.RegisterPlugin(AppCompatCachePlugin) diff --git a/plaso/parsers/winreg_plugins/appcompatcache_test.py b/plaso/parsers/winreg_plugins/appcompatcache_test.py new file mode 100644 index 0000000..84dca55 --- /dev/null +++ b/plaso/parsers/winreg_plugins/appcompatcache_test.py @@ -0,0 +1,73 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Application Compatibility Cache key Windows Registry plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.lib import timelib_test +from plaso.parsers.winreg_plugins import appcompatcache +from plaso.parsers.winreg_plugins import test_lib + + +class AppCompatCacheRegistryPluginTest(test_lib.RegistryPluginTestCase): + """Tests for the AppCompatCache Windows Registry plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = appcompatcache.AppCompatCachePlugin() + + def testProcess(self): + """Tests the Process function.""" + knowledge_base_values = {'current_control_set': u'ControlSet001'} + test_file_entry = self._GetTestFileEntryFromPath(['SYSTEM']) + key_path = u'\\ControlSet001\\Control\\Session Manager\\AppCompatCache' + winreg_key = self._GetKeyFromFileEntry(test_file_entry, key_path) + + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_key, + knowledge_base_values=knowledge_base_values, file_entry=test_file_entry) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 330) + + event_object = event_objects[9] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-04-04 01:46:37.932964') + self.assertEquals(event_object.timestamp, expected_timestamp) + + self.assertEquals(event_object.pathspec, test_file_entry.path_spec) + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + self.assertEquals(event_object.keyname, key_path) + expected_msg = ( + u'[{0:s}] Cached entry: 10 Path: ' + u'\\??\\C:\\Windows\\PSEXESVC.EXE'.format(event_object.keyname)) + + expected_msg_short = ( + u'Path: \\??\\C:\\Windows\\PSEXESVC.EXE') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/bagmru.py b/plaso/parsers/winreg_plugins/bagmru.py new file mode 100644 index 0000000..5e65b2e --- /dev/null +++ b/plaso/parsers/winreg_plugins/bagmru.py @@ -0,0 +1,206 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains BagMRU Windows Registry plugins (shellbags).""" + +import logging + +import construct + +from plaso.events import windows_events +from plaso.parsers.shared import shell_items +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface + + +class BagMRUPlugin(interface.KeyPlugin): + """Class that defines a BagMRU Windows Registry plugin.""" + + NAME = 'winreg_bagmru' + DESCRIPTION = u'Parser for BagMRU Registry data.' + + # TODO: remove REG_TYPE and use HKEY_CURRENT_USER instead. + REG_TYPE = 'any' + + REG_KEYS = frozenset([ + u'\\Software\\Microsoft\\Windows\\Shell\\BagMRU', + u'\\Software\\Microsoft\\Windows\\ShellNoRoam\\BagMRU', + (u'\\Local Settings\\Software\\Microsoft\\Windows\\' + u'Shell\\BagMRU'), + (u'\\Local Settings\\Software\\Microsoft\\Windows\\' + u'ShellNoRoam\\BagMRU'), + (u'\\Wow6432Node\\Local Settings\\Software\\' + u'Microsoft\\Windows\\Shell\\BagMRU'), + (u'\\Wow6432Node\\Local Settings\\Software\\' + u'Microsoft\\Windows\\ShellNoRoam\\BagMRU')]) + + URLS = [u'https://code.google.com/p/winreg-kb/wiki/MRUKeys'] + + _MRULISTEX_STRUCT = construct.Range(1, 500, construct.ULInt32('entry_number')) + + def _ParseMRUListExEntryValue( + self, parser_context, key, entry_index, entry_number, text_dict, + value_strings, parent_value_string, codepage='cp1252', file_entry=None, + parser_chain=None, **unused_kwargs): + """Parses the MRUListEx entry value. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: the Registry key (instance of winreg.WinRegKey) that contains + the MRUListEx value. + entry_index: integer value representing the MRUListEx entry index. + entry_number: integer value representing the entry number. + text_dict: text dictionary object to append textual strings. + value_strings: value string dictionary object to append value strings. + parent_value_string: string containing the parent value string. + codepage: Optional extended ASCII string codepage. The default is cp1252. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + value = key.GetValue(u'{0:d}'.format(entry_number)) + value_string = u'' + if value is None: + logging.debug( + u'[{0:s}] Missing MRUListEx entry value: {1:d} in key: {2:s}.'.format( + self.name, entry_number, key.path)) + + elif not value.DataIsBinaryData(): + logging.debug(( + u'[{0:s}] Non-binary MRUListEx entry value: {1:d} in key: ' + u'{2:s}.').format(self.name, entry_number, key.path)) + + elif value.data: + shell_items_parser = shell_items.ShellItemsParser(key.path) + shell_items_parser.Parse( + parser_context, value.data, codepage=codepage, file_entry=file_entry, + parser_chain=parser_chain) + + value_string = shell_items_parser.CopyToPath() + if parent_value_string: + value_string = u', '.join([parent_value_string, value_string]) + + value_strings[entry_number] = value_string + + value_string = u'Shell item list: [{0:s}]'.format(value_string) + + value_text = u'Index: {0:d} [MRU Value {1:d}]'.format( + entry_index + 1, entry_number) + + text_dict[value_text] = value_string + + def _ParseMRUListExValue(self, key): + """Parsed the MRUListEx value in a given Registry key. + + Args: + key: the Registry key (instance of winreg.WinRegKey) that contains + the MRUListEx value. + + Returns: + A MRUListEx value generator, which returns the MRU index number + and entry value. + """ + mru_list_value = key.GetValue('MRUListEx') + if not mru_list_value: + return enumerate([]) + + try: + mru_list = self._MRULISTEX_STRUCT.parse(mru_list_value.data) + except construct.FieldError: + logging.warning(u'[{0:s}] Unable to parse the MRU key: {1:s}'.format( + self.name, key.path)) + return enumerate([]) + + return enumerate(mru_list) + + def _ParseSubKey( + self, parser_context, key, parent_value_string, registry_type=None, + file_entry=None, parser_chain=None, codepage='cp1252'): + """Extract event objects from a MRUListEx Registry key. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: the Registry key (instance of winreg.WinRegKey). + parent_value_string: string containing the parent value string. + registry_type: Optional Registry type string. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + codepage: Optional extended ASCII string codepage. The default is cp1252. + """ + text_dict = {} + value_strings = {} + for index, entry_number in self._ParseMRUListExValue(key): + # TODO: detect if list ends prematurely. + # MRU lists are terminated with 0xffffffff (-1). + if entry_number == 0xffffffff: + break + + self._ParseMRUListExEntryValue( + parser_context, key, index, entry_number, text_dict, value_strings, + parent_value_string, codepage=codepage, file_entry=file_entry, + parser_chain=parser_chain) + + event_object = windows_events.WindowsRegistryEvent( + key.last_written_timestamp, key.path, text_dict, + offset=key.offset, registry_type=registry_type, urls=self.URLS, + source_append=': BagMRU') + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + for index, entry_number in self._ParseMRUListExValue(key): + # TODO: detect if list ends prematurely. + # MRU lists are terminated with 0xffffffff (-1). + if entry_number == 0xffffffff: + break + + sub_key = key.GetSubkey(u'{0:d}'.format(entry_number)) + if not sub_key: + logging.debug( + u'[{0:s}] Missing BagMRU sub key: {1:d} in key: {2:s}.'.format( + self.name, key.path, entry_number)) + continue + + value_string = value_strings.get(entry_number, u'') + self._ParseSubKey( + parser_context, sub_key, value_string, file_entry=file_entry, + parser_chain=parser_chain, codepage=codepage) + + def GetEntries( + self, parser_context, key=None, registry_type=None, codepage='cp1252', + file_entry=None, parser_chain=None, **unused_kwargs): + """Extract event objects from a Registry key containing a MRUListEx value. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + codepage: Optional extended ASCII string codepage. The default is cp1252. + """ + self._ParseSubKey( + parser_context, key, u'', registry_type=registry_type, + codepage=codepage, parser_chain=parser_chain, file_entry=file_entry) + + +winreg.WinRegistryParser.RegisterPlugin(BagMRUPlugin) diff --git a/plaso/parsers/winreg_plugins/bagmru_test.py b/plaso/parsers/winreg_plugins/bagmru_test.py new file mode 100644 index 0000000..90ae118 --- /dev/null +++ b/plaso/parsers/winreg_plugins/bagmru_test.py @@ -0,0 +1,99 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the BagMRU Windows Registry plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.lib import timelib_test +from plaso.parsers.winreg_plugins import bagmru +from plaso.parsers.winreg_plugins import test_lib + + +class TestBagMRUPlugin(test_lib.RegistryPluginTestCase): + """Tests for the BagMRU plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = bagmru.BagMRUPlugin() + + def testProcess(self): + """Tests the Process function.""" + test_file_entry = self._GetTestFileEntryFromPath(['NTUSER.DAT']) + key_path = ( + u'\\Software\\Microsoft\\Windows\\ShellNoRoam\\BagMRU') + winreg_key = self._GetKeyFromFileEntry(test_file_entry, key_path) + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_key, file_entry=test_file_entry) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 15) + + event_object = event_objects[0] + + self.assertEquals(event_object.pathspec, test_file_entry.path_spec) + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2009-08-04 15:19:16.997750') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'[{0:s}] ' + u'Index: 1 [MRU Value 0]: ' + u'Shell item list: [My Computer]').format(key_path) + + expected_msg_short = ( + u'[{0:s}] Index: 1 [MRU Value 0]: Shel...').format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + event_object = event_objects[1] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2009-08-04 15:19:10.669625') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'[{0:s}\\0] ' + u'Index: 1 [MRU Value 0]: ' + u'Shell item list: [My Computer, C:\\]').format(key_path) + + expected_msg_short = ( + u'[{0:s}\\0] Index: 1 [MRU Value 0]: Sh...').format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + event_object = event_objects[14] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2009-08-04 15:19:16.997750') + self.assertEquals(event_object.timestamp, expected_timestamp) + + # The winreg_formatter will add a space after the key path even when there + # is not text. + expected_msg = u'[{0:s}\\0\\0\\0\\0\\0] '.format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/ccleaner.py b/plaso/parsers/winreg_plugins/ccleaner.py new file mode 100644 index 0000000..de4eab9 --- /dev/null +++ b/plaso/parsers/winreg_plugins/ccleaner.py @@ -0,0 +1,87 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Parser for the CCleaner Registry key.""" + +from plaso.events import windows_events +from plaso.lib import timelib +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface + + +__author__ = 'Marc Seguin (segumarc@gmail.com)' + + +class CCleanerPlugin(interface.KeyPlugin): + """Gathers the CCleaner Keys for NTUSER hive.""" + + NAME = 'winreg_ccleaner' + DESCRIPTION = u'Parser for CCleaner Registry data.' + + REG_KEYS = [u'\\Software\\Piriform\\CCleaner'] + REG_TYPE = 'NTUSER' + + URLS = [(u'http://cheeky4n6monkey.blogspot.com/2012/02/writing-ccleaner' + u'-regripper-plugin-part_05.html')] + + def GetEntries( + self, parser_context, key=None, registry_type=None, file_entry=None, + parser_chain=None, **unused_kwargs): + """Extracts event objects from a CCleaner Registry key. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + for value in key.GetValues(): + if not value.name or not value.data: + continue + + text_dict = {} + text_dict[value.name] = value.data + + if value.name == u'UpdateKey': + timestamp = timelib.Timestamp.FromTimeString( + value.data, timezone=parser_context.timezone) + event_object = windows_events.WindowsRegistryEvent( + timestamp, key.path, text_dict, offset=key.offset, + registry_type=registry_type) + + elif value.name == '0': + event_object = windows_events.WindowsRegistryEvent( + key.timestamp, key.path, text_dict, offset=key.offset, + registry_type=registry_type) + + else: + # TODO: change this event not to set a timestamp of 0. + event_object = windows_events.WindowsRegistryEvent( + 0, key.path, text_dict, offset=key.offset, + registry_type=registry_type) + + event_object.source_append = u': CCleaner Registry key' + + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +winreg.WinRegistryParser.RegisterPlugin(CCleanerPlugin) diff --git a/plaso/parsers/winreg_plugins/ccleaner_test.py b/plaso/parsers/winreg_plugins/ccleaner_test.py new file mode 100644 index 0000000..432bbb2 --- /dev/null +++ b/plaso/parsers/winreg_plugins/ccleaner_test.py @@ -0,0 +1,83 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the CCleaner Windows Registry plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.lib import timelib_test +from plaso.parsers.winreg_plugins import ccleaner +from plaso.parsers.winreg_plugins import test_lib + + +__author__ = 'Marc Seguin (segumarc@gmail.com)' + + +class CCleanerRegistryPluginTest(test_lib.RegistryPluginTestCase): + """Tests for the CCleaner Windows Registry plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = ccleaner.CCleanerPlugin() + + def testProcess(self): + """Tests the Process function.""" + test_file_entry = self._GetTestFileEntryFromPath(['NTUSER-CCLEANER.DAT']) + key_path = u'\\Software\\Piriform\\CCleaner' + winreg_key = self._GetKeyFromFileEntry(test_file_entry, key_path) + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_key, file_entry=test_file_entry) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 17) + + event_object = event_objects[0] + + self.assertEquals(event_object.pathspec, test_file_entry.path_spec) + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2013-07-13 10:03:14') + self.assertEquals(event_object.timestamp, expected_timestamp) + + regvalue_identifier = u'UpdateKey' + expected_value = u'07/13/2013 10:03:14 AM' + self._TestRegvalue(event_object, regvalue_identifier, expected_value) + + expected_string = u'[{0:s}] {1:s}: {2:s}'.format( + key_path, regvalue_identifier, expected_value) + self._TestGetMessageStrings(event_object, expected_string, expected_string) + + event_object = event_objects[2] + + self.assertEquals(event_object.timestamp, 0) + + regvalue_identifier = u'(App)Delete Index.dat files' + expected_value = u'True' + self._TestRegvalue(event_object, regvalue_identifier, expected_value) + + expected_string = u'[{0:s}] {1:s}: {2:s}'.format( + key_path, regvalue_identifier, expected_value) + self._TestGetMessageStrings(event_object, expected_string, expected_string) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/default.py b/plaso/parsers/winreg_plugins/default.py new file mode 100644 index 0000000..a8a0e49 --- /dev/null +++ b/plaso/parsers/winreg_plugins/default.py @@ -0,0 +1,120 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The default Windows Registry plugin.""" + +from plaso.events import windows_events +from plaso.lib import utils +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface + + +class DefaultPlugin(interface.KeyPlugin): + """Default plugin that extracts minimum information from every registry key. + + The default plugin will parse every registry key that is passed to it and + extract minimum information, such as a list of available values and if + possible content of those values. The timestamp used is the timestamp + when the registry key was last modified. + """ + + NAME = 'winreg_default' + DESCRIPTION = u'Parser for Registry data.' + + REG_TYPE = 'any' + REG_KEYS = [] + + # This is a special case, plugins normally never overwrite the priority. + # However the default plugin should only run when all others plugins have + # tried and failed. + WEIGHT = 3 + + def GetEntries( + self, parser_context, key=None, registry_type=None, file_entry=None, + parser_chain=None, **unused_kwargs): + """Returns an event object based on a Registry key name and values. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + """ + text_dict = {} + + if key.number_of_values == 0: + text_dict[u'Value'] = u'No values stored in key.' + + else: + for value in key.GetValues(): + if not value.name: + value_name = '(default)' + else: + value_name = u'{0:s}'.format(value.name) + + if value.data is None: + value_string = u'[{0:s}] Empty'.format( + value.data_type_string) + elif value.DataIsString(): + string_decode = utils.GetUnicodeString(value.data) + value_string = u'[{0:s}] {1:s}'.format( + value.data_type_string, string_decode) + elif value.DataIsInteger(): + value_string = u'[{0:s}] {1:d}'.format( + value.data_type_string, value.data) + elif value.DataIsMultiString(): + if type(value.data) not in (list, tuple): + value_string = u'[{0:s}]'.format(value.data_type_string) + # TODO: Add a flag or some sort of an anomaly alert. + else: + value_string = u'[{0:s}] {1:s}'.format( + value.data_type_string, u''.join(value.data)) + else: + value_string = u'[{0:s}]'.format(value.data_type_string) + + text_dict[value_name] = value_string + + event_object = windows_events.WindowsRegistryEvent( + key.last_written_timestamp, key.path, text_dict, + offset=key.offset, registry_type=registry_type) + + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + # Even though the DefaultPlugin is derived from KeyPlugin it needs to + # overwrite the Process function to make sure it is called when no other + # plugin is available. + + def Process( + self, parser_context, key=None, registry_type=None, + parser_chain=None, **kwargs): + """Process the key and return a generator to extract event objects. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + """ + # Note that we should NOT call the Process function of the KeyPlugin here. + parser_chain = self._BuildParserChain(parser_chain) + self.GetEntries( + parser_context, key=key, registry_type=registry_type, + parser_chain=parser_chain, **kwargs) + + +winreg.WinRegistryParser.RegisterPlugin(DefaultPlugin) diff --git a/plaso/parsers/winreg_plugins/default_test.py b/plaso/parsers/winreg_plugins/default_test.py new file mode 100644 index 0000000..23a4b70 --- /dev/null +++ b/plaso/parsers/winreg_plugins/default_test.py @@ -0,0 +1,79 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the default Windows Registry plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.parsers.winreg_plugins import default +from plaso.parsers.winreg_plugins import test_lib +from plaso.winreg import test_lib as winreg_test_lib + + +class TestDefaultRegistry(test_lib.RegistryPluginTestCase): + """Tests for the default Windows Registry plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = default.DefaultPlugin() + + def testProcess(self): + """Tests the Process function.""" + key_path = u'\\Microsoft\\Some Windows\\InterestingApp\\MRU' + values = [] + values.append(winreg_test_lib.TestRegValue( + 'MRUList', 'acb'.encode('utf_16_le'), 1, 123)) + values.append(winreg_test_lib.TestRegValue( + 'a', 'Some random text here'.encode('utf_16_le'), 1, 1892)) + values.append(winreg_test_lib.TestRegValue( + 'b', 'c:/evil.exe'.encode('utf_16_le'), 3, 612)) + values.append(winreg_test_lib.TestRegValue( + 'c', 'C:/looks_legit.exe'.encode('utf_16_le'), 1, 1001)) + + winreg_key = winreg_test_lib.TestRegKey( + key_path, 1346145829002031, values, 1456) + + event_queue_consumer = self._ParseKeyWithPlugin(self._plugin, winreg_key) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 1) + + event_object = event_objects[0] + + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + self.assertEquals(event_object.timestamp, 1346145829002031) + + expected_msg = ( + u'[{0:s}] ' + u'MRUList: [REG_SZ] acb ' + u'a: [REG_SZ] Some random text here ' + u'b: [REG_BINARY] ' + u'c: [REG_SZ] C:/looks_legit.exe').format(key_path) + + expected_msg_short = ( + u'[{0:s}] MRUList: [REG_SZ] acb a: [REG_SZ...').format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/interface.py b/plaso/parsers/winreg_plugins/interface.py new file mode 100644 index 0000000..dd23953 --- /dev/null +++ b/plaso/parsers/winreg_plugins/interface.py @@ -0,0 +1,263 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The Windows Registry plugin objects interface.""" + +import abc +import logging + +from plaso.parsers import plugins +from plaso.winreg import path_expander as winreg_path_expander + + +class RegistryPlugin(plugins.BasePlugin): + """Class that defines the Windows Registry plugin object interface.""" + + __abstract = True + + NAME = 'winreg' + DESCRIPTION = u'Parser for Registry data.' + + # Indicate the type of hive this plugin belongs to (eg. NTUSER, SOFTWARE). + REG_TYPE = 'any' + + # URLS should contain a list of URLs with additional information about this + # key or value. + URLS = [] + + # WEIGHT is a simple integer value representing the priority of this plugin. + # The weight can be used by some parser implementation to prioritize the + # order in which plugins are run against the Windows Registry keys. + # By default no the Windows Registry plugin should overwrite this value, + # it should only be defined in interfaces extending the base class, providing + # higher level of prioritization to Windows Registry plugins. + WEIGHT = 3 + + def __init__(self, reg_cache=None): + """Initializes Windows Registry plugin object. + + Args: + reg_cache: Optional Windows Registry objects cache (instance of + WinRegistryCache). The default is None. + """ + super(RegistryPlugin, self).__init__() + # TODO: Clean this up, this value is stored but not used. + self._reg_cache = reg_cache + + @abc.abstractmethod + def GetEntries( + self, parser_context, file_entry=None, key=None, registry_type=None, + parser_chain=None, codepage='cp1252', **kwargs): + """Extracts event objects from the Windows Registry key. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: optional file entry object (instance of dfvfs.FileEntry). + The default is None. + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type. The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + codepage: Optional extended ASCII string codepage. The default is cp1252. + """ + + def Process(self, parser_context, parser_chain=None, key=None, **kwargs): + """Processes a Windows Registry key or value. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + + Raises: + ValueError: If the key value is not set. + """ + if key is None: + raise ValueError(u'Key is not set.') + + del kwargs['file_entry'] + del kwargs['registry_type'] + del kwargs['codepage'] + + # This will raise if unhandled keyword arguments are passed. + super(RegistryPlugin, self).Process(parser_context, parser_chain, **kwargs) + + +class KeyPlugin(RegistryPlugin): + """Class that defines the Windows Registry key-based plugin interface.""" + + __abstract = True + + # A list of all the Windows Registry key paths this plugins supports. + # Each of these key paths can contain a path that needs to be expanded, + # such as {current_control_set}, etc. + REG_KEYS = [] + + WEIGHT = 1 + + def __init__(self, reg_cache=None): + """Initializes key-based Windows Registry plugin object. + + Args: + reg_cache: Optional Windows Registry objects cache (instance of + WinRegistryCache). The default is None. + """ + super(KeyPlugin, self).__init__(reg_cache=reg_cache) + self._path_expander = winreg_path_expander.WinRegistryKeyPathExpander( + reg_cache=reg_cache) + self.expanded_keys = None + + def ExpandKeys(self, parser_context): + """Builds a list of expanded keys this plugin supports. + + Args: + parser_context: A parser context object (instance of ParserContext). + """ + self.expanded_keys = [] + for registry_key in self.REG_KEYS: + expanded_key = u'' + try: + # TODO: deprecate direct use of pre_obj. + expanded_key = self._path_expander.ExpandPath( + registry_key, pre_obj=parser_context.knowledge_base.pre_obj) + except KeyError as exception: + logging.debug(( + u'Unable to expand Registry key {0:s} for plugin {1:s} with ' + u'error: {2:s}').format(registry_key, self.NAME, exception)) + continue + + if not expanded_key: + continue + + self.expanded_keys.append(expanded_key) + + # Special case of Wow6432 Windows Registry redirection. + # URL: http://msdn.microsoft.com/en-us/library/windows/desktop/\ + # ms724072%28v=vs.85%29.aspx + if expanded_key.startswith('\\Software'): + _, first, second = expanded_key.partition('\\Software') + self.expanded_keys.append(u'{0:s}\\Wow6432Node{1:s}'.format( + first, second)) + + if self.REG_TYPE == 'SOFTWARE' or self.REG_TYPE == 'any': + self.expanded_keys.append(u'\\Wow6432Node{0:s}'.format(expanded_key)) + + @abc.abstractmethod + def GetEntries( + self, parser_context, file_entry=None, key=None, registry_type=None, + codepage='cp1252', parser_chain=None, **kwargs): + """Extracts event objects from the Windows Registry key. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: optional file entry object (instance of dfvfs.FileEntry). + The default is None. + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type. The default is None. + codepage: Optional extended ASCII string codepage. The default is cp1252. + """ + + def Process( + self, parser_context, file_entry=None, key=None, registry_type=None, + codepage='cp1252', parser_chain=None, **kwargs): + """Processes a Windows Registry key. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + codepage: Optional extended ASCII string codepage. The default is cp1252. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + if self.expanded_keys is None: + self.ExpandKeys(parser_context) + + parser_chain = self._BuildParserChain(parser_chain) + + super(KeyPlugin, self).Process( + parser_context, file_entry=file_entry, key=key, + registry_type=registry_type, codepage=codepage, + parser_chain=parser_chain, **kwargs) + + if key and key.path in self.expanded_keys: + self.GetEntries( + parser_context, file_entry=file_entry, key=key, + registry_type=registry_type, codepage=codepage, + parser_chain=parser_chain, **kwargs) + + +class ValuePlugin(RegistryPlugin): + """Class that defines the Windows Registry value-based plugin interface.""" + + __abstract = True + + # REG_VALUES should be defined as a frozenset. + REG_VALUES = frozenset() + + WEIGHT = 2 + + @abc.abstractmethod + def GetEntries( + self, parser_context, file_entry=None, key=None, registry_type=None, + parser_chain=None, codepage='cp1252', **kwargs): + """Extracts event objects from the Windows Registry key. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: optional file entry object (instance of dfvfs.FileEntry). + The default is None. + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + codepage: Optional extended ASCII string codepage. The default is cp1252. + """ + + def Process( + self, parser_context, file_entry=None, key=None, registry_type=None, + parser_chain=None, codepage='cp1252', **kwargs): + """Processes a Windows Registry value. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: optional file entry object (instance of dfvfs.FileEntry). + The default is None. + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + codepage: Optional extended ASCII string codepage. The default is cp1252. + """ + + parser_chain = self._BuildParserChain(parser_chain) + + super(ValuePlugin, self).Process( + parser_context, file_entry=file_entry, key=key, + registry_type=registry_type, codepage=codepage, **kwargs) + + values = frozenset([val.name for val in key.GetValues()]) + if self.REG_VALUES.issubset(values): + self.GetEntries( + parser_context, file_entry=file_entry, key=key, + registry_type=registry_type, parser_chain=parser_chain, + codepage=codepage, **kwargs) diff --git a/plaso/parsers/winreg_plugins/lfu.py b/plaso/parsers/winreg_plugins/lfu.py new file mode 100644 index 0000000..2ba9a37 --- /dev/null +++ b/plaso/parsers/winreg_plugins/lfu.py @@ -0,0 +1,126 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Plug-in to collect the Less Frequently Used Keys.""" + +from plaso.events import windows_events +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface + + +class BootVerificationPlugin(interface.KeyPlugin): + """Plug-in to collect the Boot Verification Key.""" + + NAME = 'winreg_boot_verify' + DESCRIPTION = u'Parser for Boot Verification Registry data.' + + REG_TYPE = 'SYSTEM' + REG_KEYS = [u'\\{current_control_set}\\Control\\BootVerificationProgram'] + + URLS = ['http://technet.microsoft.com/en-us/library/cc782537(v=ws.10).aspx'] + + def GetEntries( + self, parser_context, key=None, registry_type=None, file_entry=None, + parser_chain=None, **unused_kwargs): + """Gather the BootVerification key values and return one event for all. + + This key is rare, so its presence is suspect. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + """ + text_dict = {} + for value in key.GetValues(): + text_dict[value.name] = value.data + event_object = windows_events.WindowsRegistryEvent( + key.last_written_timestamp, key.path, text_dict, offset=key.offset, + registry_type=registry_type, urls=self.URLS) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +class BootExecutePlugin(interface.KeyPlugin): + """Plug-in to collect the BootExecute Value from the Session Manager key.""" + + NAME = 'winreg_boot_execute' + DESCRIPTION = u'Parser for Boot Execution Registry data.' + + REG_TYPE = 'SYSTEM' + REG_KEYS = [u'\\{current_control_set}\\Control\\Session Manager'] + + URLS = ['http://technet.microsoft.com/en-us/library/cc963230.aspx'] + + def GetEntries( + self, parser_context, file_entry=None, key=None, registry_type=None, + parser_chain=None, **unused_kwargs): + """Gather the BootExecute Value, compare to default, return event. + + The rest of the values in the Session Manager key are in a separate event. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: optional file entry object (instance of dfvfs.FileEntry). + The default is None. + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + text_dict = {} + + for value in key.GetValues(): + if value.name == 'BootExecute': + # MSDN: claims that the data type of this value is REG_BINARY + # although REG_MULTI_SZ is known to be used as well. + if value.DataIsString(): + value_string = value.data + elif value.DataIsMultiString(): + value_string = u''.join(value.data) + elif value.DataIsBinaryData(): + value_string = value.data + else: + value_string = u'' + error_string = ( + u'Key: {0:s}, value: {1:s}: unsupported value data type: ' + u'{2:s}.').format(key.path, value.name, value.data_type_string) + parser_context.ProduceParseError( + self.NAME, error_string, file_entry=file_entry) + + # TODO: why does this have a separate event object? Remove this. + value_dict = {'BootExecute': value_string} + event_object = windows_events.WindowsRegistryEvent( + key.last_written_timestamp, key.path, value_dict, offset=key.offset, + registry_type=registry_type, urls=self.URLS) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + else: + text_dict[value.name] = value.data + + event_object = windows_events.WindowsRegistryEvent( + key.last_written_timestamp, key.path, text_dict, offset=key.offset, + registry_type=registry_type, urls=self.URLS) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +winreg.WinRegistryParser.RegisterPlugins([ + BootVerificationPlugin, BootExecutePlugin]) diff --git a/plaso/parsers/winreg_plugins/lfu_test.py b/plaso/parsers/winreg_plugins/lfu_test.py new file mode 100644 index 0000000..b141c2d --- /dev/null +++ b/plaso/parsers/winreg_plugins/lfu_test.py @@ -0,0 +1,155 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Less Frequently Used (LFU) Windows Registry plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.lib import timelib_test +from plaso.parsers.winreg_plugins import lfu +from plaso.parsers.winreg_plugins import test_lib +from plaso.winreg import cache +from plaso.winreg import test_lib as winreg_test_lib + + +class TestBootExecutePlugin(test_lib.RegistryPluginTestCase): + """Tests for the LFU BootExecute Windows Registry plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + registry_cache = cache.WinRegistryCache() + registry_cache.attributes['current_control_set'] = 'ControlSet001' + self._plugin = lfu.BootExecutePlugin(reg_cache=registry_cache) + + def testProcess(self): + """Tests the Process function.""" + key_path = u'\\ControlSet001\\Control\\Session Manager' + values = [] + + values.append(winreg_test_lib.TestRegValue( + 'BootExecute', 'autocheck autochk *\x00'.encode('utf_16_le'), 7, 123)) + values.append(winreg_test_lib.TestRegValue( + 'CriticalSectionTimeout', '2592000'.encode('utf_16_le'), 1, 153)) + values.append(winreg_test_lib.TestRegValue( + 'ExcludeFromKnownDlls', '\x00'.encode('utf_16_le'), 7, 163)) + values.append(winreg_test_lib.TestRegValue( + 'GlobalFlag', '0'.encode('utf_16_le'), 1, 173)) + values.append(winreg_test_lib.TestRegValue( + 'HeapDeCommitFreeBlockThreshold', '0'.encode('utf_16_le'), 1, 183)) + values.append(winreg_test_lib.TestRegValue( + 'HeapDeCommitTotalFreeThreshold', '0'.encode('utf_16_le'), 1, 203)) + values.append(winreg_test_lib.TestRegValue( + 'HeapSegmentCommit', '0'.encode('utf_16_le'), 1, 213)) + values.append(winreg_test_lib.TestRegValue( + 'HeapSegmentReserve', '0'.encode('utf_16_le'), 1, 223)) + values.append(winreg_test_lib.TestRegValue( + 'NumberOfInitialSessions', '2'.encode('utf_16_le'), 1, 243)) + + timestamp = timelib_test.CopyStringToTimestamp('2012-08-31 20:45:29') + winreg_key = winreg_test_lib.TestRegKey(key_path, timestamp, values, 153) + + event_queue_consumer = self._ParseKeyWithPlugin(self._plugin, winreg_key) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 2) + + event_object = event_objects[0] + + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-08-31 20:45:29') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_string = ( + u'[{0:s}] BootExecute: autocheck autochk *').format(key_path) + + self._TestGetMessageStrings(event_object, expected_string, expected_string) + + event_object = event_objects[1] + + expected_msg = ( + u'[{0:s}] ' + u'CriticalSectionTimeout: 2592000 ' + u'ExcludeFromKnownDlls: [] ' + u'GlobalFlag: 0 ' + u'HeapDeCommitFreeBlockThreshold: 0 ' + u'HeapDeCommitTotalFreeThreshold: 0 ' + u'HeapSegmentCommit: 0 ' + u'HeapSegmentReserve: 0 ' + u'NumberOfInitialSessions: 2').format(key_path) + + expected_msg_short = ( + u'[{0:s}] CriticalSectionTimeout: 2592000 Excl...').format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +class TestBootVerificationRegistry(test_lib.RegistryPluginTestCase): + """Tests for the LFU BootVerification Windows Registry plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + registry_cache = cache.WinRegistryCache() + registry_cache.attributes['current_control_set'] = 'ControlSet001' + self._plugin = lfu.BootVerificationPlugin(reg_cache=registry_cache) + + def testProcess(self): + """Tests the Process function.""" + key_path = u'\\ControlSet001\\Control\\BootVerificationProgram' + values = [] + + values.append(winreg_test_lib.TestRegValue( + 'ImagePath', + 'C:\\WINDOWS\\system32\\googleupdater.exe'.encode('utf_16_le'), 1, + 123)) + + timestamp = timelib_test.CopyStringToTimestamp('2012-08-31 20:45:29') + winreg_key = winreg_test_lib.TestRegKey(key_path, timestamp, values, 153) + + event_queue_consumer = self._ParseKeyWithPlugin(self._plugin, winreg_key) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 1) + + event_object = event_objects[0] + + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-08-31 20:45:29') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'[{0:s}] ' + u'ImagePath: C:\\WINDOWS\\system32\\googleupdater.exe').format( + key_path) + + expected_msg_short = ( + u'[{0:s}] ImagePath: C:\\WINDOWS\\system...').format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/mountpoints.py b/plaso/parsers/winreg_plugins/mountpoints.py new file mode 100644 index 0000000..ae800d7 --- /dev/null +++ b/plaso/parsers/winreg_plugins/mountpoints.py @@ -0,0 +1,88 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the MountPoints2 plugin.""" + +from plaso.events import windows_events +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface + + +class MountPoints2Plugin(interface.KeyPlugin): + """Windows Registry plugin for parsing the MountPoints2 key.""" + + NAME = 'winreg_mountpoints2' + DESCRIPTION = u'Parser for mount points Registry data.' + + REG_TYPE = 'NTUSER' + + REG_KEYS = [ + (u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\' + u'MountPoints2')] + + URLS = [u'http://support.microsoft.com/kb/932463'] + + def GetEntries( + self, parser_context, key=None, registry_type=None, file_entry=None, + parser_chain=None, **unused_kwargs): + """Retrieves information from the MountPoints2 registry key. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + """ + for subkey in key.GetSubkeys(): + name = subkey.name + if not name: + continue + + text_dict = {} + text_dict[u'Volume'] = name + + # Get the label if it exists. + label_value = subkey.GetValue('_LabelFromReg') + if label_value: + text_dict[u'Label'] = label_value.data + + if name.startswith('{'): + text_dict[u'Type'] = u'Volume' + + elif name.startswith('#'): + # The format is: ##Server_Name#Share_Name. + text_dict[u'Type'] = u'Remote Drive' + server_name, _, share_name = name[2:].partition('#') + text_dict[u'Remote_Server'] = server_name + text_dict[u'Share_Name'] = u'\\{0:s}'.format( + share_name.replace(u'#', u'\\')) + + else: + text_dict[u'Type'] = u'Drive' + + event_object = windows_events.WindowsRegistryEvent( + subkey.last_written_timestamp, key.path, text_dict, + offset=subkey.offset, registry_type=registry_type, urls=self.URLS) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +winreg.WinRegistryParser.RegisterPlugin(MountPoints2Plugin) diff --git a/plaso/parsers/winreg_plugins/mountpoints_test.py b/plaso/parsers/winreg_plugins/mountpoints_test.py new file mode 100644 index 0000000..c4db38f --- /dev/null +++ b/plaso/parsers/winreg_plugins/mountpoints_test.py @@ -0,0 +1,72 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the MountPoints2 Windows Registry plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.lib import timelib_test +from plaso.parsers.winreg_plugins import mountpoints +from plaso.parsers.winreg_plugins import test_lib + + +class MountPoints2PluginTest(test_lib.RegistryPluginTestCase): + """Tests for the MountPoints2 Windows Registry plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = mountpoints.MountPoints2Plugin() + + def testProcess(self): + """Tests the Process function.""" + test_file_entry = self._GetTestFileEntryFromPath(['NTUSER-WIN7.DAT']) + key_path = self._plugin.REG_KEYS[0] + winreg_key = self._GetKeyFromFileEntry(test_file_entry, key_path) + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_key, file_entry=test_file_entry) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 5) + + event_object = event_objects[0] + + self.assertEquals(event_object.pathspec, test_file_entry.path_spec) + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-08-23 17:10:14.960960') + self.assertEquals(event_object.timestamp, expected_timestamp) + + regvalue = event_object.regvalue + self.assertEquals(regvalue.get('Share_Name'), r'\home\nfury') + + expected_string = ( + u'[{0:s}] Label: Home Drive Remote_Server: controller Share_Name: ' + u'\\home\\nfury Type: Remote Drive Volume: ' + u'##controller#home#nfury').format(key_path) + expected_string_short = u'{0:s}...'.format(expected_string[0:77]) + + self._TestGetMessageStrings( + event_object, expected_string, expected_string_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/mrulist.py b/plaso/parsers/winreg_plugins/mrulist.py new file mode 100644 index 0000000..61556c6 --- /dev/null +++ b/plaso/parsers/winreg_plugins/mrulist.py @@ -0,0 +1,308 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a MRUList Registry plugin.""" + +import abc +import logging + +import construct + +from plaso.events import windows_events +from plaso.lib import binary +from plaso.parsers import winreg +from plaso.parsers.shared import shell_items +from plaso.parsers.winreg_plugins import interface + + +# A mixin class is used here to not to have the duplicate functionality +# to parse the MRUList Registry values. However multiple inheritance +# and thus mixins are to be used sparsely in this codebase, hence we need +# to find a better solution in not needing to distinguish between key and +# value plugins. +# TODO: refactor Registry key and value plugin to rid ourselves of the mixin. +class MRUListPluginMixin(object): + """Class for common MRUList Windows Registry plugin functionality.""" + + _MRULIST_STRUCT = construct.Range(1, 500, construct.ULInt16('entry_letter')) + + @abc.abstractmethod + def _ParseMRUListEntryValue( + self, parser_context, key, entry_index, entry_letter, file_entry=None, + parser_chain=None, **kwargs): + """Parses the MRUList entry value. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: the Registry key (instance of winreg.WinRegKey) that contains + the MRUList value. + entry_index: integer value representing the MRUList entry index. + entry_letter: character value representing the entry. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Returns: + A string containing the value. + """ + + def _ParseMRUListValue(self, key): + """Parses the MRUList value in a given Registry key. + + Args: + key: the Registry key (instance of winreg.WinRegKey) that contains + the MRUList value. + + Returns: + A MRUList value generator, which returns the MRU index number + and entry value. + """ + mru_list_value = key.GetValue('MRUList') + + # The key exists but does not contain a value named "MRUList". + if not mru_list_value: + return enumerate([]) + + try: + mru_list = self._MRULIST_STRUCT.parse(mru_list_value.raw_data) + except construct.FieldError: + logging.warning(u'[{0:s}] Unable to parse the MRU key: {1:s}'.format( + self.NAME, key.path)) + return enumerate([]) + + return enumerate(mru_list) + + def _ParseMRUListKey( + self, parser_context, key, registry_type=None, file_entry=None, + parser_chain=None, codepage='cp1252'): + """Extract event objects from a MRUList Registry key. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: the Registry key (instance of winreg.WinRegKey). + registry_type: Optional Registry type string. The default is None. + codepage: Optional extended ASCII string codepage. The default is cp1252. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + text_dict = {} + for entry_index, entry_letter in self._ParseMRUListValue(key): + # TODO: detect if list ends prematurely. + # MRU lists are terminated with \0 (0x0000). + if entry_letter == 0: + break + + entry_letter = chr(entry_letter) + + value_string = self._ParseMRUListEntryValue( + parser_context, key, entry_index, entry_letter, + codepage=codepage, file_entry=file_entry, parser_chain=parser_chain) + + value_text = u'Index: {0:d} [MRU Value {1:s}]'.format( + entry_index + 1, entry_letter) + + text_dict[value_text] = value_string + + event_object = windows_events.WindowsRegistryEvent( + key.last_written_timestamp, key.path, text_dict, + offset=key.offset, registry_type=registry_type, + source_append=': MRU List') + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +class MRUListStringPlugin(interface.ValuePlugin, MRUListPluginMixin): + """Windows Registry plugin to parse a string MRUList.""" + + NAME = 'winreg_mrulist_string' + DESCRIPTION = u'Parser for Most Recently Used (MRU) Registry data.' + + REG_TYPE = 'any' + REG_VALUES = frozenset(['MRUList', 'a']) + URLS = [u'http://forensicartifacts.com/tag/mru/'] + + def _ParseMRUListEntryValue( + self, parser_context, key, entry_index, entry_letter, **unused_kwargs): + """Parses the MRUList entry value. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: the Registry key (instance of winreg.WinRegKey) that contains + the MRUList value. + entry_index: integer value representing the MRUList entry index. + entry_letter: character value representing the entry. + + Returns: + A string containing the value. + """ + value_string = u'' + + value = key.GetValue(u'{0:s}'.format(entry_letter)) + if value is None: + logging.debug( + u'[{0:s}] Missing MRUList entry value: {1:s} in key: {2:s}.'.format( + self.NAME, entry_letter, key.path)) + + elif value.DataIsString(): + value_string = value.data + + elif value.DataIsBinaryData(): + logging.debug(( + u'[{0:s}] Non-string MRUList entry value: {1:s} parsed as string ' + u'in key: {2:s}.').format(self.NAME, entry_letter, key.path)) + utf16_stream = binary.ByteStreamCopyToUtf16Stream(value.data) + + try: + value_string = utf16_stream.decode('utf-16-le') + except UnicodeDecodeError as exception: + value_string = binary.HexifyBuffer(utf16_stream) + logging.warning(( + u'[{0:s}] Unable to decode UTF-16 stream: {1:s} in MRUList entry ' + u'value: {2:s} in key: {3:s} with error: {4:s}').format( + self.NAME, value_string, entry_letter, key.path, exception)) + + return value_string + + def GetEntries( + self, parser_context, file_entry=None, key=None, registry_type=None, + parser_chain=None, codepage='cp1252', **unused_kwargs): + """Extracts event objects from a MRU list. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: optional file entry object (instance of dfvfs.FileEntry). + The default is None. + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + codepage: Optional extended ASCII string codepage. The default is cp1252. + """ + self._ParseMRUListKey( + parser_context, key, registry_type=registry_type, + parser_chain=parser_chain, file_entry=file_entry, codepage=codepage) + + def Process( + self, parser_context, file_entry=None, key=None, registry_type=None, + codepage='cp1252', parser_chain=None, **kwargs): + """Determine if we can process this Registry key or not. + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + codepage: Optional extended ASCII string codepage. The default is cp1252. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + # Prevent this plugin triggering on sub paths of non-string MRUList values. + if u'Explorer\\DesktopStreamMRU' in key.path: + return + + super(MRUListStringPlugin, self).Process( + parser_context, file_entry=file_entry, key=key, + registry_type=registry_type, codepage=codepage, + parser_chain=parser_chain, **kwargs) + + +class MRUListShellItemListPlugin(interface.KeyPlugin, MRUListPluginMixin): + """Windows Registry plugin to parse a shell item list MRUList.""" + + NAME = 'winreg_mrulist_shell_item_list' + DESCRIPTION = u'Parser for Most Recently Used (MRU) Registry data.' + + REG_TYPE = 'any' + REG_KEYS = frozenset([ + (u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\' + u'DesktopStreamMRU')]) + + URLS = [u'https://github.com/libyal/winreg-kb/wiki/MRU-keys'] + + def _ParseMRUListEntryValue( + self, parser_context, key, entry_index, entry_letter, codepage='cp1252', + file_entry=None, parser_chain=None, **unused_kwargs): + """Parses the MRUList entry value. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: the Registry key (instance of winreg.WinRegKey) that contains + the MRUList value. + entry_index: integer value representing the MRUList entry index. + entry_letter: character value representing the entry. + codepage: Optional extended ASCII string codepage. The default is cp1252. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Returns: + A string containing the value. + """ + value_string = u'' + + value = key.GetValue(u'{0:s}'.format(entry_letter)) + if value is None: + logging.debug( + u'[{0:s}] Missing MRUList entry value: {1:s} in key: {2:s}.'.format( + self.NAME, entry_letter, key.path)) + + elif not value.DataIsBinaryData(): + logging.debug(( + u'[{0:s}] Non-binary MRUList entry value: {1:s} in key: ' + u'{2:s}.').format(self.NAME, entry_letter, key.path)) + + elif value.data: + shell_items_parser = shell_items.ShellItemsParser(key.path) + shell_items_parser.Parse( + parser_context, value.data, codepage=codepage, file_entry=file_entry, + parser_chain=parser_chain) + + value_string = u'Shell item list: [{0:s}]'.format( + shell_items_parser.CopyToPath()) + + return value_string + + def GetEntries( + self, parser_context, key=None, registry_type=None, codepage='cp1252', + file_entry=None, parser_chain=None, **unused_kwargs): + """Extract event objects from a Registry key containing a MRUList value. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + codepage: Optional extended ASCII string codepage. The default is cp1252. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + self._ParseMRUListKey( + parser_context, key, registry_type=registry_type, codepage=codepage, + parser_chain=parser_chain, file_entry=file_entry) + + +winreg.WinRegistryParser.RegisterPlugins([ + MRUListStringPlugin, MRUListShellItemListPlugin]) diff --git a/plaso/parsers/winreg_plugins/mrulist_test.py b/plaso/parsers/winreg_plugins/mrulist_test.py new file mode 100644 index 0000000..2de69ba --- /dev/null +++ b/plaso/parsers/winreg_plugins/mrulist_test.py @@ -0,0 +1,171 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the MRUList Windows Registry plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.lib import timelib_test +from plaso.parsers.winreg_plugins import mrulist +from plaso.parsers.winreg_plugins import test_lib +from plaso.winreg import test_lib as winreg_test_lib + + +class TestMRUListStringPlugin(test_lib.RegistryPluginTestCase): + """Tests for the string MRUList plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = mrulist.MRUListStringPlugin() + + def testProcess(self): + """Tests the Process function.""" + key_path = u'\\Microsoft\\Some Windows\\InterestingApp\\MRU' + values = [] + + values.append(winreg_test_lib.TestRegValue( + 'MRUList', 'acb'.encode('utf_16_le'), + winreg_test_lib.TestRegValue.REG_SZ, offset=123)) + values.append(winreg_test_lib.TestRegValue( + 'a', 'Some random text here'.encode('utf_16_le'), + winreg_test_lib.TestRegValue.REG_SZ, offset=1892)) + values.append(winreg_test_lib.TestRegValue( + 'b', 'c:/evil.exe'.encode('utf_16_le'), + winreg_test_lib.TestRegValue.REG_BINARY, offset=612)) + values.append(winreg_test_lib.TestRegValue( + 'c', 'C:/looks_legit.exe'.encode('utf_16_le'), + winreg_test_lib.TestRegValue.REG_SZ, offset=1001)) + + timestamp = timelib_test.CopyStringToTimestamp('2012-08-28 09:23:49.002031') + winreg_key = winreg_test_lib.TestRegKey( + key_path, timestamp, values, 1456) + + event_queue_consumer = self._ParseKeyWithPlugin(self._plugin, winreg_key) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 1) + + event_object = event_objects[0] + + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-08-28 09:23:49.002031') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'[{0:s}] ' + u'Index: 1 [MRU Value a]: Some random text here ' + u'Index: 2 [MRU Value c]: C:/looks_legit.exe ' + u'Index: 3 [MRU Value b]: c:/evil.exe').format(key_path) + + expected_msg_short = ( + u'[{0:s}] Index: 1 [MRU Value a]: Some ran...').format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +class TestMRUListShellItemListPlugin(test_lib.RegistryPluginTestCase): + """Tests for the shell item list MRUList plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = mrulist.MRUListShellItemListPlugin() + + def testProcess(self): + """Tests the Process function.""" + key_path = ( + u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\' + u'DesktopStreamMRU') + values = [] + + data = ''.join(map(chr, [ + 0x14, 0x00, 0x1f, 0x00, 0xe0, 0x4f, 0xd0, 0x20, 0xea, 0x3a, 0x69, 0x10, + 0xa2, 0xd8, 0x08, 0x00, 0x2b, 0x30, 0x30, 0x9d, 0x19, 0x00, 0x23, 0x43, + 0x3a, 0x5c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0xee, 0x15, 0x00, 0x31, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x2e, 0x3e, 0x7a, 0x60, 0x10, 0x80, 0x57, + 0x69, 0x6e, 0x6e, 0x74, 0x00, 0x00, 0x18, 0x00, 0x31, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x2e, 0x3e, 0xe4, 0x62, 0x10, 0x00, 0x50, 0x72, 0x6f, 0x66, + 0x69, 0x6c, 0x65, 0x73, 0x00, 0x00, 0x25, 0x00, 0x31, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x2e, 0x3e, 0xe4, 0x62, 0x10, 0x00, 0x41, 0x64, 0x6d, 0x69, + 0x6e, 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x00, 0x41, 0x44, + 0x4d, 0x49, 0x4e, 0x49, 0x7e, 0x31, 0x00, 0x17, 0x00, 0x31, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x2e, 0x3e, 0xe4, 0x62, 0x10, 0x00, 0x44, 0x65, 0x73, + 0x6b, 0x74, 0x6f, 0x70, 0x00, 0x00, 0x00, 0x00])) + + values.append(winreg_test_lib.TestRegValue( + 'MRUList', 'a'.encode('utf_16_le'), + winreg_test_lib.TestRegValue.REG_SZ, offset=123)) + values.append(winreg_test_lib.TestRegValue( + 'a', data, winreg_test_lib.TestRegValue.REG_BINARY, offset=612)) + + timestamp = timelib_test.CopyStringToTimestamp('2012-08-28 09:23:49.002031') + winreg_key = winreg_test_lib.TestRegKey( + key_path, timestamp, values, 1456) + + event_queue_consumer = self._ParseKeyWithPlugin(self._plugin, winreg_key) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 5) + + # A MRUList event object. + event_object = event_objects[4] + + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-08-28 09:23:49.002031') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'[{0:s}] ' + u'Index: 1 [MRU Value a]: Shell item list: ' + u'[My Computer, C:\\, Winnt, Profiles, Administrator, Desktop]').format( + key_path) + + expected_msg_short = u'[{0:s}] Index:...'.format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + # A shell item event object. + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-01-14 12:03:52') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'Name: Winnt ' + u'Origin: {0:s}').format(key_path) + + expected_msg_short = ( + u'Name: Winnt ' + u'Origin: \\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\' + u'Deskt...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/mrulistex.py b/plaso/parsers/winreg_plugins/mrulistex.py new file mode 100644 index 0000000..21e26c0 --- /dev/null +++ b/plaso/parsers/winreg_plugins/mrulistex.py @@ -0,0 +1,528 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains MRUListEx Windows Registry plugins.""" + +import abc +import logging + +import construct + +from plaso.events import windows_events +from plaso.lib import binary +from plaso.parsers import winreg +from plaso.parsers.shared import shell_items +from plaso.parsers.winreg_plugins import interface + + +# A mixin class is used here to not to have the duplicate functionality +# to parse the MRUListEx Registry values. However multiple inheritance +# and thus mixins are to be used sparsely in this codebase, hence we need +# to find a better solution in not needing to distinguish between key and +# value plugins. +# TODO: refactor Registry key and value plugin to rid ourselves of the mixin. +class MRUListExPluginMixin(object): + """Class for common MRUListEx Windows Registry plugin functionality.""" + + _MRULISTEX_STRUCT = construct.Range(1, 500, construct.ULInt32('entry_number')) + + @abc.abstractmethod + def _ParseMRUListExEntryValue( + self, parser_context, key, entry_index, entry_number, **kwargs): + """Parses the MRUListEx entry value. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: the Registry key (instance of winreg.WinRegKey) that contains + the MRUListEx value. + entry_index: integer value representing the MRUListEx entry index. + entry_number: integer value representing the entry number. + + Returns: + A string containing the value. + """ + + def _ParseMRUListExValue(self, key): + """Parses the MRUListEx value in a given Registry key. + + Args: + key: the Registry key (instance of winreg.WinRegKey) that contains + the MRUListEx value. + + Returns: + A MRUListEx value generator, which returns the MRU index number + and entry value. + """ + mru_list_value = key.GetValue('MRUListEx') + + # The key exists but does not contain a value named "MRUListEx". + if not mru_list_value: + return enumerate([]) + + try: + mru_list = self._MRULISTEX_STRUCT.parse(mru_list_value.data) + except construct.FieldError: + logging.warning(u'[{0:s}] Unable to parse the MRU key: {1:s}'.format( + self.NAME, key.path)) + return enumerate([]) + + return enumerate(mru_list) + + def _ParseMRUListExKey( + self, parser_context, key, registry_type=None, codepage='cp1252', + file_entry=None, parser_chain=None): + """Extract event objects from a MRUListEx Registry key. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: the Registry key (instance of winreg.WinRegKey). + registry_type: Optional Registry type string. The default is None. + codepage: Optional extended ASCII string codepage. The default is cp1252. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + """ + text_dict = {} + for entry_index, entry_number in self._ParseMRUListExValue(key): + # TODO: detect if list ends prematurely. + # MRU lists are terminated with 0xffffffff (-1). + if entry_number == 0xffffffff: + break + + value_string = self._ParseMRUListExEntryValue( + parser_context, key, entry_index, entry_number, + codepage=codepage, file_entry=file_entry, parser_chain=parser_chain) + + value_text = u'Index: {0:d} [MRU Value {1:d}]'.format( + entry_index + 1, entry_number) + + text_dict[value_text] = value_string + + event_object = windows_events.WindowsRegistryEvent( + key.last_written_timestamp, key.path, text_dict, + offset=key.offset, registry_type=registry_type, + source_append=': MRUListEx') + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +class MRUListExStringPlugin(interface.ValuePlugin, MRUListExPluginMixin): + """Windows Registry plugin to parse a string MRUListEx.""" + + NAME = 'winreg_mrulistex_string' + DESCRIPTION = u'Parser for Most Recently Used (MRU) Registry data.' + + REG_TYPE = 'any' + REG_VALUES = frozenset(['MRUListEx', '0']) + + URLS = [ + u'http://forensicartifacts.com/2011/02/recentdocs/', + u'https://github.com/libyal/winreg-kb/wiki/MRU-keys'] + + _STRING_STRUCT = construct.Struct( + 'string_and_shell_item', + construct.RepeatUntil( + lambda obj, ctx: obj == '\x00\x00', construct.Field('string', 2))) + + def _ParseMRUListExEntryValue( + self, parser_context, key, entry_index, entry_number, **unused_kwargs): + """Parses the MRUListEx entry value. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: the Registry key (instance of winreg.WinRegKey) that contains + the MRUListEx value. + entry_index: integer value representing the MRUListEx entry index. + entry_number: integer value representing the entry number. + + Returns: + A string containing the value. + """ + value_string = u'' + + value = key.GetValue(u'{0:d}'.format(entry_number)) + if value is None: + logging.debug( + u'[{0:s}] Missing MRUListEx entry value: {1:d} in key: {2:s}.'.format( + self.NAME, entry_number, key.path)) + + elif value.DataIsString(): + value_string = value.data + + elif value.DataIsBinaryData(): + logging.debug(( + u'[{0:s}] Non-string MRUListEx entry value: {1:d} parsed as string ' + u'in key: {2:s}.').format(self.NAME, entry_number, key.path)) + utf16_stream = binary.ByteStreamCopyToUtf16Stream(value.data) + + try: + value_string = utf16_stream.decode('utf-16-le') + except UnicodeDecodeError as exception: + value_string = binary.HexifyBuffer(utf16_stream) + logging.warning(( + u'[{0:s}] Unable to decode UTF-16 stream: {1:s} in MRUListEx entry ' + u'value: {2:d} in key: {3:s} with error: {4:s}').format( + self.NAME, value_string, entry_number, key.path, exception)) + + return value_string + + def GetEntries( + self, parser_context, key=None, registry_type=None, codepage='cp1252', + file_entry=None, parser_chain=None, **unused_kwargs): + """Extract event objects from a Registry key containing a MRUListEx value. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + codepage: Optional extended ASCII string codepage. The default is cp1252. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + """ + self._ParseMRUListExKey( + parser_context, key, registry_type=registry_type, codepage=codepage, + parser_chain=parser_chain, file_entry=file_entry) + + def Process(self, parser_context, key=None, codepage='cp1252', **kwargs): + """Determine if we can process this Registry key or not. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: A Windows Registry key (instance of WinRegKey). + codepage: Optional extended ASCII string codepage. The default is cp1252. + """ + # Prevent this plugin triggering on sub paths of non-string MRUListEx + # values. + if (u'BagMRU' in key.path or u'Explorer\\StreamMRU' in key.path or + u'\\Explorer\\ComDlg32\\OpenSavePidlMRU' in key.path): + return + + super(MRUListExStringPlugin, self).Process( + parser_context, key=key, codepage=codepage, **kwargs) + + +class MRUListExShellItemListPlugin(interface.KeyPlugin, MRUListExPluginMixin): + """Windows Registry plugin to parse a shell item list MRUListEx.""" + + NAME = 'winreg_mrulistex_shell_item_list' + DESCRIPTION = u'Parser for Most Recently Used (MRU) Registry data.' + + REG_TYPE = 'any' + REG_KEYS = frozenset([ + # The regular expression indicated a file extension (.jpg) or '*'. + (u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\ComDlg32\\' + u'OpenSavePidlMRU'), + u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StreamMRU']) + + def _ParseMRUListExEntryValue( + self, parser_context, key, entry_index, entry_number, codepage='cp1252', + file_entry=None, parser_chain=None, **unused_kwargs): + """Parses the MRUListEx entry value. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: the Registry key (instance of winreg.WinRegKey) that contains + the MRUListEx value. + entry_index: integer value representing the MRUListEx entry index. + entry_number: integer value representing the entry number. + codepage: Optional extended ASCII string codepage. The default is cp1252. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Returns: + A string containing the value. + """ + value_string = u'' + + value = key.GetValue(u'{0:d}'.format(entry_number)) + if value is None: + logging.debug( + u'[{0:s}] Missing MRUListEx entry value: {1:d} in key: {2:s}.'.format( + self.NAME, entry_number, key.path)) + + elif not value.DataIsBinaryData(): + logging.debug(( + u'[{0:s}] Non-binary MRUListEx entry value: {1:d} in key: ' + u'{2:s}.').format(self.NAME, entry_number, key.path)) + + elif value.data: + shell_items_parser = shell_items.ShellItemsParser(key.path) + shell_items_parser.Parse( + parser_context, value.data, codepage=codepage, file_entry=file_entry, + parser_chain=parser_chain) + + value_string = u'Shell item list: [{0:s}]'.format( + shell_items_parser.CopyToPath()) + + return value_string + + def GetEntries( + self, parser_context, key=None, registry_type=None, codepage='cp1252', + file_entry=None, parser_chain=None, **unused_kwargs): + """Extract event objects from a Registry key containing a MRUListEx value. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + codepage: Optional extended ASCII string codepage. The default is cp1252. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + """ + if key.name != u'OpenSavePidlMRU': + self._ParseMRUListExKey( + parser_context, key, registry_type=registry_type, codepage=codepage, + parser_chain=parser_chain, file_entry=file_entry) + + if key.name == u'OpenSavePidlMRU': + # For the OpenSavePidlMRU MRUListEx we also need to parse its subkeys + # since the Registry key path does not support wildcards yet. + for subkey in key.GetSubkeys(): + self._ParseMRUListExKey( + parser_context, subkey, registry_type=registry_type, + codepage=codepage, parser_chain=parser_chain, file_entry=file_entry) + + +class MRUListExStringAndShellItemPlugin( + interface.KeyPlugin, MRUListExPluginMixin): + """Windows Registry plugin to parse a string and shell item MRUListEx.""" + + NAME = 'winreg_mrulistex_string_and_shell_item' + DESCRIPTION = u'Parser for Most Recently Used (MRU) Registry data.' + + REG_TYPE = 'any' + REG_KEYS = frozenset([ + u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\RecentDocs']) + + _STRING_AND_SHELL_ITEM_STRUCT = construct.Struct( + 'string_and_shell_item', + construct.RepeatUntil( + lambda obj, ctx: obj == '\x00\x00', construct.Field('string', 2)), + construct.Anchor('shell_item')) + + def _ParseMRUListExEntryValue( + self, parser_context, key, entry_index, entry_number, codepage='cp1252', + file_entry=None, parser_chain=None, **unused_kwargs): + """Parses the MRUListEx entry value. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: the Registry key (instance of winreg.WinRegKey) that contains + the MRUListEx value. + entry_index: integer value representing the MRUListEx entry index. + entry_number: integer value representing the entry number. + codepage: Optional extended ASCII string codepage. The default is cp1252. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Returns: + A string containing the value. + """ + value_string = u'' + + value = key.GetValue(u'{0:d}'.format(entry_number)) + if value is None: + logging.debug( + u'[{0:s}] Missing MRUListEx entry value: {1:d} in key: {2:s}.'.format( + self.NAME, entry_number, key.path)) + + elif not value.DataIsBinaryData(): + logging.debug(( + u'[{0:s}] Non-binary MRUListEx entry value: {1:d} in key: ' + u'{2:s}.').format(self.NAME, entry_number, key.path)) + + elif value.data: + value_struct = self._STRING_AND_SHELL_ITEM_STRUCT.parse(value.data) + + try: + # The struct includes the end-of-string character that we need + # to strip off. + path = ''.join(value_struct.string).decode('utf16')[:-1] + except UnicodeDecodeError as exception: + logging.warning(( + u'[{0:s}] Unable to decode string MRUListEx entry value: {1:d} ' + u'in key: {2:s} with error: {3:s}').format( + self.NAME, entry_number, key.path, exception)) + path = u'' + + if path: + shell_item_list_data = value.data[value_struct.shell_item:] + if not shell_item_list_data: + logging.debug(( + u'[{0:s}] Missing shell item in MRUListEx entry value: {1:d}' + u'in key: {2:s}').format(self.NAME, entry_number, key.path)) + value_string = u'Path: {0:s}'.format(path) + + else: + shell_items_parser = shell_items.ShellItemsParser(key.path) + shell_items_parser.Parse( + parser_context, shell_item_list_data, codepage=codepage, + parser_chain=parser_chain, file_entry=file_entry) + + value_string = u'Path: {0:s}, Shell item: [{1:s}]'.format( + path, shell_items_parser.CopyToPath()) + + return value_string + + def GetEntries( + self, parser_context, key=None, registry_type=None, codepage='cp1252', + file_entry=None, parser_chain=None, **unused_kwargs): + """Extract event objects from a Registry key containing a MRUListEx value. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + codepage: Optional extended ASCII string codepage. The default is cp1252. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + self._ParseMRUListExKey( + parser_context, key, registry_type=registry_type, codepage=codepage, + parser_chain=parser_chain, file_entry=file_entry) + + if key.name == u'RecentDocs': + # For the RecentDocs MRUListEx we also need to parse its subkeys + # since the Registry key path does not support wildcards yet. + for subkey in key.GetSubkeys(): + self._ParseMRUListExKey( + parser_context, subkey, registry_type=registry_type, + codepage=codepage, parser_chain=parser_chain, file_entry=file_entry) + + +class MRUListExStringAndShellItemListPlugin( + interface.KeyPlugin, MRUListExPluginMixin): + """Windows Registry plugin to parse a string and shell item list MRUListEx.""" + + NAME = 'winreg_mrulistex_string_and_shell_item_list' + DESCRIPTION = u'Parser for Most Recently Used (MRU) Registry data.' + + REG_TYPE = 'any' + REG_KEYS = frozenset([ + (u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\ComDlg32\\' + u'LastVisitedPidlMRU')]) + + _STRING_AND_SHELL_ITEM_LIST_STRUCT = construct.Struct( + 'string_and_shell_item', + construct.RepeatUntil( + lambda obj, ctx: obj == '\x00\x00', construct.Field('string', 2)), + construct.Anchor('shell_item_list')) + + def _ParseMRUListExEntryValue( + self, parser_context, key, entry_index, entry_number, codepage='cp1252', + file_entry=None, parser_chain=None, **unused_kwargs): + """Parses the MRUListEx entry value. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: the Registry key (instance of winreg.WinRegKey) that contains + the MRUListEx value. + entry_index: integer value representing the MRUListEx entry index. + entry_number: integer value representing the entry number. + codepage: Optional extended ASCII string codepage. The default is cp1252. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Returns: + A string containing the value. + """ + value_string = u'' + + value = key.GetValue(u'{0:d}'.format(entry_number)) + if value is None: + logging.debug( + u'[{0:s}] Missing MRUListEx entry value: {1:d} in key: {2:s}.'.format( + self.NAME, entry_number, key.path)) + + elif not value.DataIsBinaryData(): + logging.debug(( + u'[{0:s}] Non-binary MRUListEx entry value: {1:d} in key: ' + u'{2:s}.').format(self.NAME, entry_number, key.path)) + + elif value.data: + value_struct = self._STRING_AND_SHELL_ITEM_LIST_STRUCT.parse(value.data) + + try: + # The struct includes the end-of-string character that we need + # to strip off. + path = ''.join(value_struct.string).decode('utf16')[:-1] + except UnicodeDecodeError as exception: + logging.warning(( + u'[{0:s}] Unable to decode string MRUListEx entry value: {1:d} ' + u'in key: {2:s} with error: {3:s}').format( + self.NAME, entry_number, key.path, exception)) + path = u'' + + if path: + shell_item_list_data = value.data[value_struct.shell_item_list:] + if not shell_item_list_data: + logging.debug(( + u'[{0:s}] Missing shell item in MRUListEx entry value: {1:d}' + u'in key: {2:s}').format(self.NAME, entry_number, key.path)) + value_string = u'Path: {0:s}'.format(path) + + else: + shell_items_parser = shell_items.ShellItemsParser(key.path) + shell_items_parser.Parse( + parser_context, shell_item_list_data, codepage=codepage, + parser_chain=parser_chain, file_entry=file_entry) + + value_string = u'Path: {0:s}, Shell item list: [{1:s}]'.format( + path, shell_items_parser.CopyToPath()) + + return value_string + + def GetEntries( + self, parser_context, key=None, registry_type=None, codepage='cp1252', + file_entry=None, parser_chain=None, **unused_kwargs): + """Extract event objects from a Registry key containing a MRUListEx value. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + codepage: Optional extended ASCII string codepage. The default is cp1252. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + self._ParseMRUListExKey( + parser_context, key, registry_type=registry_type, codepage=codepage, + parser_chain=parser_chain, file_entry=file_entry) + + +winreg.WinRegistryParser.RegisterPlugins([ + MRUListExStringPlugin, MRUListExShellItemListPlugin, + MRUListExStringAndShellItemPlugin, MRUListExStringAndShellItemListPlugin]) diff --git a/plaso/parsers/winreg_plugins/mrulistex_test.py b/plaso/parsers/winreg_plugins/mrulistex_test.py new file mode 100644 index 0000000..28231e5 --- /dev/null +++ b/plaso/parsers/winreg_plugins/mrulistex_test.py @@ -0,0 +1,303 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the MRUListEx Windows Registry plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.lib import timelib_test +from plaso.parsers.winreg_plugins import mrulistex +from plaso.parsers.winreg_plugins import test_lib +from plaso.winreg import interface as winreg_interface +from plaso.winreg import test_lib as winreg_test_lib + + +class TestMRUListExStringPlugin(test_lib.RegistryPluginTestCase): + """Tests for the string MRUListEx plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = mrulistex.MRUListExStringPlugin() + + def testProcess(self): + """Tests the Process function.""" + key_path = u'\\Microsoft\\Some Windows\\InterestingApp\\MRUlist' + values = [] + + # The order is: 201 + values.append(winreg_test_lib.TestRegValue( + 'MRUListEx', '\x02\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00', + winreg_interface.WinRegValue.REG_BINARY, 123)) + values.append(winreg_test_lib.TestRegValue( + '0', 'Some random text here'.encode('utf_16_le'), + winreg_interface.WinRegValue.REG_SZ, 1892)) + values.append(winreg_test_lib.TestRegValue( + '1', 'c:\\evil.exe'.encode('utf_16_le'), + winreg_interface.WinRegValue.REG_BINARY, 612)) + values.append(winreg_test_lib.TestRegValue( + '2', 'C:\\looks_legit.exe'.encode('utf_16_le'), + winreg_interface.WinRegValue.REG_SZ, 1001)) + + winreg_key = winreg_test_lib.TestRegKey( + key_path, 1346145829002031, values, 1456) + + event_queue_consumer = self._ParseKeyWithPlugin(self._plugin, winreg_key) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 1) + + # A MRUListEx event object. + event_object = event_objects[0] + + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-08-28 09:23:49.002031') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'[{0:s}] ' + u'Index: 1 [MRU Value 2]: C:\\looks_legit.exe ' + u'Index: 2 [MRU Value 0]: Some random text here ' + u'Index: 3 [MRU Value 1]: c:\\evil.exe').format(key_path) + + expected_msg_short = ( + u'[{0:s}] Index: 1 [MRU Value 2]: C:\\l...').format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +class TestMRUListExShellItemListPlugin(test_lib.RegistryPluginTestCase): + """Tests for the shell item list MRUListEx plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = mrulistex.MRUListExShellItemListPlugin() + + def testProcess(self): + """Tests the Process function.""" + test_file_entry = self._GetTestFileEntryFromPath(['NTUSER-WIN7.DAT']) + key_path = ( + u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\ComDlg32\\' + u'OpenSavePidlMRU') + winreg_key = self._GetKeyFromFileEntry(test_file_entry, key_path) + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_key, file_entry=test_file_entry) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 65) + + # A MRUListEx event object. + event_object = event_objects[40] + + self.assertEquals(event_object.pathspec, test_file_entry.path_spec) + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-08-28 22:48:28.159308') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'[{0:s}\\exe] ' + u'Index: 1 [MRU Value 1]: Shell item list: [My Computer, P:\\, ' + u'Application Tools, Firefox 6.0, Firefox Setup 6.0.exe] ' + u'Index: 2 [MRU Value 0]: Shell item list: [Computers and Devices, ' + u'UNKNOWN: 0x00, \\\\controller\\WebDavShare, Firefox Setup 3.6.12.exe' + u']').format(key_path) + + expected_msg_short = ( + u'[\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\ComDlg32\\' + u'OpenSavePidlMRU...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + # A shell item event object. + event_object = event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-03-08 22:16:02') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'Name: ALLOYR~1 ' + u'Long name: Alloy Research ' + u'NTFS file reference: 44518-33 ' + u'Origin: {0:s}\\*').format(key_path) + + expected_msg_short = ( + u'Name: ALLOYR~1 ' + u'NTFS file reference: 44518-33 ' + u'Origin: \\Software\\Microsoft\\Wind...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +class TestMRUListExStringAndShellItemPlugin(test_lib.RegistryPluginTestCase): + """Tests for the string and shell item MRUListEx plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = mrulistex.MRUListExStringAndShellItemPlugin() + + def testProcess(self): + """Tests the Process function.""" + test_file_entry = self._GetTestFileEntryFromPath(['NTUSER-WIN7.DAT']) + key_path = ( + u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\RecentDocs') + winreg_key = self._GetKeyFromFileEntry(test_file_entry, key_path) + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_key, file_entry=test_file_entry) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 6) + + # A MRUListEx event object. + event_object = event_objects[0] + + self.assertEquals(event_object.pathspec, test_file_entry.path_spec) + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-04-01 13:52:39.113741') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'[{0:s}] ' + u'Index: 1 [MRU Value 17]: Path: The SHIELD, ' + u'Shell item: [The SHIELD.lnk] ' + u'Index: 10 [MRU Value 11]: Path: 5031RR_BalancedLeadership.pdf, ' + u'Shell item: [5031RR_BalancedLeadership.lnk] ' + u'Index: 11 [MRU Value 10]: ' + u'Path: SA-23E Mitchell-Hyundyne Starfury.docx, ' + u'Shell item: [SA-23E Mitchell-Hyundyne Starfury.lnk] ' + u'Index: 12 [MRU Value 9]: Path: StarFury.docx, ' + u'Shell item: [StarFury (3).lnk] ' + u'Index: 13 [MRU Value 6]: Path: StarFury.zip, ' + u'Shell item: [StarFury.lnk] ' + u'Index: 14 [MRU Value 4]: Path: VIBRANIUM.docx, ' + u'Shell item: [VIBRANIUM.lnk] ' + u'Index: 15 [MRU Value 5]: Path: ADAMANTIUM-Background.docx, ' + u'Shell item: [ADAMANTIUM-Background.lnk] ' + u'Index: 16 [MRU Value 3]: Path: Pictures, ' + u'Shell item: [Pictures.lnk] ' + u'Index: 17 [MRU Value 2]: Path: nick_fury_77831.jpg, ' + u'Shell item: [nick_fury_77831.lnk] ' + u'Index: 18 [MRU Value 1]: Path: Downloads, ' + u'Shell item: [Downloads.lnk] ' + u'Index: 19 [MRU Value 0]: Path: wallpaper_medium.jpg, ' + u'Shell item: [wallpaper_medium.lnk] ' + u'Index: 2 [MRU Value 18]: ' + u'Path: captain_america_shield_by_almogrem-d48x9x8.jpg, ' + u'Shell item: [captain_america_shield_by_almogrem-d48x9x8.lnk] ' + u'Index: 3 [MRU Value 16]: Path: captain-america-shield-front.jpg, ' + u'Shell item: [captain-america-shield-front.lnk] ' + u'Index: 4 [MRU Value 12]: Path: Leadership, ' + u'Shell item: [Leadership.lnk] ' + u'Index: 5 [MRU Value 15]: Path: followership.pdf, ' + u'Shell item: [followership.lnk] ' + u'Index: 6 [MRU Value 14]: Path: leaderqualities.pdf, ' + u'Shell item: [leaderqualities.lnk] ' + u'Index: 7 [MRU Value 13]: Path: htlhtl.pdf, ' + u'Shell item: [htlhtl.lnk] ' + u'Index: 8 [MRU Value 8]: Path: StarFury, ' + u'Shell item: [StarFury (2).lnk] ' + u'Index: 9 [MRU Value 7]: Path: Earth_SA-26_Thunderbolt.jpg, ' + u'Shell item: [Earth_SA-26_Thunderbolt.lnk]').format(key_path) + + expected_msg_short = ( + u'[{0:s}] Index: 1 [MR...').format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +class TestMRUListExStringAndShellItemListPlugin( + test_lib.RegistryPluginTestCase): + """Tests for the string and shell item list MRUListEx plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = mrulistex.MRUListExStringAndShellItemListPlugin() + + def testProcess(self): + """Tests the Process function.""" + test_file_entry = self._GetTestFileEntryFromPath(['NTUSER-WIN7.DAT']) + key_path = ( + u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\ComDlg32\\' + u'LastVisitedPidlMRU') + winreg_key = self._GetKeyFromFileEntry(test_file_entry, key_path) + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_key, file_entry=test_file_entry) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 31) + + # A MRUListEx event object. + event_object = event_objects[30] + + self.assertEquals(event_object.pathspec, test_file_entry.path_spec) + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-04-01 13:52:38.966290') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'[{0:s}] ' + u'Index: 1 [MRU Value 1]: Path: chrome.exe, ' + u'Shell item list: [Users Libraries, UNKNOWN: 0x00, UNKNOWN: 0x00, ' + u'UNKNOWN: 0x00] ' + u'Index: 2 [MRU Value 7]: ' + u'Path: {{48E1ED6B-CF49-4609-B1C1-C082BFC3D0B4}}, ' + u'Shell item list: [Shared Documents Folder (Users Files), ' + u'UNKNOWN: 0x00, Alloy Research] ' + u'Index: 3 [MRU Value 6]: ' + u'Path: {{427865A0-03AF-4F25-82EE-10B6CB1DED3E}}, ' + u'Shell item list: [Users Libraries, UNKNOWN: 0x00, UNKNOWN: 0x00] ' + u'Index: 4 [MRU Value 5]: ' + u'Path: {{24B5C9BB-48B5-47FF-8343-40481DBA1E2B}}, ' + u'Shell item list: [My Computer, C:\\, Users, nfury, Documents] ' + u'Index: 5 [MRU Value 4]: ' + u'Path: {{0B8CFE96-DB69-4D33-8E3C-36EAB4F709E0}}, ' + u'Shell item list: [My Computer, C:\\, Users, nfury, Documents, ' + u'Alloy Research] ' + u'Index: 6 [MRU Value 3]: ' + u'Path: {{D4F85F66-003D-4127-BCE9-CAD7A57B2857}}, ' + u'Shell item list: [Users Libraries, UNKNOWN: 0x00, UNKNOWN: 0x00] ' + u'Index: 7 [MRU Value 0]: Path: iexplore.exe, ' + u'Shell item list: [My Computer, P:\\, Application Tools, Firefox 6.0] ' + u'Index: 8 [MRU Value 2]: Path: Skype.exe, ' + u'Shell item list: [Users Libraries, UNKNOWN: 0x00]').format(key_path) + + expected_msg_short = ( + u'[\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\ComDlg32\\' + u'LastVisitedPidl...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/msie_zones.py b/plaso/parsers/winreg_plugins/msie_zones.py new file mode 100644 index 0000000..211f051 --- /dev/null +++ b/plaso/parsers/winreg_plugins/msie_zones.py @@ -0,0 +1,292 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the MSIE zone settings plugin.""" + +from plaso.events import windows_events +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface + + +__author__ = 'Elizabeth Schweinsberg (beth@bethlogic.net)' + + +class MsieZoneSettingsPlugin(interface.KeyPlugin): + """Windows Registry plugin for parsing the MSIE Zones settings.""" + + NAME = 'winreg_msie_zone' + DESCRIPTION = u'Parser for Internet Explorer zone settings Registry data.' + + REG_TYPE = 'NTUSER' + + REG_KEYS = [ + (u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' + u'\\Zones'), + (u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' + u'\\Lockdown_Zones')] + + URLS = ['http://support.microsoft.com/kb/182569'] + + ZONE_NAMES = { + '0': '0 (My Computer)', + '1': '1 (Local Intranet Zone)', + '2': '2 (Trusted sites Zone)', + '3': '3 (Internet Zone)', + '4': '4 (Restricted Sites Zone)', + '5': '5 (Custom)' + } + + KNOWN_PERMISSIONS_VALUE_NAMES = [ + '1001', '1004', '1200', '1201', '1400', '1402', '1405', '1406', '1407', + '1601', '1604', '1606', '1607', '1608', '1609', '1800', '1802', '1803', + '1804', '1809', '1A04', '2000', '2001', '2004', '2100', '2101', '2102', + '2200', '2201', '2300'] + + CONTROL_VALUES_PERMISSIONS = { + 0x00000000: '0 (Allow)', + 0x00000001: '1 (Prompt User)', + 0x00000003: '3 (Not Allowed)', + 0x00010000: '0x00010000 (Administrator approved)' + } + + CONTROL_VALUES_SAFETY = { + 0x00010000: '0x00010000 (High safety)', + 0x00020000: '0x00020000 (Medium safety)', + 0x00030000: '0x00030000 (Low safety)' + } + + CONTROL_VALUES_1A00 = { + 0x00000000: ('0x00000000 (Automatic logon with current user name and ' + 'password)'), + 0x00010000: '0x00010000 (Prompt for user name and password)', + 0x00020000: '0x00020000 (Automatic logon only in Intranet zone)', + 0x00030000: '0x00030000 (Anonymous logon)' + } + + CONTROL_VALUES_1C00 = { + 0x00000000: '0x00000000 (Disable Java)', + 0x00010000: '0x00010000 (High safety)', + 0x00020000: '0x00020000 (Medium safety)', + 0x00030000: '0x00030000 (Low safety)', + 0x00800000: '0x00800000 (Custom)' + } + + FEATURE_CONTROLS = { + '1200': 'Run ActiveX controls and plug-ins', + '1400': 'Active scripting', + '1001': 'Download signed ActiveX controls', + '1004': 'Download unsigned ActiveX controls', + '1201': 'Initialize and script ActiveX controls not marked as safe', + '1206': 'Allow scripting of IE Web browser control', + '1207': 'Reserved', + '1208': 'Allow previously unused ActiveX controls to run without prompt', + '1209': 'Allow Scriptlets', + '120A': 'Override Per-Site (domain-based) ActiveX restrictions', + '120B': 'Override Per-Site (domain-based) ActiveX restrictions', + '1402': 'Scripting of Java applets', + '1405': 'Script ActiveX controls marked as safe for scripting', + '1406': 'Access data sources across domains', + '1407': 'Allow Programmatic clipboard access', + '1408': 'Reserved', + '1601': 'Submit non-encrypted form data', + '1604': 'Font download', + '1605': 'Run Java', + '1606': 'Userdata persistence', + '1607': 'Navigate sub-frames across different domains', + '1608': 'Allow META REFRESH', + '1609': 'Display mixed content', + '160A': 'Include local directory path when uploading files to a server', + '1800': 'Installation of desktop items', + '1802': 'Drag and drop or copy and paste files', + '1803': 'File Download', + '1804': 'Launching programs and files in an IFRAME', + '1805': 'Launching programs and files in webview', + '1806': 'Launching applications and unsafe files', + '1807': 'Reserved', + '1808': 'Reserved', + '1809': 'Use Pop-up Blocker', + '180A': 'Reserved', + '180B': 'Reserved', + '180C': 'Reserved', + '180D': 'Reserved', + '1A00': 'User Authentication: Logon', + '1A02': 'Allow persistent cookies that are stored on your computer', + '1A03': 'Allow per-session cookies (not stored)', + '1A04': 'Don\'t prompt for client cert selection when no certs exists', + '1A05': 'Allow 3rd party persistent cookies', + '1A06': 'Allow 3rd party session cookies', + '1A10': 'Privacy Settings', + '1C00': 'Java permissions', + '1E05': 'Software channel permissions', + '1F00': 'Reserved', + '2000': 'Binary and script behaviors', + '2001': '.NET: Run components signed with Authenticode', + '2004': '.NET: Run components not signed with Authenticode', + '2100': 'Open files based on content, not file extension', + '2101': 'Web sites in less privileged zone can navigate into this zone', + '2102': ('Allow script initiated windows without size/position ' + 'constraints'), + '2103': 'Allow status bar updates via script', + '2104': 'Allow websites to open windows without address or status bars', + '2105': 'Allow websites to prompt for information using scripted windows', + '2200': 'Automatic prompting for file downloads', + '2201': 'Automatic prompting for ActiveX controls', + '2300': 'Allow web pages to use restricted protocols for active content', + '2301': 'Use Phishing Filter', + '2400': '.NET: XAML browser applications', + '2401': '.NET: XPS documents', + '2402': '.NET: Loose XAML', + '2500': 'Turn on Protected Mode', + '2600': 'Enable .NET Framework setup', + '{AEBA21FA-782A-4A90-978D-B72164C80120}': 'First Party Cookie', + '{A8A88C49-5EB2-4990-A1A2-0876022C854F}': 'Third Party Cookie' + } + + def GetEntries( + self, parser_context, file_entry=None, key=None, registry_type=None, + parser_chain=None, **unused_kwargs): + """Retrieves information of the Internet Settings Zones values. + + The MSIE Feature controls are stored in the Zone specific subkeys in: + Internet Settings\\Zones key + Internet Settings\\Lockdown_Zones key + + Args: + parser_context: A parser context object (instance of ParserContext). + file_entry: optional file entry object (instance of dfvfs.FileEntry). + The default is None. + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + text_dict = {} + + if key.number_of_values == 0: + error_string = u'Key: {0:s} missing values.'.format(key.path) + parser_context.ProduceParseError( + self.NAME, error_string, file_entry=file_entry) + + else: + for value in key.GetValues(): + if not value.name: + value_name = '(default)' + else: + value_name = u'{0:s}'.format(value.name) + + if value.DataIsString(): + value_string = u'[{0:s}] {1:s}'.format( + value.data_type_string, value.data) + elif value.DataIsInteger(): + value_string = u'[{0:s}] {1:d}'.format( + value.data_type_string, value.data) + elif value.DataIsMultiString(): + value_string = u'[{0:s}] {1:s}'.format( + value.data_type_string, u''.join(value.data)) + else: + value_string = u'[{0:s}]'.format(value.data_type_string) + + text_dict[value_name] = value_string + + # Generate at least one event object for the key. + event_object = windows_events.WindowsRegistryEvent( + key.last_written_timestamp, key.path, text_dict, offset=key.offset, + registry_type=registry_type, urls=self.URLS) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if key.number_of_subkeys == 0: + error_string = u'Key: {0:s} missing subkeys.'.format(key.path) + parser_context.ProduceParseError( + self.NAME, error_string, file_entry=file_entry) + return + + for zone_key in key.GetSubkeys(): + # TODO: these values are stored in the Description value of the + # zone key. This solution will break on zone values that are larger + # than 5. + path = u'{0:s}\\{1:s}'.format(key.path, self.ZONE_NAMES[zone_key.name]) + + text_dict = {} + + # TODO: this plugin currently just dumps the values and does not + # distinguish between what is a feature control or not. + for value in zone_key.GetValues(): + # Ignore the default value. + if not value.name: + continue + + if value.DataIsString(): + value_string = value.data + + elif value.DataIsInteger(): + if value.name in self.KNOWN_PERMISSIONS_VALUE_NAMES: + value_string = self.CONTROL_VALUES_PERMISSIONS.get( + value.data, u'UNKNOWN') + elif value.name == '1A00': + value_string = self.CONTROL_VALUES_1A00.get(value.data, u'UNKNOWN') + elif value.name == '1C00': + value_string = self.CONTROL_VALUES_1C00.get(value.data, u'UNKNOWN') + elif value.name == '1E05': + value_string = self.CONTROL_VALUES_SAFETY.get( + value.data, u'UNKNOWN') + else: + value_string = u'{0:d}'.format(value.data) + + else: + value_string = u'[{0:s}]'.format(value.data_type_string) + + if len(value.name) == 4 and value.name != 'Icon': + value_description = self.FEATURE_CONTROLS.get(value.name, 'UNKNOWN') + else: + value_description = self.FEATURE_CONTROLS.get(value.name, '') + + if value_description: + feature_control = u'[{0:s}] {1:s}'.format( + value.name, value_description) + else: + feature_control = u'[{0:s}]'.format(value.name) + + text_dict[feature_control] = value_string + + event_object = windows_events.WindowsRegistryEvent( + zone_key.last_written_timestamp, path, text_dict, + offset=zone_key.offset, registry_type=registry_type, + urls=self.URLS) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +class MsieZoneSettingsSoftwareZonesPlugin(MsieZoneSettingsPlugin): + """Parses the Zones key in the Software hive.""" + + NAME = 'winreg_msie_zone_software' + + REG_TYPE = 'SOFTWARE' + REG_KEYS = [ + u'\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\\Zones', + (u'\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' + u'\\Lockdown_Zones'), + (u'\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' + u'\\Zones'), + (u'\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' + u'\\Lockdown_Zones')] + + +winreg.WinRegistryParser.RegisterPlugins([ + MsieZoneSettingsPlugin, MsieZoneSettingsSoftwareZonesPlugin]) diff --git a/plaso/parsers/winreg_plugins/msie_zones_test.py b/plaso/parsers/winreg_plugins/msie_zones_test.py new file mode 100644 index 0000000..55c9729 --- /dev/null +++ b/plaso/parsers/winreg_plugins/msie_zones_test.py @@ -0,0 +1,384 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the MSIE Zone settings Windows Registry plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.lib import timelib_test +from plaso.parsers.winreg_plugins import msie_zones +from plaso.parsers.winreg_plugins import test_lib + + +class MsieZoneSettingsSoftwareZonesPluginTest(test_lib.RegistryPluginTestCase): + """Tests for Internet Settings Zones plugin on the Software hive.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = msie_zones.MsieZoneSettingsSoftwareZonesPlugin() + self._test_file = self._GetTestFilePath(['SOFTWARE']) + + def testProcessForZone(self): + """Tests the Process function.""" + key_path = u'\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\\Zones' + winreg_key = self._GetKeyFromFile(self._test_file, key_path) + event_queue_consumer = self._ParseKeyWithPlugin(self._plugin, winreg_key) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 6) + + event_object = event_objects[1] + + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-08-28 21:32:44.937675') + self.assertEquals(event_object.timestamp, expected_timestamp) + + regvalue_identifier = u'[1200] Run ActiveX controls and plug-ins' + expected_value = u'0 (Allow)' + self._TestRegvalue(event_object, regvalue_identifier, expected_value) + + expected_msg = ( + u'[{0:s}\\0 (My Computer)] ' + u'[1001] Download signed ActiveX controls: 0 (Allow) ' + u'[1004] Download unsigned ActiveX controls: 0 (Allow) ' + u'[1200] Run ActiveX controls and plug-ins: 0 (Allow) ' + u'[1201] Initialize and script ActiveX controls not marked as safe: 1 ' + u'(Prompt User) ' + u'[1206] Allow scripting of IE Web browser control: 0 ' + u'[1207] Reserved: 0 ' + u'[1208] Allow previously unused ActiveX controls to run without ' + u'prompt: 0 ' + u'[1209] Allow Scriptlets: 0 ' + u'[120A] Override Per-Site (domain-based) ActiveX restrictions: 0 ' + u'[120B] Override Per-Site (domain-based) ActiveX restrictions: 0 ' + u'[1400] Active scripting: 0 (Allow) ' + u'[1402] Scripting of Java applets: 0 (Allow) ' + u'[1405] Script ActiveX controls marked as safe for scripting: 0 ' + u'(Allow) ' + u'[1406] Access data sources across domains: 0 (Allow) ' + u'[1407] Allow Programmatic clipboard access: 0 (Allow) ' + u'[1408] Reserved: 0 ' + u'[1409] UNKNOWN: 3 ' + u'[1601] Submit non-encrypted form data: 0 (Allow) ' + u'[1604] Font download: 0 (Allow) ' + u'[1605] Run Java: 0 ' + u'[1606] Userdata persistence: 0 (Allow) ' + u'[1607] Navigate sub-frames across different domains: 0 (Allow) ' + u'[1608] Allow META REFRESH: 0 (Allow) ' + u'[1609] Display mixed content: 1 (Prompt User) ' + u'[160A] Include local directory path when uploading files to a ' + u'server: 0 ' + u'[1802] Drag and drop or copy and paste files: 0 (Allow) ' + u'[1803] File Download: 0 (Allow) ' + u'[1804] Launching programs and files in an IFRAME: 0 (Allow) ' + u'[1805] Launching programs and files in webview: 0 ' + u'[1806] Launching applications and unsafe files: 0 ' + u'[1807] Reserved: 0 ' + u'[1808] Reserved: 0 ' + u'[1809] Use Pop-up Blocker: 3 (Not Allowed) ' + u'[180A] Reserved: 0 ' + u'[180C] Reserved: 0 ' + u'[180D] Reserved: 0 ' + u'[180E] UNKNOWN: 0 ' + u'[180F] UNKNOWN: 0 ' + u'[1A00] User Authentication: Logon: 0x00000000 (Automatic logon with ' + u'current user name and password) ' + u'[1A02] Allow persistent cookies that are stored on your computer: 0 ' + u'[1A03] Allow per-session cookies (not stored): 0 ' + u'[1A04] Don\'t prompt for client cert selection when no certs exists: ' + u'0 (Allow) ' + u'[1A05] Allow 3rd party persistent cookies: 0 ' + u'[1A06] Allow 3rd party session cookies: 0 ' + u'[1A10] Privacy Settings: 0 ' + u'[1C00] Java permissions: 0x00020000 (Medium safety) ' + u'[2000] Binary and script behaviors: 0 (Allow) ' + u'[2001] .NET: Run components signed with Authenticode: ' + u'3 (Not Allowed) ' + u'[2004] .NET: Run components not signed with Authenticode: ' + u'3 (Not Allowed) ' + u'[2005] UNKNOWN: 0 ' + u'[2007] UNKNOWN: 3 ' + u'[2100] Open files based on content, not file extension: 0 (Allow) ' + u'[2101] Web sites in less privileged zone can navigate into this ' + u'zone: 3 (Not Allowed) ' + u'[2102] Allow script initiated windows without size/position ' + u'constraints: 0 (Allow) ' + u'[2103] Allow status bar updates via script: 0 ' + u'[2104] Allow websites to open windows without address or status ' + u'bars: 0 ' + u'[2105] Allow websites to prompt for information using scripted ' + u'windows: 0 ' + u'[2106] UNKNOWN: 0 ' + u'[2107] UNKNOWN: 0 ' + u'[2200] Automatic prompting for file downloads: 0 (Allow) ' + u'[2201] Automatic prompting for ActiveX controls: 0 (Allow) ' + u'[2300] Allow web pages to use restricted protocols for active ' + u'content: 1 (Prompt User) ' + u'[2301] Use Phishing Filter: 3 ' + u'[2400] .NET: XAML browser applications: 0 ' + u'[2401] .NET: XPS documents: 0 ' + u'[2402] .NET: Loose XAML: 0 ' + u'[2500] Turn on Protected Mode: 3 ' + u'[2600] Enable .NET Framework setup: 0 ' + u'[2700] UNKNOWN: 3 ' + u'[2701] UNKNOWN: 0 ' + u'[2702] UNKNOWN: 3 ' + u'[2703] UNKNOWN: 3 ' + u'[2708] UNKNOWN: 0 ' + u'[2709] UNKNOWN: 0 ' + u'[CurrentLevel]: 0 ' + u'[Description]: Your computer ' + u'[DisplayName]: Computer ' + u'[Flags]: 33 ' + u'[Icon]: shell32.dll#0016 ' + u'[LowIcon]: inetcpl.cpl#005422 ' + u'[PMDisplayName]: Computer ' + u'[Protected Mode]').format(key_path) + + expected_msg_short = u'[{0:s}\\0 (My Computer)] [...'.format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + def testProcessForLockDown(self): + """Tests the Process function for the lockdown zone key.""" + key_path = ( + u'\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' + u'\\Lockdown_Zones') + winreg_key = self._GetKeyFromFile(self._test_file, key_path) + event_queue_consumer = self._ParseKeyWithPlugin(self._plugin, winreg_key) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 6) + + event_object = event_objects[1] + + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-08-28 21:32:44.937675') + self.assertEquals(event_object.timestamp, expected_timestamp) + + regvalue_identifier = u'[1200] Run ActiveX controls and plug-ins' + expected_value = u'3 (Not Allowed)' + self._TestRegvalue(event_object, regvalue_identifier, expected_value) + + expected_msg = ( + u'[{0:s}\\0 (My Computer)] ' + u'[1001] Download signed ActiveX controls: 1 (Prompt User) ' + u'[1004] Download unsigned ActiveX controls: 3 (Not Allowed) ' + u'[1200] Run ActiveX controls and plug-ins: 3 (Not Allowed) ' + u'[1201] Initialize and script ActiveX controls not marked as safe: 3 ' + u'(Not Allowed) ' + u'[1206] Allow scripting of IE Web browser control: 0 ' + u'[1207] Reserved: 3 ' + u'[1208] Allow previously unused ActiveX controls to run without ' + u'prompt: 3 ' + u'[1209] Allow Scriptlets: 3 ' + u'[120A] Override Per-Site (domain-based) ActiveX restrictions: 3 ' + u'[120B] Override Per-Site (domain-based) ActiveX restrictions: 0 ' + u'[1400] Active scripting: 1 (Prompt User) ' + u'[1402] Scripting of Java applets: 0 (Allow) ' + u'[1405] Script ActiveX controls marked as safe for scripting: 0 ' + u'(Allow) ' + u'[1406] Access data sources across domains: 0 (Allow) ' + u'[1407] Allow Programmatic clipboard access: 1 (Prompt User) ' + u'[1408] Reserved: 3 ' + u'[1409] UNKNOWN: 3 ' + u'[1601] Submit non-encrypted form data: 0 (Allow) ' + u'[1604] Font download: 0 (Allow) ' + u'[1605] Run Java: 0 ' + u'[1606] Userdata persistence: 0 (Allow) ' + u'[1607] Navigate sub-frames across different domains: 0 (Allow) ' + u'[1608] Allow META REFRESH: 0 (Allow) ' + u'[1609] Display mixed content: 1 (Prompt User) ' + u'[160A] Include local directory path when uploading files to a ' + u'server: 0 ' + u'[1802] Drag and drop or copy and paste files: 0 (Allow) ' + u'[1803] File Download: 0 (Allow) ' + u'[1804] Launching programs and files in an IFRAME: 0 (Allow) ' + u'[1805] Launching programs and files in webview: 0 ' + u'[1806] Launching applications and unsafe files: 0 ' + u'[1807] Reserved: 0 ' + u'[1808] Reserved: 0 ' + u'[1809] Use Pop-up Blocker: 3 (Not Allowed) ' + u'[180A] Reserved: 0 ' + u'[180C] Reserved: 0 ' + u'[180D] Reserved: 0 ' + u'[180E] UNKNOWN: 0 ' + u'[180F] UNKNOWN: 0 ' + u'[1A00] User Authentication: Logon: 0x00000000 (Automatic logon with ' + u'current user name and password) ' + u'[1A02] Allow persistent cookies that are stored on your computer: 0 ' + u'[1A03] Allow per-session cookies (not stored): 0 ' + u'[1A04] Don\'t prompt for client cert selection when no certs exists: ' + u'3 (Not Allowed) ' + u'[1A05] Allow 3rd party persistent cookies: 0 ' + u'[1A06] Allow 3rd party session cookies: 0 ' + u'[1A10] Privacy Settings: 0 ' + u'[1C00] Java permissions: 0x00000000 (Disable Java) ' + u'[2000] Binary and script behaviors: 0x00010000 ' + u'(Administrator approved) ' + u'[2005] UNKNOWN: 3 ' + u'[2100] Open files based on content, not file extension: 3 ' + u'(Not Allowed) ' + u'[2101] Web sites in less privileged zone can navigate into this ' + u'zone: 3 (Not Allowed) ' + u'[2102] Allow script initiated windows without size/position ' + u'constraints: ' + u'3 (Not Allowed) ' + u'[2103] Allow status bar updates via script: 3 ' + u'[2104] Allow websites to open windows without address or status ' + u'bars: 3 ' + u'[2105] Allow websites to prompt for information using scripted ' + u'windows: 3 ' + u'[2106] UNKNOWN: 3 ' + u'[2107] UNKNOWN: 3 ' + u'[2200] Automatic prompting for file downloads: 3 (Not Allowed) ' + u'[2201] Automatic prompting for ActiveX controls: 3 (Not Allowed) ' + u'[2301] Use Phishing Filter: 3 ' + u'[2400] .NET: XAML browser applications: 0 ' + u'[2401] .NET: XPS documents: 0 ' + u'[2402] .NET: Loose XAML: 0 ' + u'[2500] Turn on Protected Mode: 3 ' + u'[2600] Enable .NET Framework setup: 0 ' + u'[2700] UNKNOWN: 3 ' + u'[2701] UNKNOWN: 3 ' + u'[2702] UNKNOWN: 3 ' + u'[2703] UNKNOWN: 3 ' + u'[2708] UNKNOWN: 0 ' + u'[2709] UNKNOWN: 0 ' + u'[CurrentLevel]: 0 ' + u'[Description]: Your computer ' + u'[DisplayName]: Computer ' + u'[Flags]: 33 ' + u'[Icon]: shell32.dll#0016 ' + u'[LowIcon]: inetcpl.cpl#005422 ' + u'[PMDisplayName]: Computer ' + u'[Protected Mode]').format(key_path) + + expected_msg_short = u'[{0:s}\\0 (My Com...'.format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +class MsieZoneSettingsUserZonesPluginTest(test_lib.RegistryPluginTestCase): + """Tests for Internet Settings Zones plugin on the User hive.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = msie_zones.MsieZoneSettingsPlugin() + self._test_file = self._GetTestFilePath(['NTUSER-WIN7.DAT']) + + def testProcessForZone(self): + """Tests the Process function.""" + key_path = ( + u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' + u'\\Zones') + winreg_key = self._GetKeyFromFile(self._test_file, key_path) + event_queue_consumer = self._ParseKeyWithPlugin(self._plugin, winreg_key) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 6) + + event_object = event_objects[1] + + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-09-16 21:12:40.145514') + self.assertEquals(event_object.timestamp, expected_timestamp) + + regvalue_identifier = u'[1200] Run ActiveX controls and plug-ins' + expected_value = u'0 (Allow)' + self._TestRegvalue(event_object, regvalue_identifier, expected_value) + + expected_msg = ( + u'[{0:s}\\0 (My Computer)] ' + u'[1200] Run ActiveX controls and plug-ins: 0 (Allow) ' + u'[1400] Active scripting: 0 (Allow) ' + u'[2001] .NET: Run components signed with Authenticode: 3 (Not ' + u'Allowed) ' + u'[2004] .NET: Run components not signed with Authenticode: 3 (Not ' + u'Allowed) ' + u'[2007] UNKNOWN: 3 ' + u'[CurrentLevel]: 0 ' + u'[Description]: Your computer ' + u'[DisplayName]: Computer ' + u'[Flags]: 33 [Icon]: shell32.dll#0016 ' + u'[LowIcon]: inetcpl.cpl#005422 ' + u'[PMDisplayName]: Computer ' + u'[Protected Mode]').format(key_path) + + expected_msg_short = u'[{0:s}\\0 (My Com...'.format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + def testProcessForLockDown(self): + """Tests the Process function.""" + key_path = ( + u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings' + u'\\Lockdown_Zones') + winreg_key = self._GetKeyFromFile(self._test_file, key_path) + event_queue_consumer = self._ParseKeyWithPlugin(self._plugin, winreg_key) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 6) + + event_object = event_objects[1] + + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-09-16 21:12:40.145514') + self.assertEquals(event_object.timestamp, expected_timestamp) + + regvalue_identifier = u'[1200] Run ActiveX controls and plug-ins' + expected_value = u'3 (Not Allowed)' + self._TestRegvalue(event_object, regvalue_identifier, expected_value) + + expected_msg = ( + u'[{0:s}\\0 (My Computer)] ' + u'[1200] Run ActiveX controls and plug-ins: 3 (Not Allowed) ' + u'[1400] Active scripting: 1 (Prompt User) ' + u'[CurrentLevel]: 0 ' + u'[Description]: Your computer ' + u'[DisplayName]: Computer ' + u'[Flags]: 33 ' + u'[Icon]: shell32.dll#0016 ' + u'[LowIcon]: inetcpl.cpl#005422 ' + u'[PMDisplayName]: Computer ' + u'[Protected Mode]').format(key_path) + + expected_msg_short = u'[{0:s}\\...'.format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/officemru.py b/plaso/parsers/winreg_plugins/officemru.py new file mode 100644 index 0000000..487133a --- /dev/null +++ b/plaso/parsers/winreg_plugins/officemru.py @@ -0,0 +1,116 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a parser for MS Office MRUs for Plaso.""" + +import logging +import re + +from plaso.events import windows_events +from plaso.lib import timelib +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface + + +__author__ = 'David Nides (david.nides@gmail.com)' + + +class OfficeMRUPlugin(interface.KeyPlugin): + """Plugin that parses Microsoft Office MRU keys.""" + + NAME = 'winreg_office_mru' + DESCRIPTION = u'Parser for Microsoft Office MRU Registry data.' + + REG_TYPE = 'NTUSER' + + REG_KEYS = [ + u'\\Software\\Microsoft\\Office\\14.0\\Word\\Place MRU', + u'\\Software\\Microsoft\\Office\\14.0\\Access\\File MRU', + u'\\Software\\Microsoft\\Office\\14.0\\Access\\Place MRU', + u'\\Software\\Microsoft\\Office\\14.0\\PowerPoint\\File MRU', + u'\\Software\\Microsoft\\Office\\14.0\\PowerPoint\\Place MRU', + u'\\Software\\Microsoft\\Office\\14.0\\Excel\\File MRU', + u'\\Software\\Microsoft\\Office\\14.0\\Excel\\Place MRU', + u'\\Software\\Microsoft\\Office\\14.0\\Word\\File MRU'] + + _RE_VALUE_NAME = re.compile(r'^Item [0-9]+$', re.I) + + # The Office 12 item MRU is formatted as: + # [F00000000][T%FILETIME%]*\\%FILENAME% + + # The Office 14 item MRU is formatted as: + # [F00000000][T%FILETIME%][O00000000]*%FILENAME% + _RE_VALUE_DATA = re.compile(r'\[F00000000\]\[T([0-9A-Z]+)\].*\*[\\]?(.*)') + + def GetEntries( + self, parser_context, key=None, registry_type=None, file_entry=None, + parser_chain=None, **unused_kwargs): + """Collect Values under Office 2010 MRUs and return events for each one. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + # TODO: Test other Office versions to make sure this plugin is applicable. + for value in key.GetValues(): + # Ignore any value not in the form: 'Item [0-9]+'. + if not value.name or not self._RE_VALUE_NAME.search(value.name): + continue + + # Ignore any value that is empty or that does not contain a string. + if not value.data or not value.DataIsString(): + continue + + values = self._RE_VALUE_DATA.findall(value.data) + + # Values will contain a list containing a tuple containing 2 values. + if len(values) != 1 or len(values[0]) != 2: + continue + + try: + filetime = int(values[0][0], 16) + except ValueError: + logging.warning('Unable to convert filetime string to an integer.') + filetime = 0 + + # TODO: why this behavior? Only the first Item is stored with its + # timestamp. Shouldn't this be: Store all the Item # values with + # their timestamp and store the entire MRU as one event with the + # registry key last written time? + if value.name == 'Item 1': + timestamp = timelib.Timestamp.FromFiletime(filetime) + else: + timestamp = 0 + + text_dict = {} + text_dict[value.name] = value.data + + event_object = windows_events.WindowsRegistryEvent( + timestamp, key.path, text_dict, offset=key.offset, + registry_type=registry_type, + source_append=': Microsoft Office MRU') + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +winreg.WinRegistryParser.RegisterPlugin(OfficeMRUPlugin) diff --git a/plaso/parsers/winreg_plugins/officemru_test.py b/plaso/parsers/winreg_plugins/officemru_test.py new file mode 100644 index 0000000..7f7cb22 --- /dev/null +++ b/plaso/parsers/winreg_plugins/officemru_test.py @@ -0,0 +1,76 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Microsoft Office MRUs Windows Registry plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.lib import timelib_test +from plaso.parsers.winreg_plugins import officemru +from plaso.parsers.winreg_plugins import test_lib + + +__author__ = 'David Nides (david.nides@gmail.com)' + + +class OfficeMRUPluginTest(test_lib.RegistryPluginTestCase): + """Tests for the Microsoft Office MRUs Windows Registry plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = officemru.OfficeMRUPlugin() + + def testProcess(self): + """Tests the Process function.""" + test_file_entry = self._GetTestFileEntryFromPath(['NTUSER-WIN7.DAT']) + key_path = u'\\Software\\Microsoft\\Office\\14.0\\Word\\File MRU' + winreg_key = self._GetKeyFromFileEntry(test_file_entry, key_path) + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_key, file_entry=test_file_entry) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 5) + + event_object = event_objects[0] + + self.assertEquals(event_object.pathspec, test_file_entry.path_spec) + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-03-13 18:27:15.083') + self.assertEquals(event_object.timestamp, expected_timestamp) + + regvalue_identifier = u'Item 1' + expected_value = ( + u'[F00000000][T01CD0146EA1EADB0][O00000000]*' + u'C:\\Users\\nfury\\Documents\\StarFury\\StarFury\\' + u'SA-23E Mitchell-Hyundyne Starfury.docx') + self._TestRegvalue(event_object, regvalue_identifier, expected_value) + + expected_msg = u'[{0:s}] {1:s}: {2:s}'.format( + key_path, regvalue_identifier, expected_value) + expected_msg_short = u'[{0:s}] {1:s}: [F00000000][T01CD0146...'.format( + key_path, regvalue_identifier) + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/outlook.py b/plaso/parsers/winreg_plugins/outlook.py new file mode 100644 index 0000000..a4a2cf8 --- /dev/null +++ b/plaso/parsers/winreg_plugins/outlook.py @@ -0,0 +1,97 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains an Outlook Registry parser.""" + +from plaso.events import windows_events +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface + + +__author__ = 'David Nides (david.nides@gmail.com)' + + +class OutlookSearchMRUPlugin(interface.KeyPlugin): + """Windows Registry plugin parsing Outlook Search MRU keys.""" + + NAME = 'winreg_outlook_mru' + DESCRIPTION = u'Parser for Microsoft Outlook search MRU Registry data.' + + REG_KEYS = [ + u'\\Software\\Microsoft\\Office\\15.0\\Outlook\\Search', + u'\\Software\\Microsoft\\Office\\14.0\\Outlook\\Search'] + + # TODO: The catalog for Office 2013 (15.0) contains binary values not + # dword values. Check if Office 2007 and 2010 have the same. Re-enable the + # plug-ins once confirmed and OutlookSearchMRUPlugin has been extended to + # handle the binary data or create a OutlookSearchCatalogMRUPlugin. + # Registry keys for: + # MS Outlook 2007 Search Catalog: + # '\\Software\\Microsoft\\Office\\12.0\\Outlook\\Catalog' + # MS Outlook 2010 Search Catalog: + # '\\Software\\Microsoft\\Office\\14.0\\Outlook\\Search\\Catalog' + # MS Outlook 2013 Search Catalog: + # '\\Software\\Microsoft\\Office\\15.0\\Outlook\\Search\\Catalog' + + REG_TYPE = 'NTUSER' + + def GetEntries( + self, parser_context, key=None, registry_type=None, file_entry=None, + parser_chain=None, **unused_kwargs): + """Collect the values under Outlook and return event for each one. + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + """ + value_index = 0 + for value in key.GetValues(): + # Ignore the default value. + if not value.name: + continue + + # Ignore any value that is empty or that does not contain an integer. + if not value.data or not value.DataIsInteger(): + continue + + # TODO: change this 32-bit integer into something meaningful, for now + # the value name is the most interesting part. + text_dict = {} + text_dict[value.name] = '0x{0:08x}'.format(value.data) + + if value_index == 0: + timestamp = key.last_written_timestamp + else: + timestamp = 0 + + event_object = windows_events.WindowsRegistryEvent( + timestamp, key.path, text_dict, offset=key.offset, + registry_type=registry_type, + source_append=': PST Paths') + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + value_index += 1 + + +winreg.WinRegistryParser.RegisterPlugin(OutlookSearchMRUPlugin) diff --git a/plaso/parsers/winreg_plugins/outlook_test.py b/plaso/parsers/winreg_plugins/outlook_test.py new file mode 100644 index 0000000..374cdb6 --- /dev/null +++ b/plaso/parsers/winreg_plugins/outlook_test.py @@ -0,0 +1,103 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Outlook Windows Registry plugins.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.parsers.winreg_plugins import outlook +from plaso.parsers.winreg_plugins import test_lib +from plaso.winreg import test_lib as winreg_test_lib + + +class MSOutlook2013SearchMRUPluginTest(test_lib.RegistryPluginTestCase): + """Tests for the Outlook Search MRU Windows Registry plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = outlook.OutlookSearchMRUPlugin() + + def testProcess(self): + """Tests the Process function.""" + key_path = u'\\Software\\Microsoft\\Office\\15.0\\Outlook\\Search' + values = [] + + values.append(winreg_test_lib.TestRegValue( + ('C:\\Users\\username\\AppData\\Local\\Microsoft\\Outlook\\' + 'username@example.com.ost'), '\xcf\x2b\x37\x00', + winreg_test_lib.TestRegValue.REG_DWORD, offset=1892)) + + winreg_key = winreg_test_lib.TestRegKey( + key_path, 1346145829002031, values, 1456) + + event_queue_consumer = self._ParseKeyWithPlugin(self._plugin, winreg_key) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + expected_msg = ( + u'[{0:s}] ' + u'C:\\Users\\username\\AppData\\Local\\Microsoft\\Outlook\\' + u'username@example.com.ost: 0x00372bcf').format(key_path) + + expected_msg_short = u'[{0:s}] C:\\Users\\username\\AppData\\Lo...'.format( + key_path) + + self.assertEquals(len(event_objects), 1) + + event_object = event_objects[0] + + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + self.assertEquals(event_object.timestamp, 1346145829002031) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +# TODO: The catalog for Office 2013 (15.0) contains binary values not +# dword values. Check if Office 2007 and 2010 have the same. Re-enable the +# plug-ins once confirmed and OutlookSearchMRUPlugin has been extended to +# handle the binary data or create a OutlookSearchCatalogMRUPlugin. + +# class MSOutlook2013SearchCatalogMRUPluginTest(unittest.TestCase): +# """Tests for the Outlook Search Catalog MRU Windows Registry plugin.""" +# +# def setUp(self): +# """Sets up the needed objects used throughout the test.""" +# self._plugin = outlook.MSOutlook2013SearchCatalogMRUPlugin() +# +# def testProcess(self): +# """Tests the Process function.""" +# key_path = ( +# u'\\Software\\Microsoft\\Office\\15.0\\Outlook\\Search\\Catalog') +# values = [] +# +# values.append(winreg_test_lib.TestRegValue( +# ('C:\\Users\\username\\AppData\\Local\\Microsoft\\Outlook\\' +# 'username@example.com.ost'), '\x94\x01\x00\x00\x00\x00', +# winreg_test_lib.TestRegValue.REG_BINARY, offset=827)) +# +# winreg_key = winreg_test_lib.TestRegKey( +# key_path, 1346145829002031, values, 3421) +# +# # TODO: add test for Catalog key. + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/run.py b/plaso/parsers/winreg_plugins/run.py new file mode 100644 index 0000000..3b3391a --- /dev/null +++ b/plaso/parsers/winreg_plugins/run.py @@ -0,0 +1,90 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the Run/RunOnce Key plugins for Plaso.""" + +from plaso.events import windows_events +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface + + +class RunUserPlugin(interface.KeyPlugin): + """Windows Registry plugin for parsing user specific auto runs.""" + + NAME = 'winreg_run' + DESCRIPTION = u'Parser for run and run once Registry data.' + + REG_TYPE = 'NTUSER' + + REG_KEYS = [ + u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Run', + u'\\Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce'] + + URLS = ['http://msdn.microsoft.com/en-us/library/aa376977(v=vs.85).aspx'] + + def GetEntries( + self, parser_context, key=None, registry_type=None, file_entry=None, + parser_chain=None, **unused_kwargs): + """Collect the Values under the Run Key and return an event for each one. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + for value in key.GetValues(): + # Ignore the default value. + if not value.name: + continue + + # Ignore any value that is empty or that does not contain a string. + if not value.data or not value.DataIsString(): + continue + + text_dict = {} + text_dict[value.name] = value.data + + event_object = windows_events.WindowsRegistryEvent( + key.last_written_timestamp, key.path, text_dict, offset=key.offset, + urls=self.URLS, registry_type=registry_type, + source_append=': Run Key') + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +class RunSoftwarePlugin(RunUserPlugin): + """Windows Registry plugin for parsing system wide auto runs.""" + + NAME = 'winreg_run_software' + + REG_TYPE = 'SOFTWARE' + + REG_KEYS = [ + u'\\Microsoft\\Windows\\CurrentVersion\\Run', + u'\\Microsoft\\Windows\\CurrentVersion\\RunOnce', + u'\\Microsoft\\Windows\\CurrentVersion\\RunOnce\\Setup', + u'\\Microsoft\\Windows\\CurrentVersion\\RunServices', + u'\\Microsoft\\Windows\\CurrentVersion\\RunServicesOnce'] + + +winreg.WinRegistryParser.RegisterPlugins([ + RunUserPlugin, RunSoftwarePlugin]) diff --git a/plaso/parsers/winreg_plugins/run_test.py b/plaso/parsers/winreg_plugins/run_test.py new file mode 100644 index 0000000..ac91f1d --- /dev/null +++ b/plaso/parsers/winreg_plugins/run_test.py @@ -0,0 +1,179 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Run Windows Registry plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.parsers.winreg_plugins import run +from plaso.parsers.winreg_plugins import test_lib + + +class RunNtuserPlugintest(test_lib.RegistryPluginTestCase): + """Tests for the Run Windows Registry plugin on the User hive.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = run.RunUserPlugin() + + def testProcess(self): + """Tests the Process function.""" + test_file_entry = self._GetTestFileEntryFromPath(['NTUSER-RunTests.DAT']) + key_path = u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' + winreg_key = self._GetKeyFromFileEntry(test_file_entry, key_path) + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_key, file_entry=test_file_entry) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 1) + + event_object = event_objects[0] + + self.assertEquals(event_object.pathspec, test_file_entry.path_spec) + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + # Timestamp is: 2012-04-05T17:03:53.992061+00:00 + self.assertEquals(event_object.timestamp, 1333645433992061) + + expected_msg = ( + u'[{0:s}] Sidebar: %ProgramFiles%\\Windows Sidebar\\Sidebar.exe ' + u'/autoRun').format(key_path) + expected_msg_short = ( + u'[{0:s}] Sidebar: %ProgramFiles%\\Wind...').format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +class RunOnceNtuserPlugintest(test_lib.RegistryPluginTestCase): + """Tests for the RunOnce Windows Registry plugin on the User hive.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = run.RunUserPlugin() + + def testProcess(self): + """Tests the Process function.""" + test_file_entry = self._GetTestFileEntryFromPath(['NTUSER-RunTests.DAT']) + key_path = u'\\Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce' + winreg_key = self._GetKeyFromFileEntry(test_file_entry, key_path) + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_key, file_entry=test_file_entry) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 1) + + event_object = event_objects[0] + + self.assertEquals(event_object.pathspec, test_file_entry.path_spec) + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + # Timestamp is: 2012-04-05T17:03:53.992061+00:00 + self.assertEquals(event_object.timestamp, 1333645433992061) + + expected_msg = ( + u'[{0:s}] mctadmin: C:\\Windows\\System32\\mctadmin.exe').format( + key_path) + expected_msg_short = ( + u'[{0:s}] mctadmin: C:\\Windows\\Sys...').format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +class RunSoftwarePluginTest(test_lib.RegistryPluginTestCase): + """Tests for the Run Windows Registry plugin on the Software hive.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = run.RunSoftwarePlugin() + + def testProcess(self): + """Tests the Process function.""" + test_file_entry = self._GetTestFileEntryFromPath(['SOFTWARE-RunTests']) + key_path = u'\\Microsoft\\Windows\\CurrentVersion\\Run' + winreg_key = self._GetKeyFromFileEntry(test_file_entry, key_path) + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_key, file_entry=test_file_entry) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 3) + + event_object = event_objects[0] + + self.assertEquals(event_object.pathspec, test_file_entry.path_spec) + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + # Timestamp is: 2011-09-16T20:57:09.067575+00:00 + self.assertEquals(event_object.timestamp, 1316206629067575) + + expected_msg = ( + u'[{0:s}] VMware Tools: \"C:\\Program Files\\VMware\\VMware Tools' + u'\\VMwareTray.exe\"').format(key_path) + expected_msg_short = ( + u'[{0:s}] VMware Tools: \"C:\\Program Files\\VMwar...').format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + self.assertEquals(event_objects[1].timestamp, 1316206629067575) + + +class RunOnceSoftwarePluginTest(test_lib.RegistryPluginTestCase): + """Tests for the RunOnce Windows Registry plugin on the Software hive.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = run.RunSoftwarePlugin() + + def testProcess(self): + """Tests the Process function.""" + test_file_entry = self._GetTestFileEntryFromPath(['SOFTWARE-RunTests']) + key_path = u'\\Microsoft\\Windows\\CurrentVersion\\RunOnce' + winreg_key = self._GetKeyFromFileEntry(test_file_entry, key_path) + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_key, file_entry=test_file_entry) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 1) + + event_object = event_objects[0] + + self.assertEquals(event_object.pathspec, test_file_entry.path_spec) + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + # Timestamp is: 2012-04-06T14:07:27.750000+00:00 + self.assertEquals(event_object.timestamp, 1333721247750000) + + expected_msg = ( + u'[{0:s}] *WerKernelReporting: %SYSTEMROOT%\\SYSTEM32\\WerFault.exe ' + u'-k -rq').format(key_path) + expected_msg_short = ( + u'[{0:s}] *WerKernelReporting: %SYSTEMROOT%...').format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/sam_users.py b/plaso/parsers/winreg_plugins/sam_users.py new file mode 100644 index 0000000..60a9d3d --- /dev/null +++ b/plaso/parsers/winreg_plugins/sam_users.py @@ -0,0 +1,191 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors.# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the SAM Users & Names key plugin.""" + +import construct +import logging +from plaso.events import windows_events +from plaso.lib import binary +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface + + +__author__ = 'Preston Miller, dpmforensics.com, github.com/prmiller91' + + +class UsersPlugin(interface.KeyPlugin): + """SAM Windows Registry plugin for Users Account information.""" + + NAME = 'winreg_sam_users' + DESCRIPTION = u'Parser for SAM Users and Names Registry keys.' + + REG_KEYS = [u'\\SAM\\Domains\\Account\\Users'] + REG_TYPE = 'SAM' + F_VALUE_STRUCT = construct.Struct( + 'f_struct', construct.Padding(8), construct.ULInt64('last_login'), + construct.Padding(8), construct.ULInt64('password_reset'), + construct.Padding(16), construct.ULInt16('rid'), construct.Padding(16), + construct.ULInt8('login_count')) + V_VALUE_HEADER = construct.Struct( + 'v_header', construct.Array(11, construct.ULInt32('values'))) + V_VALUE_HEADER_SIZE = 0xCC + + def GetEntries( + self, parser_context, key=None, registry_type=None, file_entry=None, + parser_chain=None, **unused_kwargs): + """Collect data from Users and Names and produce event objects. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + + name_dict = {} + + name_key = key.GetSubkey('Names') + if not name_key: + logging.error(u'Unable to locate Names key.') + return + values = [(v.name, v.last_written_timestamp) for v in name_key.GetSubkeys()] + name_dict = dict(values) + + for subkey in key.GetSubkeys(): + text_dict = {} + if subkey.name == 'Names': + continue + text_dict['user_guid'] = subkey.name + parsed_v_value = self._ParseVValue(subkey) + if not parsed_v_value: + logging.error(u'V Value was not succesfully parsed by ParseVValue.') + return + username = parsed_v_value[0] + full_name = parsed_v_value[1] + comments = parsed_v_value[2] + if username: + text_dict['username'] = username + if full_name: + text_dict['full_name'] = full_name + if comments: + text_dict['comments'] = comments + if name_dict: + account_create_time = name_dict.get(text_dict.get('username'), 0) + else: + account_create_time = 0 + + f_data = self._ParseFValue(subkey) + last_login_time = timelib.Timestamp.FromFiletime(f_data.last_login) + password_reset_time = timelib.Timestamp.FromFiletime( + f_data.password_reset) + text_dict['account_rid'] = f_data.rid + text_dict['login_count'] = f_data.login_count + + if account_create_time > 0: + event_object = windows_events.WindowsRegistryEvent( + account_create_time, key.path, text_dict, + usage=eventdata.EventTimestamp.ACCOUNT_CREATED, + offset=key.offset, registry_type=registry_type, + source_append=u'User Account Information') + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if last_login_time > 0: + event_object = windows_events.WindowsRegistryEvent( + last_login_time, key.path, text_dict, + usage=eventdata.EventTimestamp.LAST_LOGIN_TIME, + offset=key.offset, + registry_type=registry_type, + source_append=u'User Account Information') + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if password_reset_time > 0: + event_object = windows_events.WindowsRegistryEvent( + password_reset_time, key.path, text_dict, + usage=eventdata.EventTimestamp.LAST_PASSWORD_RESET, + offset=key.offset, registry_type=registry_type, + source_append=u'User Account Information') + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + def _ParseVValue(self, key): + """Parses V value and returns name, fullname, and comments data. + + Args: + key: Registry key (instance of winreg.WinRegKey). + + Returns: + name: Name data parsed with name start and length values. + fullname: Fullname data parsed with fullname start and length values. + comments: Comments data parsed with comments start and length values. + """ + + v_value = key.GetValue('V') + if not v_value: + logging.error(u'Unable to locate V Value in key.') + return + try: + structure = self.V_VALUE_HEADER.parse(v_value.data) + except construct.FieldError as exception: + logging.error( + u'Unable to extract V value header data: {:s}'.format(exception)) + return + name_offset = structure.values()[0][3] + self.V_VALUE_HEADER_SIZE + full_name_offset = structure.values()[0][6] + self.V_VALUE_HEADER_SIZE + comments_offset = structure.values()[0][9] + self.V_VALUE_HEADER_SIZE + name_raw = v_value.data[ + name_offset:name_offset + structure.values()[0][4]] + full_name_raw = v_value.data[ + full_name_offset:full_name_offset + structure.values()[0][7]] + comments_raw = v_value.data[ + comments_offset:comments_offset + structure.values()[0][10]] + name = binary.ReadUtf16(name_raw) + full_name = binary.ReadUtf16(full_name_raw) + comments = binary.ReadUtf16(comments_raw) + return name, full_name, comments + + def _ParseFValue(self, key): + """Parses F value and returns parsed F data construct object. + + Args: + key: Registry key (instance of winreg.WinRegKey). + + Returns: + f_data: Construct parsed F value containing rid, login count, + and timestamp information. + """ + f_value = key.GetValue('F') + if not f_value: + logging.error(u'Unable to locate F Value in key.') + return + try: + f_data = self.F_VALUE_STRUCT.parse(f_value.data) + except construct.FieldError as exception: + logging.error( + u'Unable to extract F value data: {:s}'.format(exception)) + return + return f_data + + +winreg.WinRegistryParser.RegisterPlugin(UsersPlugin) diff --git a/plaso/parsers/winreg_plugins/sam_users_test.py b/plaso/parsers/winreg_plugins/sam_users_test.py new file mode 100644 index 0000000..25810c2 --- /dev/null +++ b/plaso/parsers/winreg_plugins/sam_users_test.py @@ -0,0 +1,80 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Users key plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.lib import event +from plaso.lib import timelib_test +from plaso.parsers.winreg_plugins import test_lib +from plaso.parsers.winreg_plugins import sam_users + + +__author__ = 'Preston Miller, dpmforensics.com, github.com/prmiller91' + + +class UsersPluginTest(test_lib.RegistryPluginTestCase): + """Tests for the SAM Users key plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = sam_users.UsersPlugin() + + def testProcess(self): + """Tests the Process function.""" + test_file = self._GetTestFilePath(['SAM']) + key_path = u'\\SAM\\Domains\\Account\\Users' + winreg_key = self._GetKeyFromFile(test_file, key_path) + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_key) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 7) + + event_object = event_objects[0] + + self._TestRegvalue(event_object, u'account_rid', 500) + self._TestRegvalue(event_object, u'login_count', 6) + self._TestRegvalue(event_object, u'user_guid', u'000001F4') + self._TestRegvalue(event_object, u'username', u'Administrator') + + expected_msg = ( + u'[\\SAM\\Domains\\Account\\Users] ' + u'account_rid: 500 ' + u'comments: Built-in account for administering the computer/domain ' + u'login_count: 6 ' + u'user_guid: 000001F4 ' + u'username: Administrator') + + # Match UTC timestamp. + time = long(timelib_test.CopyStringToTimestamp( + u'2014-09-24 03:36:06.358837')) + self.assertEquals(event_object.timestamp, time) + + expected_msg_short = ( + u'[\\SAM\\Domains\\Account\\Users] ' + u'account_rid: 500 ' + u'comments: Built-in account for ...') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/services.py b/plaso/parsers/winreg_plugins/services.py new file mode 100644 index 0000000..975df8f --- /dev/null +++ b/plaso/parsers/winreg_plugins/services.py @@ -0,0 +1,98 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Plug-in to format the Services and Drivers key with Start and Type values.""" + +from plaso.events import windows_events +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface + + +class ServicesPlugin(interface.ValuePlugin): + """Plug-in to format the Services and Drivers keys having Type and Start.""" + + NAME = 'winreg_services' + DESCRIPTION = u'Parser for services and drivers Registry data.' + + REG_VALUES = frozenset(['Type', 'Start']) + REG_TYPE = 'SYSTEM' + URLS = ['http://support.microsoft.com/kb/103000'] + + + def GetServiceDll(self, key): + """Get the Service DLL for a service, if it exists. + + Checks for a ServiceDLL for in the Parameters subkey of a service key in + the Registry. + + Args: + key: A Windows Registry key (instance of WinRegKey). + """ + parameters_key = key.GetSubkey('Parameters') + if parameters_key: + service_dll = parameters_key.GetValue('ServiceDll') + if service_dll: + return service_dll.data + else: + return None + + def GetEntries( + self, parser_context, key=None, registry_type=None, file_entry=None, + parser_chain=None, **unused_kwargs): + """Create one event for each subkey under Services that has Type and Start. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + text_dict = {} + + service_type_value = key.GetValue('Type') + service_start_value = key.GetValue('Start') + + # Grab the ServiceDLL value if it exists. + if service_type_value and service_start_value: + service_dll = self.GetServiceDll(key) + if service_dll: + text_dict['ServiceDll'] = service_dll + + # Gather all the other string and integer values and insert as they are. + for value in key.GetValues(): + if not value.name: + continue + if value.name not in text_dict: + if value.DataIsString() or value.DataIsInteger(): + text_dict[value.name] = value.data + elif value.DataIsMultiString(): + text_dict[value.name] = u', '.join(value.data) + + # Create a specific service event, so that we can recognize and expand + # certain values when we're outputting the event. + event_object = windows_events.WindowsRegistryServiceEvent( + key.last_written_timestamp, key.path, text_dict, offset=key.offset, + registry_type=registry_type, urls=self.URLS) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +winreg.WinRegistryParser.RegisterPlugin(ServicesPlugin) diff --git a/plaso/parsers/winreg_plugins/services_test.py b/plaso/parsers/winreg_plugins/services_test.py new file mode 100644 index 0000000..202772f --- /dev/null +++ b/plaso/parsers/winreg_plugins/services_test.py @@ -0,0 +1,170 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains tests for Services Windows Registry plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.lib import timelib_test +from plaso.parsers.winreg_plugins import services +from plaso.parsers.winreg_plugins import test_lib +from plaso.winreg import test_lib as winreg_test_lib + + +class ServicesRegistryPluginTest(test_lib.RegistryPluginTestCase): + """The unit test for Services Windows Registry plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = services.ServicesPlugin() + + def testProcess(self): + """Tests the Process function on a virtual key.""" + key_path = u'\\ControlSet001\\services\\TestDriver' + + values = [] + values.append(winreg_test_lib.TestRegValue( + 'Type', '\x02\x00\x00\x00', 4, 123)) + values.append(winreg_test_lib.TestRegValue( + 'Start', '\x02\x00\x00\x00', 4, 127)) + values.append(winreg_test_lib.TestRegValue( + 'ErrorControl', '\x01\x00\x00\x00', 4, 131)) + values.append(winreg_test_lib.TestRegValue( + 'Group', 'Pnp Filter'.encode('utf_16_le'), 1, 140)) + values.append(winreg_test_lib.TestRegValue( + 'DisplayName', 'Test Driver'.encode('utf_16_le'), 1, 160)) + values.append(winreg_test_lib.TestRegValue( + 'DriverPackageId', + 'testdriver.inf_x86_neutral_dd39b6b0a45226c4'.encode('utf_16_le'), 1, + 180)) + values.append(winreg_test_lib.TestRegValue( + 'ImagePath', 'C:\\Dell\\testdriver.sys'.encode('utf_16_le'), 1, 200)) + + timestamp = timelib_test.CopyStringToTimestamp( + '2012-08-28 09:23:49.002031') + winreg_key = winreg_test_lib.TestRegKey( + key_path, timestamp, values, 1456) + + event_queue_consumer = self._ParseKeyWithPlugin(self._plugin, winreg_key) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 1) + + event_object = event_objects[0] + + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-08-28 09:23:49.002031') + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = ( + u'[{0:s}] ' + u'DisplayName: Test Driver ' + u'DriverPackageId: testdriver.inf_x86_neutral_dd39b6b0a45226c4 ' + u'ErrorControl: Normal (1) ' + u'Group: Pnp Filter ' + u'ImagePath: C:\\Dell\\testdriver.sys ' + u'Start: Auto Start (2) ' + u'Type: File System Driver (0x2)').format(key_path) + expected_msg_short = ( + u'[{0:s}] ' + u'DisplayName: Test Driver ' + u'DriverPackageId...').format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + def testProcessFile(self): + """Tests the Process function on a key in a file.""" + test_file_entry = self._GetTestFileEntryFromPath(['SYSTEM']) + key_path = u'\\ControlSet001\\services' + winreg_key = self._GetKeyFromFileEntry(test_file_entry, key_path) + + event_objects = [] + + # Select a few service subkeys to perform additional testing. + bits_event_objects = None + mc_task_manager_event_objects = None + rdp_video_miniport_event_objects = None + + for winreg_subkey in winreg_key.GetSubkeys(): + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_subkey, file_entry=test_file_entry) + sub_event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + event_objects.extend(sub_event_objects) + + if winreg_subkey.name == 'BITS': + bits_event_objects = sub_event_objects + elif winreg_subkey.name == 'McTaskManager': + mc_task_manager_event_objects = sub_event_objects + elif winreg_subkey.name == 'RdpVideoMiniport': + rdp_video_miniport_event_objects = sub_event_objects + + self.assertEquals(len(event_objects), 416) + + # Test the BITS subkey event objects. + self.assertEquals(len(bits_event_objects), 1) + + event_object = bits_event_objects[0] + + self.assertEquals(event_object.pathspec, test_file_entry.path_spec) + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-04-06 20:43:27.639075') + self.assertEquals(event_object.timestamp, expected_timestamp) + + self._TestRegvalue(event_object, u'Type', 0x20) + self._TestRegvalue(event_object, u'Start', 3) + self._TestRegvalue( + event_object, u'ServiceDll', u'%SystemRoot%\\System32\\qmgr.dll') + + # Test the McTaskManager subkey event objects. + self.assertEquals(len(mc_task_manager_event_objects), 1) + + event_object = mc_task_manager_event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-09-16 20:49:16.877415') + self.assertEquals(event_object.timestamp, expected_timestamp) + + self._TestRegvalue(event_object, u'DisplayName', u'McAfee Task Manager') + self._TestRegvalue(event_object, u'Type', 0x10) + + # Test the RdpVideoMiniport subkey event objects. + self.assertEquals(len(rdp_video_miniport_event_objects), 1) + + event_object = rdp_video_miniport_event_objects[0] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-09-17 13:37:59.347157') + self.assertEquals(event_object.timestamp, expected_timestamp) + + self._TestRegvalue(event_object, u'Start', 3) + expected_value = u'System32\\drivers\\rdpvideominiport.sys' + self._TestRegvalue(event_object, u'ImagePath', expected_value) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/shutdown.py b/plaso/parsers/winreg_plugins/shutdown.py new file mode 100644 index 0000000..26ce50e --- /dev/null +++ b/plaso/parsers/winreg_plugins/shutdown.py @@ -0,0 +1,78 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors.# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the LastShutdown value plugin.""" + +import construct +import logging +from plaso.events import windows_events +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface + + +__author__ = 'Preston Miller, dpmforensics.com, github.com/prmiller91' + + +class ShutdownPlugin(interface.KeyPlugin): + """Windows Registry plugin for parsing the last shutdown time of a system.""" + + NAME = 'winreg_shutdown' + DESCRIPTION = u'Parser for ShutdownTime Registry value.' + + REG_KEYS = [u'\\{current_control_set}\\Control\\Windows'] + REG_TYPE = 'SYSTEM' + FILETIME_STRUCT = construct.ULInt64('filetime_timestamp') + + def GetEntries( + self, parser_context, key=None, registry_type=None, file_entry=None, + parser_chain=None, **unused_kwargs): + """Collect ShutdownTime value under Windows and produce an event object. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + shutdown_value = key.GetValue('ShutdownTime') + if not shutdown_value: + return + text_dict = {} + text_dict['Description'] = shutdown_value.name + try: + filetime = self.FILETIME_STRUCT.parse(shutdown_value.data) + except construct.FieldError as exception: + logging.error( + u'Unable to extract shutdown timestamp: {0:s}'.format(exception)) + return + timestamp = timelib.Timestamp.FromFiletime(filetime) + + event_object = windows_events.WindowsRegistryEvent( + timestamp, key.path, text_dict, + usage=eventdata.EventTimestamp.LAST_SHUTDOWN, offset=key.offset, + registry_type=registry_type, + source_append=u'Shutdown Entry') + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +winreg.WinRegistryParser.RegisterPlugin(ShutdownPlugin) diff --git a/plaso/parsers/winreg_plugins/shutdown_test.py b/plaso/parsers/winreg_plugins/shutdown_test.py new file mode 100644 index 0000000..21a1c21 --- /dev/null +++ b/plaso/parsers/winreg_plugins/shutdown_test.py @@ -0,0 +1,79 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the LastShutdown value plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.lib import timelib_test +from plaso.parsers.winreg_plugins import test_lib +from plaso.parsers.winreg_plugins import shutdown + + +__author__ = 'Preston Miller, dpmforensics.com, github.com/prmiller91' + + +class ShutdownPluginTest(test_lib.RegistryPluginTestCase): + """Tests for the LastShutdown value plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = shutdown.ShutdownPlugin() + + def testProcess(self): + """Tests the Process function.""" + knowledge_base_values = {'current_control_set': u'ControlSet001'} + test_file_entry = self._GetTestFileEntryFromPath(['SYSTEM']) + key_path = u'\\ControlSet001\\Control\\Windows' + winreg_key = self._GetKeyFromFileEntry(test_file_entry, key_path) + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_key, knowledge_base_values=knowledge_base_values, + file_entry=test_file_entry) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 1) + + event_object = event_objects[0] + + self.assertEquals(event_object.pathspec, test_file_entry.path_spec) + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_value = u'ShutdownTime' + self._TestRegvalue(event_object, u'Description', expected_value) + + expected_msg = ( + u'[\\ControlSet001\\Control\\Windows] ' + u'Description: ShutdownTime') + + # Match UTC timestamp. + time = long(timelib_test.CopyStringToTimestamp( + u'2012-04-04 01:58:40.839249')) + self.assertEquals(event_object.timestamp, time) + + expected_msg_short = ( + u'[\\ControlSet001\\Control\\Windows] ' + u'Description: ShutdownTime') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/task_scheduler.py b/plaso/parsers/winreg_plugins/task_scheduler.py new file mode 100644 index 0000000..4506e2f --- /dev/null +++ b/plaso/parsers/winreg_plugins/task_scheduler.py @@ -0,0 +1,169 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the Task Scheduler Registry keys plugins.""" + +import logging + +import construct + +from plaso.events import windows_events +from plaso.events import time_events +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface + + +class TaskCacheEvent(time_events.FiletimeEvent): + """Convenience class for a Task Cache event.""" + + DATA_TYPE = 'task_scheduler:task_cache:entry' + + def __init__( + self, timestamp, timestamp_description, task_name, task_identifier): + """Initializes the event. + + Args: + timestamp: The FILETIME value for the timestamp. + timestamp_description: The usage string for the timestamp value. + task_name: String containing the name of the task. + task_identifier: String containing the identifier of the task. + """ + super(TaskCacheEvent, self).__init__(timestamp, timestamp_description) + + self.offset = 0 + self.task_name = task_name + self.task_identifier = task_identifier + + +class TaskCachePlugin(interface.KeyPlugin): + """Plugin that parses a Task Cache key.""" + + NAME = 'winreg_task_cache' + DESCRIPTION = u'Parser for Task Scheduler cache Registry data.' + + REG_TYPE = 'SOFTWARE' + REG_KEYS = [ + u'\\Microsoft\\Windows NT\\CurrentVersion\\Schedule\\TaskCache'] + + URL = [ + u'https://code.google.com/p/winreg-kb/wiki/TaskSchedulerKeys'] + + _DYNAMIC_INFO_STRUCT = construct.Struct( + 'dynamic_info_record', + construct.ULInt32('version'), + construct.ULInt64('last_registered_time'), + construct.ULInt64('launch_time'), + construct.Padding(8)) + + _DYNAMIC_INFO_STRUCT_SIZE = _DYNAMIC_INFO_STRUCT.sizeof() + + def _GetIdValue(self, key): + """Retrieves the Id value from Task Cache Tree key. + + Args: + key: A Windows Registry key (instance of WinRegKey). + + Yields: + A tuple containing a Windows Registry Key (instance of WinRegKey) and + a Windows Registry value (instance of WinRegValue). + """ + id_value = key.GetValue(u'Id') + if id_value: + yield key, id_value + + for sub_key in key.GetSubkeys(): + for value_key, id_value in self._GetIdValue(sub_key): + yield value_key, id_value + + def GetEntries( + self, parser_context, key=None, registry_type=None, file_entry=None, + parser_chain=None, **unused_kwargs): + """Parses a Task Cache Registry key. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + tasks_key = key.GetSubkey(u'Tasks') + tree_key = key.GetSubkey(u'Tree') + + if not tasks_key or not tree_key: + logging.warning(u'Task Cache is missing a Tasks or Tree sub key.') + return + + task_guids = {} + for sub_key in tree_key.GetSubkeys(): + for value_key, id_value in self._GetIdValue(sub_key): + # The GUID is in the form {%GUID%} and stored an UTF-16 little-endian + # string and should be 78 bytes in size. + if len(id_value.raw_data) != 78: + logging.warning( + u'[{0:s}] unsupported Id value data size.'.format(self.NAME)) + continue + task_guids[id_value.data] = value_key.name + + for sub_key in tasks_key.GetSubkeys(): + dynamic_info_value = sub_key.GetValue(u'DynamicInfo') + if not dynamic_info_value: + continue + + if len(dynamic_info_value.raw_data) != self._DYNAMIC_INFO_STRUCT_SIZE: + logging.warning( + u'[{0:s}] unsupported DynamicInfo value data size.'.format( + self.NAME)) + continue + + dynamic_info = self._DYNAMIC_INFO_STRUCT.parse( + dynamic_info_value.raw_data) + + name = task_guids.get(sub_key.name, sub_key.name) + + text_dict = {} + text_dict[u'Task: {0:s}'.format(name)] = u'[ID: {0:s}]'.format( + sub_key.name) + event_object = windows_events.WindowsRegistryEvent( + key.last_written_timestamp, key.path, text_dict, offset=key.offset, + registry_type=registry_type) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if dynamic_info.last_registered_time: + # Note this is likely either the last registered time or + # the update time. + event_object = TaskCacheEvent( + dynamic_info.last_registered_time, u'Last registered time', name, + sub_key.name) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + if dynamic_info.launch_time: + # Note this is likely the launch time. + event_object = TaskCacheEvent( + dynamic_info.launch_time, u'Launch time', name, sub_key.name) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + # TODO: Add support for the Triggers value. + + +winreg.WinRegistryParser.RegisterPlugin(TaskCachePlugin) diff --git a/plaso/parsers/winreg_plugins/task_scheduler_test.py b/plaso/parsers/winreg_plugins/task_scheduler_test.py new file mode 100644 index 0000000..e109822 --- /dev/null +++ b/plaso/parsers/winreg_plugins/task_scheduler_test.py @@ -0,0 +1,87 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Task Scheduler Windows Registry plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.lib import timelib_test +from plaso.parsers.winreg_plugins import task_scheduler +from plaso.parsers.winreg_plugins import test_lib + + +class TaskCachePluginTest(test_lib.RegistryPluginTestCase): + """Tests for the Task Cache key Windows Registry plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = task_scheduler.TaskCachePlugin() + + def testProcess(self): + """Tests the Process function.""" + test_file = self._GetTestFilePath(['SOFTWARE-RunTests']) + key_path = ( + u'\\Microsoft\\Windows NT\\CurrentVersion\\Schedule\\TaskCache') + winreg_key = self._GetKeyFromFile(test_file, key_path) + event_queue_consumer = self._ParseKeyWithPlugin(self._plugin, winreg_key) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 174) + + event_object = event_objects[0] + + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2009-07-14 04:53:25.811618') + self.assertEquals(event_object.timestamp, expected_timestamp) + + regvalue_identifier = u'Task: SynchronizeTime' + expected_value = u'[ID: {044A6734-E90E-4F8F-B357-B2DC8AB3B5EC}]' + self._TestRegvalue(event_object, regvalue_identifier, expected_value) + + expected_msg = u'[{0:s}] {1:s}: {2:s}'.format( + key_path, regvalue_identifier, expected_value) + + expected_msg_short = u'[{0:s}] Task: SynchronizeTi...'.format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + event_object = event_objects[1] + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2009-07-14 05:08:50.811626') + self.assertEquals(event_object.timestamp, expected_timestamp) + + regvalue_identifier = u'Task: SynchronizeTime' + + expected_msg = ( + u'Task: SynchronizeTime ' + u'[Identifier: {044A6734-E90E-4F8F-B357-B2DC8AB3B5EC}]') + + expected_msg_short = ( + u'Task: SynchronizeTime') + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/terminal_server.py b/plaso/parsers/winreg_plugins/terminal_server.py new file mode 100644 index 0000000..66afdb6 --- /dev/null +++ b/plaso/parsers/winreg_plugins/terminal_server.py @@ -0,0 +1,127 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the Terminal Server Registry plugins.""" + +from plaso.events import windows_events +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface + + +__author__ = 'David Nides (david.nides@gmail.com)' + + +class TerminalServerClientPlugin(interface.KeyPlugin): + """Windows Registry plugin for Terminal Server Client Connection keys.""" + + NAME = 'winreg_rdp' + DESCRIPTION = u'Parser for Terminal Server Client Connection Registry data.' + + REG_TYPE = 'NTUSER' + REG_KEYS = [ + u'\\Software\\Microsoft\\Terminal Server Client\\Servers', + u'\\Software\\Microsoft\\Terminal Server Client\\Default\\AddIns\\RDPDR'] + + def GetEntries( + self, parser_context, key=None, registry_type=None, file_entry=None, + parser_chain=None, **unused_kwargs): + """Collect Values in Servers and return event for each one. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + for subkey in key.GetSubkeys(): + username_value = subkey.GetValue('UsernameHint') + + if (username_value and username_value.data and + username_value.DataIsString()): + username = username_value.data + else: + username = u'None' + + text_dict = {} + text_dict['UsernameHint'] = username + + event_object = windows_events.WindowsRegistryEvent( + key.last_written_timestamp, key.path, text_dict, offset=key.offset, + registry_type=registry_type, + source_append=': RDP Connection') + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +class TerminalServerClientMRUPlugin(interface.KeyPlugin): + """Windows Registry plugin for Terminal Server Client Connection MRUs keys.""" + + NAME = 'winreg_rdp_mru' + DESCRIPTION = u'Parser for Terminal Server Client MRU Registry data.' + + REG_TYPE = 'NTUSER' + REG_KEYS = [ + u'\\Software\\Microsoft\\Terminal Server Client\\Default', + u'\\Software\\Microsoft\\Terminal Server Client\\LocalDevices'] + + def GetEntries( + self, parser_context, key=None, registry_type=None, file_entry=None, + parser_chain=None, **unused_kwargs): + """Collect MRU Values and return event for each one. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + """ + for value in key.GetValues(): + # TODO: add a check for the value naming scheme. + # Ignore the default value. + if not value.name: + continue + + # Ignore any value that is empty or that does not contain a string. + if not value.data or not value.DataIsString(): + continue + + text_dict = {} + text_dict[value.name] = value.data + + if value.name == 'MRU0': + timestamp = key.last_written_timestamp + else: + timestamp = 0 + + event_object = windows_events.WindowsRegistryEvent( + timestamp, key.path, text_dict, offset=key.offset, + registry_type=registry_type, + source_append=u': RDP Connection') + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +winreg.WinRegistryParser.RegisterPlugins([ + TerminalServerClientPlugin, TerminalServerClientMRUPlugin]) diff --git a/plaso/parsers/winreg_plugins/terminal_server_test.py b/plaso/parsers/winreg_plugins/terminal_server_test.py new file mode 100644 index 0000000..c70c72e --- /dev/null +++ b/plaso/parsers/winreg_plugins/terminal_server_test.py @@ -0,0 +1,130 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Terminal Server Windows Registry plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.lib import timelib_test +from plaso.parsers.winreg_plugins import terminal_server +from plaso.parsers.winreg_plugins import test_lib +from plaso.winreg import test_lib as winreg_test_lib + + +class ServersTerminalServerClientPluginTest(test_lib.RegistryPluginTestCase): + """Tests for the Terminal Server Client Windows Registry plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = terminal_server.TerminalServerClientPlugin() + + def testProcess(self): + """Tests the Process function.""" + key_path = u'\\Software\\Microsoft\\Terminal Server Client\\Servers' + values = [] + + values.append(winreg_test_lib.TestRegValue( + 'UsernameHint', 'DOMAIN\\username'.encode('utf_16_le'), + winreg_test_lib.TestRegValue.REG_SZ, offset=1892)) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-08-28 09:23:49.002031') + + server_key_path = ( + u'\\Software\\Microsoft\\Terminal Server Client\\Servers\\myserver.com') + server_key = winreg_test_lib.TestRegKey( + server_key_path, expected_timestamp, values, offset=1456) + + winreg_key = winreg_test_lib.TestRegKey( + key_path, expected_timestamp, None, offset=865, subkeys=[server_key]) + + event_queue_consumer = self._ParseKeyWithPlugin(self._plugin, winreg_key) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 1) + + event_object = event_objects[0] + + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = u'[{0:s}] UsernameHint: DOMAIN\\username'.format(key_path) + expected_msg_short = ( + u'[{0:s}] UsernameHint: DOMAIN\\use...').format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +class DefaultTerminalServerClientMRUPluginTest(test_lib.RegistryPluginTestCase): + """Tests for the Terminal Server Client MRU Windows Registry plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = terminal_server.TerminalServerClientMRUPlugin() + + def testProcess(self): + """Tests the Process function.""" + key_path = u'\\Software\\Microsoft\\Terminal Server Client\\Default' + values = [] + + values.append(winreg_test_lib.TestRegValue( + 'MRU0', '192.168.16.60'.encode('utf_16_le'), + winreg_test_lib.TestRegValue.REG_SZ, offset=1892)) + values.append(winreg_test_lib.TestRegValue( + 'MRU1', 'computer.domain.com'.encode('utf_16_le'), + winreg_test_lib.TestRegValue.REG_SZ, 612)) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-08-28 09:23:49.002031') + winreg_key = winreg_test_lib.TestRegKey( + key_path, expected_timestamp, values, 1456) + + event_queue_consumer = self._ParseKeyWithPlugin(self._plugin, winreg_key) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 2) + + event_object = event_objects[0] + + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_msg = u'[{0:s}] MRU0: 192.168.16.60'.format(key_path) + expected_msg_short = u'[{0:s}] MRU0: 192.168.16.60'.format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + event_object = event_objects[1] + + self.assertEquals(event_object.timestamp, 0) + + expected_msg = u'[{0:s}] MRU1: computer.domain.com'.format(key_path) + expected_msg_short = u'[{0:s}] MRU1: computer.domain.com'.format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/test_lib.py b/plaso/parsers/winreg_plugins/test_lib.py new file mode 100644 index 0000000..17dc9f7 --- /dev/null +++ b/plaso/parsers/winreg_plugins/test_lib.py @@ -0,0 +1,106 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Windows Registry plugin related functions and classes for testing.""" + +from dfvfs.lib import definitions +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver import resolver as path_spec_resolver + +from plaso.engine import single_process +from plaso.parsers import test_lib +from plaso.winreg import winregistry + + +class RegistryPluginTestCase(test_lib.ParserTestCase): + """The unit test case for a Windows Registry plugin.""" + + def _GetKeyFromFile(self, path, key_path): + """Retrieves a Windows Registry key from a file. + + Args: + path: The path to the file, as a string. + key_path: The path of the key to parse. + + Returns: + A Windows Registry key (instance of WinRegKey). + """ + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=path) + file_entry = path_spec_resolver.Resolver.OpenFileEntry(path_spec) + return self._GetKeyFromFileEntry(file_entry, key_path) + + def _GetKeyFromFileEntry(self, file_entry, key_path): + """Retrieves a Windows Registry key from a file. + + Args: + file_entry: A dfVFS file_entry object that references a test file. + key_path: The path of the key to parse. + + Returns: + A Windows Registry key (instance of WinRegKey). + """ + registry = winregistry.WinRegistry(winregistry.WinRegistry.BACKEND_PYREGF) + winreg_file = registry.OpenFile(file_entry, codepage='cp1252') + return winreg_file.GetKeyByPath(key_path) + + def _ParseKeyWithPlugin( + self, plugin_object, winreg_key, knowledge_base_values=None, + file_entry=None, parser_chain=None): + """Parses a key within a Windows Registry file using the plugin object. + + Args: + plugin_object: The plugin object. + winreg_key: The Windows Registry Key. + knowledge_base_values: Optional dict containing the knowledge base + values. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + + Returns: + An event object queue consumer object (instance of + TestEventObjectQueueConsumer). + """ + self.assertNotEquals(winreg_key, None) + + event_queue = single_process.SingleProcessQueue() + event_queue_consumer = test_lib.TestEventObjectQueueConsumer(event_queue) + + parse_error_queue = single_process.SingleProcessQueue() + + parser_context = self._GetParserContext( + event_queue, parse_error_queue, + knowledge_base_values=knowledge_base_values) + plugin_object.Process( + parser_context, key=winreg_key, parser_chain=parser_chain, + file_entry=file_entry) + + return event_queue_consumer + + def _TestRegvalue(self, event_object, identifier, expected_value): + """Tests a specific 'regvalue' attribute within the event object. + + Args: + event_object: the event object (instance of EventObject). + identifier: the identifier of the 'regvalue' attribute. + expected_value: the expected value of the 'regvalue' attribute. + """ + self.assertTrue(hasattr(event_object, 'regvalue')) + self.assertIn(identifier, event_object.regvalue) + self.assertEquals(event_object.regvalue[identifier], expected_value) diff --git a/plaso/parsers/winreg_plugins/typedurls.py b/plaso/parsers/winreg_plugins/typedurls.py new file mode 100644 index 0000000..dafd8bf --- /dev/null +++ b/plaso/parsers/winreg_plugins/typedurls.py @@ -0,0 +1,84 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors.# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the typed URLs plugins for Plaso.""" + +import re + +from plaso.events import windows_events +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface + + +__author__ = 'David Nides (david.nides@gmail.com)' + + +class TypedURLsPlugin(interface.KeyPlugin): + """A Windows Registry plugin for typed URLs history.""" + + NAME = 'winreg_typed_urls' + DESCRIPTION = u'Parser for Internet Explorer typed URLs Registry data.' + + REG_TYPE = 'NTUSER' + REG_KEYS = [ + u'\\Software\\Microsoft\\Internet Explorer\\TypedURLs', + u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\TypedPaths'] + + _RE_VALUE_NAME = re.compile(r'^url[0-9]+$', re.I) + + def GetEntries( + self, parser_context, key=None, registry_type=None, file_entry=None, + parser_chain=None, **unused_kwargs): + """Collect typed URLs values. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + registry_type: Optional Registry type string. The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + for value in key.GetValues(): + # Ignore any value not in the form: 'url[0-9]+'. + if not value.name or not self._RE_VALUE_NAME.search(value.name): + continue + + # Ignore any value that is empty or that does not contain a string. + if not value.data or not value.DataIsString(): + continue + + # TODO: shouldn't this behavior be, put all the typed urls + # into a single event object with the last written time of the key? + if value.name == 'url1': + timestamp = key.last_written_timestamp + else: + timestamp = 0 + + text_dict = {} + text_dict[value.name] = value.data + + event_object = windows_events.WindowsRegistryEvent( + timestamp, key.path, text_dict, offset=key.offset, + registry_type=registry_type, + source_append=u': Typed URLs') + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +winreg.WinRegistryParser.RegisterPlugin(TypedURLsPlugin) diff --git a/plaso/parsers/winreg_plugins/typedurls_test.py b/plaso/parsers/winreg_plugins/typedurls_test.py new file mode 100644 index 0000000..bfbefec --- /dev/null +++ b/plaso/parsers/winreg_plugins/typedurls_test.py @@ -0,0 +1,112 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the MSIE typed URLs Windows Registry plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.lib import timelib_test +from plaso.parsers.winreg_plugins import test_lib +from plaso.parsers.winreg_plugins import typedurls + + +__author__ = 'David Nides (david.nides@gmail.com)' + + +class MsieTypedURLsPluginTest(test_lib.RegistryPluginTestCase): + """Tests for the MSIE typed URLs Windows Registry plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = typedurls.TypedURLsPlugin() + + def testProcess(self): + """Tests the Process function.""" + test_file_entry = self._GetTestFileEntryFromPath(['NTUSER-WIN7.DAT']) + key_path = u'\\Software\\Microsoft\\Internet Explorer\\TypedURLs' + winreg_key = self._GetKeyFromFileEntry(test_file_entry, key_path) + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_key, file_entry=test_file_entry) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 13) + + event_object = event_objects[0] + + self.assertEquals(event_object.pathspec, test_file_entry.path_spec) + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-03-12 21:23:53.307749') + self.assertEquals(event_object.timestamp, expected_timestamp) + + regvalue_identifier = u'url1' + expected_value = u'http://cnn.com/' + self._TestRegvalue(event_object, regvalue_identifier, expected_value) + + expected_string = u'[{0:s}] {1:s}: {2:s}'.format( + key_path, regvalue_identifier, expected_value) + self._TestGetMessageStrings(event_object, expected_string, expected_string) + + +class TypedPathsPluginTest(test_lib.RegistryPluginTestCase): + """Tests for the typed paths Windows Registry plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = typedurls.TypedURLsPlugin() + + def testProcess(self): + """Tests the Process function.""" + test_file_entry = self._GetTestFileEntryFromPath(['NTUSER-WIN7.DAT']) + key_path = ( + u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\TypedPaths') + winreg_key = self._GetKeyFromFileEntry(test_file_entry, key_path) + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_key, file_entry=test_file_entry) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 1) + + event_object = event_objects[0] + + self.assertEquals(event_object.pathspec, test_file_entry.path_spec) + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2010-11-10 07:58:15.811625') + self.assertEquals(event_object.timestamp, expected_timestamp) + + regvalue_identifier = u'url1' + expected_value = u'\\\\controller' + self._TestRegvalue(event_object, regvalue_identifier, expected_value) + + expected_msg = u'[{0:s}] {1:s}: {2:s}'.format( + key_path, regvalue_identifier, expected_value) + expected_msg_short = u'[{0:s}] {1:s}: \\\\cont...'.format( + key_path, regvalue_identifier) + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/usb.py b/plaso/parsers/winreg_plugins/usb.py new file mode 100644 index 0000000..08c9a6b --- /dev/null +++ b/plaso/parsers/winreg_plugins/usb.py @@ -0,0 +1,87 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors.# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the USB key plugin.""" + +import logging + +from plaso.events import windows_events +from plaso.lib import eventdata +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface + + +__author__ = 'Preston Miller, dpmforensics.com, github.com/prmiller91' + + +class USBPlugin(interface.KeyPlugin): + """USB Windows Registry plugin for last connection time.""" + + NAME = 'winreg_usb' + DESCRIPTION = u'Parser for USB storage Registry data.' + + REG_KEYS = [u'\\{current_control_set}\\Enum\\USB'] + REG_TYPE = 'SYSTEM' + + def GetEntries( + self, parser_context, key=None, registry_type=None, file_entry=None, + parser_chain=None, **unused_kwargs): + """Collect SubKeys under USB and produce an event object for each one. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + for subkey in key.GetSubkeys(): + text_dict = {} + text_dict['subkey_name'] = subkey.name + + vendor_identification = None + product_identification = None + try: + subkey_name_parts = subkey.name.split(u'&') + if len(subkey_name_parts) >= 2: + vendor_identification = subkey_name_parts[0] + product_identification = subkey_name_parts[1] + except ValueError as exception: + logging.warning( + u'Unable to split string: {0:s} with error: {1:s}'.format( + subkey.name, exception)) + + if vendor_identification and product_identification: + text_dict['vendor'] = vendor_identification + text_dict['product'] = product_identification + + for devicekey in subkey.GetSubkeys(): + text_dict['serial'] = devicekey.name + + # Last USB connection per USB device recorded in the Registry. + event_object = windows_events.WindowsRegistryEvent( + devicekey.last_written_timestamp, key.path, text_dict, + usage=eventdata.EventTimestamp.LAST_CONNECTED, offset=key.offset, + registry_type=registry_type, + source_append=': USB Entries') + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +winreg.WinRegistryParser.RegisterPlugin(USBPlugin) diff --git a/plaso/parsers/winreg_plugins/usb_test.py b/plaso/parsers/winreg_plugins/usb_test.py new file mode 100644 index 0000000..0015c6a --- /dev/null +++ b/plaso/parsers/winreg_plugins/usb_test.py @@ -0,0 +1,80 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the USB Windows Registry plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.lib import event +from plaso.lib import timelib_test +from plaso.parsers.winreg_plugins import test_lib +from plaso.parsers.winreg_plugins import usb + + +__author__ = 'Preston Miller, dpmforensics.com, github.com/prmiller91' + + +class USBPluginTest(test_lib.RegistryPluginTestCase): + """Tests for the USB Windows Registry plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = usb.USBPlugin() + + def testProcess(self): + """Tests the Process function.""" + knowledge_base_values = {u'current_control_set': u'ControlSet001'} + test_file_entry = self._GetTestFileEntryFromPath([u'SYSTEM']) + key_path = u'\\ControlSet001\\Enum\\USB' + winreg_key = self._GetKeyFromFileEntry(test_file_entry, key_path) + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_key, knowledge_base_values=knowledge_base_values, + file_entry=test_file_entry) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 7) + + event_object = event_objects[3] + + self.assertEquals(event_object.pathspec, test_file_entry.path_spec) + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_value = u'VID_0E0F&PID_0002' + self._TestRegvalue(event_object, u'subkey_name', expected_value) + self._TestRegvalue(event_object, u'vendor', u'VID_0E0F') + self._TestRegvalue(event_object, u'product', u'PID_0002') + + expected_msg = ( + r'[\ControlSet001\Enum\USB] product: PID_0002 serial: 6&2ab01149&0&2 ' + r'subkey_name: VID_0E0F&PID_0002 vendor: VID_0E0F') + + # Match UTC timestamp. + time = long(timelib_test.CopyStringToTimestamp( + u'2012-04-07 10:31:37.625246')) + self.assertEquals(event_object.timestamp, time) + + expected_msg_short = u'{0:s}...'.format(expected_msg[0:77]) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/usbstor.py b/plaso/parsers/winreg_plugins/usbstor.py new file mode 100644 index 0000000..86f8f88 --- /dev/null +++ b/plaso/parsers/winreg_plugins/usbstor.py @@ -0,0 +1,133 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors.# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the USBStor keys plugins.""" + +import logging + +from plaso.events import windows_events +from plaso.lib import eventdata +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface + + +__author__ = 'David Nides (david.nides@gmail.com)' + + +class USBStorPlugin(interface.KeyPlugin): + """USBStor key plugin.""" + + NAME = 'winreg_usbstor' + DESCRIPTION = u'Parser for USB storage Registry data.' + + REG_KEYS = [u'\\{current_control_set}\\Enum\\USBSTOR'] + REG_TYPE = 'SYSTEM' + + def GetEntries( + self, parser_context, key=None, registry_type=None, file_entry=None, + parser_chain=None, **unused_kwargs): + """Collect Values under USBStor and return an event object for each one. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + for subkey in key.GetSubkeys(): + text_dict = {} + text_dict['subkey_name'] = subkey.name + + # Time last USB device of this class was first inserted. + event_object = windows_events.WindowsRegistryEvent( + subkey.last_written_timestamp, key.path, text_dict, + usage=eventdata.EventTimestamp.FIRST_CONNECTED, offset=key.offset, + registry_type=registry_type, + source_append=': USBStor Entries') + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + # TODO: Determine if these 4 fields always exist. + try: + device_type, vendor, product, revision = subkey.name.split('&') + except ValueError as exception: + logging.warning( + u'Unable to split string: {0:s} with error: {1:s}'.format( + subkey.name, exception)) + + text_dict['device_type'] = device_type + text_dict['vendor'] = vendor + text_dict['product'] = product + text_dict['revision'] = revision + + for devicekey in subkey.GetSubkeys(): + text_dict['serial'] = devicekey.name + + friendly_name_value = devicekey.GetValue('FriendlyName') + if friendly_name_value: + text_dict['friendly_name'] = friendly_name_value.data + else: + text_dict.pop('friendly_name', None) + + # ParentIdPrefix applies to Windows XP Only. + parent_id_prefix_value = devicekey.GetValue('ParentIdPrefix') + if parent_id_prefix_value: + text_dict['parent_id_prefix'] = parent_id_prefix_value.data + else: + text_dict.pop('parent_id_prefix', None) + + # Win7 - Last Connection. + # Vista/XP - Time of an insert. + event_object = windows_events.WindowsRegistryEvent( + devicekey.last_written_timestamp, key.path, text_dict, + usage=eventdata.EventTimestamp.LAST_CONNECTED, offset=key.offset, + registry_type=registry_type, + source_append=': USBStor Entries') + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + # Build list of first Insertion times. + first_insert = [] + device_parameter_key = devicekey.GetSubkey('Device Parameters') + if device_parameter_key: + first_insert.append(device_parameter_key.last_written_timestamp) + + log_configuration_key = devicekey.GetSubkey('LogConf') + if (log_configuration_key and + log_configuration_key.last_written_timestamp not in first_insert): + first_insert.append(log_configuration_key.last_written_timestamp) + + properties_key = devicekey.GetSubkey('Properties') + if (properties_key and + properties_key.last_written_timestamp not in first_insert): + first_insert.append(properties_key.last_written_timestamp) + + # Add first Insertion times. + for timestamp in first_insert: + event_object = windows_events.WindowsRegistryEvent( + timestamp, key.path, text_dict, + usage=eventdata.EventTimestamp.LAST_CONNECTED, offset=key.offset, + registry_type=registry_type, + source_append=': USBStor Entries') + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +winreg.WinRegistryParser.RegisterPlugin(USBStorPlugin) diff --git a/plaso/parsers/winreg_plugins/usbstor_test.py b/plaso/parsers/winreg_plugins/usbstor_test.py new file mode 100644 index 0000000..add1b76 --- /dev/null +++ b/plaso/parsers/winreg_plugins/usbstor_test.py @@ -0,0 +1,84 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the USBStor Windows Registry plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.parsers.winreg_plugins import test_lib +from plaso.parsers.winreg_plugins import usbstor + + +class USBStorPlugin(test_lib.RegistryPluginTestCase): + """Tests for the USBStor Windows Registry plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = usbstor.USBStorPlugin() + + def testProcess(self): + """Tests the Process function.""" + knowledge_base_values = {'current_control_set': u'ControlSet001'} + test_file_entry = self._GetTestFileEntryFromPath(['SYSTEM']) + key_path = u'\\ControlSet001\\Enum\\USBSTOR' + winreg_key = self._GetKeyFromFileEntry(test_file_entry, key_path) + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_key, knowledge_base_values=knowledge_base_values, + file_entry=test_file_entry) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 3) + + event_object = event_objects[0] + + self.assertEquals(event_object.pathspec, test_file_entry.path_spec) + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + self.assertEquals(event_object.timestamp, 1333794697640871) + + expected_value = u'Disk&Ven_HP&Prod_v100w&Rev_1024' + self._TestRegvalue(event_object, u'subkey_name', expected_value) + + self._TestRegvalue(event_object, u'device_type', u'Disk') + self._TestRegvalue(event_object, u'vendor', u'Ven_HP') + self._TestRegvalue(event_object, u'product', u'Prod_v100w') + self._TestRegvalue(event_object, u'revision', u'Rev_1024') + + expected_msg = ( + u'[{0:s}] ' + u'device_type: Disk ' + u'friendly_name: HP v100w USB Device ' + u'product: Prod_v100w ' + u'revision: Rev_1024 ' + u'serial: AA951D0000007252&0 ' + u'subkey_name: Disk&Ven_HP&Prod_v100w&Rev_1024 ' + u'vendor: Ven_HP').format(key_path) + + expected_msg_short = ( + u'[{0:s}] ' + u'device_type: Disk ' + u'friendly_name: HP v100w USB D...').format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/userassist.py b/plaso/parsers/winreg_plugins/userassist.py new file mode 100644 index 0000000..da0cd2d --- /dev/null +++ b/plaso/parsers/winreg_plugins/userassist.py @@ -0,0 +1,208 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the UserAssist Windows Registry plugin.""" + +import logging + +import construct + +from plaso.events import windows_events +from plaso.lib import timelib +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface +from plaso.winnt import environ_expand +from plaso.winnt import known_folder_ids + + +class UserAssistPlugin(interface.KeyPlugin): + """Plugin that parses an UserAssist key.""" + + NAME = 'winreg_userassist' + DESCRIPTION = u'Parser for User Assist Registry data.' + + REG_TYPE = 'NTUSER' + REG_KEYS = [ + (u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer' + u'\\UserAssist\\{{FA99DFC7-6AC2-453A-A5E2-5E2AFF4507BD}}'), + (u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer' + u'\\UserAssist\\{{F4E57C4B-2036-45F0-A9AB-443BCFE33D9F}}'), + (u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer' + u'\\UserAssist\\{{F2A1CB5A-E3CC-4A2E-AF9D-505A7009D442}}'), + (u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer' + u'\\UserAssist\\{{CEBFF5CD-ACE2-4F4F-9178-9926F41749EA}}'), + (u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer' + u'\\UserAssist\\{{CAA59E3C-4792-41A5-9909-6A6A8D32490E}}'), + (u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer' + u'\\UserAssist\\{{B267E3AD-A825-4A09-82B9-EEC22AA3B847}}'), + (u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer' + u'\\UserAssist\\{{A3D53349-6E61-4557-8FC7-0028EDCEEBF6}}'), + (u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer' + u'\\UserAssist\\{{9E04CAB2-CC14-11DF-BB8C-A2F1DED72085}}'), + (u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer' + u'\\UserAssist\\{{75048700-EF1F-11D0-9888-006097DEACF9}}'), + (u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer' + u'\\UserAssist\\{{5E6AB780-7743-11CF-A12B-00AA004AE837}}'), + (u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer' + u'\\UserAssist\\{{0D6D4F41-2994-4BA0-8FEF-620E43CD2812}}'), + (u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer' + u'\\UserAssist\\{{BCB48336-4DDD-48FF-BB0B-D3190DACB3E2}}')] + + URL = [ + u'http://blog.didierstevens.com/programs/userassist/', + u'https://code.google.com/p/winreg-kb/wiki/UserAssistKeys', + u'http://intotheboxes.files.wordpress.com/2010/04' + u'/intotheboxes_2010_q1.pdf'] + + # UserAssist format version used in Windows 2000, XP, 2003, Vista. + USERASSIST_V3_STRUCT = construct.Struct( + 'userassist_entry', + construct.Padding(4), + construct.ULInt32('count'), + construct.ULInt64('timestamp')) + + # UserAssist format version used in Windows 2008, 7, 8. + USERASSIST_V5_STRUCT = construct.Struct( + 'userassist_entry', + construct.Padding(4), + construct.ULInt32('count'), + construct.ULInt32('app_focus_count'), + construct.ULInt32('focus_duration'), + construct.Padding(44), + construct.ULInt64('timestamp'), + construct.Padding(4)) + + def GetEntries( + self, parser_context, key=None, registry_type=None, file_entry=None, + parser_chain=None, **unused_kwargs): + """Parses a UserAssist Registry key. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + version_value = key.GetValue('Version') + count_subkey = key.GetSubkey('Count') + + if not version_value: + logging.error(u'missing version value') + elif not version_value.DataIsInteger(): + logging.error(u'unsupported version value data type') + elif version_value.data not in [3, 5]: + logging.error(u'unsupported version: {0:d}'.format(version_value.data)) + elif not count_subkey: + logging.error(u'missing count subkey') + else: + userassist_entry_index = 0 + + for value in count_subkey.GetValues(): + try: + value_name = value.name.decode('rot-13') + except UnicodeEncodeError as exception: + logging.debug(( + u'Unable to decode UserAssist string: {0:s} with error: {1:s}.\n' + u'Attempting piecewise decoding.').format( + value.name, exception)) + + characters = [] + for char in value.name: + if ord(char) < 128: + try: + characters.append(char.decode('rot-13')) + except UnicodeEncodeError: + characters.append(char) + else: + characters.append(char) + + value_name = u''.join(characters) + + if version_value.data == 5: + path_segments = value_name.split(u'\\') + + for segment_index in range(0, len(path_segments)): + # Remove the { } from the path segment to get the GUID. + guid = path_segments[segment_index][1:-1] + path_segments[segment_index] = known_folder_ids.PATHS.get( + guid, path_segments[segment_index]) + + value_name = u'\\'.join(path_segments) + # Check if we might need to substitute values. + if '%' in value_name: + # TODO: deprecate direct use of pre_obj. + value_name = environ_expand.ExpandWindowsEnvironmentVariables( + value_name, parser_context.knowledge_base.pre_obj) + + if not value.DataIsBinaryData(): + logging.error(u'unsupported value data type: {0:s}'.format( + value.data_type_string)) + + elif version_value.data == 3: + if len(value.data) != self.USERASSIST_V3_STRUCT.sizeof(): + logging.error(u'unsupported value data size: {0:d}'.format( + len(value.data))) + + else: + parsed_data = self.USERASSIST_V3_STRUCT.parse(value.data) + filetime = parsed_data.get('timestamp', 0) + count = parsed_data.get('count', 0) + + if count > 5: + count -= 5 + + text_dict = {} + text_dict[value_name] = u'[Count: {0:d}]'.format(count) + event_object = windows_events.WindowsRegistryEvent( + timelib.Timestamp.FromFiletime(filetime), count_subkey.path, + text_dict, offset=value.offset, registry_type=registry_type) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + elif version_value.data == 5: + if len(value.data) != self.USERASSIST_V5_STRUCT.sizeof(): + logging.error(u'unsupported value data size: {0:d}'.format( + len(value.data))) + + parsed_data = self.USERASSIST_V5_STRUCT.parse(value.data) + + userassist_entry_index += 1 + count = parsed_data.get('count', None) + app_focus_count = parsed_data.get('app_focus_count', None) + focus_duration = parsed_data.get('focus_duration', None) + timestamp = parsed_data.get('timestamp', 0) + + text_dict = {} + text_dict[value_name] = ( + u'[UserAssist entry: {0:d}, Count: {1:d}, ' + u'Application focus count: {2:d}, Focus duration: {3:d}]').format( + userassist_entry_index, count, app_focus_count, + focus_duration) + + event_object = windows_events.WindowsRegistryEvent( + timelib.Timestamp.FromFiletime(timestamp), count_subkey.path, + text_dict, offset=count_subkey.offset, + registry_type=registry_type) + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +winreg.WinRegistryParser.RegisterPlugin(UserAssistPlugin) diff --git a/plaso/parsers/winreg_plugins/userassist_test.py b/plaso/parsers/winreg_plugins/userassist_test.py new file mode 100644 index 0000000..594f298 --- /dev/null +++ b/plaso/parsers/winreg_plugins/userassist_test.py @@ -0,0 +1,112 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the UserAssist Windows Registry plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.lib import timelib_test +from plaso.parsers.winreg_plugins import test_lib +from plaso.parsers.winreg_plugins import userassist + + +class UserAssistPluginTest(test_lib.RegistryPluginTestCase): + """Tests for the UserAssist Windows Registry plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = userassist.UserAssistPlugin() + + def testProcessOnWinXP(self): + """Tests the Process function on a Windows XP Registry file.""" + test_file_entry = self._GetTestFileEntryFromPath(['NTUSER.DAT']) + key_path = ( + u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist' + u'\\{75048700-EF1F-11D0-9888-006097DEACF9}') + winreg_key = self._GetKeyFromFileEntry(test_file_entry, key_path) + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_key, file_entry=test_file_entry) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 14) + + event_object = event_objects[0] + + self.assertEquals(event_object.pathspec, test_file_entry.path_spec) + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2009-08-04 15:11:22.811067') + self.assertEquals(event_object.timestamp, expected_timestamp) + + regvalue_identifier = u'UEME_RUNPIDL:%csidl2%\\MSN.lnk' + expected_value = u'[Count: 14]' + self._TestRegvalue(event_object, regvalue_identifier, expected_value) + + expected_msg = u'[{0:s}\\Count] {1:s}: {2:s}'.format( + key_path, regvalue_identifier, expected_value) + # The short message contains the first 76 characters of the key path. + expected_msg_short = u'[{0:s}...'.format(key_path[:76]) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + def testProcessOnWin7(self): + """Tests the Process function on a Windows 7 Registry file.""" + test_file_entry = self._GetTestFileEntryFromPath(['NTUSER-WIN7.DAT']) + + key_path = ( + u'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\UserAssist' + u'\\{CEBFF5CD-ACE2-4F4F-9178-9926F41749EA}') + winreg_key = self._GetKeyFromFileEntry(test_file_entry, key_path) + event_queue_consumer = self._ParseKeyWithPlugin( + self._plugin, winreg_key, file_entry=test_file_entry) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 62) + + event_object = event_objects[0] + + self.assertEquals(event_object.pathspec, test_file_entry.path_spec) + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2010-11-10 07:49:37.078067') + self.assertEquals(event_object.timestamp, expected_timestamp) + + regvalue_identifier = u'Microsoft.Windows.GettingStarted' + expected_value = ( + u'[UserAssist entry: 1, Count: 14, Application focus count: 21, ' + u'Focus duration: 420000]') + self._TestRegvalue(event_object, regvalue_identifier, expected_value) + + expected_msg = u'[{0:s}\\Count] {1:s}: {2:s}'.format( + key_path, regvalue_identifier, expected_value) + # The short message contains the first 76 characters of the key path. + expected_msg_short = u'[{0:s}...'.format(key_path[:76]) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/winrar.py b/plaso/parsers/winreg_plugins/winrar.py new file mode 100644 index 0000000..5fe2a41 --- /dev/null +++ b/plaso/parsers/winreg_plugins/winrar.py @@ -0,0 +1,87 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a parser for WinRAR for Plaso.""" + +import re + +from plaso.events import windows_events +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface + + +__author__ = 'David Nides (david.nides@gmail.com)' + + +class WinRarHistoryPlugin(interface.KeyPlugin): + """Windows Registry plugin for parsing WinRAR History keys.""" + # TODO: Create NTUSER.DAT test file with WinRAR data. + + NAME = 'winreg_winrar' + DESCRIPTION = u'Parser for WinRAR History Registry data.' + + REG_TYPE = 'NTUSER' + REG_KEYS = [ + u'\\Software\\WinRAR\\DialogEditHistory\\ExtrPath', + u'\\Software\\WinRAR\\DialogEditHistory\\ArcName', + u'\\Software\\WinRAR\\ArcHistory'] + + _RE_VALUE_NAME = re.compile(r'^[0-9]+$', re.I) + + def GetEntries( + self, parser_context, key=None, registry_type=None, file_entry=None, + parser_chain=None, **unused_kwargs): + """Collect values under WinRAR ArcHistory and return event for each one. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + for value in key.GetValues(): + # Ignore any value not in the form: '[0-9]+'. + if not value.name or not self._RE_VALUE_NAME.search(value.name): + continue + + # Ignore any value that is empty or that does not contain a string. + if not value.data or not value.DataIsString(): + continue + + if value.name == '0': + timestamp = key.last_written_timestamp + else: + timestamp = 0 + + text_dict = {} + text_dict[value.name] = value.data + + # TODO: shouldn't this behavior be, put all the values + # into a single event object with the last written time of the key? + event_object = windows_events.WindowsRegistryEvent( + timestamp, key.path, text_dict, offset=key.offset, + registry_type=registry_type, + source_append=': WinRAR History') + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +winreg.WinRegistryParser.RegisterPlugin(WinRarHistoryPlugin) diff --git a/plaso/parsers/winreg_plugins/winrar_test.py b/plaso/parsers/winreg_plugins/winrar_test.py new file mode 100644 index 0000000..1a44bab --- /dev/null +++ b/plaso/parsers/winreg_plugins/winrar_test.py @@ -0,0 +1,83 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the WinRAR Windows Registry plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.lib import timelib_test +from plaso.parsers.winreg_plugins import test_lib +from plaso.parsers.winreg_plugins import winrar +from plaso.winreg import test_lib as winreg_test_lib + + +class WinRarArcHistoryPluginTest(test_lib.RegistryPluginTestCase): + """Tests for the WinRAR ArcHistory Windows Registry plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = winrar.WinRarHistoryPlugin() + + def testProcess(self): + """Tests the Process function.""" + key_path = u'\\Software\\WinRAR\\ArcHistory' + + values = [] + values.append(winreg_test_lib.TestRegValue( + '0', 'C:\\Downloads\\The Sleeping Dragon CD1.iso'.encode('utf_16_le'), + winreg_test_lib.TestRegValue.REG_SZ, offset=1892)) + values.append(winreg_test_lib.TestRegValue( + '1', 'C:\\Downloads\\plaso-static.rar'.encode('utf_16_le'), + winreg_test_lib.TestRegValue.REG_SZ, offset=612)) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-08-28 09:23:49.002031') + + winreg_key = winreg_test_lib.TestRegKey( + key_path, expected_timestamp, values, offset=1456) + + event_queue_consumer = self._ParseKeyWithPlugin(self._plugin, winreg_key) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 2) + + event_object = event_objects[0] + + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + self.assertEquals(event_object.timestamp, expected_timestamp) + + expected_string = ( + u'[{0:s}] 0: C:\\Downloads\\The Sleeping Dragon CD1.iso').format( + key_path) + self._TestGetMessageStrings(event_object, expected_string, expected_string) + + event_object = event_objects[1] + + self.assertEquals(event_object.timestamp, 0) + + expected_string = u'[{0:s}] 1: C:\\Downloads\\plaso-static.rar'.format( + key_path) + self._TestGetMessageStrings(event_object, expected_string, expected_string) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_plugins/winver.py b/plaso/parsers/winreg_plugins/winver.py new file mode 100644 index 0000000..878e074 --- /dev/null +++ b/plaso/parsers/winreg_plugins/winver.py @@ -0,0 +1,102 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Plug-in to collect information about the Windows version.""" + +import construct + +from plaso.events import windows_events +from plaso.lib import timelib +from plaso.parsers import winreg +from plaso.parsers.winreg_plugins import interface + + +class WinVerPlugin(interface.KeyPlugin): + """Plug-in to collect information about the Windows version.""" + + NAME = 'winreg_winver' + DESCRIPTION = u'Parser for Windows version Registry data.' + + REG_KEYS = [u'\\Microsoft\\Windows NT\\CurrentVersion'] + REG_TYPE = 'SOFTWARE' + URLS = [] + + INT_STRUCT = construct.ULInt32('install') + + # TODO: Refactor remove this function in a later CL. + def GetValueString(self, key, value_name): + """Retrieves a specific string value from the Registry key. + + Args: + key: A Windows Registry key (instance of WinRegKey). + value_name: The name of the value. + + Returns: + A string value if one is available, otherwise an empty string. + """ + value = key.GetValue(value_name) + + if not value: + return '' + + if not value.data or not value.DataIsString(): + return '' + return value.data + + def GetEntries( + self, parser_context, key=None, registry_type=None, file_entry=None, + parser_chain=None, **unused_kwargs): + """Gather minimal information about system install and return an event. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: Optional Registry key (instance of winreg.WinRegKey). + The default is None. + registry_type: Optional Registry type string. The default is None. + file_entry: Optional file entry object (instance of dfvfs.FileEntry). + The default is None. + parser_chain: Optional string containing the parsing chain up to this + point. The default is None. + """ + text_dict = {} + text_dict[u'Owner'] = self.GetValueString(key, 'RegisteredOwner') + text_dict[u'sp'] = self.GetValueString(key, 'CSDBuildNumber') + text_dict[u'Product name'] = self.GetValueString(key, 'ProductName') + text_dict[u' Windows Version Information'] = u'' + + install_raw = key.GetValue('InstallDate').raw_data + # TODO: move this to a function in utils with a more descriptive name + # e.g. CopyByteStreamToInt32BigEndian. + try: + install = self.INT_STRUCT.parse(install_raw) + except construct.FieldError: + install = 0 + + event_object = windows_events.WindowsRegistryEvent( + timelib.Timestamp.FromPosixTime(install), key.path, text_dict, + usage='OS Install Time', offset=key.offset, + registry_type=registry_type, urls=self.URLS) + + event_object.prodname = text_dict[u'Product name'] + event_object.source_long = 'SOFTWARE WinVersion key' + if text_dict[u'Owner']: + event_object.owner = text_dict[u'Owner'] + parser_context.ProduceEvent( + event_object, parser_chain=parser_chain, file_entry=file_entry) + + +winreg.WinRegistryParser.RegisterPlugin(WinVerPlugin) diff --git a/plaso/parsers/winreg_plugins/winver_test.py b/plaso/parsers/winreg_plugins/winver_test.py new file mode 100644 index 0000000..4c69dc0 --- /dev/null +++ b/plaso/parsers/winreg_plugins/winver_test.py @@ -0,0 +1,85 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the WinVer Windows Registry plugin.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import winreg as winreg_formatter +from plaso.lib import timelib_test +from plaso.parsers.winreg_plugins import test_lib +from plaso.parsers.winreg_plugins import winver +from plaso.winreg import test_lib as winreg_test_lib + + +class WinVerPluginTest(test_lib.RegistryPluginTestCase): + """Tests for the WinVer Windows Registry plugin.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._plugin = winver.WinVerPlugin() + + def testWinVer(self): + """Test the WinVer plugin.""" + key_path = u'\\Microsoft\\Windows NT\\CurrentVersion' + values = [] + + values.append(winreg_test_lib.TestRegValue( + 'ProductName', 'MyTestOS'.encode('utf_16_le'), 1, 123)) + values.append(winreg_test_lib.TestRegValue( + 'CSDBuildNumber', '5'.encode('utf_16_le'), 1, 1892)) + values.append(winreg_test_lib.TestRegValue( + 'RegisteredOwner', 'A Concerned Citizen'.encode('utf_16_le'), 1, 612)) + values.append(winreg_test_lib.TestRegValue( + 'InstallDate', '\x13\x1aAP', 3, 1001)) + + winreg_key = winreg_test_lib.TestRegKey( + key_path, 1346445929000000, values, 153) + + event_queue_consumer = self._ParseKeyWithPlugin(self._plugin, winreg_key) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 1) + + event_object = event_objects[0] + + # This should just be the plugin name, as we're invoking it directly, + # and not through the parser. + self.assertEquals(event_object.parser, self._plugin.plugin_name) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2012-08-31 20:09:55') + self.assertEquals(event_object.timestamp, expected_timestamp) + + # Note that the double spaces here are intentional. + expected_msg = ( + u'[{0:s}] ' + u'Windows Version Information: ' + u'Owner: A Concerned Citizen ' + u'Product name: MyTestOS sp: 5').format(key_path) + + expected_msg_short = ( + u'[{0:s}] ' + u'Windows Version Information: ' + u'Owner: ...').format(key_path) + + self._TestGetMessageStrings(event_object, expected_msg, expected_msg_short) + # TODO: Write a test for a non-synthetic key + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/winreg_test.py b/plaso/parsers/winreg_test.py new file mode 100644 index 0000000..a20c46f --- /dev/null +++ b/plaso/parsers/winreg_test.py @@ -0,0 +1,106 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Windows Registry file parser.""" + +import unittest + +from plaso.parsers import test_lib +from plaso.parsers import winreg + + +class WinRegTest(test_lib.ParserTestCase): + """Tests for the Windows Registry file parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = winreg.WinRegistryParser() + + def _GetParserChains(self, event_objects): + """Return a dict with a plugin count given a list of event objects.""" + parser_chains = {} + for event_object in event_objects: + parser_chain = getattr(event_object, 'parser', None) + if not parser_chain: + continue + + if parser_chain in parser_chains: + parser_chains[parser_chain] += 1 + else: + parser_chains[parser_chain] = 1 + + return parser_chains + + def _PluginNameToParserChain(self, plugin_name): + """Generate the correct parser chain for a given plugin.""" + return 'winreg/{0:s}'.format(plugin_name) + + def testNtuserParsing(self): + """Parse a NTUSER.dat file and check few items.""" + knowledge_base_values = {'current_control_set': u'ControlSet001'} + test_file = self._GetTestFilePath(['NTUSER.DAT']) + event_queue_consumer = self._ParseFile( + self._parser, test_file, knowledge_base_values=knowledge_base_values) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + parser_chains = self._GetParserChains(event_objects) + + # The _registry_type member is created dynamically by invoking + # the _GetParserChains function. + registry_type = getattr(self._parser, '_registry_type', '') + self.assertEquals(registry_type, 'NTUSER') + + expected_chain = self._PluginNameToParserChain('winreg_userassist') + self.assertTrue(expected_chain in parser_chains) + + self.assertEquals(parser_chains[expected_chain], 14) + + def testSystemParsing(self): + """Parse a SYSTEM hive an run few tests.""" + knowledge_base_values = {'current_control_set': u'ControlSet001'} + test_file = self._GetTestFilePath(['SYSTEM']) + event_queue_consumer = self._ParseFile( + self._parser, test_file, knowledge_base_values=knowledge_base_values) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + parser_chains = self._GetParserChains(event_objects) + + # The _registry_type member is created dynamically by invoking + # the _GetParserChains function. + registry_type = getattr(self._parser, '_registry_type', '') + self.assertEquals(registry_type, 'SYSTEM') + + # Check the existence of few known plugins, see if they + # are being properly picked up and are parsed. + plugin_names = ['winreg_usbstor', 'winreg_boot_execute', 'winreg_services'] + for plugin in plugin_names: + expected_chain = self._PluginNameToParserChain(plugin) + self.assertTrue( + expected_chain in parser_chains, + u'Chain {0:s} not found in events.'.format(expected_chain)) + + # Check that the number of events produced by each plugin are correct. + self.assertEquals(parser_chains.get( + self._PluginNameToParserChain('winreg_usbstor'), 0), 3) + self.assertEquals(parser_chains.get( + self._PluginNameToParserChain('winreg_boot_execute'), 0), 2) + self.assertEquals(parser_chains.get( + self._PluginNameToParserChain('winreg_services'), 0), 831) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/xchatlog.py b/plaso/parsers/xchatlog.py new file mode 100644 index 0000000..8ff1a6a --- /dev/null +++ b/plaso/parsers/xchatlog.py @@ -0,0 +1,264 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains XChat log file parser in plaso. + + Information updated 24 July 2013. + + The parser applies to XChat log files. Despite their apparent + simplicity it's not straightforward to manage every possible case. + XChat tool allows users to specify how timestamp will be + encoded (using the strftime function), by letting them to specify + additional separators. This parser will accept only the simplest + default English form of an XChat log file, as the following: + + **** BEGIN LOGGING AT Mon Dec 31 21:11:55 2001 + + dec 31 21:11:55 --> You are now talking on #gugle + dec 31 21:11:55 --- Topic for #gugle is plaso, nobody knows what it means + dec 31 21:11:55 Topic for #gugle set by Kristinn + dec 31 21:11:55 --- Joachim gives voice to fpi + dec 31 21:11:55 * XChat here + dec 31 21:11:58 ola plas-ing guys! + dec 31 21:12:00 ftw! + + It could be managed the missing month/day case too, by extracting + the month/day information from the header. But the parser logic + would become intricate, since it would need to manage day transition, + chat lines crossing the midnight. From there derives the last day of + the year bug, since the parser will not manage that transition. + + Moreover the strftime is locale-dependant, so month names, footer and + headers can change, even inside the same log file. Being said that, the + following will be the main logic used to parse the log files (note that + the first header *must be* '**** BEGIN ...' otherwise file will be skipped). + + 1) Check for '****' + 1.1) If 'BEGIN LOGGING AT' (English) + 1.1.1) Extract the YEAR + 1.1.2) Generate new event start logging + 1.1.3) set parsing = True + 1.2) If 'END LOGGING' + 1.2.1) If parsing, set parsing=False + 1.2.2) If not parsing, log debug + 1.2.3) Generate new event end logging + 1.3) If not BEGIN|END we are facing a different language + and we don't now which language! + If parsing is True, set parsing=False and log debug + 2) Not '****' so we are parsing a line + 2.1) If parsing = True, try to parse line and generate event + 2.2) If parsing = False, skip until next good header is found + + References + http://xchat.org +""" + +import logging + +import pyparsing + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import manager +from plaso.parsers import text_parser + + +__author__ = 'Francesco Picasso (francesco.picasso@gmail.com)' + + +class XChatLogEvent(time_events.TimestampEvent): + """Convenience class for a XChat Log line event.""" + DATA_TYPE = 'xchat:log:line' + + def __init__(self, timestamp, text, nickname=None): + """Initializes the event object. + + Args: + timestamp: Microseconds since Epoch in UTC. + text: The text sent by nickname or other text (server, messages, etc.). + """ + super(XChatLogEvent, self).__init__( + timestamp, eventdata.EventTimestamp.ADDED_TIME) + self.text = text + if nickname: + self.nickname = nickname + + +class XChatLogParser(text_parser.PyparsingSingleLineTextParser): + """Parse XChat log files.""" + + NAME = 'xchatlog' + DESCRIPTION = u'Parser for XChat log files.' + + ENCODING = 'UTF-8' + + # Common (header/footer/body) pyparsing structures. + # TODO: Only English ASCII timestamp supported ATM, add support for others. + IGNORE_STRING = pyparsing.Word(pyparsing.printables).suppress() + LOG_ACTION = pyparsing.Word( + pyparsing.printables, min=3, max=5).setResultsName('log_action') + MONTH_NAME = pyparsing.Word( + pyparsing.printables, exact=3).setResultsName('month_name') + DAY = pyparsing.Word(pyparsing.nums, max=2).setParseAction( + text_parser.PyParseIntCast).setResultsName('day') + TIME = text_parser.PyparsingConstants.TIME.setResultsName('time') + YEAR = text_parser.PyparsingConstants.YEAR.setResultsName('year') + NICKNAME = pyparsing.QuotedString( + u'<', endQuoteChar=u'>').setResultsName('nickname') + TEXT = pyparsing.SkipTo(pyparsing.lineEnd).setResultsName('text') + + # Header/footer pyparsing structures. + # Sample: "**** BEGIN LOGGING AT Mon Dec 31 21:11:55 2011". + # Note that "BEGIN LOGGING" text is localized (default, English) and can be + # different if XChat locale is different. + HEADER_SIGNATURE = pyparsing.Keyword(u'****') + HEADER = ( + HEADER_SIGNATURE.suppress() + LOG_ACTION + + pyparsing.Keyword(u'LOGGING AT').suppress() + IGNORE_STRING + + MONTH_NAME + DAY + TIME + YEAR) + + # Body (nickname, text and/or service messages) pyparsing structures. + # Sample: "dec 31 21:11:58 ola plas-ing guys!". + LOG_LINE = MONTH_NAME + DAY + TIME + pyparsing.Optional(NICKNAME) + TEXT + + # Define the available log line structures. + LINE_STRUCTURES = [ + ('logline', LOG_LINE), + ('header', HEADER), + ('header_signature', HEADER_SIGNATURE), + ] + + def __init__(self): + """Initializes a XChatLog parser object.""" + super(XChatLogParser, self).__init__() + self.offset = 0 + self.xchat_year = 0 + + def _GetTimestamp(self, parse_result, timezone, year=0): + """Determines the timestamp from the pyparsing ParseResults. + + Args: + parse_result: The pyparsing ParseResults object. + timezone: The timezone object. + year: Optional current year. The default is 0. + + Returns: + A timelib timestamp or 0. + """ + month = timelib.MONTH_DICT.get(parse_result.month_name.lower(), None) + if not month: + logging.debug(u'XChatLog unmanaged month name [{0:s}]'.format( + parse_result.month_name)) + return 0 + + hour, minute, second = parse_result.time + if not year: + # This condition could happen when parsing the header line: if unable + # to get a valid year, returns a '0' timestamp, thus preventing any + # log line parsing (since xchat_year is unset to '0') until a new good + # (it means supported) header with a valid year information is found. + # TODO: reconsider this behaviour. + year = parse_result.get('year', 0) + + if not year: + return 0 + + self.xchat_year = year + + day = parse_result.get('day', 0) + return timelib.Timestamp.FromTimeParts( + year, month, day, hour, minute, second, timezone=timezone) + + def VerifyStructure(self, parser_context, line): + """Verify that this file is a XChat log file. + + Args: + parser_context: A parser context object (instance of ParserContext). + line: A single line from the text file. + + Returns: + True if this is the correct parser, False otherwise. + """ + try: + parse_result = self.HEADER.parseString(line) + except pyparsing.ParseException: + logging.debug(u'Unable to parse, not a valid XChat log file header') + return False + timestamp = self._GetTimestamp(parse_result, parser_context.timezone) + if not timestamp: + logging.debug(u'Wrong XChat timestamp: {0:s}'.format(parse_result)) + return False + # Unset the xchat_year since we are only verifying structure. + # The value gets set in _GetTimestamp during the actual parsing. + self.xchat_year = 0 + return True + + def ParseRecord(self, parser_context, key, structure): + """Parse each record structure and return an event object if applicable. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: An identification string indicating the name of the parsed + structure. + structure: A pyparsing.ParseResults object from a line in the + log file. + + Returns: + An event object (instance of EventObject) or None. + """ + if key == 'logline': + if not self.xchat_year: + logging.debug(u'XChatLogParser, missing year information.') + return + timestamp = self._GetTimestamp( + structure, parser_context.timezone, year=self.xchat_year) + if not timestamp: + logging.debug(u'XChatLogParser, cannot get timestamp from line.') + return + # The text string contains multiple unnecessary whitespaces that need to + # be removed, thus the split and re-join. + return XChatLogEvent( + timestamp, u' '.join(structure.text.split()), structure.nickname) + elif key == 'header': + timestamp = self._GetTimestamp(structure, parser_context.timezone) + if not timestamp: + logging.warning(u'XChatLogParser, cannot get timestamp from header.') + return + if structure.log_action == u'BEGIN': + return XChatLogEvent(timestamp, u'XChat start logging') + elif structure.log_action == u'END': + # End logging, unset year. + self.xchat_year = 0 + return XChatLogEvent(timestamp, u'XChat end logging') + else: + logging.warning(u'Unknown log action: {0:s}.'.format( + structure.log_action)) + elif key == 'header_signature': + # If this key is matched (after others keys failed) we got a different + # localized header and we should stop parsing until a new good header + # is found. Stop parsing is done setting xchat_year to 0. + # Note that the code assumes that LINE_STRUCTURES will be used in the + # exact order as defined! + logging.warning(u'Unknown locale header.') + self.xchat_year = 0 + else: + logging.warning( + u'Unable to parse record, unknown structure: {0:s}'.format(key)) + + +manager.ParsersManager.RegisterParser(XChatLogParser) diff --git a/plaso/parsers/xchatlog_test.py b/plaso/parsers/xchatlog_test.py new file mode 100644 index 0000000..429fadd --- /dev/null +++ b/plaso/parsers/xchatlog_test.py @@ -0,0 +1,101 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the xchatlog parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import xchatlog as xchatlog_formatter +from plaso.lib import timelib_test +from plaso.parsers import test_lib +from plaso.parsers import xchatlog + +import pytz + + +__author__ = 'Francesco Picasso (francesco.picasso@gmail.com)' + + +class XChatLogUnitTest(test_lib.ParserTestCase): + """Tests for the xchatlog parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = xchatlog.XChatLogParser() + + def testParse(self): + """Tests the Parse function.""" + knowledge_base_values = {'zone': pytz.timezone('Europe/Rome')} + test_file = self._GetTestFilePath(['xchat.log']) + event_queue_consumer = self._ParseFile( + self._parser, test_file, knowledge_base_values=knowledge_base_values) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 9) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-12-31 21:11:55+01:00') + self.assertEquals(event_objects[0].timestamp, expected_timestamp) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-12-31 23:00:00+01:00') + self.assertEquals(event_objects[7].timestamp, expected_timestamp) + + expected_timestamp = timelib_test.CopyStringToTimestamp( + '2011-12-31 23:59:00+01:00') + self.assertEquals(event_objects[8].timestamp, expected_timestamp) + + expected_string = u'XChat start logging' + self._TestGetMessageStrings( + event_objects[0], expected_string, expected_string) + + expected_string = u'--> You are now talking on #gugle' + self._TestGetMessageStrings( + event_objects[1], expected_string, expected_string) + + expected_string = u'--- Topic for #gugle is plaso, a difficult word' + self._TestGetMessageStrings( + event_objects[2], expected_string, expected_string) + + expected_string = u'Topic for #gugle set by Kristinn' + self._TestGetMessageStrings( + event_objects[3], expected_string, expected_string) + + expected_string = u'--- Joachim gives voice to fpi' + self._TestGetMessageStrings( + event_objects[4], expected_string, expected_string) + + expected_string = u'* XChat here' + self._TestGetMessageStrings( + event_objects[5], expected_string, expected_string) + + expected_string = u'[nickname: fpi] ola plas-ing guys!' + self._TestGetMessageStrings( + event_objects[6], expected_string, expected_string) + + expected_string = u'[nickname: STRANGER] \u65e5\u672c' + self._TestGetMessageStrings( + event_objects[7], expected_string, expected_string) + + expected_string = u'XChat end logging' + self._TestGetMessageStrings( + event_objects[8], expected_string, expected_string) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/parsers/xchatscrollback.py b/plaso/parsers/xchatscrollback.py new file mode 100644 index 0000000..23d0fb0 --- /dev/null +++ b/plaso/parsers/xchatscrollback.py @@ -0,0 +1,212 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains XChat scrollback log file parser in plaso. + + Information updated 06 September 2013. + + Besides the logging capability, the XChat IRC client has the option to + record the text for opened tabs. So, when rejoining a particular channel + and/or a particular conversation, XChat will display the last messages + exchanged. This artifact could be present, if not disabled, even if + normal logging is disabled. + + From the XChat FAQ (http://xchatdata.net/Using/FAQ): + Q: 'How do I keep text from previous sessions from being displayed when + I join a channel?' + R: 'Starting in XChat 2.8.4, XChat implemented the Scrollback feature which + displays text from the last time you had a particular tab open. + To disable this setting for all channels, Go to Settings -> Preferences + -> Logging and uncheck Display scrollback from previous session. + In XChat 2.8.6, XChat implemented both Per Channel Logging, and + Per Channel Scrollbacks. If you are on 2.8.6 or newer, you can disable + loading scrollback for just one particular tab name by right clicking on + the tab name, selecting Settings, and then unchecking Reload scrollback' + + The log file format differs from logging format, but it's quite simple + 'T 1232315916 Python interface unloaded' + <\n> + + The time reported in the log is Unix Epoch (from source code, time(0)). + The part could contain some 'decorators' (bold, underline, colors + indication, etc.), so the parser should strip those control fields. + + References + http://xchat.org +""" + +import logging + +import pyparsing + +from plaso.events import time_events +from plaso.lib import eventdata +from plaso.lib import timelib +from plaso.parsers import manager +from plaso.parsers import text_parser + + +__author__ = 'Francesco Picasso (francesco.picasso@gmail.com)' + + +class XChatScrollbackEvent(time_events.PosixTimeEvent): + """Convenience class for a XChat Scrollback line event.""" + DATA_TYPE = 'xchat:scrollback:line' + + def __init__(self, timestamp, offset, nickname, text): + """Initializes the event object. + + Args: + timestamp: The timestamp time value, epoch. + offset: The offset of the event. + nickname: The nickname used. + text: The text sent by nickname or other text (server, messages, etc.). + """ + super(XChatScrollbackEvent, self).__init__( + timestamp, eventdata.EventTimestamp.ADDED_TIME) + self.offset = offset + self.nickname = nickname + self.text = text + + +class XChatScrollbackParser(text_parser.PyparsingSingleLineTextParser): + """Parse XChat scrollback log files.""" + + NAME = 'xchatscrollback' + DESCRIPTION = u'Parser for XChat scrollback log files.' + + ENCODING = 'UTF-8' + + # Define how a log line should look like. + LOG_LINE = ( + pyparsing.Literal(u'T').suppress() + + pyparsing.Word(pyparsing.nums).setResultsName('epoch') + + pyparsing.SkipTo(pyparsing.LineEnd()).setResultsName('text')) + LOG_LINE.parseWithTabs() + + # Define the available log line structures. + LINE_STRUCTURES = [ + ('logline', LOG_LINE), + ] + + # Define for the stripping phase. + STRIPPER = ( + pyparsing.Word(u'\x03', pyparsing.nums, max=3).suppress() | + pyparsing.Word(u'\x02\x07\x08\x0f\x16\x1d\x1f', exact=1).suppress()) + + # Define the structure for parsing and get and + MSG_NICK_START = pyparsing.Literal(u'<') + MSG_NICK_END = pyparsing.Literal(u'>') + MSG_NICK = pyparsing.SkipTo(MSG_NICK_END).setResultsName('nickname') + MSG_ENTRY_NICK = pyparsing.Optional(MSG_NICK_START + MSG_NICK + MSG_NICK_END) + MSG_ENTRY_TEXT = pyparsing.SkipTo(pyparsing.LineEnd()).setResultsName('text') + MSG_ENTRY = MSG_ENTRY_NICK + MSG_ENTRY_TEXT + MSG_ENTRY.parseWithTabs() + + def __init__(self): + """Initializes a parser object.""" + super(XChatScrollbackParser, self).__init__() + self.use_local_zone = False + self.offset = 0 + + def VerifyStructure(self, parser_context, line): + """Verify that this file is a XChat scrollback log file. + + Args: + parser_context: A parser context object (instance of ParserContext). + line: A single line from the text file. + + Returns: + True if this is the correct parser, False otherwise. + """ + structure = self.LOG_LINE + parsed_structure = None + epoch = None + try: + parsed_structure = structure.parseString(line) + except pyparsing.ParseException: + logging.debug(u'Not a XChat scrollback log file') + return False + try: + epoch = int(parsed_structure.epoch) + except ValueError: + logging.debug(u'Not a XChat scrollback log file, invalid epoch string') + return False + if not timelib.Timestamp.FromPosixTime(epoch): + logging.debug(u'Not a XChat scrollback log file, invalid timestamp') + return False + return True + + def ParseRecord(self, parser_context, key, structure): + """Parse each record structure and return an EventObject if applicable. + + Args: + parser_context: A parser context object (instance of ParserContext). + key: An identification string indicating the name of the parsed + structure. + structure: A pyparsing.ParseResults object from a line in the + log file. + + Returns: + An event object (instance of EventObject) or None. + """ + if key != 'logline': + logging.warning( + u'Unable to parse record, unknown structure: {0:s}'.format(key)) + return + + try: + epoch = int(structure.epoch) + except ValueError: + logging.debug(u'Invalid epoch string {0:s}, skipping record'.format( + structure.epoch)) + return + + try: + nickname, text = self._StripThenGetNicknameAndText(structure.text) + except pyparsing.ParseException: + logging.debug(u'Error parsing entry at offset {0:d}'.format(self.offset)) + return + + return XChatScrollbackEvent(epoch, self.offset, nickname, text) + + def _StripThenGetNicknameAndText(self, text): + """Strips decorators from text and gets if available. + + This method implements the XChat strip_color2 and fe_print_text + functions, slightly modified to get pure text. From the parsing point + of view, after having stripped, the code takes everything as is, + simply replacing tabs with spaces (as the original XChat code). + So the VerifyStructure plays an important role in checking if + the source file has the right format, since the method will not raise + any parse exception and every content will be good. + + Args: + text: The text obtained from the record entry. + + Returns: + A list containing two entries: + nickname: The nickname if present. + text: The text written by nickname or service messages. + """ + stripped = self.STRIPPER.transformString(text) + structure = self.MSG_ENTRY.parseString(stripped) + text = structure.text.replace(u'\t', u' ') + return structure.nickname, text + + +manager.ParsersManager.RegisterParser(XChatScrollbackParser) diff --git a/plaso/parsers/xchatscrollback_test.py b/plaso/parsers/xchatscrollback_test.py new file mode 100644 index 0000000..745a217 --- /dev/null +++ b/plaso/parsers/xchatscrollback_test.py @@ -0,0 +1,87 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the xchatscrollback log parser.""" + +import unittest + +# pylint: disable=unused-import +from plaso.formatters import xchatscrollback as xchatscrollback_formatter +from plaso.parsers import test_lib +from plaso.parsers import xchatscrollback + + +__author__ = 'Francesco Picasso (francesco.picasso@gmail.com)' + + +class XChatScrollbackUnitTest(test_lib.ParserTestCase): + """Tests for the xchatscrollback log parser.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._parser = xchatscrollback.XChatScrollbackParser() + + def testParse(self): + """Tests the Parse function.""" + test_file = self._GetTestFilePath(['xchatscrollback.log']) + event_queue_consumer = self._ParseFile(self._parser, test_file) + event_objects = self._GetEventObjectsFromQueue(event_queue_consumer) + + self.assertEquals(len(event_objects), 10) + + # TODO: refactor this to use timelib_test. + self.assertEquals(event_objects[0].timestamp, 1232074579000000) + self.assertEquals(event_objects[1].timestamp, 1232074587000000) + self.assertEquals(event_objects[2].timestamp, 1232315916000000) + self.assertEquals(event_objects[3].timestamp, 1232315916000000) + self.assertEquals(event_objects[4].timestamp, 1232959856000000) + self.assertEquals(event_objects[5].timestamp, 0) + self.assertEquals(event_objects[7].timestamp, 1232959862000000) + self.assertEquals(event_objects[8].timestamp, 1232959932000000) + self.assertEquals(event_objects[9].timestamp, 1232959993000000) + + expected_string = u'[] * Speaking now on ##plaso##' + self._TestGetMessageStrings( + event_objects[0], expected_string, expected_string) + + expected_string = u'[] * Joachim \xe8 uscito (Client exited)' + self._TestGetMessageStrings( + event_objects[1], expected_string, expected_string) + + expected_string = u'[] Tcl interface unloaded' + self._TestGetMessageStrings( + event_objects[2], expected_string, expected_string) + + expected_string = u'[] Python interface unloaded' + self._TestGetMessageStrings( + event_objects[3], expected_string, expected_string) + + expected_string = u'[] * Topic of #plasify \xe8: .' + self._TestGetMessageStrings( + event_objects[6], expected_string, expected_string) + + expected_string = u'[nickname: fpi] Hi Kristinn!' + self._TestGetMessageStrings( + event_objects[8], expected_string, expected_string) + + expected_string = u'[nickname: Kristinn] GO AND WRITE PARSERS!!! O_o' + self._TestGetMessageStrings( + event_objects[9], expected_string, expected_string) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/preprocessors/__init__.py b/plaso/preprocessors/__init__.py new file mode 100644 index 0000000..ae14266 --- /dev/null +++ b/plaso/preprocessors/__init__.py @@ -0,0 +1,22 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains an import statement for each preprocess plugin.""" + +from plaso.preprocessors import linux +from plaso.preprocessors import macosx +from plaso.preprocessors import windows diff --git a/plaso/preprocessors/interface.py b/plaso/preprocessors/interface.py new file mode 100644 index 0000000..2a8dfc4 --- /dev/null +++ b/plaso/preprocessors/interface.py @@ -0,0 +1,223 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains classes used for preprocessing in plaso.""" + +import abc +import logging + +from dfvfs.helpers import file_system_searcher + +from plaso.lib import errors + + +class PreprocessPlugin(object): + """Class that defines the preprocess plugin object interface. + + Any preprocessing plugin that implements this interface + should define which operating system this plugin supports. + + The OS variable supports the following values: + + Windows + + Linux + + MacOSX + + Since some plugins may require knowledge gained from + other checks all plugins have a weight associated to it. + The weight variable can have values from one to three: + + 1 - Requires no prior knowledge, can run immediately. + + 2 - Requires knowledge from plugins with weight 1. + + 3 - Requires knowledge from plugins with weight 2. + + The default weight of 3 is assigned to plugins, so each + plugin needs to overwrite that value if needed. + + The plugins are grouped by the operating system they work + on and then on their weight. That means that if the tool + is run against a Windows system all plugins that support + Windows are grouped together, and only plugins with weight + one are run, then weight two followed by the rest of the + plugins with the weight of three. There is no priority or + guaranteed order of plugins that have the same weight, which + makes it important to define the weight appropriately. + """ + + # Defines the OS that this plugin supports. + SUPPORTED_OS = [] + + # Weight is an INT, with the value of 1-3. + WEIGHT = 3 + + # Defines the knowledge base attribute to be set. + ATTRIBUTE = '' + + @property + def plugin_name(self): + """Return the name of the plugin.""" + return self.__class__.__name__ + + def _FindFileEntry(self, searcher, path): + """Searches for a file entry that matches the path. + + Args: + searcher: The file system searcher object (instance of + dfvfs.FileSystemSearcher). + path: The location of the file entry relative to the file system + of the searcher. + + Returns: + The file entry if successful or None otherwise. + + Raises: + errors.PreProcessFail: if the file entry cannot be found or opened. + """ + find_spec = file_system_searcher.FindSpec( + location=path, case_sensitive=False) + + path_specs = list(searcher.Find(find_specs=[find_spec])) + if not path_specs or len(path_specs) != 1: + raise errors.PreProcessFail(u'Unable to find: {0:s}'.format(path)) + + try: + file_entry = searcher.GetFileEntryByPathSpec(path_specs[0]) + except IOError as exception: + raise errors.PreProcessFail( + u'Unable to retrieve file entry: {0:s} with error: {1:s}'.format( + path, exception)) + + return file_entry + + @abc.abstractmethod + def GetValue(self, searcher, knowledge_base): + """Return the value for the attribute. + + Args: + searcher: The file system searcher object (instance of + dfvfs.FileSystemSearcher). + knowledge_base: A knowledge base object (instance of KnowledgeBase), + which contains information from the source data needed + for parsing. + """ + raise NotImplementedError + + def Run(self, searcher, knowledge_base): + """Set the attribute of the object store to the value from GetValue. + + Args: + searcher: The file system searcher object (instance of + dfvfs.FileSystemSearcher). + knowledge_base: A knowledge base object (instance of KnowledgeBase), + which contains information from the source data needed + for parsing. + """ + value = self.GetValue(searcher, knowledge_base) + knowledge_base.SetValue(self.ATTRIBUTE, value) + value = knowledge_base.GetValue(self.ATTRIBUTE, default_value=u'N/A') + logging.info(u'[PreProcess] Set attribute: {0:s} to {1:s}'.format( + self.ATTRIBUTE, value)) + + +class PathPreprocessPlugin(PreprocessPlugin): + """Return a simple path.""" + + WEIGHT = 1 + + def GetValue(self, searcher, unused_knowledge_base): + """Returns the path as found by the searcher. + + Args: + searcher: The file system searcher object (instance of + dfvfs.FileSystemSearcher). + knowledge_base: A knowledge base object (instance of KnowledgeBase), + which contains information from the source data needed + for parsing. + + Returns: + The first path location string. + + Raises: + PreProcessFail: if the path could not be found. + """ + find_spec = file_system_searcher.FindSpec( + location_regex=self.PATH, case_sensitive=False) + path_specs = list(searcher.Find(find_specs=[find_spec])) + + if not path_specs: + raise errors.PreProcessFail( + u'Unable to find path: {0:s}'.format(self.PATH)) + + relative_path = searcher.GetRelativePath(path_specs[0]) + if not relative_path: + raise errors.PreProcessFail( + u'Missing relative path for: {0:s}'.format(self.PATH)) + + return relative_path + + +def GuessOS(searcher): + """Returns a string representing what we think the underlying OS is. + + The available return strings are: + + Windows + + MacOSX + + Linux + + Args: + searcher: The file system searcher object (instance of + dfvfs.FileSystemSearcher). + + Returns: + A string indicating which OS we are dealing with. + """ + find_specs = [ + file_system_searcher.FindSpec( + location=u'/etc', case_sensitive=False), + file_system_searcher.FindSpec( + location=u'/System/Library', case_sensitive=False), + file_system_searcher.FindSpec( + location=u'/Windows/System32', case_sensitive=False), + file_system_searcher.FindSpec( + location=u'/WINNT/System32', case_sensitive=False), + file_system_searcher.FindSpec( + location=u'/WINNT35/System32', case_sensitive=False), + file_system_searcher.FindSpec( + location=u'/WTSRV/System32', case_sensitive=False)] + + locations = [] + for path_spec in searcher.Find(find_specs=find_specs): + relative_path = searcher.GetRelativePath(path_spec) + if relative_path: + locations.append(relative_path.lower()) + + # We need to check for both forward and backward slashes since the path + # spec will be OS dependent, as in running the tool on Windows will return + # Windows paths (backward slash) vs. forward slash on *NIX systems. + windows_locations = set([ + u'/windows/system32', u'\\windows\\system32', u'/winnt/system32', + u'\\winnt\\system32', u'/winnt35/system32', u'\\winnt35\\system32', + u'\\wtsrv\\system32', u'/wtsrv/system32']) + + if windows_locations.intersection(set(locations)): + return 'Windows' + + if u'/system/library' in locations: + return 'MacOSX' + + if u'/etc' in locations: + return 'Linux' + + return 'None' diff --git a/plaso/preprocessors/linux.py b/plaso/preprocessors/linux.py new file mode 100644 index 0000000..1e5075a --- /dev/null +++ b/plaso/preprocessors/linux.py @@ -0,0 +1,118 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains preprocessors for Linux.""" + +import csv + +from dfvfs.helpers import text_file + +from plaso.lib import errors +from plaso.preprocessors import interface +from plaso.preprocessors import manager + + +class LinuxHostname(interface.PreprocessPlugin): + """A preprocessing class that fetches hostname on Linux.""" + + SUPPORTED_OS = ['Linux'] + WEIGHT = 1 + ATTRIBUTE = 'hostname' + + def GetValue(self, searcher, unused_knowledge_base): + """Determines the hostname based on the contents of /etc/hostname. + + Args: + searcher: The file system searcher object (instance of + dfvfs.FileSystemSearcher). + knowledge_base: A knowledge base object (instance of KnowledgeBase), + which contains information from the source data needed + for parsing. + + Returns: + The hostname. + + Raises: + errors.PreProcessFail: if the preprocessing fails. + """ + path = u'/etc/hostname' + file_entry = self._FindFileEntry(searcher, path) + if not file_entry: + raise errors.PreProcessFail( + u'Unable to find file entry for path: {0:s}.'.format(path)) + + file_object = file_entry.GetFileObject() + file_data = file_object.read(512) + file_object.close() + + hostname, _, _ = file_data.partition('\n') + return u'{0:s}'.format(hostname) + + +class LinuxUsernames(interface.PreprocessPlugin): + """A preprocessing class that fetches usernames on Linux.""" + + SUPPORTED_OS = ['Linux'] + WEIGHT = 1 + ATTRIBUTE = 'users' + + def GetValue(self, searcher, unused_knowledge_base): + """Determines the user information based on the contents of /etc/passwd. + + Args: + searcher: The file system searcher object (instance of + dfvfs.FileSystemSearcher). + knowledge_base: A knowledge base object (instance of KnowledgeBase), + which contains information from the source data needed + for parsing. + + Returns: + A list containing username information dicts. + + Raises: + errors.PreProcessFail: if the preprocessing fails. + """ + # TODO: Add passwd.cache, might be good if nss cache is enabled. + + path = u'/etc/passwd' + file_entry = self._FindFileEntry(searcher, path) + if not file_entry: + raise errors.PreProcessFail( + u'Unable to find file entry for path: {0:s}.'.format(path)) + + file_object = file_entry.GetFileObject() + text_file_object = text_file.TextFile(file_object) + + reader = csv.reader(text_file_object, delimiter=':') + + users = [] + for row in reader: + # TODO: as part of artifacts, create a proper object for this. + user = { + 'uid': row[2], + 'gid': row[3], + 'name': row[0], + 'path': row[5], + 'shell': row[6]} + users.append(user) + + file_object.close() + return users + + +manager.PreprocessPluginsManager.RegisterPlugins([ + LinuxHostname, LinuxUsernames]) diff --git a/plaso/preprocessors/linux_test.py b/plaso/preprocessors/linux_test.py new file mode 100644 index 0000000..53095b5 --- /dev/null +++ b/plaso/preprocessors/linux_test.py @@ -0,0 +1,100 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Linux preprocess plug-ins.""" + +import unittest + +from dfvfs.helpers import file_system_searcher +from dfvfs.path import fake_path_spec + +from plaso.artifacts import knowledge_base +from plaso.preprocessors import linux +from plaso.preprocessors import test_lib + + +class LinuxHostnameTest(test_lib.PreprocessPluginTest): + """Tests for the Linux hostname preprocess plug-in object.""" + + _FILE_DATA = ( + 'plaso.kiddaland.net\n') + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._fake_file_system = self._BuildSingleFileFakeFileSystem( + u'/etc/hostname', self._FILE_DATA) + + mount_point = fake_path_spec.FakePathSpec(location=u'/') + self._searcher = file_system_searcher.FileSystemSearcher( + self._fake_file_system, mount_point) + + def testGetValue(self): + """Tests the GetValue function.""" + knowledge_base_object = knowledge_base.KnowledgeBase() + + plugin = linux.LinuxHostname() + plugin.Run(self._searcher, knowledge_base_object) + + self.assertEquals(knowledge_base_object.hostname, u'plaso.kiddaland.net') + + +class LinuxUsernamesTest(test_lib.PreprocessPluginTest): + """Tests for the Linux usernames preprocess plug-in object.""" + + _FILE_DATA = ( + 'root:x:0:0:root:/root:/bin/bash\n' + 'bin:x:1:1:bin:/bin:/sbin/nologin\n' + 'daemon:x:2:2:daemon:/sbin:/sbin/nologin\n' + 'adm:x:3:4:adm:/var/adm:/sbin/nologin\n' + 'lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin\n' + 'sync:x:5:0:sync:/sbin:/bin/sync\n' + 'shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown\n' + 'halt:x:7:0:halt:/sbin:/sbin/halt\n' + 'mail:x:8:12:mail:/var/spool/mail:/sbin/nologin\n' + 'operator:x:11:0:operator:/root:/sbin/nologin\n' + 'games:x:12:100:games:/usr/games:/sbin/nologin\n' + 'ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin\n' + 'nobody:x:99:99:Nobody:/:/sbin/nologin\n') + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._fake_file_system = self._BuildSingleFileFakeFileSystem( + u'/etc/passwd', self._FILE_DATA) + + mount_point = fake_path_spec.FakePathSpec(location=u'/') + self._searcher = file_system_searcher.FileSystemSearcher( + self._fake_file_system, mount_point) + + def testGetValue(self): + """Tests the GetValue function.""" + knowledge_base_object = knowledge_base.KnowledgeBase() + + plugin = linux.LinuxUsernames() + plugin.Run(self._searcher, knowledge_base_object) + + users = knowledge_base_object.GetValue('users') + self.assertEquals(len(users), 13) + + self.assertEquals(users[11].get('uid', None), u'14') + self.assertEquals(users[11].get('gid', None), u'50') + self.assertEquals(users[11].get('name', None), u'ftp') + self.assertEquals(users[11].get('path', None), u'/var/ftp') + self.assertEquals(users[11].get('shell', None), u'/sbin/nologin') + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/preprocessors/macosx.py b/plaso/preprocessors/macosx.py new file mode 100644 index 0000000..5ae5843 --- /dev/null +++ b/plaso/preprocessors/macosx.py @@ -0,0 +1,390 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains preprocessors for Mac OS X.""" + +import logging + +from binplist import binplist +from dfvfs.helpers import file_system_searcher +from xml.etree import ElementTree + +from plaso.lib import errors +from plaso.lib import utils +from plaso.parsers.plist_plugins import interface as plist_interface +from plaso.preprocessors import interface +from plaso.preprocessors import manager + + +class PlistPreprocessPlugin(interface.PreprocessPlugin): + """Class that defines the plist preprocess plugin object.""" + + SUPPORTED_OS = ['MacOSX'] + WEIGHT = 2 + + # Path to the plist file to be parsed, can depend on paths discovered + # in previous preprocessors. + PLIST_PATH = '' + + # The key that's value should be returned back. It is an ordered list + # of preference. If the first value is found it will be returned and no + # others will be searched. + PLIST_KEYS = [''] + + def GetValue(self, searcher, unused_knowledge_base): + """Returns a value retrieved from keys within a plist file. + + Where the name of the keys are defined in PLIST_KEYS. + + Args: + searcher: The file system searcher object (instance of + dfvfs.FileSystemSearcher). + knowledge_base: A knowledge base object (instance of KnowledgeBase), + which contains information from the source data needed + for parsing. + + Returns: + The value of the first key that is found. + + Raises: + errors.PreProcessFail: if the preprocessing fails. + """ + file_entry = self._FindFileEntry(searcher, self.PLIST_PATH) + if not file_entry: + raise errors.PreProcessFail( + u'Unable to open file: {0:s}'.format(self.PLIST_PATH)) + + file_object = file_entry.GetFileObject() + value = self.ParseFile(file_entry, file_object) + file_object.close() + + return value + + def ParseFile(self, file_entry, file_object): + """Parses the plist file and returns the parsed key. + + Args: + file_entry: The file entry (instance of dfvfs.FileEntry). + file_object: The file-like object. + + Returns: + The value of the first key defined by PLIST_KEYS that is found. + + Raises: + errors.PreProcessFail: if the preprocessing fails. + """ + try: + plist_file = binplist.BinaryPlist(file_object) + top_level_object = plist_file.Parse() + + except binplist.FormatError as exception: + raise errors.PreProcessFail( + u'File is not a plist: {0:s} with error: {1:s}'.format( + file_entry.path_spec.comparable, exception)) + + except OverflowError as exception: + raise errors.PreProcessFail( + u'Unable to process plist: {0:s} with error: {1:s}'.format( + file_entry.path_spec.comparable, exception)) + + if not plist_file: + raise errors.PreProcessFail( + u'File is not a plist: {0:s}'.format(file_entry.path_spec.comparable)) + + match = None + key_name = '' + for plist_key in self.PLIST_KEYS: + try: + match = plist_interface.GetKeys( + top_level_object, frozenset([plist_key])) + except KeyError: + continue + if match: + key_name = plist_key + break + + if not match: + raise errors.PreProcessFail( + u'Keys not found inside plist file: {0:s}.'.format( + u','.join(self.PLIST_KEYS))) + + return self.ParseKey(match, key_name) + + def ParseKey(self, key, key_name): + """Retrieves a specific value from the key. + + Args: + key: The key object (instance of dict). + key_name: The name of the key. + + Returns: + The value of the key defined by key_name. + + Raises: + errors.PreProcessFail: if the preprocessing fails. + """ + value = key.get(key_name, None) + if not value: + raise errors.PreProcessFail( + u'Value of key: {0:s} not found.'.format(key_name)) + + return value + + +class XMLPlistPreprocessPlugin(PlistPreprocessPlugin): + """Class that defines the Mac OS X XML plist preprocess plugin object.""" + + def _GetKeys(self, xml_root, key_name): + """Return a dict with the requested keys.""" + match = {} + + generator = xml_root.iter() + for key in generator: + if 'key' in key.tag and key_name in key.text: + value_key = generator.next() + value = '' + for subkey in value_key.iter(): + if 'string' in subkey.tag: + value = subkey.text + match[key.text] = value + + # Now we need to go over the match dict and retrieve values. + return match + + def ParseFile(self, file_entry, file_object): + """Parse the file and return parsed key. + + Args: + file_entry: The file entry (instance of dfvfs.FileEntry). + file_object: The file-like object. + + Returns: + The value of the first key defined by PLIST_KEYS that is found. + + Raises: + errors.PreProcessFail: if the preprocessing fails. + """ + # TODO: Move to defusedxml for safer XML parsing. + try: + xml = ElementTree.parse(file_object) + except ElementTree.ParseError: + raise errors.PreProcessFail(u'File is not a XML file.') + except IOError: + raise errors.PreProcessFail(u'File is not a XML file.') + + xml_root = xml.getroot() + key_name = '' + match = None + for key in self.PLIST_KEYS: + match = self._GetKeys(xml_root, key) + if match: + key_name = key + break + + if not match: + raise errors.PreProcessFail( + u'Keys not found inside plist file: {0:s}.'.format( + u','.join(self.PLIST_KEYS))) + + return self.ParseKey(match, key_name) + + +class MacOSXBuild(XMLPlistPreprocessPlugin): + """Fetches build information about a Mac OS X system.""" + + ATTRIBUTE = 'build' + PLIST_PATH = '/System/Library/CoreServices/SystemVersion.plist' + + PLIST_KEYS = ['ProductUserVisibleVersion'] + + +class MacOSXHostname(XMLPlistPreprocessPlugin): + """Fetches hostname information about a Mac OS X system.""" + + ATTRIBUTE = 'hostname' + PLIST_PATH = '/Library/Preferences/SystemConfiguration/preferences.plist' + + PLIST_KEYS = ['ComputerName', 'LocalHostName'] + + +class MacOSXKeyboard(PlistPreprocessPlugin): + """Fetches keyboard information from a Mac OS X system.""" + + ATTRIBUTE = 'keyboard_layout' + PLIST_PATH = '/Library/Preferences/com.apple.HIToolbox.plist' + + PLIST_KEYS = ['AppleCurrentKeyboardLayoutInputSourceID'] + + def ParseKey(self, key, key_name): + """Determines the keyboard layout.""" + value = super(MacOSXKeyboard, self).ParseKey(key, key_name) + if type(value) in (list, tuple): + value = value[0] + _, _, keyboard_layout = value.rpartition('.') + + return keyboard_layout + + +class MacOSXTimeZone(interface.PreprocessPlugin): + """Gather timezone information from a Mac OS X system.""" + + ATTRIBUTE = 'time_zone_str' + SUPPORTED_OS = ['MacOSX'] + + WEIGHT = 1 + + ZONE_FILE_PATH = u'/private/etc/localtime' + + def GetValue(self, searcher, unused_knowledge_base): + """Determines the local time zone settings. + + Args: + searcher: The file system searcher object (instance of + dfvfs.FileSystemSearcher). + knowledge_base: A knowledge base object (instance of KnowledgeBase), + which contains information from the source data needed + for parsing. + + Returns: + The local timezone settings. + + Raises: + errors.PreProcessFail: if the preprocessing fails. + """ + path = self.ZONE_FILE_PATH + file_entry = self._FindFileEntry(searcher, path) + if not file_entry: + raise errors.PreProcessFail( + u'Unable to find file: {0:s}'.format(path)) + + if not file_entry.link: + raise errors.PreProcessFail( + u'Unable to retrieve timezone information from: {0:s}.'.format(path)) + + _, _, zone = file_entry.link.partition(u'zoneinfo/') + return zone + + +class MacOSXUsers(interface.PreprocessPlugin): + """Get information about user accounts on a Mac OS X system.""" + + SUPPORTED_OS = ['MacOSX'] + ATTRIBUTE = 'users' + WEIGHT = 1 + + # Define the path to the user account information. + USER_PATH = '/private/var/db/dslocal/nodes/Default/users/[^_].+.plist' + + _KEYS = frozenset(['name', 'uid', 'home', 'realname']) + + def _OpenPlistFile(self, searcher, path_spec): + """Open a Plist file given a path and returns a plist top level object. + + Args: + searcher: The file system searcher object (instance of + dfvfs.FileSystemSearcher). + path_spec: The path specification (instance of dfvfs.PathSpec) + of the plist file. + + Raises: + errors.PreProcessFail: if the preprocessing fails. + """ + plist_file_location = getattr(path_spec, 'location', u'') + file_entry = searcher.GetFileEntryByPathSpec(path_spec) + file_object = file_entry.GetFileObject() + + try: + plist_file = binplist.BinaryPlist(file_object) + top_level_object = plist_file.Parse() + + except binplist.FormatError as exception: + exception = utils.GetUnicodeString(exception) + raise errors.PreProcessFail( + u'File is not a plist: {0:s}'.format(exception)) + + except OverflowError as exception: + raise errors.PreProcessFail( + u'Error processing: {0:s} with error: {1:s}'.format( + plist_file_location, exception)) + + if not plist_file: + raise errors.PreProcessFail( + u'File is not a plist: {0:s}'.format(plist_file_location)) + + return top_level_object + + def GetValue(self, searcher, unused_knowledge_base): + """Determines the user accounts. + + Args: + searcher: The file system searcher object (instance of + dfvfs.FileSystemSearcher). + knowledge_base: A knowledge base object (instance of KnowledgeBase), + which contains information from the source data needed + for parsing. + + Returns: + A list containing username information dicts. + + Raises: + errors.PreProcessFail: if the preprocessing fails. + """ + find_spec = file_system_searcher.FindSpec( + location_regex=self.USER_PATH, case_sensitive=False) + + path_specs = list(searcher.Find(find_specs=[find_spec])) + if not path_specs: + raise errors.PreProcessFail(u'Unable to find user plist files.') + + users = [] + for path_spec in path_specs: + plist_file_location = getattr(path_spec, 'location', u'') + if not plist_file_location: + raise errors.PreProcessFail(u'Missing user plist file location.') + + try: + top_level_object = self._OpenPlistFile(searcher, path_spec) + except IOError: + logging.warning(u'Unable to parse user plist file: {0:s}'.format( + plist_file_location)) + continue + + try: + match = plist_interface.GetKeysDefaultEmpty( + top_level_object, self._KEYS) + except KeyError as exception: + logging.warning( + u'Unable to read user plist file: {0:s} with error: {1:s}'.format( + plist_file_location, exception)) + continue + + # TODO: as part of artifacts, create a proper object for this. + user = { + 'uid': match.get('uid', [-1])[0], + 'path': match.get('home', [u''])[0], + 'name': match.get('name', [u''])[0], + 'realname': match.get('realname', [u'N/A'])[0]} + users.append(user) + + if not users: + raise errors.PreProcessFail(u'Unable to find any users on the system.') + + return users + + +manager.PreprocessPluginsManager.RegisterPlugins([ + MacOSXBuild, MacOSXHostname, MacOSXKeyboard, MacOSXTimeZone, MacOSXUsers]) diff --git a/plaso/preprocessors/macosx_test.py b/plaso/preprocessors/macosx_test.py new file mode 100644 index 0000000..5e80c7b --- /dev/null +++ b/plaso/preprocessors/macosx_test.py @@ -0,0 +1,221 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Mac OS X preprocess plug-ins.""" + +import os +import unittest + +from dfvfs.helpers import file_system_searcher +from dfvfs.path import fake_path_spec + +from plaso.artifacts import knowledge_base +from plaso.preprocessors import macosx +from plaso.preprocessors import test_lib + + +class MacOSXBuildTest(test_lib.PreprocessPluginTest): + """Tests for the Mac OS X build information preprocess plug-in object.""" + + _FILE_DATA = ( + '\n' + '\n' + '\n' + '\n' + '\tProductBuildVersion\n' + '\t13C64\n' + '\tProductCopyright\n' + '\t1983-2014 Apple Inc.\n' + '\tProductName\n' + '\tMac OS X\n' + '\tProductUserVisibleVersion\n' + '\t10.9.2\n' + '\tProductVersion\n' + '\t10.9.2\n' + '\n' + '\n') + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._fake_file_system = self._BuildSingleFileFakeFileSystem( + u'/System/Library/CoreServices/SystemVersion.plist', + self._FILE_DATA) + + mount_point = fake_path_spec.FakePathSpec(location=u'/') + self._searcher = file_system_searcher.FileSystemSearcher( + self._fake_file_system, mount_point) + + def testGetValue(self): + """Tests the GetValue function.""" + knowledge_base_object = knowledge_base.KnowledgeBase() + + plugin = macosx.MacOSXBuild() + plugin.Run(self._searcher, knowledge_base_object) + + build = knowledge_base_object.GetValue('build') + self.assertEquals(build, u'10.9.2') + + +class MacOSXHostname(test_lib.PreprocessPluginTest): + """Tests for the Mac OS X hostname preprocess plug-in object.""" + + # Note that is only part of the normal preferences.plist file data. + _FILE_DATA = ( + '\n' + '\n' + '\n' + '\n' + '\tSystem\n' + '\t\n' + '\t\tNetwork\n' + '\t\t\n' + '\t\t\tHostNames\n' + '\t\t\t\n' + '\t\t\t\tLocalHostName\n' + '\t\t\t\tPlaso\'s Mac mini\n' + '\t\t\t\n' + '\t\t\n' + '\t\tSystem\n' + '\t\t\n' + '\t\t\tComputerName\n' + '\t\t\tPlaso\'s Mac mini\n' + '\t\t\tComputerNameEncoding\n' + '\t\t\t0\n' + '\t\t\n' + '\t\n' + '\n' + '\n') + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._fake_file_system = self._BuildSingleFileFakeFileSystem( + u'/Library/Preferences/SystemConfiguration/preferences.plist', + self._FILE_DATA) + + mount_point = fake_path_spec.FakePathSpec(location=u'/') + self._searcher = file_system_searcher.FileSystemSearcher( + self._fake_file_system, mount_point) + + def testGetValue(self): + """Tests the GetValue function.""" + knowledge_base_object = knowledge_base.KnowledgeBase() + + plugin = macosx.MacOSXHostname() + plugin.Run(self._searcher, knowledge_base_object) + + self.assertEquals(knowledge_base_object.hostname, u'Plaso\'s Mac mini') + + +class MacOSXKeyboard(test_lib.PreprocessPluginTest): + """Tests for the Mac OS X keyboard layout preprocess plug-in object.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + file_object = open(os.path.join( + self._TEST_DATA_PATH, u'com.apple.HIToolbox.plist')) + file_data = file_object.read() + file_object.close() + + self._fake_file_system = self._BuildSingleFileFakeFileSystem( + u'/Library/Preferences/com.apple.HIToolbox.plist', + file_data) + + mount_point = fake_path_spec.FakePathSpec(location=u'/') + self._searcher = file_system_searcher.FileSystemSearcher( + self._fake_file_system, mount_point) + + def testGetValue(self): + """Tests the GetValue function.""" + knowledge_base_object = knowledge_base.KnowledgeBase() + + plugin = macosx.MacOSXKeyboard() + plugin.Run(self._searcher, knowledge_base_object) + + keyboard_layout = knowledge_base_object.GetValue('keyboard_layout') + self.assertEquals(keyboard_layout, u'US') + + +class MacOSXTimezone(test_lib.PreprocessPluginTest): + """Tests for the Mac OS X timezone preprocess plug-in object.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._fake_file_system = self._BuildSingleLinkFakeFileSystem( + u'/private/etc/localtime', u'/usr/share/zoneinfo/Europe/Amsterdam') + + mount_point = fake_path_spec.FakePathSpec(location=u'/') + self._searcher = file_system_searcher.FileSystemSearcher( + self._fake_file_system, mount_point) + + def testGetValue(self): + """Tests the GetValue function.""" + knowledge_base_object = knowledge_base.KnowledgeBase() + + plugin = macosx.MacOSXTimeZone() + plugin.Run(self._searcher, knowledge_base_object) + + time_zone_str = knowledge_base_object.GetValue('time_zone_str') + self.assertEquals(time_zone_str, u'Europe/Amsterdam') + + +class MacOSXUsersTest(test_lib.PreprocessPluginTest): + """Tests for the Mac OS X usernames preprocess plug-in object.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + file_object = open(os.path.join( + self._TEST_DATA_PATH, u'com.apple.HIToolbox.plist')) + file_data = file_object.read() + file_object.close() + + self._fake_file_system = self._BuildSingleFileFakeFileSystem( + u'/private/var/db/dslocal/nodes/Default/users/nobody.plist', + file_data) + + mount_point = fake_path_spec.FakePathSpec(location=u'/') + self._searcher = file_system_searcher.FileSystemSearcher( + self._fake_file_system, mount_point) + + def testGetValue(self): + """Tests the GetValue function.""" + knowledge_base_object = knowledge_base.KnowledgeBase() + + plugin = macosx.MacOSXUsers() + plugin.Run(self._searcher, knowledge_base_object) + + users = knowledge_base_object.GetValue('users') + self.assertEquals(len(users), 1) + + # TODO: fix the parsing of the following values to match the behavior on + # Mac OS X. + + # The string -2 is converted into the integer -1. + self.assertEquals(users[0].get('uid', None), -1) + # 'home' is 0 which represents: /var/empty but we convert it + # into u''. + self.assertEquals(users[0].get('path', None), u'') + # 'name' is 0 which represents: nobody but we convert it into u''. + self.assertEquals(users[0].get('name', None), u'') + # 'realname' is 0 which represents: 'Unprivileged User' but we convert it + # into u'N/A'. + self.assertEquals(users[0].get('realname', None), u'N/A') + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/preprocessors/manager.py b/plaso/preprocessors/manager.py new file mode 100644 index 0000000..d70d46e --- /dev/null +++ b/plaso/preprocessors/manager.py @@ -0,0 +1,138 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The preprocess plugins manager.""" + +import logging + +from plaso.lib import errors + + +class PreprocessPluginsManager(object): + """Class that implements the preprocess plugins manager.""" + + _plugin_classes = {} + + @classmethod + def _GetPluginsByWeight(cls, platform, weight): + """Returns all plugins for a specific platform of a certain weight. + + Args: + platform: A string containing the supported operating system + of the plugin. + weight: An integer containing the weight of the plugin. + + Yields: + A preprocess plugin objects that matches the platform and weight. + """ + for plugin_class in cls._plugin_classes.itervalues(): + plugin_supported_os = getattr(plugin_class, 'SUPPORTED_OS', []) + plugin_weight = getattr(plugin_class, 'WEIGHT', 0) + if platform in plugin_supported_os and weight == plugin_weight: + yield plugin_class() + + @classmethod + def _GetWeights(cls, platform): + """Returns a list of all weights that are used by preprocessing plugins. + + Args: + platform: A string containing the supported operating system + of the plugin. + + Returns: + A list of weights. + """ + weights = {} + for plugin_class in cls._plugin_classes.itervalues(): + plugin_supported_os = getattr(plugin_class, 'SUPPORTED_OS', []) + plugin_weight = getattr(plugin_class, 'WEIGHT', 0) + if platform in plugin_supported_os: + weights[plugin_weight] = 1 + + return sorted(weights.keys()) + + @classmethod + def DeregisterPlugin(cls, plugin_class): + """Deregisters a plugin class. + + Args: + plugin_class: the class object of the plugin. + + Raises: + KeyError: if plugin class is not set for the corresponding name. + """ + if plugin_class.__name__ not in cls._plugin_classes: + raise KeyError( + u'Plugin class not set for name: {0:s}.'.format( + plugin_class.__name__)) + + del cls._plugin_classes[plugin_class.__name__] + + @classmethod + def RegisterPlugin(cls, plugin_class): + """Registers a plugin class. + + Args: + plugin_class: the class object of the plugin. + + Raises: + KeyError: if plugin class is already set for the corresponding name. + """ + if plugin_class.__name__ in cls._plugin_classes: + raise KeyError(( + u'Plugin class already set for name: {0:s}.').format( + plugin_class.__name__)) + + cls._plugin_classes[plugin_class.__name__] = plugin_class + + @classmethod + def RegisterPlugins(cls, plugin_classes): + """Registers a plugin classes. + + Args: + plugin_classes: a list of class objects of the plugins. + + Raises: + KeyError: if plugin class is already set for the corresponding name. + """ + for plugin_class in plugin_classes: + cls.RegisterPlugin(plugin_class) + + @classmethod + def RunPlugins(cls, platform, searcher, knowledge_base): + """Runs the plugins for a specific platform. + + Args: + platform: A string containing the supported operating system + of the plugin. + searcher: The file system searcher object (instance of + dfvfs.FileSystemSearcher). + knowledge_base: A knowledge base object (instance of KnowledgeBase), + which contains information from the source data needed + for parsing. + """ + for weight in cls._GetWeights(platform): + for plugin_object in cls._GetPluginsByWeight(platform, weight): + try: + plugin_object.Run(searcher, knowledge_base) + + except (IOError, errors.PreProcessFail) as exception: + logging.warning(( + u'Unable to run preprocessor: {0:s} for attribute: {1:s} ' + u'with error: {2:s}').format( + plugin_object.plugin_name, plugin_object.ATTRIBUTE, + exception)) diff --git a/plaso/preprocessors/manager_test.py b/plaso/preprocessors/manager_test.py new file mode 100644 index 0000000..4fdf638 --- /dev/null +++ b/plaso/preprocessors/manager_test.py @@ -0,0 +1,71 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the preprocess plugins manager.""" + +import unittest + +from plaso.preprocessors import interface +from plaso.preprocessors import manager + + +class TestPreprocessPlugin(interface.PreprocessPlugin): + """Preprocess test plugin.""" + + def GetValue(self, searcher, unused_knowledge_base): + """Returns the path as found by the searcher. + + Args: + searcher: The file system searcher object (instance of + dfvfs.FileSystemSearcher). + knowledge_base: A knowledge base object (instance of KnowledgeBase), + which contains information from the source data needed + for parsing. + + Returns: + The first path location string. + + Raises: + PreProcessFail: if the path could not be found. + """ + return + + +class PreprocessPluginsManagerTest(unittest.TestCase): + """Tests for the preprocess plugins manager.""" + + def testRegistration(self): + """Tests the RegisterPlugin and DeregisterPlugin functions.""" + # pylint: disable=protected-access + number_of_plugins = len(manager.PreprocessPluginsManager._plugin_classes) + + manager.PreprocessPluginsManager.RegisterPlugin(TestPreprocessPlugin) + self.assertEquals( + len(manager.PreprocessPluginsManager._plugin_classes), + number_of_plugins + 1) + + with self.assertRaises(KeyError): + manager.PreprocessPluginsManager.RegisterPlugin(TestPreprocessPlugin) + + manager.PreprocessPluginsManager.DeregisterPlugin(TestPreprocessPlugin) + self.assertEquals( + len(manager.PreprocessPluginsManager._plugin_classes), + number_of_plugins) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/preprocessors/test_lib.py b/plaso/preprocessors/test_lib.py new file mode 100644 index 0000000..03ec7a8 --- /dev/null +++ b/plaso/preprocessors/test_lib.py @@ -0,0 +1,91 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Preprocess plug-in related functions and classes for testing.""" + +import os +import unittest + +from dfvfs.lib import definitions as dfvfs_definitions +from dfvfs.resolver import context +from dfvfs.vfs import fake_file_system + + +class PreprocessPluginTest(unittest.TestCase): + """The unit test case for a preprocess plug-in object.""" + + _TEST_DATA_PATH = os.path.join(os.getcwd(), 'test_data') + + def _BuildSingleFileFakeFileSystem(self, path, file_data): + """Builds a single file fake file system. + + Args: + path: The path of the file. + file_data: The data of the file. + + Returns: + The fake file system (instance of dvfvs.FakeFileSystem). + """ + resolver_context = context.Context() + file_system = fake_file_system.FakeFileSystem( + resolver_context) + + file_system.AddFileEntry( + u'/', file_entry_type=dfvfs_definitions.FILE_ENTRY_TYPE_DIRECTORY) + + path_segments = path.split(u'/') + for segment_index in range(2, len(path_segments)): + path_segment = u'{0:s}'.format( + u'/'.join(path_segments[:segment_index])) + file_system.AddFileEntry( + path_segment, + file_entry_type=dfvfs_definitions.FILE_ENTRY_TYPE_DIRECTORY) + + file_system.AddFileEntry(path, file_data=file_data) + + return file_system + + def _BuildSingleLinkFakeFileSystem(self, path, linked_path): + """Builds a single link fake file system. + + Args: + path: The path of the link. + linked_path: The path that is linked. + + Returns: + The fake file system (instance of dvfvs.FakeFileSystem). + """ + resolver_context = context.Context() + file_system = fake_file_system.FakeFileSystem( + resolver_context) + + file_system.AddFileEntry( + u'/', file_entry_type=dfvfs_definitions.FILE_ENTRY_TYPE_DIRECTORY) + + path_segments = path.split(u'/') + for segment_index in range(2, len(path_segments)): + path_segment = u'{0:s}'.format( + u'/'.join(path_segments[:segment_index])) + file_system.AddFileEntry( + path_segment, + file_entry_type=dfvfs_definitions.FILE_ENTRY_TYPE_DIRECTORY) + + file_system.AddFileEntry( + path, file_entry_type=dfvfs_definitions.FILE_ENTRY_TYPE_LINK, + link_data=linked_path) + + return file_system diff --git a/plaso/preprocessors/windows.py b/plaso/preprocessors/windows.py new file mode 100644 index 0000000..a280424 --- /dev/null +++ b/plaso/preprocessors/windows.py @@ -0,0 +1,556 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains preprocessors for Windows.""" + +import abc +import logging + +from dfvfs.helpers import file_system_searcher + +from plaso.lib import errors +from plaso.preprocessors import interface +from plaso.preprocessors import manager +from plaso.winreg import cache +from plaso.winreg import path_expander as winreg_path_expander +from plaso.winreg import utils +from plaso.winreg import winregistry + + +class WindowsRegistryPreprocessPlugin(interface.PreprocessPlugin): + """Class that defines the Windows Registry preprocess plugin object. + + By default registry needs information about system paths, which excludes + them to run in priority 1, in some cases they may need to run in priority + 3, for instance if the Registry key is dependent on which version of Windows + is running, information that is collected during priority 2. + """ + __abstract = True + + SUPPORTED_OS = ['Windows'] + WEIGHT = 2 + + REG_KEY = '\\' + REG_PATH = '{sysregistry}' + REG_FILE = 'SOFTWARE' + + def __init__(self): + """Initializes the Window Registry preprocess plugin object.""" + super(WindowsRegistryPreprocessPlugin, self).__init__() + self._file_path_expander = winreg_path_expander.WinRegistryKeyPathExpander() + self._key_path_expander = None + + def GetValue(self, searcher, knowledge_base): + """Returns a value gathered from a Registry key for preprocessing. + + Args: + searcher: The file system searcher object (instance of + dfvfs.FileSystemSearcher). + knowledge_base: A knowledge base object (instance of KnowledgeBase), + which contains information from the source data needed + for parsing. + + Raises: + errors.PreProcessFail: If the preprocessing fails. + """ + # TODO: optimize this in one find. + try: + # TODO: do not pass the full pre_obj here but just the necessary values. + path = self._file_path_expander.ExpandPath( + self.REG_PATH, pre_obj=knowledge_base.pre_obj) + except KeyError: + path = u'' + + if not path: + raise errors.PreProcessFail( + u'Unable to expand path: {0:s}'.format(self.REG_PATH)) + + find_spec = file_system_searcher.FindSpec( + location=path, case_sensitive=False) + path_specs = list(searcher.Find(find_specs=[find_spec])) + + if not path_specs or len(path_specs) != 1: + raise errors.PreProcessFail( + u'Unable to find directory: {0:s}'.format(self.REG_PATH)) + + directory_location = searcher.GetRelativePath(path_specs[0]) + if not directory_location: + raise errors.PreProcessFail( + u'Missing directory location for: {0:s}'.format(self.REG_PATH)) + + # The path is split in segments to make it path segement separator + # independent (and thus platform independent). + path_segments = searcher.SplitPath(directory_location) + path_segments.append(self.REG_FILE) + + find_spec = file_system_searcher.FindSpec( + location=path_segments, case_sensitive=False) + path_specs = list(searcher.Find(find_specs=[find_spec])) + + if not path_specs: + raise errors.PreProcessFail( + u'Unable to find file: {0:s} in directory: {1:s}'.format( + self.REG_FILE, directory_location)) + + if len(path_specs) != 1: + raise errors.PreProcessFail(( + u'Find for file: {1:s} in directory: {0:s} returned {2:d} ' + u'results.').format( + self.REG_FILE, directory_location, len(path_specs))) + + file_location = getattr(path_specs[0], 'location', None) + if not directory_location: + raise errors.PreProcessFail( + u'Missing file location for: {0:s} in directory: {1:s}'.format( + self.REG_FILE, directory_location)) + + try: + file_entry = searcher.GetFileEntryByPathSpec(path_specs[0]) + except IOError as exception: + raise errors.PreProcessFail( + u'Unable to open file entry: {0:s} with error: {1:s}'.format( + file_location, exception)) + + if not file_entry: + raise errors.PreProcessFail( + u'Unable to open file entry: {0:s}'.format(file_location)) + + # TODO: remove this check win_registry.OpenFile doesn't fail instead? + try: + file_object = file_entry.GetFileObject() + file_object.close() + except IOError as exception: + raise errors.PreProcessFail( + u'Unable to open file object: {0:s} with error: {1:s}'.format( + file_location, exception)) + + win_registry = winregistry.WinRegistry( + winregistry.WinRegistry.BACKEND_PYREGF) + + try: + winreg_file = win_registry.OpenFile( + file_entry, codepage=knowledge_base.codepage) + except IOError as exception: + raise errors.PreProcessFail( + u'Unable to open Registry file: {0:s} with error: {1:s}'.format( + file_location, exception)) + + self.winreg_file = winreg_file + + if not self._key_path_expander: + # TODO: it is more efficient to have one cache that is passed to every + # plugin, or maybe one path expander. Or replace the path expander by + # dfvfs WindowsPathResolver? + reg_cache = cache.WinRegistryCache() + reg_cache.BuildCache(winreg_file, self.REG_FILE) + self._key_path_expander = winreg_path_expander.WinRegistryKeyPathExpander( + reg_cache=reg_cache) + + try: + # TODO: do not pass the full pre_obj here but just the necessary values. + key_path = self._key_path_expander.ExpandPath( + self.REG_KEY, pre_obj=knowledge_base.pre_obj) + except KeyError: + key_path = u'' + + if not key_path: + raise errors.PreProcessFail( + u'Unable to expand path: {0:s}'.format(self.REG_KEY)) + + try: + key = winreg_file.GetKeyByPath(key_path) + except IOError as exception: + raise errors.PreProcessFail( + u'Unable to fetch Registry key: {0:s} with error: {1:s}'.format( + key_path, exception)) + + if not key: + raise errors.PreProcessFail( + u'Registry key {0:s} does not exist.'.format(self.REG_KEY)) + + return self.ParseKey(key) + + @abc.abstractmethod + def ParseKey(self, key): + """Extract information from a Registry key and save in storage.""" + + +class WindowsCodepage(WindowsRegistryPreprocessPlugin): + """A preprocessing class that fetches codepage information.""" + + # Defines the preprocess attribute to be set. + ATTRIBUTE = 'code_page' + + # Depend upon the current control set, thus lower the priority. + WEIGHT = 3 + + REG_KEY = '{current_control_set}\\Control\\Nls\\CodePage' + REG_FILE = 'SYSTEM' + + def ParseKey(self, key): + """Retrieves the codepage or cp1252 by default.""" + value = key.GetValue('ACP') + if value and type(value.data) == unicode: + return u'cp{0:s}'.format(value.data) + + logging.warning( + u'Unable to determine ASCII string codepage, defaulting to cp1252.') + + return u'cp1252' + + +class WindowsHostname(WindowsRegistryPreprocessPlugin): + """A preprocessing class that fetches the hostname information.""" + + ATTRIBUTE = 'hostname' + + # Depend upon the current control set to be found. + WEIGHT = 3 + + REG_KEY = '{current_control_set}\\Control\\ComputerName\\ComputerName' + REG_FILE = 'SYSTEM' + + def ParseKey(self, key): + """Extract the hostname from the registry.""" + value = key.GetValue('ComputerName') + if value and type(value.data) == unicode: + return value.data + + +class WindowsProgramFilesPath(WindowsRegistryPreprocessPlugin): + """Fetch about the location for the Program Files directory.""" + + ATTRIBUTE = 'programfiles' + + REGFILE = 'SOFTWARE' + REG_KEY = '\\Microsoft\\Windows\\CurrentVersion' + + def ParseKey(self, key): + """Extract the version information from the key.""" + value = key.GetValue('ProgramFilesDir') + if value: + # Remove the first drive letter, eg: "C:\Program Files". + return u'{0:s}'.format(value.data.partition('\\')[2]) + + +class WindowsProgramFilesX86Path(WindowsRegistryPreprocessPlugin): + """Fetch about the location for the Program Files directory.""" + + ATTRIBUTE = 'programfilesx86' + + REGFILE = 'SOFTWARE' + REG_KEY = '\\Microsoft\\Windows\\CurrentVersion' + + def ParseKey(self, key): + """Extract the version information from the key.""" + value = key.GetValue(u'ProgramFilesDir (x86)') + if value: + # Remove the first drive letter, eg: "C:\Program Files". + return u'{0:s}'.format(value.data.partition('\\')[2]) + + +class WindowsSystemRegistryPath(interface.PathPreprocessPlugin): + """Get the system registry path.""" + SUPPORTED_OS = ['Windows'] + ATTRIBUTE = 'sysregistry' + PATH = '/(Windows|WinNT|WINNT35|WTSRV)/System32/config' + + +class WindowsSystemRootPath(interface.PathPreprocessPlugin): + """Get the system root path.""" + SUPPORTED_OS = ['Windows'] + ATTRIBUTE = 'systemroot' + PATH = '/(Windows|WinNT|WINNT35|WTSRV)' + + +class WindowsTimeZone(WindowsRegistryPreprocessPlugin): + """A preprocessing class that fetches timezone information.""" + + # Defines the preprocess attribute to be set. + ATTRIBUTE = 'time_zone_str' + + # Depend upon the current control set, thus lower the priority. + WEIGHT = 3 + + REG_KEY = '{current_control_set}\\Control\\TimeZoneInformation' + REG_FILE = 'SYSTEM' + + # transform gathered from these sources: + # Prebuilt from: + # HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\ + ZONE_LIST = { + 'IndiaStandardTime': 'Asia/Kolkata', + 'EasternStandardTime': 'EST5EDT', + 'EasternDaylightTime': 'EST5EDT', + 'MountainStandardTime': 'MST7MDT', + 'MountainDaylightTime': 'MST7MDT', + 'PacificStandardTime': 'PST8PDT', + 'PacificDaylightTime': 'PST8PDT', + 'CentralStandardTime': 'CST6CDT', + 'CentralDaylightTime': 'CST6CDT', + 'SamoaStandardTime': 'US/Samoa', + 'HawaiianStandardTime': 'US/Hawaii', + 'AlaskanStandardTime': 'US/Alaska', + 'MexicoStandardTime2': 'MST7MDT', + 'USMountainStandardTime': 'MST7MDT', + 'CanadaCentralStandardTime': 'CST6CDT', + 'MexicoStandardTime': 'CST6CDT', + 'CentralAmericaStandardTime': 'CST6CDT', + 'USEasternStandardTime': 'EST5EDT', + 'SAPacificStandardTime': 'EST5EDT', + 'MalayPeninsulaStandardTime': 'Asia/Kuching', + 'PacificSAStandardTime': 'Canada/Atlantic', + 'AtlanticStandardTime': 'Canada/Atlantic', + 'SAWesternStandardTime': 'Canada/Atlantic', + 'NewfoundlandStandardTime': 'Canada/Newfoundland', + 'AzoresStandardTime': 'Atlantic/Azores', + 'CapeVerdeStandardTime': 'Atlantic/Azores', + 'GMTStandardTime': 'GMT', + 'GreenwichStandardTime': 'GMT', + 'W.CentralAfricaStandardTime': 'Europe/Belgrade', + 'W.EuropeStandardTime': 'Europe/Belgrade', + 'CentralEuropeStandardTime': 'Europe/Belgrade', + 'RomanceStandardTime': 'Europe/Belgrade', + 'CentralEuropeanStandardTime': 'Europe/Belgrade', + 'E.EuropeStandardTime': 'Egypt', + 'SouthAfricaStandardTime': 'Egypt', + 'IsraelStandardTime': 'Egypt', + 'EgyptStandardTime': 'Egypt', + 'NorthAsiaEastStandardTime': 'Asia/Bangkok', + 'SingaporeStandardTime': 'Asia/Bangkok', + 'ChinaStandardTime': 'Asia/Bangkok', + 'W.AustraliaStandardTime': 'Australia/Perth', + 'TaipeiStandardTime': 'Asia/Bangkok', + 'TokyoStandardTime': 'Asia/Tokyo', + 'KoreaStandardTime': 'Asia/Seoul', + '@tzres.dll,-10': 'Atlantic/Azores', + '@tzres.dll,-11': 'Atlantic/Azores', + '@tzres.dll,-12': 'Atlantic/Azores', + '@tzres.dll,-20': 'Atlantic/Cape_Verde', + '@tzres.dll,-21': 'Atlantic/Cape_Verde', + '@tzres.dll,-22': 'Atlantic/Cape_Verde', + '@tzres.dll,-40': 'Brazil/East', + '@tzres.dll,-41': 'Brazil/East', + '@tzres.dll,-42': 'Brazil/East', + '@tzres.dll,-70': 'Canada/Newfoundland', + '@tzres.dll,-71': 'Canada/Newfoundland', + '@tzres.dll,-72': 'Canada/Newfoundland', + '@tzres.dll,-80': 'Canada/Atlantic', + '@tzres.dll,-81': 'Canada/Atlantic', + '@tzres.dll,-82': 'Canada/Atlantic', + '@tzres.dll,-104': 'America/Cuiaba', + '@tzres.dll,-105': 'America/Cuiaba', + '@tzres.dll,-110': 'EST5EDT', + '@tzres.dll,-111': 'EST5EDT', + '@tzres.dll,-112': 'EST5EDT', + '@tzres.dll,-120': 'EST5EDT', + '@tzres.dll,-121': 'EST5EDT', + '@tzres.dll,-122': 'EST5EDT', + '@tzres.dll,-130': 'EST5EDT', + '@tzres.dll,-131': 'EST5EDT', + '@tzres.dll,-132': 'EST5EDT', + '@tzres.dll,-140': 'CST6CDT', + '@tzres.dll,-141': 'CST6CDT', + '@tzres.dll,-142': 'CST6CDT', + '@tzres.dll,-150': 'America/Guatemala', + '@tzres.dll,-151': 'America/Guatemala', + '@tzres.dll,-152': 'America/Guatemala', + '@tzres.dll,-160': 'CST6CDT', + '@tzres.dll,-161': 'CST6CDT', + '@tzres.dll,-162': 'CST6CDT', + '@tzres.dll,-170': 'America/Mexico_City', + '@tzres.dll,-171': 'America/Mexico_City', + '@tzres.dll,-172': 'America/Mexico_City', + '@tzres.dll,-180': 'MST7MDT', + '@tzres.dll,-181': 'MST7MDT', + '@tzres.dll,-182': 'MST7MDT', + '@tzres.dll,-190': 'MST7MDT', + '@tzres.dll,-191': 'MST7MDT', + '@tzres.dll,-192': 'MST7MDT', + '@tzres.dll,-200': 'MST7MDT', + '@tzres.dll,-201': 'MST7MDT', + '@tzres.dll,-202': 'MST7MDT', + '@tzres.dll,-210': 'PST8PDT', + '@tzres.dll,-211': 'PST8PDT', + '@tzres.dll,-212': 'PST8PDT', + '@tzres.dll,-220': 'US/Alaska', + '@tzres.dll,-221': 'US/Alaska', + '@tzres.dll,-222': 'US/Alaska', + '@tzres.dll,-230': 'US/Hawaii', + '@tzres.dll,-231': 'US/Hawaii', + '@tzres.dll,-232': 'US/Hawaii', + '@tzres.dll,-260': 'GMT', + '@tzres.dll,-261': 'GMT', + '@tzres.dll,-262': 'GMT', + '@tzres.dll,-271': 'UTC', + '@tzres.dll,-272': 'UTC', + '@tzres.dll,-280': 'Europe/Budapest', + '@tzres.dll,-281': 'Europe/Budapest', + '@tzres.dll,-282': 'Europe/Budapest', + '@tzres.dll,-290': 'Europe/Warsaw', + '@tzres.dll,-291': 'Europe/Warsaw', + '@tzres.dll,-292': 'Europe/Warsaw', + '@tzres.dll,-331': 'Europe/Nicosia', + '@tzres.dll,-332': 'Europe/Nicosia', + '@tzres.dll,-340': 'Africa/Cairo', + '@tzres.dll,-341': 'Africa/Cairo', + '@tzres.dll,-342': 'Africa/Cairo', + '@tzres.dll,-350': 'Europe/Sofia', + '@tzres.dll,-351': 'Europe/Sofia', + '@tzres.dll,-352': 'Europe/Sofia', + '@tzres.dll,-365': 'Egypt', + '@tzres.dll,-390': 'Asia/Kuwait', + '@tzres.dll,-391': 'Asia/Kuwait', + '@tzres.dll,-392': 'Asia/Kuwait', + '@tzres.dll,-400': 'Asia/Baghdad', + '@tzres.dll,-401': 'Asia/Baghdad', + '@tzres.dll,-402': 'Asia/Baghdad', + '@tzres.dll,-410': 'Africa/Nairobi', + '@tzres.dll,-411': 'Africa/Nairobi', + '@tzres.dll,-412': 'Africa/Nairobi', + '@tzres.dll,-434': 'Asia/Tbilisi', + '@tzres.dll,-435': 'Asia/Tbilisi', + '@tzres.dll,-440': 'Asia/Muscat', + '@tzres.dll,-441': 'Asia/Muscat', + '@tzres.dll,-442': 'Asia/Muscat', + '@tzres.dll,-447': 'Asia/Baku', + '@tzres.dll,-448': 'Asia/Baku', + '@tzres.dll,-449': 'Asia/Baku', + '@tzres.dll,-450': 'Asia/Yerevan', + '@tzres.dll,-451': 'Asia/Yerevan', + '@tzres.dll,-452': 'Asia/Yerevan', + '@tzres.dll,-460': 'Asia/Kabul', + '@tzres.dll,-461': 'Asia/Kabul', + '@tzres.dll,-462': 'Asia/Kabul', + '@tzres.dll,-471': 'Asia/Yekaterinburg', + '@tzres.dll,-472': 'Asia/Yekaterinburg', + '@tzres.dll,-480': 'Asia/Karachi', + '@tzres.dll,-481': 'Asia/Karachi', + '@tzres.dll,-482': 'Asia/Karachi', + '@tzres.dll,-490': 'Asia/Kolkata', + '@tzres.dll,-491': 'Asia/Kolkata', + '@tzres.dll,-492': 'Asia/Kolkata', + '@tzres.dll,-500': 'Asia/Kathmandu', + '@tzres.dll,-501': 'Asia/Kathmandu', + '@tzres.dll,-502': 'Asia/Kathmandu', + '@tzres.dll,-510': 'Asia/Dhaka', + '@tzres.dll,-511': 'Asia/Aqtau', + '@tzres.dll,-512': 'Asia/Aqtau', + '@tzres.dll,-570': 'Asia/Chongqing', + '@tzres.dll,-571': 'Asia/Chongqing', + '@tzres.dll,-572': 'Asia/Chongqing', + '@tzres.dll,-650': 'Australia/Darwin', + '@tzres.dll,-651': 'Australia/Darwin', + '@tzres.dll,-652': 'Australia/Darwin', + '@tzres.dll,-660': 'Australia/Adelaide', + '@tzres.dll,-661': 'Australia/Adelaide', + '@tzres.dll,-662': 'Australia/Adelaide', + '@tzres.dll,-670': 'Australia/Sydney', + '@tzres.dll,-671': 'Australia/Sydney', + '@tzres.dll,-672': 'Australia/Sydney', + '@tzres.dll,-680': 'Australia/Brisbane', + '@tzres.dll,-681': 'Australia/Brisbane', + '@tzres.dll,-682': 'Australia/Brisbane', + '@tzres.dll,-721': 'Pacific/Port_Moresby', + '@tzres.dll,-722': 'Pacific/Port_Moresby', + '@tzres.dll,-731': 'Pacific/Fiji', + '@tzres.dll,-732': 'Pacific/Fiji', + '@tzres.dll,-840': 'America/Argentina/Buenos_Aires', + '@tzres.dll,-841': 'America/Argentina/Buenos_Aires', + '@tzres.dll,-842': 'America/Argentina/Buenos_Aires', + '@tzres.dll,-880': 'UTC', + '@tzres.dll,-930': 'UTC', + '@tzres.dll,-931': 'UTC', + '@tzres.dll,-932': 'UTC', + '@tzres.dll,-1010': 'Asia/Aqtau', + '@tzres.dll,-1020': 'Asia/Dhaka', + '@tzres.dll,-1021': 'Asia/Dhaka', + '@tzres.dll,-1022': 'Asia/Dhaka', + '@tzres.dll,-1070': 'Asia/Tbilisi', + '@tzres.dll,-1120': 'America/Cuiaba', + '@tzres.dll,-1140': 'Pacific/Fiji', + '@tzres.dll,-1460': 'Pacific/Port_Moresby', + '@tzres.dll,-1530': 'Asia/Yekaterinburg', + '@tzres.dll,-1630': 'Europe/Nicosia', + '@tzres.dll,-1660': 'America/Bahia', + '@tzres.dll,-1661': 'America/Bahia', + '@tzres.dll,-1662': 'America/Bahia', + 'Central Standard Time': 'CST6CDT', + 'Pacific Standard Time': 'PST8PDT', + } + + def ParseKey(self, key): + """Extract timezone information from the registry.""" + value = key.GetValue('StandardName') + if value and type(value.data) == unicode: + # Do a mapping to a value defined as in the Olson database. + return self.ZONE_LIST.get(value.data.replace(' ', ''), value.data) + + +class WindowsUsers(WindowsRegistryPreprocessPlugin): + """Fetch information about user profiles.""" + + ATTRIBUTE = 'users' + + REG_FILE = 'SOFTWARE' + REG_KEY = '\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList' + + def ParseKey(self, key): + """Extract current control set information.""" + users = [] + + for sid in key.GetSubkeys(): + # TODO: as part of artifacts, create a proper object for this. + user = {} + user['sid'] = sid.name + value = sid.GetValue('ProfileImagePath') + if value: + user['path'] = value.data + user['name'] = utils.WinRegBasename(user['path']) + + users.append(user) + + return users + + +class WindowsVersion(WindowsRegistryPreprocessPlugin): + """Fetch information about the current Windows version.""" + + ATTRIBUTE = 'osversion' + + REGFILE = 'SOFTWARE' + REG_KEY = '\\Microsoft\\Windows NT\\CurrentVersion' + + def ParseKey(self, key): + """Extract the version information from the key.""" + value = key.GetValue('ProductName') + if value: + return u'{0:s}'.format(value.data) + + +class WindowsWinDirPath(interface.PathPreprocessPlugin): + """Get the system path.""" + SUPPORTED_OS = ['Windows'] + ATTRIBUTE = 'windir' + PATH = '/(Windows|WinNT|WINNT35|WTSRV)' + + +manager.PreprocessPluginsManager.RegisterPlugins([ + WindowsCodepage, WindowsHostname, WindowsProgramFilesPath, + WindowsProgramFilesX86Path, WindowsSystemRegistryPath, + WindowsSystemRootPath, WindowsTimeZone, WindowsUsers, WindowsVersion, + WindowsWinDirPath]) diff --git a/plaso/preprocessors/windows_test.py b/plaso/preprocessors/windows_test.py new file mode 100644 index 0000000..c69bcdf --- /dev/null +++ b/plaso/preprocessors/windows_test.py @@ -0,0 +1,265 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Windows preprocess plug-ins.""" + +import os +import unittest + +from dfvfs.helpers import file_system_searcher +from dfvfs.path import fake_path_spec + +from plaso.artifacts import knowledge_base +from plaso.preprocessors import windows +from plaso.preprocessors import test_lib + + +class WindowsSoftwareRegistryTest(test_lib.PreprocessPluginTest): + """Base class for tests that use the SOFTWARE Registry file.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + file_object = open(os.path.join( + self._TEST_DATA_PATH, u'SYSTEM'), 'rb') + file_data = file_object.read() + file_object.close() + + self._fake_file_system = self._BuildSingleFileFakeFileSystem( + u'/Windows/System32/config/SYSTEM', file_data) + + file_object = open(os.path.join( + self._TEST_DATA_PATH, u'SOFTWARE'), 'rb') + file_data = file_object.read() + file_object.close() + + self._fake_file_system.AddFileEntry( + u'/Windows/System32/config/SOFTWARE', file_data=file_data) + + mount_point = fake_path_spec.FakePathSpec(location=u'/') + self._searcher = file_system_searcher.FileSystemSearcher( + self._fake_file_system, mount_point) + + +class WindowsSystemRegistryTest(test_lib.PreprocessPluginTest): + """Base class for tests that use the SYSTEM Registry file.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + file_object = open(os.path.join( + self._TEST_DATA_PATH, u'SYSTEM'), 'rb') + file_data = file_object.read() + file_object.close() + + self._fake_file_system = self._BuildSingleFileFakeFileSystem( + u'/Windows/System32/config/SYSTEM', file_data) + + mount_point = fake_path_spec.FakePathSpec(location=u'/') + self._searcher = file_system_searcher.FileSystemSearcher( + self._fake_file_system, mount_point) + + +class WindowsCodepageTest(WindowsSystemRegistryTest): + """Tests for the Windows codepage preprocess plug-in object.""" + + def testGetValue(self): + """Tests the GetValue function.""" + knowledge_base_object = knowledge_base.KnowledgeBase() + + # The plug-in needs to expand {sysregistry} so we need to run + # the WindowsSystemRegistryPath plug-in first. + plugin = windows.WindowsSystemRegistryPath() + plugin.Run(self._searcher, knowledge_base_object) + + plugin = windows.WindowsCodepage() + plugin.Run(self._searcher, knowledge_base_object) + + self.assertEquals(knowledge_base_object.codepage, u'cp1252') + + +class WindowsHostnameTest(WindowsSystemRegistryTest): + """Tests for the Windows hostname preprocess plug-in object.""" + + def testGetValue(self): + """Tests the GetValue function.""" + knowledge_base_object = knowledge_base.KnowledgeBase() + + # The plug-in needs to expand {sysregistry} so we need to run + # the WindowsSystemRegistryPath plug-in first. + plugin = windows.WindowsSystemRegistryPath() + plugin.Run(self._searcher, knowledge_base_object) + + plugin = windows.WindowsHostname() + plugin.Run(self._searcher, knowledge_base_object) + + self.assertEquals(knowledge_base_object.hostname, u'WKS-WIN732BITA') + + +class WindowsProgramFilesPath(WindowsSoftwareRegistryTest): + """Tests for the Windows Program Files path preprocess plug-in object.""" + + def testGetValue(self): + """Tests the GetValue function.""" + knowledge_base_object = knowledge_base.KnowledgeBase() + + # The plug-in needs to expand {sysregistry} so we need to run + # the WindowsSystemRegistryPath plug-in first. + plugin = windows.WindowsSystemRegistryPath() + plugin.Run(self._searcher, knowledge_base_object) + + plugin = windows.WindowsProgramFilesPath() + plugin.Run(self._searcher, knowledge_base_object) + + path = knowledge_base_object.GetValue('programfiles') + self.assertEquals(path, u'Program Files') + + +class WindowsProgramFilesX86Path(WindowsSoftwareRegistryTest): + """Tests for the Windows Program Files X86 path preprocess plug-in object.""" + + def testGetValue(self): + """Tests the GetValue function.""" + knowledge_base_object = knowledge_base.KnowledgeBase() + + # The plug-in needs to expand {sysregistry} so we need to run + # the WindowsSystemRegistryPath plug-in first. + plugin = windows.WindowsSystemRegistryPath() + plugin.Run(self._searcher, knowledge_base_object) + + plugin = windows.WindowsProgramFilesX86Path() + + plugin.Run(self._searcher, knowledge_base_object) + + path = knowledge_base_object.GetValue('programfilesx86') + # The test SOFTWARE Registry file does not contain a value for + # the Program Files X86 path. + self.assertEquals(path, None) + + +class WindowsSystemRegistryPathTest(test_lib.PreprocessPluginTest): + """Tests for the Windows system Registry path preprocess plug-in object.""" + + _FILE_DATA = 'regf' + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._fake_file_system = self._BuildSingleFileFakeFileSystem( + u'/Windows/System32/config/SYSTEM', self._FILE_DATA) + + mount_point = fake_path_spec.FakePathSpec(location=u'/') + self._searcher = file_system_searcher.FileSystemSearcher( + self._fake_file_system, mount_point) + + def testGetValue(self): + """Tests the GetValue function.""" + knowledge_base_object = knowledge_base.KnowledgeBase() + + plugin = windows.WindowsSystemRegistryPath() + plugin.Run(self._searcher, knowledge_base_object) + + path = knowledge_base_object.GetValue('sysregistry') + self.assertEquals(path, u'/Windows/System32/config') + + +class WindowsSystemRootPathTest(test_lib.PreprocessPluginTest): + """Tests for the Windows system Root path preprocess plug-in object.""" + + _FILE_DATA = 'regf' + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._fake_file_system = self._BuildSingleFileFakeFileSystem( + u'/Windows/System32/config/SYSTEM', self._FILE_DATA) + + mount_point = fake_path_spec.FakePathSpec(location=u'/') + self._searcher = file_system_searcher.FileSystemSearcher( + self._fake_file_system, mount_point) + + def testGetValue(self): + """Tests the GetValue function.""" + knowledge_base_object = knowledge_base.KnowledgeBase() + + plugin = windows.WindowsSystemRootPath() + plugin.Run(self._searcher, knowledge_base_object) + + path = knowledge_base_object.GetValue('systemroot') + self.assertEquals(path, u'/Windows') + + +class WindowsTimeZoneTest(WindowsSystemRegistryTest): + """Tests for the Windows timezone preprocess plug-in object.""" + + def testGetValue(self): + """Tests the GetValue function.""" + knowledge_base_object = knowledge_base.KnowledgeBase() + + # The plug-in needs to expand {sysregistry} so we need to run + # the WindowsSystemRegistryPath plug-in first. + plugin = windows.WindowsSystemRegistryPath() + plugin.Run(self._searcher, knowledge_base_object) + + plugin = windows.WindowsTimeZone() + plugin.Run(self._searcher, knowledge_base_object) + + time_zone_str = knowledge_base_object.GetValue('time_zone_str') + self.assertEquals(time_zone_str, u'EST5EDT') + + +class WindowsUsersTest(WindowsSoftwareRegistryTest): + """Tests for the Windows username preprocess plug-in object.""" + + def testGetValue(self): + """Tests the GetValue function.""" + knowledge_base_object = knowledge_base.KnowledgeBase() + + # The plug-in needs to expand {sysregistry} so we need to run + # the WindowsSystemRegistryPath plug-in first. + plugin = windows.WindowsSystemRegistryPath() + plugin.Run(self._searcher, knowledge_base_object) + + plugin = windows.WindowsUsers() + plugin.Run(self._searcher, knowledge_base_object) + + users = knowledge_base_object.GetValue('users') + self.assertEquals(len(users), 11) + + expected_sid = u'S-1-5-21-2036804247-3058324640-2116585241-1114' + self.assertEquals(users[9].get('sid', None), expected_sid) + self.assertEquals(users[9].get('name', None), u'rsydow') + self.assertEquals(users[9].get('path', None), u'C:\\Users\\rsydow') + + +class WindowsVersionTest(WindowsSoftwareRegistryTest): + """Tests for the Windows version preprocess plug-in object.""" + + def testGetValue(self): + """Tests the GetValue function.""" + knowledge_base_object = knowledge_base.KnowledgeBase() + + # The plug-in needs to expand {sysregistry} so we need to run + # the WindowsSystemRegistryPath plug-in first. + plugin = windows.WindowsSystemRegistryPath() + plugin.Run(self._searcher, knowledge_base_object) + + plugin = windows.WindowsVersion() + plugin.Run(self._searcher, knowledge_base_object) + + osversion = knowledge_base_object.GetValue('osversion') + self.assertEquals(osversion, u'Windows 7 Ultimate') + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/proto/__init__.py b/plaso/proto/__init__.py new file mode 100644 index 0000000..1f5c4b3 --- /dev/null +++ b/plaso/proto/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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/plaso/proto/plaso_storage.proto b/plaso/proto/plaso_storage.proto new file mode 100644 index 0000000..b0c2798 --- /dev/null +++ b/plaso/proto/plaso_storage.proto @@ -0,0 +1,367 @@ +// Copyright 2012 The Plaso Project Authors. +// Please see the AUTHORS file for details on individual authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Kristinn Gudjonsson > + +// This is the main protobuf for event storage in plaso.o + +syntax = "proto2"; + +package plaso_storage; + +// Each EventObject can contain any attribute +// as long as it can be expressed in any of the supported +// formats (eg, string, int, array, dict). +// This can be looked as a hash or a dict object, with a key +// and a value values. +message Attribute { + // The key to the dict object, something like 'username'. + required string key = 1; + + // If the value is a string. + optional string string = 2; + // If the value is an integer. + optional int64 integer = 3; + // If the value is an array. + optional Array array = 4; + // If the value is a dictionary. + optional Dict dict = 5; + // If the value is a boolean value. + optional bool boolean = 6; + // If we have a "raw" byte value. + optional bytes data = 7; + // If we have a "float" value. + optional float float = 8; + // If there is a None value (happens). + optional bool none = 9; +}; + +// A list of of Attributes, to build up a dictionary. +message Dict { + repeated Attribute attributes = 1; +}; + +// A value, used for lists or arrays. +message Value { + optional int64 integer = 1; + optional string string = 2; + optional bytes data = 3; + optional Array array = 4; + optional Dict dict = 5; + optional bool boolean = 6; + optional float float = 7; + optional bool none = 8; +}; + +// A list of values, either integers or strings, to make up an array. +message Array { + repeated Value values = 1; +}; + +// Each event read by the tool is stored as an EventObject, +// an EventObject contains certain fixed sets of attributes +// and it can also store any additional attributes. +// This message stores the main attributes inside the each record +// instead of nesting them possibly deep down. +message EventObject { + // The timestamp is presented as a 64 bit Unix Epoch time, + // stored in UTC. + optional int64 timestamp = 1; + + // A short description of the meaning of the timestamp. + // This could be something as 'File Written', 'Last Written', + // 'Page Visited', 'File Downloaded', or something like that. + optional string timestamp_desc = 2; + + // The type of the event data stored in the attributes. + required string data_type = 3; + + // A list of all the stored attributes within the event. + repeated Attribute attributes = 4; + + // The timezone of the source where the timestamp came from. + optional string timezone = 5; + + // The filename from where the event was extracted from. + optional string filename = 6; + optional string display_name = 7; + + // The full PathSpec where the file was extracted from. + optional bytes pathspec = 8; + + // The offset into the original file from where the event came from. + optional int64 offset = 9; + + // Information about where this object is stored, added by the storage + // library to make it easier to quickly recover the EventObject from + // the storage file. + optional int64 store_number = 10; + optional int64 store_index = 11; + + // EventTagging is a message that can be added that include information + // about coloring, tagging or comments that this object contains. + optional EventTagging tag = 12; + + // Description of the origin of the event in generic terms that + // mostly adhere to the definition of the TLN format. + // TODO: Remove this field from the EventObject message. + enum SourceShort { + AV = 1; // All Anti-Virus engine log files. + BACK = 2; // Information from backup points, eg. restore points, VSS. + EVT = 3; // EventLog entries, both the EVT format and EVTX. + EXIF = 4; // EXIF information. + FILE = 5; // FILE related information, mactime information. + LOG = 6; // Generic log file, most log files should fit this. + LNK = 7; // Shortcut or link files. + LSO = 8; // Flash cookies, or Local Shared Objects. + META = 9; // Metadata information. + PLIST = 10; // Information extracted from plist files. + RAM = 11; // Information extracted from RAM. + RECBIN = 12; // Recycle bin or deleted items. + REG = 13; // Registry information. + WEBHIST = 14; // Browser history. + TORRENT = 15; // Torrent files. + JOB = 16; // Scheduled tasks or jobs. + } + + // The category or short description of the source. + // TODO: Remove this field from the EventObject message. + optional SourceShort source_short = 13; + + // The short description is not sufficient to describe the source + // of the event. A longer source field is therefore provided to add + // more context to the source. The source_long field should not be + // long, two or three words should be sufficient for most parts. + // The field is not strictly defined, it should just be short and + // fully descriptive. + // + // Example field names are: + // Chrome Browser History + // Chrome Download History + // Recycle Bin + // NTUSER.DAT Registry + // Sophos AV Log + // TODO: Remove this field from the EventObject message. + optional string source_long = 14; + + ///////////////////////////////////////////////////////////////////// + // Include common attribute names to flatten out the storage. + ///////////////////////////////////////////////////////////////////// + + // The name of the parser used to extract this item. + optional string parser = 15; + // The integer value of the inode of the file this entry is extracted from. + optional int64 inode = 16; + // The extracted hostname this entry came from. + optional string hostname = 17; + // The name of the plugin that was used, if applicable. + optional string plugin = 18; + // For Windows Registry files, defines the type of registry, eg: NTUSER, SAM. + optional string registry_type = 19; + // Boolean value that indicates whether the file was allocated or not. + optional bool allocated = 20; + // For filesystem records, defines the type of filesystem, eg: NTFS, FAT. + optional string fs_type = 21; + // Many parsers attempt to recover partially deleted entries, this boolean + // value is present in those parsers and indicates whether this particular + // entry is recovered or not. + optional bool recovered = 22; + // Contains the record number in log files that contain sequential + // information, such as Windows EventLog. + optional int64 record_number = 23; + // If the file being parsed contains different sources, such as "Security" or + // other similar source types it can be stored here. + optional string source_name = 24; + // Some log files, such as the EventLog, stores information about from which + // computer this particular entry came from. Often used in log files that can + // consolidate entries from more than a single host. + optional string computer_name = 25; + // Few entries that are specific to Windows EventLog entries, common enough + // to get specially defined attributes. + optional int64 event_identifier = 26; + optional int64 event_level = 27; + optional string xml_string = 28; + optional Array strings = 29; + // Some files contain information about the username that produced the + // extracted record. + optional string username = 30; + // Sometimes the username is not available but a SID or a UID. + optional string user_sid = 31; + // A field indicating the size of a cache file. + optional int64 cached_file_size = 32; + // Mostly used in browser history plugins referencing the number of times + // someone visited that particular entry. + optional int64 number_of_hits = 33; + // Used in MSIECF to indicate the index name of the cache directory. + optional int64 cache_directory_index = 34; + // Mostly used in browser history plugins. Contains the title of the web + // visited web page (store in tag). + optional string title = 35; + // Several parsers extract metadata items from events and store them + // in a dictionary. + optional Dict metadata = 36; + // An URL extracted from things like browser history. + optional string url = 37; + // Windows registry keyname attribute. + optional string keyname = 38; + // Extracted values from a Windows registry key. + optional Dict regvalue = 39; + // Some text based parsers define this attribute for their text + // representation. + optional string text = 40; + + // The UUID is a hex string that uniquely identifies the EventObject. + optional string uuid = 41; +}; + +// The EventTagging is a simple message that describes comments, +// color information or tagging of EventObjects. This information +// is usually manually added by an investigator and can be used +// to add more context to certain events. +message EventTagging { + // Description of where the EventObject is stored that this + // tag is describing. It is necessary to either define these + // two values or the event_uuid, otherwise it will not be + // possible to locate the event object this belongs to. + optional int64 store_number = 1; + optional int64 store_index = 2; + + // An arbitrary string that contains a comment describing + // observations the investigator has about this EventObject. + optional string comment = 3; + + // Color information, used in some front-ends to make this + // event stand out. This should be either a simple description + // of the color, eg "red", "yellow", etc or a HTML color code, + // eg: "#E11414". + optional string color = 4; + + // A short string or a tag that describes that can be used to + // group together events that are related to one another, eg + // "Malware", "Entry Point", "Event of Interest", etc. + message Tag { + required string value = 1; + }; + + repeated Tag tags = 5; + + // An UUID value of the particular event object that this tag + // belongs to. This value has to be set if the store_number and + // store_index are not know at the time of tagging. + optional string event_uuid = 6; +}; + +// The EventGroup is a simple mechanism to describe a group of +// events that belong to the same action or behavior. This is +// a simple mechanism to store this information so a front-end +// can collapse all these events into a single source. +message EventGroup { + // The name of the EventGroup, what is displayed in the front-end + // as a substitute for the other events, should be descriptive + // of the events that are grouped together, as in "USB Drive inserted", + // or "User Logged On". + required string name = 1; + + // Optional longer description of the group, giving a more detailed + // description of what the grouping describes or why these events + // were grouped together. + optional string description = 2; + + // If these events contain a timestamp it can be beneficial to + // include the timerange of events this group spans. That time range + // can be described by the first and last timestamp that is contained + // within the group. + optional int64 first_timestamp = 3; + optional int64 last_timestamp = 4; + + // Optional color information that can be used in the front-end + // to give color information about the group. This can be described + // as a simple color, eg: "red", "orange", "green" or as a HTML + // color code, eg "#E11414". + optional string color = 5; + + // If this group of events falls into a specific category it can + // be included here, eg: "User Behavior", "Malware Related", etc. + optional string category = 6; + + // Information about which EventObjects are included in this group. + // The information is stored in an attribute called EventDescription + // that simply defines where the EventObjects are stored so they can + // be easily identified and recovered. + message EventDescription { + // Description of where these events are stored within the storage + // file. + required int64 store_number = 1; + required int64 store_index = 2; + }; + + repeated EventDescription events = 7; +}; + +// The PreProcess protobuf is a simple message that stores information +// gathered from the preprocessing stage of plaso. +message PreProcess { + // Storing information about the runtime of the tool. + optional Dict collection_information = 1; + + // A dict that contains information about counters stored within the + // the store. + optional Dict counter = 2; + + // A list value that depicts the range of store numbers this particular + // PreProcess message applies to. + optional Array store_range = 3; + + // All attributes that each preprocessing module produces gets stored + // inside this field. + repeated Attribute attributes = 4; + + // A dict that contains information about plugin counters. + optional Dict plugin_counter = 5; +}; + +// The AnalysisReport object is a simple message describing a report +// created from an analysis plugin. +message AnalysisReport { + // Name of the analysis plugin that created this report. + optional string plugin_name = 1; + // The timestamp of when this report was created. + optional int64 time_compiled = 2; + + // The actual report content, the free flowing text. + // The text will have few notations possible: + // {image:X} - Where X is an integer, indicating the entry number + // inside the images field (counter starting from zero). + // This will indicate where images should be included in the + // final displayed report. + // {heading_start} / {heading_end}: An indication of main header. + // {subheading_start} / {subheading_end}: An indication of a subheader. + // This is no way meant as a "HTML/XML look-alike" in terms of definitions. + // This is merely a very simple implementation that only contains these + // "special" tags, meant to make it easier to export the final report in a + // HTML or any other format for later viewing. + optional string text = 3; + + // Optional repeated field of images that can be saved as binary data. + repeated bytes images = 4; + + // Some reports may contain counters, or some statistics that can be + // retrieved later on for additional analysis or processing. + optional Dict report_dict = 5; + optional Array report_array = 6; + + // If a filter string was used on the output, it's saved here. + optional string filter_string = 7; +}; diff --git a/plaso/proto/plaso_storage_pb2.py b/plaso/proto/plaso_storage_pb2.py new file mode 100644 index 0000000..a18267c --- /dev/null +++ b/plaso/proto/plaso_storage_pb2.py @@ -0,0 +1,1041 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! + +from google.protobuf import descriptor +from google.protobuf import message +from google.protobuf import reflection +from google.protobuf import descriptor_pb2 +# @@protoc_insertion_point(imports) + + + +DESCRIPTOR = descriptor.FileDescriptor( + name='plaso/proto/plaso_storage.proto', + package='plaso_storage', + serialized_pb='\n\x1fplaso/proto/plaso_storage.proto\x12\rplaso_storage\"\xbd\x01\n\tAttribute\x12\x0b\n\x03key\x18\x01 \x02(\t\x12\x0e\n\x06string\x18\x02 \x01(\t\x12\x0f\n\x07integer\x18\x03 \x01(\x03\x12#\n\x05\x61rray\x18\x04 \x01(\x0b\x32\x14.plaso_storage.Array\x12!\n\x04\x64ict\x18\x05 \x01(\x0b\x32\x13.plaso_storage.Dict\x12\x0f\n\x07\x62oolean\x18\x06 \x01(\x08\x12\x0c\n\x04\x64\x61ta\x18\x07 \x01(\x0c\x12\r\n\x05\x66loat\x18\x08 \x01(\x02\x12\x0c\n\x04none\x18\t \x01(\x08\"4\n\x04\x44ict\x12,\n\nattributes\x18\x01 \x03(\x0b\x32\x18.plaso_storage.Attribute\"\xac\x01\n\x05Value\x12\x0f\n\x07integer\x18\x01 \x01(\x03\x12\x0e\n\x06string\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12#\n\x05\x61rray\x18\x04 \x01(\x0b\x32\x14.plaso_storage.Array\x12!\n\x04\x64ict\x18\x05 \x01(\x0b\x32\x13.plaso_storage.Dict\x12\x0f\n\x07\x62oolean\x18\x06 \x01(\x08\x12\r\n\x05\x66loat\x18\x07 \x01(\x02\x12\x0c\n\x04none\x18\x08 \x01(\x08\"-\n\x05\x41rray\x12$\n\x06values\x18\x01 \x03(\x0b\x32\x14.plaso_storage.Value\"\xf5\x08\n\x0b\x45ventObject\x12\x11\n\ttimestamp\x18\x01 \x01(\x03\x12\x16\n\x0etimestamp_desc\x18\x02 \x01(\t\x12\x11\n\tdata_type\x18\x03 \x02(\t\x12,\n\nattributes\x18\x04 \x03(\x0b\x32\x18.plaso_storage.Attribute\x12\x10\n\x08timezone\x18\x05 \x01(\t\x12\x10\n\x08\x66ilename\x18\x06 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x07 \x01(\t\x12\x10\n\x08pathspec\x18\x08 \x01(\x0c\x12\x0e\n\x06offset\x18\t \x01(\x03\x12\x14\n\x0cstore_number\x18\n \x01(\x03\x12\x13\n\x0bstore_index\x18\x0b \x01(\x03\x12(\n\x03tag\x18\x0c \x01(\x0b\x32\x1b.plaso_storage.EventTagging\x12<\n\x0csource_short\x18\r \x01(\x0e\x32&.plaso_storage.EventObject.SourceShort\x12\x13\n\x0bsource_long\x18\x0e \x01(\t\x12\x0e\n\x06parser\x18\x0f \x01(\t\x12\r\n\x05inode\x18\x10 \x01(\x03\x12\x10\n\x08hostname\x18\x11 \x01(\t\x12\x0e\n\x06plugin\x18\x12 \x01(\t\x12\x15\n\rregistry_type\x18\x13 \x01(\t\x12\x11\n\tallocated\x18\x14 \x01(\x08\x12\x0f\n\x07\x66s_type\x18\x15 \x01(\t\x12\x11\n\trecovered\x18\x16 \x01(\x08\x12\x15\n\rrecord_number\x18\x17 \x01(\x03\x12\x13\n\x0bsource_name\x18\x18 \x01(\t\x12\x15\n\rcomputer_name\x18\x19 \x01(\t\x12\x18\n\x10\x65vent_identifier\x18\x1a \x01(\x03\x12\x13\n\x0b\x65vent_level\x18\x1b \x01(\x03\x12\x12\n\nxml_string\x18\x1c \x01(\t\x12%\n\x07strings\x18\x1d \x01(\x0b\x32\x14.plaso_storage.Array\x12\x10\n\x08username\x18\x1e \x01(\t\x12\x10\n\x08user_sid\x18\x1f \x01(\t\x12\x18\n\x10\x63\x61\x63hed_file_size\x18 \x01(\x03\x12\x16\n\x0enumber_of_hits\x18! \x01(\x03\x12\x1d\n\x15\x63\x61\x63he_directory_index\x18\" \x01(\x03\x12\r\n\x05title\x18# \x01(\t\x12%\n\x08metadata\x18$ \x01(\x0b\x32\x13.plaso_storage.Dict\x12\x0b\n\x03url\x18% \x01(\t\x12\x0f\n\x07keyname\x18& \x01(\t\x12%\n\x08regvalue\x18\' \x01(\x0b\x32\x13.plaso_storage.Dict\x12\x0c\n\x04text\x18( \x01(\t\x12\x0c\n\x04uuid\x18) \x01(\t\"\xad\x01\n\x0bSourceShort\x12\x06\n\x02\x41V\x10\x01\x12\x08\n\x04\x42\x41\x43K\x10\x02\x12\x07\n\x03\x45VT\x10\x03\x12\x08\n\x04\x45XIF\x10\x04\x12\x08\n\x04\x46ILE\x10\x05\x12\x07\n\x03LOG\x10\x06\x12\x07\n\x03LNK\x10\x07\x12\x07\n\x03LSO\x10\x08\x12\x08\n\x04META\x10\t\x12\t\n\x05PLIST\x10\n\x12\x07\n\x03RAM\x10\x0b\x12\n\n\x06RECBIN\x10\x0c\x12\x07\n\x03REG\x10\r\x12\x0b\n\x07WEBHIST\x10\x0e\x12\x0b\n\x07TORRENT\x10\x0f\x12\x07\n\x03JOB\x10\x10\"\xb2\x01\n\x0c\x45ventTagging\x12\x14\n\x0cstore_number\x18\x01 \x01(\x03\x12\x13\n\x0bstore_index\x18\x02 \x01(\x03\x12\x0f\n\x07\x63omment\x18\x03 \x01(\t\x12\r\n\x05\x63olor\x18\x04 \x01(\t\x12-\n\x04tags\x18\x05 \x03(\x0b\x32\x1f.plaso_storage.EventTagging.Tag\x12\x12\n\nevent_uuid\x18\x06 \x01(\t\x1a\x14\n\x03Tag\x12\r\n\x05value\x18\x01 \x02(\t\"\xfc\x01\n\nEventGroup\x12\x0c\n\x04name\x18\x01 \x02(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x17\n\x0f\x66irst_timestamp\x18\x03 \x01(\x03\x12\x16\n\x0elast_timestamp\x18\x04 \x01(\x03\x12\r\n\x05\x63olor\x18\x05 \x01(\t\x12\x10\n\x08\x63\x61tegory\x18\x06 \x01(\t\x12:\n\x06\x65vents\x18\x07 \x03(\x0b\x32*.plaso_storage.EventGroup.EventDescription\x1a=\n\x10\x45ventDescription\x12\x14\n\x0cstore_number\x18\x01 \x02(\x03\x12\x13\n\x0bstore_index\x18\x02 \x02(\x03\"\xed\x01\n\nPreProcess\x12\x33\n\x16\x63ollection_information\x18\x01 \x01(\x0b\x32\x13.plaso_storage.Dict\x12$\n\x07\x63ounter\x18\x02 \x01(\x0b\x32\x13.plaso_storage.Dict\x12)\n\x0bstore_range\x18\x03 \x01(\x0b\x32\x14.plaso_storage.Array\x12,\n\nattributes\x18\x04 \x03(\x0b\x32\x18.plaso_storage.Attribute\x12+\n\x0eplugin_counter\x18\x05 \x01(\x0b\x32\x13.plaso_storage.Dict\"\xc7\x01\n\x0e\x41nalysisReport\x12\x13\n\x0bplugin_name\x18\x01 \x01(\t\x12\x15\n\rtime_compiled\x18\x02 \x01(\x03\x12\x0c\n\x04text\x18\x03 \x01(\t\x12\x0e\n\x06images\x18\x04 \x03(\x0c\x12(\n\x0breport_dict\x18\x05 \x01(\x0b\x32\x13.plaso_storage.Dict\x12*\n\x0creport_array\x18\x06 \x01(\x0b\x32\x14.plaso_storage.Array\x12\x15\n\rfilter_string\x18\x07 \x01(\t') + + + +_EVENTOBJECT_SOURCESHORT = descriptor.EnumDescriptor( + name='SourceShort', + full_name='plaso_storage.EventObject.SourceShort', + filename=None, + file=DESCRIPTOR, + values=[ + descriptor.EnumValueDescriptor( + name='AV', index=0, number=1, + options=None, + type=None), + descriptor.EnumValueDescriptor( + name='BACK', index=1, number=2, + options=None, + type=None), + descriptor.EnumValueDescriptor( + name='EVT', index=2, number=3, + options=None, + type=None), + descriptor.EnumValueDescriptor( + name='EXIF', index=3, number=4, + options=None, + type=None), + descriptor.EnumValueDescriptor( + name='FILE', index=4, number=5, + options=None, + type=None), + descriptor.EnumValueDescriptor( + name='LOG', index=5, number=6, + options=None, + type=None), + descriptor.EnumValueDescriptor( + name='LNK', index=6, number=7, + options=None, + type=None), + descriptor.EnumValueDescriptor( + name='LSO', index=7, number=8, + options=None, + type=None), + descriptor.EnumValueDescriptor( + name='META', index=8, number=9, + options=None, + type=None), + descriptor.EnumValueDescriptor( + name='PLIST', index=9, number=10, + options=None, + type=None), + descriptor.EnumValueDescriptor( + name='RAM', index=10, number=11, + options=None, + type=None), + descriptor.EnumValueDescriptor( + name='RECBIN', index=11, number=12, + options=None, + type=None), + descriptor.EnumValueDescriptor( + name='REG', index=12, number=13, + options=None, + type=None), + descriptor.EnumValueDescriptor( + name='WEBHIST', index=13, number=14, + options=None, + type=None), + descriptor.EnumValueDescriptor( + name='TORRENT', index=14, number=15, + options=None, + type=None), + descriptor.EnumValueDescriptor( + name='JOB', index=15, number=16, + options=None, + type=None), + ], + containing_type=None, + options=None, + serialized_start=1487, + serialized_end=1660, +) + + +_ATTRIBUTE = descriptor.Descriptor( + name='Attribute', + full_name='plaso_storage.Attribute', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + descriptor.FieldDescriptor( + name='key', full_name='plaso_storage.Attribute.key', index=0, + number=1, type=9, cpp_type=9, label=2, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='string', full_name='plaso_storage.Attribute.string', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='integer', full_name='plaso_storage.Attribute.integer', index=2, + number=3, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='array', full_name='plaso_storage.Attribute.array', index=3, + number=4, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='dict', full_name='plaso_storage.Attribute.dict', index=4, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='boolean', full_name='plaso_storage.Attribute.boolean', index=5, + number=6, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='data', full_name='plaso_storage.Attribute.data', index=6, + number=7, type=12, cpp_type=9, label=1, + has_default_value=False, default_value="", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='float', full_name='plaso_storage.Attribute.float', index=7, + number=8, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='none', full_name='plaso_storage.Attribute.none', index=8, + number=9, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=51, + serialized_end=240, +) + + +_DICT = descriptor.Descriptor( + name='Dict', + full_name='plaso_storage.Dict', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + descriptor.FieldDescriptor( + name='attributes', full_name='plaso_storage.Dict.attributes', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=242, + serialized_end=294, +) + + +_VALUE = descriptor.Descriptor( + name='Value', + full_name='plaso_storage.Value', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + descriptor.FieldDescriptor( + name='integer', full_name='plaso_storage.Value.integer', index=0, + number=1, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='string', full_name='plaso_storage.Value.string', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='data', full_name='plaso_storage.Value.data', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value="", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='array', full_name='plaso_storage.Value.array', index=3, + number=4, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='dict', full_name='plaso_storage.Value.dict', index=4, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='boolean', full_name='plaso_storage.Value.boolean', index=5, + number=6, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='float', full_name='plaso_storage.Value.float', index=6, + number=7, type=2, cpp_type=6, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='none', full_name='plaso_storage.Value.none', index=7, + number=8, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=297, + serialized_end=469, +) + + +_ARRAY = descriptor.Descriptor( + name='Array', + full_name='plaso_storage.Array', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + descriptor.FieldDescriptor( + name='values', full_name='plaso_storage.Array.values', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=471, + serialized_end=516, +) + + +_EVENTOBJECT = descriptor.Descriptor( + name='EventObject', + full_name='plaso_storage.EventObject', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + descriptor.FieldDescriptor( + name='timestamp', full_name='plaso_storage.EventObject.timestamp', index=0, + number=1, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='timestamp_desc', full_name='plaso_storage.EventObject.timestamp_desc', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='data_type', full_name='plaso_storage.EventObject.data_type', index=2, + number=3, type=9, cpp_type=9, label=2, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='attributes', full_name='plaso_storage.EventObject.attributes', index=3, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='timezone', full_name='plaso_storage.EventObject.timezone', index=4, + number=5, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='filename', full_name='plaso_storage.EventObject.filename', index=5, + number=6, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='display_name', full_name='plaso_storage.EventObject.display_name', index=6, + number=7, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='pathspec', full_name='plaso_storage.EventObject.pathspec', index=7, + number=8, type=12, cpp_type=9, label=1, + has_default_value=False, default_value="", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='offset', full_name='plaso_storage.EventObject.offset', index=8, + number=9, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='store_number', full_name='plaso_storage.EventObject.store_number', index=9, + number=10, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='store_index', full_name='plaso_storage.EventObject.store_index', index=10, + number=11, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='tag', full_name='plaso_storage.EventObject.tag', index=11, + number=12, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='source_short', full_name='plaso_storage.EventObject.source_short', index=12, + number=13, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=1, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='source_long', full_name='plaso_storage.EventObject.source_long', index=13, + number=14, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='parser', full_name='plaso_storage.EventObject.parser', index=14, + number=15, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='inode', full_name='plaso_storage.EventObject.inode', index=15, + number=16, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='hostname', full_name='plaso_storage.EventObject.hostname', index=16, + number=17, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='plugin', full_name='plaso_storage.EventObject.plugin', index=17, + number=18, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='registry_type', full_name='plaso_storage.EventObject.registry_type', index=18, + number=19, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='allocated', full_name='plaso_storage.EventObject.allocated', index=19, + number=20, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='fs_type', full_name='plaso_storage.EventObject.fs_type', index=20, + number=21, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='recovered', full_name='plaso_storage.EventObject.recovered', index=21, + number=22, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='record_number', full_name='plaso_storage.EventObject.record_number', index=22, + number=23, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='source_name', full_name='plaso_storage.EventObject.source_name', index=23, + number=24, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='computer_name', full_name='plaso_storage.EventObject.computer_name', index=24, + number=25, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='event_identifier', full_name='plaso_storage.EventObject.event_identifier', index=25, + number=26, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='event_level', full_name='plaso_storage.EventObject.event_level', index=26, + number=27, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='xml_string', full_name='plaso_storage.EventObject.xml_string', index=27, + number=28, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='strings', full_name='plaso_storage.EventObject.strings', index=28, + number=29, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='username', full_name='plaso_storage.EventObject.username', index=29, + number=30, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='user_sid', full_name='plaso_storage.EventObject.user_sid', index=30, + number=31, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='cached_file_size', full_name='plaso_storage.EventObject.cached_file_size', index=31, + number=32, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='number_of_hits', full_name='plaso_storage.EventObject.number_of_hits', index=32, + number=33, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='cache_directory_index', full_name='plaso_storage.EventObject.cache_directory_index', index=33, + number=34, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='title', full_name='plaso_storage.EventObject.title', index=34, + number=35, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='metadata', full_name='plaso_storage.EventObject.metadata', index=35, + number=36, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='url', full_name='plaso_storage.EventObject.url', index=36, + number=37, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='keyname', full_name='plaso_storage.EventObject.keyname', index=37, + number=38, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='regvalue', full_name='plaso_storage.EventObject.regvalue', index=38, + number=39, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='text', full_name='plaso_storage.EventObject.text', index=39, + number=40, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='uuid', full_name='plaso_storage.EventObject.uuid', index=40, + number=41, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _EVENTOBJECT_SOURCESHORT, + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=519, + serialized_end=1660, +) + + +_EVENTTAGGING_TAG = descriptor.Descriptor( + name='Tag', + full_name='plaso_storage.EventTagging.Tag', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + descriptor.FieldDescriptor( + name='value', full_name='plaso_storage.EventTagging.Tag.value', index=0, + number=1, type=9, cpp_type=9, label=2, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=1821, + serialized_end=1841, +) + +_EVENTTAGGING = descriptor.Descriptor( + name='EventTagging', + full_name='plaso_storage.EventTagging', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + descriptor.FieldDescriptor( + name='store_number', full_name='plaso_storage.EventTagging.store_number', index=0, + number=1, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='store_index', full_name='plaso_storage.EventTagging.store_index', index=1, + number=2, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='comment', full_name='plaso_storage.EventTagging.comment', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='color', full_name='plaso_storage.EventTagging.color', index=3, + number=4, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='tags', full_name='plaso_storage.EventTagging.tags', index=4, + number=5, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='event_uuid', full_name='plaso_storage.EventTagging.event_uuid', index=5, + number=6, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[_EVENTTAGGING_TAG, ], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=1663, + serialized_end=1841, +) + + +_EVENTGROUP_EVENTDESCRIPTION = descriptor.Descriptor( + name='EventDescription', + full_name='plaso_storage.EventGroup.EventDescription', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + descriptor.FieldDescriptor( + name='store_number', full_name='plaso_storage.EventGroup.EventDescription.store_number', index=0, + number=1, type=3, cpp_type=2, label=2, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='store_index', full_name='plaso_storage.EventGroup.EventDescription.store_index', index=1, + number=2, type=3, cpp_type=2, label=2, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=2035, + serialized_end=2096, +) + +_EVENTGROUP = descriptor.Descriptor( + name='EventGroup', + full_name='plaso_storage.EventGroup', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + descriptor.FieldDescriptor( + name='name', full_name='plaso_storage.EventGroup.name', index=0, + number=1, type=9, cpp_type=9, label=2, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='description', full_name='plaso_storage.EventGroup.description', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='first_timestamp', full_name='plaso_storage.EventGroup.first_timestamp', index=2, + number=3, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='last_timestamp', full_name='plaso_storage.EventGroup.last_timestamp', index=3, + number=4, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='color', full_name='plaso_storage.EventGroup.color', index=4, + number=5, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='category', full_name='plaso_storage.EventGroup.category', index=5, + number=6, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='events', full_name='plaso_storage.EventGroup.events', index=6, + number=7, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[_EVENTGROUP_EVENTDESCRIPTION, ], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=1844, + serialized_end=2096, +) + + +_PREPROCESS = descriptor.Descriptor( + name='PreProcess', + full_name='plaso_storage.PreProcess', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + descriptor.FieldDescriptor( + name='collection_information', full_name='plaso_storage.PreProcess.collection_information', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='counter', full_name='plaso_storage.PreProcess.counter', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='store_range', full_name='plaso_storage.PreProcess.store_range', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='attributes', full_name='plaso_storage.PreProcess.attributes', index=3, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='plugin_counter', full_name='plaso_storage.PreProcess.plugin_counter', index=4, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=2099, + serialized_end=2336, +) + + +_ANALYSISREPORT = descriptor.Descriptor( + name='AnalysisReport', + full_name='plaso_storage.AnalysisReport', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + descriptor.FieldDescriptor( + name='plugin_name', full_name='plaso_storage.AnalysisReport.plugin_name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='time_compiled', full_name='plaso_storage.AnalysisReport.time_compiled', index=1, + number=2, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='text', full_name='plaso_storage.AnalysisReport.text', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='images', full_name='plaso_storage.AnalysisReport.images', index=3, + number=4, type=12, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='report_dict', full_name='plaso_storage.AnalysisReport.report_dict', index=4, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='report_array', full_name='plaso_storage.AnalysisReport.report_array', index=5, + number=6, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + descriptor.FieldDescriptor( + name='filter_string', full_name='plaso_storage.AnalysisReport.filter_string', index=6, + number=7, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=2339, + serialized_end=2538, +) + +_ATTRIBUTE.fields_by_name['array'].message_type = _ARRAY +_ATTRIBUTE.fields_by_name['dict'].message_type = _DICT +_DICT.fields_by_name['attributes'].message_type = _ATTRIBUTE +_VALUE.fields_by_name['array'].message_type = _ARRAY +_VALUE.fields_by_name['dict'].message_type = _DICT +_ARRAY.fields_by_name['values'].message_type = _VALUE +_EVENTOBJECT.fields_by_name['attributes'].message_type = _ATTRIBUTE +_EVENTOBJECT.fields_by_name['tag'].message_type = _EVENTTAGGING +_EVENTOBJECT.fields_by_name['source_short'].enum_type = _EVENTOBJECT_SOURCESHORT +_EVENTOBJECT.fields_by_name['strings'].message_type = _ARRAY +_EVENTOBJECT.fields_by_name['metadata'].message_type = _DICT +_EVENTOBJECT.fields_by_name['regvalue'].message_type = _DICT +_EVENTOBJECT_SOURCESHORT.containing_type = _EVENTOBJECT; +_EVENTTAGGING_TAG.containing_type = _EVENTTAGGING; +_EVENTTAGGING.fields_by_name['tags'].message_type = _EVENTTAGGING_TAG +_EVENTGROUP_EVENTDESCRIPTION.containing_type = _EVENTGROUP; +_EVENTGROUP.fields_by_name['events'].message_type = _EVENTGROUP_EVENTDESCRIPTION +_PREPROCESS.fields_by_name['collection_information'].message_type = _DICT +_PREPROCESS.fields_by_name['counter'].message_type = _DICT +_PREPROCESS.fields_by_name['store_range'].message_type = _ARRAY +_PREPROCESS.fields_by_name['attributes'].message_type = _ATTRIBUTE +_PREPROCESS.fields_by_name['plugin_counter'].message_type = _DICT +_ANALYSISREPORT.fields_by_name['report_dict'].message_type = _DICT +_ANALYSISREPORT.fields_by_name['report_array'].message_type = _ARRAY +DESCRIPTOR.message_types_by_name['Attribute'] = _ATTRIBUTE +DESCRIPTOR.message_types_by_name['Dict'] = _DICT +DESCRIPTOR.message_types_by_name['Value'] = _VALUE +DESCRIPTOR.message_types_by_name['Array'] = _ARRAY +DESCRIPTOR.message_types_by_name['EventObject'] = _EVENTOBJECT +DESCRIPTOR.message_types_by_name['EventTagging'] = _EVENTTAGGING +DESCRIPTOR.message_types_by_name['EventGroup'] = _EVENTGROUP +DESCRIPTOR.message_types_by_name['PreProcess'] = _PREPROCESS +DESCRIPTOR.message_types_by_name['AnalysisReport'] = _ANALYSISREPORT + +class Attribute(message.Message): + __metaclass__ = reflection.GeneratedProtocolMessageType + DESCRIPTOR = _ATTRIBUTE + + # @@protoc_insertion_point(class_scope:plaso_storage.Attribute) + +class Dict(message.Message): + __metaclass__ = reflection.GeneratedProtocolMessageType + DESCRIPTOR = _DICT + + # @@protoc_insertion_point(class_scope:plaso_storage.Dict) + +class Value(message.Message): + __metaclass__ = reflection.GeneratedProtocolMessageType + DESCRIPTOR = _VALUE + + # @@protoc_insertion_point(class_scope:plaso_storage.Value) + +class Array(message.Message): + __metaclass__ = reflection.GeneratedProtocolMessageType + DESCRIPTOR = _ARRAY + + # @@protoc_insertion_point(class_scope:plaso_storage.Array) + +class EventObject(message.Message): + __metaclass__ = reflection.GeneratedProtocolMessageType + DESCRIPTOR = _EVENTOBJECT + + # @@protoc_insertion_point(class_scope:plaso_storage.EventObject) + +class EventTagging(message.Message): + __metaclass__ = reflection.GeneratedProtocolMessageType + + class Tag(message.Message): + __metaclass__ = reflection.GeneratedProtocolMessageType + DESCRIPTOR = _EVENTTAGGING_TAG + + # @@protoc_insertion_point(class_scope:plaso_storage.EventTagging.Tag) + DESCRIPTOR = _EVENTTAGGING + + # @@protoc_insertion_point(class_scope:plaso_storage.EventTagging) + +class EventGroup(message.Message): + __metaclass__ = reflection.GeneratedProtocolMessageType + + class EventDescription(message.Message): + __metaclass__ = reflection.GeneratedProtocolMessageType + DESCRIPTOR = _EVENTGROUP_EVENTDESCRIPTION + + # @@protoc_insertion_point(class_scope:plaso_storage.EventGroup.EventDescription) + DESCRIPTOR = _EVENTGROUP + + # @@protoc_insertion_point(class_scope:plaso_storage.EventGroup) + +class PreProcess(message.Message): + __metaclass__ = reflection.GeneratedProtocolMessageType + DESCRIPTOR = _PREPROCESS + + # @@protoc_insertion_point(class_scope:plaso_storage.PreProcess) + +class AnalysisReport(message.Message): + __metaclass__ = reflection.GeneratedProtocolMessageType + DESCRIPTOR = _ANALYSISREPORT + + # @@protoc_insertion_point(class_scope:plaso_storage.AnalysisReport) + +# @@protoc_insertion_point(module_scope) diff --git a/plaso/serializer/__init__.py b/plaso/serializer/__init__.py new file mode 100644 index 0000000..f462564 --- /dev/null +++ b/plaso/serializer/__init__.py @@ -0,0 +1,17 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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/plaso/serializer/interface.py b/plaso/serializer/interface.py new file mode 100644 index 0000000..2b94d0a --- /dev/null +++ b/plaso/serializer/interface.py @@ -0,0 +1,181 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The serializer object interfaces.""" + +# Since abc does not seem to have an @abc.abstractclassmethod we're using +# @abc.abstractmethod instead and shutting up pylint about: +# E0213: Method should have "self" as first argument. +# pylint: disable=no-self-argument + +import abc + + +class AnalysisReportSerializer(object): + """Class that implements the analysis report serializer interface.""" + + @abc.abstractmethod + def ReadSerialized(cls, serialized): + """Reads an analysis report from serialized form. + + Args: + serialized: an object containing the serialized form. + + Returns: + An analysis report (instance of AnalysisReport). + """ + + @abc.abstractmethod + def WriteSerialized(cls, analysis_report): + """Writes an analysis report to serialized form. + + Args: + analysis_report: an analysis report (instance of AnalysisReport). + + Returns: + An object containing the serialized form. + """ + + +class EventGroupSerializer(object): + """Class that implements the event group serializer interface.""" + + @abc.abstractmethod + def ReadSerialized(cls, serialized): + """Reads an event group from serialized form. + + Args: + serialized: an group containing the serialized form. + + Returns: + An event group (instance of EventGroup). + """ + + @abc.abstractmethod + def WriteSerialized(cls, event_group): + """Writes an event group to serialized form. + + Args: + event_group: an event group (instance of EventGroup). + + Returns: + An group containing the serialized form. + """ + + +class EventObjectSerializer(object): + """Class that implements the event object serializer interface.""" + + @abc.abstractmethod + def ReadSerialized(cls, serialized): + """Reads an event object from serialized form. + + Args: + serialized: an object containing the serialized form. + + Returns: + An event object (instance of EventObject). + """ + + @abc.abstractmethod + def WriteSerialized(cls, event_object): + """Writes an event object to serialized form. + + Args: + event_object: an event object (instance of EventObject). + + Returns: + An object containing the serialized form. + """ + + +class EventTagSerializer(object): + """Class that implements the event tag serializer interface.""" + + @abc.abstractmethod + def ReadSerialized(cls, serialized): + """Reads an event tag from serialized form. + + Args: + serialized: an object containing the serialized form. + + Returns: + An event tag (instance of EventTag). + """ + + @abc.abstractmethod + def WriteSerialized(cls, event_tag): + """Writes an event tag to serialized form. + + Args: + event_tag: an event tag (instance of EventTag). + + Returns: + An object containing the serialized form. + """ + + +class PathFilterSerializer(object): + """Class that implements the path filter serializer interface.""" + + @abc.abstractmethod + def ReadSerialized(cls, serialized): + """Reads a path filter from serialized form. + + Args: + serialized: an object containing the serialized form. + + Returns: + A path filter (instance of PathFilter). + """ + + @abc.abstractmethod + def WriteSerialized(cls, path_filter): + """Writes a path filter to serialized form. + + Args: + path_filter: a path filter (instance of PathFilter). + + Returns: + An object containing the serialized form. + """ + + +class PreprocessObjectSerializer(object): + """Class that implements the preprocessing object serializer interface.""" + + @abc.abstractmethod + def ReadSerialized(cls, serialized): + """Reads a path filter from serialized form. + + Args: + serialized: an object containing the serialized form. + + Returns: + A preprocessing object (instance of PreprocessObject). + """ + + @abc.abstractmethod + def WriteSerialized(cls, pre_obj): + """Writes a preprocessing object to serialized form. + + Args: + pro_obj: a preprocessing object (instance of PreprocessObject). + + Returns: + An object containing the serialized form. + """ diff --git a/plaso/serializer/json_serializer.py b/plaso/serializer/json_serializer.py new file mode 100644 index 0000000..ee1cf31 --- /dev/null +++ b/plaso/serializer/json_serializer.py @@ -0,0 +1,232 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The json serializer object implementation.""" + +import logging +import json + +from dfvfs.serializer import json_serializer as dfvfs_json_serializer + +from plaso.lib import event +from plaso.serializer import interface + + +class _EventTypeJsonEncoder(json.JSONEncoder): + """A class that implements an event type object JSON encoder.""" + + # pylint: disable=method-hidden + def default(self, object_instance): + """Returns a serialized version of an event type object. + + Args: + object_instance: instance of an event type object. + """ + # TODO: add support for the rest of the event type objects. + if isinstance(object_instance, event.EventTag): + return JsonEventTagSerializer.WriteSerialized(object_instance) + + else: + return super(_EventTypeJsonEncoder, self).default(object_instance) + + +class JsonAnalysisReportSerializer(interface.AnalysisReportSerializer): + """Class that implements the json analysis report serializer.""" + + @classmethod + def ReadSerialized(cls, json_string): + """Reads an analysis report from serialized form. + + Args: + json_string: a JSON string containing the serialized form. + + Returns: + An analysis report (instance of AnalysisReport). + """ + # TODO: implement. + pass + + @classmethod + def WriteSerialized(cls, analysis_report): + """Writes an analysis report to serialized form. + + Args: + analysis_report: an analysis report (instance of AnalysisReport). + + Returns: + A JSON string containing the serialized form. + """ + # TODO: implement. + pass + + +class JsonEventObjectSerializer(interface.EventObjectSerializer): + """Class that implements the json event object serializer.""" + + @classmethod + def ReadSerialized(cls, json_string): + """Reads an event object from serialized form. + + Args: + json_string: an object containing the serialized form. + + Returns: + An event object (instance of EventObject). + """ + event_object = event.EventObject() + json_attributes = json.loads(json_string) + + for key, value in json_attributes.iteritems(): + if key == 'tag': + value = JsonEventTagSerializer.ReadSerialized(value) + elif key == 'pathspec': + value = dfvfs_json_serializer.JsonPathSpecSerializer.ReadSerialized( + value) + + setattr(event_object, key, value) + + return event_object + + @classmethod + def WriteSerialized(cls, event_object): + """Writes an event object to serialized form. + + Args: + event_object: an event object (instance of EventObject). + + Returns: + An object containing the serialized form or None if the event + cannot be serialized. + """ + event_attributes = event_object.GetValues() + + serializer = dfvfs_json_serializer.JsonPathSpecSerializer + if 'pathspec' in event_attributes: + event_attributes['pathspec'] = serializer.WriteSerialized( + event_attributes['pathspec']) + + try: + return json.dumps(event_attributes, cls=_EventTypeJsonEncoder) + except UnicodeDecodeError as exception: + # TODO: Add better error handling so this can be traced to a parser or + # a plugin and to which file that caused it. + logging.error(u'Unable to serialize event with error: {0:s}'.format( + exception)) + + +class JsonEventTagSerializer(interface.EventTagSerializer): + """Class that implements the json event tag serializer.""" + + @classmethod + def ReadSerialized(cls, json_string): + """Reads an event tag from serialized form. + + Args: + json_string: a JSON string containing the serialized form. + + Returns: + An event tag (instance of EventTag). + """ + if not json_string: + return + + event_tag = event.EventTag() + + json_attributes = json.loads(json_string) + + for key, value in json_attributes.iteritems(): + setattr(event_tag, key, value) + + return event_tag + + @classmethod + def WriteSerialized(cls, event_tag): + """Writes an event tag to serialized form. + + Args: + event_tag: an event tag (instance of EventTag). + + Returns: + A JSON string containing the serialized form. + + Raises: + RuntimeError: when the event tag is not valid for serialization. + """ + if not event_tag.IsValidForSerialization(): + raise RuntimeError(u'Invalid tag object not valid for serialization.') + + return json.dumps(event_tag.__dict__) + + +class JsonPathFilterSerializer(interface.PathFilterSerializer): + """Class that implements the json path filter serializer.""" + + @classmethod + def ReadSerialized(cls, serialized): + """Reads a path filter from serialized form. + + Args: + serialized: a JSON string containing the serialized form. + + Returns: + A path filter (instance of PathFilter). + """ + # TODO: implement. + pass + + @classmethod + def WriteSerialized(cls, path_filter): + """Writes a path filter to serialized form. + + Args: + path_filter: a path filter (instance of PathFilter). + + Returns: + A JSON string containing the serialized form. + """ + # TODO: implement. + pass + + +class JsonPreprocessObjectSerializer(interface.PreprocessObjectSerializer): + """Class that implements the json preprocessing object serializer.""" + + @classmethod + def ReadSerialized(cls, json_string): + """Reads a path filter from serialized form. + + Args: + json_string: a JSON string containing the serialized form. + + Returns: + A preprocessing object (instance of PreprocessObject). + """ + # TODO: implement. + pass + + @classmethod + def WriteSerialized(cls, pre_obj): + """Writes a preprocessing object to serialized form. + + Args: + pro_obj: a preprocessing object (instance of PreprocessObject). + + Returns: + A JSON string containing the serialized form. + """ + # TODO: implement. + pass diff --git a/plaso/serializer/json_serializer_test.py b/plaso/serializer/json_serializer_test.py new file mode 100644 index 0000000..f0a9b83 --- /dev/null +++ b/plaso/serializer/json_serializer_test.py @@ -0,0 +1,126 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the serializer object implementation using json.""" + +import re +import unittest + +from plaso.lib import event +from plaso.serializer import json_serializer + +# TODO: add tests for the non implemented serializer objects when implemented. + + +class JsonEventObjectSerializerTest(unittest.TestCase): + """Tests for the json event object serializer object.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + self._json_string = """{ + "zero_integer": 0, + "my_dict": { + "a": "not b", + "c": 34, + "list": ["sf", 234], "an": [234, 32]}, + "uuid": "5a78777006de4ddb8d7bbe12ab92ccf8", + "timestamp_desc": "Written", + "a_tuple": [ + "some item", + [234, 52, 15], + {"a": "not a", "b": "not b"}, + 35], + "timestamp": 1234124, + "my_list": ["asf", 4234, 2, 54, "asf"], + "empty_string": "", + "data_type": "test:event2", + "null_value": null, + "unicode_string": "And I'm a unicorn.", + "integer": 34, + "string": "Normal string"}""" + + # Collapse multiple spaces and new lines into a single space. + expression = re.compile(r'[ \n]+') + self._json_string = expression.sub(' ', self._json_string) + # Remove spaces after { and [ characters. + expression = re.compile(r'([{[])[ ]+') + self._json_string = expression.sub('\\1', self._json_string) + # Remove spaces before } and ] characters. + expression = re.compile(r'[ ]+([}\]])') + self._json_string = expression.sub('\\1', self._json_string) + + def testReadSerialized(self): + """Test the read serialized functionality.""" + serializer = json_serializer.JsonEventObjectSerializer + event_object = serializer.ReadSerialized(self._json_string) + + # An integer value containing 0 should get stored. + self.assertTrue(hasattr(event_object, 'zero_integer')) + + attribute_value = getattr(event_object, 'integer', 0) + self.assertEquals(attribute_value, 34) + + attribute_value = getattr(event_object, 'my_list', []) + self.assertEquals(len(attribute_value), 5) + + attribute_value = getattr(event_object, 'string', '') + self.assertEquals(attribute_value, 'Normal string') + + attribute_value = getattr(event_object, 'unicode_string', u'') + self.assertEquals(attribute_value, u'And I\'m a unicorn.') + + attribute_value = getattr(event_object, 'a_tuple', ()) + self.assertEquals(len(attribute_value), 4) + + def testWriteSerialized(self): + """Test the write serialized functionality.""" + event_object = event.EventObject() + + event_object.data_type = 'test:event2' + event_object.timestamp = 1234124 + event_object.timestamp_desc = 'Written' + # Prevent the event object for generating its own UUID. + event_object.uuid = '5a78777006de4ddb8d7bbe12ab92ccf8' + + event_object.empty_string = u'' + event_object.zero_integer = 0 + event_object.integer = 34 + event_object.string = 'Normal string' + event_object.unicode_string = u'And I\'m a unicorn.' + event_object.my_list = ['asf', 4234, 2, 54, 'asf'] + event_object.my_dict = { + 'a': 'not b', 'c': 34, 'list': ['sf', 234], 'an': [234, 32]} + event_object.a_tuple = ( + 'some item', [234, 52, 15], {'a': 'not a', 'b': 'not b'}, 35) + event_object.null_value = None + + serializer = json_serializer.JsonEventObjectSerializer + json_string = serializer.WriteSerialized(event_object) + self.assertEquals(sorted(json_string), sorted(self._json_string)) + + event_object = serializer.ReadSerialized(json_string) + + # TODO: fix this. + # An empty string should not get stored. + # self.assertFalse(hasattr(event_object, 'empty_string')) + + # A None (or Null) value should not get stored. + # self.assertFalse(hasattr(event_object, 'null_value')) + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/serializer/protobuf_serializer.py b/plaso/serializer/protobuf_serializer.py new file mode 100644 index 0000000..495cd9a --- /dev/null +++ b/plaso/serializer/protobuf_serializer.py @@ -0,0 +1,737 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The protobuf serializer object implementation.""" + +import logging + +from dfvfs.serializer import protobuf_serializer as dfvfs_protobuf_serializer +from google.protobuf import message + +from plaso.lib import event +from plaso.lib import utils +from plaso.proto import plaso_storage_pb2 +from plaso.serializer import interface + + +class ProtobufEventAttributeSerializer(object): + """Class that implements the protobuf event attribute serializer.""" + + @classmethod + def ReadSerializedObject(cls, proto_attribute): + """Reads an event attribute from serialized form. + + Args: + proto_attribute: a protobuf attribute object containing the serialized + form. + + Returns: + A tuple containing the attribute name and value. + + Raises: + RuntimeError: when the protobuf attribute (field) type is not supported. + """ + attribute_name = u'' + try: + if proto_attribute.HasField('key'): + attribute_name = proto_attribute.key + except ValueError: + pass + + if not isinstance(proto_attribute, ( + plaso_storage_pb2.Attribute, plaso_storage_pb2.Value)): + raise RuntimeError(u'Unsupported protobuf type.') + + if proto_attribute.HasField('string'): + return attribute_name, proto_attribute.string + + elif proto_attribute.HasField('integer'): + return attribute_name, proto_attribute.integer + + elif proto_attribute.HasField('boolean'): + return attribute_name, proto_attribute.boolean + + elif proto_attribute.HasField('dict'): + attribute_value = {} + + for proto_dict in proto_attribute.dict.attributes: + dict_key, dict_value = cls.ReadSerializedObject(proto_dict) + attribute_value[dict_key] = dict_value + return attribute_name, attribute_value + + elif proto_attribute.HasField('array'): + attribute_value = [] + + for proto_array in proto_attribute.array.values: + _, list_value = cls.ReadSerializedObject(proto_array) + attribute_value.append(list_value) + return attribute_name, attribute_value + + elif proto_attribute.HasField('data'): + return attribute_name, proto_attribute.data + + elif proto_attribute.HasField('float'): + return attribute_name, proto_attribute.float + + elif proto_attribute.HasField('none'): + return attribute_name, None + + else: + raise RuntimeError(u'Unsupported proto attribute type.') + + @classmethod + def ReadSerializedDictObject(cls, proto_dict): + """Reads a dictionary event attribute from serialized form. + + Args: + proto_dict: a protobuf Dict object containing the serialized form. + + Returns: + A dictionary object. + """ + dict_object = {} + for proto_attribute in proto_dict.attributes: + dict_key, dict_value = cls.ReadSerializedObject(proto_attribute) + dict_object[dict_key] = dict_value + + return dict_object + + @classmethod + def ReadSerializedListObject(cls, proto_list): + """Reads a list event attribute from serialized form. + + Args: + proto_list: a protobuf List object containing the serialized form. + + Returns: + A list object. + """ + list_object = [] + for proto_value in proto_list.values: + _, list_value = cls.ReadSerializedObject(proto_value) + list_object.append(list_value) + + return list_object + + @classmethod + def WriteSerializedObject( + cls, proto_attribute, attribute_name, attribute_value): + """Writes an event attribute to serialized form. + + The attribute of an event object can store almost any + arbitrary data, so the corresponding protobuf storage must deal with the + various data types. This method identifies the data type and assigns it + properly to the attribute protobuf. + + Args: + proto_attribute: a protobuf attribute object. + attribute_name: the name of the attribute. + attribute_value: the value of the attribute. + + Returns: + A protobuf object containing the serialized form. + """ + if attribute_name: + proto_attribute.key = attribute_name + + if isinstance(attribute_value, (str, unicode)): + proto_attribute.string = utils.GetUnicodeString(attribute_value) + + elif isinstance(attribute_value, bool): + proto_attribute.boolean = attribute_value + + elif isinstance(attribute_value, (int, long)): + # TODO: add some bounds checking. + proto_attribute.integer = attribute_value + + elif isinstance(attribute_value, dict): + cls.WriteSerializedDictObject(proto_attribute, 'dict', attribute_value) + + elif isinstance(attribute_value, (list, tuple)): + cls.WriteSerializedListObject(proto_attribute, 'array', attribute_value) + + elif isinstance(attribute_value, float): + proto_attribute.float = attribute_value + + elif not attribute_value: + proto_attribute.none = True + + else: + proto_attribute.data = attribute_value + + @classmethod + def WriteSerializedDictObject( + cls, proto_attribute, attribute_name, dict_object): + """Writes a dictionary event attribute to serialized form. + + Args: + proto_attribute: a protobuf attribute object. + attribute_name: the name of the attribute. + ditctobject: a dictionary object that is the value of the attribute. + """ + dict_proto = plaso_storage_pb2.Dict() + + for dict_key, dict_value in dict_object.items(): + dict_proto_add = dict_proto.attributes.add() + cls.WriteSerializedObject(dict_proto_add, dict_key, dict_value) + + dict_attribute = getattr(proto_attribute, attribute_name) + dict_attribute.MergeFrom(dict_proto) + + @classmethod + def WriteSerializedListObject( + cls, proto_attribute, attribute_name, list_object): + """Writes a list event attribute to serialized form. + + Args: + proto_attribute: a protobuf attribute object. + attribute_name: the name of the attribute. + list_object: a list object that is the value of the attribute. + """ + list_proto = plaso_storage_pb2.Array() + + for list_value in list_object: + list_proto_add = list_proto.values.add() + cls.WriteSerializedObject(list_proto_add, '', list_value) + + list_attribute = getattr(proto_attribute, attribute_name) + list_attribute.MergeFrom(list_proto) + + +class ProtobufAnalysisReportSerializer(interface.AnalysisReportSerializer): + """Class that implements the protobuf analysis report serializer.""" + + @classmethod + def ReadSerializedObject(cls, proto): + """Reads an analysis report from serialized form. + + Args: + proto: a protobuf object containing the serialized form (instance of + plaso_storage_pb2.AnalysisReport). + + Returns: + An analysis report (instance of AnalysisReport). + """ + analysis_report = event.AnalysisReport() + + for proto_attribute, value in proto.ListFields(): + # TODO: replace by ReadSerializedDictObject, need tests first. + # dict_object = ProtobufEventAttributeSerializer.ReadSerializedDictObject( + # proto.report_dict) + if proto_attribute.name == 'report_dict': + new_value = {} + for proto_dict in proto.report_dict.attributes: + dict_key, dict_value = ( + ProtobufEventAttributeSerializer.ReadSerializedObject(proto_dict)) + new_value[dict_key] = dict_value + setattr(analysis_report, proto_attribute.name, new_value) + + # TODO: replace by ReadSerializedListObject, need tests first. + # list_object = ProtobufEventAttributeSerializer.ReadSerializedListObject( + # proto.report_array) + elif proto_attribute.name == 'report_array': + new_value = [] + + for proto_array in proto.report_array.values: + _, list_value = ProtobufEventAttributeSerializer.ReadSerializedObject( + proto_array) + new_value.append(list_value) + setattr(analysis_report, proto_attribute.name, new_value) + + else: + setattr(analysis_report, proto_attribute.name, value) + + return analysis_report + + @classmethod + def ReadSerialized(cls, proto_string): + """Reads an analysis report from serialized form. + + Args: + proto_string: a protobuf string containing the serialized form. + + Returns: + An analysis report (instance of AnalysisReport). + """ + proto = plaso_storage_pb2.AnalysisReport() + proto.ParseFromString(proto_string) + + return cls.ReadSerializedObject(proto) + + @classmethod + def WriteSerializedObject(cls, analysis_report): + """Writes an analysis report to serialized form. + + Args: + analysis_report: an analysis report (instance of AnalysisReport). + + Returns: + A protobuf object containing the serialized form (instance of + plaso_storage_pb2.AnalysisReport). + """ + proto = plaso_storage_pb2.AnalysisReport() + proto.time_compiled = getattr(analysis_report, 'time_compiled', 0) + plugin_name = getattr(analysis_report, 'plugin_name', None) + + if plugin_name: + proto.plugin_name = plugin_name + + proto.text = getattr(analysis_report, 'text', 'N/A') + + for image in getattr(analysis_report, 'images', []): + proto.images.append(image) + + if hasattr(analysis_report, 'report_dict'): + dict_proto = plaso_storage_pb2.Dict() + for key, value in getattr(analysis_report, 'report_dict', {}).iteritems(): + sub_proto = dict_proto.attributes.add() + ProtobufEventAttributeSerializer.WriteSerializedObject( + sub_proto, key, value) + proto.report_dict.MergeFrom(dict_proto) + + if hasattr(analysis_report, 'report_array'): + list_proto = plaso_storage_pb2.Array() + for value in getattr(analysis_report, 'report_array', []): + sub_proto = list_proto.values.add() + ProtobufEventAttributeSerializer.WriteSerializedObject( + sub_proto, '', value) + + proto.report_array.MergeFrom(list_proto) + + return proto + + @classmethod + def WriteSerialized(cls, analysis_report): + """Writes an analysis report to serialized form. + + Args: + analysis_report: an analysis report (instance of AnalysisReport). + + Returns: + A protobuf string containing the serialized form. + """ + proto = cls.WriteSerializedObject(analysis_report) + return proto.SerializeToString() + + +class ProtobufEventObjectSerializer(interface.EventObjectSerializer): + """Class that implements the protobuf event object serializer.""" + + # TODO: check if the next TODO still applies. + # TODO: remove this once source_short has been moved to event formatter. + # Lists of the mappings between the source short values of the event object + # and those used in the protobuf. + _SOURCE_SHORT_FROM_PROTO_MAP = {} + _SOURCE_SHORT_TO_PROTO_MAP = {} + for value in plaso_storage_pb2.EventObject.DESCRIPTOR.enum_types_by_name[ + 'SourceShort'].values: + _SOURCE_SHORT_FROM_PROTO_MAP[value.number] = value.name + _SOURCE_SHORT_TO_PROTO_MAP[value.name] = value.number + _SOURCE_SHORT_FROM_PROTO_MAP.setdefault(6) + _SOURCE_SHORT_TO_PROTO_MAP.setdefault('LOG') + + _path_spec_serializer = dfvfs_protobuf_serializer.ProtobufPathSpecSerializer + + @classmethod + def ReadSerializedObject(cls, proto): + """Reads an event object from serialized form. + + Args: + proto: a protobuf object containing the serialized form (instance of + plaso_storage_pb2.EventObject). + + Returns: + An event object (instance of EventObject). + """ + event_object = event.EventObject() + event_object.data_type = proto.data_type + + for proto_attribute, value in proto.ListFields(): + if proto_attribute.name == 'source_short': + event_object.source_short = cls._SOURCE_SHORT_FROM_PROTO_MAP[value] + + elif proto_attribute.name == 'pathspec': + event_object.pathspec = ( + cls._path_spec_serializer.ReadSerialized(proto.pathspec)) + + elif proto_attribute.name == 'tag': + event_object.tag = ProtobufEventTagSerializer.ReadSerializedObject( + proto.tag) + + elif proto_attribute.name == 'attributes': + continue + + else: + # Register the attribute correctly. + # The attribute can be a 'regular' high level attribute or + # a message (Dict/Array) that need special handling. + if isinstance(value, message.Message): + if value.DESCRIPTOR.full_name.endswith('.Dict'): + value = ProtobufEventAttributeSerializer.ReadSerializedDictObject( + value) + elif value.DESCRIPTOR.full_name.endswith('.Array'): + value = ProtobufEventAttributeSerializer.ReadSerializedListObject( + value) + else: + value = ProtobufEventAttributeSerializer.ReadSerializedObject(value) + + setattr(event_object, proto_attribute.name, value) + + # The plaso_storage_pb2.EventObject protobuf contains a field named + # attributes which technically not a Dict but behaves similar. + dict_object = ProtobufEventAttributeSerializer.ReadSerializedDictObject( + proto) + + for attribute, value in dict_object.iteritems(): + setattr(event_object, attribute, value) + + return event_object + + @classmethod + def ReadSerialized(cls, proto_string): + """Reads an event object from serialized form. + + Args: + proto_string: a protobuf string containing the serialized form. + + Returns: + An event object (instance of EventObject). + """ + proto = plaso_storage_pb2.EventObject() + proto.ParseFromString(proto_string) + + return cls.ReadSerializedObject(proto) + + @classmethod + def WriteSerializedObject(cls, event_object): + """Writes an event object to serialized form. + + Args: + event_object: an event object (instance of EventObject). + + Returns: + A protobuf object containing the serialized form (instance of + plaso_storage_pb2.EventObject). + """ + proto = plaso_storage_pb2.EventObject() + + proto.data_type = getattr(event_object, 'data_type', 'event') + + for attribute_name in event_object.GetAttributes(): + if attribute_name == 'source_short': + proto.source_short = cls._SOURCE_SHORT_TO_PROTO_MAP[ + event_object.source_short] + + elif attribute_name == 'pathspec': + attribute_value = getattr(event_object, attribute_name, None) + if attribute_value: + attribute_value = cls._path_spec_serializer.WriteSerialized( + attribute_value) + setattr(proto, attribute_name, attribute_value) + + elif attribute_name == 'tag': + attribute_value = getattr(event_object, attribute_name, None) + if attribute_value: + event_tag_proto = ProtobufEventTagSerializer.WriteSerializedObject( + attribute_value) + proto.tag.MergeFrom(event_tag_proto) + + elif hasattr(proto, attribute_name): + attribute_value = getattr(event_object, attribute_name) + + if attribute_value is None: + continue + + if isinstance(attribute_value, (str, unicode)): + attribute_value = utils.GetUnicodeString(attribute_value) + if not attribute_value: + continue + + if isinstance(attribute_value, dict): + ProtobufEventAttributeSerializer.WriteSerializedDictObject( + proto, attribute_name, attribute_value) + + elif isinstance(attribute_value, (list, tuple)): + ProtobufEventAttributeSerializer.WriteSerializedListObject( + proto, attribute_name, attribute_value) + + else: + try: + setattr(proto, attribute_name, attribute_value) + except ValueError as exception: + path_spec = getattr(event_object, 'pathspec', None) + path = getattr(path_spec, 'location', u'') + logging.error(( + u'Unable to save value for: {0:s} [{1:s}] with error: {2:s} ' + u'coming from file: {3:s}').format( + attribute_name, type(attribute_value), exception, path)) + # Catch potential out of range errors. + if isinstance(attribute_value, (int, long)): + setattr(proto, attribute_name, -1) + + else: + attribute_value = getattr(event_object, attribute_name) + + # TODO: check if the next TODO still applies. + # Serialize the attribute value only if it is an integer type + # (int or long) or if it has a value. + # TODO: fix logic. + if (isinstance(attribute_value, (bool, int, float, long)) or + attribute_value): + proto_attribute = proto.attributes.add() + ProtobufEventAttributeSerializer.WriteSerializedObject( + proto_attribute, attribute_name, attribute_value) + + return proto + + @classmethod + def WriteSerialized(cls, event_object): + """Writes an event object to serialized form. + + Args: + event_object: an event object (instance of EventObject). + + Returns: + A protobuf string containing the serialized form or None if + there is an error encoding the protobuf. + """ + proto = cls.WriteSerializedObject(event_object) + try: + return proto.SerializeToString() + except message.EncodeError: + # TODO: Add better error handling so this can be traced to a parser or + # a plugin and to which file that caused it. + logging.error(u'Unable to serialize event object.') + + +class ProtobufEventTagSerializer(interface.EventTagSerializer): + """Class that implements the protobuf event tag serializer.""" + + @classmethod + def ReadSerializedObject(cls, proto): + """Reads an event tag from serialized form. + + Args: + proto: a protobuf object containing the serialized form (instance of + plaso_storage_pb2.EventTag). + + Returns: + An event tag (instance of EventTag). + """ + event_tag = event.EventTag() + + for proto_attribute, attribute_value in proto.ListFields(): + if proto_attribute.name == 'tags': + event_tag.tags = [] + for proto_tag in proto.tags: + event_tag.tags.append(proto_tag.value) + else: + setattr(event_tag, proto_attribute.name, attribute_value) + + return event_tag + + @classmethod + def ReadSerialized(cls, proto_string): + """Reads an event tag from serialized form. + + Args: + proto_string: a protobuf string containing the serialized form. + + Returns: + An event tag (instance of EventTag). + """ + proto = plaso_storage_pb2.EventTagging() + proto.ParseFromString(proto_string) + + return cls.ReadSerializedObject(proto) + + @classmethod + def WriteSerializedObject(cls, event_tag): + """Writes an event tag to serialized form. + + Args: + event_tag: an event tag (instance of EventTag). + + Returns: + A protobuf object containing the serialized form (instance of + plaso_storage_pb2.EventTagging). + """ + proto = plaso_storage_pb2.EventTagging() + + # TODO: Once we move EventTag to slots we need to query __slots__ + # instead of __dict__ + for attribute_name in event_tag.__dict__: + attribute_value = getattr(event_tag, attribute_name, None) + + if attribute_name == 'tags' and type(attribute_value) in (tuple, list): + for tag_string in attribute_value: + proto_tag_add = proto.tags.add() + proto_tag_add.value = tag_string + + elif attribute_value is not None: + setattr(proto, attribute_name, attribute_value) + + comment = getattr(event_tag, 'comment', '') + if comment: + proto.comment = comment + + color = getattr(event_tag, 'color', '') + if color: + proto.color = color + + return proto + + @classmethod + def WriteSerialized(cls, event_tag): + """Writes an event tag to serialized form. + + Args: + event_tag: an event tag (instance of EventTag). + + Returns: + A protobuf string containing the serialized form. + + Raises: + RuntimeError: when the event tag is not valid for serialization. + """ + if not event_tag.IsValidForSerialization(): + raise RuntimeError(u'Invalid tag object not valid for serialization.') + + proto = cls.WriteSerializedObject(event_tag) + return proto.SerializeToString() + + +class ProtobufPreprocessObjectSerializer(interface.PreprocessObjectSerializer): + """Class that implements the protobuf preprocessing object serializer.""" + + @classmethod + def ReadSerializedObject(cls, proto): + """Reads a preprocess object from serialized form. + + Args: + proto: a protobuf object containing the serialized form (instance of + plaso_storage_pb2.Preprocess). + + Returns: + A preprocessing object (instance of PreprocessObject). + """ + pre_obj = event.PreprocessObject() + + for attribute in proto.attributes: + key, value = ProtobufEventAttributeSerializer.ReadSerializedObject( + attribute) + if key == 'zone': + pre_obj.SetTimezone(value) + else: + setattr(pre_obj, key, value) + + if proto.HasField('counter'): + dict_object = ProtobufEventAttributeSerializer.ReadSerializedDictObject( + proto.counter) + pre_obj.SetCounterValues(dict_object) + + if proto.HasField('plugin_counter'): + dict_object = ProtobufEventAttributeSerializer.ReadSerializedDictObject( + proto.plugin_counter) + pre_obj.SetPluginCounterValues(dict_object) + + if proto.HasField('store_range'): + range_list = [] + for value in proto.store_range.values: + if value.HasField('integer'): + range_list.append(value.integer) + pre_obj.store_range = (range_list[0], range_list[-1]) + + if proto.HasField('collection_information'): + dict_object = ProtobufEventAttributeSerializer.ReadSerializedDictObject( + proto.collection_information) + pre_obj.SetCollectionInformationValues(dict_object) + + return pre_obj + + @classmethod + def ReadSerialized(cls, proto_string): + """Reads a preprocess object from serialized form. + + Args: + proto_string: a protobuf string containing the serialized form. + + Returns: + A preprocessing object (instance of PreprocessObject). + """ + proto = plaso_storage_pb2.PreProcess() + proto.ParseFromString(proto_string) + + return cls.ReadSerializedObject(proto) + + @classmethod + def WriteSerializedObject(cls, pre_obj): + """Writes a preprocessing object to serialized form. + + Args: + pre_obj: a preprocessing object (instance of PreprocessObject). + + Returns: + A protobuf object containing the serialized form (instance of + plaso_storage_pb2.PreProcess). + """ + proto = plaso_storage_pb2.PreProcess() + + for attribute, value in pre_obj.__dict__.items(): + if attribute == 'collection_information': + zone = value.get('configured_zone', '') + if zone and hasattr(zone, 'zone'): + value['configured_zone'] = zone.zone + ProtobufEventAttributeSerializer.WriteSerializedDictObject( + proto, 'collection_information', value) + elif attribute == 'counter': + value_dict = dict(value.items()) + ProtobufEventAttributeSerializer.WriteSerializedDictObject( + proto, 'counter', value_dict) + elif attribute == 'plugin_counter': + value_dict = dict(value.items()) + ProtobufEventAttributeSerializer.WriteSerializedDictObject( + proto, 'plugin_counter', value_dict) + elif attribute == 'store_range': + range_proto = plaso_storage_pb2.Array() + range_start = range_proto.values.add() + range_start.integer = int(value[0]) + range_end = range_proto.values.add() + range_end.integer = int(value[-1]) + proto.store_range.MergeFrom(range_proto) + else: + if attribute == 'zone': + value = value.zone + if isinstance(value, (bool, int, float, long)) or value: + proto_attribute = proto.attributes.add() + ProtobufEventAttributeSerializer.WriteSerializedObject( + proto_attribute, attribute, value) + + return proto + + @classmethod + def WriteSerialized(cls, pre_obj): + """Writes a preprocessing object to serialized form. + + Args: + pre_obj: a preprocessing object (instance of PreprocessObject). + + Returns: + A protobuf string containing the serialized form. + """ + proto = cls.WriteSerializedObject(pre_obj) + return proto.SerializeToString() diff --git a/plaso/serializer/protobuf_serializer_test.py b/plaso/serializer/protobuf_serializer_test.py new file mode 100644 index 0000000..d72ea9a --- /dev/null +++ b/plaso/serializer/protobuf_serializer_test.py @@ -0,0 +1,211 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the serializer object implementation using protobuf.""" + +import unittest + +from plaso.lib import event +from plaso.proto import plaso_storage_pb2 +from plaso.serializer import protobuf_serializer + + +class ProtobufAnalysisReportSerializerTest(unittest.TestCase): + """Tests for the protobuf analysis report serializer object.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + # TODO: add an analysis report test. + pass + + def testReadSerialized(self): + """Test the read serialized functionality.""" + # TODO: add an analysis report test. + pass + + def testWriteSerialized(self): + """Test the write serialized functionality.""" + # TODO: add an analysis report test. + pass + + +class ProtobufEventObjectSerializerTest(unittest.TestCase): + """Tests for the protobuf event object serializer object.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + proto = plaso_storage_pb2.EventObject() + + proto.data_type = 'test:event2' + proto.timestamp = 1234124 + proto.timestamp_desc = 'Written' + + serializer = protobuf_serializer.ProtobufEventAttributeSerializer + + proto_attribute = proto.attributes.add() + serializer.WriteSerializedObject(proto_attribute, 'zero_integer', 0) + + proto_attribute = proto.attributes.add() + dict_object = { + 'a': 'not b', 'c': 34, 'list': ['sf', 234], 'an': [234, 32]} + serializer.WriteSerializedObject(proto_attribute, 'my_dict', dict_object) + + proto_attribute = proto.attributes.add() + tuple_object = ( + 'some item', [234, 52, 15], {'a': 'not a', 'b': 'not b'}, 35) + serializer.WriteSerializedObject(proto_attribute, 'a_tuple', tuple_object) + + proto_attribute = proto.attributes.add() + list_object = ['asf', 4234, 2, 54, 'asf'] + serializer.WriteSerializedObject(proto_attribute, 'my_list', list_object) + + proto_attribute = proto.attributes.add() + serializer.WriteSerializedObject( + proto_attribute, 'unicode_string', u'And I\'m a unicorn.') + + proto_attribute = proto.attributes.add() + serializer.WriteSerializedObject(proto_attribute, 'integer', 34) + + proto_attribute = proto.attributes.add() + serializer.WriteSerializedObject(proto_attribute, 'string', 'Normal string') + + proto.uuid = '5a78777006de4ddb8d7bbe12ab92ccf8' + + self._proto_string = proto.SerializeToString() + + def testReadSerialized(self): + """Test the read serialized functionality.""" + serializer = protobuf_serializer.ProtobufEventObjectSerializer + event_object = serializer.ReadSerialized(self._proto_string) + + # An integer value containing 0 should get stored. + self.assertTrue(hasattr(event_object, 'zero_integer')) + + attribute_value = getattr(event_object, 'integer', 0) + self.assertEquals(attribute_value, 34) + + attribute_value = getattr(event_object, 'my_list', []) + self.assertEquals(len(attribute_value), 5) + + attribute_value = getattr(event_object, 'string', '') + self.assertEquals(attribute_value, 'Normal string') + + attribute_value = getattr(event_object, 'unicode_string', u'') + self.assertEquals(attribute_value, u'And I\'m a unicorn.') + + attribute_value = getattr(event_object, 'a_tuple', ()) + self.assertEquals(len(attribute_value), 4) + + def testWriteSerialized(self): + """Test the write serialized functionality.""" + event_object = event.EventObject() + + event_object.data_type = 'test:event2' + event_object.timestamp = 1234124 + event_object.timestamp_desc = 'Written' + # Prevent the event object for generating its own UUID. + event_object.uuid = '5a78777006de4ddb8d7bbe12ab92ccf8' + + event_object.empty_string = u'' + event_object.zero_integer = 0 + event_object.integer = 34 + event_object.string = 'Normal string' + event_object.unicode_string = u'And I\'m a unicorn.' + event_object.my_list = ['asf', 4234, 2, 54, 'asf'] + event_object.my_dict = { + 'a': 'not b', 'c': 34, 'list': ['sf', 234], 'an': [234, 32]} + event_object.a_tuple = ( + 'some item', [234, 52, 15], {'a': 'not a', 'b': 'not b'}, 35) + event_object.null_value = None + + serializer = protobuf_serializer.ProtobufEventObjectSerializer + proto_string = serializer.WriteSerialized(event_object) + self.assertEquals(proto_string, self._proto_string) + + event_object = serializer.ReadSerialized(proto_string) + + # An empty string should not get stored. + self.assertFalse(hasattr(event_object, 'empty_string')) + + # A None (or Null) value should not get stored. + self.assertFalse(hasattr(event_object, 'null_value')) + + +class ProtobufEventTagSerializerTest(unittest.TestCase): + """Tests for the protobuf event tag serializer object.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + proto = plaso_storage_pb2.EventTagging() + proto.store_number = 234 + proto.store_index = 18 + proto.comment = u'My first comment.' + proto.color = u'Red' + proto_tag = proto.tags.add() + proto_tag.value = u'Malware' + proto_tag = proto.tags.add() + proto_tag.value = u'Common' + + self._proto_string = proto.SerializeToString() + + def testReadSerialized(self): + """Test the read serialized functionality.""" + serializer = protobuf_serializer.ProtobufEventTagSerializer + event_tag = serializer.ReadSerialized(self._proto_string) + + self.assertEquals(event_tag.color, u'Red') + self.assertEquals(event_tag.comment, u'My first comment.') + self.assertEquals(event_tag.store_index, 18) + self.assertEquals(len(event_tag.tags), 2) + self.assertEquals(event_tag.tags, [u'Malware', u'Common']) + + def testWriteSerialized(self): + """Test the write serialized functionality.""" + event_tag = event.EventTag() + + event_tag.store_number = 234 + event_tag.store_index = 18 + event_tag.comment = u'My first comment.' + event_tag.color = u'Red' + event_tag.tags = [u'Malware', u'Common'] + + serializer = protobuf_serializer.ProtobufEventTagSerializer + proto_string = serializer.WriteSerialized(event_tag) + self.assertEquals(proto_string, self._proto_string) + + +class ProtobufPreprocessObjectSerializerTest(unittest.TestCase): + """Tests for the protobuf preprocess object serializer object.""" + + def setUp(self): + """Sets up the needed objects used throughout the test.""" + # TODO: add a preprocess object test. + pass + + def testReadSerialized(self): + """Test the read serialized functionality.""" + # TODO: add a preprocess object test. + pass + + def testWriteSerialized(self): + """Test the write serialized functionality.""" + # TODO: add a preprocess object test. + pass + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/unix/__init__.py b/plaso/unix/__init__.py new file mode 100644 index 0000000..ae78399 --- /dev/null +++ b/plaso/unix/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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/plaso/unix/bsmtoken.py b/plaso/unix/bsmtoken.py new file mode 100644 index 0000000..15de3b6 --- /dev/null +++ b/plaso/unix/bsmtoken.py @@ -0,0 +1,810 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the Basic Security Module definitions.""" + +# Arbitrary tokens. +# Type of data to print in a BSM_TOKEN_DATA. +BSM_TOKEN_DATA_TYPE = { + 0: u'AUR_CHAR', + 1: u'AUR_SHORT', + 2: u'AUR_INT32'} + +BSM_TOKEN_DATA_PRINT = { + 0: u'Binary', + 1: u'Octal', + 2: u'Decimal', + 3: u'Hexadecimal', + 4: u'String'} + +# BSM identification errors. +BSM_ERRORS = { + 0: u'Success', + 1: u'Operation not permitted', + 2: u'No such file or directory', + 3: u'No such process', + 4: u'Interrupted system call', + 5: u'Input/output error', + 6: u'Device not configured', + 7: u'Argument list too long', + 8: u'Exec format error', + 9: u'Bad file descriptor', + 10: u'No child processes', + 11: u'Resource temporarily unavailable', + 12: u'Cannot allocate memory', + 13: u'Permission denied', + 14: u'Bad address', + 15: u'Block device required', + 16: u'Device busy', + 17: u'File exists', + 18: u'ross-device link', + 19: u'Operation not supported by device', + 20: u'Not a directory', + 21: u'Is a directory', + 22: u'Invalid argument', + 23: u'Too many open files in system', + 24: u'Too many open files', + 25: u'Inappropriate ioctl for device', + 26: u'Text file busy', + 27: u'File too large', + 28: u'No space left on device', + 29: u'Illegal seek', + 30: u'Read-only file system', + 31: u'Too many links', + 32: u'Broken pipe', + 33: u'Numerical argument out of domain', + 34: u'Result too large', + 35: u'No message of desired type', + 36: u'Identifier removed', + 45: u'Resource deadlock avoided', + 46: u'No locks available', + 47: u'Operation canceled', + 48: u'Operation not supported', + 49: u'Disc quota exceeded', + 66: u'Too many levels of remote in path', + 67: u'Link has been severed', + 71: u'Protocol error', + 74: u'Multihop attempted', + 77: u'Bad message', + 78: u'File name too long', + 79: u'Value too large to be stored in data type', + 88: u'Illegal byte sequence', + 89: u'Function not implemented', + 90: u'Too many levels of symbolic links', + 91: u'Restart syscall', + 93: u'Directory not empty', + 94: u'Too many users', + 95: u'Socket operation on non-socket', + 96: u'Destination address required', + 97: u'Message too long', + 98: u'Protocol wrong type for socket', + 99: u'Protocol not available', + 120: u'Protocol not supported', + 121: u'Socket type not supported', + 122: u'Operation not supported', + 123: u'Protocol family not supported', + 124: u'Address family not supported by protocol family', + 125: u'Address already in use', + 126: u'Can\'t assign requested address', + 127: u'Network is down', + 128: u'Network unreachable', + 129: u'Network dropped connection on reset', + 130: u'Software caused connection abort', + 131: u'Connection reset by peer', + 132: u'No buffer space available', + 133: u'Socket is already connected', + 134: u'Socket is not connected', + 143: u'Can\'t send after socket shutdown', + 144: u'Too many references: can\'t splice', + 145: u'Operation timed out', + 146: u'Connection refused', + 147: u'Host is down', + 148: u'No route to host', + 149: u'Operation already in progress', + 150: u'Operation now in progress', + 151: u'Stale NFS file handle', + 190: u'PROCLIM', + 191: u'BADRPC', + 192: u'RPCMISMATCH', + 193: u'PROGUNAVAIL', + 194: u'PROGMISMATCH', + 195: u'PROCUNAVAIL', + 196: u'FTYPE', + 197: u'AUTH', + 198: u'NEEDAUTH', + 199: u'NOATTR', + 200: u'DOOFUS', + 201: u'USTRETURN', + 202: u'NOIOCTL', + 203: u'DIRIOCTL', + 204: u'PWROFF', + 205: u'DEVERR', + 206: u'BADEXEC', + 207: u'BADARCH', + 208: u'SHLIBVERS', + 209: u'BADMACHO', + 210: u'POLICY'} + +# BSM network protocols. The informations comes from OpenBSD project, +# it might not be exacly. +BSM_PROTOCOLS = { + 0: u'UNSPEC', + 1: u'LOCAL', + 2: u'INET', + 3: u'IMPLINK', + 4: u'PUP', + 5: u'CHAOS', + 6: u'NS', + 8: u'ECMA', + 9: u'DATAKIT', + 10: u'CCITT', + 11: u'SNA', + 12: u'DECnet', + 13: u'DLI', + 14: u'LAT', + 15: u'HYLINK', + 16: u'APPLETALK', + 19: u'OSI', + 23: u'IPX', + 24: u'ROUTE', + 25: u'LINK', + 26: u'INET6', + 27: u'KEY', + 500: u'NETBIOS', + 501: u'ISO', + 502: u'XTP', + 503: u'COIP', + 504: u'CNT', + 505: u'RTIP', + 506: u'SIP', + 507: u'PIP', + 508: u'ISDN', + 509: u'E164', + 510: u'NATM', + 511: u'ATM', + 512: u'NETGRAPH', + 513: u'SLOW', + 514: u'CLUSTER', + 515: u'ARP', + 516: u'BLUETOOTH'} + +# key event types. +BSM_AUDIT_EVENT = { + 0: u'indir system call', + 1: u'exit(2)', + 2: u'fork(2)', + 3: u'open(2) - attr only', + 4: u'creat(2)', + 5: u'link(2)', + 6: u'unlink(2)', + 7: u'exec(2)', + 8: u'chdir(2)', + 9: u'mknod(2)', + 10: u'chmod(2)', + 11: u'chown(2)', + 12: u'umount(2) - old version', + 13: u'junk', + 14: u'access(2)', + 15: u'kill(2)', + 16: u'stat(2)', + 17: u'lstat(2)', + 18: u'acct(2)', + 19: u'mctl(2)', + 20: u'reboot(2)', + 21: u'symlink(2)', + 22: u'readlink(2)', + 23: u'execve(2)', + 24: u'chroot(2)', + 25: u'vfork(2)', + 26: u'setgroups(2)', + 27: u'setpgrp(2)', + 28: u'swapon(2)', + 29: u'sethostname(2)', + 30: u'fcntl(2)', + 31: u'setpriority(2)', + 32: u'connect(2)', + 33: u'accept(2)', + 34: u'bind(2)', + 35: u'setsockopt(2)', + 36: u'vtrace(2)', + 37: u'settimeofday(2)', + 38: u'fchown(2)', + 39: u'fchmod(2)', + 40: u'setreuid(2)', + 41: u'setregid(2)', + 42: u'rename(2)', + 43: u'truncate(2)', + 44: u'ftruncate(2)', + 45: u'flock(2)', + 46: u'shutdown(2)', + 47: u'mkdir(2)', + 48: u'rmdir(2)', + 49: u'utimes(2)', + 50: u'adjtime(2)', + 51: u'setrlimit(2)', + 52: u'killpg(2)', + 53: u'nfs_svc(2)', + 54: u'statfs(2)', + 55: u'fstatfs(2)', + 56: u'unmount(2)', + 57: u'async_daemon(2)', + 58: u'nfs_getfh(2)', + 59: u'setdomainname(2)', + 60: u'quotactl(2)', + 61: u'exportfs(2)', + 62: u'mount(2)', + 63: u'semsys(2)', + 64: u'msgsys(2)', + 65: u'shmsys(2)', + 66: u'bsmsys(2)', + 67: u'rfssys(2)', + 68: u'fchdir(2)', + 69: u'fchroot(2)', + 70: u'vpixsys(2)', + 71: u'pathconf(2)', + 72: u'open(2) - read', + 73: u'open(2) - read,creat', + 74: u'open(2) - read,trunc', + 75: u'open(2) - read,creat,trunc', + 76: u'open(2) - write', + 77: u'open(2) - write,creat', + 78: u'open(2) - write,trunc', + 79: u'open(2) - write,creat,trunc', + 80: u'open(2) - read,write', + 81: u'open(2) - read,write,creat', + 82: u'open(2) - read,write,trunc', + 83: u'open(2) - read,write,creat,trunc', + 84: u'msgctl(2) - illegal command', + 85: u'msgctl(2) - IPC_RMID command', + 86: u'msgctl(2) - IPC_SET command', + 87: u'msgctl(2) - IPC_STAT command', + 88: u'msgget(2)', + 89: u'msgrcv(2)', + 90: u'msgsnd(2)', + 91: u'shmctl(2) - illegal command', + 92: u'shmctl(2) - IPC_RMID command', + 93: u'shmctl(2) - IPC_SET command', + 94: u'shmctl(2) - IPC_STAT command', + 95: u'shmget(2)', + 96: u'shmat(2)', + 97: u'shmdt(2)', + 98: u'semctl(2) - illegal command', + 99: u'semctl(2) - IPC_RMID command', + 100: u'semctl(2) - IPC_SET command', + 101: u'semctl(2) - IPC_STAT command', + 102: u'semctl(2) - GETNCNT command', + 103: u'semctl(2) - GETPID command', + 104: u'semctl(2) - GETVAL command', + 105: u'semctl(2) - GETALL command', + 106: u'semctl(2) - GETZCNT command', + 107: u'semctl(2) - SETVAL command', + 108: u'semctl(2) - SETALL command', + 109: u'semget(2)', + 110: u'semop(2)', + 111: u'process dumped core', + 112: u'close(2)', + 113: u'system booted', + 114: u'async_daemon(2) exited', + 115: u'nfssvc(2) exited', + 128: u'writel(2)', + 129: u'writevl(2)', + 130: u'getauid(2)', + 131: u'setauid(2)', + 132: u'getaudit(2)', + 133: u'setaudit(2)', + 134: u'getuseraudit(2)', + 135: u'setuseraudit(2)', + 136: u'auditsvc(2)', + 137: u'audituser(2)', + 138: u'auditon(2)', + 139: u'auditon(2) - GETTERMID command', + 140: u'auditon(2) - SETTERMID command', + 141: u'auditon(2) - GPOLICY command', + 142: u'auditon(2) - SPOLICY command', + 143: u'auditon(2) - GESTATE command', + 144: u'auditon(2) - SESTATE command', + 145: u'auditon(2) - GQCTRL command', + 146: u'auditon(2) - SQCTRL command', + 147: u'getkernstate(2)', + 148: u'setkernstate(2)', + 149: u'getportaudit(2)', + 150: u'auditstat(2)', + 151: u'revoke(2)', + 152: u'Solaris AUE_MAC', + 153: u'enter prom', + 154: u'exit prom', + 155: u'Solaris AUE_IFLOAT', + 156: u'Solaris AUE_PFLOAT', + 157: u'Solaris AUE_UPRIV', + 158: u'ioctl(2)', + 173: u'one-sided session record', + 174: u'msggetl(2)', + 175: u'msgrcvl(2)', + 176: u'msgsndl(2)', + 177: u'semgetl(2)', + 178: u'shmgetl(2)', + 183: u'socket(2)', + 184: u'sendto(2)', + 185: u'pipe(2)', + 186: u'socketpair(2)', + 187: u'send(2)', + 188: u'sendmsg(2)', + 189: u'recv(2)', + 190: u'recvmsg(2)', + 191: u'recvfrom(2)', + 192: u'read(2)', + 193: u'getdents(2)', + 194: u'lseek(2)', + 195: u'write(2)', + 196: u'writev(2)', + 197: u'nfs server', + 198: u'readv(2)', + 199: u'Solaris old stat(2)', + 200: u'setuid(2)', + 201: u'old stime(2)', + 202: u'old utime(2)', + 203: u'old nice(2)', + 204: u'Solaris old setpgrp(2)', + 205: u'setgid(2)', + 206: u'readl(2)', + 207: u'readvl(2)', + 208: u'fstat(2)', + 209: u'dup2(2)', + 210: u'mmap(2)', + 211: u'audit(2)', + 212: u'Solaris priocntlsys(2)', + 213: u'munmap(2)', + 214: u'setegid(2)', + 215: u'seteuid(2)', + 216: u'putmsg(2)', + 217: u'getmsg(2)', + 218: u'putpmsg(2)', + 219: u'getpmsg(2)', + 220: u'audit system calls place holder', + 221: u'auditon(2) - get kernel mask', + 222: u'auditon(2) - set kernel mask', + 223: u'auditon(2) - get cwd', + 224: u'auditon(2) - get car', + 225: u'auditon(2) - get audit statistics', + 226: u'auditon(2) - reset audit statistics', + 227: u'auditon(2) - set mask per uid', + 228: u'auditon(2) - set mask per session ID', + 229: u'auditon(2) - get audit state', + 230: u'auditon(2) - set audit state', + 231: u'auditon(2) - get event class', + 232: u'auditon(2) - set event class', + 233: u'utssys(2) - fusers', + 234: u'statvfs(2)', + 235: u'xstat(2)', + 236: u'lxstat(2)', + 237: u'lchown(2)', + 238: u'memcntl(2)', + 239: u'sysinfo(2)', + 240: u'xmknod(2)', + 241: u'fork1(2)', + 242: u'modctl(2) system call place holder', + 243: u'modctl(2) - load module', + 244: u'modctl(2) - unload module', + 245: u'modctl(2) - configure module', + 246: u'modctl(2) - bind module', + 247: u'getmsg-accept', + 248: u'putmsg-connect', + 249: u'putmsg-send', + 250: u'getmsg-receive', + 251: u'acl(2) - SETACL comand', + 252: u'facl(2) - SETACL command', + 253: u'doorfs(2) - system call place holder', + 254: u'doorfs(2) - DOOR_CALL', + 255: u'doorfs(2) - DOOR_RETURN', + 256: u'doorfs(2) - DOOR_CREATE', + 257: u'doorfs(2) - DOOR_REVOKE', + 258: u'doorfs(2) - DOOR_INFO', + 259: u'doorfs(2) - DOOR_CRED', + 260: u'doorfs(2) - DOOR_BIND', + 261: u'doorfs(2) - DOOR_UNBIND', + 262: u'p_online(2)', + 263: u'processor_bind(2)', + 264: u'inst_sync(2)', + 265: u'configure socket', + 266: u'setaudit_addr(2)', + 267: u'getaudit_addr(2)', + 268: u'Solaris umount(2)', + 269: u'fsat(2) - place holder', + 270: u'openat(2) - read', + 271: u'openat(2) - read,creat', + 272: u'openat(2) - read,trunc', + 273: u'openat(2) - read,creat,trunc', + 274: u'openat(2) - write', + 275: u'openat(2) - write,creat', + 276: u'openat(2) - write,trunc', + 277: u'openat(2) - write,creat,trunc', + 278: u'openat(2) - read,write', + 279: u'openat(2) - read,write,create', + 280: u'openat(2) - read,write,trunc', + 281: u'openat(2) - read,write,creat,trunc', + 282: u'renameat(2)', + 283: u'fstatat(2)', + 284: u'fchownat(2)', + 285: u'futimesat(2)', + 286: u'unlinkat(2)', + 287: u'clock_settime(2)', + 288: u'ntp_adjtime(2)', + 289: u'setppriv(2)', + 290: u'modctl(2) - configure device policy', + 291: u'modctl(2) - configure additional privilege', + 292: u'kernel cryptographic framework', + 293: u'configure kernel SSL', + 294: u'brandsys(2)', + 295: u'Add IPsec policy rule', + 296: u'Delete IPsec policy rule', + 297: u'Clone IPsec policy', + 298: u'Flip IPsec policy', + 299: u'Flush IPsec policy rules', + 300: u'Update IPsec algorithms', + 301: u'portfs', + 302: u'ptrace(2)', + 303: u'chflags(2)', + 304: u'fchflags(2)', + 305: u'profil(2)', + 306: u'ktrace(2)', + 307: u'setlogin(2)', + 308: u'reboot(2)', + 309: u'revoke(2)', + 310: u'umask(2)', + 311: u'mprotect(2)', + 312: u'setpriority(2)', + 313: u'settimeofday(2)', + 314: u'flock(2)', + 315: u'mkfifo(2)', + 316: u'poll(2)', + 317: u'socketpair(2)', + 318: u'futimes(2)', + 319: u'setsid(2)', + 320: u'setprivexec(2)', + 321: u'nfssvc(2)', + 322: u'getfh(2)', + 323: u'quotactl(2)', + 324: u'add_profil()', + 325: u'kdebug_trace()', + 326: u'fstat(2)', + 327: u'fpathconf(2)', + 328: u'getdirentries(2)', + 329: u'truncate(2)', + 330: u'ftruncate(2)', + 331: u'sysctl(3)', + 332: u'mlock(2)', + 333: u'munlock(2)', + 334: u'undelete(2)', + 335: u'getattrlist()', + 336: u'setattrlist()', + 337: u'getdirentriesattr()', + 338: u'exchangedata()', + 339: u'searchfs()', + 340: u'minherit(2)', + 341: u'semconfig()', + 342: u'sem_open(2)', + 343: u'sem_close(2)', + 344: u'sem_unlink(2)', + 345: u'shm_open(2)', + 346: u'shm_unlink(2)', + 347: u'load_shared_file()', + 348: u'reset_shared_file()', + 349: u'new_system_share_regions()', + 350: u'pthread_kill(2)', + 351: u'pthread_sigmask(2)', + 352: u'auditctl(2)', + 353: u'rfork(2)', + 354: u'lchmod(2)', + 355: u'swapoff(2)', + 356: u'init_process()', + 357: u'map_fd()', + 358: u'task_for_pid()', + 359: u'pid_for_task()', + 360: u'sysctl() - non-admin', + 361: u'copyfile()', + 43001: u'getfsstat(2)', + 43002: u'ptrace(2)', + 43003: u'chflags(2)', + 43004: u'fchflags(2)', + 43005: u'profil(2)', + 43006: u'ktrace(2)', + 43007: u'setlogin(2)', + 43008: u'revoke(2)', + 43009: u'umask(2)', + 43010: u'mprotect(2)', + 43011: u'mkfifo(2)', + 43012: u'poll(2)', + 43013: u'futimes(2)', + 43014: u'setsid(2)', + 43015: u'setprivexec(2)', + 43016: u'add_profil()', + 43017: u'kdebug_trace()', + 43018: u'fstat(2)', + 43019: u'fpathconf(2)', + 43020: u'getdirentries(2)', + 43021: u'sysctl(3)', + 43022: u'mlock(2)', + 43023: u'munlock(2)', + 43024: u'undelete(2)', + 43025: u'getattrlist()', + 43026: u'setattrlist()', + 43027: u'getdirentriesattr()', + 43028: u'exchangedata()', + 43029: u'searchfs()', + 43030: u'minherit(2)', + 43031: u'semconfig()', + 43032: u'sem_open(2)', + 43033: u'sem_close(2)', + 43034: u'sem_unlink(2)', + 43035: u'shm_open(2)', + 43036: u'shm_unlink(2)', + 43037: u'load_shared_file()', + 43038: u'reset_shared_file()', + 43039: u'new_system_share_regions()', + 43040: u'pthread_kill(2)', + 43041: u'pthread_sigmask(2)', + 43042: u'auditctl(2)', + 43043: u'rfork(2)', + 43044: u'lchmod(2)', + 43045: u'swapoff(2)', + 43046: u'init_process()', + 43047: u'map_fd()', + 43048: u'task_for_pid()', + 43049: u'pid_for_task()', + 43050: u'sysctl() - non-admin', + 43051: u'copyfile(2)', + 43052: u'lutimes(2)', + 43053: u'lchflags(2)', + 43054: u'sendfile(2)', + 43055: u'uselib(2)', + 43056: u'getresuid(2)', + 43057: u'setresuid(2)', + 43058: u'getresgid(2)', + 43059: u'setresgid(2)', + 43060: u'wait4(2)', + 43061: u'lgetfh(2)', + 43062: u'fhstatfs(2)', + 43063: u'fhopen(2)', + 43064: u'fhstat(2)', + 43065: u'jail(2)', + 43066: u'eaccess(2)', + 43067: u'kqueue(2)', + 43068: u'kevent(2)', + 43069: u'fsync(2)', + 43070: u'nmount(2)', + 43071: u'bdflush(2)', + 43072: u'setfsuid(2)', + 43073: u'setfsgid(2)', + 43074: u'personality(2)', + 43075: u'getscheduler(2)', + 43076: u'setscheduler(2)', + 43077: u'prctl(2)', + 43078: u'getcwd(2)', + 43079: u'capget(2)', + 43080: u'capset(2)', + 43081: u'pivot_root(2)', + 43082: u'rtprio(2)', + 43083: u'sched_getparam(2)', + 43084: u'sched_setparam(2)', + 43085: u'sched_get_priority_max(2)', + 43086: u'sched_get_priority_min(2)', + 43087: u'sched_rr_get_interval(2)', + 43088: u'acl_get_file(2)', + 43089: u'acl_set_file(2)', + 43090: u'acl_get_fd(2)', + 43091: u'acl_set_fd(2)', + 43092: u'acl_delete_file(2)', + 43093: u'acl_delete_fd(2)', + 43094: u'acl_aclcheck_file(2)', + 43095: u'acl_aclcheck_fd(2)', + 43096: u'acl_get_link(2)', + 43097: u'acl_set_link(2)', + 43098: u'acl_delete_link(2)', + 43099: u'acl_aclcheck_link(2)', + 43100: u'sysarch(2)', + 43101: u'extattrctl(2)', + 43102: u'extattr_get_file(2)', + 43103: u'extattr_set_file(2)', + 43104: u'extattr_list_file(2)', + 43105: u'extattr_delete_file(2)', + 43106: u'extattr_get_fd(2)', + 43107: u'extattr_set_fd(2)', + 43108: u'extattr_list_fd(2)', + 43109: u'extattr_delete_fd(2)', + 43110: u'extattr_get_link(2)', + 43111: u'extattr_set_link(2)', + 43112: u'extattr_list_link(2)', + 43113: u'extattr_delete_link(2)', + 43114: u'kenv(8)', + 43115: u'jail_attach(2)', + 43116: u'sysctl(3)', + 43117: u'linux ioperm', + 43118: u'readdir(3)', + 43119: u'linux iopl', + 43120: u'linux vm86', + 43121: u'mac_get_proc(2)', + 43122: u'mac_set_proc(2)', + 43123: u'mac_get_fd(2)', + 43124: u'mac_get_file(2)', + 43125: u'mac_set_fd(2)', + 43126: u'mac_set_file(2)', + 43127: u'mac_syscall(2)', + 43128: u'mac_get_pid(2)', + 43129: u'mac_get_link(2)', + 43130: u'mac_set_link(2)', + 43131: u'mac_execve(2)', + 43132: u'getpath_fromfd(2)', + 43133: u'getpath_fromaddr(2)', + 43134: u'mq_open(2)', + 43135: u'mq_setattr(2)', + 43136: u'mq_timedreceive(2)', + 43137: u'mq_timedsend(2)', + 43138: u'mq_notify(2)', + 43139: u'mq_unlink(2)', + 43140: u'listen(2)', + 43141: u'mlockall(2)', + 43142: u'munlockall(2)', + 43143: u'closefrom(2)', + 43144: u'fexecve(2)', + 43145: u'faccessat(2)', + 43146: u'fchmodat(2)', + 43147: u'linkat(2)', + 43148: u'mkdirat(2)', + 43149: u'mkfifoat(2)', + 43150: u'mknodat(2)', + 43151: u'readlinkat(2)', + 43152: u'symlinkat(2)', + 43153: u'mac_getfsstat(2)', + 43154: u'mac_get_mount(2)', + 43155: u'mac_get_lcid(2)', + 43156: u'mac_get_lctx(2)', + 43157: u'mac_set_lctx(2)', + 43158: u'mac_mount(2)', + 43159: u'getlcid(2)', + 43160: u'setlcid(2)', + 43161: u'taskname_for_pid()', + 43162: u'access_extended(2)', + 43163: u'chmod_extended(2)', + 43164: u'fchmod_extended(2)', + 43165: u'fstat_extended(2)', + 43166: u'lstat_extended(2)', + 43167: u'mkdir_extended(2)', + 43168: u'mkfifo_extended(2)', + 43169: u'open_extended(2) - attr only', + 43170: u'open_extended(2) - read', + 43171: u'open_extended(2) - read,creat', + 43172: u'open_extended(2) - read,trunc', + 43173: u'open_extended(2) - read,creat,trunc', + 43174: u'open_extended(2) - write', + 43175: u'open_extended(2) - write,creat', + 43176: u'open_extended(2) - write,trunc', + 43177: u'open_extended(2) - write,creat,trunc', + 43178: u'open_extended(2) - read,write', + 43179: u'open_extended(2) - read,write,creat', + 43180: u'open_extended(2) - read,write,trunc', + 43181: u'open_extended(2) - read,write,creat,trunc', + 43182: u'stat_extended(2)', + 43183: u'umask_extended(2)', + 43184: u'openat(2) - attr only', + 43185: u'posix_openpt(2)', + 43186: u'cap_new(2)', + 43187: u'cap_getrights(2)', + 43188: u'cap_enter(2)', + 43189: u'cap_getmode(2)', + 43190: u'posix_spawn(2)', + 43191: u'fsgetpath(2)', + 43192: u'pread(2)', + 43193: u'pwrite(2)', + 43194: u'fsctl()', + 43195: u'ffsctl()', + 43196: u'lpathconf(2)', + 43197: u'pdfork(2)', + 43198: u'pdkill(2)', + 43199: u'pdgetpid(2)', + 43200: u'pdwait(2)', + 44901: u'session start', + 44902: u'session update', + 44903: u'session end', + 44904: u'session close', + 6144: u'at-create atjob', + 6145: u'at-delete atjob (at or atrm)', + 6146: u'at-permission', + 6147: u'cron-invoke', + 6148: u'crontab-crontab created', + 6149: u'crontab-crontab deleted', + 6150: u'crontab-permission', + 6151: u'inetd connection', + 6152: u'login - local', + 6153: u'logout - local', + 6154: u'login - telnet', + 6155: u'login - rlogin', + 6156: u'mount', + 6157: u'unmount', + 6158: u'rsh access', + 6159: u'su(1)', + 6160: u'system halt', + 6161: u'system reboot', + 6162: u'rexecd', + 6163: u'passwd', + 6164: u'rexd', + 6165: u'ftp access', + 6166: u'init', + 6167: u'uadmin', + 6168: u'system shutdown', + 6170: u'crontab-modify', + 6171: u'ftp logout', + 6172: u'login - ssh', + 6173: u'role login', + 6180: u' profile command', + 6181: u'add filesystem', + 6182: u'delete filesystem', + 6183: u'modify filesystem', + 6200: u'allocate-device success', + 6201: u'allocate-device failure', + 6202: u'deallocate-device success', + 6203: u'deallocate-device failure', + 6204: u'allocate-list devices success', + 6205: u'allocate-list devices failure', + 6207: u'create user', + 6208: u'modify user', + 6209: u'delete user', + 6210: u'disable user', + 6211: u'enable user', + 6212: u'newgrp login', + 6213: u'admin login', + 6214: u'authenticated kadmind request', + 6215: u'unauthenticated kadmind req', + 6216: u'kdc authentication svc request', + 6217: u'kdc tkt-grant svc request', + 6218: u'kdc tgs 2ndtkt mismtch', + 6219: u'kdc tgs issue alt tgt', + 6300: u'sudo(1)', + 6501: u'modify password', + 6511: u'create group', + 6512: u'delete group', + 6513: u'modify group', + 6514: u'add to group', + 6515: u'remove from group', + 6521: u'revoke object priv', + 6600: u'loginwindow login', + 6601: u'loginwindow logout', + 7000: u'user authentication', + 7001: u'SecSrvr connection setup', + 7002: u'SecSrvr AuthEngine', + 7003: u'SecSrvr authinternal mech', + 32800: u'OpenSSH login', + 45000: u'audit startup', + 45001: u'audit shutdown', + 45014: u'modify password', + 45015: u'create group', + 45016: u'delete group', + 45017: u'modify group', + 45018: u'add to group', + 45019: u'remove from group', + 45020: u'revoke object priv', + 45021: u'loginwindow login', + 45022: u'loginwindow logout', + 45023: u'user authentication', + 45024: u'SecSrvr connection setup', + 45025: u'SecSrvr AuthEngine', + 45026: u'SecSrvr authinternal mech', + 45027: u'Calife', + 45028: u'sudo(1)', + 45029: u'audit crash recovery', + 45030: u'SecSrvr AuthMechanism', + 45031: u'Security Assessment' +} diff --git a/plaso/winnt/__init__.py b/plaso/winnt/__init__.py new file mode 100644 index 0000000..ae78399 --- /dev/null +++ b/plaso/winnt/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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/plaso/winnt/environ_expand.py b/plaso/winnt/environ_expand.py new file mode 100644 index 0000000..4cd2ac0 --- /dev/null +++ b/plaso/winnt/environ_expand.py @@ -0,0 +1,52 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains a method to expand Windows environment variables.""" + +import re + + +# TODO: Remove this file once we have a better replacement for it, either +# to use the artifact library or dfVFS, since this is part of both of these +# libraries. + +# Taken from: https://code.google.com/p/grr/source/browse/lib/artifact_lib.py +def ExpandWindowsEnvironmentVariables(data_string, pre_obj): + """Take a string and expand any windows environment variables. + + Args: + data_string: A string, e.g. "%SystemRoot%\\LogFiles" + pre_obj: A pre-process object. + + Returns: + A string with available environment variables expanded. + """ + win_environ_regex = re.compile(r'%([^%]+?)%') + components = [] + offset = 0 + for match in win_environ_regex.finditer(data_string): + components.append(data_string[offset:match.start()]) + + kb_value = getattr( + pre_obj, match.group(1).lower(), None) + if isinstance(kb_value, basestring) and kb_value: + components.append(kb_value) + else: + components.append(u'%%{0:s}%%'.format(match.group(1))) + offset = match.end() + components.append(data_string[offset:]) # Append the final chunk. + return u''.join(components) diff --git a/plaso/winnt/human_readable_service_enums.py b/plaso/winnt/human_readable_service_enums.py new file mode 100644 index 0000000..b4b9922 --- /dev/null +++ b/plaso/winnt/human_readable_service_enums.py @@ -0,0 +1,44 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains constants for making service keys more readable.""" + +SERVICE_ENUMS = { + # Human readable strings for the service type. + 'Type': { + 1: 'Kernel Device Driver (0x1)', + 2: 'File System Driver (0x2)', + 4: 'Adapter (0x4)', + 16: 'Service - Own Process (0x10)', + 32: 'Service - Share Process (0x20)' + }, + # Human readable strings for the service start type. + 'Start': { + 0: 'Boot (0)', + 1: 'System (1)', + 2: 'Auto Start (2)', + 3: 'Manual (3)', + 4: 'Disabled (4)' + }, + # Human readable strings for the error handling. + 'ErrorControl': { + 0: 'Ignore (0)', + 1: 'Normal (1)', + 2: 'Severe (2)', + 3: 'Critical (3)' + } +} diff --git a/plaso/winnt/known_folder_ids.py b/plaso/winnt/known_folder_ids.py new file mode 100644 index 0000000..d8b5247 --- /dev/null +++ b/plaso/winnt/known_folder_ids.py @@ -0,0 +1,270 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the Windows NT Known Folder identifier definitions.""" + +# For now ignore the line too long errors. +# pylint: disable=line-too-long + +# For now copied from: +# https://code.google.com/p/libfwsi/wiki/KnownFolderIdentifiers + +# TODO: store these in a database or equiv. + +DESCRIPTIONS = { + u'008ca0b1-55b4-4c56-b8a8-4de4b299d3be': u'Account Pictures', + u'00bcfc5a-ed94-4e48-96a1-3f6217f21990': u'Roaming Tiles', + u'0139d44e-6afe-49f2-8690-3dafcae6ffb8': u'(Common) Programs', + u'0482af6c-08f1-4c34-8c90-e17ec98b1e17': u'Public Account Pictures', + u'054fae61-4dd8-4787-80b6-090220c4b700': u'Game Explorer (Game Tasks)', + u'0762d272-c50a-4bb0-a382-697dcd729b80': u'Users (User Profiles)', + u'0ac0837c-bbf8-452a-850d-79d08e667ca7': u'Computer (My Computer)', + u'0d4c3db6-03a3-462f-a0e6-08924c41b5d4': u'History', + u'0f214138-b1d3-4a90-bba9-27cbc0c5389a': u'Sync Setup', + u'15ca69b3-30ee-49c1-ace1-6b5ec372afb5': u'Sample Playlists', + u'1777f761-68ad-4d8a-87bd-30b759fa33dd': u'Favorites', + u'18989b1d-99b5-455b-841c-ab7c74e4ddfc': u'Videos (My Video)', + u'190337d1-b8ca-4121-a639-6d472d16972a': u'Search Results (Search Home)', + u'1a6fdba2-f42d-4358-a798-b74d745926c5': u'Recorded TV', + u'1ac14e77-02e7-4e5d-b744-2eb1ae5198b7': u'System32 (System)', + u'1b3ea5dc-b587-4786-b4ef-bd1dc332aeae': u'Libraries', + u'1e87508d-89c2-42f0-8a7e-645a0f50ca58': u'Applications', + u'2112ab0a-c86a-4ffe-a368-0de96e47012e': u'Music', + u'2400183a-6185-49fb-a2d8-4a392a602ba3': u'Public Videos (Common Video)', + u'24d89e24-2f19-4534-9dde-6a6671fbb8fe': u'One Drive Documents', + u'289a9a43-be44-4057-a41b-587a76d7e7f9': u'Sync Results', + u'2a00375e-224c-49de-b8d1-440df7ef3ddc': u'Localized Resources (Directory)', + u'2b0f765d-c0e9-4171-908e-08a611b84ff6': u'Cookies', + u'2c36c0aa-5812-4b87-bfd0-4cd0dfb19b39': u'Original Images', + u'3214fab5-9757-4298-bb61-92a9deaa44ff': u'Public Music (Common Music)', + u'339719b5-8c47-4894-94c2-d8f77add44a6': u'One Drive Pictures', + u'33e28130-4e1e-4676-835a-98395c3bc3bb': u'Pictures (My Pictures)', + u'352481e8-33be-4251-ba85-6007caedcf9d': u'Internet Cache (Temporary Internet Files)', + u'374de290-123f-4565-9164-39c4925e467b': u'Downloads', + u'3d644c9b-1fb8-4f30-9b45-f670235f79c0': u'Public Downloads (Common Downloads)', + u'3eb685db-65f9-4cf6-a03a-e3ef65729f3d': u'Roaming Application Data (Roaming)', + u'43668bf8-c14e-49b2-97c9-747784d784b7': u'Sync Center (Sync Manager)', + u'48daf80b-e6cf-4f4e-b800-0e69d84ee384': u'Libraries', + u'491e922f-5643-4af4-a7eb-4e7a138d8174': u'Videos', + u'4bd8d571-6d19-48d3-be97-422220080e43': u'Music (My Music)', + u'4bfefb45-347d-4006-a5be-ac0cb0567192': u'Conflicts', + u'4c5c32ff-bb9d-43b0-b5b4-2d72e54eaaa4': u'Saved Games', + u'4d9f7874-4e0c-4904-967b-40b0d20c3e4b': u'Internet (The Internet)', + u'52528a6b-b9e3-4add-b60d-588c2dba842d': u'Homegroup', + u'52a4f021-7b75-48a9-9f6b-4b87a210bc8f': u'Quick Launch', + u'56784854-c6cb-462b-8169-88e350acb882': u'Contacts', + u'5b3749ad-b49f-49c1-83eb-15370fbd4882': u'Tree Properties', + u'5cd7aee2-2219-4a67-b85d-6c9ce15660cb': u'Programs', + u'5ce4a5e9-e4eb-479d-b89f-130c02886155': u'Device Metadata Store', + u'5e6c858f-0e22-4760-9afe-ea3317b67173': u'Profile (User\'s name)', + u'625b53c3-ab48-4ec1-ba1f-a1ef4146fc19': u'Start Menu', + u'62ab5d82-fdc1-4dc3-a9dd-070d1d495d97': u'Program Data', + u'6365d5a7-0f0d-45e5-87f6-0da56b6a4f7d': u'Common Files (x64)', + u'69d2cf90-fc33-4fb7-9a0c-ebb0f0fcb43c': u'Slide Shows (Photo Albums)', + u'6d809377-6af0-444b-8957-a3773f02200e': u'Program Files (x64)', + u'6f0cd92b-2e97-45d1-88ff-b0d186b8dedd': u'Network Connections', + u'724ef170-a42d-4fef-9f26-b60e846fba4f': u'Administrative Tools', + u'767e6811-49cb-4273-87c2-20f355e1085b': u'One Drive Camera Roll', + u'76fc4e2d-d6ad-4519-a663-37bd56068185': u'Printers', + u'7b0db17d-9cd2-4a93-9733-46cc89022e7c': u'Documents', + u'7b396e54-9ec5-4300-be0a-2482ebae1a26': u'Default Gadgets (Sidebar Default Parts)', + u'7c5a40ef-a0fb-4bfc-874a-c0f2e0b9fa8e': u'Program Files (x86)', + u'7d1d3a04-debb-4115-95cf-2f29da2920da': u'Saved Searches (Searches)', + u'7e636bfe-dfa9-4d5e-b456-d7b39851d8a9': u'Templates', + u'82a5ea35-d9cd-47c5-9629-e15d2f714e6e': u'(Common) Startup', + u'82a74aeb-aeb4-465c-a014-d097ee346d63': u'Control Panel', + u'859ead94-2e85-48ad-a71a-0969cb56a6cd': u'Sample Videos', + u'8983036c-27c0-404b-8f08-102d10dcfd74': u'Send To', + u'8ad10c31-2adb-4296-a8f7-e4701232c972': u'Resources (Resources Directory)', + u'905e63b6-c1bf-494e-b29c-65b732d3d21a': u'Program Files', + u'9274bd8d-cfd1-41c3-b35e-b13f55a758f4': u'Printer Shortcuts (PrintHood)', + u'98ec0e18-2098-4d44-8644-66979315a281': u'Microsoft Office Outlook (MAPI)', + u'9b74b6a3-0dfd-4f11-9e78-5f7800f2e772': u'User\'s name', + u'9e3995ab-1f9c-4f13-b827-48b24b6c7174': u'User Pinned', + u'9e52ab10-f80d-49df-acb8-4330f5687855': u'Temporary Burn Folder (CD Burning)', + u'a302545d-deff-464b-abe8-61c8648d939b': u'Libraries', + u'a305ce99-f527-492b-8b1a-7e76fa98d6e4': u'Installed Updates (Application Updates)', + u'a3918781-e5f2-4890-b3d9-a7e54332328c': u'Application Shortcuts', + u'a4115719-d62e-491d-aa7c-e74b8be3b067': u'(Common) Start Menu', + u'a520a1a4-1780-4ff6-bd18-167343c5af16': u'Local Application Data Low (Local Low)', + u'a52bba46-e9e1-435f-b3d9-28daa648c0f6': u'One Drive', + u'a63293e8-664e-48db-a079-df759e0509f7': u'Templates', + u'a75d362e-50fc-4fb7-ac2c-a8beaa314493': u'Gadgets (Sidebar Parts)', + u'a77f5d77-2e2b-44c3-a6a2-aba601054a51': u'Programs', + u'a990ae9f-a03b-4e80-94bc-9912d7504104': u'Pictures', + u'aaa8d5a5-f1d6-4259-baa8-78e7ef60835e': u'Roamed Tile Images', + u'ab5fb87b-7ce2-4f83-915d-550846c9537b': u'Camera Roll', + u'ae50c081-ebd2-438a-8655-8a092e34987a': u'Recent (Recent Items)', + u'b250c668-f57d-4ee1-a63c-290ee7d1aa1f': u'Sample Music', + u'b4bfcc3a-db2c-424c-b029-7fe99a87c641': u'Desktop', + u'b6ebfb86-6907-413c-9af7-4fc2abf07cc5': u'Public Pictures (Common Pictures)', + u'b7534046-3ecb-4c18-be4e-64cd4cb7d6ac': u'Recycle Bin (Bit Bucket)', + u'b7bede81-df94-4682-a7d8-57a52620b86f': u'Screenshots', + u'b94237e7-57ac-4347-9151-b08c6c32d1f7': u'(Common) Templates', + u'b97d20bb-f46a-4c97-ba10-5e3608430854': u'Startup', + u'bcb5256f-79f6-4cee-b725-dc34e402fd46': u'Implicit Application Shortcuts', + u'bcbd3057-ca5c-4622-b42d-bc56db0ae516': u'Programs', + u'bd85e001-112e-431e-983b-7b15ac09fff1': u'Recorded TV', + u'bfb9d5e0-c6a9-404c-b2b2-ae6db6af4968': u'Links', + u'c1bae2d0-10df-4334-bedd-7aa20b227a9d': u'(Common) OEM Links', + u'c4900540-2379-4c75-844b-64e6faf8716b': u'Sample Pictures', + u'c4aa340d-f20f-4863-afef-f87ef2e6ba25': u'Public Desktop (Common Desktop)', + u'c5abbf53-e17f-4121-8900-86626fc2c973': u'Network Shortcuts (NetHood)', + u'c870044b-f49e-4126-a9c3-b52a1ff411e8': u'Ringtones', + u'cac52c1a-b53d-4edc-92d7-6b2e8ac19434': u'Games', + u'd0384e7d-bac3-4797-8f14-cba229b392b5': u'(Common) Administrative Tools', + u'd20beec4-5ca8-4905-ae3b-bf251ea09b53': u'Network (Places)', + u'd65231b0-b2f1-4857-a4ce-a8e7c6ea7d27': u'System32 (x86)', + u'd9dc8a3b-b784-432e-a781-5a1130a75963': u'History', + u'de61d971-5ebc-4f02-a3a9-6c82895e5c04': u'Add New Programs (Get Programs)', + u'de92c1c7-837f-4f69-a3bb-86e631204a23': u'Playlists', + u'de974d24-d9c6-4d3e-bf91-f4455120b917': u'Common Files (x86)', + u'debf2536-e1a8-4c59-b6a2-414586476aea': u'Game Explorer (Public Game Tasks)', + u'df7266ac-9274-4867-8d55-3bd661de872d': u'Programs and Features (Change and Remove Programs)', + u'dfdf76a2-c82a-4d63-906a-5644ac457385': u'Public', + u'e555ab60-153b-4d17-9f04-a5fe99fc15ec': u'Ringtones', + u'ed4824af-dce4-45a8-81e2-fc7965083634': u'Public Documents (Common Documents)', + u'ee32e446-31ca-4aba-814f-a5ebd2fd6d5e': u'Offline Files (CSC)', + u'f1b32785-6fba-4fcf-9d55-7b8e7f157091': u'Local Application Data', + u'f38bf404-1d43-42f2-9305-67de0b28fc23': u'Windows', + u'f3ce0f7c-4901-4acc-8648-d5d44b04ef8f': u'User\'s Files', + u'f7f1ed05-9f6d-47a2-aaae-29d317c6f066': u'Common Files', + u'fd228cb7-ae11-4ae3-864c-16f3910ab8fe': u'Fonts', + u'fdd39ad0-238f-46af-adb4-6c85480369c7': u'Documents (Personal)', +} + +PATHS = { + u'008ca0b1-55b4-4c56-b8a8-4de4b299d3be': u'%APPDATA%\\Microsoft\\Windows\\AccountPictures', + u'00bcfc5a-ed94-4e48-96a1-3f6217f21990': u'%LOCALAPPDATA%\\Microsoft\\Windows\\RoamingTiles', + u'0139d44e-6afe-49f2-8690-3dafcae6ffb8': u'%ALLUSERSPROFILE%\\Microsoft\\Windows\\Start Menu\\Programs', + u'0482af6c-08f1-4c34-8c90-e17ec98b1e17': u'%PUBLIC%\\AccountPictures', + u'054fae61-4dd8-4787-80b6-090220c4b700': u'%LOCALAPPDATA%\\Microsoft\\Windows\\GameExplorer', + u'0762d272-c50a-4bb0-a382-697dcd729b80': u'%SYSTEMDRIVE%\\Users', + u'0ac0837c-bbf8-452a-850d-79d08e667ca7': u'', + u'0d4c3db6-03a3-462f-a0e6-08924c41b5d4': u'%LOCALAPPDATA%\\Microsoft\\Windows\\ConnectedSearch\\History', + u'0f214138-b1d3-4a90-bba9-27cbc0c5389a': u'', + u'15ca69b3-30ee-49c1-ace1-6b5ec372afb5': u'%PUBLIC%\\Music\\Sample Playlists', + u'1777f761-68ad-4d8a-87bd-30b759fa33dd': u'%USERPROFILE%\\Favorites', + u'18989b1d-99b5-455b-841c-ab7c74e4ddfc': u'%USERPROFILE%\\Videos', + u'190337d1-b8ca-4121-a639-6d472d16972a': u'', + u'1a6fdba2-f42d-4358-a798-b74d745926c5': u'%PUBLIC%\\RecordedTV.library-ms', + u'1ac14e77-02e7-4e5d-b744-2eb1ae5198b7': u'%WINDIR%\\System32', + u'1b3ea5dc-b587-4786-b4ef-bd1dc332aeae': u'%APPDATA%\\Microsoft\\Windows\\Libraries', + u'1e87508d-89c2-42f0-8a7e-645a0f50ca58': u'', + u'2112ab0a-c86a-4ffe-a368-0de96e47012e': u'%APPDATA%\\Microsoft\\Windows\\Libraries\\Music.library-ms', + u'2400183a-6185-49fb-a2d8-4a392a602ba3': u'%PUBLIC%\\Videos', + u'24d89e24-2f19-4534-9dde-6a6671fbb8fe': u'%USERPROFILE%\\OneDrive\\Documents', + u'289a9a43-be44-4057-a41b-587a76d7e7f9': u'', + u'2a00375e-224c-49de-b8d1-440df7ef3ddc': u'%WINDIR%\\resources\\%CODEPAGE%', + u'2b0f765d-c0e9-4171-908e-08a611b84ff6': u'%APPDATA%\\Microsoft\\Windows\\Cookies', + u'2c36c0aa-5812-4b87-bfd0-4cd0dfb19b39': u'%LOCALAPPDATA%\\Microsoft\\Windows Photo Gallery\\Original Images', + u'3214fab5-9757-4298-bb61-92a9deaa44ff': u'%PUBLIC%\\Music', + u'339719b5-8c47-4894-94c2-d8f77add44a6': u'%USERPROFILE%\\OneDrive\\Pictures', + u'33e28130-4e1e-4676-835a-98395c3bc3bb': u'%USERPROFILE%\\Pictures', + u'352481e8-33be-4251-ba85-6007caedcf9d': u'%LOCALAPPDATA%\\Microsoft\\Windows\\Temporary Internet Files', + u'374de290-123f-4565-9164-39c4925e467b': u'%USERPROFILE%\\Downloads', + u'3d644c9b-1fb8-4f30-9b45-f670235f79c0': u'%PUBLIC%\\Downloads', + u'3eb685db-65f9-4cf6-a03a-e3ef65729f3d': u'%USERPROFILE%\\AppData\\Roaming', + u'43668bf8-c14e-49b2-97c9-747784d784b7': u'', + u'48daf80b-e6cf-4f4e-b800-0e69d84ee384': u'%ALLUSERSPROFILE%\\Microsoft\\Windows\\Libraries', + u'491e922f-5643-4af4-a7eb-4e7a138d8174': u'%APPDATA%\\Microsoft\\Windows\\Libraries\\Videos.library-ms', + u'4bd8d571-6d19-48d3-be97-422220080e43': u'%USERPROFILE%\\Music', + u'4bfefb45-347d-4006-a5be-ac0cb0567192': u'', + u'4c5c32ff-bb9d-43b0-b5b4-2d72e54eaaa4': u'%USERPROFILE%\\Saved Games', + u'4d9f7874-4e0c-4904-967b-40b0d20c3e4b': u'', + u'52528a6b-b9e3-4add-b60d-588c2dba842d': u'', + u'52a4f021-7b75-48a9-9f6b-4b87a210bc8f': u'%APPDATA%\\Microsoft\\Internet Explorer\\Quick Launch', + u'56784854-c6cb-462b-8169-88e350acb882': u'', + u'5b3749ad-b49f-49c1-83eb-15370fbd4882': u'', + u'5cd7aee2-2219-4a67-b85d-6c9ce15660cb': u'%LOCALAPPDATA%\\Programs', + u'5ce4a5e9-e4eb-479d-b89f-130c02886155': u'%ALLUSERSPROFILE%\\Microsoft\\Windows\\DeviceMetadataStore', + u'5e6c858f-0e22-4760-9afe-ea3317b67173': u'%SYSTEMDRIVE%\\Users\\%USERNAME%', + u'625b53c3-ab48-4ec1-ba1f-a1ef4146fc19': u'%APPDATA%\\Microsoft\\Windows\\Start Menu', + u'62ab5d82-fdc1-4dc3-a9dd-070d1d495d97': u'%SYSTEMDRIVE%\\ProgramData', + u'6365d5a7-0f0d-45e5-87f6-0da56b6a4f7d': u'%PROGRAMFILES%\\Common Files', + u'69d2cf90-fc33-4fb7-9a0c-ebb0f0fcb43c': u'%USERPROFILE%\\Pictures\\Slide Shows', + u'6d809377-6af0-444b-8957-a3773f02200e': u'%SYSTEMDRIVE%\\Program Files', + u'6f0cd92b-2e97-45d1-88ff-b0d186b8dedd': u'', + u'724ef170-a42d-4fef-9f26-b60e846fba4f': u'%APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Administrative Tools', + u'767e6811-49cb-4273-87c2-20f355e1085b': u'%USERPROFILE%\\OneDrive\\Pictures\\Camera Roll', + u'76fc4e2d-d6ad-4519-a663-37bd56068185': u'', + u'7b0db17d-9cd2-4a93-9733-46cc89022e7c': u'%APPDATA%\\Microsoft\\Windows\\Libraries\\Documents.library-ms', + u'7b396e54-9ec5-4300-be0a-2482ebae1a26': u'%PROGRAMFILES%\\Windows Sidebar\\Gadgets', + u'7c5a40ef-a0fb-4bfc-874a-c0f2e0b9fa8e': u'%PROGRAMFILES% (%SYSTEMDRIVE%\\Program Files)', + u'7d1d3a04-debb-4115-95cf-2f29da2920da': u'%USERPROFILE%\\Searches', + u'7e636bfe-dfa9-4d5e-b456-d7b39851d8a9': u'%LOCALAPPDATA%\\Microsoft\\Windows\\ConnectedSearch\\Templates', + u'82a5ea35-d9cd-47c5-9629-e15d2f714e6e': u'%ALLUSERSPROFILE%\\Microsoft\\Windows\\Start Menu\\Programs\\StartUp', + u'82a74aeb-aeb4-465c-a014-d097ee346d63': u'', + u'859ead94-2e85-48ad-a71a-0969cb56a6cd': u'%PUBLIC%\\Videos\\Sample Videos', + u'8983036c-27c0-404b-8f08-102d10dcfd74': u'%APPDATA%\\Microsoft\\Windows\\SendTo', + u'8ad10c31-2adb-4296-a8f7-e4701232c972': u'%WINDIR%\\Resources', + u'905e63b6-c1bf-494e-b29c-65b732d3d21a': u'%SYSTEMDRIVE%\\Program Files', + u'9274bd8d-cfd1-41c3-b35e-b13f55a758f4': u'%APPDATA%\\Microsoft\\Windows\\Printer Shortcuts', + u'98ec0e18-2098-4d44-8644-66979315a281': u'', + u'9b74b6a3-0dfd-4f11-9e78-5f7800f2e772': u'', + u'9e3995ab-1f9c-4f13-b827-48b24b6c7174': u'%APPDATA%\\Microsoft\\Internet Explorer\\Quick Launch\\User Pinned', + u'9e52ab10-f80d-49df-acb8-4330f5687855': u'%LOCALAPPDATA%\\Microsoft\\Windows\\Burn\\Burn', + u'a302545d-deff-464b-abe8-61c8648d939b': u'', + u'a305ce99-f527-492b-8b1a-7e76fa98d6e4': u'', + u'a3918781-e5f2-4890-b3d9-a7e54332328c': u'%LOCALAPPDATA%\\Microsoft\\Windows\\Application Shortcuts', + u'a4115719-d62e-491d-aa7c-e74b8be3b067': u'%ALLUSERSPROFILE%\\Microsoft\\Windows\\Start Menu', + u'a520a1a4-1780-4ff6-bd18-167343c5af16': u'%USERPROFILE%\\AppData\\LocalLow', + u'a52bba46-e9e1-435f-b3d9-28daa648c0f6': u'%USERPROFILE%\\OneDrive', + u'a63293e8-664e-48db-a079-df759e0509f7': u'%APPDATA%\\Microsoft\\Windows\\Templates', + u'a75d362e-50fc-4fb7-ac2c-a8beaa314493': u'%LOCALAPPDATA%\\Microsoft\\Windows Sidebar\\Gadgets', + u'a77f5d77-2e2b-44c3-a6a2-aba601054a51': u'%APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs', + u'a990ae9f-a03b-4e80-94bc-9912d7504104': u'%APPDATA%\\Microsoft\\Windows\\Libraries\\Pictures.library-ms', + u'aaa8d5a5-f1d6-4259-baa8-78e7ef60835e': u'%LOCALAPPDATA%\\Microsoft\\Windows\\RoamedTileImages', + u'ab5fb87b-7ce2-4f83-915d-550846c9537b': u'%USERPROFILE%\\Pictures\\Camera Roll', + u'ae50c081-ebd2-438a-8655-8a092e34987a': u'%APPDATA%\\Microsoft\\Windows\\Recent', + u'b250c668-f57d-4ee1-a63c-290ee7d1aa1f': u'%PUBLIC%\\Music\\Sample Music', + u'b4bfcc3a-db2c-424c-b029-7fe99a87c641': u'%USERPROFILE%\\Desktop', + u'b6ebfb86-6907-413c-9af7-4fc2abf07cc5': u'%PUBLIC%\\Pictures', + u'b7534046-3ecb-4c18-be4e-64cd4cb7d6ac': u'', + u'b7bede81-df94-4682-a7d8-57a52620b86f': u'%USERPROFILE%\\Pictures\\Screenshots', + u'b94237e7-57ac-4347-9151-b08c6c32d1f7': u'%ALLUSERSPROFILE%\\Microsoft\\Windows\\Templates', + u'b97d20bb-f46a-4c97-ba10-5e3608430854': u'%APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\StartUp', + u'bcb5256f-79f6-4cee-b725-dc34e402fd46': u'%APPDATA%\\Microsoft\\Internet Explorer\\Quick Launch\\User Pinned\\ImplicitAppShortcuts', + u'bcbd3057-ca5c-4622-b42d-bc56db0ae516': u'%LOCALAPPDATA%\\Programs\\Common', + u'bd85e001-112e-431e-983b-7b15ac09fff1': u'', + u'bfb9d5e0-c6a9-404c-b2b2-ae6db6af4968': u'%USERPROFILE%\\Links', + u'c1bae2d0-10df-4334-bedd-7aa20b227a9d': u'%ALLUSERSPROFILE%\\OEM Links', + u'c4900540-2379-4c75-844b-64e6faf8716b': u'%PUBLIC%\\Pictures\\Sample Pictures', + u'c4aa340d-f20f-4863-afef-f87ef2e6ba25': u'%PUBLIC%\\Desktop', + u'c5abbf53-e17f-4121-8900-86626fc2c973': u'%APPDATA%\\Microsoft\\Windows\\Network Shortcuts', + u'c870044b-f49e-4126-a9c3-b52a1ff411e8': u'%LOCALAPPDATA%\\Microsoft\\Windows\\Ringtones', + u'cac52c1a-b53d-4edc-92d7-6b2e8ac19434': u'', + u'd0384e7d-bac3-4797-8f14-cba229b392b5': u'%ALLUSERSPROFILE%\\Microsoft\\Windows\\Start Menu\\Programs\\Administrative Tools', + u'd20beec4-5ca8-4905-ae3b-bf251ea09b53': u'', + u'd65231b0-b2f1-4857-a4ce-a8e7c6ea7d27': u'%WINDIR%\\system32', + u'd9dc8a3b-b784-432e-a781-5a1130a75963': u'%LOCALAPPDATA%\\Microsoft\\Windows\\History', + u'de61d971-5ebc-4f02-a3a9-6c82895e5c04': u'', + u'de92c1c7-837f-4f69-a3bb-86e631204a23': u'%USERPROFILE%\\Music\\Playlists', + u'de974d24-d9c6-4d3e-bf91-f4455120b917': u'%PROGRAMFILES%\\Common Files', + u'debf2536-e1a8-4c59-b6a2-414586476aea': u'%ALLUSERSPROFILE%\\Microsoft\\Windows\\GameExplorer', + u'df7266ac-9274-4867-8d55-3bd661de872d': u'', + u'dfdf76a2-c82a-4d63-906a-5644ac457385': u'%SYSTEMDRIVE%\\Users\\Public', + u'e555ab60-153b-4d17-9f04-a5fe99fc15ec': u'%ALLUSERSPROFILE%\\Microsoft\\Windows\\Ringtones', + u'ed4824af-dce4-45a8-81e2-fc7965083634': u'%PUBLIC%\\Documents', + u'ee32e446-31ca-4aba-814f-a5ebd2fd6d5e': u'', + u'f1b32785-6fba-4fcf-9d55-7b8e7f157091': u'%USERPROFILE%\\AppData\\Local', + u'f38bf404-1d43-42f2-9305-67de0b28fc23': u'%WINDIR%', + u'f3ce0f7c-4901-4acc-8648-d5d44b04ef8f': u'', + u'f7f1ed05-9f6d-47a2-aaae-29d317c6f066': u'%PROGRAMFILES%\\Common Files', + u'fd228cb7-ae11-4ae3-864c-16f3910ab8fe': u'%WINDIR%\\Fonts', + u'fdd39ad0-238f-46af-adb4-6c85480369c7': u'%USERPROFILE%\\Documents', +} diff --git a/plaso/winnt/shell_folder_ids.py b/plaso/winnt/shell_folder_ids.py new file mode 100644 index 0000000..818da71 --- /dev/null +++ b/plaso/winnt/shell_folder_ids.py @@ -0,0 +1,204 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the Windows NT shell folder identifier definitions.""" + +# For now ignore the line too long errors. +# pylint: disable=line-too-long + +# For now copied from: +# https://code.google.com/p/libfwsi/wiki/ShellFolderIdentifiers + +# TODO: store these in a database or equiv. + +DESCRIPTIONS = { + u'00020d75-0000-0000-c000-000000000046': u'Inbox', + u'00020d76-0000-0000-c000-000000000046': u'Inbox', + u'00c6d95f-329c-409a-81d7-c46c66ea7f33': u'Default Location', + u'0142e4d0-fb7a-11dc-ba4a-000ffe7ab428': u'Biometric Devices (Biometrics)', + u'025a5937-a6be-4686-a844-36fe4bec8b6d': u'Power Options', + u'031e4825-7b94-4dc3-b131-e946b44c8dd5': u'Users Libraries', + u'04731b67-d933-450a-90e6-4acd2e9408fe': u'Search Folder', + u'05d7b0f4-2121-4eff-bf6b-ed3f69b894d9': u'Taskbar (Notification Area Icons)', + u'0afaced1-e828-11d1-9187-b532f1e9575d': u'Folder Shortcut', + u'0cd7a5c0-9f37-11ce-ae65-08002b2e1262': u'Cabinet File', + u'0df44eaa-ff21-4412-828e-260a8728e7f1': u'Taskbar and Start Menu', + u'11016101-e366-4d22-bc06-4ada335c892b': u'Internet Explorer History and Feeds Shell Data Source for Windows Search', + u'1206f5f1-0569-412c-8fec-3204630dfb70': u'Credential Manager', + u'13e7f612-f261-4391-bea2-39df4f3fa311': u'Windows Desktop Search', + u'15eae92e-f17a-4431-9f28-805e482dafd4': u'Install New Programs (Get Programs)', + u'1723d66a-7a12-443e-88c7-05e1bfe79983': u'Previous Versions Delegate Folder', + u'17cd9488-1228-4b2f-88ce-4298e93e0966': u'Default Programs (Set User Defaults)', + u'1a9ba3a0-143a-11cf-8350-444553540000': u'Shell Favorite Folder', + u'1d2680c9-0e2a-469d-b787-065558bc7d43': u'Fusion Cache', + u'1f3427c8-5c10-4210-aa03-2ee45287d668': u'User Pinned', + u'1f43a58c-ea28-43e6-9ec4-34574a16ebb7': u'Windows Desktop Search MAPI Namespace Extension Class', + u'1f4de370-d627-11d1-ba4f-00a0c91eedba': u'Search Results - Computers (Computer Search Results Folder, Network Computers)', + u'1fa9085f-25a2-489b-85d4-86326eedcd87': u'Manage Wireless Networks', + u'208d2c60-3aea-1069-a2d7-08002b30309d': u'My Network Places', + u'20d04fe0-3aea-1069-a2d8-08002b30309d': u'My Computer', + u'21ec2020-3aea-1069-a2dd-08002b30309d': u'Control Panel', + u'2227a280-3aea-1069-a2de-08002b30309d': u'Printers and Faxes', + u'241d7c96-f8bf-4f85-b01f-e2b043341a4b': u'Workspaces Center (Remote Application and Desktop Connections)', + u'2559a1f0-21d7-11d4-bdaf-00c04f60b9f0': u'Search', + u'2559a1f1-21d7-11d4-bdaf-00c04f60b9f0': u'Help and Support', + u'2559a1f2-21d7-11d4-bdaf-00c04f60b9f0': u'Windows Security', + u'2559a1f3-21d7-11d4-bdaf-00c04f60b9f0': u'Run...', + u'2559a1f4-21d7-11d4-bdaf-00c04f60b9f0': u'Internet', + u'2559a1f5-21d7-11d4-bdaf-00c04f60b9f0': u'E-mail', + u'2559a1f7-21d7-11d4-bdaf-00c04f60b9f0': u'Set Program Access and Defaults', + u'267cf8a9-f4e3-41e6-95b1-af881be130ff': u'Location Folder', + u'26ee0668-a00a-44d7-9371-beb064c98683': u'Control Panel', + u'2728520d-1ec8-4c68-a551-316b684c4ea7': u'Network Setup Wizard', + u'28803f59-3a75-4058-995f-4ee5503b023c': u'Bluetooth Devices', + u'289978ac-a101-4341-a817-21eba7fd046d': u'Sync Center Conflict Folder', + u'289af617-1cc3-42a6-926c-e6a863f0e3ba': u'DLNA Media Servers Data Source', + u'2965e715-eb66-4719-b53f-1672673bbefa': u'Results Folder', + u'2e9e59c0-b437-4981-a647-9c34b9b90891': u'Sync Setup Folder', + u'2f6ce85c-f9ee-43ca-90c7-8a9bd53a2467': u'File History Data Source', + u'3080f90d-d7ad-11d9-bd98-0000947b0257': u'Show Desktop', + u'3080f90e-d7ad-11d9-bd98-0000947b0257': u'Window Switcher', + u'323ca680-c24d-4099-b94d-446dd2d7249e': u'Common Places', + u'328b0346-7eaf-4bbe-a479-7cb88a095f5b': u'Layout Folder', + u'335a31dd-f04b-4d76-a925-d6b47cf360df': u'Backup and Restore Center', + u'35786d3c-b075-49b9-88dd-029876e11c01': u'Portable Devices', + u'36eef7db-88ad-4e81-ad49-0e313f0c35f8': u'Windows Update', + u'3c5c43a3-9ce9-4a9b-9699-2ac0cf6cc4bf': u'Configure Wireless Network', + u'3f6bc534-dfa1-4ab4-ae54-ef25a74e0107': u'System Restore', + u'4026492f-2f69-46b8-b9bf-5654fc07e423': u'Windows Firewall', + u'418c8b64-5463-461d-88e0-75e2afa3c6fa': u'Explorer Browser Results Folder', + u'4234d49b-0245-4df3-b780-3893943456e1': u'Applications', + u'437ff9c0-a07f-4fa0-af80-84b6c6440a16': u'Command Folder', + u'450d8fba-ad25-11d0-98a8-0800361b1103': u'My Documents', + u'48e7caab-b918-4e58-a94d-505519c795dc': u'Start Menu Folder', + u'5399e694-6ce5-4d6c-8fce-1d8870fdcba0': u'Control Panel command object for Start menu and desktop', + u'58e3c745-d971-4081-9034-86e34b30836a': u'Speech Recognition Options', + u'59031a47-3f72-44a7-89c5-5595fe6b30ee': u'Shared Documents Folder (Users Files)', + u'5ea4f148-308c-46d7-98a9-49041b1dd468': u'Mobility Center Control Panel', + u'60632754-c523-4b62-b45c-4172da012619': u'User Accounts', + u'63da6ec0-2e98-11cf-8d82-444553540000': u'Microsoft FTP Folder', + u'640167b4-59b0-47a6-b335-a6b3c0695aea': u'Portable Media Devices', + u'645ff040-5081-101b-9f08-00aa002f954e': u'Recycle Bin', + u'64693913-1c21-4f30-a98f-4e52906d3b56': u'CLSID_AppInstanceFolder', + u'67718415-c450-4f3c-bf8a-b487642dc39b': u'Windows Features', + u'6785bfac-9d2d-4be5-b7e2-59937e8fb80a': u'Other Users Folder', + u'67ca7650-96e6-4fdd-bb43-a8e774f73a57': u'Home Group Control Panel (Home Group)', + u'692f0339-cbaa-47e6-b5b5-3b84db604e87': u'Extensions Manager Folder', + u'6dfd7c5c-2451-11d3-a299-00c04f8ef6af': u'Folder Options', + u'7007acc7-3202-11d1-aad2-00805fc1270e': u'Network Connections (Network and Dial-up Connections)', + u'708e1662-b832-42a8-bbe1-0a77121e3908': u'Tree property value folder', + u'71d99464-3b6b-475c-b241-e15883207529': u'Sync Results Folder', + u'72b36e70-8700-42d6-a7f7-c9ab3323ee51': u'Search Connector Folder', + u'78f3955e-3b90-4184-bd14-5397c15f1efc': u'Performance Information and Tools', + u'7a9d77bd-5403-11d2-8785-2e0420524153': u'User Accounts (Users and Passwords)', + u'7b81be6a-ce2b-4676-a29e-eb907a5126c5': u'Programs and Features', + u'7bd29e00-76c1-11cf-9dd0-00a0c9034933': u'Temporary Internet Files', + u'7bd29e01-76c1-11cf-9dd0-00a0c9034933': u'Temporary Internet Files', + u'7be9d83c-a729-4d97-b5a7-1b7313c39e0a': u'Programs Folder', + u'8060b2e3-c9d7-4a5d-8c6b-ce8eba111328': u'Proximity CPL', + u'8343457c-8703-410f-ba8b-8b026e431743': u'Feedback Tool', + u'85bbd920-42a0-1069-a2e4-08002b30309d': u'Briefcase', + u'863aa9fd-42df-457b-8e4d-0de1b8015c60': u'Remote Printers', + u'865e5e76-ad83-4dca-a109-50dc2113ce9a': u'Programs Folder and Fast Items', + u'871c5380-42a0-1069-a2ea-08002b30309d': u'Internet Explorer (Homepage)', + u'87630419-6216-4ff8-a1f0-143562d16d5c': u'Mobile Broadband Profile Settings Editor', + u'877ca5ac-cb41-4842-9c69-9136e42d47e2': u'File Backup Index', + u'88c6c381-2e85-11d0-94de-444553540000': u'ActiveX Cache Folder', + u'896664f7-12e1-490f-8782-c0835afd98fc': u'Libraries delegate folder that appears in Users Files Folder', + u'8e908fc9-becc-40f6-915b-f4ca0e70d03d': u'Network and Sharing Center', + u'8fd8b88d-30e1-4f25-ac2b-553d3d65f0ea': u'DXP', + u'9113a02d-00a3-46b9-bc5f-9c04daddd5d7': u'Enhanced Storage Data Source', + u'93412589-74d4-4e4e-ad0e-e0cb621440fd': u'Font Settings', + u'9343812e-1c37-4a49-a12e-4b2d810d956b': u'Search Home', + u'96437431-5a90-4658-a77c-25478734f03e': u'Server Manager', + u'96ae8d84-a250-4520-95a5-a47a7e3c548b': u'Parental Controls', + u'98d99750-0b8a-4c59-9151-589053683d73': u'Windows Search Service Media Center Namespace Extension Handler', + u'98f275b4-4fff-11e0-89e2-7b86dfd72085': u'CLSID_StartMenuLauncherProviderFolder', + u'992cffa0-f557-101a-88ec-00dd010ccc48': u'Network Connections (Network and Dial-up Connections)', + u'9a096bb5-9dc3-4d1c-8526-c3cbf991ea4e': u'Internet Explorer RSS Feeds Folder', + u'9c60de1e-e5fc-40f4-a487-460851a8d915': u'AutoPlay', + u'9c73f5e5-7ae7-4e32-a8e8-8d23b85255bf': u'Sync Center Folder', + u'9db7a13c-f208-4981-8353-73cc61ae2783': u'Previous Versions', + u'9f433b7c-5f96-4ce1-ac28-aeaa1cc04d7c': u'Security Center', + u'9fe63afd-59cf-4419-9775-abcc3849f861': u'System Recovery (Recovery)', + u'a00ee528-ebd9-48b8-944a-8942113d46ac': u'CLSID_StartMenuCommandingProviderFolder', + u'a3c3d402-e56c-4033-95f7-4885e80b0111': u'Previous Versions Results Delegate Folder', + u'a5a3563a-5755-4a6f-854e-afa3230b199f': u'Library Folder', + u'a5e46e3a-8849-11d1-9d8c-00c04fc99d61': u'Microsoft Browser Architecture', + u'a6482830-08eb-41e2-84c1-73920c2badb9': u'Removable Storage Devices', + u'a8a91a66-3a7d-4424-8d24-04e180695c7a': u'Device Center (Devices and Printers)', + u'aee2420f-d50e-405c-8784-363c582bf45a': u'Device Pairing Folder', + u'afdb1f70-2a4c-11d2-9039-00c04f8eeb3e': u'Offline Files Folder', + u'b155bdf8-02f0-451e-9a26-ae317cfd7779': u'Delegate folder that appears in Computer', + u'b2952b16-0e07-4e5a-b993-58c52cb94cae': u'DB Folder', + u'b4fb3f98-c1ea-428d-a78a-d1f5659cba93': u'Other Users Folder', + u'b98a2bea-7d42-4558-8bd1-832f41bac6fd': u'Backup And Restore (Backup and Restore Center)', + u'bb06c0e4-d293-4f75-8a90-cb05b6477eee': u'System', + u'bb64f8a7-bee7-4e1a-ab8d-7d8273f7fdb6': u'Action Center Control Panel', + u'bc476f4c-d9d7-4100-8d4e-e043f6dec409': u'Microsoft Browser Architecture', + u'bc48b32f-5910-47f5-8570-5074a8a5636a': u'Sync Results Delegate Folder', + u'bd84b380-8ca2-1069-ab1d-08000948f534': u'Microsoft Windows Font Folder', + u'bdeadf00-c265-11d0-bced-00a0c90ab50f': u'Web Folders', + u'be122a0e-4503-11da-8bde-f66bad1e3f3a': u'Windows Anytime Upgrade', + u'bf782cc9-5a52-4a17-806c-2a894ffeeac5': u'Language Settings', + u'c291a080-b400-4e34-ae3f-3d2b9637d56c': u'UNCFATShellFolder Class', + u'c2b136e2-d50e-405c-8784-363c582bf43e': u'Device Center Initialization', + u'c555438b-3c23-4769-a71f-b6d3d9b6053a': u'Display', + u'c57a6066-66a3-4d91-9eb9-41532179f0a5': u'Application Suggested Locations', + u'c58c4893-3be0-4b45-abb5-a63e4b8c8651': u'Troubleshooting', + u'cb1b7f8c-c50a-4176-b604-9e24dee8d4d1': u'Welcome Center (Getting Started)', + u'd2035edf-75cb-4ef1-95a7-410d9ee17170': u'DLNA Content Directory Data Source', + u'd20ea4e1-3957-11d2-a40b-0c5020524152': u'Fonts', + u'd20ea4e1-3957-11d2-a40b-0c5020524153': u'Administrative Tools', + u'd34a6ca6-62c2-4c34-8a7c-14709c1ad938': u'Common Places FS Folder', + u'd426cfd0-87fc-4906-98d9-a23f5d515d61': u'Windows Search Service Outlook Express Protocol Handler', + u'd4480a50-ba28-11d1-8e75-00c04fa31a86': u'Add Network Place', + u'd450a8a1-9568-45c7-9c0e-b4f9fb4537bd': u'Installed Updates', + u'd555645e-d4f8-4c29-a827-d93c859c4f2a': u'Ease of Access (Ease of Access Center)', + u'd5b1944e-db4e-482e-b3f1-db05827f0978': u'Softex OmniPass Encrypted Folder', + u'd6277990-4c6a-11cf-8d87-00aa0060f5bf': u'Scheduled Tasks', + u'd8559eb9-20c0-410e-beda-7ed416aecc2a': u'Windows Defender', + u'd9ef8727-cac2-4e60-809e-86f80a666c91': u'Secure Startup (BitLocker Drive Encryption)', + u'daf95313-e44d-46af-be1b-cbacea2c3065': u'CLSID_StartMenuProviderFolder', + u'dffacdc5-679f-4156-8947-c5c76bc0b67f': u'Delegate folder that appears in Users Files Folder', + u'e17d4fc0-5564-11d1-83f2-00a0c90dc849': u'Search Results Folder', + u'e211b736-43fd-11d1-9efb-0000f8757fcd': u'Scanners and Cameras', + u'e345f35f-9397-435c-8f95-4e922c26259e': u'CLSID_StartMenuPathCompleteProviderFolder', + u'e413d040-6788-4c22-957e-175d1c513a34': u'Sync Center Conflict Delegate Folder', + u'e773f1af-3a65-4866-857d-846fc9c4598a': u'Shell Storage Folder Viewer', + u'e7de9b1a-7533-4556-9484-b26fb486475e': u'Network Map', + u'e7e4bc40-e76a-11ce-a9bb-00aa004ae837': u'Shell DocObject Viewer', + u'e88dcce0-b7b3-11d1-a9f0-00aa0060fa31': u'Compressed Folder', + u'e95a4861-d57a-4be1-ad0f-35267e261739': u'Windows SideShow', + u'e9950154-c418-419e-a90a-20c5287ae24b': u'Sensors (Location and Other Sensors)', + u'ed50fc29-b964-48a9-afb3-15ebb9b97f36': u'PrintHood delegate folder', + u'ed7ba470-8e54-465e-825c-99712043e01c': u'All Tasks', + u'ed834ed6-4b5a-4bfe-8f11-a626dcb6a921': u'Personalization Control Panel', + u'edc978d6-4d53-4b2f-a265-5805674be568': u'Stream Backed Folder', + u'f02c1a0d-be21-4350-88b0-7367fc96ef3c': u'Computers and Devices', + u'f1390a9a-a3f4-4e5d-9c5f-98f3bd8d935c': u'Sync Setup Delegate Folder', + u'f3f5824c-ad58-4728-af59-a1ebe3392799': u'Sticky Notes Namespace Extension for Windows Desktop Search', + u'f5175861-2688-11d0-9c5e-00aa00a45957': u'Subscription Folder', + u'f6b6e965-e9b2-444b-9286-10c9152edbc5': u'History Vault', + u'f8c2ab3b-17bc-41da-9758-339d7dbf2d88': u'Previous Versions Results Folder', + u'f90c627b-7280-45db-bc26-cce7bdd620a4': u'All Tasks', + u'f942c606-0914-47ab-be56-1321b8035096': u'Storage Spaces', + u'fb0c9c8a-6c50-11d1-9f1d-0000f8757fcd': u'Scanners & Cameras', + u'fbf23b42-e3f0-101b-8488-00aa003e56f8': u'Internet Explorer', + u'fe1290f0-cfbd-11cf-a330-00aa00c16e65': u'Directory', + u'ff393560-c2a7-11cf-bff4-444553540000': u'History', +} diff --git a/plaso/winreg/__init__.py b/plaso/winreg/__init__.py new file mode 100644 index 0000000..0c8696c --- /dev/null +++ b/plaso/winreg/__init__.py @@ -0,0 +1,17 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT 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/plaso/winreg/cache.py b/plaso/winreg/cache.py new file mode 100644 index 0000000..dc78e31 --- /dev/null +++ b/plaso/winreg/cache.py @@ -0,0 +1,142 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Interface and plugins for caching of Windows Registry objects.""" + +import abc + +from plaso.lib import errors +from plaso.lib import registry + + +class WinRegistryCache(object): + """Class that implements the Windows Registry objects cache. + + There are some values that are valid for the duration of an entire run + against an image, such as code_page, etc. + + However there are other values that should only be valid for each + Windows Registry file, such as a current_control_set. The Windows Registry + objects cache is designed to store those short lived cache values, so they + can be calculated once for each Windows Registry file, yet do not live + across all files parsed within an image. + """ + + def __init__(self): + """Initialize the cache object.""" + super(WinRegistryCache, self).__init__() + self.attributes = {} + + def BuildCache(self, hive, reg_type): + """Builds up the cache. + + Args: + hive: The WinRegistry object. + reg_type: The Registry type, eg. "SYSTEM", "NTUSER". + """ + for _, cl in WinRegCachePlugin.classes.items(): + try: + plugin = cl(reg_type) + value = plugin.Process(hive) + if value: + self.attributes[plugin.ATTRIBUTE] = value + except errors.WrongPlugin: + pass + + +class WinRegCachePlugin(object): + """Class that implement the Window Registry cache plugin interface.""" + + __metaclass__ = registry.MetaclassRegistry + __abstract = True + + # Define the needed attributes. + ATTRIBUTE = '' + + REG_TYPE = '' + REG_KEY = '' + + def __init__(self, reg_type): + """Initialize the plugin. + + Args: + reg_type: The detected Windows Registry type. This value should match + the REG_TYPE value defined by the plugins. + """ + super(WinRegCachePlugin, self).__init__() + if self.REG_TYPE.lower() != reg_type.lower(): + raise errors.WrongPlugin(u'Not the correct Windows Registry type.') + + def Process(self, hive): + """Extract the correct key and get the value. + + Args: + hive: The Windows Registry hive object (instance of WinRegistry). + """ + if not self.REG_KEY: + return + + key = hive.GetKeyByPath(self.REG_KEY) + + if not key: + return + + return self.GetValue(key) + + @abc.abstractmethod + def GetValue(self, key): + """Extract the attribute from the provided key.""" + + +class CurrentControl(WinRegCachePlugin): + """Fetch information about the current control set.""" + + ATTRIBUTE = 'current_control_set' + + REG_TYPE = 'SYSTEM' + REG_KEY = '\\Select' + + def GetValue(self, key): + """Extract current control set information.""" + value = key.GetValue('Current') + + if not value and not value.DataIsInteger(): + return None + + key_number = value.data + + # If the value is Zero then we need to check + # other keys. + # The default behavior is: + # 1. Use the "Current" value. + # 2. Use the "Default" value. + # 3. Use the "LastKnownGood" value. + if key_number == 0: + default_value = key.GetValue('Default') + lastgood_value = key.GetValue('LastKnownGood') + + if default_value and default_value.DataIsInteger(): + key_number = default_value.data + + if not key_number: + if lastgood_value and lastgood_value.DataIsInteger(): + key_number = lastgood_value.data + + if key_number <= 0 or key_number > 999: + return None + + return u'ControlSet{0:03d}'.format(key_number) diff --git a/plaso/winreg/cache_test.py b/plaso/winreg/cache_test.py new file mode 100644 index 0000000..37086ee --- /dev/null +++ b/plaso/winreg/cache_test.py @@ -0,0 +1,49 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the Windows Registry objects cache.""" + +import unittest + +from plaso.winreg import cache +from plaso.winreg import test_lib +from plaso.winreg import winregistry + + +class CacheTest(test_lib.WinRegTestCase): + """Tests for the Windows Registry objects cache.""" + + def testBuildCache(self): + """Tests creating a Windows Registry objects cache.""" + registry = winregistry.WinRegistry( + winregistry.WinRegistry.BACKEND_PYREGF) + + test_file = self._GetTestFilePath(['SYSTEM']) + file_entry = self._GetTestFileEntry(test_file) + winreg_file = registry.OpenFile(file_entry, codepage='cp1252') + + winreg_cache = cache.WinRegistryCache() + + # Test if this function does not raise an exception. + winreg_cache.BuildCache(winreg_file, 'SYSTEM') + + self.assertEqual( + winreg_cache.attributes['current_control_set'], 'ControlSet001') + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/winreg/interface.py b/plaso/winreg/interface.py new file mode 100644 index 0000000..86526e2 --- /dev/null +++ b/plaso/winreg/interface.py @@ -0,0 +1,227 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The interface for Windows Registry related objects.""" + +import abc + + +class WinRegKey(object): + """Abstract class to represent the Windows Registry key interface.""" + + PATH_SEPARATOR = u'\\' + + @abc.abstractproperty + def path(self): + """The path of the key.""" + + @abc.abstractproperty + def name(self): + """The name of the key.""" + + @abc.abstractproperty + def offset(self): + """The offset of the key within the Windows Registry file.""" + + @abc.abstractproperty + def last_written_timestamp(self): + """The last written time of the key represented as a timestamp.""" + + @abc.abstractproperty + def number_of_values(self): + """The number of values within the key.""" + + @abc.abstractmethod + def GetValue(self, name): + """Retrieves a value by name. + + Args: + name: Name of the value or an empty string for the default value. + + Returns: + An instance of a Windows Registry value object (WinRegValue) if + a corresponding value was found or None if not. + """ + + @abc.abstractmethod + def GetValues(self): + """Retrieves all values within the key. + + Yields: + Windows Registry value objects (instances of WinRegValue) that represent + the values stored within the key. + """ + + @abc.abstractproperty + def number_of_subkeys(self): + """The number of subkeys within the key.""" + + @abc.abstractmethod + def GetSubkey(self, name): + """Retrive a subkey by name. + + Args: + name: The relative path of the current key to the desired one. + + Returns: + The subkey with the relative path of name or None if not found. + """ + + @abc.abstractmethod + def GetSubkeys(self): + """Retrieves all subkeys within the key. + + Yields: + Windows Registry key objects (instances of WinRegKey) that represent + the subkeys stored within the key. + """ + + +class WinRegValue(object): + """Abstract class to represent the Windows Registry value interface.""" + + REG_NONE = 0 + REG_SZ = 1 + REG_EXPAND_SZ = 2 + REG_BINARY = 3 + REG_DWORD = 4 + REG_DWORD_LITTLE_ENDIAN = 4 + REG_DWORD_BIG_ENDIAN = 5 + REG_LINK = 6 + REG_MULTI_SZ = 7 + REG_RESOURCE_LIST = 8 + REG_FULL_RESOURCE_DESCRIPTOR = 9 + REG_RESOURCE_REQUIREMENT_LIST = 10 + REG_QWORD = 11 + + _DATA_TYPE_STRINGS = { + 0: u'REG_NONE', + 1: u'REG_SZ', + 2: u'REG_EXPAND_SZ', + 3: u'REG_BINARY', + 4: u'REG_DWORD_LE', + 5: u'REG_DWORD_BE', + 6: u'REG_LINK', + 7: u'REG_MULTI_SZ', + 8: u'REG_RESOURCE_LIST', + 9: u'REG_FULL_RESOURCE_DESCRIPTOR', + 10: u'REG_RESOURCE_REQUIREMENT_LIST', + 11: u'REG_QWORD' + } + + def __init__(self): + """Default constructor for the Windows Registry value.""" + self._data = u'' + + @abc.abstractproperty + def name(self): + """The name of the value.""" + + @abc.abstractproperty + def offset(self): + """The offset of the value within the Windows Registry file.""" + + @abc.abstractproperty + def data_type(self): + """Numeric value that contains the data type.""" + + @property + def data_type_string(self): + """String representation of the data type.""" + return self._DATA_TYPE_STRINGS.get(self.data_type, u'UNKNOWN') + + @abc.abstractproperty + def raw_data(self): + """The value data as a byte string.""" + + @abc.abstractproperty + def data(self): + """The value data as a native Python object.""" + + def DataIsInteger(self): + """Determines, based on the data type, if the data is an integer. + + The data types considered strings are: REG_DWORD (REG_DWORD_LITTLE_ENDIAN), + REG_DWORD_BIG_ENDIAN and REG_QWORD. + + Returns: + True if the data is an integer, false otherwise. + """ + return self.data_type in [ + self.REG_DWORD, self.REG_DWORD_BIG_ENDIAN, self.REG_QWORD] + + def DataIsString(self): + """Determines, based on the data type, if the data is a string. + + The data types considered strings are: REG_SZ and REG_EXPAND_SZ. + + Returns: + True if the data is a string, false otherwise. + """ + return self.data_type in [self.REG_SZ, self.REG_EXPAND_SZ] + + def DataIsMultiString(self): + """Determines, based on the data type, if the data is a multi string. + + The data types considered multi strings are: REG_MULTI_SZ. + + Returns: + True if the data is a multi string, false otherwise. + """ + return self.data_type == self.REG_MULTI_SZ + + def DataIsBinaryData(self): + """Determines, based on the data type, if the data is binary data. + + The data types considered binary data are: REG_BINARY. + + Returns: + True if the data is a multi string, false otherwise. + """ + return self.data_type == self.REG_BINARY + + +class WinRegFile(object): + """Abstract class to represent the Windows Registry file interface.""" + + def __init__(self): + """Default constructor for the Windows Registry file.""" + self._mounted_key_path = u'' + + @abc.abstractmethod + def Open(self, file_object, codepage='cp1252'): + """Opens the Windows Registry file. + + Args: + file_object: The file-like object of the Windows Registry file. + codepage: Optional codepage for ASCII strings, default is cp1252. + """ + + @abc.abstractmethod + def Close(self): + """Closes the Windows Registry file.""" + + @abc.abstractmethod + def GetKeyByPath(self, registry_path): + """Retrieves a specific key defined by the Registry path. + + Args: + path: the Registry path. + + Returns: + The key (instance of WinRegKey) if available or None otherwise. + """ diff --git a/plaso/winreg/path_expander.py b/plaso/winreg/path_expander.py new file mode 100644 index 0000000..d8bdcaf --- /dev/null +++ b/plaso/winreg/path_expander.py @@ -0,0 +1,81 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2014 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The Windows Registry key path expander.""" + + +class WinRegistryKeyPathExpander(object): + """Class that implements the Windows Registry key path expander object.""" + + def __init__(self, reg_cache=None): + """Initialize the path expander object. + + Args: + reg_cache: Optional Registry objects cache (insance of WinRegistryCache). + """ + super(WinRegistryKeyPathExpander, self).__init__() + self._reg_cache = reg_cache + + def ExpandPath(self, key_path, pre_obj=None): + """Expand a Registry key path based on attributes in pre calculated values. + + A Registry key path may contain paths that are attributes, based on + calculations from either preprocessing or based on each individual + Windows Registry file. + + An attribute is defined as anything within a curly bracket, eg. + "\\System\\{my_attribute}\\Path\\Keyname". If the attribute my_attribute + is defined in either the preprocessing object or the Registry objects + cache it's value will be replaced with the attribute name, e.g. + "\\System\\MyValue\\Path\\Keyname". + + If the Registry path needs to have curly brackets in the path then + they need to be escaped with another curly bracket, eg + "\\System\\{my_attribute}\\{{123-AF25-E523}}\\KeyName". In this + case the {{123-AF25-E523}} will be replaced with "{123-AF25-E523}". + + Args: + key_path: The Registry key path before being expanded. + pre_obj: Optional preprocess object that contains stored values from + the image. + + Returns: + A Registry key path that's expanded based on attribute values. + + Raises: + KeyError: If an attribute name is in the key path yet not set in + either the Registry objects cache nor in the preprocessing + object a KeyError will be raised. + """ + expanded_key_path = u'' + key_dict = {} + if self._reg_cache: + key_dict.update(self._reg_cache.attributes.items()) + + if pre_obj: + key_dict.update(pre_obj.__dict__.items()) + + try: + expanded_key_path = key_path.format(**key_dict) + except KeyError as exception: + raise KeyError(u'Unable to expand path with error: {0:s}'.format( + exception)) + + if not expanded_key_path: + raise KeyError(u'Unable to expand path, no value returned.') + + return expanded_key_path diff --git a/plaso/winreg/test_lib.py b/plaso/winreg/test_lib.py new file mode 100644 index 0000000..db92d3c --- /dev/null +++ b/plaso/winreg/test_lib.py @@ -0,0 +1,220 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Windows Registry related functions and classes for testing.""" + +import construct +import os +import unittest + +from dfvfs.lib import definitions +from dfvfs.path import factory as path_spec_factory +from dfvfs.resolver import resolver as path_spec_resolver + +from plaso.winreg import interface + + +class TestRegKey(interface.WinRegKey): + """Implementation of the Registry key interface for testing.""" + + def __init__(self, path, last_written_timestamp, values, offset=0, + subkeys=None): + """An abstract object for a Windows Registry key. + + This implementation is more a manual one, so it can be used for + testing the Registry plugins without requiring a full blown + Windows Registry file to extract key values. + + Args: + path: The full key name and path. + last_written_timestamp: An integer containing the the last written + timestamp of the Registry key. + values: A list of TestRegValue values this key holds. + offset: A byte offset into the Windows Registry file where the entry lies. + subkeys: A list of subkeys this key has. + """ + super(TestRegKey, self).__init__() + self._name = None + self._path = path + self._last_written_timestamp = last_written_timestamp + self._values = values + self._offset = offset + if subkeys is None: + self._subkeys = [] + else: + self._subkeys = subkeys + + @property + def path(self): + """The path of the key.""" + return self._path + + @property + def name(self): + """The name of the key.""" + if not self._name and self._path: + self._name = self._path.split(self.PATH_SEPARATOR)[-1] + return self._name + + @property + def offset(self): + """The offset of the key within the Windows Registry file.""" + return self._offset + + @property + def last_written_timestamp(self): + """The last written time of the key represented as a timestamp.""" + return self._last_written_timestamp + + def number_of_values(self): + """The number of values within the key.""" + return len(self._values) + + def GetValue(self, name): + """Return a WinRegValue object for a specific Registry key path.""" + for value in self._values: + if value.name == name: + return value + + def GetValues(self): + """Return a list of all values from the Registry key.""" + return self._values + + def number_of_subkeys(self): + """The number of subkeys within the key.""" + return len(self._subkeys) + + def GetSubkey(self, name): + """Retrieve a subkey by name. + + Args: + name: The relative path of the current key to the desired one. + + Returns: + The subkey with the relative path of name or None if not found. + """ + for subkey in self._subkeys: + if subkey.name == name: + return subkey + return + + def GetSubkeys(self): + """Return a list of all subkeys.""" + return self._subkeys + + +class TestRegValue(interface.WinRegValue): + """Implementation of the Registry value interface for testing.""" + + _INT32_BIG_ENDIAN = construct.SBInt32('value') + _INT32_LITTLE_ENDIAN = construct.SLInt32('value') + _INT64_LITTLE_ENDIAN = construct.SLInt64('value') + + def __init__(self, name, data, data_type, offset=0): + """Set up the test reg value object.""" + super(TestRegValue, self).__init__() + self._name = name + self._data = data + self._data_type = data_type + self._offset = offset + self._type_str = '' + + @property + def name(self): + """The name of the value.""" + return self._name + + @property + def offset(self): + """The offset of the value within the Windows Registry file.""" + return self._offset + + @property + def data_type(self): + """Numeric value that contains the data type.""" + return self._data_type + + @property + def raw_data(self): + """The value data as a byte string.""" + return self._data + + @property + def data(self): + """The value data as a native Python object.""" + if not self._data: + return None + + if self._data_type in [self.REG_SZ, self.REG_EXPAND_SZ, self.REG_LINK]: + try: + return unicode(self._data.decode('utf-16-le')) + except UnicodeError: + pass + + elif self._data_type == self.REG_DWORD and len(self._data) == 4: + return self._INT32_LITTLE_ENDIAN.parse(self._data) + + elif self._data_type == self.REG_DWORD_BIG_ENDIAN and len(self._data) == 4: + return self._INT32_BIG_ENDIAN.parse(self._data) + + elif self._data_type == self.REG_QWORD and len(self._data) == 8: + return self._INT64_LITTLE_ENDIAN.parse(self._data) + + elif self._data_type == self.REG_MULTI_SZ: + try: + utf16_string = unicode(self._data.decode('utf-16-le')) + return filter(None, utf16_string.split('\x00')) + except UnicodeError: + pass + + return self._data + + +class WinRegTestCase(unittest.TestCase): + """The unit test case for winreg.""" + + _TEST_DATA_PATH = os.path.join(os.getcwd(), 'test_data') + + # Show full diff results, part of TestCase so does not follow our naming + # conventions. + maxDiff = None + + def _GetTestFilePath(self, path_segments): + """Retrieves the path of a test file relative to the test data directory. + + Args: + path_segments: the path segments inside the test data directory. + + Returns: + A path of the test file. + """ + # Note that we need to pass the individual path segments to os.path.join + # and not a list. + return os.path.join(self._TEST_DATA_PATH, *path_segments) + + def _GetTestFileEntry(self, path): + """Retrieves the test file entry. + + Args: + path: the path of the test file. + + Returns: + The test file entry (instance of dfvfs.FileEntry). + """ + path_spec = path_spec_factory.Factory.NewPathSpec( + definitions.TYPE_INDICATOR_OS, location=path) + return path_spec_resolver.Resolver.OpenFileEntry(path_spec) diff --git a/plaso/winreg/utils.py b/plaso/winreg/utils.py new file mode 100644 index 0000000..3a7e0f7 --- /dev/null +++ b/plaso/winreg/utils.py @@ -0,0 +1,44 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains Windows Registry utility functions.""" + +from plaso.winreg import interface + + +def WinRegBasename(path): + """Determines the basename for a Windows Registry path. + + Trailing key separators are igored. + + Args: + path: a Windows registy path with \\ as the key separator. + + Returns: + The basename (or last path segment). + """ + # Strip trailing key separators. + while path and path[-1] == interface.WinRegKey.PATH_SEPARATOR: + path = path[:-1] + if path: + _, _, path = path.rpartition(interface.WinRegKey.PATH_SEPARATOR) + return path + +# TOOD: create a function to return the values as a dict. +# this function should replace the repeated code blocks in multiple plugins. + +# TODO: create a function to extract string data from a registry value. diff --git a/plaso/winreg/winpyregf.py b/plaso/winreg/winpyregf.py new file mode 100644 index 0000000..a551591 --- /dev/null +++ b/plaso/winreg/winpyregf.py @@ -0,0 +1,384 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Pyregf specific implementation for the Windows Registry file access.""" + +import logging + +from plaso.lib import errors +from plaso.lib import timelib +from plaso.winreg import interface + +import pyregf + + +if pyregf.get_version() < '20130716': + raise ImportWarning('WinPyregf requires at least pyregf 20130716.') + + +class WinPyregfKey(interface.WinRegKey): + """Implementation of a Windows Registry key using pyregf.""" + + def __init__(self, pyregf_key, parent_path=u'', root=False): + """Initializes a Windows Registry key object. + + Args: + pyregf_key: An instance of a pyregf.key object. + parent_path: The path of the parent key. + root: A boolean key indicating we are dealing with a root key. + """ + super(WinPyregfKey, self).__init__() + self._pyregf_key = pyregf_key + # Adding few checks to make sure the root key is not + # invalid in plugin checks (root key is equal to the + # path separator). + if parent_path == self.PATH_SEPARATOR: + parent_path = u'' + if root: + self._path = self.PATH_SEPARATOR + else: + self._path = self.PATH_SEPARATOR.join( + [parent_path, self._pyregf_key.name]) + + # pylint: disable=method-hidden + @property + def path(self): + """The path of the key.""" + return self._path + + # pylint: disable=function-redefined,arguments-differ,method-hidden + @path.setter + def path(self, value): + """Set the value of the path explicitly.""" + self._path = value + + @property + def name(self): + """The name of the key.""" + return self._pyregf_key.name + + @property + def offset(self): + """The offset of the key within the Windows Registry file.""" + return self._pyregf_key.offset + + @property + def last_written_timestamp(self): + """The last written time of the key represented as a timestamp.""" + return timelib.Timestamp.FromFiletime( + self._pyregf_key.get_last_written_time_as_integer()) + + @property + def number_of_values(self): + """The number of values within the key.""" + return self._pyregf_key.number_of_values + + def GetValue(self, name): + """Retrieves a value by name. + + Args: + name: Name of the value or an empty string for the default value. + + Returns: + A Windows Registry value object (instance of WinRegValue) if + a corresponding value was found or None if not. + """ + # Value names are not unique and pyregf provides first match for + # the value. If this becomes problematic this method needs to + # be changed into a generator, iterating through all returned value + # for a given name. + pyregf_value = self._pyregf_key.get_value_by_name(name) + if pyregf_value: + return WinPyregfValue(pyregf_value) + return None + + @property + def number_of_subkeys(self): + """The number of subkeys within the key.""" + return self._pyregf_key.number_of_sub_keys + + def GetValues(self): + """Retrieves all values within the key. + + Yields: + Windows Registry value objects (instances of WinRegValue) that represent + the values stored within the key. + """ + for pyregf_value in self._pyregf_key.values: + yield WinPyregfValue(pyregf_value) + + def GetSubkey(self, name): + """Retrive a subkey by name. + + Args: + name: The relative path of the current key to the desired one. + + Returns: + The subkey with the relative path of name or None if not found. + """ + subkey = self._pyregf_key.get_sub_key_by_name(name) + + if subkey: + return WinPyregfKey(subkey, self.path) + + path_subkey = self._pyregf_key.get_sub_key_by_path(name) + if path_subkey: + path, _, _ = name.rpartition('\\') + path = u'\\'.join([self.path, path]) + return WinPyregfKey(path_subkey, path) + + def GetSubkeys(self): + """Retrieves all subkeys within the key. + + Yields: + Windows Registry key objects (instances of WinRegKey) that represent + the subkeys stored within the key. + """ + for pyregf_key in self._pyregf_key.sub_keys: + yield WinPyregfKey(pyregf_key, self.path) + + +class WinPyregfValue(interface.WinRegValue): + """Implementation of a Windows Registry value using pyregf.""" + + def __init__(self, pyregf_value): + """Initializes a Windows Registry value object. + + Args: + pyregf_value: An instance of a pyregf.value object. + """ + super(WinPyregfValue, self).__init__() + self._pyregf_value = pyregf_value + self._type_str = '' + + @property + def name(self): + """The name of the value.""" + return self._pyregf_value.name + + @property + def offset(self): + """The offset of the value within the Windows Registry file.""" + return self._pyregf_value.offset + + @property + def data_type(self): + """Numeric value that contains the data type.""" + return self._pyregf_value.type + + @property + def raw_data(self): + """The value data as a byte string.""" + try: + return self._pyregf_value.data + except IOError: + raise errors.WinRegistryValueError( + 'Unable to read data from value: {0:s}'.format( + self._pyregf_value.name)) + + @property + def data(self): + """The value data as a native Python object.""" + if self._pyregf_value.type in [ + self.REG_SZ, self.REG_EXPAND_SZ, self.REG_LINK]: + try: + return self._pyregf_value.data_as_string + except IOError: + pass + + elif self._pyregf_value.type in [ + self.REG_DWORD, self.REG_DWORD_BIG_ENDIAN, self.REG_QWORD]: + try: + return self._pyregf_value.data_as_integer + except (IOError, OverflowError): + # TODO: Rethink this approach. The value is not -1, but we cannot + # return the raw data, since the calling plugin expects an integer + # here. + return -1 + + # TODO: Add support for REG_MULTI_SZ to pyregf. + elif self._pyregf_value.type == self.REG_MULTI_SZ: + if self._pyregf_value.data is None: + return u'' + + try: + utf16_string = unicode(self._pyregf_value.data.decode('utf-16-le')) + return filter(None, utf16_string.split('\x00')) + except UnicodeError: + pass + + return self._pyregf_value.data + + +class WinPyregfFile(interface.WinRegFile): + """Implementation of a Windows Registry file pyregf.""" + + def __init__(self): + """Initializes a Windows Registry key object.""" + super(WinPyregfFile, self).__init__() + self._pyregf_file = pyregf.file() + self.name = '' + self._base_key = None + + def Open(self, file_entry, codepage='cp1252'): + """Opens the Windows Registry file. + + Args: + file_entry: The file entry object. + name: The name of the file. + codepage: Optional codepage for ASCII strings, default is cp1252. + """ + # TODO: Add a more elegant error handling to this issue. There are some + # code pages that are not supported by the parent library. However we + # need to properly set the codepage so the library can properly interpret + # values in the Registry. + try: + self._pyregf_file.set_ascii_codepage(codepage) + + except (TypeError, IOError): + logging.error(( + u'Unable to set the Windows Registry file codepage: {0:s}. ' + u'Ignoring provided value.').format(codepage)) + + self._file_object = file_entry.GetFileObject() + self._pyregf_file.open_file_object(self._file_object) + + self._base_key = self._pyregf_file.get_root_key() + + # TODO: move to a pyvfs like Registry sub-system. + self.name = file_entry.name + + def Close(self): + """Closes the Windows Registry file.""" + self._pyregf_file.close() + self._file_object.close() + + def GetKeyByPath(self, path): + """Retrieves a specific key defined by the Registry path. + + Args: + path: the Registry path. + + Returns: + The key (instance of WinRegKey) if available or None otherwise. + """ + if not path: + return None + + if not self._base_key: + return None + + pyregf_key = self._base_key.get_sub_key_by_path(path) + + if not pyregf_key: + return None + + if pyregf_key.name == self._base_key.name: + root = True + else: + root = False + + parent_path, _, _ = path.rpartition(interface.WinRegKey.PATH_SEPARATOR) + return WinPyregfKey(pyregf_key, parent_path, root) + + +class WinRegistry(object): + """Provides access to the Windows Registry file.""" + # TODO: deprecate this class. + + def __init__(self, file_entry, codepage='cp1252'): + """Constructor for the Registry object. + + Args: + file_entry: A file entry object. + codepage: The codepage of the Registry hive, used for string + representation. + """ + self._pyregf_file = pyregf.file() + + try: + # TODO: Add a more elegant error handling to this issue. There are some + # code pages that are not supported by the parent library. However we + # need to properly set the codepage so the library can properly interpret + # values in the Registry. + self._pyregf_file.set_ascii_codepage(codepage) + except (TypeError, IOError): + logging.error( + u'Unable to set the Registry codepage to: {}. Not setting it'.format( + codepage)) + + file_object = file_entry.GetFileObject() + self._pyregf_file.open_file_object(file_object) + + def GetRoot(self): + """Return the root key of the Registry hive.""" + key = WinPyregfKey(self._pyregf_file.get_root_key()) + # Change root key name to avoid key based plugins failing. + key.path = '' + return key + + def GetKey(self, key): + """Return a Registry key as a WinPyregfKey object.""" + if not key: + return None + + my_key = self._pyregf_file.get_key_by_path(key) + if not my_key: + return None + + path, _, _ = key.rpartition('\\') + + return WinPyregfKey(my_key, path) + + def __contains__(self, key): + """Check if a certain Registry key exists within the hive.""" + try: + return bool(self.GetKey(key)) + except KeyError: + return False + + def GetAllSubkeys(self, key): + """Generator that returns all sub keys of any given Registry key. + + Args: + key: A Windows Registry key string or object (instance of WinPyregfKey). + + Yields: + Windows Registry key objects (instances of WinPyregfKey) that represent + the subkeys stored within the key. + """ + # TODO: refactor this function. + # TODO: remove the hasattr check. + if not hasattr(key, 'GetSubkeys'): + key = self.GetKey(key) + + for subkey in key.GetSubkeys(): + yield subkey + if subkey.number_of_subkeys != 0: + for s in self.GetAllSubkeys(subkey): + yield s + + def __iter__(self): + """Default iterator, returns all subkeys of the Windows Registry file.""" + root = self.GetRoot() + for key in self.GetAllSubkeys(root): + yield key + + +def GetLibraryVersion(): + """Return the pyregf and libregf version.""" + return pyregf.get_version() diff --git a/plaso/winreg/winpyregf_test.py b/plaso/winreg/winpyregf_test.py new file mode 100644 index 0000000..881551e --- /dev/null +++ b/plaso/winreg/winpyregf_test.py @@ -0,0 +1,61 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for the pyregf Windows Registry back-end.""" + +import unittest + +from plaso.winreg import test_lib +from plaso.winreg import winpyregf + + +class RegistryUnitTest(test_lib.WinRegTestCase): + """Tests for the pyregf Windows Registry back-end.""" + + def _KeyPathCompare(self, winreg_file, key_path): + """Retrieves a key from the file and checks if the path key matches. + + Args: + winreg_file: the Windows Registry file (instance of WinPyregfFile). + key_path: the key path to retrieve and compare. + """ + key = winreg_file.GetKeyByPath(key_path) + self.assertEquals(key.path, key_path) + + def testListKeys(self): + test_file = self._GetTestFilePath(['NTUSER.DAT']) + file_entry = self._GetTestFileEntry(test_file) + winreg_file = winpyregf.WinRegistry(file_entry) + keys = list(winreg_file) + + # Count the number of Registry keys in the hive. + self.assertEquals(len(keys), 1126) + + def testWinPyregf(self): + test_file = self._GetTestFilePath(['NTUSER.DAT']) + file_entry = self._GetTestFileEntry(test_file) + winreg_file = winpyregf.WinPyregfFile() + winreg_file.Open(file_entry) + + self._KeyPathCompare(winreg_file, u'\\') + self._KeyPathCompare(winreg_file, u'\\Printers') + self._KeyPathCompare(winreg_file, u'\\Printers\\Connections') + self._KeyPathCompare(winreg_file, u'\\Software') + + +if __name__ == '__main__': + unittest.main() diff --git a/plaso/winreg/winregistry.py b/plaso/winreg/winregistry.py new file mode 100644 index 0000000..9999858 --- /dev/null +++ b/plaso/winreg/winregistry.py @@ -0,0 +1,149 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2013 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the Windows Registry class.""" + +from plaso.winreg import interface +from plaso.winreg import winpyregf + + +class WinRegistry(object): + """Class to provided a uniform way to access the Windows Registry.""" + + BACKEND_PYREGF = 1 + + _KNOWN_KEYS = { + 'NTUSER.DAT': '\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer', + 'SAM': '\\SAM\\Domains\\Account\\Users', + 'SECURITY': '\\Policy\\PolAdtEv', + 'SOFTWARE': '\\Microsoft\\Windows\\CurrentVersion\\App Paths', + 'SYSTEM': '\\Select', + } + + # TODO: this list is not finished yet and will need some more research. + # For now an empty string represent the root and None an unknown or + # not mounted. + _FILENAME_MOUNTED_PATHS = { + 'DEFAULT': None, + 'NTUSER.DAT': 'HKEY_CURRENT_USER', + 'NTUSER.MAN': None, + 'REG.DAT': '', + 'SAM': 'HKEY_LOCAL_MACHINE\\SAM', + 'SECURITY': 'HKEY_LOCAL_MACHINE\\Security', + 'SOFTWARE': 'HKEY_LOCAL_MACHINE\\Software', + 'SYSTEM': 'HKEY_LOCAL_MACHINE\\System', + 'SYSCACHE.HVE': None, + 'SYSTEM.DAT': 'HKEY_LOCAL_MACHINE', + 'USERDIFF': None, + 'USERS.DAT': 'HKEY_USERS', + 'USRCLASS.DAT': 'HKEY_CURRENT_USER\\Software\\Classes', + } + + def __init__(self, backend=1): + """Initializes the Windows Registry. + + Args: + backend: The back-end to use to read the Registry structures, the + default is 1 (pyregf). + """ + self._backend = backend + self._files = {} + + @classmethod + def GetMountedPath(cls, filename): + """Determines the mounted path based on the filename. + + Args: + filename: The name of the Windows Registry file. + + Returns: + The mounted path if successful or None otherwise. + """ + return cls._FILENAME_MOUNTED_PATHS.get(filename.upper(), None) + + def OpenFile(self, file_entry, codepage='cp1252'): + """Opens the file object based on the back-end. + + Args: + file_entry: The file entry object. + codepage: Optional extended ASCII string codepage. The default is cp1252. + + Returns: + The a Windows Registry file (instance of WinRegFile) if successful + or None otherwise. + """ + winreg_file = None + + if self._backend == self.BACKEND_PYREGF: + winreg_file = winpyregf.WinPyregfFile() + + if winreg_file: + winreg_file.Open(file_entry, codepage=codepage) + + return winreg_file + + def MountFile(self, winreg_file, mounted_path): + """Mounts a file in the Registry. + + Args: + winreg_file: The Windows Registry file (instance of WinRegFile). + mounted_path: The path of the key where the Windows Registry file + is mounted. + + Raises: + KeyError: if mounted path is already set. + ValueError: if mounted path is not set. + """ + if not mounted_path: + raise ValueError(u'Missing mounted path value') + + if mounted_path in self._files: + raise KeyError(u'Mounted path: {0:s} already set.'.format(mounted_path)) + + self._files[mounted_path] = winreg_file + + def GetKeyByPath(self, path): + """Retrieves a specific key defined by the Registry path. + + Returns: + The key (instance of WinRegKey) if available or None otherwise. + """ + mounted_path = None + if self._files: + for mounted_path in self._files.keys(): + if path.startswith(mounted_path): + break + + if not mounted_path: + return None + + winreg_file = self._files[mounted_path] + + mounted_path_length = len(mounted_path) + + if mounted_path.endswith(interface.WinRegKey.PATH_SEPARATOR): + mounted_path_length -= 1 + + path = path[mounted_path_length:] + + if not winreg_file: + return None + + winreg_key = winreg_file.GetKeyByPath(path) + + # TODO: correct the path of the key for the mounted location. + + return winreg_key diff --git a/plaso/winreg/winregistry_test.py b/plaso/winreg/winregistry_test.py new file mode 100644 index 0000000..69745b6 --- /dev/null +++ b/plaso/winreg/winregistry_test.py @@ -0,0 +1,51 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2012 The Plaso Project Authors. +# Please see the AUTHORS file for details on individual authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This file contains the tests for the Windows Registry library.""" + +import unittest + +from plaso.winreg import test_lib +from plaso.winreg import winregistry + + +class RegistryUnitTest(test_lib.WinRegTestCase): + """Tests for the Windows Registry library.""" + + def testMountFile(self): + """Tests mounting REGF files in the Registry.""" + registry = winregistry.WinRegistry( + winregistry.WinRegistry.BACKEND_PYREGF) + + test_file = self._GetTestFilePath(['SOFTWARE']) + file_entry = self._GetTestFileEntry(test_file) + winreg_file = registry.OpenFile(file_entry, codepage='cp1252') + + registry.MountFile(winreg_file, u'HKEY_LOCAL_MACHINE\\Software') + + test_file = self._GetTestFilePath(['NTUSER-WIN7.DAT']) + file_entry = self._GetTestFileEntry(test_file) + winreg_file = registry.OpenFile(file_entry, codepage='cp1252') + + with self.assertRaises(KeyError): + registry.MountFile(winreg_file, u'HKEY_LOCAL_MACHINE\\Software') + + registry.MountFile(winreg_file, u'HKEY_CURRENT_USER') + + +if __name__ == '__main__': + unittest.main() diff --git a/plasov1.2.0-rubanetra0.0.6-distribution.zip b/plasov1.2.0-rubanetra0.0.6-distribution.zip new file mode 100755 index 0000000000000000000000000000000000000000..0c74a671c7f532567ab90fb4596d4fb12fe347f0 GIT binary patch literal 40799744 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 z�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%0D<dY>MT4!zp?(Ei*(wns` zt7RuORHCLMuh@Zk$o!eCxbpT-EQ=&)i{|nGNG+ARF=<LC>N=c4O(xj_AvC<UA02Bx zO&OElgppb>N}Z2=gIM=joFKrVWpChePh}`v%V12m;?F}5t@>--X~lO<rL6`qXNSar z#?1WWzY-dvB3o?qzlDc{(fH@}1}7KdPV)*Q-$ZX*GR*eiD%H9>bHGWlQ6q<c;k|hE zI@RZRr0f1jVB%wrc-nI)B>Ssx{`0tT;~p=->X3$vRWh7+z@ILv6BH~a5`=M7fMxt` zJ4$WgnS0u>(2ms(1;%=&_v?=L34R1?GYbij>Nlqi*H$CJ(e@NAN*kKX`k>SKx_>rB z<0Xg*3OEQ(A*uGl4zjp56X5eE4f*X>c+wCs7ZwNhp-dV9h>z2yslwR6q9$er>lEi$ zXxmtJgnT2)0G4c*ZI9`U9>J#|C(doHU6R{LNqFIP2fZKY$}Tb5O2yg~x)W1V^XQ~b z`gj;0F#Fi8qVnX^N{rND8?-gq-{`{5P4?>rAcC*4KSDU}y-Jz)%uH_h{L{mVOL+uI z@deI4=-D|8kQ^2&H$b0!0Mu8`L%DYI_-$R`%PS<jXXL@&jT!yHz98?fGY}F$eW0$X z)#@2mG#CBl#n0J|kw^J;odWgp!d|$+$c2U!)3Y5g8bz;z72;VVjnS+j;vx{+$$j4O zx2cV$gRif(wzH*h`>M-KU@9Ohw*wu>A<%4ZGDsH7o{C}BT-JrnGjzlKlT}D%7Z*-T zvYdLSAG2uJu(BNTCQMLt@r+3Zh-a9=h3%Wlp;4zJW-59SSW10zd}ECtS|kQ;>3&NC zKsN<(G&)p!K0MOtF1tQjh;JXzmIpo~b*O_8)2_d;%3}u&)n$oUexbIL^w4Sj*y1y* z2oOcTr~d=wkIlL7*P*z-UE?_W3;n3SJ9*+UTG~T~1Te$;lYf&0$+(%F4zw8YQk6^f zrhlQdxA~S`^77rB;v;X@Zn)ZMlp%Excm{S`JRSDL!@#4}sG3W#7}$-aCp)2D`VH23 z`J3kO&%%Tns}x_+%Eca`K3QNTW3g^gM@yF_u{I7n(D&kNe>H}itK1qIm-w^IzNo6W z*RW%0%b2c2+mu$cSG|96HzbKAwo*-=k*WS@6|c)^Qc-(yRPbq5pgpi~jXUE-^J>ag znlT*C#gH<{SaJgL(E^AkL0&sSX|BZZ*93Q!r0gbdJ$Dq2*_@bp;$(UlqB>bpBdEJs zhGax^60^VY!O>m`cc!}eCgLH~F7T&6kf7|@L6DQU53?(^c)iDu#BMEV0Zz%80AEz@ z4aAn+jx8dde&D3QC(!zQvU8lNuBiz?RXd#;XHwVl{&p||!X(RCt5b1VhP@NGlvbYw z<VF2nGeS=?&!|VHLas6XcuRptzH7tKb;?0@SrYL%wnShpwfKCF2FxAoi{^2hv(Y9K zvo-^QIPgD7?sMFSvC>NS!ad!88y~Z&IZpvhw>E)zWwj*l)k)0q*SFA-Z8`s<Sx}Yf zcsGfs&8~cyrH!ZeukR+nRj$keZ9(*=&^qLnnCLFoZRxk&g?318?<Ts4Vm6p8OK6m^ z9wL#tQxmnx;RFdk8ZmO6C2i0U&*&2O=j|(ezYmrsQt69%67TC(GmRuG)3m`?tH@k8 zDpfO-7Uk!Sno{~RBWjb!sPatgEYIQ4h+Y-SVCii_gE_(XH4R_JuXP^S4RB(S88LuF zRq)rG^aAjGZkZw*+2d$x5-cRSBnexZ5#;m;reCtHunv8K0%IZ+@|!QZg4w(Q6?Y5v z+c#+tuuRCh{qb3S)JZr(?~TGRXB==($|;8Mer<R<9wy@?ZXSaOWg6y_3s_JD=~slL zM+8hWQrr+5!Fl1h-VKgmnx1x3Is_$y22CtJe*%f(qJ_bpr>IuHRg}8nnwJ(|{=ZM} zL-B%*AP{>vv?^jlB*LC+XP-^F3+OsF50@;3NS2vWCol;xhy<8EA1~_v#=!J_U$%NW zpF4$woK3>%@;%4SjGI8R*fXy3tDy|8N0mZ|FeKWKwBQSP2j))2$5p8XiVL05PRX%F zkkzmBS|`OM=L=+TSXQLNYEU4EuEl;A6=bI70KFJ|{8f0cH=0Icn6*ONoDX>jkF_Y) z({@*qpp~yG1U_uW%%@{1L3y1iQojohrt(N?n3lEH7@3`6;Al*D?>kycPF5c`inlaA z4?~>8|CMbvoD|P<CUYnB@k<@Wz`?0r&wRdJIzQEfl|6rsd>gGuR{uOamTY8hyM_*g zVEV}33PK03_!BR;4kRhf&oOCRrBc^RdcAVf7ZJi|(4>AMqgY;I(Kl@=G2-wL@vaw! zFK}tmAbcQ!2&rRuRt&QT#%@{W8opX5TDo(cs#5o&wrx_-w%u`i1BdR$VwQS}3XF2< z^;fgmcHcMjLlOI`JW#DL+QPnakS)(T_OyN%uC_XN59jWct>E(Msi<wK#WSuT((Y6b z(Z?vsk^0NNO4pSZe01uyIi#G%K8nvK?!DVp^T9&YC_CK-f>)uZbnFNYW5ZCbl6O4v z7yuw^xE6h0n`z8(&W}H#y};AA?n8XZHg=8cPdK2pixk$VS@MSqKCwY7A84E6MT(t@ z|7ky$=Z~Tz1GmC*IM1t7T^J*gea8N5KJeI&_<l&u`ndv-{QUXXZf|dQAaVbRp9@6I zHC+S_M7oxfJr_d;L_~;Wosb8q5UXqJkSUOOis%>80jPL;wPoHjuCA))$_gRb$jgqn zEMemAs-~u@#^>w#ulUf(%z)fb;#vD6nEGSm2R8D@w>y~ueH>F=6$?#Fpq~p{nrj)o z=%_cAI1%#8vhEDXS+a_9*=_pWT-`kIVH&VR7X*nw2@2&-&BN{iG9|=#K7M+9J~H3O zs@j_lo))F=<+6xWEk4AFdo|Mxg7VzogA^q(m3DiF@EpS4M**)WWFGCb;Q+1C<90>4 zAiB+`(ZZb%U|<L0ga}EEx2aFE8P!)5WX%RTa{?v~IIl7iFZAVCpkSsEOMFt(c|NBg zB1-Ut!6V&s`nbR}2Q`L0(9?ZvHxE?@|5|wH_B?3ta91`#sM{V&|AOB_`U#9l<)xo* zLG|vOHTaT)x#@Z(DNw%3*6q5)@2(t+h>~;euAxv&JJ57m%5W;zJ2_z8*}Sybwp!=O zl369%it8J^lfk&!P)f#AqeN2kNUSP4f1W0gu)PMH06t<Hv!m*k%`6Ap#)gJ^+~!o_ zpocEtaXNyS!41Xcc7+Msg6ofXOx!f&o>r&l97Gy>-sIlfzBShle}qI5FgK_QyH0;& zR?`&-77}zt+U_OVGl;iA1iVyf?7=Ifw~cV@QS!Dj1fy=4{6?m*BjA&b$6@YIljp~5 zs6opQ=iDWdbyAF_aFb(EsD?jgHUvoi^lDXDasmrSNqtm-Y^FoIJ{=EZPmfP@ClzOw zPee51U|KRJ#&xZ?VwF(ebJXD;!sX#O&0V1rH30`@MZl1CVaK8b)AXm~EH7YIb~pzo z4_F!!{R(!@@!1)TKPU~4y>9Mv{DEC;&C&_jJkJ(sy7@2<_h*QdI%JZ)oH8XP7xQ(b zWFMkOx+hf%mXa{ADc5_Vhik7=xnFoeeTKY}Xu>gI#7;qW3}qxzgl#(Rm8IzWBWIr+ z7q!^QR&f%F(vVh|w3O=9eQuvl7#sUVJ{0gBDFmPNM;l$TWS1-?rU*+}Ri=BMfPU%I zodP+5>#n-tb)-+xZ=S@%O5HA|%SwQYV&7a!5sH><%yry#v^JK5&R@^#$7~@b0G~oI zb8h=Gi4>%RCTj<bO=0d2EP6$sd%Eku^4v&CRe;0N8n|<4Jlj-vJ(7MeH0u~v-yR2B zE09fQ-_k<gEnbX@J5U{nS*M}=>!bh<JIxJtxt0;0@Ye>Rh(W*r8Cjj=Wi&cut-pKR zrHfIDGn~#qfeIU99E!t&7JhFTKpBF3*%ombK(KW_J%0D+<AfHm^Nn@FBfxj}N+pK2 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%0DIae1O4 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%0D^4_1q}O_(I(Eiew9) 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 eIMsc4Q`6`5G(aNrwejV91V*xI105Wx<Zw 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 *?)`y8uD zxBevK)}<0s!p{ol#<RlX`9ZYv$-`q;ae6N6wpO-?n8$ptBRj{h`W#z@s}@4;?7`hW zCfRcMVmdQF?b9&4TVqE}+Ul*68m4ykhM^%;h!a%s7~4Bu(z-3OW=9$&T{WwhP_ubo zPcbHzAZm;ZkUN&ne!<YdF!s%m=B_L=Kn3c<_bP7e*gn0xG5#rvL`aqt3Q`?e@ss2y zjq6BJAT@;d1PE}W5R(4s*dal%{FM+M`*ldf;}*Ck9^i(_?GFduaG&S$j3&5J2E%Fy z9#$8NVM-WP;+G>RO;c2?$Ywz;f=Fmxq|?sK0kM2b8zK+Am*JQDlE_O_;ecOBBwf?k zXz_jL*uDwY=K!0it24wqrQL<CyB%4~hBmM~*&=254;JV|VX-6{7d*u*<Y0jKkRdd2 zlDb79Lq+P9s+5$}Ja9p>3l%e>ByrTDW87-GDkukKD<;2X7|2>61x$&}iw!-FtY^@z zi#YUew!r4VQDgdaSi55S?4C!r2QPN4Xt9S=RImcq8$))FEnwoGst6xRmV2`Jc$I-r zl8hrOUHF~DqiNDXd0G>3XOar(${!pkuCw4Ff4Bz`Y|Rf26d`ZGk*KQnDx)AjK(&3- z^c&$xbcLrOA|zH?!V(;L-m-CnEOktU(7O0<0qVt~Z8s=C;Ii#PH6(Fgk#y``AVq+C zo`_L=uE>3w00tJK3gX&}L_}7RKzvsDxO2V*52{{KB)*}b{CJUo6j&(aBd&W~ma?k3 zRtq(v>P<(e{Iy7CUb7&v1puVbTP-6b-K$`nIcVlI;!F4`oeI=_t8`)3G#A*uNsZ`i zh+dSkpq6xcYkylC%3lGpj6jSzYYM|xG)%eHnO+KD`WdSIz&xNWI%0yqZbG-~z<2J8 zDBSEGlV?h@RC41(`dE#<1FQfR+9rQzRBGY8#qiqsGsd0a5{k;^k|!_8+`ZpWEMmm( zll4E{#1hsBBW!lMa~tzik#X!t)e<}Yfb&}!go^wPqW4qAIPKW$<^+f@W}a89kg3lS zF9K29!!n4)Rsp0qAjh5~o2RgRm)I<VWwBv2GVu3I_$lvG(SV(DS0RD^)%Ja{om=Ol zmNZBW?ATCY*=n1sFDEp(UT2}~`f)lG=ee3`iBkQG<%$y{j)nsM5g6;d^m2hsEo`=- z6@*3ZHBOZXA)^k(GaZ&QS-*1lO9-Pj4l8qANAjS0=QG4nzq>62I5DTK1L1E5E1kf7 zk9L{>awp*h#^Ax;g;LU*JbcB6<;xkE8*ynIUF+;_M!Xgmi7?3|MDwA=P1YtI*<B4= z=eO3RZ}noLD4llGFtyF1KKXZzVcDKcv*lE~Ve^eB`qi`K)%?8Y=}5XKzCJN|Uak`x zmdiQUF|)10K5$yb`r$%(2(1<v%f7$;dZ>|(Q#lKlQ=JoUotDRrN14<1`_ZHHLO8GQ zu5x#==B&xW)m_75ZQwy*pg}BZV{Yco`Igy|c(ph#A^eT9ndv--wQ8f%$wO}(omarR z9VgR3*i|B)iHUj5?pK4(xIL_CRUP}DO)?(qJ5~4m_Hhn7>=QD`rMe~R$A$=&_Zq(& zA@Sy3#0y*HWYkhP`7xWW9TtKW@@%yS)Ur%Zj5F*44iH|r$)CqfSq;kJJTLoKsN{4G zwHB8(XWE>che<SBcTlVE83YEecK<@pMkg$AUgwg!m(8W+%+IdKFMisZjIw|Qk3xkC zH<TRKv<BAOmy3^ZF{}O0;K@x6i(K1+K;50Iocn2`4W*hJu1@9JD`j%y<9V_6G0)?U zn<)W;5^~-l^^&8yNlT+YGNc2S$+9n682c+MBcFeVo)bB~8JTJ_(PgExf7dm>LkHS- zpK1@>$x&lo|ILbadN>VsGBn&|Pl%*kN1inE&)dOENY+4nJ9e{hrRL?Mx|_xynNBDE zg8Nnd%Zwh25AS(A_XE5Pdj$MlrKb48__*19#2|X7W1ZNRmI`fn`Rf|KUZ4l4>N>k? zb6?m;=pLy&NmcnsjJ8eSclap}`|^N?T{W1JvbQ}1TF_rcaj}E#e-)q3(Ih@PKPw3? zvj1Qxu{Zer+r-h<>EAp%gPQBMSfdC(OW1}(a0eF(Nyj?=9%{5Wx(t%KI9P?PK|$0| z(p6$lp3F4qXLUmu;Mb53gDzqf@9WlBWJ7!p1eVHb&#dz5*?Y6&B=YHFZZ0|(#{{#^ z<AaL~rn-?7Zmk$`5;Zg5KC;f^o5_lfl_amuxP!sLL%fm8;5b(b6b^T_qwe}Y#A+xv z<b&|$+eML3Kio)Rpv}ei^_;CO>Cy0#$jZBT>a;>`6yj-!>EsV2Jym9s1qJ|AzI)Jd z5UqGhtvbbeiW4B8G&`^ZG*u`#YF!vQaXd)b2{p3O9RGgWnvvuy@nk7Q$b}&2Dj-ko zTEPHwdS|V|OjBr4+?EY;hG59G+YE!aX@TuX`CPi48WnM|(w}J0wCI@OZ=bK58~Q@Y zWdq&nX(bu5Dc|f44<{aPC$P72r0e=bNCC;GnD7++Q!g=dz%%L->_q8PU_iB5=g}Rm zw1T!@Bq{LefC#UWyfg8DDI{oh2ltYqU3FvrqXd9%5vB(RmXf6NNOyOZoM~`=!!<|{ zGw)h==Zlk*i;tHFlZhRj1T|Z_-Ps$mTe|&veDM|M+aueg%QfIZ{w4e{p45^Hc!>um zB2B_6GBOiYK<Ks~TFBPDYF>aYCg<VR7#hbp>S_KQh5Cod(2+#|kI6kGhg!5{%5s>Z zu%2XOWU0QONr>tYb$-Gizl>6ou#$Q})hT=3&78tx1mb9t&K!ou2x?D$7F76Yof{HQ z&@xm{Q}!3fP7|lb3q&B~dK8SU{`cmT>vhrr<*`^I{$LYnUi#F)v+;3qBM_JNZ?K*` z(!W%g0K=5<fXY-t;PF&-?gN4VPIHBN;Ij`g`u(i$<3ab55K0H)t46Gcm}t%FyYx>j zbEl>k+)b(ovj6}~D89*%BnkS%#FNszYr8gn3Y?5~R9aEf!68?okt5_~9*D^CZc&%q zNQ}%`6uLz~G3M+<A<qD$#e=8vg%kSw^HSu7{28-*MsX6R^*ZdzBZ0pd_HnP{vA{$I zco|^0mXL%yjnx5mV3JBvQ8j_M8-f$1Rd<HY9`<hU2T%3}_tEZN@6uizITr}7jt}ef zl=v_P-bhuKD-P$_`-JD@-Lm|F7FnjX0*t#bL<FwxrNu$R^yA`UQ~TKxg|b%nDbel5 z80o}<YP=poCE*3<<7W@y_?d)WV3Zk-`|-fL18f1Cl6!!5fS_2$Kw=H6Zi!Pl;vy=2 zNG!3;S8HUWctAGjy6`zQNqP&ofgzmoAbwkiOULUnC0wQdevZ{yGDT`y|6s{bJ1-j} zK;keDV2v`RAlBgQdQk96G6P6d{B4Ye9o$yZgVmD0@Ea119!#cONl<o_O~zp@6_y>S zBkcDWkM2?^f1hr7&}S?Q5AqFGY!fo;R$s%8G&y8R|F%>1<8>tUu`xqHydOCDAxN*h zjHw*&2G(o!N2R^ZZ4V`@UkyKP{Q4CA=SYOs#3>P10DuRy0q(=6l632S9xeioDT#Fq z22+dO>M3#U<)cZL(V_3x<8}A3-^a~68G2zjw_GnmgSPckGr$2G(^Npv$dcpyCNw5$ zu-{ytM4*(f6y9so8tGfdq>jv7_|w$=ySz^^2&LM@&H}Zdx1Y1vN<O-L1NS4XCetX% zIwzgCaeK6jz8Dr>3hHvtttgrBOnsQV>v@!<s%r<)RZmiC%viq{mV8<PK)MDTxwc?4 z<sD3jr}LG6A9rTI+hULU#}cYSa|F)BSsf?8H@Nh*r}G;RLMmezNA6DhkGw3bnxwHG zK-v8pC~=WuUks_pnLAy6iA{;*K{N+Ntw79CEC3c4k_tKtB0oY^=4Qw(OoT6>>nlfl z^ZVui{BE!P`stxV0_?XJMkt;-4Pjmh7^y%u+u9g(_6Pk{<R*fqTm3J~+2IN;{gd59 z5E-3j_QaU4eV&%uI9WL(dN}mou5Z!BxJ@BEliVlMbUNgGrGyMQ_4D3QOR!mZ#0@JZ zRuNH@7jEwf$zihJm3!Je*n;boy@3sqMRmK4Uoake10Jr2yR)#{;ubDGT$*^xPnQSH zCBt{lq5@c~{S-$GZuIuB!ZNJjb_O;!Y^K%zlHLu?h++tjnetb)Q&!4c=$p{RPY!!i ze<nm_z8df2M#l;+DGq3V#WC6-1%|DBIS{xYC)<1wr=n->0xH}%8?UoBJyL0tox6&X zEKIkM@6w5+1@1bU6{h`x!LEjEkJ>vK)iiO#^Xy}_&`m+L=>41=bCcbIW_AK&2h*Hb z4cJqaJ<vKD(os`dt7t(}$iVLe@T`@%%cN#)D1?S+#%JfB-v10svE?l52XtOerFgJ1 z)MKjl?+3rcNNi{&)vE$Y@C?kAfCJ%2YUI?LP;Rdrb%R9W1NLokhPPGgMOLLUNo8Ab zAkJHL7*mgP4)PkiBwr)?gI4I)tK3e+V?Y9C4!EdHL!6w%!$^#D>M&qUS0U}$Q{{hg zu!HON>lJd_&GOo2jM0zkj5Bb3h58T(GxBl!g>#xy)ux3hQ;l0^iPdP0s)BKQNiCda z9J_^IDS(jXA;gOR1F`{1!Um!t%<@MQUS%@q3*P}n1Qze)S9|R@HWj2~a{8aa$5=_x z{4R~}b@34gepm#6%AsR5hK_GAYGD66TG22HTg!jmx$rfyTU;U(=F8d_ZE3K?U(_E5 z#wO%D@mnqs$f+IUfk+#^iCJb=7FM}}lH~Jx6fc3t=Hg~?IYwGAN`5V*(bG<HoCT@Q z{ju|;*dY=qU0=(?74NqI^9B4jm;ye(!fxbAvj%V6=jY-PE+_I&lniY*yc4u!r2em* z2AWzBVD>2n4>@KHo7ag}aTg#dPmR<i#K3I*cD;l3G_dO?$H@%MM2&#QRRQk8Z)riu zER4TJF|dG_12SKE06$G+<me6L?a9%hroB&XwSihxx^3Ril~%i(&h%S>!xd=$bXhPL zE4=&Z^0-~0Cw;30$w#CXUuFZJZ~UGnblX%q(#d%i>)&*#EC}a+8c%onmWp7t{Gh4- zw6&k$0COr3To&LgFpEd~O_YW{#j?6{5l7mM%TQyo<c)-i8UIGRczU7_Pdd+Si@?}! zcQy7r47S*+lkeHOMb6_HCero8umDU@h?VQzUkfNcq_MjR&tJ~JohZ6)iMZY}>L(o` zc^op*ez{E5cF`jd9@ptpbzEQA+TGn1YeMn{FZDH<YZV@dwXA3Cs8KaJQczY$AB=<r zJ3yb$&ezs@iAw$=kzOWgYZ9qnzm0Ys_ehR-K5txRzOCyUtNCT!;x_lNf1@(nwUyZr z&0SSP7nqVYdG-r!zNwZ|_zhd6EIVn{-|r>f)M}|4{lX+6$OILFuYaM+y8s8r=Ef>D zrUo)&X>+l$T`ph7w_)i%KD)!AYLhKStF0@-@jdWCYNbou{yF?%_N)=%JS~SkTEJrb zLhD|Zbw#tGDW(Rnc@lLYyrDN*WR(-&bQp8+mDY`F-AiKKzbxvx30BQ3_6v{v?abK$ z@e^9)VH?T6I9hLFvU)Pcm+wRGAM*8oZe+puA6+#QTQdt=6FL=TNC055#w6u`e4O2& z0RTZxes-=PKge`|e^tb!tMu*6KW$zcGynjE|G%$vwR5!kw>7Yxnww7hO$ffrdj2oC z#3Y<+u5vie8)b&?nlgy%jb5Fj#*CPuLBvPo0f78mSCgODn>t?bh2*QPg^v*;i0T*5 zUoN9+dRa@FS=D!U4IYeI8gROQm-V_$%O>s|nM@iry1$^eFggOr(CR!CUrnu6*6Z2q zRzFp1*8{9ZZIeDk3WFytbMt-!S=mANHtK6anCfp{e17uTU7=cMp#@YG{iG7O>6jBp z#>c6BN9xnjBEf<9_ujcticm2s#-+)niuWi!oa9I^wgpeI#XDrCRv^$aTg1rryMg|Y zY1HHR8Wu5EE{#Z3(?IVvD=ULVgDJSG#N9KZTrjX{aSkB}aQOTtK7e|m!3cRo(XQHe zj>IxV|7sdio}Zr|9v%)tstHrhnD48eFWH`^tFA^<tMS)cyX~S5T9OXaJJ<FvtRNer z(kDBl(y!Zzfl(f0lk8z4irW-R!lIuHvS?+E(T2>8KU^#IQprfJ=61q&>;PyOlyYIz zs(sxH^5)W-KtsyCHGl*2@SWzJxVkc3e!i~&ZErO~V_QzSe}A21(e5;=Kgat)J)Vz4 zBIQ!Bi+-q59N?u+C`1qnCDCL~@&KFLb}S`{-#cdqXdf0IVfC`!36vrZ7$nKJYZuV4 z)TRI?5pEv_W3|<jDKoNja0q-O{mN(V2~pr4Y`jn?j}aufaia&wE@Z5X&4A6^r7OaP zS(3a`Z}L$;2LXu%t?k{|S55{y!O=pjZZ$<=TPKlhdT<2?F32th9t!O%uI|y>7_xa} zw$ElO;Zl>7_+zyT$z`@&haBAs1x$rNVowWXWiJ;9wi_8}<x?d>vo|c#bvF%?UA!NH zqJ}9rLbq~@II(w-PbTGRMUOsndZ871My33wGV=<q_!=rJdn9`C^{1P*$No{alp!4a zs3rH(bR2fz3zL1xcm^2Em?~N|xat|b{>l2mIPYG@qF^^J<2N*d1b9M99z#F|$F{%x zVCXVG6gIS=fIx9PWyrl4D@r0*&5-b}Lk6&np=ABspG`?Ou-V?QlK#kj(x`U){ye1u zi>00-74mr&Hx>}KH*g0SNSKeD><sQI=xXa=AcI%nLUHZPS`7te?w0yPs03QbVvq{U zPUqc8W{A++MRQXfs_+%bQDj2YXz5A~dF)znE!ulVw(&#ro)ftULmmTnFz~oxnU5tB zge-O3i$O$O6KAN14bymMiK``@-G|!W2BKR6krTTwnP{)R*$yyNENT9K)7DPPxEgky zCBeeoYgm+1_8^(`bc-b~ZV*a&z)5@Bk)~a#^NaRuIaH$W6tT)tc2p0J_6Iyly`vQc z(q^J{kADq@>r9CX9hh9vKQOiz$c@gAd{#1U?D_~qjQhxL=s_%LH9Kc=TM3k)df6h4 zq2+uL;s%sRuTBidw(l3&VH_qWYqMWVcDT;^>IkJ_;7sTvIleg{3g#Wc?fd_VFs6`< z95W$ReKe)8Qd$J7d*;$LN2s#IhR0<^n}yN{bCg&uGxOB)QlIHUs3L}vr}{)KK~s}^ zBL7)0ox#!q0N_`W-4%Dd7F3Wr<Q51{k{z!v{uwQZ+=CFns1d*iQaZT&;+~MS2k=Yv z0$-INL$Laq5Z8XDa%jSCgqcA?t1Y^AQTx&@d0M;lK4D>If)j4-aqg8h$Xb%jlbwNS ze0rJjj|&BDwhfcxH!<d9@4$sK8lII>NePPR(7LGSUlfwS--&l0=IF1-U0#FQ+KCe9 z^BGrk@^czlS&bW(8icKOcXDz0tr2@WzpD1;1SD^k3h>NjE4s;#5#o5T18f=+S0w^y z!NSr{Gncq*Kq5sjUgP!_*MJl^!}CZulcD%{Nf_dy`Ir9&Rh1?-&_!1OFeP<9XC$W0 zkG}Pu&lZ1cyLXRDn+s}`!8)zh7CZAfWOyY!p=Q564=PWf(+lp2bcQ&WD(Y<4^9}~a zK$=^I!Xv#-&P?f(Bb7vQ-M<+Gi=;ZX&+P)0+`v*rQy2RL)`vf=1)*to-u8+9B^aNc zP}UQi`5O_?`coi3+BL(D+-r9#W1C>Iki&iBWt-&aDH=u@7xOs6J4zDJ5zB(bubri* z1yefJEyUtM)T}Y)6(C@R5*3?Iay?;W50<V0w-28;3%M_&dOZFVHYn6b+4Oyk3RP}g zn|vOYuZ7Jp*RDIN0RMdDuhX#8&Nh><{f>?fvJ$HvxL-^a*M?Py?twE6L*;wa8>NL* zVBiXdjc=klU3*F|wOD5Y^{*~>8rWJ(@3=&ILOSgF<%{|Cz$^56FP`^ZN>f^jSKDo_ z_KhF*s6bd7_Qw`}y`gZ&;?EgZYK{iknYLRa@KtOOU2Gj2DHrk6@0)NPdmHQH2Z%@D zT4;3E!RT09#rA`zlOP>IU1308C<T$E)i}pNmw-(ZQ0E}k7W|c?;OM_=Lw}_ja!eFY z93gOQXu016LIz9egirPr%anCyZ~hdckGZ(QFi)8m9|$qkttK6RiIm-Pv~%1X-I;X1 z#(yV#W<PSR>s>4wMYOs_AqiN*<6MVOqJ8o7AK|k3Pb>7__B^MY_n-J;PBlusm5V89 z+42Z#qMN^Qnf$q0MQ!nS%?q^H(aQg$kl(XSW4-~s*Vd}ICvQcDCm4LuZ(SwmT@O)D zd5)y{JW5o-<6!KponHGM7_g!>^Xfh3p=aX9#&vKxxetC}ae&oR3CNG5e&2j=RNDOK zH=G1K9fxsJQ|RqiQ|1GRlPK^x4uKI;e7j&f=<O`qiu<?)p=)oBgz9zqKJQNIM+iOi znGNU9q?S0#{5yY%Y`m=K2;R}ArJwf?J_ak!lzrRsgLJ-g9+RK*<IK~k6vXSr{Vp!y z+VwgOGD+sba^Rq+F)X{lpF^96*u+QMKdbgO8S<a2RuJIo%-wY_CqpisHZb5~fTtv~ z`{R=w`H%g;wWv_{u+piWzs*+z@|_%k#O=9S;8PE4&$w@ZZW6RYEfU53`wAb+2$#Wn z)3^`}LYiu@=JJEr#>Ov{_Zo6&kz58HM8hSbAc|(ZZC(RuGJX!io)GR}l^L`5%rQb5 z9XnmI7nFlY*;V`^hxqmKEh6p3z1vhvJ$8C&XCWV|t=0nF=wqSh6~%sTh~8j#6p_bp zufyw+ToU)JDE&(F7jnPB8Ndb_C01mQ33Z15tj?_>JDP-PplUAIpC`h04^8U*@-)X2 z-KMdUU4>o5)pcXNG&1WC+5qs1UjB1yqi43T$aixp{YWFJ`UdU_DX`_TRVs3MyWN#h zt^Ky@!W0FRJ3p+Px>fCI55~o3Cd9Xi=@N0#`&aFp(^`|Fm+wvg^7KM?L*o(s_SX~t z-@`WVK{*>$U(%&fF*y93V}|Mb`_d?YPE6;_6A(s60vb-ND;8RBqHyDCy3KdA9-#-` zqf`nml_&KSuTWvVdbQu~g82o1`7Tk~yIVWb7_CoO)wPo<*|aPv1c%;=f-?h8;eg~q zyo2N|Fwu1vSooZozK(e_d<8NuxJEiX@;kN~%qF=9=GYnG4c<SGe9)tT!F-lbrzyTQ zu8U=>-kh{MB36L=H~_Y>`!23*iywUSf(ol3ojWy$5?UA?{SWeATUP*Eo|X7{)}+`8 z(AE-OB`KL~(x{C8CJVHkRu0P6Zb1(LifKt%Pkr4$T#2AwWQ}evnt*$?v9Y&(3NCcH z#ppFSV+FA0w+l7c8z|&#)qg>I4>vhhZLDGLZ&zqG2;tK$*C|ATH%Io+U~Nv(NrbH) zGZ0$+VYej%B$O0%s9KH(=X|E6&(>cDPwO13pKVbA+mm=T|2VWBD}U3gbHBJ8CFm5Z zk@M-6Fj;IecZJE(zNAEQeu6fr4rP7(W@E;*U&%K!hks?@DZDnX(_Y_hh+j?O2`#XL z=v7rj1)6CA-3{7d!p@l?g9xTK68cR?YLsDZqGSB93!Ihkm^c2_oXg|-ynv*<p(;y1 z67=Y@dHhm*+Om|-X6PF~1~f=FgLa-`G*%e$d<6{RTW&Kmpqt`*VV(JcY2XP&_2(PP zUwqYv<qPI$MDHF@uwt;*IB%98xIqW7p`mKc&gX55-^C6GC;9~~qVqW?%mgUurBjdj z(cI}+&j)C|YQ$U2Ci2oNJ-7#i8ELX12-18C`<=L`(YLL;XcKM4(?!6r_0*?lMy;!@ zvN<gp<O5I&yQ}Z_+~ti&*kQ96vxm6R3T|t9mJ*+;J?}9N`_>mCB<2=j^^Jba5W%(4 zyo{dxm9gk$bwWk2l4><r&Ep8lmX`;kXP=1u_3OpX7I4eUGCPz%<}#8o19op5Kv#XP z(A&fO8*O20sI~hqk9HLxToYN#1>zvU&9k_5pAiGRX9%3@?4g2u7e)fgCnR|7Fg`$! zRu1`RIxrKu`vCRwWNv{wFJKL);Mx{0vtlw%Rw#NgfHnbNy7w<KUSqGI?IlL_XV<<D zE`uh<`;R3QAN8_~fE3g=J2#Jo+4KXjsTvvy@!70fN*_U}X@=)nSDbIqf1SsWJ+H^Y zpaB4w=>A`jq5m`D8QED|n;8AJ`;WTzE-`-s_#=-$0sKZhaS|94u84eRgIvwwEfkk! z7n&l0bg%(&Y>5qdY!g#+4T?93&+sqx=`?7L1scf<8JAgDAa(3)r=tzJevS{JlEi$b ziSQ|}vf>VNoOm<2>^KCudP7G^Nfw_cGHnZ-z6>W`y>M$rm4kzm7Y+<*jMmYzg9Fi< zoK^8STS(@hNWdpjGi63X3?itVHahTaz3rz__HhghCjrG`^JJ+$=y((hmy{xsNysV) zfH1sz1S(=F35`N&bVeu60KO*~;jgKn3BLgeFJVCt_Szb_HD5p|DcyJso%jGMMacu= zgkVq?K0_x^t-#WH_w3mpvOS@aQfCxNfMZ7!@eBM54Fu>52X~Y=yI_(j`j<>ndX5Wc z#2{gD`f_T71R0xs-M!KE$6mSbEzHXzI$cDjganAEo+%J94lQIBFc%Rg2&q#Wel8^^ z0?Zag^aZVOaSt;>oL42PGJS5msbZM5dU<d)wiABcFMzdtP>z!)i5g%2uFEISPYCml zGk<?wxRHvhOPgoQnf8-}b1g?7@5g+$6CJ(Yd6HAzPEx*yYF*(V|6+YC7e2HRKKisg zRE==TbFB<6u$he)>SOD~sCtAE6=_LM1LMByRC0c7Ml-`?iH0JOC&k=&vwCTTBDJNU za4m3f@DpE?IEwsK<+?S~J}6TC6ohFfdr9>}p@%7Sc}<#~1ZWXQ-n5>st2n7=DOstc z!z+outJt4|Jo_#!S`{p)UZQiio9aVK$-wXN>gYjo2Z}(pgcvA0AbsG|^?ATDtL(tx zqLvn0WLx<$tR*{{w$R~@Gzj1+NDJ16Xx`ZLaMJ>VaCQUGgI9~Rv1hFU>s6Yq$LNm? zRQ!J`i)|J=aSpskC7V7QK?42GjsjwO%9{G~{2RI^9P@Lus+J)1Nih6Ib@}NnOoE0U zF6U7R^uK5i3cRF6V>23-^W0l=b4M2p(CG-wG2@BNLx~{~q~>RoJ<M^^&G1qe;F_bL z0>%-wiL7ssElAfKL^^Ho4H#%oBIB48!2~9OfiuEbNPY6Jt$DTVZ>wc>0KcF@gJukv z_^UW>Pq><+&8`(cY&77dlk!UmcyO<%c!c3WVx{JVX^tyBS_<4X_$UO#ti{Eyar<iE z2Gl2zjk|02C<uWS>YB`K%cio<KVfk$uz^Z4{g)cs2YGVW)m^9Tt8W*7O2a!Iu69&8 zGu%dnlzK;IN3=GH82!SI?Xj{<{%A|}I}ZwD!pz}2o#myXAew@nKvkeD;M1saxp=a0 z+h%AkZPfI-UQ(*uNZF^d*-_=u;3!w1bOcluHSwp8OMt~13*Teo2%1^M1a0K{hi_4+ z3zhJwas1{mFYht_i{*Dl1Wwgv4h2NwJ{8Vfi7_<!4Fw6VT%4@hm!Ansu=B!R#-JAP ziz)_C2z5y#F$rxwc1#1Intt8|^E0|kDpUm7z&S??%p_wo4>u1tK&#KT&0By`{+3=S zKjdPMKZOXm=CX9T2Lgp`MWyoQh&s-x;*P1b96nESp)EpuM;9ESbq~}^tI}GVNU+1` z{!a~#LJo-;0{Hfb5Q{p>Ws;QDcQ6>!Zxjv}D%y@&QD@|g+~8$ezl2^9-`I{5xVBLl z*Bc)35t*nc-y;o#K45Wf%|ZjLI!(V52}u;#hSl8Rv^!9Y6n8idU+$rDjU!1zY6u&W zVb}%>;=>7Z1DFc{Ogd11<={*3_yBEt@G>zhSX9%8FVJ}IdC7`CxDF5tPC1Jp+>Dcr z#!)-UYk+KP%Xv6rbvUSQqZO5OOfWln{+eK$2<i6CvSSYyNNovIig$=XEt{=y^Jc?Z z!TH8Agwc>(*g@$gUH+TZU^iPu_ItBXQZax7tR=)iR3)u}Ae9SGoqpKg+LbMm!r9D= zU8`LEEP7Sk^|RHgyk{w>CNi7LHibnXxOM3v9g2Tu!M@`Pn`%$_E0}Y6(DhE3<^SX( z*e|wjB?9IO#bEzLG<R&c*08>WSigy1c0&?AchD?ZV`2cGsvHP6`I$XCo38Sh{A~i9 zXcrh49gWf}lH|#V*3EX*(J)6w5G<NisSr<Wt9GO5F1M+g?48LgM-m>Zd2u_sTE+FF z&$3*>mzzhO`N|QGR?n8w57!35EWoBz(+^u}@a!fMs-N?uhO1rWte=04JgIrveyQeu zndY`IO><D$J&CN3XczI;Fokj#s`yN)qOm&dvqP|z2SeAp?{QM;`-oY+FkrCO*Vc(# zJTtIpt^Fau;g@o_#YK#VaaCGfuoFJWcskO)f%?GB)+cu3SEZj7a*$ZfR`o3Zn}N%} zm;1;+?Hjs{HH0`W1HV9^7e%XfLyOZg3yp$YOl=mPr4ulfCJP2pDbywj4%@b9#dOJ@ z&@RGlX<#r@7T)~@8tFov_F_?awa6Ed+X-&_k+@m*p4IadYfT2jiUgTRi#=L+CeCYw ziSO?8a4~I{!|8LD`R%vy*Y)V7q2m@q!zd=x;Imk~B5#lnH?*6kp1U(>`K0g;k<;vw zF<ZzQk>u+92%Enz{Q>N<x};=ST$W35zJ!dFB$*b99ru0WWfSc7ven#evl~93zjmd4 ziSIXiT1t-u58jDVgPXu3h3?7mBBPteiIVh5&xGxkKv=ZIT}^?~s~yr344Z$FkvilW zL*gZd3U6!DNtC7Onre>_qq@DJCUyp8)4(MX%=$={geQ$bltH{rBmvfP!2ywpv)~4I zQ--J!V?%0zBE$)2LU|c$LIjqo=2CcVmSNeJr*lJDVw-3=$7u_hTa~ZYAx^<vz*D*6 zjWVhA6*O#0@;x-Yb6pyR4rCXu+H`k7d8Lx?TXpr~$>d3px1^mwSkUAxJ06sys6;Tx z1w_d#nA=&28cCX=ii^S7+=!AiaUIj-Tp|j)AoFsbSue)`f>9wDvkdGK08{~)y%B44 zC<Yy2U8-pTMYbS6xG{4;5nc(96if9e;|PFDY)F}3ihH<v03b7wP*ai?V)CH5WCWzt z`ntX>tk4lrQgzW26Eben*)(I3r5&;-*CP}^eY&Af@j<)qgKZ{QOSb*(J*rQ^YOoDP zM4W8%P|b8Q*rryAJ}Iwg0f8w^rjAIT%`};5B<d5$Qby2h)q%LN(_y8)UM%0*MRWqT z8nYcPf^t8O3w!(Ul(45!W-+}Srq7id90l8};ic`M^*fqfJ@_YHN7Q&O(t`J5rXd)I zR?-*SDxWIl=LVc(R)U_l(lfix==~$mjl0p&lVYvO-UgNF_j}H2ow#qzQjG9<jiqU^ zvl>tW9db{WP}MJ)JWGMasGO^Tp1^HTw6aLwex&O_rlo79>+jczclOusgI=XmgUg<~ z$yu%TU^Wn0NMq)AZSNT_5-cO#VL^TWitKrVg?@J(c<Rb(Cj3QpMrX<Brc%)e@QvNn z2Jd$nM4W391tPM~IYVqLhIG%RoP5@ht}oig&j^eC_61WYW0-jekDasSa++4tPLo(q zrSCD3%h=4G!ZPCz2=M-pLMtoy_>GXgHHX_SV%%H!Rh!BX7OsfaG{tSf88osx6XA!= z&OT{3`s_62V3&=dzEo2V{NFBDf&9cYpo&uyJecC$%!|)>Aw{4gWqqFL>(7{qndcJf z_Kah##z%n=Fj>Vp>5OR-4cHa$p&^S4bgG`XB@f(+hrL|1y8ZHaE?<<qR2pF$lM_Q- z)qlh7He=2?sLt$czy1&qwYM%p3bX1wMOsaC>6EWN!+&HbRTXO@dWR*YFU=Kb6=sJ| ze>_<alB#Xcp;0k!I%geapPad286t_UtCpSzAWTTb@mkiGkvfujEUvb3l!Q%Cz`hP~ z+j5DCb>CKtEcz7iECzNLr5Q8+M%W7=TxAjl?(h!H)PD+m$al)2O~cqa6k?O`Lsd>A zP7@fFjR$Bh9V4k0=)V7NL{UM?=+>Jbv46_=Z^XWng{_&jiJrZqoso&tzdJ~}#QN#7 zZTS)V{+z$08lf&P9`Xv7enrzM$*?6Gag1IdffEuy!rDp2CU4eUbAa!F5|gi{WlhLp z@m*B=1qM2G`@bK_l_XKhE;I5>=8Wqk%Wl;+`}&OcQOBRUOqDy{KmF9py90BjXnbjp zTvi+N<#fJNd9FaZpgAS|Z6|zhhLCbm0E`xV1@J_Jt_D#OtVP_R<>mQsa_=_8!hmiX zg+dLBA6NfNZNeCJP!LIp%d~|LfZRPIUnnVDDM_SY&dl*^SI^XJh&Mt|g8#POcZeH= zok5Fiz8g6BxY7{P*VIC_PLhB`(E@QmiTn<+8ca2TJDD36bbZ`WY#E(+WVi-QqzTMJ zX$X)vL@$FDd!GaW#<#t0;<%*b{d4PBxFQE-eO|r4Y+esFWx2WHyqTXa>m_kWkU}+d zn^FUy<lJn?37_o13ZL=F6D)skU9d+tjiB{iFgZ6JLus{m<6aOTS-iY1_Po5oSOp23 z+7*yrliP$0jjBmKtM(bKV$c$dy^ES(AP09PCF1=(Vm)N=F`VXl_3-Y=^0roetFH#j z#o?XmeR=Q6atK5msv1Gg-T$Yz7O{w&5P5a%q=OV-F{)dIR<JdW;3mnIstd(BgD0F2 z)J2^(T$pm92<VQm2XS0DxuY-%Q0eR7-~i-OuGb(|S=X2V(eNt@U8F6k>;}-55pb_A zA7pWmA(bM(GG)&iT7Uh`47Z9bK_-rbTCD$x1<^ETI@|&N)5`0+SyCCi=~?4eMg7F6 z)z%&j^hMN_>G%)OFY{9=sas`6xPTche~ol$Qi4o*YmvLQdBe8K*IUe~4^tb?9D1<1 z_Clw_=#)aExX=a=oJW#veE_*$^<2>Do@q8}2xH1|{2S89uf$Nc`Zr^KLIKa}aCs08 zI@hHbfJswR*?>k9h85|_<yV_zsQg58ogS9>;X6LTxJH=JbaZay_BEZG7<d_kmvAG_ zFo8b%EX-pqwu$(HrBry$3Yk~yFoU3T_=3;qn?68|J)juEC>XiL{CQS`8HYrgyUiBp zSAcuta>;N-c+dRFebHA}fJUI)GK2H{mGahJBS=@-3(e)#7v8<O1#o(e>jFQ5ow9V= z_Zn1$6)uouq4+zitY*wCroxWsJP#owA{FGq1hZiQM$pMf4rgI`+r8ZyGX6wPV&X`F zvgkh8z%z{>JfTRTV0Ll-O&EEtv+iJ_d4rVifcdd?8c|v7^;y-BGmd4~f|inRV(4u9 zEO^)$)LzB$(<esj{=zjE#XBjubBnpJL3tI5-6Qaejy=?<zYC#@?1L*mEBc_N98WXR z3T&yl2&Sb`n#fZ_m;jqYk&E#OF2e>0>>52_{|c|LKg+Lf*0hM|O=Wnj-<tpjZ;jT^ zT+B$7^vtb6eR`$m#B@2G7MHQ4voCS#YFagKr)>rs!oR>g=<o&J`DC~Z<_HMByCgeL zsb);wt7kD4Duq`Aya~wVLJoR$suFi%QR7awcS3oAxt*RBkdpO|>M!09@?~=FET-qg zsw`_+G5#`aljP(XZ%{m@&$XI`$2`jeoui1cf~xccqgv9Xxmcy>LM?_%z9dMO6N^_E z!)7PhG^~pFgQ&?Ln^h;W`^078w1;zh#lQ`M1zVs;YGDQ=Ot>v@qW4dko~ZA_^+6`5 zsylVSE2N*!%{E0eJ^TczfWzQmCQ-x=Jl(Y3NndmTS+1UcXw+SPk^|Qchw7TMOT5WS zZ<?vAis`VewF#hL0G+sgpkgZIU>F8~4+s^mU_JHKsV+0Hw4R`ItP-3QqA8u!Aao)b zDV2L_#Rl3Ugyv&e06_X2MlrE1vHE$X4Os>VK+X~Xd4&1GR!e%SzpN0EovXfUl4`Z) zoSt~c4FR)mh+=v*N?W6Epmq$%O51H@vBS7Sf5{5{jWvf_q^;P47&Hc1PwuVq;Zo6u zx#VEHcW*TGP?{GCGMe8$HQ5=M5coBt4e?ZsL>qMn#V^C|J@FojCKEIY3nB@g4F^es zX2alHI)S5J9w-Fc4?nb2Zn@}zKf?yq4G5yS>)X<Y^`!xINe5p8WlhopO>|)si7hru zf+B7)j3>a8+ijf#H^ThnO*d99j)l~X-qJiNyFR~s+^U?<0rzN3f#eXyU>%*zu%!){ z&^C8jq{dj%288?Dx{>on^_-Ploll&ndNxElX-qR1sDI{l<s?6@xN9TMkW+Y4RNTw^ zeU1#iLR4r$1&56{G+wOZAe##eLmIzbR=Jj`=+74uaX#yq+hrc=><sv?opu6!JNuzr zs*w>evdah7Iq?%_{c><IUP&=eKrxW!*vZ~CnW8c?MFbEodU+*rEgFe!8SQ4xXhT`N zQ-%lYswVK-iLF}LIfO6P-2%JZYC=<&2Ew#e^{(PoUQ`5S7Atdx$UbeI7@f>k%+h3D zsi}IRuLI+P;WI!N4?*mGKaao2A-ifTJG2#yHImcv{O5qqT>_$9V3Sx+wH=yM8bB!E zU^>)-P!k1`J&mb*R$5$+r#M-l2yls|*Hh=8$@iud^4P;gfLpG%2fJ%lS!%#0t>VCx zZKCZK#P*Q-?4R<w-|BdIw|0|v0-2on)Q~ey*jGlfjG>1fg{}~n;2`~oljnHnM8Zk{ z6p%Y4oS}Dss@Ke!Y}}UM^k(}pKfnFKa7TZg?B_t;_0pYpsbQZeuN!xg&8Kp^z;4@H zXT?MLSM+dXA~MFIQtY@IxuFl0vMqXJglO~$sCg<`X7(0!i&{F6#ayS`&9aII7oFqN zOJEjF97UbHhzHJilPOVPuqiI{jl95GR@23|8ZC$FLDE$D%?oV3JVwvCLKmR$W9bqy z_GjyvpxDRU)T2C$%mY*FrclOwLkdPB_c9o`i=G?vHM|324PhA=H-J@TvsB1I=Oq0w z71K0<1m)dm5<-iii|V@_5~kXI3)sKvst2n>FLkCL?5KkCIYC>Ea{gJP<wy^8qYj@0 z7yPtf3ln<32+yK*x(&WO&9C0q%B~GaMrquxoH1$GB9Q0LAqJ_|XFLzqIBzACl<gL9 zPl~<#7D(S=7wpfl{ugEM*qvz@ZRrLT+qRub^2SNUwr$(CZQC{~wr$&X#X8yJbdNK3 zk3M^M|AFV@TF;vIoY&k$;N}<g4FsFXWVC!Uhw`tD<AeD{M>`yB-`w>}wyI0)-6EO< z2gg9QI6?l?nLqhlnlxs_{%@#B%@7c<-b5roVKWo1b2yrE#;fq5$yE37ANEID=$t5O z!PD;(y-jsb?1>VNHplkw_IeH~>lePc>_SC6@iySf(u*eA9M2p6)U6eQeY7^4T&`TI z;92BUd$3Q{2lOOS%Re(pP-O5gBKy&!{f`z)ynonTbYIbT+*`fYIyw*dzeWn*2)^V$ zdoOAReuR+MJ+8Ak(Y3MImPG>F^6M5Q)hry|Ow8b?6<w^ITe$Hw)x?wGfee|jl0dv@ z3mzvy9;dlUE>W$kMEg%e9nQFcUQxRB4tk4J&7Fxqy6VWBl%L+vf(PzhHGWN77jPlG zrWjH1!p}Izd-<xZ>uMy{@2h;7PXQYpO+L+{-c|~%4Hlk}AurxNWOnSvD>z@NJ*0Lo zcOx#0>vvl&nChNrP560Ja0tQ5L-;0|Cq9Aau%+P25=Ov4K0mo+H9y_8PoU)}%N4Zc z$y?AKzZy?0575hQ_w~+xve3t&>6x{)38js2X%>%N*?O@|2{R#}H_~QV>BqwOX;}Qv zU2!j`ZEl=7C(nvE$FS*>9$xXmH*AnHk1<t*b$_7M7ugHE<7R$SQ|0lX!ifF~$~u~} z2NNA<%@ZM769cH#F8H&lE@vX69ZzN7*0S^X1QSdvUBZ{&mqimM<j}9c3kl(HVwR?P z<%tlo&fwr^CV1ppvXRnE>4p6YneA{@8{JFeU95PilyaY{Mstf*qQWVz_#NR*HBOIH z!`E_@am#iQ%V{1x+bnr;2=D(s27p(JqNm|!%$cO;nK!(Zd^t>SJQi#(TyP0s2km`! zeLmYOdXS!R+Soj%?nSEcf=%2-vooh8o5aaa6-rrIC39zKS4?;6WKomvVb9y)yu<hy zw%8&w$=ujJT7tG)j)gTXQq>)&D?<(c_1MzRi*cX1UdU4+b|rp;{O1P*gEHnS+aJzM z;7@T1(SK#=a&|Je`tMGOXccMO4Uzw7Op!VYED|Wm$_o_;MvyQl6aZPPy9DQ#hoo^3 zvX;1N=#Zoli2L4VlaxTW#-P;aBQ$-Q@_cCKeK*(gWYaNcu6fcoly(p~`ID<?;%FTK zwWRO;Rp})@t><oA*|ji4KD!ag)-(GVa`sngd%n6+mji~S+JDardS@dc+4O!Ab(hV% z_51VKhTl?~sk*u_D{#J;!cr_#>(6<o3RyV`5FzhbRP;%fMPj3+9(D3wX>-&mf{PC5 zzO@HGsQqbk$@VQ9;ia{-W;2B$=#oyF*^b<<EtV>9K9RF#vPOE8dFcRoh&jK|NGf`N zP47xvfNyZiV>v-NBGkcJkeu}J!In*G#VDY;1~WA}m%ulpt0#k@X1b3p$BmK9Q#R(i zyg6`wDMty6GhM^qsN5w;sl5NURUh<L_5yG*R(#7vonnlCf+azg&QgA2B`Aj&uQT7@ zr2K6s{zl>0ZE1|gAxO+zQ_9%^9`Bs(E$Nfz#!9fQ&AIo?*U+`kOOu!u%Z%xs*^#x6 zfiTQ;0n9HIjy;3~jfT;-BCEOO4?ezex|{BOsaj`n?UFL(KBZMr7jTC%gO(*7i!*)( z2#N9*&N9TFPQ+yLE}ffk)OAJ=rBu#KbL~vp_R=~sC`fgTvL>+32vT*fj$*IR)~B-& z$-#%kb}>QP&uxfpdLEk4inisRgeMu-b#{9A>tyImvLqU!WK!{IOAplxrXCRzhP*l5 zgHr$>1Rwlm%5Ohh9Hjf^rpnq>M@u($@vz><7{MzcHkNn7u4eH0-`c++P|qkW5J`?$ z`tHhUI(rd$VgdQ)MbzGf86^lfXtO;coNGPEzZ@yV=tdxSTOC*hql?8}C>42o&k!NN zm26}2+2%N<>M>>=)<vrh3~9pskVG-&Z3e(s(G?k@tG`TSu{_j)GAAY2hd=cEQ8ihK ztQC^win>tObLh`zdPt!>ybo~$@gqP(W$Nc=);H~m2I{^?-!)fjG#2zI`RsvLBc^DQ zFFKTc#VXbaCdkpOkLPxZors4Yg5$9lj6cMUVQB^}Xm_`Kl<D1|c+1{RKlcvPIH^)t z0Ti+noDxU=X=CCw8+M{o(E@Xvhzp7^e|rWF)?6rvnN_F9w(qVeP-{@~vC5N%EE!Li z)6c&G!#7~ODY)1gjmOj%2BaT;z#-l}X)grQ!z8moZwztWJ^r%3X~YAXXc}LlG6Q>K z{;Eu5n%~AgBc-oAVtvHPfPYd_Td6cP#&pGkiZ+%}rxEtP`ABn}5>II5$#nJF<yf6x z%t@5ZGzUTNj>i-6A9!3hvz#?o*^A0@g=E0d88y45uivuqGpjtwFumRCTX<8})o<U4 zJs-Jxbt?l@35@*;jP_V~%UVe*@NQb~zxA94;~%EiXYHjl9kVVE_6EH6kfF1ioHpmj zRRSaT{LdDs`;4Q@XCP{|!Orw+5jdG=t0u|l!M|MA!kPN%AN=}+gPC~lkD95wj9^c+ zHy<hD6aQD3ztNwrj`*J`#)%CCME+k*vi~nH{NF^iiq)(gu{n^xrfX+82>f8MxVEjf z&|R(Y?88ze3_4)fx=1ey2BUz;9{MG)4I7fbePl1dO#I>|swa$RqJG+YpBH3X{+-&E z7ORvhrsvYHn8+VSi(ZcH<kq!{8S&mU%unWg2<c+SGCIi9u1HK{gk<OPcn=T5^w}uX zz_=7f#1gYs=?a9-BOPxS1z7eqAx`S)d|$n!UTh#`jw#it=c^IJrk0xsPaEZodZs6F z8+#Q3QR_^o9cqwPO`_yyWst?*&BQvb)(V(Ip`FZOS<VHa{BfH9qF||tOfdGEy6>-3 zNF-C>LKzgKO#zoQtXyMC=8gr^6mP7Xa3gDqb=4969o)rnkk&J}QN<Z+ZwNO$)1PMi z{LiUbL-@zmfM(&7(eD2C@A1~a4)Bvg?o!#e&VjB@NA^up&@Wv897v|2JWxeRy+-sY zd)53Jmf}CINn(MN4m~q4QPF}j6E9+wkdah;P-cYJtda3+ahsNTBVonOt~>dhvs?6! z*$IK1-T3`p?d!SGgN*<|;^I9)UsJ0CJ2G%V)7fS8S?PMYbbBrw3grep5f)lZ{H|X_ z$`BEx#d`l60bH*2Oq)TN)|6?u``&%*Bw#AiGvC1~lrDI@s6@>oEtOA_n%TUyUiXO= z4Im>U1L+Gai^WC@jt)Rar0zEZ<s4pNsOh++=a8<enk5&q;Y!W&;J4iG3v1gi0r_<Z zjGN|~Zz(SJXKNgQz3Pw;o9UY}ahrwy!0{JF2L>+(R@KPDEm-Udkh~ybSP~IM*7%+t zb36T^Mr^{Ax{R=dB?*u6nS>Q9oezXJb@~v5%X3Fi$rPp>zYT+S$a(|lyIsODde#Pr zQ3j)0vtTb{TpAq2tNbQIH*>{_A_8&&(mC`&5YSA;kHj=gWc!@h8#-M1`cbCzVn5gP zo=RVR+!ihy{$!Sqo|yQLnBpzdnlRJB>P+7{%q%FFb$Xd?v)p|}e0_Y`Jq3P$SGhkI zqbx?hw>UfhQCZ6Veu-`GPjE7OGdx8o{ZmnKbMLLa^@?=i<Lk!h$ok3h$k{!+4XR@+ z9MWlL1ja|CuL2W))0bivNO3VLK5r!Mlu;9Y&W91uKLi}f#KzR@vqCF!ElcBpj6B1R zo^JG^zkeCJGn2*7K<z)!Iw_$CHQ1JCMAaL@nn5>yD?gmvrW5f&Mz=`bW&8G>-ih0d zFQo%(Gu*>>fl&0g<8mkEcZSjlf@7tCVlz~Jtu}=ckJJ3uuOY4-twc$v-!UG>h&rXX z*gZqz1KDd$0j(r0g&EF;30fRTSB;R%urNfKHOsmWvPB+=kes9^zP77uDJ$G6g6V*Y zSY|><4J|E=gDsAn7Do`+evP1*a)8qtu%eBSa23;THK6^J-8Om0bW<dj7_GEpV=b7I zf}}2ayuO+N#_Krd9jHzsWwAiP#362WGRP$JV>1no31G&-$=o~-n_2EZRltlGYR0o@ z&Nk+PY_6-~bD)nRcPkpg?k(Uc(1BvWN@Q?ljKU_a($%&HCuv<KWW=|Wr?a~sHZR1Y z^c<_Hh=_A)Lq^;?0;Mn5@)kXn=WUKF42}^A?5i9zV|;o~X5Qcgik-8Gu?m0=+)A6W z+mP4uh#nzM{it?9G=NYh268jEKK8QKDP=8ecnR(O<9ccT*-=`+)l&-=2FiP}w8>{R z&)JlN(cl<3!UOMFwf66;ixyMak$m=XH4VD6IvZ>9bvW67Fh*fm{&_#B8WGD69`dne z=%7_@F+gEOG4CXaqg}8ChC+uLEHB_jXybxUV#Dc(T*rykPTdYD85DMWv{kCD0*z2X zncr-hwkC2jFv#FSYqd|1*wB%^`;@*x!mVYmiRrQKMV5$I=a<+(Q3{My=4a%RJUAh$ z-d!gu-1pFX!80Iu`es94*Y_HG18mEHAWLtB#>?ueZvKjExe!Q{00xWK8jced;-+S* ziYnl(Tn1mFb=gR=*QBe;DYW}UVAq+$xREK=`T-?4io4Zr#ud^>e}A>Tc*KE6N=TKp z9{f=t5WOo^@+axaX?14H#;TyNYx_c>+jKYKPinUje%!{c&kiw|%ZYAn=7ix@PBTBq z=*RTtn`HATPEV6;K3oJG)V$CEW-qLD7|w%`*U{oP6{$QTor|u-Hg9lr-a-|TL^ecS z3kj;M0rHF8rmET<i#6X|K4)Lc^jYo)Jfv_0>yQZlnGnn|Bgcpj>*0X$EdiEaFQ%SJ zB~smhrM&fnZY%^odjv-L;}8rFh2W9h78+V?we3tZsTysOg|5-Go-Dr;CK{fW^1xZl zROXp^;UK<6y9(J_FGhk|5XyCN&fvs}_SeymrwOt9&(O2Up_`$qeqCvIx&;D?_1neh zZvyuw?Pj&LZIcUvh~i1pr&$a?thrFs&^8OI`kEsDQToy&(>@;))2)nX(NBJSto7+( z{I_fvs~Ms0`ts8aoShBa2hfdmfon5W0hvFR7B)|@RCAKw-x5^<6pgiA4(JiXH#XoX zxFVq#pLdT}zfxT4Bu5~+GN7_~c$C~77PwGzqT9Yg2od`nb2?pPHAm?C_-)$7Vi8fY z;o$F(fo%j*3F@zmJVW;sa+RfXwO_Y?e{1#gD~nOm&pEa=YOw^Qh8+*juD<weXQ_rI z<`hoMpsfdm=tT1}BYM@AmrPZMgr_=Dwq%bsg1)hZcVkr?w8x}@=7A?BkW_qjv#QZo z7IwB3judv|>GcdaC`WzoynKmVa_)exFW-9b#b2#v28*?xfcDuCfH+c`eUj?W?Bp`b zCeO9R+sH2YmjA-30ZI5s7FXI;b3+wdsxY7-thdXX&SUJ~8_Yd?8cn3W6`@SXI9{Q+ zde;3X$i(PGBLV!n6FI9t9*hvHaLrr(1t!4*!nrC>v?$2^hUW)c-zjXOX@uXul^@~r z(w*nCQs=2VYp;sh)p#}3SKPsnmk5+B%SpVT?!V)hzFxV!n|IIxuFEy`jF5d47^*Lk zO0+Ctw0OKHm6#ed@L5@JSGKlYx5(4oW>eplf3<lV{X)BG$Uatzr;-cuEc!6xY+PCJ z%cA5Oye}fVrN;rdzNJm@838rIzpJKvhjBFT>m-MVV~HWSpLppy)WI#VXv)9RN4UwA zYp^Ud)9Mci!*eZ*|DSI}RP{(rv`tQJMh{WEEYIM9WAql$gv>X|NNUbbgZx>69A(W> zTb3Z4?($=|tr8J5gG>8Zp6X5U%Z!`O)<pJ4aU$Ex9q3Oyt@ob)e7b(zVFxe#G&hC) zkYWDI)79SD*xC5M`PaKr`ziYrf&DpM^X`YnN1ZYhN3tiCoG;5uur`{LOdRfM#pTvB zw3y!Yg+CvM48$Xnj%rKH86JnoG2U-)fi44hU#?fNytBldwQMgd6y|_&yEp@y#)~v^ z{2E{95Wb|H_-IO%yXG9)8g7ptpKbN(1zM?t%6BDHef2E#&mjdf)2wQtQNjBN1IRkg zLWdYqR~$!;{o_=znVJO9!KuL%jPGSkWKan?e?Tw{ib~+jcGQA+dKU5XwI;?OJ%y|Z zgxiC)d$TxE8O3uiq5`W*@DlaF%IbE*%<<%My$n>#DO!qU#@b@}3PH_HG%gE%FYLSk zL}dXlG{}b9Gpd^n|2nBOAC={0ijk3vM^5xfK+V7VR!EoQm(R<8mj*sALSF785XyPK z^X?iGGl->G2`vf|BZ9=x4d8w_Fcw7sP4{+7I=W&LkdpB$4KvatDeOw){Y$>e6x`rV zHp1qhfH^1l##cz6oN3bsD1EOi`2b$p-GhH`9v<~+*;9M>*fI<Z+f(R`6EGf}p=U!B zFU~G^HXvdW@i-p}mNU6x61Bu8It}iL4=H}KExxRP5~ftDq2d%7TBMXEJPYAHzUcO( z;gfm9t|&QZbqN~Uo;D06+-Q-)9bC?$L&^YZdG+I7-#gZ)iAOBaxq=BV4XRx68xvPh zW942`5odCv!s9pcqSJ>MHt?ZgFEUutFj8v)iO5A5>9Goyhnl=)G-Y=2-ShT&Rwutn z$HgF2IP<i>?U?J4iU#e-5*ZHr^%_z0Xeu6uNea7Doi^lX!D2>4S0R81?$#D`actqX znsG!0t<hf@WQ!Rd^brL~DzJ4`rqmsirWGzvbrN2-v9iQKAl$|{9mqUw(iAuqlZ~l9 zhLhLtc{2g$#1yEJoETK#q<*=SVXexoLY&l09+~>^Y;0f^nVEut!qc~xyV%tW)O-c? z4XC^f<qJB_ay3$vTsbvE^Dkb`-MLTf%%;ZHY+U#|`H#U>al7|El;E?Of1*8)CM`s~ zFx~nW|K9o_2_DURS{PG@V%T+8oWr+6Xe{xJa`(iZK2IMhgcqoy3&z!L&g4`GV9oBS zy!jY1+Fr2@Yo~QAvEn0G9w%Y<Czt7}ahW$(s|q1`o@iET0_cBJWk>zMWzWu|sDo~b zs=@no7tu15=2??jp~%^IEUdUmP^h3^DXt3_5!26$p}6#b9OpNXw|j}kOkYC^42+eB z?JsD=&QCwMaZdsI*(3I%Zk=e~BBb3nJN+ERu%0)8D=nbB10ZQ{*|CS+iYbRi3<FKB zIT-7EjB_l4M)cnH1s5h8UqY^xl1gP9Z9A-E)dRMfhboodlw;WyGZOb~9Df8*?|2rk z&g@-8lO4w9;1Ck}78Z;{@JprVw`IN}NRHxhs`Xc43aLcN7Z0sLYIU~eQCnFwsWPLz zN)SdD%;ZRkyN=EdcstJso;_1=o@3cNDKIX?nW-1Q_Qze6%-%ryt&Ww(1y?oD!M0LE zoX|DLNssV_*^%$d*yxsfKl6$`HioVLGVO+&r7HGV3f58BMP>q`9Z{#|0Ym(Tco@>H zfzNZ6X9zg%UH6V<-{fb_k{2`Q((e>OG_O~A`x{(do0tFsCjF4e0qb*$XgaY1`OUuu z3rfV&G!ywFxtH)>2Up9W_@eCGCH>`?GI+cn3Y%|pDfd!$Ppk+XLw01CnoG^!GG<fY zea(u%u{`f2kWZfNB)=AtP5<r6ujU|bFYNGQult|v{6WIcf`5;m*?a<Y?pQx9w(Zz8 zOgWwAS#QC|f`PAb^g)#JrIis_e}8xE5!{62+&x70WOjhdGEjYGr=M7Y*FFRo!0$v1 zobLzkv2QWhd}p*|bG7pbpv@Z3qop4+^es!H91i5F>woqkRAac#$WZ->Kjaa<9KgQ5 zrUGoEq@ID9JPDy=|5LZ0JEF*o8-DYBU^%O5zd0sXVTN5k2~sV#V0=4#e+#YqUxiMl zXN@n$AM=Dh?SGS8{eKP9|INBM!q&FMX}i<z32?@zMH^oqTi_4&r~d1R=1{LSA&!VT z6fm!V>{+ReH`ct5{&|zVH$vYwp`5hu(F^bqBXuxg-^s!{A0nWqrlL>jY+e0RX{`>c z6PN1iC9YyT961%FWbt_qp@r4ymwn;zP5WX}IeIanT03@-691kxp0F8tPheF%+600v zKn~=Y%S??GAB6;Fw}24qoBoG(G~*<yN|K+cgmQ+A3Z_3Y>d#$O0Y^RnnD7oow_czj zN=bM)h^VoY=4IwOYD|=z6au+6QIbi>kGaE{^$kVNjh?2VaHfb7t(?@MzgXYjTfM$3 zppbuYi}$2S?YG0OMP+QgkX+16ics4x9~c2(AB3G{Ufg^MW>{a%;-|#O$br2$xhYG9 zh$&&4-NPZ-#*2-Pp)1K<RYNaI2XZo`%hU*%IHy;sQ~Xu+IWord1yEq+xoEw*QVFAH z!d8%lsBqeqb3QOoYOGX=TjOF7ZH{9;BLbi%VHgKi?YOo5sLphHlgQt*FD#&--fpD1 zB?t%$9V2U30h0uyj~~}2PwbgHeJw3U9?$8WnOj>?P@vL%zb_|r2)}ztMd=z~mH*bU zy29qQ-F0j>qUk=MjVfDFx9b}z^8S|hV>g-^jFJ2gdo)vC!ULYF(phqh+}Q5rCG4jy z!#J9qAE{dX9+;$_AtR8DL_VZ;&E`nwX77^;qgtecXv5=z2DxU+KwE+uqEnO-ujG(R zxsj6JV-P~lLow*oY6iw8qiFG(Y?m+r9E;L1Vd+4159oeQg>{67_*R9nU95ewT>eW& zjndTqjIjHt`d|Gs<bnwZz&uy0a{38kT=6NyYq5bcUmr>=6tMiMZ9sN)#e|4{r<@d! zuT-RhVA127D%GVKun(cei;FWOW)+Zf0vq8)wTX`!{qMOba!`TZ*%c2>>+)vE#C35_ z8Gbp<VP;5y1Qub*;xoAhFCLe3kcnaqIz;U}uH8QGEkz>riY0>-7Kn^ctc<MVkKEz2 zEB}xgqrChQ0}?pyGhD!&+Ehh-<Pe3Rj##lHicwb2c_4};<?gIpy(C5AshIKTsj+_6 zu8>BV#uRE9v-k#6LS@V<J_`-w8vj4qN@)!h3tcKcH%Uh<e~<-%v;t!iLfI<05bl2C zCFr77CS+3PFpH_Xt}QHZXk2%vU7*{Y#c?m;>+$|SCaNlSup&0DwVRC~cYR15AI^(# zjM>eGx20mn(m20s7!0b1l@!D_Y0;F3xN7Mc7<d@fRSbsO4Kf~^rggdyIg1072OTkl z>KXDx!aP)rY+Bafnei?|VaB+g(Gmw{6X_UfcENVEKXhThL3BH4&zejcfEiTWll?{e zlcPFjsMx!(6Dh=`#iGY(j9jFu3i<ZOny@e1$&Q&gg9QB>&Zo$2G0}@^C~0x@m;HK1 z$N_p|gGbN|QoUo2c;&+jPG2-u;dNCDx%3pcWI?=1`=-4zqqrn#83Ln95E=A@cEK<# z6R^J-(i4Wc_F{>5IDc&x*Vvvj@~2(g;OC(Ou|$v#w3bjDN~;UJs>M-c1j0G*CGH^+ z4y2ro7Y>(w@_dE<1HzGr%ifc-nV_DLlUAu$Ys6d~?nYtSAz@J};z*LAoE#YnNRu9j zHNk~xVDye9`2d;;h*$l^z)nttT?EJhn$Iu+0VE2vpArYv=Icez-l>@tl|^Tp*#e)G zDA!bnHD#IM$wrHr=|T1fsU%0M8~3zZ2KIn~4DXJxTZo2^$Y~R<Ri~+HXI8&aUwjQs zhxno~-+`opk&-nBQ%=_TG<#f<wN&y+YQFtC27XTtEf^?yyn!g8mS3djy$W)rl*ta% z^5T|*&khju_3H?^S*0tvgYpQG9*bWEC90o^mkM@maG_XAY}?`hk%g1NQ8~G3qbh(^ zaNuRaT?PTD8g(T(Iauh*Gs3fDKi{qlDpF+s#qemABy6HpWTQ9l3_WHpjDSCY8dU%p za;GAy+5x#ID9!><nXD25g4%)Ig0?l`xG#6r90WYoAW+!8Vew=IQfIIDSLy<l)J^)L zUEhUofKqe+8S7o>Ix-gtH)}E>wG}|0{=+Q*yRNkZH9&y`dKe4Bn<4!6pIMjK>U|QD zxm+Z4p>@%utR-*m{~lcw;6gV`p>qmtse{W|>Q$w;)RVRo!)oYu2{%U#w?y3%>m8O* zrQ4D0_q<e=BYkhfDYQ=Bk!AW90^Xiw`}{B#_|6zA&=TL2jn5T+a~M>b^Gw?erVw7u zpQVS~&tctEU)w$qb$IviY0cTuk*+OEaDs5=wTEh~nU9S9we1L?43s(5j6*)vA(a31 zO3#`T-a0)Si1QvAt;O0}O94ktwTfCdQ{GSdwPZBAyWyp`6(_LwEA+$$vagdv$o8*O zsSISyDbRe8xaN(90(uB+eov5$bVokA-M?=wnOA#FQ>2UEBTR2CNw7LHWpr~5up!Py zf0IAd`kr46PSqYfW>sp>6&nbmNt{e|#1FKd-J5|WyIOGKVlE?A4p!(d(lAFb%XkPZ zA~ktL+xqk;v}QD_o<&O-qepN^c}{{&=De2U2KhujI0bA4Qo6kH*jh?u|EZa-{QGnK z1FH#%Ku963#CzQAxDzYO@~~hcp93C8i;<17tR8+}Qc_X;VE8Az?;s_1mH^St1#fm$ zPR{QhiX7<D2_u?Og2kes_fOu=NP1i)v;>%ESx>{oaI1z0#vBUnheq^qrtf(cbf)IC z<%qA?GUHs)l7ljdjI#o?+n%H}5^)CPJ1-2C*rv`56uw8!nP)A_9(a?(cXRhN2pp4K zg5@nDG?&>95-~cbw~Uef2y&vp>ydh(ce?J?7pN0A@;c=Oveuo)^*-D}!^~q|=g>MY zslk!)+gb>IvndfESVAHd=p*2@K0G9nrv-euo}#;#>z+zmHy6hzEKq5*8g@p4$-<_~ zy#dxd1+`?XcCfg9dS%Kv)EG1R6|w2MxJ_sq4g0u(t@y&KmbNmd^1dVq`KZN*=#@)S zmS^c9$gJl)#U{epV=q9)f7EESB{G(KB`WzgfIc-_vwE>VCK#*w$iOYF=8~aV(SHW$ z#@Vp+)D8CzKXOq&oC(K(c!U`Wp{hv2^q}|2NgaNk9x3AdL*P6+Unt<x7q}9osL`bv z^!doyrS`CXi|SH)48>-S<$KDZ>g=P6tX}<hw@}v0c$+z`T;-C*O5n?xBmP@v8ijca z`%N?!yCy_*Rl=kprvpy(&3?z;4i^UPMH-S#BWlkKbxDXPjRnp-FHAVGxoY%{a=kWP zgsj@`YHn7Xhl7{mqq@XZfPj}5@8#;>*3}NhwQ>2BywF`@#^28~p#qo%A~VUo8|2;f zlCKWHS51&!ZDuBH_Nt#_y^T&8V~J}V0!NbUH9s_g81$!(dDxo}?f~td>yP|YKxZyC zY%~Tt#Hxb%&cZ2M+Mi<}Y5xYA+|Agy3IpS7?3$b|i6f+f&+X3e^~w!j^61T(H<yNu zc+K?@cu`n>q?&TSrfUv#-t!GA4~#R{r*@-eSogJt$GQvO>I;-^;JMNXg|w`Fs1nzr zZ%t^u5MrbK7<+j4ytfg-(*$goH*6Rz()ZOH5ad96O{0g$s$N!hH~brK_t-r!CIA=A z!);aWNh~DRmsw#i2(m4=9eR_@{JEhIiG0)Z8xh*Q?|}u=S%sD-!ofj323gM@V#^Ia zw{?W2Xw($XF^gXGOOA2Yw99R|Gf6W^HyfF=Q)T02OWD#y=uWN3)YhWBN$TlYhBI)h zpiw_khxp6e*Y_5;h+1*3iq-Di_eFr=Ex~_QZdTHQ14@6Cn_#5>KdATrNG<+nBlG_W z)cZ(v%3*^QvHODR?>N5_jbcou6X)4!-nhenIB_|-qHM{+9v>;?&m$%{5MAZXV(Dq5 zGjIU%qv1+=QR%q=@~XDi`xU**<7K6O@uA(=OWo$FW*^tg?&Srun9%skIhqnD*ZX#r zQ_kMBP!i6VN((mqbZ(E|X-Ypag+#AF#EyA^zE(b5xmS|*VG<>Dgu*N(n-4)(j&5jN zeU<<@X^|323JoGIrtBJ?BuxV78a5w_t-3&40hJPWaRaMj!Y=|FQ=lHXa?Fg@+nGLQ z5bc(wt8YJa)`EZlD?b>xWG1n+3ZmKfpcL#_5!Fa0(;y1eWvQKY!z`qO5eQQOS|kRD zfO|3@V`~n=b&%nm>gA)#J-aTQ_d$clU@f=@4H>u{@WeCPtQ;bp%DbEsC91f8hC`$t z?VCVUNHPK`kW3Vod=;Y-3Y!c4z1or`zga-#h!-r0sWimX=M(5?x<-^i;k9jG2sMKQ zU2z2vUG_%oF@r#x>{0amwngojySwpc3iPo1Kd{Lk){GIs9<Y40``n&@w|_NG0LvLc z$L2p5v%DaYGSkj4%AvQXu>nc7PPI^*_cNvB>3S*0qXj3o7A6#O$tkEa8XJ@&>O`nG zs4z-5X>&M?-#B$_g;7^qOqhYZQWuNC;y$yuQ0e}b@lLvSLM({G4zk#$o^BE$$(AAi z!C<(gQvETNw0ozT!i-qVQ|cmULEo@wVG$)zDzr$TRlzAXN1~A+H3_haO#P);9ZwdH z?qC?hI8nbx<q~O)SEO+$rdF5=S##zc*v*oBy7I+}3}C1~IMC-16o`sxA=BWNqpGLm z%9aV>f)=oJmdE*QD?kIp-(s_RKLGmnZ+gywxY0{vMOtFu_;7_5T#2H9et(!7RCVck z!oGTL<M?^Endx*}>*)ORxxmqm3V+m>FW(5J1l_zxonnsM1qzJ+4MINE3ni#>HyE;5 z@S8IQ83vK)>fw|CWmg?jPjL%~IoSf}rCAja^;f>fe-F#P)EW?{PyP6hL??87g6i2t z;uAP6IAyPU!3qn0Eh5ote8C<4wqYL9T9<gZQ(oEMon;+-dw9}=$v*SSkbr5I>!<n5 zg0hzjSQA*<PzOzt+Aw}-SU9*8FTl6gTy*)21fdEZpWBw<ca`Nr!D1%FLqSY4DeR4T zN8N(`7RbW^^TGgYTFSu66H9FkgRiUJw^Dy=C-2im#rT7)(?pq9x9SET+F=fw@*K23 zAYnNkv+IL+>F(EklqN-+<>Z>?f$@!`=0}aIQs{Rgb)fC8`?r&6+B*6}(()`UWDQk# zF&24?H&I52ycb8z@SOo}mk5OL7VqZ{*a5yzRhi6kd1#B_QsZ=yP~x^u_M(~$s`&e6 z2~=Fx+0<Y1j2fH{mq?(^OsFXLzqV?o=XdA!W;q(X7z{HrGqIY3QY`sa+1!5VyG=;w z$x?`c=>T}MN))49je(sVUo@PYH}mz%Y-H-~X;QNTGv2$}O-GRxm`zxF+mqF~!9!63 z^aV`OQ9Y)Z<CQz^!4z_2uH$G8yZ6wSZyF%^my+PTb2!hyYKQOzz)B%2*cFp&;Sa*v z%-$o^4nnkznNy2B<{S+?$-@&ut<k@I&=+!9xz<-{0l`vxkI+MaC}GL`6;ZZ#?gLL6 z3kurn;mIV*B*oeWQ2MDT7{Ec_WN3Rz>e}Z9f)iWn=IEAq2r=&u-8z4qyV-a2cYzC# z@;;I#8{|$@UTKVRwGH=Wi)PvE`ba@MV;u+ok}f8}vi)Y`8ae}R1LP_(1kF~4jkQ86 z7d@!AUBa5W`IdU%5cyAW%!-QFWyIQh>)>8T(%@#*jqZ>@)LdzSWlLg9jp*iMCyS%u zy<7$lXZ8AzNBqtnm_6i5lVA!zuA{;%d&nH5hrJf!@Ik9-#>`)w41g8>_y0l_n`XVV z9{Hh)>2U%9k^Ps}jsKZq|8L}(D;&!oBien3&rpuHUs0tL1rM&B+#W&%L3G^Wy}CpI zq3VwljqFZ<PMzLdo$h|e)<jpUn{>^5b|C-Dz>$Et@gZmJ@l1ExF+PVuz$fRj!FiIa z^zhQwbTBqBt~P{pCdKr7Fn*e9QfqjO9!~Wl%IkaoXlKl7YE)ZDSn7KI<<I2IC?y1D z`K{iS_F$Qw5Tgi_8k+~3Cr?{$f-&arhA2hkx~PzN!rv4`2>qdqqvk?oU_gZ1A-o3x zV5BF&l4xl`Z?}>&u%n(S5r}+VH(i80T6P7cGH-u?g0!;|;kJ<QO-V7t#3@msF6T_e z!OQl_GD)GSp0U<K)-(#{_-=6t0AX&x{~QGLy+L#(UGehMNiltv)l*<+W?D9?Maiml zvTCMDnq{(j@N{){WNK&pblMK^V$%>wje8FDp((U4A*KCrbLss?!)&1HB5D-C9o3@_ zZ59|o=U~IaQrA*SVDo<EkIvi0o092Z?eC6WgY2Ro9U8Ha9h^bA12SStLH+LZVL?3D zNC-rpp9?qEUOXhopAQ`zJOMe;6fL*Tf+8&hW0`kXOMZ;IsCoLa;t@ZY79xjuu+fI{ zlr|!uq39iAeV<lXZ*~mbBd4KJY9BqS(1W<3;!L?bTCg|_pd`x8AgrlFu`4;9lb=+J z=&(BU?gx7W-#aOK22wnQc$P^3tpQ?b(%;qaC~HK<+3+xXB{!hL1>k;tfVyIROn>le z)cz9mDN}C!sZ)x1CsE(MK2;|m(ogS4oDj&Q!FB92gEo-eIs#gug<EAh%;!Y=alBl^ z{<ShRhc=PFl-s%959-&4<3Vk!A2?#?79E~>%<F(K!Vo0{Zk+K+WzCSE^8ZD*ZHT)% zFgdL2kizWRF5*$TaHlWghcMTFJ|?}5nfAo+0pleW_fC6-z!l_YM1SN!CBI1bS0C3% z0?&K|#)(rR;unYHvE$)^*Y*YCD~VbYZX9W}_v{bhFf1golsEGLSd!qr3AAeg*W-GG z+c0)*$a*rJR2rF0#D0$QBN?hZ1yI0=9pjWRM2lGv%ZUIHPTZzwQNT&QqSCDwVswTC zORCisy)aX^L4xv6Rmr@C$qovGH3$oSmQb$OE;XcNsHjigsALh_Fm--ajNy<t@ftBJ zYd(=n4G=8s?Oi3`jw}dQI=Gq7Z3Cr^m6PPO6KI^DE(#uKiWX9HAFGc9d{(;0E8{M5 z94r+2i;8VpLz>b|0ku47tVX|33CtO@>jJ1SBRn)08nhU!5jGo7^?Dm$?lo40ufG&` zGn*nDM7z|T6fD}~teNsEP70Y64Wtra&9g*$ED2b8)(AHnie{xOpX_6Js)|M^0(;`I zOeM2EtE8*z&jf|>qq&WQNO%p<61%-d{>=b;7O)AlP?BI8ENKp=djxw~3qQ70YQ)-> z0juu-dQ2K@=tRfwdUzTJx`hSSTkkN8uMPRJ<!JIgm3bJhZ|Xd%{s~lyY$s%bWvGp> z2A*ZGomll3{~TL%ZqQf(v|zdj;xVd7A?1iFbrw%9RzYIi`WRQCL@(zG+2jwtSrUe> zx`g-Nyf0G-IzINWCE`xSdu&6gWi^6*%AHpDbW~1x`+g5LK`)C;A(}>(Lp1O=I2eJ7 z0H45z3gU&YN}G%IU^CivV||}l6DqVuR&wR6q53Jv2IfJ(MB~l^M#Z^H<D{eQUYVmj zgiNzmr}9}s=k^0ItN2<EchyW(6BDKi4E2S$k0%4lZc1R9$}5k!J!`XN$J5F)H{CPd z$X}kU^M*)c<|}m;>T#}xzB>)o_S0aC8Y;(AyxZC{L=SknF(!%cuU2m+K!-F$+J{>Y z*S$6}gcK)h!phAo%IfVn8rpU0hT%d7X`P-ABc%GBcCOcFc+BO-v{N~<k^ISj4#of` zrkKf#khrDPc<DWD><`ym^b;s{5x?bBB<hPbM(LE()qiQAXqn3GOKxp=YA|b2<6gsw zHMUTG(c-nAl+13AiV_g7F;bGPY}43Tt#HRFes<9YFu`U6m)X6?Kc<LWSh9iH@x)NE zvx?e?9JH*c>Z~v>0ICf4l=WLdfCZ8XPeB@9_PWu8t*9gA-cOzv%x!B`U61}bVJuA- zOpM98+SZy5LB-nh8rzW}CYJ@Dyj7%u0ewYjijL5c)WtCv#-*B&g2U_<q9fRWJKbq| zd_6C$6#j(kO+9>zWH{-oOk6}gJ}tF0<J36!P&T`N^*&62fFF=y)9o^#9HK1{>!PB_ zxjH*}a35FnQ!oHCv-%n-*EErXImh_`ad})Kbtos~Hrym!;tytk-=%cU=D}a5s_|j? zp--|fD;}u(GRsmyXtojNE>g&jVOjP0B;rB3Pf31O!7I`>9;y%`z?5$7!c1)6=2@nC zY|x|&@ElQT1;FO^VIZHF8Qwl3_p~Q}+MqA9>5uR%x*i_o(LR2Ch2>9Oga-kPO@eSq z2U{C!nb%9xss-nRAD(-Gmo55D-ZYWh85(KD9AAywUPO<6y2=y#@G=K@Xd>Ui-8UPV z+EfB-kI|D)Rb>IQe~aj`7c9mfu;HraCae5ar9bTE6$=l6o?Iqn9irj9Jcv`XxT}-p ztp|DO9Bbn3A)WE6r%9sulipc9lun212GyMytVx71I(0LpSXv5lEItTzI`u2(7Ii4L zhD+}7GOyj_L&UTe?7%q4SFemXj>U5xH6oI9NK9;LYovTzR#Mp{jz_I8;<-fWy9^NW z5hCCJ(iY_Fy_F0!UX5OO9C6dNkCns+h*47h3czJ?D%G^Et-kYz<62~!95OC#QXkcE z&)ze2ejeIO1EjZyE6`Zd%<=C$?|#bGrVCtHOrXDc4z}$`K9Kcn^}2eUR{rWi_ps2k z(y}_s<eL7LORI7pr3jre{UxKCQq1aa0djM3N!6^(F}q8t;zXMT?pm;Qn`ErPyKML| z$b>x`b$+h{<6vX$&^-qO-2x{k!R8+fjMA7!OVVyZdA!kwFjmPPgBjfa>;3v1%&$r6 zn0<O#V;jjTI&HjJTHExw-WWj;w`N<+-3)&@cww9q%R*Vr_LP=|u@2l*Y<qoK2q!BL z4|;_>OGFrD2m89l!VunYJFvmgn1C<*jA-q?$)EW~`P-xd`jo0PTy2DLCD5u!wjU(T z^Cn7G+o9&A4X!YZap~_6Y>A$;qHFo-$=KhhiU$8o)@LLMv`<QeI<;D{+G{n)wHmC& z$M+0y=`6Y}4>W?5)cG<|wItOBN&v~^Rg0IJAIk*DHkX~|S-@4xx~O@)`_`-z)$61y zyp8H-85;szEZ&z_-R+-mz5J?3xHrcSJdX}d@Qd`~HqUhY!~10Q5@vw!)W?ilIlRaL zy<%;_ukF<Dry;0PVUiJR<o*hbZ;a>pRF?Y(Nc62~o03Mm0Uc0+2(c384Uo4k;*4=s zBTlt<aFM(Sb_v9^&22OpTpMW95}$;1^Ja5Zm8rFV(bcC>1vCP*W$}`72s=Rc%DU!7 z2<l*FB=<0A3%nA2xH;I=%<P<mo1_&t)O|og`?bGGl>nHnoT}SGV0?-|AEOOaljge6 zRVfx=mj5!jY*9W6OA`Ui7e`Qap^xpT=9uFO8<ysP8oyL^@N^OWUca*FEnxRK0^CN2 zX{y+xdmkJspEoUy#p%K@dCAy3lLsvWR?&q9j+Eas#}ju}=44=SGYU;6vHYzd*;h+7 z7ZE;u9e5orj>6MXD;RS3duYG-84S9ppLj5B^74*`VP(duU7mmsag5@bu#GO5v(H9X z@=1qR8Z|Gmtkf~~&#?^%!DF}{!8QHYW=HH2ZFlW_YIHat<Rq$j@aWN(XJ}^L$GVp7 zY;9fA@;Q#xy4Ky-=|s!d0nxcX*`OEEcq{$JycG(7gWi|*=${TaVB$cFQR}*uW=l(c z4<nqGc?H^Sy0kD|&pCq}`c5l%n*twd2<iw$FDV(t!F~P`9DYm-i&bK}@8i5t_vcU> zU%qo$65No}ctr=$5%65!zj>#Pk{lwFg<F1nA`z*k_x)^A%wDp-Un}AnevQL?M6^Zg z3!<pruVOQ1pupB-Q<UN#yfMs6+LZ?TsMI)qxvtt89ns@+5;o;qs#ZM8?wR8@y9nw* zdS?y5U~YOZ=UNe%B<BL3nSK@Mn>6<eAw`O%&is=dSRPex%t|9UPpB_TE~4dB%LPWC z=OrmdmmM(o4W|mqxFMp6L3eopgJ-RPyVms)F0unMH%0-)&W_OYfZ8Oq@)kJ8#$xWo zYUo7RGu$!Y{08Xa#g`r^1eU0z^o&D6Z9<v_gNVENpfy3v6XJD_-mId^QLjNh5I&N6 z;Y#TR!5T~W<jqh?2yA#sMt;~2p(`NU-C7)>%_NzQeql;#@#fFf6x>F2d}ORx&`ldq zqevgVrX)$@Jf~*{0Hi0GO~bqXm}nno=;r#qEb{2gK!k4PdfAdp#~cdn0t2@P@-UEr z$_}N0|2)S(W~+VnNHh2ZG@RFnUbS;8&U8`X_Vv}T+xN+H=f&N$$hG@{lE0pn^M(b- z8lqF2LowGLET~Vy-d4Kh1U|o-d-(MrUk3=-0Xhj8z7$Mo%11p>xFJ%Ha<%GCL(IT( zpZjouEVeK(mvyhl-fFJyXoh7Of`NCu8yZ_&f%DO&j!IQb)*K%erUbf+ShEJ1XtnPr zOMV++TN@e{xtAXp|5-KM#7}30{)s9vLjnPj{#RGx|6v_=u<{S(Oyoc6u%(V5B>km| z`3XSKfXByWNLb?V#BC5!!Rm=a7fTN58!;x%S6ofp;}22GbVwacXR<gPO`IMFCN-Hi z!~vEuHnqx5U~IKEgbQs-N4T9dHrw}^rx7yf?Ht(Z*La=op9ivaTO`~{{T6PFZ!{+G z$g-Hj!>I3a`E`)!NVr2}u^R}_7jUQz0+dbuxY=>ZVAXsTTD)VMk<f!ce24_bA>#({ zI!9(2X(C4dma?Vx5Li@@2Qyy)Q5?JNT<tj@QjnrQ;aE|*b^n%0q4g9HUei@}3F+cx zX|mvPMELnD!3_AIi<f!+Q@lH6Ldx#^(tU3DAef-DM%SA38I#ZfB6NStDrt_&b#|`z zR;|ryR#&z`woNxBGz}Q=9(VJy`7I<NK_{kql7wV-AaOE@K&NAt1s=?^GJs-TyB&mn zoScJsc0AV&ebf1v{MZR_QHmU+1Z&-9)b5H=<E9kU!9xP}zt-THF^}x3zY`sBo3;A( zJ?+{ysn0YX9tdOSFdyAoTQb1nR1$$R(fVIP3JnoyjYgR@Cp^G5Y=1Vunbk+5&J~I= z$OOuBXDO*6uvwa$HYFv0j@xo0x@^0>DOcg4IWv}a#bPC~edPccn$B@81RyM|L{!_v zLgOieb`)VDB^-$Lr=HG)7?OfA#`cg9JUqk5tzfT*)87#VK+u}dUxT*jNs}vP!+YF1 zoK+0lvd2h3WZFTNHVj&EP?k#5Ar9=ekuu#D3`6pRBG3!Y?jXxIRwCX-4WqE#W98S3 z{57%B8$qz~hQQ|3<*h7unpF|zqbVV+y|h8}RD2N<Ux81*9ulMsBsRAFnB!h2kcSo2 zG04M@Nhp7ZzjN<xc$`1HaD9$W4%#9kIL(CpeLiJRA*~H|y*<$tbjJgN*-0MdFWql< zbP;0rmr`1iG4h4b%-Z^U$7e4l47!;&J_4Me_%)9y{jpUqS?kp$n0QVMcjw<n#r9kq ztp_+1Mh=|D0Yj^}quMcIRI{{7G3V!U+QebG-uc(PkLtDhM2NCg1w}|g<0^P~Fb#Y5 zWYZ{Zn`oWDOS*wjRPfiHWRv%+q8NKYFr1$2@L&wJCh+Z$&K_^P#DDt&G+I@8j+hGC zz}EZd5*52IP>F9Q(gpc2<M7y9^R%MCavevmpyUol&ZvsQ;ZH1;W+D1a3+yYripJjT z%FgWXW6$+P1fhltx(8{x6n!rlf!cT%9A4+IBf?`+4f*~C1RMcgBH1ZD^&c+W?|(hT zQ+I!&eBy0<KyGyBSpPDOeDiCCsvzWz@!7QwqcV&fMl9N`(no=|m2G@UcL?8pAUq@> zfn`58gC01K;vqFDjtQT@ciQpS?ZcEx?JX5w&iyaGQcIG<+}58e+V-C++W&IS{9n}2 zX7V8p-F%@czR#<$7Re(hdlbY}7Kp4ewaUl9z%rt3Z(Zjic6*!rE=LZYuyDn<^|3X1 zBzjsNBpXwUtx<HS512ZEQ>q|e+xByvI@WYkuWD^j_F!3bfv>6U_6jZ#WdGMs&5rqq zK?4zPieN~xjPHSp-bWl@sO>=Qa}~?MviQ?s$QLamXUi{6sJ=k5r1OvP3#V=fPnnW# zdDl4l*q~5&XhchWfPf`sb)U06AeO%_OL9VXXH8!Tf7;+zIj$Ry*r*QTZMnczD?pUz zA}6Q{5!_*G(S#L>1K$KjnW^?ypT|_oaWi~Srr+yD)&iS7H>@Bj`Lb9#>oo%>&Q9@$ zcFGOwB-f{X0?*G_`F3Z@YQp6=8!i_bi|fMl6D89K)N-WWg%824HGxwB*dT(h6_a1U zCU1A8FTvo7`q1r~LqScDhO+4P$CAoE06aHKJM$EQpU3XfjD*`K^nHV_csrycopOKT za-?gZA2N*_07BV_jon06|6C#}DWnU=2CwZ`7(O-ER?TQMfRa7^n#UY*(>YS~A$#6j z)>w$P#&G3YaVay$UP-EJe!&p)e{7%seVGEu|91OWJ6k!K|4-=74}s_ZqE?|uIGjiI zC%XoZ4+O;YfA;bJ0r$~0w=uE(@6=HBEyweAB;S+TRdNKrQ1-KQUMDkV#f-yErY>M` zv70OkkyQ|<SnIZgNl~uPoer9@VerJD1zuLiW75AW@2?w67T)if?^>1b#bP&&for2p z&M>Y$U4e2U#M;k9Y?U2<W;2)RYPiQP^9DT(cP9pJd=MVsLd7SWg2EGe?q_=mMe!vd z#JIva7I0Zk^x|x8S-&sbI0KWzII4;Q!f?n9;F!STMGR;PvW9vkz(@B@k?xfqAn#fB z$Lq(`!g@X=5Wc39(MR_sZ-MZLTkj;<w*C-@22K*`Lx>goYbBBkaG|~M9;WlpnOP(Z z1r!s7W;5c@3^NmPb-SZGguj*a#_<l6DksCI)k7c545wB3`u1#GftWbM6>02@#K`P? zb9;GUP3c_A>2@5@MYk>Iz5H>=pPfGfRtq@~vI>_Nwt_gN{;Pt!5=H9lDuD?rjnJf( zj;Dy*CM1AEmeN-KS|JB7U4xw0q6W<thy`oPcAMH~#eP8)s~duu*TV6^lqLgF_<Cy$ zen7<?EjlysYRBDQTFZ#(9oPdWI>WPjN`*S`oKE~HFV3%Xh}33vY^X!Yw^w4CeJ58X z6y{QVkyXu6b(B(xI^`c#DP$H=Eb>Tvh#V<|Y4X$gRv556H8s^4WkRVK5mRzYNC<ef zn1VBs2r0@HlMfwag0VBK3|^Now$sHWu@M)!lSuKMGJ46*#8NFYY$I7{L$a@IR$%gv z>LnO7fbv-i3py}#28<b!pz9ztEGAaa7PY(!RxUYk%HmETI$w>F9Vxc#fbe<eW=Z3i z(Df_r0TTV_jBRFJl1r!4V-lIs^zPp7Idgn8VMTRE7FK|RQSdAOLQqM%8Qs$!XQQv? zy7Z%z!$(G>xge}VKdzEv{|iCVg4@9%d?zu%ERaeE6-E{`TGvL#yF;Um3ulZJ7JYDA z3X~FxB}sR%%mUbkO#}?>=fbnX@q?bk<{7uZE^0h?<&0MAy1|Fc%%;QUcGMZ$Pe+o{ zy%3QjhZht<Tb&7PVS<k!dAdA7$O}WSTWy5dff6mq0E~3(;M8#oYb)QO`MHor>JF9c ziEHnZprpP9&h}n}smk9N|6xZ!5{L5_a}GVHRq^lPylYm`hOboa3S7gYH{FJn!6Dwf zxRzR~x>w<x-F~a|B?NJ>L%ewCADzpHZ_z!`%y-SUCARD+XlBi3M>LKGZv*ET+|`Eq z{JU}46;Rz<c6Qi;scM5Vc?IDam9KQ^Xh<PyPzbagTR!)^*%}crCwPpA>SuBSV-|77 zCF*y%h=GbU39>)Qhxk{!5&Vb$B+{4tE_EwX;nm<BEO<H@LOjq?R;-7K)c$z8-|K_> zdVnzIXnGI4(#Yb$&G)vBTy@`G42^N9sGoL3=x=k71kupggTS9i9tCzcW;nG#cey(x zG&F|YJ2m})bbekb;RV{P*+>dWhx1lN^gi|j^O51JnNf&`bzCnAl-aP4v;jpMhven^ z>e1mmzj_Cj;#Kl#rEvPjlBL8Zcxv%^#2Dm3z?u(Dy(OSbMcw9aPsGl|dW}OBXbvjv zVmYx~Q?tI4MyIK;vQQzCGKguAd}zSEw<+KM!`?e}X%<G=nqSzq9g$(%wr$&XWZ1Sd zY}>YNn;EuFR`*TU8C|2Q`?5#>fW5EwyT^Lhob#E}0IBkheM4l;n%EZTSNXqJuQt8$ zH=Z~f{k2L(IoRy_9XjSGNj{|Jo^5yKdN0DlM{>SSq1xKonIt0>5Dp0$qL&9z3#L=; zuKQ9R%~GogrsFlSshcO#9^3nN5zrpSO%p~NO_htys$|@GoicwiMX0XaBlP$S(A-@H z=A+D5sRA#D>*6(R&CjOz|NiYc%&bImz&7%cz*s`PNMiVn!ZUIZ8Bqqej7Z$HU09=9 z4aXhYicAbFG@i*7)FFH~q^Uyc9*4xCQGblxv13!A_AX3?89a2BQMxAbf^B=_XXDd` z<QVK{!<JKlgoN5sa4f@q@SO;;^(=NNJ#XIf2e@AeoD7~1$+jzJ&$0COcIaOzH@{2J zzGUe3NoJ`I<JI|=CghBY&*L6d7UY3bm&FK#P1}Oox_sw}*EqWkb<u_31wBU#bs_lK z+Qs&Oe8^N<Hv_4)nF$-qV=Nse*|ww+?+IlEfgKg+F^T4#+F?dIg>((NQ!@jORl8im zHOyLK$nG|!<@vwP&-3l>7m#3f`~Xx<6LsjG29ZJGB;IeUgt{uEQaddA?F{k4Q23qe z=<b3-MU8{TzhuGqp6r8<Z9DPBqr*QW56WXhdFh|PxRs*I^Q^;Hg@v_9DY3eT0Wa`h z%X`zC9u!PGR{9LO8DOmE1SW+yPeY!k7qy|OJ;}Uzp6UD6asv71(7BPpm0m^^#41V; z3MJ{=kU&$>ttwahWJw<SWQHNXl|8cfb95<o@7hI~T2pgc-A9`nJLf1~-hO#td;~}` zjP9P5k;3#G$T|@>uPstfc`ugR{AF`H6f!zYD_mUiPI2T2rI2^Exs-cvpG_XDXd*b% zn2o)nX)Wn4eP2A9_PXd*{`__RyQt;QqmTb@dC@X5ZmOiZjq+fH6;3MmX2}qAegjmj zdldHCmMes~!xDFz`!HqOcQM|jx3((2My~PuRcE&E5k>xsnKn1Y)SMMhF*l3TnQ$ja ztDD?Ja`<NfCcnqZkG!V8^Pe@R5FWhT{g<A4>EXQGGw1D!dbQ!Y-99S7L0RomnmUai zEN_$w5^ZIe4<!A8$da<aJP#z8SFzgZ8asBRr-rBz%m2EprGyzsVsp5BKk_)=Vz;Q8 z%!gZK)?kp(RN13rGV-efN*x+=6N(-(4FrW>=Gr)k6AN=Us0C|AQMP|4+~$j$ww*Rb z-klLCYH=x|E`qzl*u9QYwNxV`rDl5BE#nRRnPOeGhH~Shr3)QUkXy+Fz8M*`#V&s0 z>jbLz*ai>VynIF+sof07aM%W)$fR~x2yLeYtLse3JX$nZSu%i&&PPuImrQ>s|3io# z;zErnLm~f+0@+wyVEucBFA%Y{0j*31UVdaMMXoOKE%)?r{TYz*Tg={NBeqC)>sb+% zwX&wob&D7c`zNDN?>r7Jw-#wRjxnGG0jO2@aioH8DM-VHzR3(WRcQp5+m&6S_=MYK zyR+;!lYlzIfpE18)pUqt3B9lo{U=1a9M*U{-i6VJ5}q0X8dVw&!c#$BSHq14q|>lG z1R9HfDa>G^^VCJ4I_i>;cH4nV&vi)3ro#FEu3`I{q0kEbE6p<a*HzB(KO5ox)4=#Y zF*muY{0phE{Rdyg<{w`rT7=9F9?RWF0o8TFlBT?5i!^E6fzU}Zp9~0_Ky68XS2x_{ zXlPz^=-_&@6#+))lj&h)%1vo{qj1w2O}FoH7}ZHvLRB~5pZEbMUI4S9dG>5!((LZ= zcE4=B_7{n*xel96>?f!_sGS1d*m%Is>i@V>kZgV0eT=@dg($5+7PBejp)|DQp-HHY zL1=m?26>PYM!qnKo7Mwrp;;(0)s7xluVwb^|LrS=O+S&vU@={^gYN8y9G3909YKrn zJSL@zcqRL4Ia^|-lq+SmrSqMFgIqooN=Wwk_*_xF#J$iU2U?q`W*rH25M_G1ov)W| z+&FNJkeWs|R2tud8yjE7joPKsloxQA{;I^R1onH=!?|{F2U<wV(YcW}phFN=ikQYf zXPdP{kl^7>gI3XP3u2O{3Tc@i&2&=eJsp%CI|JIuP+?CfNzVa0GmE4-h@)Y_@Mm@~ z1ShSn56f(tC3QU^!(LBx>*SYc)qY`b7bu?@RXehPaZ0KneeJ<_$s!YsdV|S@zf;~o zK3iQav?mS8oKMA6^i;u8{CSE?@J$xjmTxq)<J95BsWC1CA!7)#!}A6VYXz=sA~^co zoVGDDnzApmO=VQ&4CD@|2|~6O@X0Q*Ooe4E9RaBj9DMtELAP#dliQrc$zwBR+*e5+ zsG^+tv|A1?50KxJh2p8)7nP%3T61t%`l0}S`%vm_=jU<=f0}hVd!;t;g=YfoeX&1b z3x*F?909kE>%TuYes|&fVL96cn4Z}lUi%ep2DgZzUou&|%7GbU_LTzRVM^8BI}i)P zoq|>d3M4)bMZIz&hzUXjCm5LW%vTZeHM=f2&S1ZAiNW49<ZwQZ#mmGNI!=0f30{?n zvA(DB=K>&RtfEJWn!N#P8)Pi)Yk38e^(<mpx$nR$&~_s5m#BOL@Xr16klUX*^FPFw zb`8+mL1n`FrKJD16llJuve^?-5nknyt{G)3o$x<!3CBkj_7)Sh1f%p*5_Puowz=%j z%z<qnjg~DhAL3cLlPW#y`Qt7b_3O`^muJ{1@upHN>81|8aDz0~XNFS^L?6!_JGg}h zKOp~kqX-LKNd%1v03@3J+baIQIJ5uVy^Gh{`H(Gf_lp|wBd91l^f05ly5qjOb>hyW zp#qD4=H8?sMY{YKQX-Zzsc`&?h4RC0ZRZ^!fRB_S_o#;MJ6;n>pm6@o9b~PrR!^<K zgEMY1?X9J2b|Hx5`QQ@6CPCD(gUe90-RF+r^>`0bYo^ghd#Lgwv768D>E`NhZetcH zDpn_D!=Kjz6LYk_Um^fsdt`j*8PL<JU_Wz$zu%6&+@_Np`!*S9GzX@b6oICcQH#I) zq6?{HG%Wt*YG3XGO9RFKF;?{?7(Cnjf2&NU`;NjOfrPL2GvXM92`{^FyvzI7O;feN zx6({YsX+~qq9cQSTlPO7mxLDAJdnaKf|+Nvb(LD8BI=CtqlMeRzn0Jhe{+6nZG35H zAP?NS>!-erPo`x}lS4wS5>l(tqpZt!`S5f0wq@#d0emcn!dUi&>H>dshQX>_dQ1)B z&u9iX2{VFmz>A>O!NOXYgk9K0bp}?VGsaSn$!RzQB8(K#@4Be)v@DMd#@!(D)Y2OU z8!5A75Z2kv7sp*-8TjN3g7~)T<IG!I&DGtLq9{9iOix$U?aAGfE~#IiQgd{_lDe~W zy0MtylLIYZOS=R<fyOb*$4E%mRj%8`0IfK`qq6A_OuPJDiz}&g99t&vJrf3;G-je) zX&mUm@-TpsN%cleH5SrF3Ic-$2ZfJ%T~m_pqPoV=!^7S&fbLz;N_qV4ARzp;)sk%# z|J8**){<%q#-)5q`d{3=JDa^nd8C9!yfw4J9sBkRqoxG~nI=?g#u6iqy=CNbNh2f1 zSDpPO{_WcOqjOhL>Y0Rwum$vI$Qh*bmq5jpnAYE^g0u@L@8MO$4*6AL{k>Ir?M`QW zbYPPPJ$J2pc`_J7dy`2SUoH-g@Yh7y!{ts6$xu(+7&&;%pf9d}M9$n1TrwxkVi*%3 zBR?W4L{_#OpAWnHZ(b(6$Z6yB+a_3egImpdWXNDffV<GUq_Y^VAcX;+9;g%gt9{py zEiB*;Ikg*J1Pi@bfQ$jx=D>wPsg5*UGlC{Fl+3*J3axnyq?G(s*+~r$R6q)4K%kWS zdF<+t^YADZi@)Hn=3xBHCZmu+1XrdF(Z<ayG?@Glw;rY5eK?aObNgFCpVcII>42eJ z$57Ko9@Q@c5`W1r8f_9Ha?c6An4`87V@$cN{vO6AF*Ki)ny{u4V@8AZH9pZOXnRO= z>hGtM53+CSMH+ZcoxWa9_-qj1g^b1))5?AHsfk2mx*xbrzE0XiRWt_%IWB(zWY?cD zQ6DyIKK!1KrXQU)Uobw<5iPP>6f?`WtH;;fk-M|Ko3A%rj=rREa(wydevquL@ACP% z@kaHdvX>at<ZYcs?$C)12RfH#)If)C_|l43y7DO-qtS+wL^96IaeVdQQNxiak0HR& zF@5OgASXRALr7R=!C(?(lQ9MreK~BL&*I30K+G4|9;4$M5xI&P^l|PPH*WoePj4$I zLD(3`=+`r7ve^P8XeJ<WyqGE^CXE`fv9Sps{i6Tcev*L)`qw^HhF-uqsQdkkoYa() zMDx{`JY}+~4;CRy=-)#cuBezZtc~wkjBb*wc61{0V1R$-N1cZ|%Fc;C*cIPo(M-u1 zyOv8RUck~}__l6uJD;JcYF-wU6$J|y#cTs<-JS$UMZW{+<D@y<NtMcX5~2Oc^GSBX zc~Jc@Njv!&$l6|+CwVV>0$Yo7383i(fKuTG%Y};r#w02X#<4{hObOIh7?CXHD5*w` zh)uDqa|(?eISA$kjH;raTw^%M0X7-2f{i+&z0T%jicV|$(Gid?>vdiZ6N}?MC(r&s z>3>|Ts3Fq&Hc&8yD6{de7oItkWY{OQ+ZsYT)tlqu#(7eGPwnBL`d-n7nl-+}d1MjN zwEE;BpqINp*-Niz{;2`oF2n+Y87q!iVbg9pJJi~bi(zcQr|!J&doflOQ68~fEJs;t z*5Zt4`DiYd5}6dm>uu?)&yA$W7<=y1o(TVFURMnBdSAoE6CJpX9D+?0U0tVu1kse# zT0962mvAr4f0c2;F6__<bA9q&ZoW~JcJhO4IuQ<5zVo=yZkvn*F~6`<d{If?fDIB4 zD;|W#UJb2J7m>XI&KxFQ-%&r{CdpO_drrkLCWiDZ6E0eYI~zHwk<>3(j$(W3Q5n5d zcog18(9{#2LE5k8ur@zr1yg2#xSW_Titu$rEgeW`hS3}J2lpBpqm*M2(;1xboY?`T z5G%C)qH`T2fy_TW*0!wv1S}#jZ!17(@M$sKCG$cpd=&!|dl2(B(b1j3GQVqCg$(3` zyd!@oAT+qUm=*=7Q>+<xA~|dd1X0K7{8kwAUmL6CuoTmR%AU{=H-eyn%c~q?d*xH% z@gZ^%rk8B~V%czHlk$78FNj(?2)z)hx?{#cxC6AtAa5yd5LW%5wTK6URwSL#ufL5{ z5%DsSbvz4nHr>4xSDB<dGME*VI#ce8lu0qj1VXsV^rqe73`Sdl2zxpX&l66r%O_}n zOapM63F+V{8e-F4u-FixZs^AKfEyzcn(ij>(v~WPXT!D8ZB<2k>$oI4woh6>JkCy7 z0zDqOP{;TYlvdk?4qHseU|dW4elJ<t{eWR$wUT|AJQ^N%e>HX9g6}KjCdfzF#k{!k z-3x8G-r#{b+?`vRu}gev+$IA!zcNB^^#YUL0}R$rfg4%KHo145geB@kXqGAt45v@% zU<6Whf1hT~dg$^VQ))RL=P%4(*EZ-X-n%Etb93didfo8QG|?k?ugmvK3Z>s+@K?Zy zv>{74)hu+LSYn1)@SA7~wv#j|d5D%EKRJj=TEnoHf~<KZfagAM>71|_M(AvozcC3l zww<pk=tK!0B;~$npk*FnT0`i@$$`I%Au49J*FhlGpz1o9nn(@ETghn-7l}x08@ZGU z#6F_#fuS>GmOcSC=cX9_===7?+&<u7G4ZL1?DO^E6=3u%+HAlp9bquXMAK}0wL(Xn z2qEy+m&4^A$O_8T1+b`cTWj<ntNHgHVJyvb1KJj|Mm)%F`*AW}f;CS#e6?z;su}9X z3Wfr^=16Q)OjcO!jxX5Ra7(RczfmoOz``u&JFMk48PDNjz^0ehggD)6HZN1M<e8Cb zoX>(Da<KUu*H1ngv4;GZ?^{_`TZqGn4z#aWAH&`NDK3?O4uldFh}9*>HKw(&t{H)t zB<O1}?}+U=Zk+c)AOA6E0Wx^&2rt2%{#BfH`!(E)idt|$b+1YnH<|RF*GbPrC5p5L zpz&3G9^z8_eBy56QbipE$kB_39A!5#fUBFD3MA<L%zyjbKX%=$<;0qWECnznM;W94 zPt!`0EI3GOIw5M)fyV2X2u}GGj{YyJvwtMX7fM_z%od#2!|+y&276Iae)XK-h2>zY z(u&KqUWF&KzIBQFY?}zyIwQe8HL5pmlC*41=v>vXXdN!}EG?5HUVEv8Lt`wWgYIl{ zL|f4&a@>T$&|cSP)=Dm|6`bBWERAHfH1}&&7iASCJ-#-UDU!<7q6r2wz7EtA(hKy2 zbAGjbRBZ@pkIScz_Ll{{mmnpbv0*=)y5asB(nPo9{(OEuo*PntksfDXtWoD_m3ocN zCL~;yT*8}8kI42?G`d$&+*eHZLgZPvZs|sSp$RsEpb7loD1%)5GQ#@XGz1$cQW>F9 zLEW}Qwj!Q*a8jZPbTT7Jf7C0ILMbwnB)S37wiLA?1$AT9gmp0H2fH|qRVb!e<kU)6 zL+y%pMX4p*Ho5dvwCzAL@MA@E`BEqrrUqV)3enw8WMXYSMNJZkaAq>>zU7{3`#z(V zf`ps;nTvErN~~F-q@KdRKGVMPl(ZHYPcSWDg8=vQ9BoLMO$X3Ez01eR2IW>v_{#jA zK45p=2D{g+nL9T$q0PK&>_YQ{2<Fw@&e8FLi^Y>fWB6wVZp<Sq{}EoRWox;}M#WMk z0$^3-`o(40-Tb@Z>5&(8pU?4}EvrU8GmO{d6#HzY+iqG-H3x45vF*h+vY_p&Rb_;x zd0uurY)tm;Vn=T`4f+xwYle-0=+0l&R*KpPl|Z&gSn*>1+AwvQz^Y{^SNRuo11`g1 zzS*1?F5|3+?k1pHI{xUuUub1Ohm`?WJcWapCugD`3V3_lj*1hRFl5Zz7gT6iCl(v0 zT_V379RCc%7!z*?R<_PpCpb;!vj3XL|Cf$zRpYT59;kI$X3W+Gdn!<@EppE&W~|F6 z*g4e47cbpOcF8P`$Q_LGb9{%Wyo@6s&D+%dE_%hqY&vnbEcGZ_pC89@o3tPaC?>RW zT84;ZK#3&_lM{>_rg6Cx;Z+iUBA!w+Q}ukmSc?yZ)_d`}Gbqj}gH_m%m{3g#7Hv>X z*Wjb~z$*}G<&{>)LLT$qY~RHDkd=_JJjf5xr6nie)V@RZ)uDPe${~DGh%B2aGD6}q z_9i$%l;)m0S=e@fLE>tTF#!&H@=fJsIOdbohs=RV2#N|cN40g>%Ug3La4l<oE*B_0 z<*aCWLkH~`q9O1Q*5>LIg#dD`m{SgcF9m`ew@j_?yRgu`!tYpCt<0}1sVD?Qm*8-< zIG*MCvTM6HG-L8V+B%AZt&#THQEwzyX^kHaA!g1IsTYro!dmkSD5F3WTEp@P^dC_% z8w)Gf3sRsiZ;{wnrG?iKGKNnEDBKodf}SmgdX4%1I<hp^n*b)94$DUw70EdcK5Sw& zV-1@FsGwyrjG#0b)cu0U78?2qKj;}-kXS+r%xs%NrQ!X+WEV$(=m*b{m7eE-;*srY zRx2LHjsurgNY<MAr8bN$A4&A*zEKxC-=Vr#D3px29<@qiv0{P|San!X+wo1SHJ0z< zLTPlLsH(c)D?t$}hg-gZCaf(CK65g@-#<!Pbq-C_FoiPBRYy3}&&Cg^Q;93317Xt< z^2}n@!K8eR7d1N&*bq-Xn)_OEytT_G?)tl3hRFzjz~F|a2*h~=?Z+l)p2^)eR(fA9 z+x)41RH%_xJ}bb|f{Nfb53rLmfh5|Wb%fq7VDmF^UB}8di=JjQ#XdS_#3ezPE>gd} zzKMkBM_NrEkr;!h!`}$mAXm11q|ODmh6pBc3@Q692`nH?WwZ57<VajtQO_1jvd!pS zd|H^4GNfO)J}g|%Q#>*ca&%6!;s4ym!wNiq3+oSO?pnb>Jd>ad@%maL3{lm_XJU zYIGnsCkof|zwlHshH?*r;vKR#5{;7T1&yz@zLxK*tk7EfKQZ;BBBb|BsN&MjUcxF7 zlSbw+)1qh&t~5$q{e3aE0*k&l7Uehbe8CHr*7I1H`+%jTJ(8IndbmtS+3r}pZG<{h z7UJ!v8L=}@vKa(Fx{KQdQO`Xv<s23e2do5S%*NH&F)Ufcb%)s50l0Mb+uI}aKP|L> zmny|O778q?Xz1&AG5OCvQx@#GGvIUy(T)H)s@aRPu6t++X#p*<pV3uRRxcxNw%I`^ z-qJg@u5Y;9hv#W9{D~MX{My1c)WaJ!N2J3AmilTVYHl%Ifyy<GcveoCvnPAtIz-&$ z70<O~8@yp+UzoZ^(&g+LiMG<~k}+=dbA*8%#mxo6XsGN1dHe?Syi@Z0H_#lup5R7^ zip9q^sEl(x2gpI&5x7uckP95MYsXJdhKmjpS#5c?*)d;L5X(cIU9Cc%4$U;)<w2>Y zkA~-_eK-<)F<G4%?QtFQ>$aQK7JpwNy4aSi_PN=2o^4%R<nm>n0r*_6R#%H$l=q^@ zvmjVoirJ-j0^x%Va0MzDX|Y@yjsan&LAudrm<CTbJ+PMMNaOFHY&#dFKO=X|7qlLx z6fk(}H-gj7jNK!?76<iPemqBaKlWyM_^Lahe$`bmoG(ncO2XQLeYo;5rRi<`)^Y9_ zt!X>Wqr6S!(Vj1}L#&o%^R}y<vmzs@axFoRVc{ZOkDiU&);AREi=wBL1B-}%>?+=h z^D-HI@cpRNc{Ce7q1>ZVTC23u&%SSyj>?YTZ*PdjZ&Y?3VaDQn#J4x!elig)y?iZ! z7KW_PpGC<eYHu`)M~{PBa<zPH*rz9(l%wk63^4T-r6+183uxsG4tH|!+AX5qyq2z< zPDLj!p4Yg0a?A~pWR><h3nnJf&qQEuG%Ehhu@}GYQ&tC!@|AZMZKjd%TuE?uzZ{tM zoM$SU_K0VdfV64CEIT?^;k4;W*sa&TvEZM&PfNVccE0V@sopodCW{};%DWUvf8H-4 z_LfpCI)Bq`bttZu-<@*wp6$B^ijNtu*~=J=fvBWg2GJ_%ac;MQsLnb$4EBMWh@Q67 z`aYHzjJiTCMJZ+J?5y>STw4fpQj1)4O6JQyt#)U<tas5%SgW!Ct98#3S9zs%1g`eW zi?kYH0hgO%0pGOd1(F(24QQIFg@a^TW;wJ}7-hr@%TZM<Euy<?Nz@G(K7x?v9BFca zE+MKS5@!e76&l2d^r5=psXx@A>nIaUqAk^t{MEWk#<&+DGP8f>(c$?;sNxpG3f?hO zpw}M%1AEC%Pp<C%jDp!X-m0z_y{)9#s`9FCP^pr?ZQ<13O?BsM@R~)(E;Kc+d>Yv4 zZM%UCoKsf*WJ#U(w{re;X}CF~S~-Sm|B`#5Vz9PjZqvQE^+Tyv>rc<pCJ%@N)K&n5 z^v=t5@BSuP^|xeLDFdMRasx}?x?WyWHYcMyB=chT{vK|}yrQEvoqxWMy+KgyrFn6H z`N~$l_7QK#6Wy3XRp3;z5`VJwB_Q9t?srVPiB$9DPKq&fXEgez>^Z%QM(^m|<@!x` zmGP!$%ZfHhGeT<~$&!l3sB_EvF&hs@->Sb{nS75w7UVF<ptRP>mH(`qAw2)UaQB11 zILzIYDD%t1&6m_zzut{M&-r$oxlj3FcnwEVONldXw(sw^o@-X{OP+SV#DC$jLs^r< zsL_i)5zx$P6OYRoY#zibv-?m1gTYXQ+egSWK5?4k=4YU|spjG6uP6+x5PLE%FvCZb zR8~e5*c##a7w7>#`Pi$k8AyeTZa=(3aI^{(-beVaYx!ivX^2BQED>cAn2Db(eUdk0 zeB^p2<azUFko!C?-#xxx2RxR~8;-m@VDzh+{L1WR=+qvyIbUz6<7wWJCNIV$wp<^x z4m6Km-*WkDNV(Or(+_1cAf6MwiZiNhG|M;z(88?vaWJ$<PZD~a>w6E7|0Y&Y<7C^S zJ=}lW#r8ci3ygdArPuiFp{OtszG4CBgHXK%(@UC)sY2SmqqdyaXox4bw$5^|`Q@UJ zG4HE;vAVBtlg;B*yWV0F8_V3#b*#n}33qOSSRU3@H|=6wM->bqZlf~anPWa}o+T$d zFLQyT?FP?O#IfF)6}<abch6j+2G14k;q8DB>DF!`C=6zv{tKU1W#vNc9Uh2#2$SiO z>55U5rMS*&AV$>Td@c3ZTsF(NNKd)z{rqli*Yq^La{)PlpASm;HR9T_{B7uiRUW}j z_T`@-b&ISHDSH)RX)Jj#bn-cNMKBm$-FtQE$z+O{DPj9W;6p897#>?|US_r8;G$Hy zQCh7sZl(KQ_8JbbMZ$LmGx3d{T?1Z`TRa`J`LpbJj9)x#bXX*oipq)j)etHenTM2k zAX^Fx*47?5+dOsv{}w2V1QefOc#<mR#hVaNf$gJuX2J=Y{d4HR(i6DTj32{^)tkm) zVTSIuY+v{1??JGg=O^-%o8hKesC{96{f1ulTPx~|4#%V74Gl?^4UZyB42cXPuvn6# zDNqn2*C?@VT6bd!QTL)!;E296qV@)K&P@Lueg)sDUkKQbINrOVsGYIV&zGemZGx~# z6>EYciz5=!%opadU)#1l&e76qr-H`i=LW>ZXI88wdc}!Pwu&RxT|a~9s)~65kD`ja zU+UJ1LfGdqH!=qZ-18*8+Sbs1BldR3?^M(jT*~7lZztL?;%{MVl>tLtJ8ooTiX$QI z_(AiNrfmkR%Fn#N;zYxLRx#=_Jrj8SDPLX#YR#$jlqg*}u+ytae+E29Ak_26xsWg} zySBVcd<%C4kuFqvg^=5_jopQL;O2>uUCVTzvZS`;{of~q_Qf>lF9iyH24Mf;K$oMS zci=VW?uMxP8EzZ5{96IWB}4wYblQ1Z+S?7#c}Kb8s7K5`RJNi#a;Uto{~P{*Ab>Ek z`yc*5AMwA!AN(g}@IS0%P7cQZ;Vkp;{BI<OW_4}n12$B@ms$=}0)Kd<b2$PomwN%p z1twXjAj#H`AxLd-*O()zd*qGvpPj;}7`UQMwnk40;&Yvzhn%P*c6~VM>QHOC(BXcY z;_#im<yH0gvE-;}NU0=UUvu{Yb`Pe6VZpoUmA=hO`<{^F)P9~)tpT0!T}>kcodURG zsEHY8F;<XiOw42)e%jw&Iz#by*@EO~MN<m7*73k?5^v(U|A10jI07gLJcdGzs8R_` z$J3}R{rvkTfL^9z&P)&KntXFGuKlvbAADL|6DV#k01;gB2J!xeu)!=8HEhR-g1hKU zxdi4_`#KkUe|XHHF9XqPxH>@e4Om@M8NSTX|IXZ6h7lY7<%2gLo;tybd8#`bpHGIu zz47%g^c`0WMFMlRU_HN74a33{kVSw4a)($v=QfCvD0}37Mgsa@J;$JOEdSJub>-nX zsflJqAKgrJbu<pxc1nL6MW7r<*u^#w{^&zX9s!)2lJvY%vMwBY^uu*0FO0!MVDh-e z&2y)Bj=^Q#X!D!>)AK7g0BnX3`&GLFI)C6%s%Wa##6kQ6w7+bt?qwhu&mq(Z85`L) z!`fJ!3=}K`bcvhJrjAu`T0u(oO<VrRbvBr2fm|ty_8(aE2Pjl2K7zV6)Q!gXctT7= zNGVaINJ&$|v@E!eid4OiR(*U0ez(Y{2pK+x#U1aR;|hVo%U=`8TkEWpWkPY-fsly- zu6uDXo9Lj!m%_z$HToPbXNA9{ZUuyG7yuOuq6t&i0T2t$LPhn91fl@Uy&=mCT2?wJ zo3H?qh&*0i%=|(NSzBt>i2Ex*ixGGw8pc+Y8t?SI4FG**y7^(xN9HbmG5cq{NqP|L z)X5eC5RpBgVcVn*W^Q>T76PN1-03P|I=4H)B+t0H!4NPkxtD6%G{p!?^J=X`<nY@y z!gZlj2GI>v=q8CP5di2SxhG+k^NM9KgCFK4H|FcU)-?F)arY-5n;_{7$Uv<kwn&to z#^n}Q+tMRR6%YzZHMhHEu~94fWhO-?$BW!eD@*lK@WROu2ZlnSVH8SIV}VR9EQ={o zx4*jfJCrILz{*!>C19(5S}i!!8v6<S*wHRy(UY>_;)@?H|Lb{kVxGJ1y0wpSv}vCG zg=O@7n?c|~=dU`jaF=F9;@j(Bv|S6yyq%y9H_);mc$kPR?QuHFu3eAz-@9DHbEOfs zPx6a~57MpdVWGwJ1mLU@bhlrY+$|Tss-o$SduzncDSoTEXpdye^Zb|<@w-A<-ISf{ z?io3N#;9U2MV@<?71@M@@}b(?xTy8mV;8p$V_{p}80mZdX^FV-IgB@tmzrm-D;-#i zPefs6VWy?lF7lYJW31dLhyD;!411HhRccV|8gg+szt%3@V2mM?avrwuD)R~XS1!IO zyJ!<b!mQE!$NE8;&ibO7yVQ9@>3L6Lr1$)C7FB=UHA`@t^$Mu6-FxL`Xn7Ah!mwr8 zstTs1-c$9ZSi%M<glT-hiR>&EFKtwTinr?aV(w(1JM2Q!&(NzC_Jh&xr*em%FCqnz zQxUr#^utRw38BvTwMM#I+AY}s{q@%QI%IPq4kDF}YV^lpf3w`}{#~U@CHDeYxrR@; z&aPymrOwaS+JJa7DNpX75r+Dqa>Aqb{ggE73u6}q<)z882jX*vdIw~Bb%A;pWM`>z z7bF68z)x}S_5B6`cy<)S57B3zyUou5`S-&c&@xryZMC9)H;FrIb8YU*5YA_>_cK!; zIfleBEh`Hu%)q#M%e5~NduTk~-X6G6<iVca;PpLaa}M~@x`j+EOvuu+6JJ*1;<_=) zyptQ7nNYgveogV-Vqwj;7CU@bZ|oI&mTLo_j*}N#RVM1eSu<{K7gV8|&DaAte{zgs zcjyJ>Fo=1XJ}0NuPf(QD{@Zn-R;?;}H^!yW9>3>U&ey*m>pMm6H~4=Rr~j`ubh`hU zIQ{<{(*HkiNdL#W20#LwQj$ffi4eLL{y{U?7ytm;|JnCpX76NbYiRf1%n;LBKhB#i zXg|8Y!K`-WluGC1R&04~cTULVO0$eVXo}nQRu%<GN%Kn}V?nFi+COi+Z-5AaBJb1K zbkjPsMS(p$^!ac01H$xiRZSh6;@1`ZN>`~a@fyta`HbtOOj@uTrRe|o#@Ks>G2AO1 zyNBphvwAyzep0h(!?>l~E7eyDef=_;mZQ#7Oh%gOmI9*(qYg2^KFR6H`Fgnbnn-0r zvnhv8g`76kVoG7mAiGy1N0Y~@g#tu4V?|Y=LRB`1poYrK7{3~ei(PRmCL>F9g~zS~ zC~(*EgZB0P2XZRbLU-$BEY(uRB52@X4uol(;IxCvvUpN>qQEw#x=U<nkdhC*rG;qy zccUHC{|qiJ{26T{58dC@y7cjJY1+0Uk?Rtx(7-D;)AMxU^YnCM<n;#sT!?Jdq?$-X zJK#^D5m*?;JLIfGc>rU`xTghCy;CJJkfAnl*Oq`Q!KzoHeM-duE+cJN3ei#FnoBW; z7kQ@eZ}Av`mn*el;x*XKmZMlin)6@>0_o1FcQ0LCEyZXYUr*N6YfMYSs3mV4KL+FJ zHY>&0ZTu}urx%kBknLwX5dIIoX^{wRB&aG|^Z&^=ZJ!dQNn-+UZ%`dp53F9Q{>6kC zc3iMFhHR?|><y(Lc`RmCb3Prbpm?*lXP~CdtcIlpk$PZe1o98zm`-Nft(dV?(Wl!| z(FPP3Vc=K`fm<mwxWNLpJM6nvsZy=R(7mdSZZ+uSa%Z7jtTpo>G)%~_(BOQX)jpM4 zv3i+dtB*qtZWUn2i2H&WL@Td7b<VmP+u3kZ4UxpyKm*4@aK-`gd^9jJ6Dtx6qc~us zK%hOY-H*=GV@P&ZM}^g(ZX=O{l(6foZ;fQxhfZBGHipQuULk^o`N7*+#s=d71w?2k zS|WW5q<f3W8`VVCcTTIGpzAbDRAn<Xq^j7*WX$Gc2nL|U+3pBI0feq~^tsW}uzzFc z7Ubn2hdZ=h#H^Jaz!y_i%E~3KPvdD98Dc>r#8Jm`8^9(6U=Z?k_IQmO<pL`RzOu6T z2QC#$q(y^R6U*M&53q_;0w!gC9e#FIHOcg6VSv&9wYZTKBbk&^4^a$kbZu8tg_Z_V zoLi|Qvp^@6xyhb3<fPMp$z_wOu9k0E#gxKY4b1IxXR-fG<_X3{$OFmFi?k;`%o$r> zemb6PJa_T)d!JtC<i0Z|nRpc=?U+zzJmW5ve-kSDLb_Z;NSz$IUL+vy7NJ?k6%xL9 z^YOpH&JW*w3zcE1l>!7Lt@6+g&%I*YG6t+8U%e+O#=u}nr=gCmS?=>BUMt}?N;7dE zC50%(hcUTLE=pm7lBFR}(cJ3xC0@R+ri+?6YUR5EMbJv6I{-^^5`uQjBOga(b39^* z#tCQ_=VrBKfnZ>xe%>Xq{}qmKo=gH56i`1f1ndSGjw&?>Lsjnl)Nw;k&7h<}VUp)~ zGrPG!I=k)UU;F?y0V<zznrqhMZ8p<ywfxPEYh;LS@{)uA?PCz?J5F#BjP=$er9}f( z7szGSr_ufvHK`99&4S|2#C)SMof$%ZCahqsiN!OCwB&qR-=sv@&JL>G9@DImc4fx9 zR#q(s#=*+E41>&4;Qf%qWT}o}uaj2#Wg-Zc#PT){Y}mmqN*TGU)wBF@0h!iRwg!cS zZDAzjX(||;ebCzkt#-+a{h<5e(E|bt=C#ouC{hJo`5)4k6qtBzWHV)jX(|_&u_f@= zb&5Smn9BNj-Yi;alFy6qC(EfHYG=+qhSQ-E;%YcV1<hBhuPyrqP&R^|M$VzE<Vpe{ zda`<0XXgx9Ggyt~B`kJ#P>$<?)1fSIbHj^5w@2{zG!}TL3nK}|z_9OTVPD{N=X1y+ zF?U|^V?j_hRQ*OVWta(8BXR_8xms7@vVBoJ<v9)(RAb<wa8uRs!%Su{f};ur!H$rL za2%qlS*)ouYJmj6f3evGVzvE5GyghNQhY!w!h{kx{yiE>pNK+;g6zM_J%k+oPQesf z=!4(*MB2(T?`16U6BnI?r(*rS=JY+hP^Q6^>fJc1c^q(-N>sqA(ls?XeM2;*dZa2u z9>{T;0KbnWHuwUTkIZC<3k0W5+i>)z(;3@zsKk~9MfGrZ`P9htn%4jOA?EK?^4_4s zNuje?9;r+|rgB}VVL~3k={LVZUW|mN-EbI>;^(hz?wSLH1N(#F0GCZfo*xw4(3x~h zCo*U!{XQ=^Ua^osVr1<{T~&^YFbD`S(=%D+KvmfR>N8BZq+B^Az9rSJFfIjHg}oPT z%}~acj!`rvr3x9Tt`i6IS(FA21VuV@ruvUEKC|>4uXfpNu|Xr^EgRhOBW_!nx%6TN z&{`$99bxhY{`jM^!Q&(6QEPudt{C97$5B~PJeV(2Za%oskRzP~r{V8e3FXayV`JPw zeItaib`@01zPhMDvR-mrDh>;Bpx^OeXfi=g29>M5WnqK<rc15h5m~_PQyo#AfH@^T zX=_nzH6f~K=VC+~sG@RG)xvlnQcF~&O~9_cyq|`s?SL-hM(|)z1#{Ix?JI~_0Z_Xm zcCD_hC1V2te9@0C@BOY2!OB!C*gzBQjbgM&_UQYl0FNrB)G|K00wF~2EXh{V@h&T~ zaUsdiR!aWIWOIvpJ$}rHw`(7s9=B&62oA*3LlC11I3N~t{2`=uf~!+6EQ`2Jy37Dh zyWom<g1FIRis$dc2gmz!2%+r|#Eck)O(tjBSaAFTKNBrLP@j*Urd2*gMT(#RnL3vI zY{QY0rz*jl_UK-OwSWBYk1v_3(f)^l++n**1GTk_&-iINecjmoSO01@ORgpjIzyaD zY-)!h(NJPGRzs0NiwZEcsMh2UT1t3enhLbGs@IQhXxHinp&=n+C_DFZX}b7gVK%D= z7`Xh6)*0tDD*5_JglNN7gs~kdxoLiOn>hIcVLGFjn3Im;CWi}11j-f~3RlgKDIN0} zt<!QzlNFce%)L=I8t}j+x^#DKtRDijGm#s@<+2(XAuE-Vy=z%Bd%mlZX?XEudzn;5 zYka6H1i|K62?(nnp)x<OC->QsxSQH5kDVRzNqfh@?Lp&8&BZDEyN|3#fFIOsm`Cik zk!4^Sna3$&lV~P`nkrJTV{1UBSE(>mJ}{p>yk8_a4d~2^i=;N3(kcNfT$P%+%Wr8G zP!}tFOFt59WH$HAMg`)Dws@E>Hp^j3B;jhMphiLU4m)hcNJ8+vHifdTc*t?Rf<7xp zuyvQLb*sYj(^k7lp76X(+w<njvGkh1MLE_jT}x_ds5tN@$Q@x!eHAglSB8C?Q0_G! zgf*hOj=1T~ap*j>cMeEfqC0)~9_o>gb=~IMeYuWWIpWT`VaQdUEd`-b?VXJz25CzY zltFwz@LDLX^s{cTaxzMj3|%bE7>2Rx=1vEFDv0Hjc%Ud_S=I6WNK=9)%S>VERZdgf zMw?rpOlgjk*88qui!M%Kq-gj!Ww7~`?)5s*9m~f2<<ml$m20yuY8P@a@>Q|hZWJ{m zO@fMpaov$w$)y(Q{dF^}{oAQo&((A2C^l={dv#C2y%R38F79{heYMo^t=T>Jn8Bt_ z%fGYQKa_vTGJi6G`5CdQSG<OLks6y(IoCP#Hm~^&Bv3&<G*dmLN4|W|r5wGe?yJ$r z7loexmG1l37)5}XlLf?2;aOXlG8fcCb1IsWfAxj*K(FvsJ(FUqCW8&Y*687l^h+J+ z?w|)94M@F!n=<{-&SqRod)Ojr(-tV{9CA0S;~R97SExx2Tl;k9f3@?UP7X~142k{@ z+Zn|WFgPs?`5TmaT)Q||sB1k`F#&k(Lmd@9h3KgZ$on8|Y~M7{#n68F*v@bNEdqo5 ztAlVyaK3NKn|KO81{dy5&}f7SqfVjMC|Rpx4J~%9eeSDayEE{hnEbRMn%C$fMcCYZ z8aLt@y*%tHrz{S9`UPGO2@OnqX%SSO)HPS&S7{tEZ2X6ZmN0t{Dv#rKgYdI47;$`f z@}>Y7+fzW_KbA3?Xg>7Mlviy(p86vz0)D~g5{752&Xc*GQ%c7<#sPDAwp2dFGkm7Q zj=O|Iu{QM<cO`qevYU_gypOi9&;5uD!jdg{fFk8dBweK$sXibbUDZYk=r|hDW(uQX zp~w8O^YsI*f!;=_*_k`Wh>ykU(a^JP^Ks3Vgx7d=k<x_j@ek6+U$}2S!QN79_=%8F z{<%ZgTR8llf-n`jkpLXuPm=D@*%X6?KT|<8k9a&^8M#*!D(VH{Ky>?0tA~eY!}p6f znKW&??b&;Z0T4Uy=Wd7mDMJn(l8Ojc2D@1>z#j5rEkEJWH~C38-|J~tZ+CYL4oBNV zXYGwauWBF1UB_Da@~wOfA6LcMhULt~uOk+%xPR)IlkR79CK}~HNNVp$x`19~-Ymms zOsOIr<9Pyh)b#<~y@9LAf4*Brf$*hLuX*0C-W-;Q5oK6*p{MVt1=Dq(J_h5J#J?6x z)J>VxBC4e>_LxONbD6hRlRI0V%z7>N@?~@Gnnya%nPe2Eb-XvID=`VuRIR*2(*>n( zVn0^jKTjRE{rFEqy|(7HbVIw8&{lR%j<&3<N#hj@4Mr_)3=4D*cay}VZQ=doYqA{_ zinzPhP6jb3s_doyd@E-R=r!zD&~^EN8zZR3AI2V}djU0%lF9Er_cz$AebE2cNoMr_ z+l!-(rP2R`@bZdt=~^x`0I;<P01*C{?{jvscQQ2pZ)=!*t=%`r>i68gp>;k$#$mxG zdm1@4uPxk6wcVQBZ9K_1nImEm5JZHKjga>Md8THGKRr(ue=o#>yrS<n8o4-8P%Kn> zD=u}H<$n&>JIep)q8>Lcmvq-m^e>v6vU87DWnvsJ>owTXyZeR7gUjzvS6VffM^#?3 zdDee?iaNRFP!OPNDw7dhT+F^z$-=Jy3hk6EJ`5AgWpi0PbDQS=?j5e*#L3GgO&TRo zq^agiJ+M)u%POTvD$YdF5BucMCQbnZ5cyFXyKL?~8|Y3wrw!EBVZY^fWD@z0T$KUg z80U6O1K{#U%Sm@X0LUz>9GXU1B{kayI|owHLE(Qw)V(T<?x0FqrD#G*BTqZ!hV_M* z{~pixFTx5KO#2LzIX|#oI@eB8Ys<b{_wAO-Zu40?JG+JGkxaFx<wmep>)bDg$BWyq z^L_Z}w4PTDy89Bzi_cS6#X%W*Hp!O2{iy~R%p>AXNrUo+O0F<>+mONT@+kBcvqY9< zQ}3i@PJs*zS#1oA%N1>EW{&g*J3wVh%6L-H;?dco<o*5L<CzOEgC9>I+`C8Wr-y@+ z)9v@w*)0nvpQIjM?{8lhH<wR*zivtRogc=x+QOGw3Q2fsu<av#b=Ft{aLqlc6K9<U z<#lIfz?R+TcGbP6qvpR>)8l2X&WWVT3u(PHh8&ZW)yabXZW{v+Xa9y@siIlUk44Po zF1JVHC_}v^)mFhxQ|7Chnk<tp4B>q8W(=26xVJSK{Yrg8CUDm$MnLIN9H8D-Jwm9K z;McAz<aGm&>1=ycrIsl8EDSblh5UHFgs20q{%nmEB0S@l2Drg4btr2UHc>Y9Ykgic z(0!9=1J?Fbn1)&Us;&v_-XH?S_K8RSohP;8X_uKD?#gXr7|YWfDJ!FosUv*-|MKQ( z`=siw&3gM>7-Ux?iK%ECCjR1fV2H4v<>YPkXo9)}jydMGW?bSLZ3JsTGGn-q>|0{A zfT=Fq`(ZLVfwrTxWv!9uBt0U|3?uV%_s8QV9eVovckd57Rdw6t7vx`(-_19s+B6R( z)2Wr@Hi96kd!F41*nJDf4w0tl4ViEXtbN*q>au!+YUsZMsyRM^CmnnOOzN=u8jfE( zC2`@~OGpNIF3yd7Ipvaqh6iZ!z)46jo9gF@rY2eQ6;%)0d&aQ&wmD{xIy(9piBo5s zI$8ZAK%(+ph+zY19Q_zH){+)%u>Dl$MKI9g3?UNIm1aTyYO-}Wt&KM7nf-AdvJ1mt zIO*;OI4)m57&t^0fp|^!w(vBw;#VKVLEQKeE(GizfJ6mi3pPesZFkTu^z-!6Oa$I> z|6~$bh*Z&I(eTeL@xHDa<qRg-bO0*a(y|wWBny6SP~@OWs921*8QwzXn;FEf?oK)Y zO%5MG9FDrx7#^gCK8?d(xw8;;HFHMb=J+FjE;3LG?mOoBJrB@(8<g-tTy%W13HHDe z!G)t)>>$aJ5KUmaz6G>RKKmkIM3Q-aU^hj5yc294u)(rB`q{Z(&A+os&e?-{g??;} zzG33Nv8`EOky?04#lYaablHYgp$db<-_U55Z#xT#;!0)`L?$xE0_L0jnbOi^cLtc6 z(RgK^&PnE$SbC|XjF3ae`Y0Q4pnZfaWZiom1|oEcLb)OMxUCIY_XKmyop~ZOx6vD) zh$|u5#&DePzv#QaoSQ_v$R}19$d&n7EL-y%BA&TN8b@R#+1wFtI5|bIhl~qj8%z|D z810khZ#(~l9p-U==H7KltF=j6T%Qn*nY{>>OdfUW&2F&%Wy*uT`Hme3#tTE>4rj9q z{O;ZNy<gStZMBx<mT&?=W+u1wd6#%LJo@EfH~XV)jZvyXtTzwq!Q{F);mwBuXYkus z6j*e`8cqk*515kjbcpk<oGXK=C_uELV$)T5>cN&nIBrliWO|Int3q`J2KDCwXQID& z@osfUF$Bq-9yL3RQO-u#tn6<vf7Nb3&KeCxUY*Siin$2^Auk}Hk5#0AW`HS%{!3@- z6iXuXp=oN<+<8wg3c5%rK8OAS7GacmuwUR2<n8wA_32LU9-SRHIIGgSmf+ihL<GP% ztSF4$sd17?*a>1F0UTVii|U{j5#`~dERqf-RSK?@L+=mMFTfs5U5Wdx-0};Up#6_O z0Z*5dyBtW6p5W0SUS1H?tb{r7fhP0s&NTpwS+%VyEq2ikc)w*)aCK<Zp~r8%)649) zVkBS3F;Eb5Dd;S;VcSBed+S6*?E{P@BB`goLt|-}GFti^p4B#Svcfh`haB_DKhtrd zu3)HN6Mu>7LPf!qpphjx#Aq*!;Gzy8K~jieZuwym3H%!CeSeBI_Ag+;(_EWD;R=B% zd7Hxov4IS&5L1p$$+hW@K*UD6x(2?|R-206S*rp((^gr3Rr%5<Ts?P@e3hJTM_TI6 zYICSmy6%=rFzlPpZo0ypD)kyK7<W8D4G+U?FF@BdTYjIGP?i3@dmb@^c!UV*ht*;q zPhMNpbPE|hxg>~pjG;18GbTlG*wI(yCf@a7$rzn~FTf^g%Jk5Sd<5d-=Jw#`<>lt% z4F1XW@o`PP;%1SOKmQRcxQ7J-o#rNxJ4&#AzY8mD_?pEi%vZw>J=Dfohz~ui$C@0C z6R*ADNPg-_7Z<xHvNl@>-WWW`g0sBeT5<*6c-xnEzY2j^yFMWsGb=Rg)I8QUA79X} z8voY@)qd|Ve^kQ1Sb7cjgT8)m1pjXE;fnC%JfrI;_56}_+M(N)u+QCGXGb*i{kre) zj-+-tJ^wMdT8G;CKKv1P11}$|*ME4b=j{(Vt4D~~*J#2es%ylB$9PU3p9``+btzep zVEB9W^MrMMm#m9yidV=q_%=t8O=Ja#m8Yy$<6Qh}^SNQvDEuo%B#s5jt}LuBO@*4Y ze_hCh)^BQnPlimJUgCcJiT~uTBJM5V*hXv)s6gjK%CCY=&^GKswqsyX0<R_@D~?zk zZv|e>J$qvq-`U$Ey`uf5GQ<Jy@gMO2i`j-$dGn<tcJik;96G?uQ+L|*P+ou;a~gd* znBzJ*BBA%(E~=hU>bQS}Tm&J8r@Qf&0G^pZf~@-Xm1uAMJG4-YvA=SY=|g)~5-#cV z!n4bYs8DiT<`XQMs>0R$*#rI%@$V0T5mrQ+>vZTKa4wKUNeMqCMEgwK&7*avBJ!~h zYV@@P5*+(TA4Yx=KQ2jlEk=!noBH#PlJ>b<4<`<EQXGa*2aR*p9cZkqHK2Es0P#St zV4$E8IK-lv?q?xdp<e<td>H`|GD$loCbPJ3{Hgx8)NIpxP9%@rM`_#LZ@ZzCoAhEO zt9Kroj~cyaIRwbdM`^vTr47A*hRpV_3;6T~^}kzQqHwagcHL07ibo+R#{YUaX>0GZ z%(+ZGOa!n$!jl5>U$;oH{=$QaIFub$8hb1WRNKjHV4m&2Bzk_4<h{O+u0gYv(nxg! zZS`ge6iyF|#r7Y%c--2(zwcccczL}(Z|{FJo}bVEflu(h?s-Mu%$v^{el&e<)~HE? z3fD?A`T2lBx;y>(Y8~%9AY<`$zg>Jh9%Z+Om#0DWc)<ZC`@smaFts=lLVs-SguV|; zI&3t?y_^4N1?OXF1M!#xZpy}d{?`wsCkM}u`zPDcoB?Ao;hJb;9^(!T34=N_{gO~p zVTQxQo8xP6fBby@&KnsKa>9BA6005C#_RF>>h^j+g2GgDUjo)3x=OaW##{iZ)-Z6< zv?Wm9?+u4X93%{JuYua9biyL3hmXhC)#(vP45DM)OOm21L(itfv}I~GM#WtUFUN>~ zP@Xa?{WU#A*r3upQa`AVtReh%TH%#(D%6BfjjzPU%7irQ06!l#_|sA=C2IOR%O>D& zyQ(b9Zvv=|p+Ls_pJdAE@Azk*HF{>x(t=6u<@o3D?)|Fhx&iuQWxHCj?lfGS@zFgZ zBHRqqp+RFW0?nvyi0KcQ;_3)Yw7KDkzj#f#jVI=Ot=1)>PC%J{u_fgbe(76v<GotP z#dqCjrsweB2`~Xk`>1>FBK^`^B+Rhul&s-sS~f9v8_+}&b^Z-n%q&^m13TPV=+D<j zQIJAc3A<5py_jsbe-4~{ggS3QLy{@V{P~F16b+NVRZ4Bnk@J61XKoqI%`-+|d5`hi zdnCl1239+fP3pD9jyGhUnkd!Ro~rIZk|q<}*NQH_FDxpv(aEi2Mi4yMb39Zx?r{7s z#?GlruqaB>iAvkHZQIUD+qP}nwr$(CZCBdnWY1gonx5_-a3Ai&IcM)!5%C35xAd_z zgouF_AOLv+nIv*DxK@0unNkpXN#MBbGBmFJo#AuNVdOTw69x2GEOOJDXxYT%y2ZS4 z*_=ao*riW2rB);jxRz_z2Qw`Rp#K>6V83K;9S}gnoh)wK|A3IMzRg~y$?>$cllgoB z)KkmT>v>0eZ?04Ci(Y<-sOWD!PqiQ_MctX~?Q-?IBJ^epIZR7UId>Y31_XU`0$p2J zXmSBNHwI18UqqfxYH#jUl#Pf$_gsLxzvSo7f)7&M)WhCx26NII>SQ-lFAD|u)}tU~ zI(7rYw;><7+KA$0=USoigeGTi*d(mYAq9JI&+T25@#`Wn_(o~S#B>H258AXYqLOBp z&MmFVfm!z2ti77E(Av5e6OPC!N#x3&sz;#OIk|%4CmI+gMv7c1mrx=52c1B@){;%j z)qvT8XJVgTMQP@$UmP3;DJ>TqKI|VG_4|?nzQvWKg$Uu5jw8@Lk}Rmbs#>obRyuR$ zlI1N_%QMc|ce`@KIH?83=;I!gm#EYg6_{glC}BjiKnCT$XJc4>ARI+iS#3A4t*7=p z?T@oVB0t;pJbWf%2zZ59*bv(;Am~vr#S!D^=w)(21phd-pXU;o0BEt<nrFmon@aBS zkJpCcfN-0|!+_-vhuX_?Q_YEfCx4f##oJV>ln-{1Vtx;KdpzpDzu!*7ftij#6M+&e z?isWGBs!(B+#|NkU(?l8sn5@Qyw(?)0T_}D)=^^(56WUT^f1c<hX(ULn>D1s%$zr$ zBxLw+_+}agf%eCVMtU&@&`k6g4FidBu?}P!h(O~KJj$DhaZfq=W?uLC8Knqf1z-e| zR=fMu_XMDP#}mQXhkq?AmxRm>EDJ>OQ!-v`|AhhW$>!`Ae#2Q*{d8i!ezRy+8`LUs z{Xz$I%w|#FTp@y+A;#!{D~+Jz@(K5CBmv(rqw!lzf?niP1({=sOK*HXpHwT<9=C6q zgLSGfbG@ZgqtYW$Ttt7RxkrQn3$fg}cK;)Vvp=>r#it*-f9PeT7uEguNX>^D%IYyA zgAoG?zqX;|?6P&UWvw9_ZW?{=$`H)R?!eqZ(g!Rr9-Vc}GxQNnd>qrT#!rutv3Lg} zuS78lg{fu#*B#zVkm$_kvu59&>bF&?#UM5RM;KmH?L12y9ShgAVO0<5I+>k~*B34} zGk&1pK*2XY0l1|9kQK86Ujpx@1nD!kHrWgx`Bb=}pB`spT^L~)3@BqqoF|=l*43Ms zg0`EkXLl7@=@SXCV@Qem>jF!@jhmX-()!Ow4yl+b2}nkYuHVqKb{+m~uC!pZZ3BT9 z&THVAg>$w(=Yv{|8o`VJS$<bx-DpS~Li{M%>n_=;pydO}L~MvAmB3%XuL|s#3q%o5 zL4yyW1LWyS&dg)9^~(n#P<}_x=CTYgqv40MN$2`BtP3}KHV?FShR%FbD^OeTD+(`E z%w0<O`XiA{mPyGYDKu(A{h;#4Ugdf)5ZBwhL6fz;l|vKH`x$K6PV&_h0}8s-VVq*) za#~XIi0Ezb$5aD+$L&WbF5+zZHMt)7Y!22*;(|jKXQo<ULj4)E&~v}BsQ55Zt%x^* zV6E34#3HnX=Mzj2Fyzv$&#Z@#>HQA|G8swPb=u?zPE!^fn0(h1HVIn2-x)3dU8Md2 zAjUz~_a0jg+ueL-F>|=1-A6);l3nIeRC;S_#IGDZ5a#l#%Z#^FvdOg}qeV{)nFHE1 z<F|L7{WUUsscvSX(yJ{sswMq})CaAH<FEUB$bl@N+oQl9yF3gHUP#FpvY0$+whzU9 z=Ik5jO!0Lvh6B<^lJ1k2n9MDeJjHa76V|1R1?C7?bCuqEqeZ*0w+7bSHIc!VYTw)t z6tVd5^}gTDw@Uab&Pfs15-V2hExt#Qj7$38e;3CVM)kvT;pYwYE9#aaP9ypk#?G<S z0k5&~h<_z2MW(Qvp9c^Xg1tvJWe@@>zFn28KWBk5oRnB;%%@Y+xRO&H<rVnnXU{n| z*G>*1_Mt(e+|*4Xjhf2nJ%pC*xaU~jh?}2(lClMno7<;SmAf&>MA@A6f8JJ6iFoB{ zn|6&OMVt@eA{#+Oktt)g&>yMyc0v+70@fCf`h3#M?JOWB?BUF$NH15I=NBJXD6=3{ zsFO0NRZp0-QaCtqZ1#MaD;0*eoXwrZK;REqWHKU=f&<}QThIOclWKsd4*oE}IV$Ls z#j`7_;;+cLeGIWQ<|7iqODZbd7P<tG=X|<vuhA>*xW4PiEC!>UL!_EP4VHOMWXqYT zTZR?=1&Gu)J}z4n!wAzycdYd5Z&_?L;{yUbC*9oZFf_U|CQ!-ksGzsNU_wKfFeVsK z(KjvFo6id4+mr~l+Bkb;-i@D?ZdU8p7iPp5f_e}X?y-xNpTY*6YnmtFXwB_Q{O9@5 z>x~%$gVJ~yb=8<q|5{4IXK%#;WCaLh(mOipH}oOVim^aM$actJhV@AI22QcZ_OayG z+d2o$!JM7B`W2KJJTjWUCB#6w#)58pk^Yq(yV<DiSItwY)H!;!LxhH4m_3i<ZL8h8 zd3SU1%1pSYSwHs1AXarSY!DHYWjL1j51|~2JwC|{1<vbQqu&+^eyJKf!Dc9p%G!?p zGa<l8hM1DdxySQ3&(teNjD&PQ))SjJzXlEbc57@^vl{pcx+q*kS>6Je-kq&35~c=d z?js$N`WR4H@Nll8T`WlIOqGX=&4r&ko0JnHLJ_KPX06XP6k|_EO?8*XW5ydasPhU& z$O^?<QZ`S+)yo~!Q9zRXfzy(q_I1Ir#myxH7Boz;PjvFR{B^|L@2`ZuT*o`;pwyFT z_QN=;<KgdR=jkZ@#<Xs2fZB!l!8I$?N^%U80gbgoH-8y;%F4k<$F|}dB&A?LS|w>V zDeqTB6~|4iIYts0f8(#0=hv5+G&qy~xLdUaPMq2B#{rmZ?VZ9#{viKkDqeZ3eVr;c ztc+yc5H3vT4^O6<@LUYqz3q>h2Xum%xqlT}u>VV3L6LT?mPvJTRLf=fQ+qmtVF|4e z(|E>aj|xT>0$j1}Hz&6b(Q_>xL*zwBi>V7u_pFSsj!&(&%><XEZ&l^}gedP`*o#_G z=`3vps+i~m6yzC-Dep#;1s3@&zhaiDF9B#-T^}MkyAJb!Up>?d8<@858;{*A0F?Hl z&z}>9ZtN#K1!J>^eP=5H*W|b}d<tZQSu{(a_q0prKZYhmvY*a_(5F*s>!-@-{lXw& zd{%I>51)-y>6n^7_cEW#j+v|lYXNnN+yXoJ&--qQ>TsLNWI8gBe+@WH*vRqyv#QFp z&a-wWAyF*D;j_@RNdA1iPIfu#l8rw!adDnOxcUmW-&cKgXz5ESVu02#RJta(nI}7Y zeFTtW9E&Mxw8#*X?%5U9Hq!C;C2}m-taNtY{3zbvEr$Y}b+mP`LQMFMUr=KBALvY} z`Oi>b5F6%C06$wtP=#Fd@w%3{vR^unqp+nt!rAkry7yU!C?OU<mg-ciBo{3j{=O`R z-+zS?BVmw8K#~T<=-Zn`zYCKo0bdhw>KYI6n>AL~=5g<#nFE1B9J&!R{fh{_x-`(r z=PzlvrEyqXko2${3pCiQzUlq3>a+A=S;%qH+-;@+p~J$h29bjG_1Dp~EKyqB6Eh2d zCZ8@h1Tkmk;*wW|DCXso74ZGyHs55i@eS?<!s1?}IFW#ugvlD{b7=J2)av9zHtqLG zXPRiXNH<Rr6IZ9obxiWNs;a{S0o5Od`$`vGRsRrK;X8Tm49lM@4S;d#s=34nhPy%H z&Y}>NKv%}UCo+r`?-m5+P!FTS@}pAWG)DMdYMlwb)kxVt6>Whzy9D+`tyvU>Ar<a| z_=A}d-SOg){nY1v&bZEwfZ70COzG|ghSP<p*{>K+v5Rl#-uNsK6_I?4F0fygaRJ}A zw8bui*1KyigW6hXHi04hUCRLoz<fiFqSB)KY>l}{AlPmye)?ujz%L!OnP#PDHUxu6 zMU*jsrKBx9tNnO-q4j7_ygL*#dzvWs2woY4^GBy)(UJQr@}S3SD;+SB0$+G4Z9S15 zLw}=rbiFz;cPOHi>**N*+c)nbahZA4UMuAay7X06nBOzZsnewLTdLADLGQL=QYZIB zC!ZK~I<qO8GXGmlAk9SMi7-uI3#XzEj#>UERw-g@7bKhATN*hR-j&&B?7D+dJ3iPR zy&v)0M|TDFYEfKBmhTs@3_~wZ0I%<>6H~AhVc&T}A*UTFJhip}`u=`}$r~PMB~aaj z+NRz5oZ<4263r<?pdhL5V8gg_r@c334X=s<&NqDsl0=>!NJb~o7q5s%yQO`3CC~9s zmsf5`)Jv=ICZ!p?)Y&alb8vs9lBP2)pS??s{VRxaHBPKaAI766Wy1;f?A)ug0R9F` zF*y93M%{WFsB?@hS#QNkdsQp&c7arwC(PvL{m;7#I1gD<rOvL2Ff_^~7q+Q}rxv!c zwQ#YxO2uW)E?Bmg%{9gg(4pFSATtTcl6y~a#?WB`I>w?QLTKqYMY5a{!A+>{CYyne zv5??*df=7A1%0l*T=uyze|>yPjDbMJI?2en@gKtI#eyp*@TN_=vTa!&?i`JT1{8OU zA#UBRZu_cUrxQYOUl?OAdi5<?WV}dXUTB21ogPG;mnJDv_svpyb0PyrPXOhIsk~Uh z-Kh2_)PfbUVeHgduZR)hVN6Ul>DSS?I`k)w3vuEcEJGE*O>-6HGiZ|cCbE(k$c=Jw zD{tunqXC9`%$A!m2H|--0PZj|_z7ev1pJ7c302VwMjWNEyQN7jNHZ}Ij;hyCkgUnA z+>pM7L5Dkqf%9@dE4t&;@SIZx*k?)JBp;{VZ`WeN>~wmX`HWLrL`$i>kpk!!ud~*x z;*SDq&al?9$PiGGB@qxIWYJ7q^kqLYHik|syjEs$lqnlB{M+t0IAyj0GF<TN%{u*T zTqm!zHXSab1)P2H_j1Wx{6i3NCwU_;OLNlL1}p%SW95*@W0LJJ)t<WsN-bsv8lDbE znYIy^L15*9g*&`?<ZZ5@Xg4YScUT}{t)l_d!gN8|-Y*((OqTTh4#{maT3eKXPv1$C z65XvqtrLBH>eO7pH*1puI8*H5j?_#dEIh}bv2s`y9jmUqTyXPOvLTsl?SHQ90I?)O zkHNshfy>k*TP^3HKeiB}3uln_v+^T8uW?VI66o7ia8`G69QdElyZn%4=Uqg9{w(ed zV(M}fYCFwTi#-7QePdi1PV7Sr-AMxe=uW2`t^id~?m9_=t;Y~`ZDWH;9-G})l89im zBZqttCS1xGnKF3izNA0_FUZkrnT?v9$e;atXUuGW$LXLinqTuw%P#VLQHo?D6?Q=& zgf?Y;B()XHOwT<dg*$7qym>qp?dL$+yNDjjUr)($5#&^L5>5*f;ZgjOAz9D-Fmu9A z#dARC9|TAtK+VK_Cn)_AiDV9inIJCx!SWF!V~PRTRX7b38w!Z4II+vk-_{WQw+b6i z)wyn6^m=V}jHXF7bA`=vlBqBSR)KnsBk8x6VN&%H!p{x5S$%eI7WKQ<$N3En3Cb$8 z&IL-6OE54^$|owuQw?zPOMg*xO4eacX0=~Xzh0=3u$2?h4PQs!&*Y#7Y^ae2?#ox0 zHtj#4C{y)JNJXg4i79}Y7)>m|Gj@}mL_v5w2gw^YR-m7gE4!ClM9F-un9)QZxNiVA zZx5`x$v-E`&ytyyXJ|5{S_-;zEz%$-J9q@3YGN4kFo^o02e{b8YC^+lO<5wz{GBJl ziY40-=W{0&s1;NMOC<GhGW_9wzF2z4`cdOdk>t%Em8(}a^+br-)?e$1ky4(-q|TdX z17tFvaeX0LI!jko0Y}!FnsElD8%NG3r&!%U7CaNJ+T38!PjQ#y4e4grgv@<hl>P`m zZunF(#s2uSS(UQi_paRqzhf<8pw&lxphc$hthM!#YqlN;B!y&Lo6IekZp0faJbmpU z-^h(ytG2DSitb6pmNR@2aVGqyJtxdb7&<*tD|ti(8jImeElYsz>ZWl7)*X8>0asy# zzd1IEnV#v167w?yzr$DW6gzr5W#pMf6@~cljQT_~#Si5VkN6CtJl%3<sM9%p!ulQq zl*$(eV-KFtWLdO)RsiNactQ1s6+^P3kpXBRqiCJ}yrjL&hl~!ttcVA7#M4shL&Wl0 z%;mp1oCl{~@n{reEh&@rXk=>KGjJg`LPdh<R2-|Hv|v65no+N-V)oEtY`-tBip21# z+}5Do%gU60_g`duXp+sUn^T*S{=C?KhNx!C23>zKNTNv+kw#;k;BjMrNBA#O4i~^h zee}2GUlQ*7rie-2BwQ2$@tp56Be36HaB=F_tdG4S8$eC1y4u;UPzq<Qi(aOn%x`>V zD4x$nX&sHU5|ATX(MTPJ(K&0JrkHl0gtan8%yTm3+p=7{VCOpxqiqwWYxK0NC$Ad= zEa9c}B_}G-a3kT3<{enu#TVQbAK_vW>ZfH8eTTN4UErC}Uq=#PYju9<T<;SKpPvZm zn}-jShOnP(M#}H7p<(9MP>~I~E*MGP#XDQ2L_bxK8xYlm?5~?_dk8NSLQ{-Q5Hc&* zZtEeV?*oaA_$VT6*`?Z?pU|9$tfN?ZyLgoyM{K7DCF`ewu%$)@wl271oNpaX90l*< z<}@R;W4P~S0rpSt4IPhpTojwGr)v%9+hWyMX)SW+b<vW+Nb^XX=gxlpoye&cyH6-X zbghulza71~L4}a#RD(lnn@CbM*XLZsoxqaP43=VtQ9RMMSN^P{JL`+fQ(ZmHvR6`f z7HdXqxLf}0!%B~xpv-soQ~+Cst#vCn23@w{_pl9&*C>b8g<ZBUUOSU7Z+-B#$`;Nw z7Nxcj53rE;aY|jo{K#!Ao!ubImPeQa%o@szv%<^VVcfIw!nuCpiqxks9Wp4B4=wVC zIp`mE@jYt)Y*M(D#~uH@l`i>EG@l&a%!WdTNct#HLh;~IjSDCzUWk7RPITQvTj9a5 z@+!do%Ebg@e@$ZT`F4-3Q2`c}=-ETWVg9CLp78DQ;Q5e@9-B;X<b6=iD|aDd=t37n zZ?<<pq<#!mi#V#B3-&|n7Z1s~_Wx0@R{G9n%qXX>3x5%}35~lTTV#5D|8rGV8z)A9 zx<*E1?e$1g69SZh-oVDO0zGwkwJvbx;{N%-*(&kLzK#Fc9OzFwnd(UZ5p2Vdn=b<n zi|gvYUfu#tHLGTFc2jprql-11d`~)V=1SPwjDN-H;rqO4JDOobn0J$apFP9Z3b;J* z*Q$cud-OOi3mFzpXD={5g!dY1TigsDiK&_1Hj5p?V951jJKCj0EISN_#yqZtsBxgS z#&IsikBio!V(ex@vz^h6XtY%ZL?tsXDh_>Q+Ih#p!!f3~dP><ACUfl0P&5L<K5jx3 z_g)cXCX!o8ZhHT0XZ#uz$)F}bNT49(S-{ECfN7tj^y^a?5hX{pQ{96rDoa>(fI;j+ zJ{GHog*GgtTkKP5s=qjBtcQ_HfYYaSXvW%U(2-^*IONrN3|Mp7DIJ(E^<Hv682LT> zVTH^AfSP8(xH+1~!b0|i_1i?&JqquP7u&)t)-v{XM>9W3#Qw#b4QshPk^1CprM!~U zSt{oyaMbOByz@{A1bcPbISy`fU8whUlAO1j{zR%3(yn|{XsU!aTBwLW(+xrrhGDK~ zDz!dlEVy$3{jx+bI*}4Y3(bm019(4v1CI4_&|k6h!~5Ud@H^#0=2<t}hAheJn$uiw z=H`n7GSKDjbX{?7+oY7hx!G#Vz6cm7Spx6nrg1g8=PXCcw5sd$<lNdtFvDZ1(4XLf zuCyUJ@&f&@pL03in{zo=<21&Bjji5`3KJz!?ETG+U)EZBbA1MZ&E&Cwe>NGMVxM!K z&#B>tj+q|R$uTELnNX9`4}%9{A&K{$NIod`af~9G`iXo<PQ?-J0MWw6uWq7m`&(Vv zN^&k8owZIM`p)l3ZiF@8yGP>Lxj??saTdhvSxT;+dE0nYQo+XtI?g~4D%IYSS9|2d zbW~vxAEF<PCttl6WfED#1{?^RHP6doCq4XvvjNE2>_Z#BFKu*GBBhOslw-?I2|flG z%wMOCY#A>E=h?v<I&<}LR+{^|9IFHi?xT<ge?H3ID{Aq!mtBT5^Fu5KlS49PN<V{q ze=xRwpf6R0J1c23RSYEd?57xnxZ7$ONolx-u6vrTH&5KoUr%&`@smigVQLk|4!!EH z>RTNTY5ON`vCr!T0;9dy7Oeg7J^}4ZExC+$u9)7HN5-yp8b-wt+C&vAcUjf4<CuV> zdd=K$bWp4}o`k?$7!qwPU4iWIEzQqjmN;s;jdwU9$I-C;MH;~lj?Lxn#yu$5$qmf= zTw4d%tz}Cm2%AZPm~JY?vsht08qFX8r8bnN_^C?cuBNkjNPS&!+hpZ$qAU4T9;xX* zK({4Zi=M7f?U23<DmaSGs)31T3jm^?!R1zk#da(zYsr_)R+^QtOpNo~A8hPeDl{Vy zfZTW|P;VV&7W~>j3eH!<In|3wZDHdvJe1Jh1<gRZ+OX`e9&TeSu-(;CgcyTbs0c|f zK2+id(s<VhSUl5oE?MoLVwS;IEF3S$$Z$6vv1%p$R@BD+^RfdVTUXrKCc7|>5i+}- zgO?l~*rPqc`iMJvq*gP9F7}o}Sql*D%_^coKHUr7t}a*S67uk%!@wKif5U-+t)9PX z_Q=mV#jI{-RAe4l-v#t&ihzkP7S`l%Pd?aQ*)MFAAnTsdA&Kg7xjh@>vgS2UGjbEG z_(+xT?jTviI}h|DvJxfFLC9&blfzQQ2)JVCF>B()lK<$dU8F)6HAeCm-HtqksXh%c zdKR#$e_o&g;?qYVUIW2&aTm<i&mg3g;wwMxT3ZOyf3|KJp|;~pRO;1u^fe*!Zh!Hl z9gs&fgBGVA@|>SqoZad1-kf*lj9e3+x(jzc6+RRF8)4#h;@vfv*$Syu(m*?{*6Ot% z+@C~mmX@!3=&P%kUu`6D2J#Tf;%xLbdf<r88S}DL6*6@`t3UC+x#`i>=cS*1M$V5F zdd6WU#VenxEs~bnV;K;vZN<PoBzbV2Ufq$njwy6JOWECJ#W)rL#~cuN3xuUUl1k=S ziJNp&c&y8`WX9g~G_%R)##4abyG`u0&#NuQ!s|?1^A6DYd6|U`F@ytZl<uleXe<6; zj)IBRwOzl8)Cch{AMQ&`rvrpwYyx3GzM_Y=;o^K*S>;u6tXsu?`s=CfLtB}tU?YtA ztexyM^HVyCU3t{HO^HBiBW&+*nfuEo2Yc$8opKl-32APx#+LiX_0UC8GZB{siR|Kx z5}&Z2if>b|a#fRwk!-|gQI%|yH4+B=*HwzvIyYM-w@RYdyW6s4{-Xc}#DPaLR+|hl zyOwp%Jqwv%=rRDy;OpUEDXZ|C0+PP;w9CxWRxAsP=Zcct2O8!pe|K78+8g8gL2_Xy zQuA>&d7!{)Foc>TShan+PcBRA(DA|7+DHw5^W|`%hP^}=b$=g=p{9vv0$Q|}Zg3x{ zvx_rOyogo2+S_zEg^ggVvHKP$5zuX#qk{wrT+Wh2%w^kO{Z4P$&o4vS3evDvnQ~Xz zBa(me<NH$+R`uo)qWG2k=#0I4%hn5o@jabljD7<3>HVtm^LP9kw|)VJqq`hLj}N_r z8Fx5Y2O=%)4fR+a(`W+Vb*}RzV57b1t{27aU;D-*)&!R18z*+ySpHP9+US*lJ3D(c zg*VgBtE$@1o3#uR3JEi(OH6P{`dqAgvO69sRpx$-u80VNO3I(rFC7&*_GVXgH*t}p z;7QI5YhdKtZV(7Y`AK277hZ3**!{LMgT1-{y#gsajO?r(Pe-?dcjFjf?LD8bBbA+d z$9q<TLw&&~PrT3K6{BCI6589$1SH1`vpzvzDVOfy8m2X6H7)sO72@9<on@z*bo$|0 z*l2GB2El8n1)XERx|@i|6lLb!Q>YkCd+t27tRJldtj7=+i8;1RJvFNO)pbT!0)MTf zm#JBct|2r?a~gW7l}XLKTHizPUGD-FA5y<ox;Y(hk|t`QIYfiiib61;g~)~b9GZIN zmOx7fnvgYleW>A)B;~@N?RydeH58>F3CsQ2Y~UZx!j>E%!R?>|<~{jlgVH~{rN!DR zXlm7~Drqul)mL@p3dbgk6NN+r9My?O*vL8R+VGcuZ|AwRD&2P)cnftr*htoE=QW*` zm$5#Vw74d?E)o6c{O?(Q`;+<myw3QQBP&GNO;N1x1Fnm}C-A`SWXq*|e4npFgL)1x z#y}MGj9-Z@`{nJ<Lzbl!pY{y)=@NpEHg%x!`Ji85?TiRqlq7*s`mGV^j6w_9lHs!S zA>gw;4;V+b@{yxUI{^>F@`9a$i>RVUcY<$>WAsnWxZ8+*aWa)9XOQX+G74xYCfe7| z318@f4{_r-U$;4CYk*B*BWK&$c^KgLqDC;L8+tmOTvY}5?16SrTSJs`nnrAqvmQ^s zUOV)3-npoFB)&@}-sw{{&OPZUlDaww-e6yx^YV6eW_4I(MMQzgAODhprLoJp0efl2 zgG_&5gp^=>jCatW!fagdrht$Niw8kH=V8WtcrYpzY(~2`cEc@J^a2;*BAf~<wFHik z)}SXmsY%pJtw-aVt{(Z@(|yuTpExGtM{{ThtVIb%i8CS51y(yr?2TCgPpoCTuJT99 z+2i(S?tnZ=n6!q*aBMokYf0Ij=vleOh%|_d<S0k?IjC~H7=hF@O150Zpn+@3Ifd7K zp+KK1)$9S!SLeZVqovyr!_@{|%kX{*CZt1X=UY~<G*bu}e2U`S(V}z`jgA9E>iL3+ z<f-!$s>GKW79gjUnMTW`%&GAHARggYSI!!&Iw#eJhP)<F10xbg9hEF4Iy}lRHgxW` zSh;dK(OcZR*HVQ(v5)k8R!^HJv2G&t4k|5L54;d$q_P~M#w#djJOl*o7^~ErVX_~^ zU$hAJF<~)<pe-p%xEC_BIwAYbXj#d@zbeSpTg)T^!p7t$j8#5hroZ`_gN);0zn>UU z5#{zfBHAX>iTE5!s#7ws5$+(Sl9`(XN#l|^C^F{^Q8i=n%oijM&4f%ZamPYXwLy8R zxGXpXW%i&pb>sfJY2Uv-l)To@^%-TWniStk`Ai3Vrm8IB6J3~;U(rnD*~yfvSjg^S zt~Wlt-1uk7Yi-@=|3yACrm#dCd;g}0#CJ=VkDGLuq?13j_MUpslC%V|0ot!W&P9{a zH+z16>QooTMeUNz#z^KpA~)k{H0xBX4$+3ee^(^v5;D$4OT+6?6e}VIGR>v%X{Yr{ zh1%mEs}EX49;`~Q(nM1N+--|D2VPG8B1$&GH2q^{1`QZ_-8}dt6T3Z|BAOj+M&5N@ zuj0o#*$r#EwWY~5lXKsVJDD40ax&jPEJqBs!MntT4!NgdXNQ{@g-CT$LD(#sIh}ey zi{#Xh**yEvoricOrZ@<C*#$vG5cBY5X3L)M!~h4?lMNXINA=21WewLZUOU(BYh{eJ z#gvv*i071RRc);HZXNn>o)VIKhI3G+EVnDYPfxusy$|o^w7(zsP6f_EG#Lp+T~$3t zq?I6+yzhW5#C#ZLob`{|E$#47sAxwBi@4zIPKhosP=2bn-+#M3L)6CNi2e}OBNG~n zm(cJms=ANlO;?am67crH#z(Xq#2lk2U@z8kE68{o!24``-M26##z=Jpp5*Ur+WAL; zUUmfR#%DmK%!rWhqUu6-U6;8<k&)&^4*p!#1e!3js2xP4VR_Y0QM7$>7HkiX$VTWq z2K!agW|no7O-lA{WNs<xsLSMqAfX@`hn#%E@fJ6_v^*Y{Op6Ic4PnGALK#h?5C6bK z|HHb;Tsz3Z>B`Q@!O6uoaE^(so%sVK!#?5SGhC;$Wviyf!N*ETb`Q0S%rcLaUhS}6 zIm_vPeC<(k!K9M+4QNpU!roK7xl{29cd2CAa_#0#Y$4H<B;sata#Xwr1!&z8TCdxV z)pqu6Es1k~4}LW*D3(f0i#`wh+^Ag6MhHUtK(cYsMYVh%wantb2v5~Sv=j&>_;!t- z*{(SkFcR4B7arMBmbB80`_(lAD3Cnz?R(o##sB%F05o3SLcMDL5Va<NAy#70Tq8p3 z5<(GGU{<<#ODNDXy8upVCazxH-f<^v>Uedt5z((?YlNs^q*(8%3+c@DFpTgxVVx1v zEhxI_XdQyHn)5=ya}Ug5)^6nv&H^zGK0&__GfncT5PVANk+X%%+;1QOAwR;IGTacL zq<tG`29vD`75G9;IB>DUgKv^a#+*P#9>(3@arJlMh^4fZ6&K?1yKV`NB*2aJkArgz z=jj*<#z^@vx-HZD!0Yo9X9cSTTU>3YuJ#vBH|QtbQ%xi1&|RQuM>n(zCo?284j_m8 zrr|8vxUCP7hzswoQ8VlC_{+ARQ&Xlqg!mFvXLs9U>NWZevb`AAB81GPQX3qwuyV>_ z1QI<}Nwc)wI~WL5=^P>S_hm66PL-X~U~3k+9|7Rmx^u8g{1Z;}hssNGJE36M%_NKl zim}5BVK1MFmUq}0+*?4mI0$XPAX*r0qPzUyB8bS<gU1)A0X1LBvKNdboMPzL{(%UW zWe+qNFAt=oU@-D)KuX`jqAvs{b|$VbdGwo%fenub_xlCXAh^d?Hug^Nk~<R1WBI@4 z#4y9y8*x5QS|&H%c+I095%9ysp`F@=@l0n|AqNbw@wPgH%@Fde^7Nt2j%wV#haS|C zy0!<exmL1rqG4?X7{(iuVc8@j#cB?zR3eAV`T2Tq<j_MB1SI&0aZ-TOw)A!Q9=A8E z_9pbn6bAzqiP^tgAo76;_~SRlE@0uiNt4@Z4_H+su|_Kmv1T;sr9RJfO{{deX|2-w zId+x)?X_tymE|c(6C13gW*|IUXlw4@&cjlQ=(=#I1z5%;{&j4a`RuZlnaP)q10a-E zXDicxlmebRG<3DW^i}?v_pg@+q%Weu?6B`7KnCaw7GD6PtLo^QQc=I3YgHLG)-@Qd zrem6AV2@qR+Z6p~#^WL<D0%}>i%Nz0i^R(uHS*CQA3m=Ld^?J}&fM&36DkBC1j8p6 z`C;Tq_#TEwN~fmTr@pyfPG?JfqsWO&lij=XGu>=r&U9_fJgjZFIT$+5IeeJAcyd*@ zcSdi)OlA*tEDprE1Rg-DC8W22g7+HQ?uW~LUe72*XJf7jxZiT|QqYO+eF6V-L5bwa zIivG0kJk?h008g5&HWfVIM_Nk{tuNC$twL0glzELA5@&L2Quq*cNE82^iU|2n*0E@ zmazsUrg9c>CM3~!m#hDpP3-zu(_RlSm%iSeUoKK;eScl5PbwRa6P#7m?QKPwy0+x? z<*aWw$^@Nn+CJ@VL*soZ&5LiGb65X%ZOq$EGEgQoGdMor$o$_?=(BDHTvY1rrE}7O zF5lrM`AQ!^j63uustS_=7lO$w#WR)o&YBcJ%FzJ$y_O-cW+{|OjaU1UDEoV#k1~4d zA28utw`ZM5K(n9A9?szKms$#<E5_IOlhH^w*pOd2_8IY+6}FRtRY>(W&g)|IHQ>h& zJ)!hf_bkQxdxlh7Y{e*r2VONp<#wJtHm_PL30G^J-D}M*y>5OTMp@J$Y<pNdxYGSQ ziHCT}E@Mv0iIf6l@>FruODS@c$y_7qb%L%JPW+ZN<D1l0$%c7F8(^iX%Isy!G1v+_ zZ2Zkh`LezOha#cZACY$lnCP*F%%}Piv0rapeCjcwP#H7k^x0k*bfb$9J)%pwUZuSw ziFB(&Wyk}!j%Pu9;uhtEr3wuqn-4gE=B-{I2}s#zf85i%6gU)Za@R15gh)E0pBJRy zJSgNKyXOUhUnet*I?%FZP2U!#hcM4#Vo5Ktfk@#=DR+XmhebZ~^0BpG4F3UsoLvWi zkG>Qv@|2DgKeel#^<_x^TRe3{VQ|TDEEdVxWXU9L7;g>-c@f~Ca8XJskX9BcD_4@W zX0PJ{XQ&hL-><JZZpG7wry0-yftCifi`CM62zemb4u|<7;R{9L)~0z1NyDy0-U6B` zB&R1iA@y+&Ho2$Ti+9FC;rB6`k2wU4N)BNwQHYS9Dg;eJ#y>5hL>}X-hX5lmp{(lv z0e16ar_Gkh<I*#+568zk&C6rYBe6S`;Gki0Lhrw;y2I|j>iUh(m@5yJsF2E%1RPUN z=w^~Cf*&ele`ra8%ci;zD6r#R@XeK_qhsxWx%sMU-PtLIyq9qreYyoE7<iqAt|3JV zb)M=lMTHeskblGJfqoFVL7jR8d8Im!m!4ymq7-Y@hsEV8$y1~wB~QMFn8*M`DwO6L z9r?SWkEzh^ixfodGl7kWq1{K%dMY+9VTnGmksPtr&FFVUXgto41(8FBwd-ZFZVDAU zm`{jb-4<1|<3_BMQneGp9q<nA&|Nhd5iN5mx4X^995h7sFzTAwE%ng6L|};{4hNI< z=0N|mgo|=4!T>g+qrEAZNqYE{M=eGlZpMdJeCA}EUPk@9K@5AhKc~CC%Je8(D1Sy- zfTa~5T{UzQW`vi`^2US3?d+zaw6+xrcmS4S1OaY-p>j#Eb;@u280U7TsiXurd6R`j z6q!<1n>NxfXD&Yq@=kkeQ}5%~(d%10=V=?kglZ4XLUn7x-b-7YmRmF7saKoc3Xr$m zQ-}G;W|;@*#JZ-O3<E>kj8wse-nZ>kbz~jERWQdVLf)g^g>XZk!0I@2{H5eO*mB_N zGE-me_X6EgqSZL9VMhZyzF~R)_Fw4Yk(rn@&%eZ9xL<zP|C{gnAE`)ZCvz*u{|&0E zI%(TxgC2SG<^kFCF@RA(Pbp(XwtLKMzQ9>7zOLeT!bnKmqgYG7LUv2<?l0;V(zP0e z&9mvQ;0!{v%6;JadC-^Ld9<v4m!f@No~bIjmCbu;0V|(sBd5({b-xMR&i{(9a*05r z0v_z|WC~LSZzWkov`PMqDfiDK4Ork^-$@x`4iNn#<kHXm&7Cb0_kc){3dzVEm5Nye zK+E{u*O-yq7Xu);b&DK40&NkWZ$XbDb=J&xHo&Hofn;zBx3`4Z^ek3ABSv^>1Y_9( zOaQaALAl=%@3)CeC6rGw@2u=Vst^RjMulbqSMR=cgWw?H0YcwUYnIH{3~Z96_g)oM zPW%2fZF0y|x|~{Zni`#by&F_`A>p)5)IDQJE4hS;{9V>wAf3vkhcYg)o*<lJ1*}A= zLSDo~RJ|2_5Q4b~LgUz|4;h|dZ;h<Ga#E{-GbBIp7p6=@nx85RKT6}4B5a^cDylOM zlR0*uHf=gU0$WFnKD*zHvmNr#jx&E5Jz$*23xZ5lo4*1%xZk?C$_H`W8ik>eB=VVu z54`TG#Zp^;!ZAldr=U#TUv3OcDPX}Layi3tQ%dHZu^`E78ivW{pSnWNszvjGA)6wF zm1`4IIS8Ac9F}*K78aKUi7sV{*29XpV~Tk$IW9%&1UuWsv3q-hW!a_P#}QDd<K!(j zT`@3>V`s#;m$0Ib$L$jwS2h$}pM+Pz=(u?i^PPmN`a*X;b~fVuIK68NL4WIsl3_ZR ziclkb=3mE-Qg&6#`mK0VK_&+XYqqn;uZ}>jBg#O0%;0<)8g0;w0u(0kW-u<ag4<14 zy~4L797?3hjD-e`ukIw^?!~+BXqDE?UjNfOG(~zEy%AjB#oEzaX6&bnwX0#5Tbnx{ zHzXOf&AV3&t0~DZAPwoZ{t4bTZ?g|It5Dz%pW-?G(ySjkSm6+gRjd6EpThgktfZXo zCLr8d0*ODGh6#NncZo(at|Ss0E;TWUmM^kgJO8EU@|bUQB<PoFoQetnfcxJL2><s; z{SWg&w3@Z;COhKywQj!*@7gto<RS>F$h89)o6JiTR2HGjrlyl8D9t=8mikhH5_apD zZ?9=c5;d9i2EI78{i~f$F2?(9>fyxtVLNTiWVLR3fg^cL4o>)zQHNSi!~|W8&O+u4 zfy#_bd$ULT{lV}<qVBL$@zACgC((|^gL<(JG?kuOD*v-}O_AaN1qt_2$W6&lPe)GJ zFLPB5or>|ecqW@Zcuu6m)>&m-YYY?sK9y5BSTVgULV6uQaRT}0V1mC(b;X>&OFSnM z7Xo{zIPiPdzm6qYZL*p?3N4vPrU{C&tWzl&S8Q;}Io(pdNYbiF_l^M}=<=0FFpcAp zCMd`&yi`O}R(uxn(Dc~b-NUPE-3oEMgLH&8_5uH244v3Nw8Qb=W#t&>CgQhkrRu$M z0X1c&RWL=4EZLDfeKOjw@@Rpw%3bK(rs^4dnxDP#7Rww(j`6B%`C;PbtwIHsv=wBJ zMm;QWUpR^s>|oiBF$3D~PCp2sJ2L`q@Zw_Mjk6;ytkTlnhsB2{Bc^7=&4Vc9kZ;IW z^l%k8e~$vY@LE#pT}VPkM+u|d3eix9>OTm^sufCfPLQ1u#w4kd)YH`sAX5m%4VG8r zDX{0p{Nqm=o@Z~<M1=`6{r&w-;U*T45wL%{XPS2-M3jrVSp2n0fsdn}2-zk{U@x3d zSE@uL$}^8VXb&11P}^U!hqbmTHqV@D=t$94Z|HPpci?Kp231pU7t{Mv=!#NqoXH`0 zUE-};;w+bO!fI%xNAOfe-rkS5j5ObMC86Zsjh63KPEcw+x>aT-o!4>b^)9BV6=_K@ zOer%f(uKNTR~&63jA8DEk)g$SA;H9$kAR`KW-P2F&s33d-mOrf&<DbH)L5u0p194> zbL6YfZAw#*78%cur);G}H4B^0KsKX1(yEh$Ke2g!lqn`dYUBS=X2x682vkyLJM%s5 zpGeaTjma!$-%Pjaa9~GPQ326qN9(KoAZNmFM{>2Vh>WqL7E=m9A`HqY=u0>@7c^BF zG)Tm&v_?dzLB<j#z~=2Z5NDB>t;ofk!4!%^c01upx2%vW$$=!3T4|+6;qOf*Eu=NL zH7Y2f@e55Lor(JccxJ1#H&tcx6HvC76hNCM5?hE@`=4j9t!0AG+$f>?Wl|l~MoAmS zGg3myt}!Wwc&z0=7%G7}gOewPleHKYh7S}{EVNt5Pzv)6=>3T_Loln~2~B*MH^bkN z*~RXL{*m<|dISLER!x=$Ce)UoQv8dJ5SS81HLzw@Knzzi?FfG%1CEWSqcF_+4@pA` zn9+|K6MPQ6YEv=Z8e^lF>sYvg=_KiDldWV(-e3A_#-&#<jpDX0X))ZLOutU!N6~&5 zvOsFN79GTx1AjF!U`*pr06XjMHAf^z^jyI#Ia2@FP^z1EwU=>Z*(a4@oL#UUquGLU zs+KczF9unAsz5&f?Tegrt&VPX#C%87n6iO^+4xy@2?`Jj7bbjw7~&4|o%oKnsuhTN z;aZUNc$P9zd`C*0feop41xuQ}1=X^C-9xgV#F-nk&pMxg?z!w3{Fr=g-d4V}?ZB8% zIPLe7@F(!IF;Bwz;lp@js({|j78|20Wlv7wflUA+UXq#wJHR*0=#4C>PdDG5dW_@) zBS4haobCz0G(W|u4CZ>?TAVKkHsL@>%lpA)73v1C&j3pnliIxtR?C#00!*4wM4g8= z?o7v*SXx1j%}%hQZsRA2bE}^y%ba;JNDPwrgrC&EPD+{iRW#+9C_`wdTH|^YN}*j@ zh7Ar$@gsg|kO&y91fsi5_5NQYT){|PPM8#lZM(;qh9U!l{*MX2_tsey#3Q_!*r|qN z2w_Y-+apABE%%+|w5LotuBa^z?|x0<Zj8e)Etl76;~-hKOWOnH!IB+NXJhz6D_l+R zhrG$p8XhPW?p8t<^GONii|~O_;w|`MB;8r*xh(sjYZeMna)xn!Z`q}im$%RPbti;D zU5ZbsRjzb&MTh~U%$w`6F2TE@G%OD$);TLShx8N)i|nT(w}YKkIaC0?M|t^ul*c;Q zT*mtC)~glwI~_SRjEn*26&Z_u_dKc$+wlbceST6T#xI%yn6_k>-@u8Kk6qfja_z(& zEgLJc({sys|2o&##XLPF;KOdQuP;!=vSI)XdRVbcO@(>eMuk(`f3ckzoMp@Zme47z zY=ZidZ?H?Q&Jqz?edEQ>j&G=E<)Je{j)pylWoPv3U*MI@1k7pbCk#JmBe$cR`t3S_ zGkkyUrU0kYMwVcJKh21VVrpk4wX4mf?7oi}Ea6F^3L@}?YWv8Cr|b_#v#aD!)`TbI z>N@5HWs(5ht#@|QzmmSA6CtHR#HYu$V9gjp!Xy_gq(+mMFe0Bf)>gxIRP%>fsUA3L zsHu6YJ=u&}l5FJmfArz+%gqCRIh31+o?;FXzOKb1mic8i#mC`G!K71)fk;y0_2YkZ zO_G|>C#w34VZL&q32XK;5!NQR_RfJ?eX81n{$?#)=&%t)_xfCUf9`&j_CDR*oH6A5 z@a9O@c6{}}xUObudI~Z7G$CR2`kXJ4;m-6npCP65$LILlH|I`!$R0-mrgJhq%mNFb z#WPuf??W(z_EsW~ualBt;ou%l6;f~Q#(-RJ^<9%q%Ln01M4~2O0Qbk|ZrlVACbjG< zR?{ma_Nnh?yR0hX)4G5<##xJW%EKlFwdZJvcRt*P8#Zn14R!3Fim0{s-L_mkUEoyr zmyHiqY|8LrX+OGvK3N89GK9&B4vMP+-(9X-nfAf2a%NCz9xuV$A8vqh$5ZbZ78#k# zC_l@-PshVCUWFT8WUOUN5%JKDF4>nIPn%cMrjRk0$6ak9-nh!#`Mba^f8_slnOKHg zLV7@ak=s)7{&XpsuY`S1S3CIqTxMe(Io4bW6TAn3Et|-umgsA3g8a6PO-<sZ@m5A$ z@Qs7@8)ieMg-@x=S`p49?8ELWTvh*eM&<1Y;%E7YyWmE22g&s5)iNPHITL4EqUTm} zepu52Z)kj9Lpa#KKC*!d23yuuQhQH*0WCs@Mt<U>b>PHLk)f(F=sfp$^ECFxXb{v< zx28vTSzF%}m?B0@3+4<e{rfw*Am!Z4v=S+DOZ-PK(RSQvBQ?-giusVRvz3Kr<xtk7 z&A4_xiX0eajli6P(k<+R))C6VE;Og9<3=8vLBsHtDB6I|>Tf;VvV{vyw$nG<e-?jM za<A+Be{sjTzw6R}FaFs5Z-%>Hs<G|iFV*-$RXSTJE10H~1BLcE?*y0ybIt#XS&bSM zN&{1Znp}aT+$?(d`^CHHi1|Yvhgvbq{o$mWVAe<7X@(Y7fo`L)Hd=PZ?AK<~PEk~o zvh?Qgt@)aerT=fENsOv;V{!SoR<)@%9XH7?1h?uf)bX!681#k05HC4u%s4}$nAUUK zk7MJuJOzrPIW@{<uu@DEX*tvzb)+mgA{~HVTPla;D2Q{19#>xz;i%<S`uwg{_MahT z4~a#1dW=?5v|iwSGfs=4_@=w1_CDbbe<0t9_9yaal}r_he8&=<cg_?H0YSVQQE*29 zT^XP|8kOS<c0^vp5k=2kTC~WX8%2cnk`WasmZA3Huk&}8?#}E$+bOrDh-{+CAD4xK z#ld)CD9(0vzam5)0ZMuGksLOE8{6Vg7AA+1$L>1lBtcHw<NSwRGiSok6kRZWntbzw zE3@XzIlOh7=1jmtbC5vXm&YXQ)^(5Gj_r99snyJ<X^Y;Cs``#XtFM@EjcQwyAvs9n zS4B%;-?$_J5!wKw&iMx<e?3>V&J;)W32g7P3M3Ro<D7MiCLx+l(T=@h;BFvtN!A4@ zyxz|_r3`7(#IMNA_W6FCs<zS)2BF|c<3qQSFd0O6=9NT7<=_Ab1&u{Hte0fξ(H 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�o`l#6R$@R`<^r@_BtP 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%0AG}pGPV$;A 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 z%0D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