From 05dfa8ac2fe719fe28d26284671df021c2950005 Mon Sep 17 00:00:00 2001 From: dbrennand <52419383+dbrennand@users.noreply.github.com> Date: Sat, 21 Nov 2020 23:42:07 +0000 Subject: [PATCH] v0.1.0 - Library redesign (#24) * Add v3 progress checklist. * Add EOF. Fix a spelling mistake. Add 0.1.0 changelog (Will probably add to this more later). * Bump description and version. * Add new parameter to file_scan. * Add username to LICENSE. * Fix license in file. Remove ImportError print. Add API_VERSION parameter and docstring to __init__ Adjust headers for each API version. Raise exception when API_VERSION is invalid. file_scan: Update docstring, added new parameter upload_url, added support for v3 API. Add new method file_upload_url. * Move comment up to beginning of if state for clarity. * Remove proxies from individual make_request calls. Add inside make_request. Add file_id method. add file_id_analyse method. * Remove checklist. * Add progress on re-write for v3 API support with backwards compatibility. See issue #23 * Move check on API_KEY before self.API_KEY. * Add link to v2 docs for error(). * Rename backwards_compatibility to COMPATIBILITY_ENABLED. Move to init method of Virustotal class. * Fix for issue where API_KEY is needed in query parameters. Might find something better soon. Fix mistake with BASEURL being BASE_URL. * Make requests response object @property and fix naming causing AttributeError. * Add more @property. * Remove truthy check. * Add VirustotalError exception class. Alter wording to retrieve. Add data and object_type properties. Usage with v3 API endpoints. Simplify response_code property. Replace json parameter with data in request(). Make method the last parameter in request(). Add docstring raises for validate_response(). * Add testing file to ignore. * Replace examples.py as oldexamples.py * Add some new examples. * Fix docstring :returns:. Fix return type hint. * Update packages in lock file. * Update requirements.txt with only nessasary packages. * Push progress on new README. * Add spacing to NOTES. * Add link to file in NOTE. * Push example with timeout and proxies. * Add examples to README. Add API key image. * Add word. * Add comment about MAX with v2 API. * Add useful comments to examples. * Add examples for providing an API key via an environment variable. * Add example about retrieving info about a domain. * Edit changelog. Remove WIP. * Add correct documentation for examples. * Add domain_info example. * Add start of comment.py example. * Add example for use with environment variable. * Alter file ID. A more interesting file. * Add json to request(). Fix docstring for data. Add docstring for json. * FIX: json.decoder.JSONDecodeError when user attempts to run json() and no JSON is present. * Alter to empty dict instead of None in case the user runs .data instead of .json(). * Add comments examples for v3 and v3. * Fix raises docstring. * Add graphs examples. * Add meta, cursor and links. * Add ip examples. * Add missing example for v2. * Add search and metadata endpoint examples. * Add example of using a cursor. * Progress on new tests. * Add progress on tests. * Add final tests. Remove skips. All tests pass. * Add section on how to run tests. * Add NOTE about how links are not retrieve from objects. * Alter word. * Add comment and catch for AttributeError which can occur. * Remove "as err" * Add missing class name. * Add correct variable for example. * Alter wording slightly. * Alter wording in docstring. * Alter wording slightly in sentence. * Fixes to some wording. Added some comments to examples. * Capitalise URL_ID. * Fix example variable. * Alter comments. * Alter wording. * Move comment down. * Edit example description. Remove repeated code. * Alter comment and print. * Alter example description. * Alter description. * Alter description again. * Alter description back. * Alter keywords and description. * Alter file description. * Remove fullstops. * Fix class docstring. * Fix docstrings. * Add example to comment. * Move v2 and v3 examples around. * Move v2 example above v3 example. * Add link to PR for 0.1.0. * Alter wording slightly in CHANGELOG for 0.1.0 --- .gitignore | 1 + LICENSE | 2 +- Pipfile.lock | 334 ++++++++---- README.md | 275 ++++++---- examples/comments.py | 118 +++++ examples/cursor.py | 35 ++ examples/domain_info.py | 32 ++ examples/file_info.py | 33 ++ examples/graphs.py | 29 + examples/ip.py | 48 ++ examples/scan_file.py | 37 ++ examples/scan_urls.py | 52 ++ examples/searchmetadata.py | 39 ++ images/APIKey.png | Bin 0 -> 6786 bytes requirements.txt | Bin 1058 -> 202 bytes setup.py | 6 +- .../{examples.py => oldexamples.py} | 76 +-- virustotal_python/tests.py | 313 +++++++++-- virustotal_python/virustotal.py | 497 ++++++++++++------ 19 files changed, 1512 insertions(+), 415 deletions(-) create mode 100644 examples/comments.py create mode 100644 examples/cursor.py create mode 100644 examples/domain_info.py create mode 100644 examples/file_info.py create mode 100644 examples/graphs.py create mode 100644 examples/ip.py create mode 100644 examples/scan_file.py create mode 100644 examples/scan_urls.py create mode 100644 examples/searchmetadata.py create mode 100644 images/APIKey.png rename virustotal_python/{examples.py => oldexamples.py} (66%) diff --git a/.gitignore b/.gitignore index 894a44c..9f5febc 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ wheels/ .installed.cfg *.egg MANIFEST +run.py # PyInstaller # Usually these files are written by a python script from a template diff --git a/LICENSE b/LICENSE index 81f990d..77f0a0e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 +Copyright (c) 2020 dbrennand Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Pipfile.lock b/Pipfile.lock index 2e2aba9..e133728 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -18,10 +18,10 @@ "default": { "certifi": { "hashes": [ - "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", - "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" ], - "version": "==2019.11.28" + "version": "==2020.6.20" }, "chardet": { "hashes": [ @@ -32,10 +32,11 @@ }, "idna": { "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "version": "==2.8" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" }, "pysocks": { "hashes": [ @@ -50,64 +51,66 @@ "socks" ], "hashes": [ - "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", - "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", + "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" ], "index": "pypi", - "version": "==2.22.0" + "version": "==2.24.0" }, "urllib3": { "hashes": [ - "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", - "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" + "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", + "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" ], - "version": "==1.25.8" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.25.11" } }, "develop": { "appdirs": { "hashes": [ - "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", - "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" ], - "version": "==1.4.3" + "version": "==1.4.4" }, "atomicwrites": { "hashes": [ - "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", - "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" + "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197", + "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a" ], - "version": "==1.3.0" + "markers": "sys_platform == 'win32'", + "version": "==1.4.0" }, "attrs": { "hashes": [ - "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", - "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" + "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "version": "==19.3.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.3.0" }, "black": { "hashes": [ - "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf", - "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c" + "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea" ], "index": "pypi", - "version": "==19.3b0" + "version": "==20.8b1" }, "bleach": { "hashes": [ - "sha256:cc8da25076a1fe56c3ac63671e2194458e0c4d9c7becfd52ca251650d517903c", - "sha256:e78e426105ac07026ba098f04de8abe9b6e3e98b5befbf89b51a5ef0a4292b03" + "sha256:52b5919b81842b1854196eaae5ca29679a2f2e378905c346d3ca8227c2c66080", + "sha256:9f8ccbeb6183c6e6cddea37592dfb0167485c1e3b13b3363bc325aa8bda3adbd" ], - "index": "pypi", - "version": "==3.1.4" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==3.2.1" }, "certifi": { "hashes": [ - "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", - "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" ], - "version": "==2019.11.28" + "version": "==2020.6.20" }, "chardet": { "hashes": [ @@ -118,108 +121,201 @@ }, "click": { "hashes": [ - "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc", - "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a" + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==7.1.2" + }, + "colorama": { + "hashes": [ + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" ], - "version": "==7.1.1" + "markers": "sys_platform == 'win32'", + "version": "==0.4.4" }, "docutils": { "hashes": [ "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.16" }, "idna": { "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "version": "==2.8" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" }, "importlib-metadata": { "hashes": [ - "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", - "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" + "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da", + "sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3" ], "markers": "python_version < '3.8'", - "version": "==1.6.0" + "version": "==2.0.0" + }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, + "keyring": { + "hashes": [ + "sha256:4e34ea2fdec90c1c43d6610b5a5fafa1b9097db1802948e90caf5763974b8f8d", + "sha256:9aeadd006a852b78f4b4ef7c7556c2774d2432bbef8ee538a3e9089ac8b11466" + ], + "markers": "python_version >= '3.6'", + "version": "==21.4.0" }, - "more-itertools": { + "mypy-extensions": { "hashes": [ - "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", - "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507" + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" ], - "markers": "python_version > '2.7'", - "version": "==8.2.0" + "version": "==0.4.3" }, "packaging": { "hashes": [ - "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", - "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752" + "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", + "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" ], - "version": "==20.3" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.4" + }, + "pathspec": { + "hashes": [ + "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0", + "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061" + ], + "version": "==0.8.0" }, "pkginfo": { "hashes": [ - "sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb", - "sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32" + "sha256:a6a4ac943b496745cec21f14f021bbd869d5e9b4f6ec06918cffea5a2f4b9193", + "sha256:ce14d7296c673dc4c61c759a0b6c14bae34e34eb819c0017bb6ca5b7292c56e9" ], - "version": "==1.5.0.1" + "version": "==1.6.1" }, "pluggy": { "hashes": [ "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.13.1" }, "py": { "hashes": [ - "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", - "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" + "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", + "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" ], - "version": "==1.8.1" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.9.0" }, "pygments": { "hashes": [ - "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", - "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" + "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0", + "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773" ], - "version": "==2.6.1" + "markers": "python_version >= '3.5'", + "version": "==2.7.2" }, "pyparsing": { "hashes": [ - "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", - "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], - "version": "==2.4.6" + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.4.7" }, "pytest": { "hashes": [ - "sha256:4a784f1d4f2ef198fe9b7aef793e9fa1a3b2f84e822d9b3a64a181293a572d45", - "sha256:926855726d8ae8371803f7b2e6ec0a69953d9c6311fa7c3b6c1b929ff92d27da" + "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe", + "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e" ], "index": "pypi", - "version": "==4.6.3" + "version": "==6.1.2" }, - "readme-renderer": { + "pywin32-ctypes": { "hashes": [ - "sha256:1b6d8dd1673a0b293766b4106af766b6eff3654605f9c4f239e65de6076bc222", - "sha256:e67d64242f0174a63c3b727801a2fff4c1f38ebe5d71d95ff7ece081945a6cd4" + "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", + "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98" ], - "version": "==25.0" + "markers": "sys_platform == 'win32'", + "version": "==0.2.0" + }, + "readme-renderer": { + "hashes": [ + "sha256:267854ac3b1530633c2394ead828afcd060fc273217c42ac36b6be9c42cd9a9d", + "sha256:6b7e5aa59210a40de72eb79931491eaf46fefca2952b9181268bd7c7c65c260a" + ], + "version": "==28.0" + }, + "regex": { + "hashes": [ + "sha256:03855ee22980c3e4863dc84c42d6d2901133362db5daf4c36b710dd895d78f0a", + "sha256:06b52815d4ad38d6524666e0d50fe9173533c9cc145a5779b89733284e6f688f", + "sha256:11116d424734fe356d8777f89d625f0df783251ada95d6261b4c36ad27a394bb", + "sha256:119e0355dbdd4cf593b17f2fc5dbd4aec2b8899d0057e4957ba92f941f704bf5", + "sha256:127a9e0c0d91af572fbb9e56d00a504dbd4c65e574ddda3d45b55722462210de", + "sha256:1ec66700a10e3c75f1f92cbde36cca0d3aaee4c73dfa26699495a3a30b09093c", + "sha256:227a8d2e5282c2b8346e7f68aa759e0331a0b4a890b55a5cfbb28bd0261b84c0", + "sha256:2564def9ce0710d510b1fc7e5178ce2d20f75571f788b5197b3c8134c366f50c", + "sha256:297116e79074ec2a2f885d22db00ce6e88b15f75162c5e8b38f66ea734e73c64", + "sha256:2dc522e25e57e88b4980d2bdd334825dbf6fa55f28a922fc3bfa60cc09e5ef53", + "sha256:3a5f08039eee9ea195a89e180c5762bfb55258bfb9abb61a20d3abee3b37fd12", + "sha256:3dfca201fa6b326239e1bccb00b915e058707028809b8ecc0cf6819ad233a740", + "sha256:49461446b783945597c4076aea3f49aee4b4ce922bd241e4fcf62a3e7c61794c", + "sha256:4afa350f162551cf402bfa3cd8302165c8e03e689c897d185f16a167328cc6dd", + "sha256:4b5a9bcb56cc146c3932c648603b24514447eafa6ce9295234767bf92f69b504", + "sha256:52e83a5f28acd621ba8e71c2b816f6541af7144b69cc5859d17da76c436a5427", + "sha256:625116aca6c4b57c56ea3d70369cacc4d62fead4930f8329d242e4fe7a58ce4b", + "sha256:654c1635f2313d0843028487db2191530bca45af61ca85d0b16555c399625b0e", + "sha256:8092a5a06ad9a7a247f2a76ace121183dc4e1a84c259cf9c2ce3bbb69fac3582", + "sha256:832339223b9ce56b7b15168e691ae654d345ac1635eeb367ade9ecfe0e66bee0", + "sha256:8ca9dca965bd86ea3631b975d63b0693566d3cc347e55786d5514988b6f5b84c", + "sha256:96f99219dddb33e235a37283306834700b63170d7bb2a1ee17e41c6d589c8eb9", + "sha256:9b6305295b6591e45f069d3553c54d50cc47629eb5c218aac99e0f7fafbf90a1", + "sha256:a62162be05edf64f819925ea88d09d18b09bebf20971b363ce0c24e8b4aa14c0", + "sha256:aacc8623ffe7999a97935eeabbd24b1ae701d08ea8f874a6ff050e93c3e658cf", + "sha256:b45bab9f224de276b7bc916f6306b86283f6aa8afe7ed4133423efb42015a898", + "sha256:b88fa3b8a3469f22b4f13d045d9bd3eda797aa4e406fde0a2644bc92bbdd4bdd", + "sha256:b8a686a6c98872007aa41fdbb2e86dc03b287d951ff4a7f1da77fb7f14113e4d", + "sha256:bd904c0dec29bbd0769887a816657491721d5f545c29e30fd9d7a1a275dc80ab", + "sha256:bf4f896c42c63d1f22039ad57de2644c72587756c0cfb3cc3b7530cfe228277f", + "sha256:c13d311a4c4a8d671f5860317eb5f09591fbe8259676b86a85769423b544451e", + "sha256:c2c6c56ee97485a127555c9595c069201b5161de9d05495fbe2132b5ac104786", + "sha256:c32c91a0f1ac779cbd73e62430de3d3502bbc45ffe5bb6c376015acfa848144b", + "sha256:c3466a84fce42c2016113101018a9981804097bacbab029c2d5b4fcb224b89de", + "sha256:c454ad88e56e80e44f824ef8366bb7e4c3def12999151fd5c0ea76a18fe9aa3e", + "sha256:c8a2b7ccff330ae4c460aff36626f911f918555660cc28163417cb84ffb25789", + "sha256:cb905f3d2e290a8b8f1579d3984f2cfa7c3a29cc7cba608540ceeed18513f520", + "sha256:cfcf28ed4ce9ced47b9b9670a4f0d3d3c0e4d4779ad4dadb1ad468b097f808aa", + "sha256:dd3e6547ecf842a29cf25123fbf8d2461c53c8d37aa20d87ecee130c89b7079b", + "sha256:de7fd57765398d141949946c84f3590a68cf5887dac3fc52388df0639b01eda4", + "sha256:ea37320877d56a7f0a1e6a625d892cf963aa7f570013499f5b8d5ab8402b5625", + "sha256:f1fce1e4929157b2afeb4bb7069204d4370bab9f4fc03ca1fbec8bd601f8c87d", + "sha256:f43109822df2d3faac7aad79613f5f02e4eab0fc8ad7932d2e70e2a83bd49c26" + ], + "version": "==2020.10.28" }, "requests": { "extras": [ "socks" ], "hashes": [ - "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", - "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", + "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" ], "index": "pypi", - "version": "==2.22.0" + "version": "==2.24.0" }, "requests-toolbelt": { "hashes": [ @@ -228,48 +324,95 @@ ], "version": "==0.9.1" }, + "rfc3986": { + "hashes": [ + "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d", + "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50" + ], + "version": "==1.4.0" + }, "six": { "hashes": [ - "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", - "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "version": "==1.14.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.15.0" }, "toml": { "hashes": [ - "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", - "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "version": "==0.10.0" + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.2" }, "tqdm": { "hashes": [ - "sha256:03d2366c64d44c7f61e74c700d9b202d57e9efe355ea5c28814c52bfe7a50b8c", - "sha256:be5ddeec77d78ba781ea41eacb2358a77f74cc2407f54b82222d7ee7dc8c8ccf" + "sha256:9ad44aaf0fc3697c06f6e05c7cf025dd66bc7bcb7613c66d85f4464c47ac8fad", + "sha256:ef54779f1c09f346b2b5a8e5c61f96fbcb639929e640e59f8cf810794f406432" ], - "version": "==4.44.1" + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==4.51.0" }, "twine": { "hashes": [ - "sha256:0fb0bfa3df4f62076cab5def36b1a71a2e4acb4d1fa5c97475b048117b1a6446", - "sha256:d6c29c933ecfc74e9b1d9fa13aa1f87c5d5770e119f5a4ce032092f0ff5b14dc" + "sha256:34352fd52ec3b9d29837e6072d5a2a7c6fe4290e97bba46bb8d478b5c598f7ab", + "sha256:ba9ff477b8d6de0c89dd450e70b2185da190514e91c42cc62f96850025c10472" ], "index": "pypi", - "version": "==1.13.0" + "version": "==3.2.0" + }, + "typed-ast": { + "hashes": [ + "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", + "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", + "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d", + "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", + "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", + "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", + "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c", + "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", + "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", + "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", + "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", + "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", + "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", + "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d", + "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", + "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", + "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c", + "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", + "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395", + "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", + "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", + "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", + "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", + "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", + "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072", + "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298", + "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91", + "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", + "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f", + "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" + ], + "version": "==1.4.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", + "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", + "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" + ], + "version": "==3.7.4.3" }, "urllib3": { "hashes": [ - "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", - "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" - ], - "version": "==1.25.8" - }, - "wcwidth": { - "hashes": [ - "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", - "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" + "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", + "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" ], - "version": "==0.1.9" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.25.11" }, "webencodings": { "hashes": [ @@ -280,10 +423,11 @@ }, "zipp": { "hashes": [ - "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", - "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" + "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108", + "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb" ], - "version": "==3.1.0" + "markers": "python_version >= '3.6'", + "version": "==3.4.0" } } } diff --git a/README.md b/README.md index 9d82dac..e84391b 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,17 @@ # virustotal-python 🐍 ![PyPI](https://img.shields.io/pypi/v/virustotal-python.svg?style=flat-square) -A light wrapper around the public VirusTotal API. +A Python library to interact with the public VirusTotal v2 and v3 APIs. -# Dependencies -* Written in Python 3.7. +> [!NOTE] +> +> This library is intended to be used with the public VirusTotal APIs. However, it *could* be used to interact with premium API endpoints as well. + +# Dependencies and installation + +> [!NOTE] +> +> This library should work with Python versions >= 3.7. ``` [dev-packages] @@ -19,105 +26,193 @@ requests = {extras = ["socks"],version = "*"} Install `virustotal-python` using either: * `pip3 install virustotal-python`, `pipenv install`, `pip3 install -r requirements.txt`, `python setup.py install`. -## Example Usage +## Usage examples + +> [!NOTE] +> +> See the [examples](examples) directory for several usage examples. +> +> Furthermore, check [`virustotal_python/virustotal.py`](virustotal_python/virustotal.py) for docstrings containing full parameter descriptions. + +Authenticate using your VirusTotal API key: + +> ![NOTE] +> +> To obtain a VirusTotal API key, [sign up](https://www.virustotal.com/gui/join-us) for a VirusTotal account. +> +> Then, view your VirusTotal API key. +> +> ![VirusTotal view API key](images/APIKey.png) + ```python from virustotal_python import Virustotal -from pprint import pprint -# Normal Initialisation. -vtotal = Virustotal("Insert API Key Here.") +# v2 example +vtotal = Virustotal(API_KEY="Insert API key here.") -# NEW as of version 0.0.5: Proxy support. -# Example Usage: Using HTTP(S) -vtotal = Virustotal( - "Insert API Key Here.", - {"http": "http://10.10.1.10:3128", "https": "http://10.10.1.10:1080"}) -# Or using SOCKS +# v3 example +vtotal = Virustotal(API_KEY="Insert API key here.", API_VERSION="v3") + +# You can provide True to the `COMPATIBILITY_ENABLED` parameter to preserve the old response format of virustotal-python versions prior to 0.1.0 +vtotal = Virustotal(API_KEY="Insert API key here.", API_VERSION="v3", COMPATIBILITY_ENABLED=True) + +# You can also set proxies and timeouts for requests made by the library vtotal = Virustotal( - "Insert API Key Here.", - {"http": "socks5://user:pass@host:port", "https": "socks5://user:pass@host:port"}) - -# NOTE: Check virustotal.py for docstrings containing full parameter descriptions. - -# Send a file to Virustotal for analysis. -resp = vtotal.file_scan("./tests.py") # PATH to file for querying. - -# Retrieve scan report(s) for given file(s) from Virustotal. -# A list containing the resource (SHA256) HASH of a known malicious file. -resp = vtotal.file_report( - ["9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115"] -) -# A list of resource(s). Can be `md5/sha1/sha256 hashes` and/or combination of hashes and scan_ids (MAX 4 per standard request rate). -# The first is a scan_id, the second is a SHA256 HASH. -resp = vtotal.file_report( - [ - "75efd85cf6f8a962fe016787a7f57206ea9263086ee496fc62e3fc56734d4b53-1555351539", - "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115", - ] -) - -# Query url(s) to VirusTotal. -# A list containing a url to be scanned by VirusTotal. -resp = vtotal.url_scan(["ihaveaproblem.info"]) # Query a single url. -# A list of url(s) to be scanned by VirusTotal (MAX 4 per standard request rate). -resp = vtotal.url_scan( - ["ihaveaproblem.info", "google.com", "wikipedia.com", "github.com"] -) - -# Retrieve url report(s) -# A list containing the url of the report to be retrieved. -resp = vtotal.url_report(["ihaveaproblem.info"]) # Query a single url. -# A list of the url(s) and/or scan_id(s) report(s) to be retrieved (MAX 4 per standard request rate). -# The first object in the list is a scan_id. -resp = vtotal.url_report( - [ - "fd21590d9df715452c8c000e1b5aa909c7c5ea434c2ddcad3f4ccfe9b0ee224e-1555352750", - "google.com", - "wikipedia.com", - "github.com", - ], - scan=1, -) - -# Query an IP to Virustotal. -resp = vtotal.ipaddress_report("90.156.201.27") - -# Retrieve a domain report. -resp = vtotal.domain_report("027.ru") - -# Put a comment onto a specific resource. -resp = vtotal.put_comment( - "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115", - comment="#watchout, this looks very malicious!", -) - -pprint(resp) + API_KEY="Insert API key here.", + API_VERSION="v3", + PROXIES={"http": "http://10.10.1.10:3128", "https": "http://10.10.1.10:1080"}, + TIMEOUT=5.0) +``` + +Additionally, it is possible to provide an API key via the environment variable `VIRUSTOTAL_API_KEY`. + +Bash example: + +```bash +export VIRUSTOTAL_API_KEY="Insert API key here." +``` + +PowerShell example: + +```powershell +$Env:VIRUSTOTAL_API_KEY = "Insert API key here." +``` + +Now, initialise the `Virustotal` class: + +```python +from virustotal_python import Virustotal + +# v2 example +vtotal = Virustotal() + +# v3 example +vtotal = Virustotal(API_VERSION="v3") +``` + +Send a file for analysis: + +```python +import os.path +from pprint import pprint + +# Declare PATH to file +FILE_PATH = "/path/to/file/to/scan.txt" + +# Create dictionary containing the file to send for multipart encoding upload +files = {"file": (os.path.basename(FILE_PATH), open(os.path.abspath(FILE_PATH), "rb"))} + +# v2 example +resp = vtotal.request("file/scan", files=files, method="POST") + +# The v2 API returns a response_code +# This property retrieves it from the JSON response +print(resp.response_code) +# Print JSON response from the API +pprint(resp.json()) + +# v3 example +resp = vtotal.request("files", files=files, method="POST") + +# The v3 API returns the JSON response inside the 'data' key +# https://developers.virustotal.com/v3.0/reference#api-responses +# This property retrieves the structure inside 'data' from the JSON response +pprint(resp.data) +# Or if you provided COMPATIBILITY_ENABLED=True to the Virustotal class +pprint(resp["json_resp"]) +``` + +Retrieve information about a file: + +```python +from pprint import pprint + +# The ID (either SHA-256, SHA-1 or MD5) identifying the file +FILE_ID = "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115" + +# v2 example +resp = vtotal.request("file/report", {"resource": FILE_ID}) + +print(resp.response_code) +pprint(resp.json()) + +# v3 example +resp = vtotal.request(f"files/{FILE_ID}") + +pprint(resp.data) ``` +Send a URL for analysis, retrieve the analysis report and catch any potential exceptions that may occur (Non 200 HTTP status codes): + ```python -# Example resp for url_scan(). -# Assuming you have already initiated Virustotal() and imported pprint. -resp = vtotal.url_scan(["ihaveaproblem.info"]) # Query a single url. -pprint(resp) -{'json_resp': {'permalink': 'https://www.virustotal.com/url/fd21590d9df715452c8c000e1b5aa909c7c5ea434c2ddcad3f4ccfe9b0ee224e/analysis/1549973453/', - 'resource': 'http://ihaveaproblem.info/', - 'response_code': 1, - 'scan_date': '2019-02-12 12:10:53', - 'scan_id': 'fd21590d9df715452c8c000e1b5aa909c7c5ea434c2ddcad3f4ccfe9b0ee224e-1549973453', - 'url': 'http://ihaveaproblem.info/', - 'verbose_msg': 'Scan request successfully queued, come back ' - 'later for the report'}, - 'status_code': 200} +from virustotal_python import VirustotalError +from pprint import pprint +from base64 import urlsafe_b64encode + +url = "ihaveaproblem.info" + +# v2 example +try: + # Send a URL to VirusTotal for analysis + resp = vtotal.request("url/scan", params={"url": url}, method="POST") + url_resp = resp.json() + # Obtain scan_id + scan_id = url_resp["scan_id"] + # Request report for URL analysis + analysis_resp = vtotal.request("url/report", params={"resource": scan_id}) + print(analysis_resp.response_code) + pprint(analysis_resp.json()) +except VirustotalError as err: + print(f"An error occurred: {err}\nCatching and continuing with program.") + +# v3 example +try: + # Send URL to VirusTotal for analysis + resp = vtotal.request("urls", data={"url": url}, method="POST") + # URL safe encode URL in base64 format + # https://developers.virustotal.com/v3.0/reference#url + url_id = urlsafe_b64encode(url.encode()).decode().strip("=") + # Obtain the analysis results for the URL using the url_id + analysis_resp = vtotal.request(f"urls/{url_id}") + pprint(analysis_resp.object_type) + pprint(analysis_resp.data) +except VirustotalError as err: + print(f"An error occurred: {err}\nCatching and continuing with program.") ``` -## Running Tests +Retrieve information about a domain: + +```python +from pprint import pprint + +domain = "virustotal.com" -* `Navigate to ./virustotal_python/` +# v2 example +resp = vtotal.request("domain/report", params={"domain": domain}) -* `Run the command: pytest -s tests.py` +print(resp.response_code) +pprint(resp.json()) + +# v3 example +resp = vtotal.request(f"domains/{domain}") + +pprint(resp.data) +``` + +## Running the tests + +To run the tests, perform the following steps: + +1. Ensure pytest is installed using: `pip install pytest` + +2. Export your API key to the environment variable `VIRUSTOTAL_API_KEY` (instructions above). + +3. From the root directory of the project run `pytest -s .\virustotal_python\tests.py` ## Changelog +* 0.1.0 - Added support for the VirusTotal v3 API. Library redesign (new usage, examples, tests and more.) See [#24](https://github.com/dbrennand/virustotal-python/pull/24). + * 0.0.9 - Update dependencies for security vulnerability. * 0.0.8 - Updated dependencies, removed method `file_rescan` @@ -134,11 +229,11 @@ pprint(resp) * 0.0.2 - Changes to file_rescan(), file_report(), url_scan(), url_report() to improve ease of use of the wrapper. See issue [#2](https://github.com/dbrennand/virustotal-python/issues/2). Examples updated for changes. -* 0.0.1 - Inital release of virustotal-python. Covered all endpoints of the Virustotal public API. +* 0.0.1 - Initial release of virustotal-python. Covered all endpoints of the Virustotal public API. ## Authors -- Contributors -* **dbrennand** - *Author* - [dbrennand](https://github.com/dbrennand) +* [**dbrennand**](https://github.com/dbrennand) - *Author* ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) for details. \ No newline at end of file +This project is licensed under the MIT License - see the [LICENSE](LICENSE) for details. diff --git a/examples/comments.py b/examples/comments.py new file mode 100644 index 0000000..fef6fc6 --- /dev/null +++ b/examples/comments.py @@ -0,0 +1,118 @@ +""" +The examples in this file are for virustotal-python version >=0.1.0 + +Retrieve comments and interact with them using the VirusTotal API. + +Documentation: + + * v2 documentation - https://developers.virustotal.com/reference#comments-get + + * https://developers.virustotal.com/reference#comments-put + + * v3 documentation - https://developers.virustotal.com/v3.0/reference#comments-1 +""" +from virustotal_python import Virustotal +from base64 import urlsafe_b64encode +from pprint import pprint + +API_KEY = "Insert API key here." + +# The ID (either SHA-256, SHA-1 or MD5) identifying the file +FILE_ID = "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115" + +# URL/domain identifier +DOMAIN = "google.com" + +# Obtain the URL ID +URL_ID = urlsafe_b64encode(DOMAIN.encode()).decode().strip("=") + +# Example IP address (Google DNS) +IP = "8.8.8.8" + +# Example ID of a graph +## NOTE: There are no comments on this graph so an empty list is returned +GRAPH_ID = "g70fae134aefc4e2f90f069aba47d15a92e0073564310443aa0b6ca3384f5240d" + +# Example comment ID +COMMENT_ID = "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-07457619" + +# v2 examples +vtotal = Virustotal(API_KEY=API_KEY) + +# Retrieve comments for a given file ID +resp = vtotal.request("comments/get", params={"resource": FILE_ID}) + +pprint(resp.json()) + +# Create a comment for a given file ID +resp = vtotal.request("comments/put", params={"resource": FILE_ID, "comment": "Wow, this looks like a #malicious file!"}, method="POST") + +pprint(resp.json()) + +# v3 examples +vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3") + +## Retriving comments for resources +### Retrieve 10 comments for a file +resp = vtotal.request(f"files/{FILE_ID}/comments", params={"limit": 10}) +### Retrieve 2 comments for a URL +resp = vtotal.request(f"urls/{URL_ID}/comments", params={"limit": 2}) +### Retrieve 2 comments for a domain +resp = vtotal.request(f"domains/{DOMAIN}/comments", params={"limit": 2}) +### Retrieve 5 comments for an IP address +resp = vtotal.request(f"ip_addresses/{IP}/comments", params={"limit": 5}) +### Retrieve 3 comments for a graph +resp = vtotal.request(f"graphs/{GRAPH_ID}/comments", params={"limit": 3}) + +## Submit a comment on a file, URL, domain, IP address or graph. +## Prepare comment JSON +comment = { + "data": { + "type": "comment", + "attributes": { + "text": "Watchout! This looks dangerous!" + } + } +} + +## Submit comments on a resource +### Submit a comment on a file +resp = vtotal.request(f"files/{FILE_ID}/comments", json=comment, method="POST") +### Submit a comment on a URL +resp = vtotal.request(f"urls/{URL_ID}/comments", json=comment, method="POST") +### Submit a comment on a domain +resp = vtotal.request(f"domains/{DOMAIN}/comments", json=comment, method="POST") +### Submit a comment on a IP address +resp = vtotal.request(f"ip_addresses/{IP}/comments", json=comment, method="POST") +### Submit a comment on a graph +resp = vtotal.request(f"graphs/{GRAPH_ID}/comments", json=comment, method="POST") + +## Retrieve the latest comments added to VirusTotal +### Retrieve the 10 latest comments added to VirusTotal with no filter +resp = vtotal.request("comments", params={"limit": 10}) +### Retrieve the 10 latest comments added to VirusTotal, filtering for Remote Access Trojan (RAT) +#### When testing, for some reason there are comments that are returned which don't contain the tag 🤔 +resp = vtotal.request("comments", params={"limit": 10, "filter": "rat"}) +### Retrieve a specific comment based on the ID +resp = vtotal.request(f"comments/{COMMENT_ID}") +### Edit a specific comment based on the ID +#### Prepare comment JSON +# Old comment was '#watchout, this looks very malicious!' +edited_comment = { + "data": { + "type": "comment", + "attributes": { + "text": "#watchout, this looks quite malicious!" + } + } +} +resp = vtotal.request(f"comments/{COMMENT_ID}", json=edited_comment, method="PATCH") +### Delete a comment based on the ID +resp = vtotal.request(f"comments/{COMMENT_ID}", method="DELETE") +### Submit a vote for a comment +#### Vote options can be either positive, negative or abuse +### Submit a positive vote on a comment based on the comment ID +# This is what I got working +## I found the documentation on this endpoint confusing... :-D +### https://developers.virustotal.com/v3.0/reference#vote-comment +resp = vtotal.request(f"comments/{COMMENT_ID}/vote", json={"data": "positive"}, method="POST") diff --git a/examples/cursor.py b/examples/cursor.py new file mode 100644 index 0000000..51e39b0 --- /dev/null +++ b/examples/cursor.py @@ -0,0 +1,35 @@ +""" +The examples in this file are for virustotal-python version >=0.1.0 + +Use a cursor from the VirusTotal API JSON response to retrieve more results. + +Documentation: + + * v3 documentation - https://developers.virustotal.com/v3.0/reference#collections +""" +from virustotal_python import Virustotal +from pprint import pprint + +API_KEY = "Insert API key here." + +# Example IP address (Google DNS) +IP = "8.8.8.8" + +# v3 example +vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3") + +# Retrieve communicating_files related to the IP address with a limit of 5 +resp = vtotal.request(f"ip_addresses/{IP}/communicating_files", params={"limit": 2}) + +# Initialise count variable +count = 0 + +# While a cursor is present, keep collecting results! +while resp.cursor: + print(count) + print(f"This is the current: {resp.cursor}") + # Get more results with cursor + resp = vtotal.request(f"ip_addresses/{IP}/communicating_files", params={"limit": 2, "cursor": resp.cursor}) + # Do something with the resp here + # Add to the count to show how many times we have retrieved another cursor + count += 1 diff --git a/examples/domain_info.py b/examples/domain_info.py new file mode 100644 index 0000000..9be647f --- /dev/null +++ b/examples/domain_info.py @@ -0,0 +1,32 @@ +""" +The examples in this file are for virustotal-python version >=0.1.0 + +Retrieve information about a domain from the VirusTotal API. + +Documentation: + + * v2 documentation - https://developers.virustotal.com/reference#domain-report + + * v3 documentation - https://developers.virustotal.com/v3.0/reference#domain-info +""" +from virustotal_python import Virustotal +from pprint import pprint + +API_KEY = "Insert API key here." + +domain = "virustotal.com" + +# v2 example +vtotal = Virustotal(API_KEY=API_KEY) + +resp = vtotal.request("domain/report", params={"domain": domain}) + +print(resp.response_code) +pprint(resp.json()) + +# v3 example +vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3") + +resp = vtotal.request(f"domains/{domain}") + +pprint(resp.data) diff --git a/examples/file_info.py b/examples/file_info.py new file mode 100644 index 0000000..67554e2 --- /dev/null +++ b/examples/file_info.py @@ -0,0 +1,33 @@ +""" +The examples in this file are for virustotal-python version >=0.1.0 + +Retrieve information about a file from the VirusTotal API. + +Documentation: + + * v2 documentation - https://developers.virustotal.com/reference#file-report + + * v3 documentation - https://developers.virustotal.com/v3.0/reference#file-info +""" +from virustotal_python import Virustotal +from pprint import pprint + +API_KEY = "Insert API key here." + +# The ID (either SHA-256, SHA-1 or MD5) identifying the file +FILE_ID = "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115" + +# v2 example +vtotal = Virustotal(API_KEY=API_KEY) + +resp = vtotal.request("file/report", {"resource": FILE_ID}) + +print(resp.response_code) +pprint(resp.json()) + +# v3 example +vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3") + +resp = vtotal.request(f"files/{FILE_ID}") + +pprint(resp.data) diff --git a/examples/graphs.py b/examples/graphs.py new file mode 100644 index 0000000..1aff2f1 --- /dev/null +++ b/examples/graphs.py @@ -0,0 +1,29 @@ +""" +The examples in this file are for virustotal-python version >=0.1.0 + +Retrieve graphs and interact with them using the VirusTotal v3 API. + +Documentation: + + * v3 documentation - https://developers.virustotal.com/v3.0/reference#graphs-1 +""" +from virustotal_python import Virustotal +from pprint import pprint + +API_KEY = "Insert API key here." + +# Example ID of a graph +GRAPH_ID = "g70fae134aefc4e2f90f069aba47d15a92e0073564310443aa0b6ca3384f5240d" + +# v3 examples +vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3") + +## Retrieve 3 graphs from the VirusTotal v3 API +resp = vtotal.request("graphs", params={"limit": 3}) +## Retrieve 3 graphs from the VirusTotal v3 API filtering by owner, order and attributes +resp = vtotal.request("graphs", params={"limit": 2, "filter": "owner:hugoklugman", "order": "views_count", "attributes": "graph_data"}) +### Retrieve a graph using the graph's ID +resp = vtotal.request(f"graphs/{GRAPH_ID}") + +# For more graph endpints, see https://developers.virustotal.com/v3.0/reference#graphs-1 +# To create a graph, head to https://www.virustotal.com/graph/ diff --git a/examples/ip.py b/examples/ip.py new file mode 100644 index 0000000..00e657a --- /dev/null +++ b/examples/ip.py @@ -0,0 +1,48 @@ +""" +The examples in this file are for virustotal-python version >=0.1.0 + +Retrieve IP addresses using the VirusTotal API. + +Documentation: + + * v2 documentation - https://developers.virustotal.com/reference#ip-address-report + + * v3 documentation - https://developers.virustotal.com/v3.0/reference#ip-addresses +""" +from virustotal_python import Virustotal +from pprint import pprint + +API_KEY = "Insert API key here." + +# Example IP address (Google DNS) +IP = "8.8.8.8" + +# v2 examples +vtotal = Virustotal(API_KEY=API_KEY) + +## Retrieve information about an IP address +resp = vtotal.request("ip-address/report", params={"ip": IP}) + +pprint(resp.json()) + +# v3 examples +vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3") + +# Retrieve information about an IP address +resp = vtotal.request(f"ip_addresses/{IP}") +# Retrieve objects (relationships) related to an IP address +# Retrieve historical_whois relationship to the IP address +# For other relationships, see the table at: https://developers.virustotal.com/v3.0/reference#ip-relationships +resp = vtotal.request(f"ip_addresses/{IP}/historical_whois") +# Retrieve communicating_files related to the IP address with a limit of 5 +resp = vtotal.request( + f"ip_addresses/{IP}/communicating_files", params={"limit": 5}) + +# Retrieve votes for an IP address +resp = vtotal.request(f"ip_addresses/{IP}/votes") +# Send a vote for an IP address +# Create vote JSON +## Verdict can be either harmless or malicious +### https://developers.virustotal.com/v3.0/reference#ip-votes-post +vote = {"data": {"type": "vote", "attributes": {"verdict": "harmless"}}} +resp = vtotal.request(f"ip_addresses/{IP}/votes", json=vote, method="POST") diff --git a/examples/scan_file.py b/examples/scan_file.py new file mode 100644 index 0000000..469564c --- /dev/null +++ b/examples/scan_file.py @@ -0,0 +1,37 @@ +""" +The examples in this file are for virustotal-python version >=0.1.0 + +Send a file to the VirusTotal API for analysis. + +Documentation: + + * v2 documentation - https://developers.virustotal.com/reference#file-scan + + * v3 documentation - https://developers.virustotal.com/v3.0/reference#files-scan +""" +from virustotal_python import Virustotal +import os.path +from pprint import pprint + +API_KEY = "Insert API key here." + +# Declare PATH to file +FILE_PATH = "/path/to/file/to/scan.txt" + +# Create dictionary containing the file to send for multipart encoding upload +files = {"file": (os.path.basename(FILE_PATH), open(os.path.abspath(FILE_PATH), "rb"))} + +# v2 example +vtotal = Virustotal(API_KEY=API_KEY) + +resp = vtotal.request("file/scan", files=files, method="POST") + +print(resp.response_code) +pprint(resp.json()) + +# v3 example +vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3") + +resp = vtotal.request("files", files=files, method="POST") + +pprint(resp.data) diff --git a/examples/scan_urls.py b/examples/scan_urls.py new file mode 100644 index 0000000..9ffee16 --- /dev/null +++ b/examples/scan_urls.py @@ -0,0 +1,52 @@ +""" +The examples in this file are for virustotal-python version >=0.1.0 + +Send URLs to the VirusTotal API for analysis and retrieve the analysis results. + +Documentation: + + * v2 documentation - https://developers.virustotal.com/reference#url-scan + + * https://developers.virustotal.com/reference#url-report + + * v3 documentation - https://developers.virustotal.com/v3.0/reference#urls + + * https://developers.virustotal.com/v3.0/reference#url-info +""" +from virustotal_python import Virustotal +import os.path +from pprint import pprint +from base64 import urlsafe_b64encode + +API_KEY = "Insert API key here." + +URLS = ["google.com", "wikipedia.com", "github.com", "ihaveaproblem.info"] + +# v2 example +vtotal = Virustotal(API_KEY=API_KEY) + +# Send the URLs to VirusTotal for analysis +# A maximum of 4 URLs can be sent at once for a v2 API request +resp = vtotal.request("url/scan", params={"url": "\n".join(url)}, method="POST") +for url_resp in resp.json(): + # Obtain scan_id + scan_id = url_resp["scan_id"] + # Request report for URL analysis + analysis_resp = vtotal.request("url/report", params={"resource": scan_id}) + print(analysis_resp.response_code) + pprint(analysis_resp.json()) + +# v3 example +vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3") + +for url in URLS: + # Send the URL to VirusTotal for analysis + resp = vtotal.request("urls", data={"url": url}, method="POST") + # URL safe encode URL in base64 format + # https://developers.virustotal.com/v3.0/reference#url + url_id = urlsafe_b64encode(url.encode()).decode().strip("=") + print(f"URL: {url} ID: {url_id}") + # Obtain the analysis results for the URL using the url_id + analysis_resp = vtotal.request(f"urls/{url_id}") + print(analysis_resp.object_type) + pprint(analysis_resp.data) diff --git a/examples/searchmetadata.py b/examples/searchmetadata.py new file mode 100644 index 0000000..c1c38ab --- /dev/null +++ b/examples/searchmetadata.py @@ -0,0 +1,39 @@ +""" +The examples in this file are for virustotal-python version >=0.1.0 + +Search the VirusTotal v3 API for a domain, IP address and comment tag. + +Also, retrieve VirusTotal metadata. + +Documentation: + + * v3 documentation - https://developers.virustotal.com/v3.0/reference#search-1 + + * https://developers.virustotal.com/v3.0/reference#metadata +""" +from virustotal_python import Virustotal +from pprint import pprint + +API_KEY = "Insert API key here." + +# The ID (either SHA-256, SHA-1 or MD5) identifying the file +FILE_ID = "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115" + +# v3 examples +vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3") + +## Search the VirusTotal API for google.com +resp = vtotal.request("search", params={"query": "google.com"}) +## Search the VirusTotal API for information related to Google's DNS (8.8.8.8) +resp = vtotal.request("search", params={"query": "8.8.8.8"}) +## Search the VirusTotal API for a file ID +resp = vtotal.request("search", params={"query": FILE_ID}) +## Search the VirusTotal API for the tag comment '#malicious' +resp = vtotal.request("search", params={"query": "#malicious"}) + +## Retrieve VirusTotal metadata +resp = vtotal.request("metadata") +## Print out a list of VirusTotal's supported engines +resp = vtotal.request("metadata") +engines_dict = resp.data["engines"] +print(engines_dict.keys()) diff --git a/images/APIKey.png b/images/APIKey.png new file mode 100644 index 0000000000000000000000000000000000000000..d9ae185338972a30094127deb41c358dce25c1d6 GIT binary patch literal 6786 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 e24?ot0@neh(~=0.1.0 head to /examples directory. +""" from virustotal import Virustotal from pprint import pprint +import os.path -# Normal Initialisation. -vtotal = Virustotal("Insert API Key Here.") +# Normal Initialisation +vtotal = Virustotal("Insert API key here.") -# NEW as of version 0.0.5: Proxy support. +# NEW as of version 0.0.5: Proxy support # Example Usage: Using HTTP(S) vtotal = Virustotal( - "Insert API Key Here.", - {"http": "http://10.10.1.10:3128", "https": "http://10.10.1.10:1080"}) + "Insert API key here.", + {"http": "http://10.10.1.10:3128", "https": "http://10.10.1.10:1080"}, +) # Or using SOCKS vtotal = Virustotal( - "Insert API Key Here.", - {"http": "socks5://user:pass@host:port", "https": "socks5://user:pass@host:port"}) + "Insert API key here.", + {"http": "socks5://user:pass@host:port", "https": "socks5://user:pass@host:port"}, +) -# NOTE: Check virustotal.py for docstrings containing full parameter descriptions. +# NOTE: Check virustotal.py for docstrings containing full parameter descriptions -# Send a file to Virustotal for analysis. -resp = vtotal.file_scan("./tests.py") # PATH to file for querying. +# Send a file to Virustotal for analysis +resp = vtotal.file_scan("./tests.py") # PATH to file for querying -# NOTE: This endpoint has been removed from the Public Virustotal API. -# Resend a file to Virustotal for analysis. -# A list containing the resource (SHA256) HASH of the file above. -#resp = vtotal.file_rescan( +# NOTE: This endpoint has been removed from the Public Virustotal API +# Resend a file to Virustotal for analysis +# A list containing the resource (SHA256) HASH of the file above +# resp = vtotal.file_rescan( # ["75efd85cf6f8a962fe016787a7f57206ea9263086ee496fc62e3fc56734d4b53"] -#) -## A list containing md5/sha1/sha256 hashes. Can be a combination of any of the three allowed hashes (MAX 25 items). -## NOTE: The second hash here is flagged as malicious by multiple engines. -#resp = vtotal.file_rescan( +# ) +## A list containing md5/sha1/sha256 hashes. Can be a combination of any of the three allowed hashes (MAX 25 items) +## NOTE: The second hash here is flagged as malicious by multiple engines +# resp = vtotal.file_rescan( # [ # "75efd85cf6f8a962fe016787a7f57206ea9263086ee496fc62e3fc56734d4b53", # "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115", # ] -#) +# ) -# Retrieve scan report(s) for given file(s) from Virustotal. -# A list containing the resource (SHA256) HASH of a known malicious file. +# Retrieve scan report(s) for given file(s) from Virustotal +# A list containing the resource (SHA256) HASH of a known malicious file resp = vtotal.file_report( ["9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115"] ) -# A list of resource(s). Can be `md5/sha1/sha256 hashes` and/or combination of hashes and scan_ids (MAX 4 per standard request rate). -# The first is a scan_id, the second is a SHA256 HASH. +# A list of resource(s). Can be `md5/sha1/sha256 hashes` and/or combination of hashes and scan_ids (MAX 4 per standard request rate) +# The first is a scan_id, the second is a SHA256 HASH resp = vtotal.file_report( [ "75efd85cf6f8a962fe016787a7f57206ea9263086ee496fc62e3fc56734d4b53-1555351539", @@ -48,19 +56,19 @@ ] ) -# Query url(s) to VirusTotal. -# A list containing a url to be scanned by VirusTotal. -resp = vtotal.url_scan(["ihaveaproblem.info"]) # Query a single url. -# A list of url(s) to be scanned by VirusTotal (MAX 4 per standard request rate). +# Query url(s) to VirusTotal +# A list containing a url to be scanned by VirusTotal +resp = vtotal.url_scan(["ihaveaproblem.info"]) # Query a single url +# A list of url(s) to be scanned by VirusTotal (MAX 4 per standard request rate) resp = vtotal.url_scan( ["ihaveaproblem.info", "google.com", "wikipedia.com", "github.com"] ) # Retrieve url report(s) -# A list containing the url of the report to be retrieved. -resp = vtotal.url_report(["ihaveaproblem.info"]) # Query a single url. -# A list of the url(s) and/or scan_id(s) report(s) to be retrieved (MAX 4 per standard request rate). -# The first object in the list is a scan_id. +# A list containing the url of the report to be retrieved +resp = vtotal.url_report(["ihaveaproblem.info"]) # Query a single url +# A list of the url(s) and/or scan_id(s) report(s) to be retrieved (MAX 4 per standard request rate) +# The first object in the list is a scan_id resp = vtotal.url_report( [ "fd21590d9df715452c8c000e1b5aa909c7c5ea434c2ddcad3f4ccfe9b0ee224e-1555352750", @@ -71,13 +79,13 @@ scan=1, ) -# Query an IP to Virustotal. +# Query an IP to Virustotal resp = vtotal.ipaddress_report("90.156.201.27") -# Retrieve a domain report. +# Retrieve a domain report resp = vtotal.domain_report("027.ru") -# Put a comment onto a specific resource. +# Put a comment onto a specific resource resp = vtotal.put_comment( "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115", comment="#watchout, this looks very malicious!", diff --git a/virustotal_python/tests.py b/virustotal_python/tests.py index 80b8638..b90a92f 100644 --- a/virustotal_python/tests.py +++ b/virustotal_python/tests.py @@ -1,70 +1,297 @@ -from virustotal_python import Virustotal -from time import sleep +import virustotal_python import pytest +import os.path +from time import sleep +from base64 import urlsafe_b64encode + +# Declare variables for tests +# Create dictionary containing the file to send for multipart encoding upload +FILES = { + "file": ( + os.path.basename("virustotal_python/oldexamples.py"), + open(os.path.abspath("virustotal_python/oldexamples.py"), "rb"), + ) +} +# The ID (either SHA-256, SHA-1 or MD5) identifying the file +FILE_ID = "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115" +# Example IP address (Google DNS) +IP = "8.8.8.8" +# Example ID of a graph +## NOTE: There are no comments on this graph so an empty list is returned +GRAPH_ID = "g70fae134aefc4e2f90f069aba47d15a92e0073564310443aa0b6ca3384f5240d" +# URL/domain identifier +URL_DOMAIN = "google.com" +# Example comment ID +COMMENT_ID = "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-07457619" + + +@pytest.fixture() +def vtotal_v2(request): + yield virustotal_python.Virustotal() + + def fin(): + """ + Helper function which sleeps for 15 seconds between each test. This is to avoid VirusTotal 403 rate quota limits. + """ + print("Sleeping for 15 seconds to avoid VirusTotal 403 rate quota limits...") + sleep(15) + + request.addfinalizer(fin) -@pytest.fixture -def virustotal_object(request): - API_KEY = "Insert API Key Here." - yield Virustotal(API_KEY) +@pytest.fixture() +def vtotal_v3(request): + yield virustotal_python.Virustotal(API_VERSION="v3") def fin(): """ - Sleep for 30 seconds after each test; to avoid Virustotal 403 rate quota limit. + Helper function which sleeps for 15 seconds between each test. This is to avoid VirusTotal 403 rate quota limits. """ - print("Sleeping for 30 seconds...") - sleep(30) + print("Sleeping for 15 seconds to avoid VirusTotal 403 rate quota limits...") + sleep(15) request.addfinalizer(fin) -def assert_content(resp): +def test_file_scan_v2(vtotal_v2): """ - Check json_resp data which is nested. - :param content: The nested json_resp object. + Test for sending a file to the VirusTotal v2 API for analysis. """ - for content in enumerate(resp["json_resp"]): - assert content[1]["response_code"] == 1 + # Create dictionary containing the file to send for multipart encoding upload + files = { + "file": ( + os.path.basename("virustotal_python/oldexamples.py"), + open(os.path.abspath("virustotal_python/oldexamples.py"), "rb"), + ) + } + resp = vtotal_v2.request("file/scan", files=FILES, method="POST") + data = resp.json() + assert resp.response_code == 1 + assert data["scan_id"] + assert data["permalink"] -def test_file_scan(virustotal_object): - resp = virustotal_object.file_scan("./examples.py") - assert resp["status_code"] == 200 - assert resp["json_resp"]["response_code"] == 1 +def test_file_scan_v3(vtotal_v3): + """ + Test for sending a file to the VirusTotal v3 API for analysis. + """ + resp = vtotal_v3.request("files", files=FILES, method="POST") + assert resp.status_code == 200 + data = resp.data + assert data["id"] + assert data["type"] == "analysis" -def test_file_report(virustotal_object): - resp = virustotal_object.file_report( - [ - "75efd85cf6f8a962fe016787a7f57206ea9263086ee496fc62e3fc56734d4b53-1555351539", - "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115", - ] - ) +def test_file_info_v2(vtotal_v2): + """ + Test for retrieving information about a file from the VirusTotal v2 API. + """ + resp = vtotal_v2.request("file/report", {"resource": FILE_ID}) + assert resp.response_code == 1 + assert resp.json()["scans"] + + +def test_file_info_v3(vtotal_v3): + """ + Test for retrieving information about a file from the VirusTotal v3 API. + """ + resp = vtotal_v3.request(f"files/{FILE_ID}") + assert resp.status_code == 200 + assert resp.object_type == "file" + assert resp.data["attributes"] + assert resp.data["attributes"]["last_analysis_results"] + + +def test_compatibility(): + """ + Test COMPATIBILITY_ENABLED parameter on Virustotal class. + """ + vtotal = virustotal_python.Virustotal(API_VERSION="v3", COMPATIBILITY_ENABLED=True) + resp = vtotal.request(f"files/{FILE_ID}") assert resp["status_code"] == 200 - assert_content(resp) + assert resp["json_resp"]["data"]["type"] == "file" + assert resp["json_resp"]["data"]["attributes"] + + +def test_scan_url_info_v2(vtotal_v2): + """ + Test scanning URL and retrieving the scan results from the VirusTotal v2 API. + """ + # Send the URLs to VirusTotal for analysis + resp = vtotal_v2.request("url/scan", params={"url": URL_DOMAIN}, method="POST") + assert resp.status_code == 200 + data = resp.json() + # Obtain scan_id + scan_id = data["scan_id"] + # Request report for URL analysis + analysis_resp = vtotal_v2.request("url/report", params={"resource": scan_id}) + assert analysis_resp.status_code == 200 + assert analysis_resp.response_code == 1 + data = analysis_resp.json() + assert data["scan_id"] + assert data["verbose_msg"] + assert data["url"] == f"http://{URL_DOMAIN}/" + assert data["scan_date"] + + +def test_scan_url_info_v3(vtotal_v3): + """ + Test scanning URL and retrieving the scan results from the VirusTotal v3 API. + """ + resp = vtotal_v3.request("urls", data={"url": URL_DOMAIN}, method="POST") + assert resp.status_code == 200 + assert resp.data["id"] + # URL safe encode URL in base64 format + # https://developers.virustotal.com/v3.0/reference#url + url_id = urlsafe_b64encode(URL_DOMAIN.encode()).decode().strip("=") + print(f"URL: {URL_DOMAIN} ID: {url_id}") + # Obtain the analysis results for the URL using the url_id + analysis_resp = vtotal_v3.request(f"urls/{url_id}") + assert analysis_resp.status_code == 200 + assert analysis_resp.object_type == "url" + assert analysis_resp.data["attributes"] + + +def test_domain_info_v2(vtotal_v2): + """ + Test for retrieving domain information from the VirusTotal v2 API. + """ + resp = vtotal_v2.request("domain/report", params={"domain": URL_DOMAIN}) + assert resp.response_code == 1 + json = resp.json() + assert json["Alexa rank"] + assert json["Alexa domain info"] + assert json["Webutation domain info"]["Verdict"] + assert json["whois_timestamp"] -def test_url_scan(virustotal_object): - resp = virustotal_object.url_scan( - ["ihaveaproblem.info", "google.com", "wikipedia.com", "github.com"] +def test_domain_info_v3(vtotal_v3): + """ + Test for retrieving domain information from the VirusTotal v3 API. + """ + resp = vtotal_v3.request(f"domains/{URL_DOMAIN}") + assert resp.status_code == 200 + assert resp.object_type == "domain" + data = resp.data + assert isinstance(data["links"], dict) + assert data["attributes"]["last_analysis_results"] + assert data["attributes"]["creation_date"] + + +def test_retrieve_comment_file_id_v3(vtotal_v3): + """ + Test for retrieving a comment for a given file ID. + """ + resp = vtotal_v3.request(f"files/{FILE_ID}/comments", params={"limit": 2}) + assert resp.status_code == 200 + assert resp.links + assert resp.meta + assert resp.cursor + json = resp.data + # Retrieve first comment text + assert json[0]["attributes"]["text"] + # Retrieve second comment tags + assert json[1]["attributes"]["votes"] and isinstance( + json[1]["attributes"]["votes"], dict ) - assert resp["status_code"] == 200 - assert_content(resp) -def test_url_report(virustotal_object): - resp = virustotal_object.url_report(["ihaveaproblem.info"], scan=1) - assert resp["status_code"] == 200 - assert resp["json_resp"]["response_code"] == 1 +def test_retrieve_comment_file_id_v2(vtotal_v2): + """ + Test for retrieving a comment for a given file ID using the VirusTotal v2 API. + """ + resp = vtotal_v2.request("comments/get", params={"resource": FILE_ID}) + assert resp.status_code == 200 + assert resp.response_code == 1 + json = resp.json() + for commentdata in json["comments"]: + assert commentdata["date"] + assert commentdata["comment"] -def test_ipaddress_report(virustotal_object): - resp = virustotal_object.ipaddress_report("90.156.201.27") - assert resp["status_code"] == 200 - assert resp["json_resp"]["response_code"] == 1 +def test_retrieve_comment_latest_v3(vtotal_v3): + """ + Test for retrieveing the latest 10 comments made on VirusTotal. + """ + resp = vtotal_v3.request("comments", params={"limit": 10}) + assert resp.status_code == 200 + assert resp.links + assert resp.meta + assert resp.cursor + json = resp.data + assert json[0]["attributes"]["date"] + assert len(json) == 10 -def test_domain_report(virustotal_object): - resp = virustotal_object.domain_report("027.ru") - assert resp["status_code"] == 200 - assert resp["json_resp"]["response_code"] == 1 +def test_retrieve_ip_info_v2(vtotal_v2): + """ + Test for retrieving information about an IP address using the VirusTotal v2 API. + """ + resp = vtotal_v2.request("ip-address/report", params={"ip": IP}) + assert resp.status_code == 200 + assert resp.response_code == 1 + json = resp.json() + assert json["as_owner"] == "Google LLC" + assert json["country"] == "US" + assert json["verbose_msg"] == "IP address in dataset" + for sample in json["detected_communicating_samples"]: + assert sample["date"] + assert sample["positives"] + assert sample["sha256"] + assert sample["total"] + + +def test_retrieve_ip_info_v3(vtotal_v3): + """ + Test for retrieving information about an IP address. + """ + resp = vtotal_v3.request(f"ip_addresses/{IP}") + assert resp.status_code == 200 + data = resp.data + assert data["attributes"]["as_owner"] == "Google LLC" + assert data["attributes"]["country"] == "US" + assert data["attributes"]["last_analysis_stats"] + assert data["attributes"]["reputation"] + assert resp.object_type == "ip_address" + + +def test_retrieve_graph_v3(vtotal_v3): + """ + Test for retrieving the latest 3 graphs made on VirusTotal. + """ + resp = vtotal_v3.request("graphs", params={"limit": 3}) + assert resp.status_code == 200 + assert len(resp.data) == 3 + data = resp.data + assert data[0]["attributes"]["graph_data"] + assert resp.links + assert resp.meta + assert resp.cursor + + +def test_search_v3(vtotal_v3): + """ + Test searching the VirusTotal API. + """ + resp = vtotal_v3.request("search", params={"query": URL_DOMAIN}) + assert resp.status_code == 200 + assert resp.links + assert resp.object_type + data = resp.data + assert data[0]["attributes"]["creation_date"] + assert data[0]["attributes"]["last_analysis_results"] + assert data[0]["attributes"]["last_analysis_stats"] + assert data[0]["attributes"]["last_dns_records"] + assert data[0]["attributes"]["last_https_certificate"] + assert data[0]["attributes"]["total_votes"] + + +def test_metadata_v3(vtotal_v3): + """ + Test retrieving metadata from the VirusTotal API. + """ + resp = vtotal_v3.request("metadata") + assert resp.status_code == 200 + engines_dict = resp.data["engines"] + assert engines_dict.keys() + assert "Microsoft" in engines_dict.keys() diff --git a/virustotal_python/virustotal.py b/virustotal_python/virustotal.py index a50ef51..83ae236 100644 --- a/virustotal_python/virustotal.py +++ b/virustotal_python/virustotal.py @@ -1,7 +1,7 @@ """ MIT License -Copyright (c) 2020 dbrennand (dbrennand) +Copyright (c) 2020 dbrennand Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,167 +19,366 @@ 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.""" -try: - from requests import get, post - from os.path import abspath, basename -except ImportError as err: - print(f"Failed to import required modules: {err}") +SOFTWARE. +""" +import requests +import os +from typing import Tuple +from json.decoder import JSONDecodeError + + +class VirustotalError(Exception): + """ + Class for VirusTotal API errors. + """ + + def __init__(self, response: requests.Response): + """ + Initalisation for VirustotalError class. + + :param response: A requests.Response object from a failed API request to the VirusTotal API. + """ + self.response = response + + def __str__(self): + return f"Error {self.error().get('code', 'unknown')} ({self.response.status_code}): {self.error().get('message', 'No message')}" + + def error(self) -> dict: + """ + Retrieve the error that occurred from a VirusTotal API request. + + [v3 documentation](https://developers.virustotal.com/v3.0/reference#errors) + + [v2 documentation](https://developers.virustotal.com/reference#api-responses) + + :returns: A dictionary containing the error code and message returned from the VirusTotal API (if any) otherwise, returns an empty dictionary. + """ + # Attempt to decode JSON as the v3 VirusTotal API returns the error message as JSON + try: + return self.response.json().get("error", dict()) + except ValueError: + # Catch exception if there is no JSON to be deserialized + # Most likely using the v2 VirusTotal API + # Check there is response text, if not, return an empty dict + if self.response.text: + return dict(message=self.response.text) + else: + return dict() + + +class VirustotalResponse(object): + """ + Response class for VirusTotal API requests. + """ + + def __init__(self, response: requests.Response): + """ + Initalisation for VirustotalResponse class. + + :param response: A requests.Response object from a successfull API request to the VirusTotal API. + """ + self.response = response + + @property + def headers(self) -> dict: + """ + Retrieve the HTTP headers of a VirusTotal API request. + + :returns: The HTTP headers of the requests.Response object. + """ + return self.response.headers + + @property + def status_code(self) -> int: + """ + Retrieve the HTTP status code of a VirusTotal API request. + + :returns: The HTTP status code of the requests.Response object. + """ + return self.response.status_code + + @property + def text(self) -> str: + """ + Retrieve the HTTP text response of a VirusTotal API request. + + :returns: The HTTP text response of the requests.Response object. + """ + return self.response.text + + @property + def requests_response(self) -> requests.Response: + """ + Retrieve the HTTP requests.Response object of a VirusTotal API request. + You may want to access this property if you wanted to read other aspects of the response such as cookies. + + :returns: A requests.Response object. + """ + return self.response + + @property + def links(self) -> Tuple[dict, None]: + """ + Retrieve the value of the key 'links' in the JSON response from a VirusTotal API request. + + NOTE: Links are not retrieved for objects inside 'data'. + + [v3 documentation](https://developers.virustotal.com/v3.0/reference#collections) + + :returns: A dictionary containing the links used to retrieve the next set of objects (if any), otherwise, returns None. + """ + return self.json().get("links", None) + + @property + def meta(self) -> Tuple[dict, None]: + """ + Retrieve the value of the key 'meta' in the JSON response from a VirusTotal API request. + + [v3 documentation](https://developers.virustotal.com/v3.0/reference#collections) + + :returns: A dictionary containing metadata about the object(s) (if any), otherwise, returns None. + """ + return self.json().get("meta", None) + + @property + def cursor(self) -> Tuple[str, None]: + """ + Retrieve the value of the key 'cursor' in the JSON response value 'meta' from a VirusTotal API request. + + [v3 documentation](https://developers.virustotal.com/v3.0/reference#collections) + + :returns: A string representing the cursor used to retrieve additional related object(s), otherwise, returns None. + """ + try: + return self.meta.get("cursor", None) + # Catch AttributeError that occurs when attemping to call attribute 'get' on None + # which is returned if the 'meta' key is not present in the JSON response + except AttributeError: + return None + + @property + def data(self) -> Tuple[dict, list, None]: + """ + Retrieve the value of the key 'data' in the JSON response from a VirusTotal API request. + + [v3 documentation](https://developers.virustotal.com/v3.0/reference#objects) + + :returns: A dictionary or list depending on the number of objects returned from the VirusTotal API (if any) otherwise, returns None. + """ + return self.json().get("data", None) + + @property + def object_type(self) -> Tuple[list, str, None]: + """ + Retrieve the object type(s) in the JSON response from a VirusTotal API request. + + [v3 documentation](https://developers.virustotal.com/v3.0/reference#objects) + + [More v3 documentation](https://developers.virustotal.com/v3.0/reference#collections) + + :returns: A list or string depending on the number of objects returned from the VirusTotal API (if any) otherwise, returns None. + """ + data = self.data + # Check if data is more than one object + if isinstance(data, list): + object_list = [] + for data_object in data: + data_object_type = data_object.get("type", None) + object_list.append(data_object_type) + return object_list + elif isinstance(data, dict): + return data.get("type", None) + else: + return None + + @property + def response_code(self) -> Tuple[int, None]: + """ + Retrieve the value of the key 'response_code' in the JSON response from a VirusTotal v2 API request. + + [v2 documentation](https://developers.virustotal.com/reference#api-responses) + + :returns: An int of the response_code from the VirusTotal API (if any), otherwise, returns None. + """ + return self.json().get("response_code", None) + + def json(self, **kwargs) -> Tuple[dict, list]: + """ + Retrieve the JSON response of a VirusTotal API request. + + :param **kwargs: Parameters to pass to json. Identical to `json.loads(**kwargs)`. + :returns: JSON response of the requests.Response object. + :raises ValueError: Raises ValueError when the response body contains invalid JSON. + """ + try: + return self.response.json(**kwargs) + except JSONDecodeError: + return dict() class Virustotal(object): """ - Base class for interacting with the Virustotal Public API. (https://www.virustotal.com/en/documentation/public-api/) + Interact with the public VirusTotal v2 and v3 APIs. + + [v2 documentation](https://www.virustotal.com/en/documentation/public-api/) + + [v3 documentation](https://developers.virustotal.com/v3.0/reference) """ - def __init__(self, API_KEY: str = None, PROXIES: dict = None): + def __init__( + self, + API_KEY: str = os.environ.get("VIRUSTOTAL_API_KEY", None), + API_VERSION: str = "v2", + COMPATIBILITY_ENABLED: bool = False, + PROXIES: dict = None, + TIMEOUT: float = None, + ): + """ + Initalisation function for the Virustotal class. + + :param API_KEY: The API key used to interact with the VirusTotal v2 and v3 APIs. Alternatively, the environment variable `VIRUSTOTAL_API_KEY` can be provided. + :param API_VERSION: The version to use when interacting with the VirusTotal API. This parameter defaults to 'v2' for backwards compatibility. + :param COMPATIBILITY_ENABLED: Preserve the old response format of virustotal-python versions prior to 0.1.0 for backwards compatibility. + :param PROXIES: A dictionary containing proxies used when making requests. + :param TIMEOUT: A float for the amount of time to wait in seconds for the HTTP request before timing out. + :raises ValueError: Raises ValueError when no API_KEY is provided or the API_VERSION is invalid. + """ + self.VERSION = "0.1.0" + if API_KEY is None: + raise ValueError( + "An API key is required to interact with the VirusTotal API.\nProvide one to the API_KEY parameter or by setting the environment variable 'VIRUSTOTAL_API_KEY'." + ) self.API_KEY = API_KEY + self.COMPATIBILITY_ENABLED = COMPATIBILITY_ENABLED self.PROXIES = PROXIES - self.BASEURL = "https://www.virustotal.com/vtapi/v2/" - self.VERSION = "0.0.9" - self.headers = { - "Accept-Encoding": "gzip, deflate", - "User-Agent": f"gzip, virustotal-python {self.VERSION}", - } - if API_KEY is None: + self.TIMEOUT = TIMEOUT + # Declare appropriate variables depending on the API_VERSION provided + if API_VERSION == "v2": + self.API_VERSION = API_VERSION + self.BASEURL = "https://www.virustotal.com/vtapi/v2/" + self.HEADERS = { + "Accept-Encoding": "gzip, deflate", + "User-Agent": f"gzip, virustotal-python {self.VERSION}", + } + elif API_VERSION == "v3": + self.API_VERSION = API_VERSION + self.BASEURL = "https://www.virustotal.com/api/v3/" + self.HEADERS = { + "Accept-Encoding": "gzip, deflate", + "User-Agent": f"gzip, virustotal-python {self.VERSION}", + "x-apikey": f"{self.API_KEY}", + } + else: raise ValueError( - "An API_KEY is required to interact with the VirusTotal API." + f"The API version '{API_VERSION}' is not a valid VirusTotal API version.\nValid API versions are 'v2' or 'v3'." ) - def file_scan(self, file): - """ - Send a file to Virustotal for analysis. (https://developers.virustotal.com/v2.0/reference#file-scan) - :param file: The path to the file to be sent to Virustotal for analysis. - :rtype: A dictionary containing the resp_code and JSON response. - """ - params = {"apikey": self.API_KEY} - files = {"file": (basename(file), open(abspath(file), "rb"))} - resp = self.make_request( - f"{self.BASEURL}file/scan", params=params, files=files, proxies=self.PROXIES - ) - return resp - - def file_rescan(self, *resource: list): - """ - Resend a file to Virustotal for analysis. (https://developers.virustotal.com/v2.0/reference#file-rescan) - :param *resource: A list of resource(s) of a specified file(s). Can be `md5/sha1/sha256 hashes`. Can be a combination of any of the three allowed hashes (MAX 25 items). - :rtype: A dictionary containing the resp_code and JSON response. - """ - raise DeprecationWarning("VirusTotal removed this API endpoint from the public API.") - - def file_report(self, *resource: list): - """ - Retrieve scan report(s) for a given file from Virustotal. (https://developers.virustotal.com/v2.0/reference#file-report) - :param *resource: A list of resource(s) of a specified file(s). Can be `md5/sha1/sha256 hashes` and/or combination of hashes and scan_ids (MAX 4 per standard request rate). - :rtype: A dictionary containing the resp_code and JSON response. - """ - params = {"apikey": self.API_KEY, "resource": ",".join(*resource)} - resp = self.make_request( - f"{self.BASEURL}file/report", - params=params, - method="GET", - proxies=self.PROXIES, - ) - return resp - - def url_scan(self, *url: list): - """ - Send url(s) to Virustotal. (https://developers.virustotal.com/v2.0/reference#url-scan) - :param *url: A list of url(s) to be scanned. (MAX 4 per standard request rate). - :rtype: A dictionary containing the resp_code and JSON response. - """ - params = {"apikey": self.API_KEY, "url": "\n".join(*url)} - resp = self.make_request( - f"{self.BASEURL}url/scan", params=params, proxies=self.PROXIES - ) - return resp - - def url_report(self, *resource: list, scan: int = None): - """ - Retrieve scan report(s) for a given url(s) (https://developers.virustotal.com/v2.0/reference#url-report) - :param *resource: A list of the url(s) and/or scan_id(s) report(s) to be retrieved (MAX 4 per standard request rate). - :param scan: An optional parameter. When set to 1 it will automatically submit the URL for analysis if no report is found for it in VirusTotal's database. - :rtype: A dictionary containing the resp_code and JSON response. - """ - params = {"apikey": self.API_KEY, "resource": "\n".join(*resource)} - if scan is not None: - params["scan"] = scan - resp = self.make_request( - f"{self.BASEURL}url/report", params=params, proxies=self.PROXIES - ) - return resp - - def ipaddress_report(self, ip: str): - """ - Retrieve a scan report for a specific ip address. (https://developers.virustotal.com/v2.0/reference#ip-address-report) - :param ip: A valid IPV4 address in dotted quad notation. - :rtype: A dictionary containing the resp_code and JSON response. - """ - params = {"apikey": self.API_KEY, "ip": ip} - resp = self.make_request( - f"{self.BASEURL}ip-address/report", - params=params, - method="GET", - proxies=self.PROXIES, - ) - return resp - - def domain_report(self, domain: str): - """ - Retrieve a scan report for a specific domain name. (https://developers.virustotal.com/v2.0/reference#domain-report) - :param domain: A domain name. - :rtype: A dictionary containing the resp_code and JSON response. - """ - params = {"apikey": self.API_KEY, "domain": domain} - resp = self.make_request( - f"{self.BASEURL}domain/report", - params=params, - method="GET", - proxies=self.PROXIES, - ) - return resp - - def put_comment(self, resource: str, comment: str): - """ - Make comments on files and URLs. (https://developers.virustotal.com/v2.0/reference#comments-put) - :param resource: The `md5/sha1/sha256 hash` of the file you want to review or the URL itself that you want to comment on. - :param comment: The str comment to be submitted. - :rtype: A dictionary containing the resp_code and JSON response. - """ - params = {"apikey": self.API_KEY, "resource": resource, "comment": comment} - resp = self.make_request( - f"{self.BASEURL}comments/put", params=params, proxies=self.PROXIES - ) - return resp - - def make_request(self, endpoint: str, params: dict, method="POST", **kwargs): - """ - Helper function to make the request to the specified endpoint. - :param endpoint: The specific Virustotal API endpoint. - :param method: The request method to use. - :param params: The parameters to go along with the request. - :rtype: A dictionary containing the resp_code and JSON response. - """ - if method == "POST": - resp = post(endpoint, params=params, headers=self.headers, **kwargs) - elif method == "GET": - resp = get(endpoint, params=params, headers=self.headers, **kwargs) + def request( + self, + resource: str, + params: dict = {}, + data: dict = None, + json: dict = None, + files: dict = None, + method: str = "GET", + ) -> Tuple[dict, VirustotalResponse]: + """ + Make a request to the VirusTotal API. + + :param resource: A valid VirusTotal API endpoint. (E.g. 'files/{id}') + :param params: A dictionary containing API endpoint query parameters. + :param data: A dictionary containing the data to send in the body of the request. + :param json: A dictionary containing the JSON payload to send with the request. + :param files: A dictionary containing the file for multipart encoding upload. (E.g: {'file': ('filename', open('filename.txt', 'rb'))}) + :param method: The request method to use. + :returns: A dictionary containing the HTTP response code (resp_code) and JSON response (json_resp) if self.COMPATIBILITY_ENABLED is True. + Otherwise, a VirustotalResponse class object is returned. If a HTTP status not equal to 200 occurs. Then a VirustotalError class object is returned. + :raises Exception: Raise Exception when an unsupported method is provided. + """ + # Create API endpoint + endpoint = f"{self.BASEURL}{resource}" + # If API version being used is v2, add the API key to params + if self.API_VERSION == "v2": + params["apikey"] = self.API_KEY + if method == "GET": + response = requests.get( + endpoint, + params=params, + data=data, + json=json, + files=files, + headers=self.HEADERS, + proxies=self.PROXIES, + timeout=self.TIMEOUT, + ) + elif method == "POST": + response = requests.post( + endpoint, + params=params, + data=data, + json=json, + files=files, + headers=self.HEADERS, + proxies=self.PROXIES, + timeout=self.TIMEOUT, + ) + elif method == "PATCH": + response = requests.patch( + endpoint, + params=params, + data=data, + json=json, + files=files, + headers=self.HEADERS, + proxies=self.PROXIES, + timeout=self.TIMEOUT, + ) + elif method == "DELETE": + response = requests.delete( + endpoint, + params=params, + data=data, + json=json, + files=files, + headers=self.HEADERS, + proxies=self.PROXIES, + timeout=self.TIMEOUT, + ) else: - raise ValueError("Invalid request method.") - return self.validate_response(resp) + raise Exception(f"The request method '{method}' is not supported.") + # Validate response and return it + return self.validate_response(response) - def validate_response(self, response): + def validate_response( + self, response: requests.Response + ) -> Tuple[dict, VirustotalResponse]: """ - Helper function to validate the response request produced from make_request(). - :param response: The requests response object. - :rtype: A dictionary containing the resp_code and JSON response. + Helper function to validate the request response. + + :param response: A requests.Response object for an API request made to the VirusTotal API. + :returns: A dictionary containing the HTTP response code (resp_code) and JSON response (json_resp) if self.COMPATIBILITY_ENABLED is True otherwise, a VirustotalResponse class object is returned. + :raises VirustotalError: Raises VirustotalError when an HTTP status code other than 200 (successfull) occurs. """ - if response.status_code == 200: - json_resp = response.json() - return dict(status_code=response.status_code, json_resp=json_resp) + if self.COMPATIBILITY_ENABLED: + if response.status_code == 200: + json_resp = response.json() + return dict(status_code=response.status_code, json_resp=json_resp) + else: + # An error has occurred + # The v3 API returns the error as JSON, attempt to retrieve it + try: + error_json = response.json() + except ValueError: + # API version being used is likely to be v2. Catch the raised ValueError and continue + pass + return dict( + status_code=response.status_code, + # Provide JSON error message if retrieved successfully, otherwise fallback on response.text + error=(error_json if error_json else response.text), + resp=response.content, + ) else: - return dict( - status_code=response.status_code, - error=response.text, - resp=response.content, - ) + if response.status_code != 200: + raise VirustotalError(response) + else: + return VirustotalResponse(response)