├── .gitignore ├── LICENSE ├── README.rst ├── build.sh ├── pyldfire.py ├── requirements.txt ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | 64 | # PyCharm project files 65 | .idea/ 66 | 67 | test.py 68 | report.pdf 69 | test.exe 70 | 71 | venv/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pyldfire 2 | ======== 3 | 4 | A Python module for `Palo Alto Networks\` WildFire API`_ 5 | 6 | :: 7 | 8 | Copyright 2016 Sean Whalen 9 | 10 | Licensed under the Apache License, Version 2.0 (the "License"); 11 | you may not use this file except in compliance with the License. 12 | You may obtain a copy of the License at 13 | 14 | http://www.apache.org/licenses/LICENSE-2.0 15 | 16 | Unless required by applicable law or agreed to in writing, software 17 | distributed under the License is distributed on an "AS IS" BASIS, 18 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | See the License for the specific language governing permissions and 20 | limitations under the License. 21 | 22 | Features 23 | -------- 24 | 25 | - Python 2 and 3 support 26 | - Returns native Python objects 27 | - Raises exceptions on API errors with error details 28 | - Supports HTTPS proxies and SSL/TLS validation 29 | - Supports WildFire cloud or appliance 30 | - Supports all WildFire 8.1 API calls 31 | 32 | - Uploading sample files and URLs 33 | - Getting verdicts 34 | - Getting full reports in PDF or dictionary formats 35 | - Getting samples 36 | - Getting PCAPs 37 | - Getting a malware test file 38 | 39 | Examples 40 | -------- 41 | 42 | :: 43 | 44 | json import dumps 45 | from io import BytesIO 46 | 47 | from pyldfire import WildFire 48 | 49 | printer = PrettyPrinter(indent=2) 50 | 51 | wildfire = WildFire("api-key-goes-here") 52 | 53 | # Submit a local file 54 | with open("malware", "rb") as sample_file: 55 | results = wildfire.submit_file(sample_file) 56 | dumps(results) 57 | 58 | # File Hashes can be MD5,SHA1, or SHA256 59 | file_hash = "419251150a2f77422efa1e016d605d69" 60 | 61 | # Download a sample to a file 62 | with open("sample", "wb") as sample_file: 63 | sample_file.write(wildfire.get_sample(file_hash)) 64 | 65 | # Or keep it as a file-like object in memory instead 66 | sample = BytesIO(wildfire.get_sample(file_hash)) 67 | 68 | # Same for PCAPs and PDF reports 69 | 70 | # Get a verdict 71 | verdict = wildfire.get_verdicts([file_hash]) 72 | 73 | # Get analysis results 74 | results = wildfire.get_report(file_hash) 75 | 76 | # Test your firewall 77 | wildfire.get_malware_test_file() 78 | 79 | pyldfire.WildFire methods 80 | ------------------------- 81 | 82 | ``__init__(self, api_key, host='wildfire.paloaltonetworks.com', proxies=None, verify=True)`` 83 | 84 | Initializes the WildFire class 85 | 86 | :: 87 | 88 | Args: 89 | api_key (str): A WildFire API Key 90 | host (str): The hostname of the WildFire service or appliance 91 | proxies (dict): An optional dictionary containing proxy data, 92 | with https as the key, and the proxy path as the value 93 | verify (bool): Verify the certificate 94 | verify (str): A path to a CA cert bundle 95 | 96 | ``get_malware_test_file(self)`` 97 | 98 | Gets a unique, benign malware test file that will trigger an alert on 99 | Palo Alto Networks’ firewalls 100 | 101 | :: 102 | 103 | Returns: 104 | bytes: A malware test file 105 | 106 | ``get_pcap(self, file_hash, platform=None)`` 107 | 108 | Gets a PCAP from a sample analysis 109 | 110 | :: 111 | 112 | Args: 113 | file_hash (str): A hash of a sample 114 | platform (int): One of the following integers: 115 | 116 | WildFire Private and Global Cloud 117 | 118 | 1: Windows XP, Adobe Reader 9.3.3, Office 2003 119 | 2: Windows XP, Adobe Reader 9.4.0, Flash 10, Office 2007 120 | 3: Windows XP, Adobe Reader 11, Flash 11, Office 2010 121 | 4: Windows 7 32-bit, Adobe Reader 11, Flash 11, Office 2010 122 | 5: Windows 7 64-bit, Adobe Reader 11, Flash 11, Office 2010 123 | 100: PDF Static Analyzer 124 | 101: DOC/CDF Static Analyzer 125 | 102: Java/Jar Static Analyzer 126 | 103: Office 2007 Open XML Static Analyzer 127 | 104: Adobe Flash Static Analyzer 128 | 204: PE Static Analyzer 129 | 130 | WildFire Global Cloudonly 131 | 132 | 6: Windows XP, Internet Explorer 8, Flash 11 133 | 20: Windows XP, Adobe Reader 9.4.0, Flash 10, Office 2007 134 | 21: Windows 7, Flash 11, Office 2010 135 | 50: Mac OSX Mountain Lion 136 | 60: Windows XP, Adobe Reader 9.4.0, Flash 10, Office 2007 137 | 61: Windows 7 64-bit, Adobe Reader 11, Flash 11, Office 2010 138 | 66: Windows 10 64-bit, Adobe Reader 11, Flash 22, Office 2010 139 | 105: RTF Static Analyzer 140 | 110: Max OSX Static Analyzer 141 | 200: APK Static Analyzer 142 | 201: Android 2.3, API 10, avd2.3.1 143 | 202: Android 4.1, API 16, avd4.1.1 X86 144 | 203: Android 4.1, API 16, avd4.1.1 ARM 145 | 205: Phishing Static Analyzer 146 | 206: Android 4.3, API 18, avd4.3 ARM 147 | 300: Windows XP, Internet Explorer 8, Flash 13.0.0.281, Flash 148 | 16.0.0.305, Elink Analyzer 149 | 301: Windows 7, Internet Explorer 9, Flash 13.0.0.281, Flash 150 | 17.0.0.169, Elink Analyzer 151 | 302: Windows 7, Internet Explorer 10, Flash 16.0.0.305, Flash 152 | 17.0.0.169, Elink Analyzer 153 | 303: Windows 7, Internet Explorer 11, Flash 16.0.0.305, Flash 154 | 17.0.0.169, Elink Analyzer 155 | 400: Linux (ELF Files) 156 | 501: BareMetal Windows 7 x64, Adobe Reader 11, Flash 11, 157 | Office 2010 158 | 800: Archives (RAR and 7-Zip files) 159 | Returns: 160 | bytes: The PCAP 161 | 162 | Raises: 163 | WildFireException: If an API error occurs 164 | 165 | ``get_pdf_report(self, file_hash)`` 166 | 167 | Gets analysis results as a PDF 168 | 169 | :: 170 | 171 | Args: 172 | file_hash: A hash of a sample of a file 173 | 174 | Returns: 175 | bytes: The PDF 176 | 177 | Raises: 178 | WildFireException: If an API error occurs 179 | 180 | ``get_report(self, file_hash)`` 181 | 182 | Gets analysis results as structured data 183 | 184 | :: 185 | 186 | Args: 187 | file_hash (str): A hash of a sample 188 | 189 | Returns: 190 | dict: Analysis results 191 | 192 | Raises: 193 | WildFireException: If an API error occurs 194 | 195 | ``get_sample(self, file_hash)`` 196 | 197 | Gets a sample file 198 | 199 | :: 200 | 201 | Args: 202 | file_hash (str): A hash of a sample 203 | 204 | Returns: 205 | bytes: The sample 206 | 207 | Raises: 208 | WildFireException: If an API error occurs 209 | 210 | ``get_verdicts(self, file_hashes)`` 211 | 212 | Gets the verdict for one or more samples 213 | 214 | :: 215 | 216 | Args: 217 | file_hashes (list): A list of file hash strings 218 | file_hashes (str): A single file hash 219 | 220 | Returns: 221 | str: If a single file hash is passed, a string containing the verdict 222 | list: If multiple hashes a passed, a list of corresponding list of verdict strings 223 | 224 | Possible values: 225 | 226 | 'benign' 227 | 'malware' 228 | 'greyware' 229 | 'phishing' 230 | 'pending` 231 | 'error' 232 | 'not found` 233 | 234 | Raises: 235 | WildFireException: If an API error occurs 236 | 237 | ``change_sample_verdict(self, sha256_hash, verdict, comment)`` 238 | 239 | Change a sample's verdict 240 | 241 | :: 242 | Notes: 243 | Available on WildFire appliances only 244 | 245 | Args: 246 | sha256_hash (str): The SHA-256 hash of the sample 247 | verdict (str): The new verdict to set 248 | verdict (int): The new verdict to set 249 | comment (str): A comment describing the reason for the verdict change 250 | 251 | Returns: 252 | str: A response message 253 | 254 | Raises: 255 | WildFireException: If an API error occurs 256 | 257 | ``get_changed_verdicts(self, date)`` 258 | 259 | Returns a list of samples with changed WildFire appliance verdicts 260 | 261 | :: 262 | 263 | Args: 264 | date (str): A starting date in ``YYY-MM-DD`` format 265 | 266 | Notes: 267 | This feature is only available on WildFire appliances. 268 | Changed verdicts can only be obtained for the past 14 days. 269 | 270 | Returns: 271 | list: A list of samples with changed WildFire appliance verdicts 272 | 273 | ``submit_file(self, file_obj, filename="sample")`` 274 | 275 | Submits a file to WildFire for analysis 276 | 277 | :: 278 | 279 | Args: 280 | file_obj (file): The file to send 281 | filename (str): An optional filename 282 | 283 | Returns: 284 | dict: Analysis results 285 | 286 | Raises: 287 | WildFireException: If an API error occurs 288 | 289 | 290 | ``submit_remote_file(self, url)`` 291 | 292 | Submits a file from a remote URL for analysis 293 | 294 | :: 295 | 296 | Args: 297 | url (str): The URL where the file is located 298 | 299 | Returns: 300 | dict: Analysis results 301 | 302 | Raises: 303 | WildFireException: If an API error occurs 304 | 305 | Notes: 306 | This is for submitting files located at remote URLs, not web pages. 307 | 308 | See Also: 309 | submit_urls(self, urls) 310 | 311 | ``submit_urls(self, urls)`` 312 | 313 | Submits one or more URLs to a web page for analysis 314 | 315 | :: 316 | 317 | Args: 318 | urls (str): A single URL 319 | urls (list): A list of URLs 320 | 321 | Returns: 322 | dict: If a single URL is passed, a dictionary of analysis results 323 | list: If multiple URLs are passed, a list of corresponding dictionaries containing analysis results 324 | 325 | Raises: 326 | WildFireException: If an API error occurs 327 | 328 | .. _Palo Alto Networks\` WildFire API: https://www.paloaltonetworks.com/documentation/81/wildfire/wf_api 329 | 330 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | . venv/bin/activate 6 | 7 | pip install -U -r requirements.txt 8 | rstcheck --report warning README.rst 9 | rm -rf dist/ build/ 10 | python3 setup.py sdist 11 | python3 setup.py bdist_wheel 12 | -------------------------------------------------------------------------------- /pyldfire.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """A Python module for Palo Alto Networks' WildFire API 3 | 4 | Copyright 2016 Sean Whalen 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | """ 18 | 19 | from io import BytesIO 20 | 21 | from requests import Session 22 | import xmltodict 23 | 24 | __author__ = 'Sean Whalen' 25 | __version__ = '9.0' 26 | 27 | 28 | def _list_to_file(l): 29 | """Converts a list to a BytesIO object. One item per line""" 30 | return BytesIO('\n'.join(l)) 31 | 32 | 33 | class WildFireException(RuntimeError): 34 | """This exception is raised when an API error occurs""" 35 | pass 36 | 37 | 38 | class WildFire(object): 39 | _errors = { 40 | 401: "API key is invalid", 41 | 403: "Permission denied. This can occur when attempting to " 42 | "download benign or greyware samples.", 43 | 404: "Not found", 44 | 405: "Method other than POST used", 45 | 413: "Sample file size over max limit", 46 | 418: "Sample file type is not supported", 47 | 419: "Max calls per day reached", 48 | 421: "Invalid argument", 49 | 500: "Internal WildFire error", 50 | 513: "File upload failed" 51 | } 52 | 53 | _verdicts = { 54 | 0: "benign", 55 | 1: "malware", 56 | 2: "greyware", 57 | 4: "phishing", 58 | -100: "pending", 59 | -101: "error", 60 | -102: "not found" 61 | } 62 | 63 | _verdict_ids = { 64 | "benign": 0, 65 | "malware": 1, 66 | "greyware": 2, 67 | "phishing": 4, 68 | "pending": -100, 69 | "error": -101, 70 | "not found": -102 71 | } 72 | 73 | @staticmethod 74 | def _raise_errors(response, *args, **kwargs): 75 | """Requests response processing hook""" 76 | if response.headers['content-type'].lower() == "text/xml" and len( 77 | response.text) > 0: 78 | results = xmltodict.parse(response.text) 79 | if "error" in results.keys(): 80 | raise WildFireException(results["error"]["error-message"]) 81 | if response.status_code != 200: 82 | raise WildFireException(WildFire._errors[response.status_code]) 83 | 84 | def __init__(self, api_key, host="wildfire.paloaltonetworks.com", 85 | proxies=None, verify=True): 86 | """Initializes the WildFire class 87 | 88 | Args: 89 | api_key (str): A WildFire API Key 90 | host (str): The hostname of the WildFire service or appliance 91 | proxies (dict): An optional dictionary containing proxy data, with 92 | https as the key, and the proxy path 93 | as the value 94 | verify (bool): Verify the certificate 95 | verify (str): A path to a CA cert bundle 96 | """ 97 | 98 | self.api_key = api_key 99 | self.host = host 100 | self.api_root = "https://{0}{1}".format(self.host, "/publicapi") 101 | self.session = Session() 102 | self.session.proxies = proxies 103 | self.session.verify = verify 104 | self.session.hooks = dict(response=WildFire._raise_errors) 105 | self.session.headers.update({"User-Agent": "pyldfire/{0}".format( 106 | __version__)}) 107 | 108 | def get_verdicts(self, file_hashes): 109 | """Gets the verdict for one or more samples 110 | 111 | Args: 112 | file_hashes (list): A list of file hash strings 113 | file_hashes (str): A single file hash 114 | 115 | Returns: 116 | str: If a single file hash is passed, a string containing the 117 | verdict 118 | list: If multiple hashes a passed, a list of corresponding list of 119 | verdict strings 120 | 121 | Possible values: 122 | 123 | 'benign' 124 | 'malware' 125 | 'greyware' 126 | 'phishing' 127 | 'pending` 128 | 'rrror' 129 | 'not found` 130 | 131 | Raises: 132 | WildFireException: If an API error occurs 133 | """ 134 | 135 | multi = False 136 | if type(file_hashes) == list: 137 | if len(file_hashes) == 1: 138 | file_hashes = file_hashes[0] 139 | elif len(file_hashes) > 1: 140 | multi = True 141 | if multi: 142 | request_url = "{0}{1}".format(self.api_root, "/get/verdicts") 143 | hash_file = _list_to_file(file_hashes) 144 | files = dict(file=("hashes", hash_file)) 145 | data = dict(apikey=self.api_key) 146 | response = self.session.post(request_url, data=data, files=files) 147 | results = xmltodict.parse( 148 | response.text)['wildfire']['get-verdict-info'] 149 | for i in range(len(results)): 150 | results[i]["verdict"] = WildFire._verdicts[int( 151 | results[i]["verdict"])] 152 | results = list(map(lambda result: result["verdict"], results)) 153 | else: 154 | request_url = "{0}{1}".format(self.api_root, "/get/verdict") 155 | data = dict(apikey=self.api_key, hash=file_hashes) 156 | response = self.session.post(request_url, data=data) 157 | verdict = int(xmltodict.parse( 158 | response.text)['wildfire']['get-verdict-info']['verdict']) 159 | results = WildFire._verdicts[verdict] 160 | 161 | return results 162 | 163 | def change_sample_verdict(self, sha256_hash, verdict, comment): 164 | """ 165 | Change a sample's verdict 166 | 167 | Notes: 168 | Available on WildFire appliances only 169 | 170 | Args: 171 | sha256_hash (str): The SHA-256 hash of the sample 172 | verdict (str): The new verdict to set 173 | verdict (int): The new verdict to set 174 | comment (str): A comment describing the reason for the verdict 175 | change 176 | 177 | Returns: 178 | str: A response message 179 | 180 | Raises: 181 | WildFireException: If an API error occurs 182 | """ 183 | 184 | if type(verdict) != int: 185 | verdict = verdict.lower() 186 | verdict = self._verdict_ids[verdict] 187 | request_url = "{0}{1}".format(self.api_root, "/get/verdict") 188 | data = dict(apikey=self.api_key, hash=sha256_hash, 189 | verdict=verdict, comment=comment) 190 | response = self.session.post(request_url, data=data) 191 | results = xmltodict.parse(response)["wildfire"]["body"] 192 | 193 | return results 194 | 195 | def get_changed_verdicts(self, date): 196 | """ 197 | Returns a list of samples with changed WildFire appliance verdicts 198 | 199 | Args: 200 | date (str): A starting date in ``YYY-MM-DD`` format 201 | 202 | Notes: 203 | This feature is only available on WildFire appliances. 204 | Changed verdicts can only be obtained for the past 14 days. 205 | 206 | Returns: 207 | list: A list of samples with changed WildFire appliance verdicts 208 | 209 | """ 210 | request_url = "{0}{1}".format(self.api_root, "/get/verdicts") 211 | data = dict(apikey=self.api_key, date=date) 212 | response = self.session.post(request_url, data=data) 213 | results = xmltodict.parse( 214 | response.text)['wildfire'] 215 | results = list(map(lambda r: r["get-verdict-info"], results)) 216 | for result in results: 217 | result["verdict"] = self._verdicts[result["verdict"]] 218 | 219 | return results 220 | 221 | def submit_file(self, file_obj, filename="sample"): 222 | """Submits a file to WildFire for analysis 223 | 224 | Args: 225 | file_obj (file): The file to send 226 | filename (str): An optional filename 227 | 228 | Returns: 229 | dict: Analysis results 230 | 231 | Raises: 232 | WildFireException: If an API error occurs 233 | """ 234 | 235 | url = "{0}{1}".format(self.api_root, "/submit/file") 236 | data = dict(apikey=self.api_key) 237 | files = dict(file=(filename, file_obj)) 238 | response = self.session.post(url, data=data, files=files) 239 | 240 | return xmltodict.parse(response.text)['wildfire']['upload-file-info'] 241 | 242 | def submit_remote_file(self, url): 243 | """Submits a file from a remote URL for analysis 244 | 245 | Args: 246 | url (str): The URL where the file is located 247 | 248 | Returns: 249 | dict: Analysis results 250 | 251 | Raises: 252 | WildFireException: If an API error occurs 253 | 254 | Notes: 255 | This is for submitting files located at remote URLs, not web pages. 256 | 257 | See Also: 258 | submit_urls(self, urls) 259 | """ 260 | 261 | request_url = "{0}{1}".format(self.api_root, "/submit/url") 262 | data = dict(apikey=self.api_key, url=url) 263 | response = self.session.post(request_url, data=data) 264 | 265 | return xmltodict.parse(response.text)['wildfire']['upload-file-info'] 266 | 267 | def submit_urls(self, urls): 268 | """ 269 | Submits one or more URLs to a web page for analysis 270 | 271 | Args: 272 | urls (str): A single URL 273 | urls (list): A list of URLs 274 | 275 | Returns: 276 | dict: If a single URL is passed, a dictionary of analysis results 277 | list: If multiple URLs are passed, a list of corresponding 278 | dictionaries containing analysis results 279 | 280 | Raises: 281 | WildFireException: If an API error occurs 282 | """ 283 | 284 | multi = False 285 | if type(urls) == list: 286 | if len(urls) == 1: 287 | urls = urls[0] 288 | elif len(urls) > 1: 289 | multi = True 290 | if multi: 291 | request_url = "{0}{1}".format(self.api_root, "/submit/links") 292 | url_file = _list_to_file(['panlnk'] + urls) 293 | files = dict(file=("urls", url_file)) 294 | data = dict(apikey=self.api_key) 295 | response = self.session.post(request_url, data=data, files=files) 296 | results = xmltodict.parse( 297 | response.text)['wildfire']['submit-link-info'] 298 | 299 | else: 300 | request_url = "{0}{1}".format(self.api_root, "/submit/link") 301 | data = dict(apikey=self.api_key, link=urls) 302 | response = self.session.post(request_url, data=data, files=data) 303 | results = xmltodict.parse( 304 | response.text)['wildfire']['submit-link-info'] 305 | 306 | return results 307 | 308 | def _get_report(self, file_hash, report_format, stream=False): 309 | """An internal method for retrieving analysis reports 310 | Args: 311 | file_hash (str): A hash of a sample 312 | report_format (str): either xml or pdf 313 | stream (bool): Stream the HTTP download. Useful for binary data. 314 | 315 | Returns: 316 | dict: Analysis results 317 | bytes: PDF bytes 318 | 319 | Raises: 320 | WildFireException: If an API error occurs 321 | """ 322 | 323 | request_url = "{0}{1}".format(self.api_root, "/get/report") 324 | data = dict(apikey=self.api_key, hash=file_hash, format=report_format) 325 | response = self.session.post(request_url, data=data, stream=stream) 326 | if report_format == "pdf": 327 | response = response.content 328 | else: 329 | response = xmltodict.parse(response.text)["wildfire"] 330 | 331 | return response 332 | 333 | def get_report(self, file_hash): 334 | """Gets analysis results as structured data 335 | Args: 336 | file_hash (str): A hash of a sample 337 | 338 | Returns: 339 | dict: Analysis results 340 | 341 | Raises: 342 | WildFireException: If an API error occurs 343 | """ 344 | 345 | return self._get_report(file_hash, 'xml') 346 | 347 | def get_pdf_report(self, file_hash): 348 | """Gets analysis results as a PDF 349 | Args: 350 | file_hash: A hash of a sample of a file 351 | 352 | Returns: 353 | bytes: The PDF 354 | 355 | Raises: 356 | WildFireException: If an API error occurs 357 | """ 358 | return self._get_report(file_hash, 'pdf', stream=True) 359 | 360 | def get_sample(self, file_hash): 361 | """Gets a sample file 362 | Args: 363 | file_hash (str): A hash of a sample 364 | 365 | Returns: 366 | bytes: The sample 367 | 368 | Raises: 369 | WildFireException: If an API error occurs 370 | """ 371 | request_url = "{0}{1}".format(self.api_root, "/get/sample") 372 | data = dict(apikey=self.api_key, hash=file_hash) 373 | 374 | return self.session.post(request_url, data=data, stream=True).content 375 | 376 | def get_pcap(self, file_hash, platform=None): 377 | """Gets a PCAP from a sample analysis 378 | Args: 379 | file_hash (str): A hash of a sample 380 | platform (int): One of the following integers: 381 | 382 | WildFire Private and Global Cloud 383 | 384 | 1: Windows XP, Adobe Reader 9.3.3, Office 2003 385 | 2: Windows XP, Adobe Reader 9.4.0, Flash 10, Office 2007 386 | 3: Windows XP, Adobe Reader 11, Flash 11, Office 2010 387 | 4: Windows 7 32-bit, Adobe Reader 11, Flash 11, Office 2010 388 | 5: Windows 7 64-bit, Adobe Reader 11, Flash 11, Office 2010 389 | 100: PDF Static Analyzer 390 | 101: DOC/CDF Static Analyzer 391 | 102: Java/Jar Static Analyzer 392 | 103: Office 2007 Open XML Static Analyzer 393 | 104: Adobe Flash Static Analyzer 394 | 204: PE Static Analyzer 395 | 396 | WildFire Global Cloudonly 397 | 398 | 6: Windows XP, Internet Explorer 8, Flash 11 399 | 20: Windows XP, Adobe Reader 9.4.0, Flash 10, Office 2007 400 | 21: Windows 7, Flash 11, Office 2010 401 | 50: Mac OSX Mountain Lion 402 | 60: Windows XP, Adobe Reader 9.4.0, Flash 10, Office 2007 403 | 61: Windows 7 64-bit, Adobe Reader 11, Flash 11, Office 2010 404 | 66: Windows 10 64-bit, Adobe Reader 11, Flash 22, Office 2010 405 | 105: RTF Static Analyzer 406 | 110: Max OSX Static Analyzer 407 | 200: APK Static Analyzer 408 | 201: Android 2.3, API 10, avd2.3.1 409 | 202: Android 4.1, API 16, avd4.1.1 X86 410 | 203: Android 4.1, API 16, avd4.1.1 ARM 411 | 205: Phishing Static Analyzer 412 | 206: Android 4.3, API 18, avd4.3 ARM 413 | 300: Windows XP, Internet Explorer 8, Flash 13.0.0.281, Flash 414 | 16.0.0.305, Elink Analyzer 415 | 301: Windows 7, Internet Explorer 9, Flash 13.0.0.281, Flash 416 | 17.0.0.169, Elink Analyzer 417 | 302: Windows 7, Internet Explorer 10, Flash 16.0.0.305, Flash 418 | 17.0.0.169, Elink Analyzer 419 | 303: Windows 7, Internet Explorer 11, Flash 16.0.0.305, Flash 420 | 17.0.0.169, Elink Analyzer 421 | 400: Linux (ELF Files) 422 | 501: BareMetal Windows 7 x64, Adobe Reader 11, Flash 11, 423 | Office 2010 424 | 800: Archives (RAR and 7-Zip files) 425 | 426 | Returns: 427 | bytes: The PCAP 428 | 429 | Raises: 430 | WildFireException: If an API error occurs 431 | """ 432 | 433 | request_url = "{0}{1}".format(self.api_root, "/get/pcap") 434 | data = dict(apikey=self.api_key, hash=file_hash) 435 | if platform is not None: 436 | data['platform'] = platform 437 | 438 | return self.session.post(request_url, data=data, stream=True).content 439 | 440 | def get_malware_test_file(self): 441 | """Gets a unique, benign malware test file that will trigger an alert 442 | on Palo Alto Networks' firewalls 443 | 444 | Returns: 445 | bytes: A malware test file 446 | """ 447 | 448 | return self.session.get("{0}{1}".format(self.api_root, "/test/pe"), 449 | stream=True).content 450 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | rstcheck 2 | requests 3 | xmltodict 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3 | # 3. If at all possible, it is good practice to do this. If you cannot, you 4 | # will need to generate wheels for each Python version that you support. 5 | universal=1 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """A setuptools based setup module. 5 | See: 6 | https://packaging.python.org/en/latest/distributing.html 7 | https://github.com/pypa/sampleproject 8 | """ 9 | 10 | from __future__ import absolute_import 11 | 12 | # Always prefer setuptools over distutils 13 | from setuptools import setup 14 | # To use a consistent encoding 15 | from codecs import open 16 | from os import path 17 | 18 | from pyldfire import __version__ 19 | 20 | here = path.abspath(path.dirname(__file__)) 21 | 22 | # Get the long description from the README file 23 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 24 | long_description = f.read() 25 | 26 | setup( 27 | name='pyldfire', 28 | 29 | # Versions should comply with PEP440. For a discussion on single-sourcing 30 | # the version across setup.py and the project code, see 31 | # https://packaging.python.org/en/latest/single_source_version.html 32 | version=__version__, 33 | 34 | description="A Python module for Palo Alto Networks' WildFire API", 35 | long_description=long_description, 36 | 37 | # The project's main homepage. 38 | url='https://github.com/seanthegeek/pyldfire', 39 | 40 | # Author details 41 | author='Sean Whalen', 42 | author_email='whalenster@gmail.com', 43 | 44 | # Choose your license 45 | license='Apache 2.0', 46 | 47 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 48 | classifiers=[ 49 | # How mature is this project? Common values are 50 | # 3 - Alpha 51 | # 4 - Beta 52 | # 5 - Production/Stable 53 | 'Development Status :: 5 - Production/Stable', 54 | 55 | # Indicate who your project is intended for 56 | 'Topic :: Security', 57 | 'Intended Audience :: Developers', 58 | 'Intended Audience :: System Administrators', 59 | 'Operating System :: OS Independent', 60 | 61 | 62 | # Pick your license as you wish (should match "license" above) 63 | 'License :: OSI Approved :: Apache Software License', 64 | 65 | # Specify the Python versions you support here. In particular, ensure 66 | # that you indicate whether you support Python 2, Python 3 or both. 67 | 'Programming Language :: Python :: 2', 68 | 'Programming Language :: Python :: 2.6', 69 | 'Programming Language :: Python :: 2.7', 70 | 'Programming Language :: Python :: 3', 71 | 'Programming Language :: Python :: 3.3', 72 | 'Programming Language :: Python :: 3.4', 73 | 'Programming Language :: Python :: 3.5', 74 | ], 75 | 76 | # What does your project relate to? 77 | keywords='PaloAltoNetworks WildFire API Malware Sandbox', 78 | 79 | # You can just specify the packages manually here if your project is 80 | # simple. Or you can use find_packages(). 81 | # packages=find_packages(exclude=['contrib', 'docs', 'tests']), 82 | 83 | 84 | # Alternatively, if you want to distribute just a my_module.py, uncomment 85 | # this: 86 | py_modules=["pyldfire"], 87 | 88 | # List run-time dependencies here. These will be installed by pip when 89 | # your project is installed. For an analysis of "install_requires" vs pip's 90 | # requirements files see: 91 | # https://packaging.python.org/en/latest/requirements.html 92 | install_requires=['requests', 'xmltodict'], 93 | ) 94 | --------------------------------------------------------------------------------