pax_global_header00006660000000000000000000000064130337523270014517gustar00rootroot0000000000000052 comment=17c737a391f5f8b1f41d77a40da4429d739e09f5 bandit-1.4.0/000077500000000000000000000000001303375232700127625ustar00rootroot00000000000000bandit-1.4.0/.coveragerc000066400000000000000000000000751303375232700151050ustar00rootroot00000000000000[report] include = bandit/* omit = bandit/tests/functional/* bandit-1.4.0/.gitignore000066400000000000000000000002521303375232700147510ustar00rootroot00000000000000env* venv* *.pyc .DS_Store *.egg *.egg-info .eggs/ .tox .testrepository build/* cover/* .coverage* doc/build/* ChangeLog doc/source/api .*.sw? AUTHORS releasenotes/build bandit-1.4.0/.gitreview000066400000000000000000000001131303375232700147630ustar00rootroot00000000000000[gerrit] host=review.openstack.org port=29418 project=openstack/bandit.git bandit-1.4.0/.testr.conf000066400000000000000000000005051303375232700150500ustar00rootroot00000000000000[DEFAULT] test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ ${PYTHON:-python} -m subunit.run discover -t ./ ./tests $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--list bandit-1.4.0/LICENSE000066400000000000000000000236371303375232700140020ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. bandit-1.4.0/README.rst000066400000000000000000000333121303375232700144530ustar00rootroot00000000000000Bandit ====== .. image:: http://governance.openstack.org/badges/bandit.svg :target: http://governance.openstack.org/reference/tags/index.html :alt: Bandit team and repository tags .. image:: https://img.shields.io/pypi/v/bandit.svg :target: https://pypi.python.org/pypi/bandit/ :alt: Latest Version .. image:: https://img.shields.io/pypi/pyversions/bandit.svg :target: https://pypi.python.org/pypi/bandit/ :alt: Python Versions .. image:: https://img.shields.io/pypi/format/bandit.svg :target: https://pypi.python.org/pypi/bandit/ :alt: Format .. image:: https://img.shields.io/badge/license-Apache%202-blue.svg :target: https://git.openstack.org/cgit/openstack/bandit/plain/LICENSE :alt: License A security linter from OpenStack Security * Free software: Apache license * Documentation: https://wiki.openstack.org/wiki/Security/Projects/Bandit * Source: https://git.openstack.org/cgit/openstack/bandit * Bugs: https://bugs.launchpad.net/bandit Overview -------- Bandit is a tool designed to find common security issues in Python code. To do this Bandit processes each file, builds an AST from it, and runs appropriate plugins against the AST nodes. Once Bandit has finished scanning all the files it generates a report. Installation ------------ Bandit is distributed on PyPI. The best way to install it is with pip: Create a virtual environment (optional):: virtualenv bandit-env Install Bandit:: pip install bandit # Or if you're working with a Python 3.5 project pip3.5 install bandit Run Bandit:: bandit -r path/to/your/code Bandit can also be installed from source. To do so, download the source tarball from PyPI, then install it:: python setup.py install Usage ----- Example usage across a code tree:: bandit -r ~/openstack-repo/keystone Example usage across the ``examples/`` directory, showing three lines of context and only reporting on the high-severity issues:: bandit examples/*.py -n 3 -lll Bandit can be run with profiles. To run Bandit against the examples directory using only the plugins listed in the ``ShellInjection`` profile:: bandit examples/*.py -p ShellInjection Bandit also supports passing lines of code to scan using standard input. To run Bandit with standard input:: cat examples/imports.py | bandit - Usage:: $ bandit -h usage: bandit [-h] [-r] [-a {file,vuln}] [-n CONTEXT_LINES] [-c CONFIG_FILE] [-p PROFILE] [-t TESTS] [-s SKIPS] [-l] [-i] [-f {csv,html,json,screen,txt,xml}] [-o [OUTPUT_FILE]] [-v] [-d] [--ignore-nosec] [-x EXCLUDED_PATHS] [-b BASELINE] [--ini INI_PATH] [--version] targets [targets ...] Bandit - a Python source code security analyzer positional arguments: targets source file(s) or directory(s) to be tested optional arguments: -h, --help show this help message and exit -r, --recursive find and process files in subdirectories -a {file,vuln}, --aggregate {file,vuln} aggregate output by vulnerability (default) or by filename -n CONTEXT_LINES, --number CONTEXT_LINES maximum number of code lines to output for each issue -c CONFIG_FILE, --configfile CONFIG_FILE optional config file to use for selecting plugins and overriding defaults -p PROFILE, --profile PROFILE profile to use (defaults to executing all tests) -t TESTS, --tests TESTS comma-separated list of test IDs to run -s SKIPS, --skip SKIPS comma-separated list of test IDs to skip -l, --level report only issues of a given severity level or higher (-l for LOW, -ll for MEDIUM, -lll for HIGH) -i, --confidence report only issues of a given confidence level or higher (-i for LOW, -ii for MEDIUM, -iii for HIGH) -f {csv,html,json,screen,txt,xml}, --format {csv,html,json,screen,txt,xml} specify output format -o [OUTPUT_FILE], --output [OUTPUT_FILE] write report to filename -v, --verbose output extra information like excluded and included files -d, --debug turn on debug mode --ignore-nosec do not skip lines with # nosec comments -x EXCLUDED_PATHS, --exclude EXCLUDED_PATHS comma-separated list of paths to exclude from scan (note that these are in addition to the excluded paths provided in the config file) -b BASELINE, --baseline BASELINE path of a baseline report to compare against (only JSON-formatted files are accepted) --ini INI_PATH path to a .bandit file that supplies command line arguments --version show program's version number and exit The following tests were discovered and loaded: B101 assert_used B102 exec_used B103 set_bad_file_permissions B104 hardcoded_bind_all_interfaces B105 hardcoded_password_string B106 hardcoded_password_funcarg B107 hardcoded_password_default B108 hardcoded_tmp_directory B109 password_config_option_not_marked_secret B110 try_except_pass B111 execute_with_run_as_root_equals_true B112 try_except_continue B201 flask_debug_true B301 pickle B302 marshal B303 md5 B304 ciphers B305 cipher_modes B306 mktemp_q B307 eval B308 mark_safe B309 httpsconnection B310 urllib_urlopen B311 random B312 telnetlib B313 xml_bad_cElementTree B314 xml_bad_ElementTree B315 xml_bad_expatreader B316 xml_bad_expatbuilder B317 xml_bad_sax B318 xml_bad_minidom B319 xml_bad_pulldom B320 xml_bad_etree B321 ftplib B322 input B401 import_telnetlib B402 import_ftplib B403 import_pickle B404 import_subprocess B405 import_xml_etree B406 import_xml_sax B407 import_xml_expat B408 import_xml_minidom B409 import_xml_pulldom B410 import_lxml B411 import_xmlrpclib B412 import_httpoxy B501 request_with_no_cert_validation B502 ssl_with_bad_version B503 ssl_with_bad_defaults B504 ssl_with_no_version B505 weak_cryptographic_key B506 yaml_load B601 paramiko_calls B602 subprocess_popen_with_shell_equals_true B603 subprocess_without_shell_equals_true B604 any_other_function_with_shell_equals_true B605 start_process_with_a_shell B606 start_process_with_no_shell B607 start_process_with_partial_path B608 hardcoded_sql_expressions B609 linux_commands_wildcard_injection B701 jinja2_autoescape_false B702 use_of_mako_templates Configuration ------------- An optional config file may be supplied and may include: - lists of tests which should or shouldn't be run - exclude_dirs - sections of the path, that if matched, will be excluded from scanning - overridden plugin settings - may provide different settings for some plugins Per Project Command Line Args ----------------------------- Projects may include a `.bandit` file that specifies command line arguments that should be supplied for that project. The currently supported arguments are: - exclude: comma separated list of excluded paths - skips: comma separated list of tests to skip - tests: comma separated list of tests to run To use this, put a .bandit file in your project's directory. For example: :: [bandit] exclude: /test :: [bandit] tests: B101,B102,B301 Exclusions ---------- In the event that a line of code triggers a Bandit issue, but that the line has been reviewed and the issue is a false positive or acceptable for some other reason, the line can be marked with a ``# nosec`` and any results associated with it will not be reported. For example, although this line may cause Bandit to report a potential security issue, it will not be reported:: self.process = subprocess.Popen('/bin/echo', shell=True) # nosec Vulnerability Tests ------------------- Vulnerability tests or "plugins" are defined in files in the plugins directory. Tests are written in Python and are autodiscovered from the plugins directory. Each test can examine one or more type of Python statements. Tests are marked with the types of Python statements they examine (for example: function call, string, import, etc). Tests are executed by the ``BanditNodeVisitor`` object as it visits each node in the AST. Test results are maintained in the ``BanditResultStore`` and aggregated for output at the completion of a test run. Writing Tests ------------- To write a test: - Identify a vulnerability to build a test for, and create a new file in examples/ that contains one or more cases of that vulnerability. - Consider the vulnerability you're testing for, mark the function with one or more of the appropriate decorators: - @checks('Call') - @checks('Import', 'ImportFrom') - @checks('Str') - Create a new Python source file to contain your test, you can reference existing tests for examples. - The function that you create should take a parameter "context" which is an instance of the context class you can query for information about the current element being examined. You can also get the raw AST node for more advanced use cases. Please see the context.py file for more. - Extend your Bandit configuration file as needed to support your new test. - Execute Bandit against the test file you defined in examples/ and ensure that it detects the vulnerability. Consider variations on how this vulnerability might present itself and extend the example file and the test function accordingly. Extending Bandit ---------------- Bandit allows users to write and register extensions for checks and formatters. Bandit will load plugins from two entry-points: - `bandit.formatters` - `bandit.plugins` Formatters need to accept 4 things: - `result_store`: An instance of `bandit.core.BanditResultStore` - `file_list`: The list of files which were inspected in the scope - `scores`: The scores awarded to each file in the scope - `excluded_files`: The list of files that were excluded from the scope Plugins tend to take advantage of the `bandit.checks` decorator which allows the author to register a check for a particular type of AST node. For example :: @bandit.checks('Call') def prohibit_unsafe_deserialization(context): if 'unsafe_load' in context.call_function_name_qual: return bandit.Issue( severity=bandit.HIGH, confidence=bandit.HIGH, text="Unsafe deserialization detected." ) To register your plugin, you have two options: 1. If you're using setuptools directly, add something like the following to your ``setup`` call:: # If you have an imaginary bson formatter in the bandit_bson module # and a function called `formatter`. entry_points={'bandit.formatters': ['bson = bandit_bson:formatter']} # Or a check for using mako templates in bandit_mako that entry_points={'bandit.plugins': ['mako = bandit_mako']} 2. If you're using pbr, add something like the following to your `setup.cfg` file:: [entry_points] bandit.formatters = bson = bandit_bson:formatter bandit.plugins = mako = bandit_mako Contributing ------------ Contributions to Bandit are always welcome! We can be found on #openstack-security on Freenode IRC. The best way to get started with Bandit is to grab the source:: git clone https://git.openstack.org/openstack/bandit.git You can test any changes with tox:: pip install tox tox -e pep8 tox -e py27 tox -e py35 tox -e docs tox -e cover Reporting Bugs -------------- Bugs should be reported on Launchpad. To file a bug against Bandit, visit: https://bugs.launchpad.net/bandit/+filebug Under Which Version of Python Should I Install Bandit? ------------------------------------------------------ The answer to this question depends on the project(s) you will be running Bandit against. If your project is only compatible with Python 2.7, you should install Bandit to run under Python 2.7. If your project is only compatible with Python 3.5, then use 3.5 respectively. If your project supports both, you *could* run Bandit with both versions but you don't have to. Bandit uses the `ast` module from Python's standard library in order to analyze your Python code. The `ast` module is only able to parse Python code that is valid in the version of the interpreter from which it is imported. In other words, if you try to use Python 2.7's `ast` module to parse code written for 3.5 that uses, for example, `yield from` with asyncio, then you'll have syntax errors that will prevent Bandit from working properly. Alternatively, if you are relying on 2.7's octal notation of `0777` then you'll have a syntax error if you run Bandit on 3.x. References ========== Bandit wiki: https://wiki.openstack.org/wiki/Security/Projects/Bandit Python AST module documentation: https://docs.python.org/2/library/ast.html Green Tree Snakes - the missing Python AST docs: http://greentreesnakes.readthedocs.org/en/latest/ Documentation of the various types of AST nodes that Bandit currently covers or could be extended to cover: http://greentreesnakes.readthedocs.org/en/latest/nodes.html bandit-1.4.0/bandit/000077500000000000000000000000001303375232700142235ustar00rootroot00000000000000bandit-1.4.0/bandit/__init__.py000066400000000000000000000022271303375232700163370ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import pbr.version from bandit.core import config # noqa from bandit.core import context # noqa from bandit.core import manager # noqa from bandit.core import meta_ast # noqa from bandit.core import node_visitor # noqa from bandit.core import test_set # noqa from bandit.core import tester # noqa from bandit.core import utils # noqa from bandit.core.constants import * # noqa from bandit.core.issue import * # noqa from bandit.core.test_properties import * # noqa __version__ = pbr.version.VersionInfo('bandit').version_string() bandit-1.4.0/bandit/blacklists/000077500000000000000000000000001303375232700163565ustar00rootroot00000000000000bandit-1.4.0/bandit/blacklists/__init__.py000066400000000000000000000000001303375232700204550ustar00rootroot00000000000000bandit-1.4.0/bandit/blacklists/calls.py000066400000000000000000000537401303375232700200370ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2016 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ==================================================== Blacklist various Python calls known to be dangerous ==================================================== This blacklist data checks for a number of Python calls known to have possible security implications. The following blacklist tests are run against any function calls encoutered in the scanned code base, triggered by encoutering ast.Call nodes. B301: pickle ------------ Pickle library appears to be in use, possible security issue. +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | B301 | pickle | - pickle.loads | Medium | | | | - pickle.load | | | | | - pickle.Unpickler | | | | | - cPickle.loads | | | | | - cPickle.load | | | | | - cPickle.Unpickler | | +------+---------------------+------------------------------------+-----------+ B302: marshal ------------- Deserialization with the marshal module is possibly dangerous. +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | B302 | marshal | - marshal.load | Medium | | | | - marshal.loads | | +------+---------------------+------------------------------------+-----------+ B303: md5 --------- Use of insecure MD2, MD4, or MD5 hash function. +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | B303 | md5 | - hashlib.md5 | Medium | | | | - Crypto.Hash.MD2.new | | | | | - Crypto.Hash.MD4.new | | | | | - Crypto.Hash.MD5.new | | | | | - cryptography.hazmat.primitives | | | | | .hashes.MD5 | | +------+---------------------+------------------------------------+-----------+ B304 - B305: ciphers and modes ------------------------------ Use of insecure cipher or cipher mode. Replace with a known secure cipher such as AES. +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | B304 | ciphers | - Crypto.Cipher.ARC2.new | High | | | | - Crypto.Cipher.ARC4.new | | | | | - Crypto.Cipher.Blowfish.new | | | | | - Crypto.Cipher.DES.new | | | | | - Crypto.Cipher.XOR.new | | | | | - cryptography.hazmat.primitives | | | | | .ciphers.algorithms.ARC4 | | | | | - cryptography.hazmat.primitives | | | | | .ciphers.algorithms.Blowfish | | | | | - cryptography.hazmat.primitives | | | | | .ciphers.algorithms.IDEA | | +------+---------------------+------------------------------------+-----------+ | B305 | cipher_modes | - cryptography.hazmat.primitives | Medium | | | | .ciphers.modes.ECB | | +------+---------------------+------------------------------------+-----------+ B306: mktemp_q -------------- Use of insecure and deprecated function (mktemp). +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | B306 | mktemp_q | - tempfile.mktemp | Medium | +------+---------------------+------------------------------------+-----------+ B307: eval ---------- Use of possibly insecure function - consider using safer ast.literal_eval. +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | B307 | eval | - eval | Medium | +------+---------------------+------------------------------------+-----------+ B308: mark_safe --------------- Use of mark_safe() may expose cross-site scripting vulnerabilities and should be reviewed. +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | B308 | mark_safe | - django.utils.safestring.mark_safe| Medium | +------+---------------------+------------------------------------+-----------+ B309: httpsconnection --------------------- Use of HTTPSConnection does not provide security, see https://wiki.openstack.org/wiki/OSSN/OSSN-0033 +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | B309 | httpsconnection | - httplib.HTTPSConnection | Medium | | | | - http.client.HTTPSConnection | | | | | - six.moves.http_client | | | | | .HTTPSConnection | | +------+---------------------+------------------------------------+-----------+ B310: urllib_urlopen -------------------- Audit url open for permitted schemes. Allowing use of 'file:'' or custom schemes is often unexpected. +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | B310 | urllib_urlopen | - urllib.urlopen | Medium | | | | - urllib.request.urlopen | | | | | - urllib.urlretrieve | | | | | - urllib.request.urlretrieve | | | | | - urllib.URLopener | | | | | - urllib.request.URLopener | | | | | - urllib.FancyURLopener | | | | | - urllib.request.FancyURLopener | | | | | - urllib2.urlopen | | | | | - urllib2.Request | | | | | - six.moves.urllib.request.urlopen | | | | | - six.moves.urllib.request | | | | | .urlretrieve | | | | | - six.moves.urllib.request | | | | | .URLopener | | | | | - six.moves.urllib.request | | | | | .FancyURLopener | | +------+---------------------+------------------------------------+-----------+ B311: random ------------ Standard pseudo-random generators are not suitable for security/cryptographic purposes. +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | B311 | random | - random.random | Low | | | | - random.randrange | | | | | - random.randint | | | | | - random.choice | | | | | - random.uniform | | | | | - random.triangular | | +------+---------------------+------------------------------------+-----------+ B312: telnetlib --------------- Telnet-related functions are being called. Telnet is considered insecure. Use SSH or some other encrypted protocol. +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | B312 | telnetlib | - telnetlib.\* | High | +------+---------------------+------------------------------------+-----------+ B313 - B320: XML ---------------- Most of this is based off of Christian Heimes' work on defusedxml: https://pypi.python.org/pypi/defusedxml/#defusedxml-sax Using various XLM methods to parse untrusted XML data is known to be vulnerable to XML attacks. Methods should be replaced with their defusedxml equivalents. +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | B313 | xml_bad_cElementTree| - xml.etree.cElementTree.parse | Medium | | | | - xml.etree.cElementTree.iterparse | | | | | - xml.etree.cElementTree.fromstring| | | | | - xml.etree.cElementTree.XMLParser | | +------+---------------------+------------------------------------+-----------+ | B314 | xml_bad_ElementTree | - xml.etree.ElementTree.parse | Medium | | | | - xml.etree.ElementTree.iterparse | | | | | - xml.etree.ElementTree.fromstring | | | | | - xml.etree.ElementTree.XMLParser | | +------+---------------------+------------------------------------+-----------+ | B315 | xml_bad_expatreader | - xml.sax.expatreader.create_parser| Medium | +------+---------------------+------------------------------------+-----------+ | B316 | xml_bad_expatbuilder| - xml.dom.expatbuilder.parse | Medium | | | | - xml.dom.expatbuilder.parseString | | +------+---------------------+------------------------------------+-----------+ | B317 | xml_bad_sax | - xml.sax.parse | Medium | | | | - xml.sax.parseString | | | | | - xml.sax.make_parser | | +------+---------------------+------------------------------------+-----------+ | B318 | xml_bad_minidom | - xml.dom.minidom.parse | Medium | | | | - xml.dom.minidom.parseString | | +------+---------------------+------------------------------------+-----------+ | B319 | xml_bad_pulldom | - xml.dom.pulldom.parse | Medium | | | | - xml.dom.pulldom.parseString | | +------+---------------------+------------------------------------+-----------+ | B319 | xml_bad_pulldom | - xml.dom.pulldom.parse | Medium | | | | - xml.dom.pulldom.parseString | | +------+---------------------+------------------------------------+-----------+ | B320 | xml_bad_etree | - lxml.etree.parse | Medium | | | | - lxml.etree.fromstring | | | | | - lxml.etree.RestrictedElement | | | | | - lxml.etree.GlobalParserTLS | | | | | - lxml.etree.getDefaultParser | | | | | - lxml.etree.check_docinfo | | +------+---------------------+------------------------------------+-----------+ B321: ftplib ------------ FTP-related functions are being called. FTP is considered insecure. Use SSH/SFTP/SCP or some other encrypted protocol. +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | B321 | ftplib | - ftplib.\* | High | +------+---------------------+------------------------------------+-----------+ B322: input ------------ The input method in Python 2 will read from standard input, evaluate and run the resulting string as python source code. This is similar, though in many ways worse, then using eval. On Python 2, use raw_input instead, input is safe in Python 3. +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | B322 | input | - input | High | +------+---------------------+------------------------------------+-----------+ """ from bandit.blacklists import utils def gen_blacklist(): """Generate a list of items to blacklist. Methods of this type, "bandit.blacklist" plugins, are used to build a list of items that bandit's built in blacklisting tests will use to trigger issues. They replace the older blacklist* test plugins and allow blacklisted items to have a unique bandit ID for filtering and profile usage. :return: a dictionary mapping node types to a list of blacklist data """ sets = [] sets.append(utils.build_conf_dict( 'pickle', 'B301', ['pickle.loads', 'pickle.load', 'pickle.Unpickler', 'cPickle.loads', 'cPickle.load', 'cPickle.Unpickler'], 'Pickle library appears to be in use, possible security issue.' )) sets.append(utils.build_conf_dict( 'marshal', 'B302', ['marshal.load', 'marshal.loads'], 'Deserialization with the marshal module is possibly dangerous.' )) sets.append(utils.build_conf_dict( 'md5', 'B303', ['hashlib.md5', 'Crypto.Hash.MD2.new', 'Crypto.Hash.MD4.new', 'Crypto.Hash.MD5.new', 'cryptography.hazmat.primitives.hashes.MD5'], 'Use of insecure MD2, MD4, or MD5 hash function.' )) sets.append(utils.build_conf_dict( 'ciphers', 'B304', ['Crypto.Cipher.ARC2.new', 'Crypto.Cipher.ARC4.new', 'Crypto.Cipher.Blowfish.new', 'Crypto.Cipher.DES.new', 'Crypto.Cipher.XOR.new', 'cryptography.hazmat.primitives.ciphers.algorithms.ARC4', 'cryptography.hazmat.primitives.ciphers.algorithms.Blowfish', 'cryptography.hazmat.primitives.ciphers.algorithms.IDEA'], 'Use of insecure cipher {name}. Replace with a known secure' ' cipher such as AES.', 'HIGH' )) sets.append(utils.build_conf_dict( 'cipher_modes', 'B305', ['cryptography.hazmat.primitives.ciphers.modes.ECB'], 'Use of insecure cipher mode {name}.' )) sets.append(utils.build_conf_dict( 'mktemp_q', 'B306', ['tempfile.mktemp'], 'Use of insecure and deprecated function (mktemp).' )) sets.append(utils.build_conf_dict( 'eval', 'B307', ['eval'], 'Use of possibly insecure function - consider using safer ' 'ast.literal_eval.' )) sets.append(utils.build_conf_dict( 'mark_safe', 'B308', ['django.utils.safestring.mark_safe'], 'Use of mark_safe() may expose cross-site scripting ' 'vulnerabilities and should be reviewed.' )) sets.append(utils.build_conf_dict( 'httpsconnection', 'B309', ['httplib.HTTPSConnection', 'http.client.HTTPSConnection', 'six.moves.http_client.HTTPSConnection'], 'Use of HTTPSConnection does not provide security, see ' 'https://wiki.openstack.org/wiki/OSSN/OSSN-0033' )) sets.append(utils.build_conf_dict( 'urllib_urlopen', 'B310', ['urllib.urlopen', 'urllib.request.urlopen', 'urllib.urlretrieve', 'urllib.request.urlretrieve', 'urllib.URLopener', 'urllib.request.URLopener', 'urllib.FancyURLopener', 'urllib.request.FancyURLopener', 'urllib2.urlopen', 'urllib2.Request', 'six.moves.urllib.request.urlopen', 'six.moves.urllib.request.urlretrieve', 'six.moves.urllib.request.URLopener', 'six.moves.urllib.request.FancyURLopener'], 'Audit url open for permitted schemes. Allowing use of file:/ or ' 'custom schemes is often unexpected.' )) sets.append(utils.build_conf_dict( 'random', 'B311', ['random.random', 'random.randrange', 'random.randint', 'random.choice', 'random.uniform', 'random.triangular'], 'Standard pseudo-random generators are not suitable for ' 'security/cryptographic purposes.', 'LOW' )) sets.append(utils.build_conf_dict( 'telnetlib', 'B312', ['telnetlib.*'], 'Telnet-related functions are being called. Telnet is considered ' 'insecure. Use SSH or some other encrypted protocol.', 'HIGH' )) # Most of this is based off of Christian Heimes' work on defusedxml: # https://pypi.python.org/pypi/defusedxml/#defusedxml-sax xml_msg = ('Using {name} to parse untrusted XML data is known to be ' 'vulnerable to XML attacks. Replace {name} with its ' 'defusedxml equivalent function or make sure ' 'defusedxml.defuse_stdlib() is called') sets.append(utils.build_conf_dict( 'xml_bad_cElementTree', 'B313', ['xml.etree.cElementTree.parse', 'xml.etree.cElementTree.iterparse', 'xml.etree.cElementTree.fromstring', 'xml.etree.cElementTree.XMLParser'], xml_msg )) sets.append(utils.build_conf_dict( 'xml_bad_ElementTree', 'B314', ['xml.etree.ElementTree.parse', 'xml.etree.ElementTree.iterparse', 'xml.etree.ElementTree.fromstring', 'xml.etree.ElementTree.XMLParser'], xml_msg )) sets.append(utils.build_conf_dict( 'xml_bad_expatreader', 'B315', ['xml.sax.expatreader.create_parser'], xml_msg )) sets.append(utils.build_conf_dict( 'xml_bad_expatbuilder', 'B316', ['xml.dom.expatbuilder.parse', 'xml.dom.expatbuilder.parseString'], xml_msg )) sets.append(utils.build_conf_dict( 'xml_bad_sax', 'B317', ['xml.sax.parse', 'xml.sax.parseString', 'xml.sax.make_parser'], xml_msg )) sets.append(utils.build_conf_dict( 'xml_bad_minidom', 'B318', ['xml.dom.minidom.parse', 'xml.dom.minidom.parseString'], xml_msg )) sets.append(utils.build_conf_dict( 'xml_bad_pulldom', 'B319', ['xml.dom.pulldom.parse', 'xml.dom.pulldom.parseString'], xml_msg )) sets.append(utils.build_conf_dict( 'xml_bad_etree', 'B320', ['lxml.etree.parse', 'lxml.etree.fromstring', 'lxml.etree.RestrictedElement', 'lxml.etree.GlobalParserTLS', 'lxml.etree.getDefaultParser', 'lxml.etree.check_docinfo'], ('Using {name} to parse untrusted XML data is known to be ' 'vulnerable to XML attacks. Replace {name} with its ' 'defusedxml equivalent function.') )) # end of XML tests sets.append(utils.build_conf_dict( 'ftplib', 'B321', ['ftplib.*'], 'FTP-related functions are being called. FTP is considered ' 'insecure. Use SSH/SFTP/SCP or some other encrypted protocol.', 'HIGH' )) sets.append(utils.build_conf_dict( 'input', 'B322', ['input'], 'The input method in Python 2 will read from standard input, ' 'evaluate and run the resulting string as python source code. This ' 'is similar, though in many ways worse, then using eval. On Python ' '2, use raw_input instead, input is safe in Python 3.', 'HIGH' )) return {'Call': sets} bandit-1.4.0/bandit/blacklists/imports.py000066400000000000000000000277101303375232700204340ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2016 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ====================================================== Blacklist various Python imports known to be dangerous ====================================================== This blacklist data checks for a number of Python modules known to have possible security implications. The following blacklist tests are run against any import statements or calls encountered in the scanned code base. Note that the XML rules listed here are mostly based off of Christian Heimes' work on defusedxml: https://pypi.python.org/pypi/defusedxml B401: import_telnetlib ---------------------- A telnet-related module is being imported. Telnet is considered insecure. Use SSH or some other encrypted protocol. +------+---------------------+------------------------------------+-----------+ | ID | Name | Imports | Severity | +======+=====================+====================================+===========+ | B401 | import_telnetlib | - telnetlib | high | +------+---------------------+------------------------------------+-----------+ B402: import_ftplib ------------------- A FTP-related module is being imported. FTP is considered insecure. Use SSH/SFTP/SCP or some other encrypted protocol. +------+---------------------+------------------------------------+-----------+ | ID | Name | Imports | Severity | +======+=====================+====================================+===========+ | B402 | inport_ftplib | - ftplib | high | +------+---------------------+------------------------------------+-----------+ B403: import_pickle ------------------- Consider possible security implications associated with these modules. +------+---------------------+------------------------------------+-----------+ | ID | Name | Imports | Severity | +======+=====================+====================================+===========+ | B403 | import_pickle | - pickle | low | | | | - cPickle | | +------+---------------------+------------------------------------+-----------+ B404: import_subprocess ----------------------- Consider possible security implications associated with these modules. +------+---------------------+------------------------------------+-----------+ | ID | Name | Imports | Severity | +======+=====================+====================================+===========+ | B404 | import_subprocess | - subprocess | low | +------+---------------------+------------------------------------+-----------+ B405: import_xml_etree ---------------------- Using various methods to parse untrusted XML data is known to be vulnerable to XML attacks. Replace vulnerable imports with the equivalent defusedxml package, or make sure defusedxml.defuse_stdlib() is called. +------+---------------------+------------------------------------+-----------+ | ID | Name | Imports | Severity | +======+=====================+====================================+===========+ | B405 | import_xml_etree | - xml.etree.cElementTree | low | | | | - xml.etree.ElementTree | | +------+---------------------+------------------------------------+-----------+ B406: import_xml_sax -------------------- Using various methods to parse untrusted XML data is known to be vulnerable to XML attacks. Replace vulnerable imports with the equivalent defusedxml package, or make sure defusedxml.defuse_stdlib() is called. +------+---------------------+------------------------------------+-----------+ | ID | Name | Imports | Severity | +======+=====================+====================================+===========+ | B406 | import_xml_sax | - xml.sax | low | +------+---------------------+------------------------------------+-----------+ B407: import_xml_expat ---------------------- Using various methods to parse untrusted XML data is known to be vulnerable to XML attacks. Replace vulnerable imports with the equivalent defusedxml package, or make sure defusedxml.defuse_stdlib() is called. +------+---------------------+------------------------------------+-----------+ | ID | Name | Imports | Severity | +======+=====================+====================================+===========+ | B407 | import_xml_expat | - xml.dom.expatbuilder | low | +------+---------------------+------------------------------------+-----------+ B408: import_xml_minidom ------------------------ Using various methods to parse untrusted XML data is known to be vulnerable to XML attacks. Replace vulnerable imports with the equivalent defusedxml package, or make sure defusedxml.defuse_stdlib() is called. +------+---------------------+------------------------------------+-----------+ | ID | Name | Imports | Severity | +======+=====================+====================================+===========+ | B408 | import_xml_minidom | - xml.dom.minidom | low | +------+---------------------+------------------------------------+-----------+ B409: import_xml_pulldom ------------------------ Using various methods to parse untrusted XML data is known to be vulnerable to XML attacks. Replace vulnerable imports with the equivalent defusedxml package, or make sure defusedxml.defuse_stdlib() is called. +------+---------------------+------------------------------------+-----------+ | ID | Name | Imports | Severity | +======+=====================+====================================+===========+ | B409 | import_xml_pulldom | - xml.dom.pulldom | low | +------+---------------------+------------------------------------+-----------+ B410: import_lxml ----------------- Using various methods to parse untrusted XML data is known to be vulnerable to XML attacks. Replace vulnerable imports with the equivalent defusedxml package. +------+---------------------+------------------------------------+-----------+ | ID | Name | Imports | Severity | +======+=====================+====================================+===========+ | B410 | import_lxml | - lxml | low | +------+---------------------+------------------------------------+-----------+ B411: import_xmlrpclib ---------------------- XMLRPC is particularly dangerous as it is also concerned with communicating data over a network. Use defused.xmlrpc.monkey_patch() function to monkey-patch xmlrpclib and mitigate remote XML attacks. +------+---------------------+------------------------------------+-----------+ | ID | Name | Imports | Severity | +======+=====================+====================================+===========+ | B411 | import_xmlrpclib | - xmlrpclib | high | +------+---------------------+------------------------------------+-----------+ B412: import_httpoxy -------------------- httpoxy is a set of vulnerabilities that affect application code running in CGI, or CGI-like environments. The use of CGI for web applications should be avoided to prevent this class of attack. More details are available at https://httpoxy.org/. +------+---------------------+------------------------------------+-----------+ | ID | Name | Imports | Severity | +======+=====================+====================================+===========+ | B412 | import_httpoxy | - wsgiref.handlers.CGIHandler | high | | | | - twisted.web.twcgi.CGIScript | | +------+---------------------+------------------------------------+-----------+ """ from bandit.blacklists import utils def gen_blacklist(): """Generate a list of items to blacklist. Methods of this type, "bandit.blacklist" plugins, are used to build a list of items that bandit's built in blacklisting tests will use to trigger issues. They replace the older blacklist* test plugins and allow blacklisted items to have a unique bandit ID for filtering and profile usage. :return: a dictionary mapping node types to a list of blacklist data """ sets = [] sets.append(utils.build_conf_dict( 'import_telnetlib', 'B401', ['telnetlib'], 'A telnet-related module is being imported. Telnet is ' 'considered insecure. Use SSH or some other encrypted protocol.', 'HIGH' )) sets.append(utils.build_conf_dict( 'import_ftplib', 'B402', ['ftplib'], 'A FTP-related module is being imported. FTP is considered ' 'insecure. Use SSH/SFTP/SCP or some other encrypted protocol.', 'HIGH' )) sets.append(utils.build_conf_dict( 'import_pickle', 'B403', ['pickle', 'cPickle'], 'Consider possible security implications associated with ' '{name} module.', 'LOW' )) sets.append(utils.build_conf_dict( 'import_subprocess', 'B404', ['subprocess'], 'Consider possible security implications associated with ' '{name} module.', 'LOW' )) # Most of this is based off of Christian Heimes' work on defusedxml: # https://pypi.python.org/pypi/defusedxml/#defusedxml-sax xml_msg = ('Using {name} to parse untrusted XML data is known to be ' 'vulnerable to XML attacks. Replace {name} with the equivalent ' 'defusedxml package, or make sure defusedxml.defuse_stdlib() ' 'is called.') lxml_msg = ('Using {name} to parse untrusted XML data is known to be ' 'vulnerable to XML attacks. Replace {name} with the ' 'equivalent defusedxml package.') sets.append(utils.build_conf_dict( 'import_xml_etree', 'B405', ['xml.etree.cElementTree', 'xml.etree.ElementTree'], xml_msg, 'LOW')) sets.append(utils.build_conf_dict( 'import_xml_sax', 'B406', ['xml.sax'], xml_msg, 'LOW')) sets.append(utils.build_conf_dict( 'import_xml_expat', 'B407', ['xml.dom.expatbuilder'], xml_msg, 'LOW')) sets.append(utils.build_conf_dict( 'import_xml_minidom', 'B408', ['xml.dom.minidom'], xml_msg, 'LOW')) sets.append(utils.build_conf_dict( 'import_xml_pulldom', 'B409', ['xml.dom.pulldom'], xml_msg, 'LOW')) sets.append(utils.build_conf_dict( 'import_lxml', 'B410', ['lxml'], lxml_msg, 'LOW')) sets.append(utils.build_conf_dict( 'import_xmlrpclib', 'B411', ['xmlrpclib'], 'Using {name} to parse untrusted XML data is known to be ' 'vulnerable to XML attacks. Use defused.xmlrpc.monkey_patch() ' 'function to monkey-patch xmlrpclib and mitigate XML ' 'vulnerabilities.', 'HIGH')) sets.append(utils.build_conf_dict( 'import_httpoxy', 'B412', ['wsgiref.handlers.CGIHandler', 'twisted.web.twcgi.CGIScript', 'twisted.web.twcgi.CGIDirectory'], 'Consider possible security implications associated with ' '{name} module.', 'HIGH' )) return {'Import': sets, 'ImportFrom': sets, 'Call': sets} bandit-1.4.0/bandit/blacklists/utils.py000066400000000000000000000015461303375232700200760ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2016 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. def build_conf_dict(name, bid, qualnames, message, level='MEDIUM'): """Build and return a blacklist configuration dict.""" return {'name': name, 'id': bid, 'message': message, 'qualnames': qualnames, 'level': level} bandit-1.4.0/bandit/cli/000077500000000000000000000000001303375232700147725ustar00rootroot00000000000000bandit-1.4.0/bandit/cli/__init__.py000066400000000000000000000000001303375232700170710ustar00rootroot00000000000000bandit-1.4.0/bandit/cli/baseline.py000066400000000000000000000167351303375232700171420ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2015 Hewlett-Packard Enterprise # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # ############################################################################# # Bandit Baseline is a tool that runs Bandit against a Git commit, and compares # the current commit findings to the parent commit findings. # To do this it checks out the parent commit, runs Bandit (with any provided # filters or profiles), checks out the current commit, runs Bandit, and then # reports on any new findings. # ############################################################################# import argparse import contextlib import logging import os import shutil import subprocess import sys import tempfile import git bandit_args = sys.argv[1:] baseline_tmp_file = '_bandit_baseline_run.json_' current_commit = None default_output_format = 'terminal' LOG = logging.getLogger(__name__) repo = None report_basename = 'bandit_baseline_result' valid_baseline_formats = ['txt', 'html', 'json'] def main(): # our cleanup function needs this and can't be passed arguments global current_commit global repo parent_commit = None output_format = None repo = None report_fname = None init_logger() output_format, repo, report_fname = initialize() if not repo: sys.exit(2) # #################### Find current and parent commits #################### try: commit = repo.commit() current_commit = commit.hexsha LOG.info('Got current commit: [%s]', commit.name_rev) commit = commit.parents[0] parent_commit = commit.hexsha LOG.info('Got parent commit: [%s]', commit.name_rev) except git.GitCommandError: LOG.error("Unable to get current or parent commit") sys.exit(2) except IndexError: LOG.error("Parent commit not available") sys.exit(2) # #################### Run Bandit against both commits #################### output_type = (['-f', 'txt'] if output_format == default_output_format else ['-o', report_fname]) with baseline_setup() as t: bandit_tmpfile = "{}/{}".format(t, baseline_tmp_file) steps = [{'message': 'Getting Bandit baseline results', 'commit': parent_commit, 'args': bandit_args + ['-f', 'json', '-o', bandit_tmpfile]}, {'message': 'Comparing Bandit results to baseline', 'commit': current_commit, 'args': bandit_args + ['-b', bandit_tmpfile] + output_type}] return_code = None for step in steps: repo.head.reset(commit=step['commit'], working_tree=True) LOG.info(step['message']) bandit_command = ['bandit'] + step['args'] try: output = subprocess.check_output(bandit_command) except subprocess.CalledProcessError as e: output = e.output return_code = e.returncode else: return_code = 0 output = output.decode('utf-8') # subprocess returns bytes if return_code not in [0, 1]: LOG.error("Error running command: %s\nOutput: %s\n", bandit_args, output) # #################### Output and exit #################################### # print output or display message about written report if output_format == default_output_format: print(output) else: LOG.info("Successfully wrote %s", report_fname) # exit with the code the last Bandit run returned sys.exit(return_code) # #################### Clean up before exit ################################### @contextlib.contextmanager def baseline_setup(): d = tempfile.mkdtemp() yield d shutil.rmtree(d, True) if repo: repo.head.reset(commit=current_commit, working_tree=True) # #################### Setup logging ########################################## def init_logger(): LOG.handlers = [] log_level = logging.INFO log_format_string = "[%(levelname)7s ] %(message)s" logging.captureWarnings(True) LOG.setLevel(log_level) handler = logging.StreamHandler(sys.stdout) handler.setFormatter(logging.Formatter(log_format_string)) LOG.addHandler(handler) # #################### Perform initialization and validate assumptions ######## def initialize(): valid = True # #################### Parse Args ######################################### parser = argparse.ArgumentParser( description='Bandit Baseline - Generates Bandit results compared to "' 'a baseline', formatter_class=argparse.RawDescriptionHelpFormatter, epilog='Additional Bandit arguments such as severity filtering (-ll) ' 'can be added and will be passed to Bandit.' ) parser.add_argument('targets', metavar='targets', type=str, nargs='+', help='source file(s) or directory(s) to be tested') parser.add_argument('-f', dest='output_format', action='store', default='terminal', help='specify output format', choices=valid_baseline_formats) args, unknown = parser.parse_known_args() # #################### Setup Output ####################################### # set the output format, or use a default if not provided output_format = (args.output_format if args.output_format else default_output_format) if output_format == default_output_format: LOG.info("No output format specified, using %s", default_output_format) # set the report name based on the output format report_fname = "{}.{}".format(report_basename, output_format) # #################### Check Requirements ################################# try: repo = git.Repo(os.getcwd()) except git.exc.InvalidGitRepositoryError: LOG.error("Bandit baseline must be called from a git project root") valid = False except git.exc.GitCommandNotFound: LOG.error("Git command not found") valid = False else: if repo.is_dirty(): LOG.error("Current working directory is dirty and must be " "resolved") valid = False # if output format is specified, we need to be able to write the report if output_format != default_output_format and os.path.exists(report_fname): LOG.error("File %s already exists, aborting", report_fname) valid = False # Bandit needs to be able to create this temp file if os.path.exists(baseline_tmp_file): LOG.error("Temporary file %s needs to be removed prior to running", baseline_tmp_file) valid = False # we must validate -o is not provided, as it will mess up Bandit baseline if '-o' in bandit_args: LOG.error("Bandit baseline must not be called with the -o option") valid = False return (output_format, repo, report_fname) if valid else (None, None, None) if __name__ == '__main__': main() bandit-1.4.0/bandit/cli/config_generator.py000066400000000000000000000142511303375232700206620ustar00rootroot00000000000000# Copyright 2015 Red Hat Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from __future__ import print_function import argparse import importlib import logging import os import sys import six import yaml from bandit.core import extension_loader PROG_NAME = 'bandit_conf_generator' LOG = logging.getLogger(__name__) template = """ ### Bandit config file generated from: # '{cli}' ### This config may optionally select a subset of tests to run or skip by ### filling out the 'tests' and 'skips' lists given below. If no tests are ### specified for inclusion then it is assumed all tests are desired. The skips ### set will remove specific tests from the include set. This can be controlled ### using the -t/-s CLI options. Note that the same test ID should not appear ### in both 'tests' and 'skips', this would be nonsensical and is detected by ### Bandit at runtime. # Available tests: {test_list} # (optional) list included test IDs here, eg '[B101, B406]': {test} # (optional) list skipped test IDs here, eg '[B101, B406]': {skip} ### (optional) plugin settings - some test plugins require configuration data ### that may be given here, per-plugin. All bandit test plugins have a built in ### set of sensible defaults and these will be used if no configuration is ### provided. It is not necessary to provide settings for every (or any) plugin ### if the defaults are acceptable. {settings} """ def init_logger(): LOG.handlers = [] log_level = logging.INFO log_format_string = "[%(levelname)5s]: %(message)s" logging.captureWarnings(True) LOG.setLevel(log_level) handler = logging.StreamHandler(sys.stdout) handler.setFormatter(logging.Formatter(log_format_string)) LOG.addHandler(handler) def parse_args(): help_description = """Bandit Config Generator This tool is used to generate an optional profile. The profile may be used to include or skip tests and override values for plugins. When used to store an output profile, this tool will output a template that includes all plugins and their default settings. Any settings which aren't being overridden can be safely removed from the profile and default values will be used. Bandit will prefer settings from the profile over the built in values.""" parser = argparse.ArgumentParser( description=help_description, formatter_class=argparse.RawTextHelpFormatter) parser.add_argument('--show-defaults', dest='show_defaults', action='store_true', help='show the default settings values for each ' 'plugin but do not output a profile') parser.add_argument('-o', '--out', dest='output_file', action='store', help='output file to save profile') parser.add_argument( '-t', '--tests', dest='tests', action='store', default=None, type=str, help='list of test names to run') parser.add_argument( '-s', '--skip', dest='skips', action='store', default=None, type=str, help='list of test names to skip') args = parser.parse_args() if not args.output_file and not args.show_defaults: parser.print_help() parser.exit(1) return args def get_config_settings(): config = {} for plugin in extension_loader.MANAGER.plugins: fn_name = plugin.name function = plugin.plugin # if a function takes config... if hasattr(function, '_takes_config'): fn_module = importlib.import_module(function.__module__) # call the config generator if it exists if hasattr(fn_module, 'gen_config'): config[fn_name] = fn_module.gen_config(function._takes_config) return yaml.safe_dump(config) def main(): init_logger() args = parse_args() yaml_settings = get_config_settings() if args.show_defaults: print(yaml_settings) if args.output_file: if os.path.exists(os.path.abspath(args.output_file)): LOG.error("File %s already exists, exiting", args.output_file) sys.exit(2) try: with open(args.output_file, 'w') as f: skips = args.skips.split(',') if args.skips else [] tests = args.tests.split(',') if args.tests else [] for skip in skips: if not extension_loader.MANAGER.check_id(skip): raise RuntimeError('unknown ID in skips: %s' % skip) for test in tests: if not extension_loader.MANAGER.check_id(test): raise RuntimeError('unknown ID in tests: %s' % test) tpl = "# {0} : {1}" test_list = [tpl.format(t.plugin._test_id, t.name) for t in extension_loader.MANAGER.plugins] others = [tpl.format(k, v['name']) for k, v in six.iteritems( extension_loader.MANAGER.blacklist_by_id)] test_list.extend(others) test_list.sort() contents = template.format( cli=" ".join(sys.argv), settings=yaml_settings, test_list="\n".join(test_list), skip='skips: ' + str(skips) if skips else 'skips:', test='tests: ' + str(tests) if tests else 'tests:') f.write(contents) except IOError: LOG.error("Unable to open %s for writing", args.output_file) except Exception as e: LOG.error("Error: %s", e) else: LOG.info("Successfully wrote profile: %s", args.output_file) return 0 if __name__ == '__main__': sys.exit(main()) bandit-1.4.0/bandit/cli/main.py000066400000000000000000000306621303375232700162770ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import argparse import fnmatch import logging import os import sys import six import bandit from bandit.core import config as b_config from bandit.core import constants from bandit.core import manager as b_manager from bandit.core import utils BASE_CONFIG = 'bandit.yaml' LOG = logging.getLogger() def _init_logger(debug=False, log_format=None): '''Initialize the logger :param debug: Whether to enable debug mode :return: An instantiated logging instance ''' LOG.handlers = [] log_level = logging.INFO if debug: log_level = logging.DEBUG if not log_format: # default log format log_format_string = constants.log_format_string else: log_format_string = log_format logging.captureWarnings(True) LOG.setLevel(log_level) handler = logging.StreamHandler(sys.stderr) handler.setFormatter(logging.Formatter(log_format_string)) LOG.addHandler(handler) LOG.debug("logging initialized") def _get_options_from_ini(ini_path, target): """Return a dictionary of config options or None if we can't load any.""" ini_file = None if ini_path: ini_file = ini_path else: bandit_files = [] for t in target: for root, dirnames, filenames in os.walk(t): for filename in fnmatch.filter(filenames, '.bandit'): bandit_files.append(os.path.join(root, filename)) if len(bandit_files) > 1: LOG.error('Multiple .bandit files found - scan separately or ' 'choose one with --ini\n\t%s', ', '.join(bandit_files)) sys.exit(2) elif len(bandit_files) == 1: ini_file = bandit_files[0] LOG.info('Found project level .bandit file: %s', bandit_files[0]) if ini_file: return utils.parse_ini_file(ini_file) else: return None def _init_extensions(): from bandit.core import extension_loader as ext_loader return ext_loader.MANAGER def _log_option_source(arg_val, ini_val, option_name): """It's useful to show the source of each option.""" if arg_val: LOG.info("Using command line arg for %s", option_name) return arg_val elif ini_val: LOG.info("Using .bandit arg for %s", option_name) return ini_val else: return None def _running_under_virtualenv(): if hasattr(sys, 'real_prefix'): return True elif sys.prefix != getattr(sys, 'base_prefix', sys.prefix): return True def _get_profile(config, profile_name, config_path): profile = {} if profile_name: profiles = config.get_option('profiles') or {} profile = profiles.get(profile_name) if profile is None: raise utils.ProfileNotFound(config_path, profile_name) LOG.debug("read in legacy profile '%s': %s", profile_name, profile) else: profile['include'] = set(config.get_option('tests') or []) profile['exclude'] = set(config.get_option('skips') or []) return profile def _log_info(args, profile): inc = ",".join([t for t in profile['include']]) or "None" exc = ",".join([t for t in profile['exclude']]) or "None" LOG.info("profile include tests: %s", inc) LOG.info("profile exclude tests: %s", exc) LOG.info("cli include tests: %s", args.tests) LOG.info("cli exclude tests: %s", args.skips) def main(): # bring our logging stuff up as early as possible debug = ('-d' in sys.argv or '--debug' in sys.argv) _init_logger(debug) extension_mgr = _init_extensions() baseline_formatters = [f.name for f in filter(lambda x: hasattr(x.plugin, '_accepts_baseline'), extension_mgr.formatters)] # now do normal startup parser = argparse.ArgumentParser( description='Bandit - a Python source code security analyzer', formatter_class=argparse.RawDescriptionHelpFormatter ) parser.add_argument( 'targets', metavar='targets', type=str, nargs='+', help='source file(s) or directory(s) to be tested' ) parser.add_argument( '-r', '--recursive', dest='recursive', action='store_true', help='find and process files in subdirectories' ) parser.add_argument( '-a', '--aggregate', dest='agg_type', action='store', default='file', type=str, choices=['file', 'vuln'], help='aggregate output by vulnerability (default) or by filename' ) parser.add_argument( '-n', '--number', dest='context_lines', action='store', default=3, type=int, help='maximum number of code lines to output for each issue' ) parser.add_argument( '-c', '--configfile', dest='config_file', action='store', default=None, type=str, help='optional config file to use for selecting plugins and ' 'overriding defaults' ) parser.add_argument( '-p', '--profile', dest='profile', action='store', default=None, type=str, help='profile to use (defaults to executing all tests)' ) parser.add_argument( '-t', '--tests', dest='tests', action='store', default=None, type=str, help='comma-separated list of test IDs to run' ) parser.add_argument( '-s', '--skip', dest='skips', action='store', default=None, type=str, help='comma-separated list of test IDs to skip' ) parser.add_argument( '-l', '--level', dest='severity', action='count', default=1, help='report only issues of a given severity level or ' 'higher (-l for LOW, -ll for MEDIUM, -lll for HIGH)' ) parser.add_argument( '-i', '--confidence', dest='confidence', action='count', default=1, help='report only issues of a given confidence level or ' 'higher (-i for LOW, -ii for MEDIUM, -iii for HIGH)' ) output_format = 'screen' if sys.stdout.isatty() else 'txt' parser.add_argument( '-f', '--format', dest='output_format', action='store', default=output_format, help='specify output format', choices=sorted(extension_mgr.formatter_names) ) parser.add_argument( '-o', '--output', dest='output_file', action='store', nargs='?', type=argparse.FileType('w'), default=sys.stdout, help='write report to filename' ) parser.add_argument( '-v', '--verbose', dest='verbose', action='store_true', help='output extra information like excluded and included files' ) parser.add_argument( '-d', '--debug', dest='debug', action='store_true', help='turn on debug mode' ) parser.add_argument( '--ignore-nosec', dest='ignore_nosec', action='store_true', help='do not skip lines with # nosec comments' ) parser.add_argument( '-x', '--exclude', dest='excluded_paths', action='store', default='', help='comma-separated list of paths to exclude from scan ' '(note that these are in addition to the excluded ' 'paths provided in the config file)' ) parser.add_argument( '-b', '--baseline', dest='baseline', action='store', default=None, help='path of a baseline report to compare against ' '(only JSON-formatted files are accepted)' ) parser.add_argument( '--ini', dest='ini_path', action='store', default=None, help='path to a .bandit file that supplies command line arguments' ) parser.add_argument( '--version', action='version', version='%(prog)s {version}'.format(version=bandit.__version__) ) parser.set_defaults(debug=False) parser.set_defaults(verbose=False) parser.set_defaults(ignore_nosec=False) plugin_info = ["%s\t%s" % (a[0], a[1].name) for a in six.iteritems(extension_mgr.plugins_by_id)] blacklist_info = [] for a in six.iteritems(extension_mgr.blacklist): for b in a[1]: blacklist_info.append('%s\t%s' % (b['id'], b['name'])) plugin_list = '\n\t'.join(sorted(set(plugin_info + blacklist_info))) parser.epilog = ('The following tests were discovered and' ' loaded:\n\t{0}\n'.format(plugin_list)) # setup work - parse arguments, and initialize BanditManager args = parser.parse_args() try: b_conf = b_config.BanditConfig(config_file=args.config_file) except utils.ConfigError as e: LOG.error(e) sys.exit(2) # Handle .bandit files in projects to pass cmdline args from file ini_options = _get_options_from_ini(args.ini_path, args.targets) if ini_options: # prefer command line, then ini file args.excluded_paths = _log_option_source(args.excluded_paths, ini_options.get('exclude'), 'excluded paths') args.skips = _log_option_source(args.skips, ini_options.get('skips'), 'skipped tests') args.tests = _log_option_source(args.tests, ini_options.get('tests'), 'selected tests') # TODO(tmcpeak): any other useful options to pass from .bandit? # if the log format string was set in the options, reinitialize if b_conf.get_option('log_format'): log_format = b_conf.get_option('log_format') _init_logger(debug, log_format=log_format) try: profile = _get_profile(b_conf, args.profile, args.config_file) _log_info(args, profile) profile['include'].update(args.tests.split(',') if args.tests else []) profile['exclude'].update(args.skips.split(',') if args.skips else []) extension_mgr.validate_profile(profile) except (utils.ProfileNotFound, ValueError) as e: LOG.error(e) sys.exit(2) b_mgr = b_manager.BanditManager(b_conf, args.agg_type, args.debug, profile=profile, verbose=args.verbose, ignore_nosec=args.ignore_nosec) if args.baseline is not None: try: with open(args.baseline) as bl: data = bl.read() b_mgr.populate_baseline(data) except IOError: LOG.warning("Could not open baseline report: %s", args.baseline) sys.exit(2) if args.output_format not in baseline_formatters: LOG.warning('Baseline must be used with one of the following ' 'formats: ' + str(baseline_formatters)) sys.exit(2) if args.output_format != "json": if args.config_file: LOG.info("using config: %s", args.config_file) LOG.info("running on Python %d.%d.%d", sys.version_info.major, sys.version_info.minor, sys.version_info.micro) # initiate file discovery step within Bandit Manager b_mgr.discover_files(args.targets, args.recursive, args.excluded_paths) if not b_mgr.b_ts.tests: LOG.error('No tests would be run, please check the profile.') sys.exit(2) # initiate execution of tests within Bandit Manager b_mgr.run_tests() LOG.debug(b_mgr.b_ma) LOG.debug(b_mgr.metrics) # trigger output of results by Bandit Manager sev_level = constants.RANKING[args.severity - 1] conf_level = constants.RANKING[args.confidence - 1] b_mgr.output_results(args.context_lines, sev_level, conf_level, args.output_file, args.output_format) # return an exit code of 1 if there are results, 0 otherwise if b_mgr.results_count(sev_filter=sev_level, conf_filter=conf_level) > 0: sys.exit(1) else: sys.exit(0) if __name__ == '__main__': main() bandit-1.4.0/bandit/core/000077500000000000000000000000001303375232700151535ustar00rootroot00000000000000bandit-1.4.0/bandit/core/__init__.py000066400000000000000000000021011303375232700172560ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from bandit.core import config # noqa from bandit.core import context # noqa from bandit.core import manager # noqa from bandit.core import meta_ast # noqa from bandit.core import node_visitor # noqa from bandit.core import test_set # noqa from bandit.core import tester # noqa from bandit.core import utils # noqa from bandit.core.constants import * # noqa from bandit.core.issue import * # noqa from bandit.core.test_properties import * # noqa bandit-1.4.0/bandit/core/blacklisting.py000066400000000000000000000052051303375232700201750ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2016 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import ast import fnmatch from bandit.core import issue def report_issue(check, name): return issue.Issue( severity=check.get('level', 'MEDIUM'), confidence='HIGH', text=check['message'].replace('{name}', name), ident=name, test_id=check.get("id", 'LEGACY')) def blacklist(context, config): """Generic blacklist test, B001. This generic blacklist test will be called for any encountered node with defined blacklist data available. This data is loaded via plugins using the 'bandit.blacklists' entry point. Please see the documentation for more details. Each blacklist datum has a unique bandit ID that may be used for filtering purposes, or alternatively all blacklisting can be filtered using the id of this built in test, 'B001'. """ blacklists = config node_type = context.node.__class__.__name__ if node_type == 'Call': func = context.node.func if isinstance(func, ast.Name) and func.id == '__import__': if len(context.node.args): if isinstance(context.node.args[0], ast.Str): name = context.node.args[0].s else: # TODO(??): import through a variable, need symbol tab name = "UNKNOWN" else: name = "" # handle '__import__()' else: name = context.call_function_name_qual for check in blacklists[node_type]: for qn in check['qualnames']: if fnmatch.fnmatch(name, qn): return report_issue(check, name) if node_type.startswith('Import'): prefix = "" if node_type == "ImportFrom": if context.node.module is not None: prefix = context.node.module + "." for check in blacklists[node_type]: for name in context.node.names: for qn in check['qualnames']: if (prefix + name.name).startswith(qn): return report_issue(check, name.name) bandit-1.4.0/bandit/core/config.py000066400000000000000000000224171303375232700170000ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import logging import six import yaml from bandit.core import constants from bandit.core import extension_loader from bandit.core import utils LOG = logging.getLogger(__name__) class BanditConfig(object): def __init__(self, config_file=None): '''Attempt to initialize a config dictionary from a yaml file. Error out if loading the yaml file fails for any reason. :param config_file: The Bandit yaml config file :raises bandit.utils.ConfigError: If the config is invalid or unreadable. ''' self.config_file = config_file self._config = {} if config_file: try: f = open(config_file, 'r') except IOError: raise utils.ConfigError("Could not read config file.", config_file) try: self._config = yaml.safe_load(f) self.validate(config_file) except yaml.YAMLError: raise utils.ConfigError("Error parsing file.", config_file) # valid config must be a dict if not isinstance(self._config, dict): raise utils.ConfigError("Error parsing file.", config_file) self.convert_legacy_config() else: # use sane defaults self._config['plugin_name_pattern'] = '*.py' self._config['include'] = ['*.py', '*.pyw'] self._init_settings() def get_option(self, option_string): '''Returns the option from the config specified by the option_string. '.' can be used to denote levels, for example to retrieve the options from the 'a' profile you can use 'profiles.a' :param option_string: The string specifying the option to retrieve :return: The object specified by the option_string, or None if it can't be found. ''' option_levels = option_string.split('.') cur_item = self._config for level in option_levels: if cur_item and (level in cur_item): cur_item = cur_item[level] else: return None return cur_item def get_setting(self, setting_name): if setting_name in self._settings: return self._settings[setting_name] else: return None @property def config(self): '''Property to return the config dictionary :return: Config dictionary ''' return self._config def _init_settings(self): '''This function calls a set of other functions (one per setting) This function calls a set of other functions (one per setting) to build out the _settings dictionary. Each other function will set values from the config (if set), otherwise use defaults (from constants if possible). :return: - ''' self._settings = {} self._init_plugin_name_pattern() def _init_plugin_name_pattern(self): '''Sets settings['plugin_name_pattern'] from default or config file.''' plugin_name_pattern = constants.plugin_name_pattern if self.get_option('plugin_name_pattern'): plugin_name_pattern = self.get_option('plugin_name_pattern') self._settings['plugin_name_pattern'] = plugin_name_pattern def convert_legacy_config(self): updated_profiles = self.convert_names_to_ids() bad_calls, bad_imports = self.convert_legacy_blacklist_data() if updated_profiles: self.convert_legacy_blacklist_tests(updated_profiles, bad_calls, bad_imports) self._config['profiles'] = updated_profiles def convert_names_to_ids(self): '''Convert test names to IDs, unknown names are left unchanged.''' extman = extension_loader.MANAGER updated_profiles = {} for name, profile in six.iteritems(self.get_option('profiles') or {}): # NOTE(tkelsey): can't use default of get() because value is # sometimes explicity 'None', for example when the list if given in # yaml but not populated with any values. include = set((extman.get_plugin_id(i) or i) for i in (profile.get('include') or [])) exclude = set((extman.get_plugin_id(i) or i) for i in (profile.get('exclude') or [])) updated_profiles[name] = {'include': include, 'exclude': exclude} return updated_profiles def convert_legacy_blacklist_data(self): '''Detect legacy blacklist data and convert it to new format.''' bad_calls_list = [] bad_imports_list = [] bad_calls = self.get_option('blacklist_calls') or {} bad_calls = bad_calls.get('bad_name_sets', {}) for item in bad_calls: for key, val in six.iteritems(item): val['name'] = key val['message'] = val['message'].replace('{func}', '{name}') bad_calls_list.append(val) bad_imports = self.get_option('blacklist_imports') or {} bad_imports = bad_imports.get('bad_import_sets', {}) for item in bad_imports: for key, val in six.iteritems(item): val['name'] = key val['message'] = val['message'].replace('{module}', '{name}') val['qualnames'] = val['imports'] del val['imports'] bad_imports_list.append(val) if bad_imports_list or bad_calls_list: LOG.warning('Legacy blacklist data found in config, overriding ' 'data plugins') return bad_calls_list, bad_imports_list @staticmethod def convert_legacy_blacklist_tests(profiles, bad_imports, bad_calls): '''Detect old blacklist tests, convert to use new builtin.''' def _clean_set(name, data): if name in data: data.remove(name) data.add('B001') for name, profile in six.iteritems(profiles): blacklist = {} include = profile['include'] exclude = profile['exclude'] name = 'blacklist_calls' if name in include and name not in exclude: blacklist.setdefault('Call', []).extend(bad_calls) _clean_set(name, include) _clean_set(name, exclude) name = 'blacklist_imports' if name in include and name not in exclude: blacklist.setdefault('Import', []).extend(bad_imports) blacklist.setdefault('ImportFrom', []).extend(bad_imports) blacklist.setdefault('Call', []).extend(bad_imports) _clean_set(name, include) _clean_set(name, exclude) _clean_set('blacklist_import_func', include) _clean_set('blacklist_import_func', exclude) # This can happen with a legacy config that includes # blacklist_calls but exclude blacklist_imports for example if 'B001' in include and 'B001' in exclude: exclude.remove('B001') profile['blacklist'] = blacklist def validate(self, path): '''Validate the config data.''' legacy = False message = ("Config file has an include or exclude reference " "to legacy test '{0}' but no configuration data for " "it. Configuration data is required for this test. " "Please consider switching to the new config file " "format, the tool 'bandit-config-generator' can help " "you with this.") def _test(key, block, exclude, include): if key in exclude or key in include: if self._config.get(block) is None: raise utils.ConfigError(message.format(key), path) if 'profiles' in self._config: legacy = True for profile in self._config['profiles'].values(): inc = profile.get('include') or set() exc = profile.get('exclude') or set() _test('blacklist_imports', 'blacklist_imports', inc, exc) _test('blacklist_import_func', 'blacklist_imports', inc, exc) _test('blacklist_calls', 'blacklist_calls', inc, exc) # show deprecation message if legacy: LOG.warning("Config file '%s' contains deprecated legacy config " "data. Please consider upgrading to the new config " "format. The tool 'bandit-config-generator' can help " "you with this. Support for legacy configs will be " "removed in a future bandit version.", path) bandit-1.4.0/bandit/core/constants.py000066400000000000000000000031471303375232700175460ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # default plugin name pattern plugin_name_pattern = '*.py' # default progress increment progress_increment = 50 RANKING = ['UNDEFINED', 'LOW', 'MEDIUM', 'HIGH'] RANKING_VALUES = {'UNDEFINED': 1, 'LOW': 3, 'MEDIUM': 5, 'HIGH': 10} CRITERIA = [('SEVERITY', 'UNDEFINED'), ('CONFIDENCE', 'UNDEFINED')] # add each ranking to globals, to allow direct access in module name space for rank in RANKING: globals()[rank] = rank CONFIDENCE_DEFAULT = 'UNDEFINED' # A list of values Python considers to be False. # These can be useful in tests to check if a value is True or False. # We don't handle the case of user-defined classes being false. # These are only useful when we have a constant in code. If we # have a variable we cannot determine if False. # See https://docs.python.org/2/library/stdtypes.html#truth-value-testing FALSE_VALUES = [None, False, 'False', 0, 0.0, 0j, '', (), [], {}] # override with "log_format" option in config file log_format_string = '[%(module)s]\t%(levelname)s\t%(message)s' bandit-1.4.0/bandit/core/context.py000066400000000000000000000265341303375232700172230ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import _ast import six from bandit.core import utils class Context(object): def __init__(self, context_object=None): '''Initialize the class with a context, empty dict otherwise :param context_object: The context object to create class from :return: - ''' if context_object is not None: self._context = context_object else: self._context = dict() def __repr__(self): '''Generate representation of object for printing / interactive use Most likely only interested in non-default properties, so we return the string version of _context. Example string returned: , 'function': None, 'name': 'socket', 'imports': set(['socket']), 'module': None, 'filename': 'examples/binding.py', 'call': <_ast.Call object at 0x110252510>, 'lineno': 3, 'import_aliases': {}, 'qualname': 'socket.socket'}> :return: A string representation of the object ''' return "" % self._context @property def call_args(self): '''Get a list of function args :return: A list of function args ''' args = [] for arg in self._context['call'].args: if hasattr(arg, 'attr'): args.append(arg.attr) else: args.append(self._get_literal_value(arg)) return args @property def call_args_count(self): '''Get the number of args a function call has :return: The number of args a function call has ''' if 'call' in self._context and hasattr(self._context['call'], 'args'): return len(self._context['call'].args) else: return None @property def call_function_name(self): '''Get the name (not FQ) of a function call :return: The name (not FQ) of a function call ''' if 'name' in self._context: return self._context['name'] else: return None @property def call_function_name_qual(self): '''Get the FQ name of a function call :return: The FQ name of a function call ''' if 'qualname' in self._context: return self._context['qualname'] else: return None @property def call_keywords(self): '''Get a dictionary of keyword parameters :return: A dictionary of keyword parameters for a call as strings ''' if ('call' in self._context and hasattr(self._context['call'], 'keywords')): return_dict = {} for li in self._context['call'].keywords: if hasattr(li.value, 'attr'): return_dict[li.arg] = li.value.attr else: return_dict[li.arg] = self._get_literal_value(li.value) return return_dict else: return None @property def node(self): '''Get the raw AST node associated with the context :return: The raw AST node associated with the context ''' if 'node' in self._context: return self._context['node'] else: return None @property def string_val(self): '''Get the value of a standalone unicode or string object :return: value of a standalone unicode or string object ''' if 'str' in self._context: return self._context['str'] else: return None @property def bytes_val(self): '''Get the value of a standalone bytes object (py3 only) :return: value of a standalone bytes object ''' return self._context.get('bytes') @property def string_val_as_escaped_bytes(self): '''Get escaped value of the object. Turn the value of a string or bytes object into byte sequence with unknown, control, and \\ characters escaped. This function should be used when looking for a known sequence in a potentially badly encoded string in the code. :return: sequence of printable ascii bytes representing original string ''' val = self.string_val if val is not None: # it's any of str or unicode in py2, or str in py3 return val.encode('unicode_escape') val = self.bytes_val if val is not None: return utils.escaped_bytes_representation(val) return None @property def statement(self): '''Get the raw AST for the current statement :return: The raw AST for the current statement ''' if 'statement' in self._context: return self._context['statement'] else: return None @property def function_def_defaults_qual(self): '''Get a list of fully qualified default values in a function def :return: List of defaults ''' defaults = [] if 'node' in self._context: for default in self._context['node'].args.defaults: defaults.append(utils.get_qual_attr( default, self._context['import_aliases'])) return defaults def _get_literal_value(self, literal): '''Utility function to turn AST literals into native Python types :param literal: The AST literal to convert :return: The value of the AST literal ''' if isinstance(literal, _ast.Num): literal_value = literal.n elif isinstance(literal, _ast.Str): literal_value = literal.s elif isinstance(literal, _ast.List): return_list = list() for li in literal.elts: return_list.append(self._get_literal_value(li)) literal_value = return_list elif isinstance(literal, _ast.Tuple): return_tuple = tuple() for ti in literal.elts: return_tuple = return_tuple + (self._get_literal_value(ti),) literal_value = return_tuple elif isinstance(literal, _ast.Set): return_set = set() for si in literal.elts: return_set.add(self._get_literal_value(si)) literal_value = return_set elif isinstance(literal, _ast.Dict): literal_value = dict(zip(literal.keys, literal.values)) elif isinstance(literal, _ast.Ellipsis): # what do we want to do with this? literal_value = None elif isinstance(literal, _ast.Name): literal_value = literal.id # NOTE(sigmavirus24): NameConstants are only part of the AST in Python # 3. NameConstants tend to refer to things like True and False. This # prevents them from being re-assigned in Python 3. elif six.PY3 and isinstance(literal, _ast.NameConstant): literal_value = str(literal.value) # NOTE(sigmavirus24): Bytes are only part of the AST in Python 3 elif six.PY3 and isinstance(literal, _ast.Bytes): literal_value = literal.s else: literal_value = None return literal_value def get_call_arg_value(self, argument_name): '''Gets the value of a named argument in a function call. :return: named argument value ''' kwd_values = self.call_keywords if kwd_values is not None and argument_name in kwd_values: return kwd_values[argument_name] def check_call_arg_value(self, argument_name, argument_values=None): '''Checks for a value of a named argument in a function call. Returns none if the specified argument is not found. :param argument_name: A string - name of the argument to look for :param argument_values: the value, or list of values to test against :return: Boolean True if argument found and matched, False if found and not matched, None if argument not found at all ''' arg_value = self.get_call_arg_value(argument_name) if arg_value is not None: if not isinstance(argument_values, list): # if passed a single value, or a tuple, convert to a list argument_values = list((argument_values,)) for val in argument_values: if arg_value == val: return True return False else: # argument name not found, return None to allow testing for this # eventuality return None def get_lineno_for_call_arg(self, argument_name): '''Get the line number for a specific named argument In case the call is split over multiple lines, get the correct one for the argument. :param argument_name: A string - name of the argument to look for :return: Integer - the line number of the found argument, or -1 ''' for key in self.node.keywords: if key.arg == argument_name: return key.value.lineno def get_call_arg_at_position(self, position_num): '''Returns positional argument at the specified position (if it exists) :param position_num: The index of the argument to return the value for :return: Value of the argument at the specified position if it exists ''' if ('call' in self._context and hasattr(self._context['call'], 'args') and position_num < len(self._context['call'].args)): return self._get_literal_value( self._context['call'].args[position_num] ) else: return None def is_module_being_imported(self, module): '''Check for the specified module is currently being imported :param module: The module name to look for :return: True if the module is found, False otherwise ''' return 'module' in self._context and self._context['module'] == module def is_module_imported_exact(self, module): '''Check if a specified module has been imported; only exact matches. :param module: The module name to look for :return: True if the module is found, False otherwise ''' return ('imports' in self._context and module in self._context['imports']) def is_module_imported_like(self, module): '''Check if a specified module has been imported Check if a specified module has been imported; specified module exists as part of any import statement. :param module: The module name to look for :return: True if the module is found, False otherwise ''' if 'imports' in self._context: for imp in self._context['imports']: if module in imp: return True return False bandit-1.4.0/bandit/core/docs_utils.py000066400000000000000000000031421303375232700176750ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2016 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # where our docs are hosted BASE_URL = 'http://docs.openstack.org/developer/bandit/' def get_url(bid): # NOTE(tkelsey): for some reason this import can't be found when stevedore # loads up the formatter plugin that imports this file. It is available # later though. from bandit.core import extension_loader info = extension_loader.MANAGER.plugins_by_id.get(bid, None) if info is not None: return BASE_URL + ('plugins/%s.html' % info.plugin.__name__) info = extension_loader.MANAGER.blacklist_by_id.get(bid, None) if info is not None: template = 'blacklists/blacklist_{kind}.html#{id}-{name}' if info['id'].startswith('B3'): # B3XX ext = template.format( kind='calls', id=info['id'], name=info['name']) else: ext = template.format( kind='imports', id=info['id'], name=info['name']) return BASE_URL + ext.lower() return BASE_URL # no idea, give the docs main page bandit-1.4.0/bandit/core/extension_loader.py000066400000000000000000000106011303375232700210650ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from __future__ import print_function import sys import six from stevedore import extension from bandit.core import utils class Manager(object): # These IDs are for bandit built in tests builtin = [ 'B001' # Built in blacklist test ] def __init__(self, formatters_namespace='bandit.formatters', plugins_namespace='bandit.plugins', blacklists_namespace='bandit.blacklists'): # Cache the extension managers, loaded extensions, and extension names self.load_formatters(formatters_namespace) self.load_plugins(plugins_namespace) self.load_blacklists(blacklists_namespace) def load_formatters(self, formatters_namespace): self.formatters_mgr = extension.ExtensionManager( namespace=formatters_namespace, invoke_on_load=False, verify_requirements=False, ) self.formatters = list(self.formatters_mgr) self.formatter_names = self.formatters_mgr.names() def load_plugins(self, plugins_namespace): self.plugins_mgr = extension.ExtensionManager( namespace=plugins_namespace, invoke_on_load=False, verify_requirements=False, ) def test_has_id(plugin): if not hasattr(plugin.plugin, "_test_id"): # logger not setup yet, so using print print("WARNING: Test '%s' has no ID, skipping." % plugin.name, file=sys.stderr) return False return True self.plugins = list(filter(test_has_id, list(self.plugins_mgr))) self.plugin_names = [plugin.name for plugin in self.plugins] self.plugins_by_id = {p.plugin._test_id: p for p in self.plugins} self.plugins_by_name = {p.name: p for p in self.plugins} def get_plugin_id(self, plugin_name): if plugin_name in self.plugins_by_name: return self.plugins_by_name[plugin_name].plugin._test_id return None def load_blacklists(self, blacklist_namespace): self.blacklists_mgr = extension.ExtensionManager( namespace=blacklist_namespace, invoke_on_load=False, verify_requirements=False, ) self.blacklist = {} blacklist = list(self.blacklists_mgr) for item in blacklist: for key, val in item.plugin().items(): utils.check_ast_node(key) self.blacklist.setdefault(key, []).extend(val) self.blacklist_by_id = {} self.blacklist_by_name = {} for val in six.itervalues(self.blacklist): for b in val: self.blacklist_by_id[b['id']] = b self.blacklist_by_name[b['name']] = b def validate_profile(self, profile): '''Validate that everything in the configured profiles looks good.''' for inc in profile['include']: if not self.check_id(inc): raise ValueError('Unknown test found in profile: %s' % inc) for exc in profile['exclude']: if not self.check_id(exc): raise ValueError('Unknown test found in profile: %s' % exc) union = set(profile['include']) & set(profile['exclude']) if len(union) > 0: raise ValueError('Non-exclusive include/exclude test sets: %s' % union) def check_id(self, test): return ( test in self.plugins_by_id or test in self.blacklist_by_id or test in self.builtin) # Using entry-points and pkg_resources *can* be expensive. So let's load these # once, store them on the object, and have a module global object for # accessing them. After the first time this module is imported, it should save # this attribute on the module and not have to reload the entry-points. MANAGER = Manager() bandit-1.4.0/bandit/core/issue.py000066400000000000000000000113621303375232700166600ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2015 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from __future__ import division from __future__ import unicode_literals import linecache from six import moves from bandit.core import constants class Issue(object): def __init__(self, severity, confidence=constants.CONFIDENCE_DEFAULT, text="", ident=None, lineno=None, test_id=""): self.severity = severity self.confidence = confidence if isinstance(text, bytes): text = text.decode('utf-8') self.text = text self.ident = ident self.fname = "" self.test = "" self.test_id = test_id self.lineno = lineno self.linerange = [] def __str__(self): return ("Issue: '%s' from %s:%s: Severity: %s Confidence: " "%s at %s:%i") % (self.text, self.test_id, (self.ident or self.test), self.severity, self.confidence, self.fname, self.lineno) def __eq__(self, other): # if the issue text, severity, confidence, and filename match, it's # the same issue from our perspective match_types = ['text', 'severity', 'confidence', 'fname', 'test', 'test_id'] return all(getattr(self, field) == getattr(other, field) for field in match_types) def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return id(self) def filter(self, severity, confidence): '''Utility to filter on confidence and severity This function determines whether an issue should be included by comparing the severity and confidence rating of the issue to minimum thresholds specified in 'severity' and 'confidence' respectively. Formatters should call manager.filter_results() directly. This will return false if either the confidence or severity of the issue are lower than the given threshold values. :param severity: Severity threshold :param confidence: Confidence threshold :return: True/False depending on whether issue meets threshold ''' rank = constants.RANKING return (rank.index(self.severity) >= rank.index(severity) and rank.index(self.confidence) >= rank.index(confidence)) def get_code(self, max_lines=3, tabbed=False): '''Gets lines of code from a file the generated this issue. :param max_lines: Max lines of context to return :param tabbed: Use tabbing in the output :return: strings of code ''' lines = [] max_lines = max(max_lines, 1) lmin = max(1, self.lineno - max_lines // 2) lmax = lmin + len(self.linerange) + max_lines - 1 tmplt = "%i\t%s" if tabbed else "%i %s" for line in moves.xrange(lmin, lmax): text = linecache.getline(self.fname, line) if isinstance(text, bytes): text = text.decode('utf-8') if not len(text): break lines.append(tmplt % (line, text)) return ''.join(lines) def as_dict(self, with_code=True): '''Convert the issue to a dict of values for outputting.''' out = { 'filename': self.fname, 'test_name': self.test, 'test_id': self.test_id, 'issue_severity': self.severity, 'issue_confidence': self.confidence, 'issue_text': self.text.encode('utf-8').decode('utf-8'), 'line_number': self.lineno, 'line_range': self.linerange, } if with_code: out['code'] = self.get_code() return out def from_dict(self, data, with_code=True): self.code = data["code"] self.fname = data["filename"] self.severity = data["issue_severity"] self.confidence = data["issue_confidence"] self.text = data["issue_text"] self.test = data["test_name"] self.test_id = data["test_id"] self.lineno = data["line_number"] self.linerange = data["line_range"] def issue_from_dict(data): i = Issue(severity=data["issue_severity"]) i.from_dict(data) return i bandit-1.4.0/bandit/core/manager.py000066400000000000000000000350121303375232700171400ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections import fnmatch import json import logging import os import sys import traceback from bandit.core import constants as b_constants from bandit.core import extension_loader from bandit.core import issue from bandit.core import meta_ast as b_meta_ast from bandit.core import metrics from bandit.core import node_visitor as b_node_visitor from bandit.core import test_set as b_test_set LOG = logging.getLogger(__name__) class BanditManager(object): scope = [] def __init__(self, config, agg_type, debug=False, verbose=False, profile=None, ignore_nosec=False): '''Get logger, config, AST handler, and result store ready :param config: config options object :type config: bandit.core.BanditConfig :param agg_type: aggregation type :param debug: Whether to show debug messages or not :param verbose: Whether to show verbose output :param profile_name: Optional name of profile to use (from cmd line) :param ignore_nosec: Whether to ignore #nosec or not :return: ''' self.debug = debug self.verbose = verbose if not profile: profile = {} self.ignore_nosec = ignore_nosec self.b_conf = config self.files_list = [] self.excluded_files = [] self.b_ma = b_meta_ast.BanditMetaAst() self.skipped = [] self.results = [] self.baseline = [] self.agg_type = agg_type self.metrics = metrics.Metrics() self.b_ts = b_test_set.BanditTestSet(config, profile) # set the increment of after how many files to show progress self.progress = b_constants.progress_increment self.scores = [] def get_skipped(self): ret = [] # "skip" is a tuple of name and reason, decode just the name for skip in self.skipped: if isinstance(skip[0], bytes): ret.append((skip[0].decode('utf-8'), skip[1])) else: ret.append(skip) return ret def get_issue_list(self, sev_level=b_constants.LOW, conf_level=b_constants.LOW): return self.filter_results(sev_level, conf_level) def populate_baseline(self, data): '''Populate a baseline set of issues from a JSON report This will populate a list of baseline issues discovered from a previous run of bandit. Later this baseline can be used to filter out the result set, see filter_results. ''' items = [] try: jdata = json.loads(data) items = [issue.issue_from_dict(j) for j in jdata["results"]] except Exception as e: LOG.warning("Failed to load baseline data: %s", e) self.baseline = items def filter_results(self, sev_filter, conf_filter): '''Returns a list of results filtered by the baseline This works by checking the number of results returned from each file we process. If the number of results is different to the number reported for the same file in the baseline, then we return all results for the file. We can't reliably return just the new results, as line numbers will likely have changed. :param sev_filter: severity level filter to apply :param conf_filter: confidence level filter to apply ''' results = [i for i in self.results if i.filter(sev_filter, conf_filter)] if not self.baseline: return results unmatched = _compare_baseline_results(self.baseline, results) # if it's a baseline we'll return a dictionary of issues and a list of # candidate issues return _find_candidate_matches(unmatched, results) def results_count(self, sev_filter=b_constants.LOW, conf_filter=b_constants.LOW): '''Return the count of results :param sev_filter: Severity level to filter lower :param conf_filter: Confidence level to filter :return: Number of results in the set ''' return len(self.get_issue_list(sev_filter, conf_filter)) def output_results(self, lines, sev_level, conf_level, output_file, output_format): '''Outputs results from the result store :param lines: How many surrounding lines to show per result :param sev_level: Which severity levels to show (LOW, MEDIUM, HIGH) :param conf_level: Which confidence levels to show (LOW, MEDIUM, HIGH) :param output_file: File to store results :param output_format: output format plugin name :return: - ''' try: formatters_mgr = extension_loader.MANAGER.formatters_mgr if output_format not in formatters_mgr: output_format = 'screen' if sys.stdout.isatty() else 'txt' formatter = formatters_mgr[output_format] report_func = formatter.plugin report_func(self, fileobj=output_file, sev_level=sev_level, conf_level=conf_level, lines=lines) except Exception as e: raise RuntimeError("Unable to output report using '%s' formatter: " "%s" % (output_format, str(e))) def discover_files(self, targets, recursive=False, excluded_paths=''): '''Add tests directly and from a directory to the test set :param targets: The command line list of files and directories :param recursive: True/False - whether to add all files from dirs :return: ''' # We'll mantain a list of files which are added, and ones which have # been explicitly excluded files_list = set() excluded_files = set() excluded_path_strings = self.b_conf.get_option('exclude_dirs') or [] included_globs = self.b_conf.get_option('include') or ['*.py'] # if there are command line provided exclusions add them to the list if excluded_paths: for path in excluded_paths.split(','): excluded_path_strings.append(path) # build list of files we will analyze for fname in targets: # if this is a directory and recursive is set, find all files if os.path.isdir(fname): if recursive: new_files, newly_excluded = _get_files_from_dir( fname, included_globs=included_globs, excluded_path_strings=excluded_path_strings ) files_list.update(new_files) excluded_files.update(newly_excluded) else: LOG.warning("Skipping directory (%s), use -r flag to " "scan contents", fname) else: # if the user explicitly mentions a file on command line, # we'll scan it, regardless of whether it's in the included # file types list if _is_file_included(fname, included_globs, excluded_path_strings, enforce_glob=False): files_list.add(fname) else: excluded_files.add(fname) self.files_list = sorted(files_list) self.excluded_files = sorted(excluded_files) def run_tests(self): '''Runs through all files in the scope :return: - ''' # display progress, if number of files warrants it if len(self.files_list) > self.progress: sys.stderr.write("%s [" % len(self.files_list)) # if we have problems with a file, we'll remove it from the files_list # and add it to the skipped list instead new_files_list = list(self.files_list) for count, fname in enumerate(self.files_list): LOG.debug("working on file : %s", fname) if len(self.files_list) > self.progress: # is it time to update the progress indicator? if count % self.progress == 0: sys.stderr.write("%s.. " % count) sys.stderr.flush() try: if fname == '-': sys.stdin = os.fdopen(sys.stdin.fileno(), 'rb', 0) self._parse_file('', sys.stdin, new_files_list) else: with open(fname, 'rb') as fdata: self._parse_file(fname, fdata, new_files_list) except IOError as e: self.skipped.append((fname, e.strerror)) new_files_list.remove(fname) if len(self.files_list) > self.progress: sys.stderr.write("]\n") sys.stderr.flush() # reflect any files which may have been skipped self.files_list = new_files_list # do final aggregation of metrics self.metrics.aggregate() def _parse_file(self, fname, fdata, new_files_list): try: # parse the current file data = fdata.read() lines = data.splitlines() self.metrics.begin(fname) self.metrics.count_locs(lines) if self.ignore_nosec: nosec_lines = set() else: nosec_lines = set( lineno + 1 for (lineno, line) in enumerate(lines) if b'#nosec' in line or b'# nosec' in line) score = self._execute_ast_visitor(fname, data, nosec_lines) self.scores.append(score) self.metrics.count_issues([score, ]) except KeyboardInterrupt as e: sys.exit(2) except SyntaxError as e: self.skipped.append((fname, "syntax error while parsing AST from file")) new_files_list.remove(fname) except Exception as e: LOG.error("Exception occurred when executing tests against " "%s. Run \"bandit --debug %s\" to see the full " "traceback.", fname, fname) self.skipped.append((fname, 'exception while scanning file')) new_files_list.remove(fname) LOG.debug(" Exception string: %s", e) LOG.debug(" Exception traceback: %s", traceback.format_exc()) def _execute_ast_visitor(self, fname, data, nosec_lines): '''Execute AST parse on each file :param fname: The name of the file being parsed :param data: Original file contents :param lines: The lines of code to process :return: The accumulated test score ''' score = [] res = b_node_visitor.BanditNodeVisitor(fname, self.b_ma, self.b_ts, self.debug, nosec_lines, self.metrics) score = res.process(data) self.results.extend(res.tester.results) return score def _get_files_from_dir(files_dir, included_globs=None, excluded_path_strings=None): if not included_globs: included_globs = ['*.py'] if not excluded_path_strings: excluded_path_strings = [] files_list = set() excluded_files = set() for root, subdirs, files in os.walk(files_dir): for filename in files: path = os.path.join(root, filename) if _is_file_included(path, included_globs, excluded_path_strings): files_list.add(path) else: excluded_files.add(path) return files_list, excluded_files def _is_file_included(path, included_globs, excluded_path_strings, enforce_glob=True): '''Determine if a file should be included based on filename This utility function determines if a file should be included based on the file name, a list of parsed extensions, excluded paths, and a flag specifying whether extensions should be enforced. :param path: Full path of file to check :param parsed_extensions: List of parsed extensions :param excluded_paths: List of paths from which we should not include files :param enforce_glob: Can set to false to bypass extension check :return: Boolean indicating whether a file should be included ''' return_value = False # if this is matches a glob of files we look at, and it isn't in an # excluded path if _matches_glob_list(path, included_globs) or not enforce_glob: if not any(x in path for x in excluded_path_strings): return_value = True return return_value def _matches_glob_list(filename, glob_list): for glob in glob_list: if fnmatch.fnmatch(filename, glob): return True return False def _compare_baseline_results(baseline, results): """Compare a baseline list of issues to list of results This function compares a baseline set of issues to a current set of issues to find results that weren't present in the baseline. :param baseline: Baseline list of issues :param results: Current list of issues :return: List of unmatched issues """ return [a for a in results if a not in baseline] def _find_candidate_matches(unmatched_issues, results_list): """Returns a dictionary with issue candidates For example, let's say we find a new command injection issue in a file which used to have two. Bandit can't tell which of the command injection issues in the file are new, so it will show all three. The user should be able to pick out the new one. :param unmatched_issues: List of issues that weren't present before :param results_list: Master list of current Bandit findings :return: A dictionary with a list of candidates for each issue """ issue_candidates = collections.OrderedDict() for unmatched in unmatched_issues: issue_candidates[unmatched] = ([i for i in results_list if unmatched == i]) return issue_candidates bandit-1.4.0/bandit/core/meta_ast.py000066400000000000000000000031731303375232700173260ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections import logging LOG = logging.getLogger(__name__) class BanditMetaAst(object): nodes = collections.OrderedDict() def __init__(self): pass def add_node(self, node, parent_id, depth): '''Add a node to the AST node collection :param node: The AST node to add :param parent_id: The ID of the node's parent :param depth: The depth of the node :return: - ''' node_id = hex(id(node)) LOG.debug('adding node : %s [%s]', node_id, depth) self.nodes[node_id] = { 'raw': node, 'parent_id': parent_id, 'depth': depth } def __str__(self): '''Dumps a listing of all of the nodes Dumps a listing of all of the nodes for debugging purposes :return: - ''' tmpstr = "" for k, v in self.nodes.items(): tmpstr += "Node: %s\n" % k tmpstr += "\t%s\n" % str(v) tmpstr += "Length: %s\n" % len(self.nodes) return tmpstr bandit-1.4.0/bandit/core/metrics.py000066400000000000000000000070311303375232700171740ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2015 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections from bandit.core import constants class Metrics(object): """Bandit metric gathering. This class is a singleton used to gather and process metrics collected when processing a code base with bandit. Metric collection is stateful, that is, an active metric block will be set when requested and all subsequent operations will effect that metric block until it is replaced by a setting a new one. """ def __init__(self): self.data = dict() self.data['_totals'] = {'loc': 0, 'nosec': 0} # initialize 0 totals for criteria and rank; this will be reset later for rank in constants.RANKING: for criteria in constants.CRITERIA: self.data['_totals']['{0}.{1}'.format(criteria[0], rank)] = 0 def begin(self, fname): """Begin a new metric block. This starts a new metric collection name "fname" and makes is active. :param fname: the metrics unique name, normally the file name. """ self.data[fname] = {'loc': 0, 'nosec': 0} self.current = self.data[fname] def note_nosec(self, num=1): """Note a "nosec" commnet. Increment the currently active metrics nosec count. :param num: number of nosecs seen, defaults to 1 """ self.current['nosec'] += num def count_locs(self, lines): """Count lines of code. We count lines that are not empty and are not comments. The result is added to our currently active metrics loc count (normally this is 0). :param lines: lines in the file to process """ def proc(line): tmp = line.strip() return bool(tmp and not tmp.startswith(b'#')) self.current['loc'] += sum(proc(line) for line in lines) def count_issues(self, scores): self.current.update(self._get_issue_counts(scores)) def aggregate(self): """Do final aggregation of metrics.""" c = collections.Counter() for fname in self.data: c.update(self.data[fname]) self.data['_totals'] = dict(c) @staticmethod def _get_issue_counts(scores): """Get issue counts aggregated by confidence/severity rankings. :param scores: list of scores to aggregate / count :return: aggregated total (count) of issues identified """ issue_counts = {} for score in scores: for (criteria, default) in constants.CRITERIA: for i, rank in enumerate(constants.RANKING): label = '{0}.{1}'.format(criteria, rank) if label not in issue_counts: issue_counts[label] = 0 count = ( score[criteria][i] / constants.RANKING_VALUES[rank] ) issue_counts[label] += count return issue_counts bandit-1.4.0/bandit/core/node_visitor.py000066400000000000000000000241711303375232700202360ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import ast import logging import operator from bandit.core import constants from bandit.core import tester as b_tester from bandit.core import utils as b_utils LOG = logging.getLogger(__name__) class BanditNodeVisitor(object): def __init__(self, fname, metaast, testset, debug, nosec_lines, metrics): self.debug = debug self.nosec_lines = nosec_lines self.seen = 0 self.scores = { 'SEVERITY': [0] * len(constants.RANKING), 'CONFIDENCE': [0] * len(constants.RANKING) } self.depth = 0 self.fname = fname self.metaast = metaast self.testset = testset self.imports = set() self.import_aliases = {} self.tester = b_tester.BanditTester( self.testset, self.debug, nosec_lines) # in some cases we can't determine a qualified name try: self.namespace = b_utils.get_module_qualname_from_path(fname) except b_utils.InvalidModulePath: LOG.info('Unable to find qualified name for module: %s', self.fname) self.namespace = "" LOG.debug('Module qualified name: %s', self.namespace) self.metrics = metrics def visit_ClassDef(self, node): '''Visitor for AST ClassDef node Add class name to current namespace for all descendants. :param node: Node being inspected :return: - ''' # For all child nodes, add this class name to current namespace self.namespace = b_utils.namespace_path_join(self.namespace, node.name) def visit_FunctionDef(self, node): '''Visitor for AST FunctionDef nodes add relevant information about the node to the context for use in tests which inspect function definitions. Add the function name to the current namespace for all descendants. :param node: The node that is being inspected :return: - ''' self.context['function'] = node qualname = self.namespace + '.' + b_utils.get_func_name(node) name = qualname.split('.')[-1] self.context['qualname'] = qualname self.context['name'] = name # For all child nodes and any tests run, add this function name to # current namespace self.namespace = b_utils.namespace_path_join(self.namespace, name) self.update_scores(self.tester.run_tests(self.context, 'FunctionDef')) def visit_Call(self, node): '''Visitor for AST Call nodes add relevant information about the node to the context for use in tests which inspect function calls. :param node: The node that is being inspected :return: - ''' self.context['call'] = node qualname = b_utils.get_call_name(node, self.import_aliases) name = qualname.split('.')[-1] self.context['qualname'] = qualname self.context['name'] = name self.update_scores(self.tester.run_tests(self.context, 'Call')) def visit_Import(self, node): '''Visitor for AST Import nodes add relevant information about node to the context for use in tests which inspect imports. :param node: The node that is being inspected :return: - ''' for nodename in node.names: if nodename.asname: self.import_aliases[nodename.asname] = nodename.name self.imports.add(nodename.name) self.context['module'] = nodename.name self.update_scores(self.tester.run_tests(self.context, 'Import')) def visit_ImportFrom(self, node): '''Visitor for AST ImportFrom nodes add relevant information about node to the context for use in tests which inspect imports. :param node: The node that is being inspected :return: - ''' module = node.module if module is None: return self.visit_Import(node) for nodename in node.names: # TODO(ljfisher) Names in import_aliases could be overridden # by local definitions. If this occurs bandit will see the # name in import_aliases instead of the local definition. # We need better tracking of names. if nodename.asname: self.import_aliases[nodename.asname] = ( module + "." + nodename.name ) else: # Even if import is not aliased we need an entry that maps # name to module.name. For example, with 'from a import b' # b should be aliased to the qualified name a.b self.import_aliases[nodename.name] = (module + '.' + nodename.name) self.imports.add(module + "." + nodename.name) self.context['module'] = module self.context['name'] = nodename.name self.update_scores(self.tester.run_tests(self.context, 'ImportFrom')) def visit_Str(self, node): '''Visitor for AST String nodes add relevant information about node to the context for use in tests which inspect strings. :param node: The node that is being inspected :return: - ''' self.context['str'] = node.s if not isinstance(node.parent, ast.Expr): # docstring self.context['linerange'] = b_utils.linerange_fix(node.parent) self.update_scores(self.tester.run_tests(self.context, 'Str')) def visit_Bytes(self, node): '''Visitor for AST Bytes nodes add relevant information about node to the context for use in tests which inspect strings. :param node: The node that is being inspected :return: - ''' self.context['bytes'] = node.s if not isinstance(node.parent, ast.Expr): # docstring self.context['linerange'] = b_utils.linerange_fix(node.parent) self.update_scores(self.tester.run_tests(self.context, 'Bytes')) def pre_visit(self, node): self.context = {} self.context['imports'] = self.imports self.context['import_aliases'] = self.import_aliases if self.debug: LOG.debug(ast.dump(node)) self.metaast.add_node(node, '', self.depth) if hasattr(node, 'lineno'): self.context['lineno'] = node.lineno if node.lineno in self.nosec_lines: LOG.debug("skipped, nosec") self.metrics.note_nosec() return False self.context['node'] = node self.context['linerange'] = b_utils.linerange_fix(node) self.context['filename'] = self.fname self.seen += 1 LOG.debug("entering: %s %s [%s]", hex(id(node)), type(node), self.depth) self.depth += 1 LOG.debug(self.context) return True def visit(self, node): name = node.__class__.__name__ method = 'visit_' + name visitor = getattr(self, method, None) if visitor is not None: if self.debug: LOG.debug("%s called (%s)", method, ast.dump(node)) visitor(node) else: self.update_scores(self.tester.run_tests(self.context, name)) def post_visit(self, node): self.depth -= 1 LOG.debug("%s\texiting : %s", self.depth, hex(id(node))) # HACK(tkelsey): this is needed to clean up post-recursion stuff that # gets setup in the visit methods for these node types. if isinstance(node, ast.FunctionDef) or isinstance(node, ast.ClassDef): self.namespace = b_utils.namespace_path_split(self.namespace)[0] def generic_visit(self, node): """Drive the visitor.""" for _, value in ast.iter_fields(node): if isinstance(value, list): max_idx = len(value) - 1 for idx, item in enumerate(value): if isinstance(item, ast.AST): if idx < max_idx: setattr(item, 'sibling', value[idx + 1]) else: setattr(item, 'sibling', None) setattr(item, 'parent', node) if self.pre_visit(item): self.visit(item) self.generic_visit(item) self.post_visit(item) elif isinstance(value, ast.AST): setattr(value, 'sibling', None) setattr(value, 'parent', node) if self.pre_visit(value): self.visit(value) self.generic_visit(value) self.post_visit(value) def update_scores(self, scores): '''Score updater Since we moved from a single score value to a map of scores per severity, this is needed to update the stored list. :param score: The score list to update our scores with ''' # we'll end up with something like: # SEVERITY: {0, 0, 0, 10} where 10 is weighted by finding and level for score_type in self.scores: self.scores[score_type] = list(map( operator.add, self.scores[score_type], scores[score_type] )) def process(self, data): '''Main process loop Build and process the AST :param lines: lines code to process :return score: the aggregated score for the current file ''' f_ast = ast.parse(data) self.generic_visit(f_ast) return self.scores bandit-1.4.0/bandit/core/test_properties.py000066400000000000000000000047261303375232700207710ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import logging from bandit.core import utils LOG = logging.getLogger(__name__) def checks(*args): '''Decorator function to set checks to be run.''' def wrapper(func): if not hasattr(func, "_checks"): func._checks = [] func._checks.extend(utils.check_ast_node(a) for a in args) LOG.debug('checks() decorator executed') LOG.debug(' func._checks: %s', func._checks) return func return wrapper def takes_config(*args): '''Test function takes config Use of this delegate before a test function indicates that it should be passed data from the config file. Passing a name parameter allows aliasing tests and thus sharing config options. ''' name = "" def _takes_config(func): if not hasattr(func, "_takes_config"): func._takes_config = name return func if len(args) == 1 and callable(args[0]): name = args[0].__name__ return _takes_config(args[0]) else: name = args[0] return _takes_config def test_id(id_val): '''Test function identifier Use this decorator before a test function indicates its simple ID ''' def _has_id(func): if not hasattr(func, "_test_id"): func._test_id = id_val return func return _has_id def accepts_baseline(*args): """Decorator to indicate formatter accepts baseline results Use of this decorator before a formatter indicates that it is able to deal with baseline results. Specifically this means it has a way to display candidate results and know when it should do so. """ def wrapper(func): if not hasattr(func, '_accepts_baseline'): func._accepts_baseline = True LOG.debug('accepts_baseline() decorator executed on %s', func.__name__) return func return wrapper(args[0]) bandit-1.4.0/bandit/core/test_set.py000066400000000000000000000107631303375232700173660ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import importlib import logging import six from bandit.core import blacklisting from bandit.core import extension_loader LOG = logging.getLogger(__name__) class BanditTestSet(object): def __init__(self, config, profile=None): if not profile: profile = {} extman = extension_loader.MANAGER filtering = self._get_filter(config, profile) self.plugins = [p for p in extman.plugins if p.plugin._test_id in filtering] self.plugins.extend(self._load_builtins(filtering, profile)) self._load_tests(config, self.plugins) @staticmethod def _get_filter(config, profile): extman = extension_loader.MANAGER inc = set(profile.get('include', [])) exc = set(profile.get('exclude', [])) all_blacklist_tests = set() for _node, tests in six.iteritems(extman.blacklist): all_blacklist_tests.update(t['id'] for t in tests) # this block is purely for backwards compatibility, the rules are as # follows: # B001,B401 means B401 # B401 means B401 # B001 means all blacklist tests if 'B001' in inc: if not inc.intersection(all_blacklist_tests): inc.update(all_blacklist_tests) inc.discard('B001') if 'B001' in exc: if not exc.intersection(all_blacklist_tests): exc.update(all_blacklist_tests) exc.discard('B001') if inc: filtered = inc else: filtered = set(extman.plugins_by_id.keys()) filtered.update(extman.builtin) filtered.update(all_blacklist_tests) return filtered - exc def _load_builtins(self, filtering, profile): '''loads up builtin functions, so they can be filtered.''' class Wrapper(object): def __init__(self, name, plugin): self.name = name self.plugin = plugin extman = extension_loader.MANAGER blacklist = profile.get('blacklist') if not blacklist: # not overridden by legacy data blacklist = {} for node, tests in six.iteritems(extman.blacklist): values = [t for t in tests if t['id'] in filtering] if values: blacklist[node] = values if not blacklist: return [] # this dresses up the blacklist to look like a plugin, but # the '_checks' data comes from the blacklist information. # the '_config' is the filtered blacklist data set. setattr(blacklisting.blacklist, "_test_id", 'B001') setattr(blacklisting.blacklist, "_checks", blacklist.keys()) setattr(blacklisting.blacklist, "_config", blacklist) return [Wrapper('blacklist', blacklisting.blacklist)] def _load_tests(self, config, plugins): '''Builds a dict mapping tests to node types.''' self.tests = {} for plugin in plugins: if hasattr(plugin.plugin, '_takes_config'): # TODO(??): config could come from profile ... cfg = config.get_option(plugin.plugin._takes_config) if cfg is None: genner = importlib.import_module(plugin.plugin.__module__) cfg = genner.gen_config(plugin.plugin._takes_config) plugin.plugin._config = cfg for check in plugin.plugin._checks: self.tests.setdefault(check, []).append(plugin.plugin) LOG.debug('added function %s (%s) targetting %s', plugin.name, plugin.plugin._test_id, check) def get_tests(self, checktype): '''Returns all tests that are of type checktype :param checktype: The type of test to filter on :return: A list of tests which are of the specified type ''' return self.tests.get(checktype) or [] bandit-1.4.0/bandit/core/tester.py000066400000000000000000000100271303375232700170330ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy import logging import warnings from bandit.core import constants from bandit.core import context as b_context from bandit.core import utils warnings.formatwarning = utils.warnings_formatter LOG = logging.getLogger(__name__) class BanditTester(object): def __init__(self, testset, debug, nosec_lines): self.results = [] self.testset = testset self.last_result = None self.debug = debug self.nosec_lines = nosec_lines def run_tests(self, raw_context, checktype): '''Runs all tests for a certain type of check, for example Runs all tests for a certain type of check, for example 'functions' store results in results. :param raw_context: Raw context dictionary :param checktype: The type of checks to run :param nosec_lines: Lines which should be skipped because of nosec :return: a score based on the number and type of test results ''' scores = { 'SEVERITY': [0] * len(constants.RANKING), 'CONFIDENCE': [0] * len(constants.RANKING) } tests = self.testset.get_tests(checktype) for test in tests: name = test.__name__ # execute test with the an instance of the context class temp_context = copy.copy(raw_context) context = b_context.Context(temp_context) try: if hasattr(test, '_config'): result = test(context, test._config) else: result = test(context) # if we have a result, record it and update scores if (result is not None and result.lineno not in self.nosec_lines and temp_context['lineno'] not in self.nosec_lines): if isinstance(temp_context['filename'], bytes): result.fname = temp_context['filename'].decode('utf-8') else: result.fname = temp_context['filename'] if result.lineno is None: result.lineno = temp_context['lineno'] result.linerange = temp_context['linerange'] result.test = name if result.test_id == "": result.test_id = test._test_id self.results.append(result) LOG.debug("Issue identified by %s: %s", name, result) sev = constants.RANKING.index(result.severity) val = constants.RANKING_VALUES[result.severity] scores['SEVERITY'][sev] += val con = constants.RANKING.index(result.confidence) val = constants.RANKING_VALUES[result.confidence] scores['CONFIDENCE'][con] += val except Exception as e: self.report_error(name, context, e) if self.debug: raise LOG.debug("Returning scores: %s", scores) return scores @staticmethod def report_error(test, context, error): what = "Bandit internal error running: " what += "%s " % test what += "on file %s at line %i: " % ( context._context['filename'], context._context['lineno'] ) what += str(error) import traceback what += traceback.format_exc() LOG.error(what) bandit-1.4.0/bandit/core/utils.py000066400000000000000000000253111303375232700166670ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import _ast import ast import logging import os.path import sys try: import configparser except ImportError: import ConfigParser as configparser LOG = logging.getLogger(__name__) """Various helper functions.""" def _get_attr_qual_name(node, aliases): '''Get a the full name for the attribute node. This will resolve a pseudo-qualified name for the attribute rooted at node as long as all the deeper nodes are Names or Attributes. This will give you how the code referenced the name but will not tell you what the name actually refers to. If we encounter a node without a static name we punt with an empty string. If this encounters something more complex, such as foo.mylist[0](a,b) we just return empty string. :param node: AST Name or Attribute node :param aliases: Import aliases dictionary :returns: Qualified name referred to by the attribute or name. ''' if isinstance(node, _ast.Name): if node.id in aliases: return aliases[node.id] return node.id elif isinstance(node, _ast.Attribute): name = '%s.%s' % (_get_attr_qual_name(node.value, aliases), node.attr) if name in aliases: return aliases[name] return name else: return "" def get_call_name(node, aliases): if isinstance(node.func, _ast.Name): if deepgetattr(node, 'func.id') in aliases: return aliases[deepgetattr(node, 'func.id')] return deepgetattr(node, 'func.id') elif isinstance(node.func, _ast.Attribute): return _get_attr_qual_name(node.func, aliases) else: return "" def get_func_name(node): return node.name # TODO(tkelsey): get that qualname using enclosing scope def get_qual_attr(node, aliases): prefix = "" if isinstance(node, _ast.Attribute): try: val = deepgetattr(node, 'value.id') if val in aliases: prefix = aliases[val] else: prefix = deepgetattr(node, 'value.id') except Exception: # NOTE(tkelsey): degrade gracefully when we can't get the fully # qualified name for an attr, just return its base name. pass return "%s.%s" % (prefix, node.attr) else: return "" # TODO(tkelsey): process other node types def deepgetattr(obj, attr): """Recurses through an attribute chain to get the ultimate value.""" for key in attr.split('.'): obj = getattr(obj, key) return obj class InvalidModulePath(Exception): pass class ConfigError(Exception): """Raised when the config file fails validation.""" def __init__(self, message, config_file): self.config_file = config_file self.message = "{0} : {1}".format(config_file, message) super(ConfigError, self).__init__(self.message) class ProfileNotFound(Exception): """Raised when chosen profile cannot be found.""" def __init__(self, config_file, profile): self.config_file = config_file self.profile = profile message = 'Unable to find profile (%s) in config file: %s' % ( self.profile, self.config_file) super(ProfileNotFound, self).__init__(message) def warnings_formatter(message, category=UserWarning, filename='', lineno=-1, line=''): '''Monkey patch for warnings.warn to suppress cruft output.''' return "{0}\n".format(message) def get_module_qualname_from_path(path): '''Get the module's qualified name by analysis of the path. Resolve the absolute pathname and eliminate symlinks. This could result in an incorrect name if symlinks are used to restructure the python lib directory. Starting from the right-most directory component look for __init__.py in the directory component. If it exists then the directory name is part of the module name. Move left to the subsequent directory components until a directory is found without __init__.py. :param: Path to module file. Relative paths will be resolved relative to current working directory. :return: fully qualified module name ''' (head, tail) = os.path.split(path) if head == '' or tail == '': raise InvalidModulePath('Invalid python file path: "%s"' ' Missing path or file name' % (path)) qname = [os.path.splitext(tail)[0]] while head not in ['/', '.']: if os.path.isfile(os.path.join(head, '__init__.py')): (head, tail) = os.path.split(head) qname.insert(0, tail) else: break qualname = '.'.join(qname) return qualname def namespace_path_join(base, name): '''Extend the current namespace path with an additional name Take a namespace path (i.e., package.module.class) and extends it with an additional name (i.e., package.module.class.subclass). This is similar to how os.path.join works. :param base: (String) The base namespace path. :param name: (String) The new name to append to the base path. :returns: (String) A new namespace path resulting from combination of base and name. ''' return '%s.%s' % (base, name) def namespace_path_split(path): '''Split the namespace path into a pair (head, tail). Tail will be the last namespace path component and head will be everything leading up to that in the path. This is similar to os.path.split. :param path: (String) A namespace path. :returns: (String, String) A tuple where the first component is the base path and the second is the last path component. ''' return tuple(path.rsplit('.', 1)) def escaped_bytes_representation(b): '''PY3 bytes need escaping for comparison with other strings. In practice it turns control characters into acceptable codepoints then encodes them into bytes again to turn unprintable bytes into printable escape sequences. This is safe to do for the whole range 0..255 and result matches unicode_escape on a unicode string. ''' return b.decode('unicode_escape').encode('unicode_escape') def linerange(node): """Get line number range from a node.""" strip = {"body": None, "orelse": None, "handlers": None, "finalbody": None} for key in strip.keys(): if hasattr(node, key): strip[key] = getattr(node, key) setattr(node, key, []) lines_min = 9999999999 lines_max = -1 for n in ast.walk(node): if hasattr(n, 'lineno'): lines_min = min(lines_min, n.lineno) lines_max = max(lines_max, n.lineno) for key in strip.keys(): if strip[key] is not None: setattr(node, key, strip[key]) if lines_max > -1: return list(range(lines_min, lines_max + 1)) return [0, 1] def linerange_fix(node): """Try and work around a known Python bug with multi-line strings.""" # deal with multiline strings lineno behavior (Python issue #16806) lines = linerange(node) if hasattr(node, 'sibling') and hasattr(node.sibling, 'lineno'): start = min(lines) delta = node.sibling.lineno - start if delta > 1: return list(range(start, node.sibling.lineno)) return lines def concat_string(node, stop=None): '''Builds a string from a ast.BinOp chain. This will build a string from a series of ast.Str nodes wrapped in ast.BinOp nodes. Something like "a" + "b" + "c" or "a %s" % val etc. The provided node can be any participant in the BinOp chain. :param node: (ast.Str or ast.BinOp) The node to process :param stop: (ast.Str or ast.BinOp) Optional base node to stop at :returns: (Tuple) the root node of the expression, the string value ''' def _get(node, bits, stop=None): if node != stop: bits.append( _get(node.left, bits, stop) if isinstance(node.left, ast.BinOp) else node.left) bits.append( _get(node.right, bits, stop) if isinstance(node.right, ast.BinOp) else node.right) bits = [node] while isinstance(node.parent, ast.BinOp): node = node.parent if isinstance(node, ast.BinOp): _get(node, bits, stop) return (node, " ".join([x.s for x in bits if isinstance(x, ast.Str)])) def get_called_name(node): '''Get a function name from an ast.Call node. An ast.Call node representing a method call with present differently to one wrapping a function call: thing.call() vs call(). This helper will grab the unqualified call name correctly in either case. :param node: (ast.Call) the call node :returns: (String) the function name ''' func = node.func try: return func.attr if isinstance(func, ast.Attribute) else func.id except AttributeError: return "" def get_path_for_function(f): '''Get the path of the file where the function is defined. :returns: the path, or None if one could not be found or f is not a real function ''' if hasattr(f, "__module__"): module_name = f.__module__ elif hasattr(f, "im_func"): module_name = f.im_func.__module__ else: LOG.warning("Cannot resolve file where %s is defined", f) return None module = sys.modules[module_name] if hasattr(module, "__file__"): return module.__file__ else: LOG.warning("Cannot resolve file path for module %s", module_name) return None def parse_ini_file(f_loc): config = configparser.ConfigParser() try: config.read(f_loc) return {k: v for k, v in config.items('bandit')} except (configparser.Error, KeyError, TypeError): LOG.warning("Unable to parse config file %s or missing [bandit] " "section", f_loc) return None def check_ast_node(name): 'Check if the given name is that of a valid AST node.' try: node = getattr(ast, name) if issubclass(node, ast.AST): return name except AttributeError: # nosec(tkelsey): catching expected exception pass raise TypeError("Error: %s is not a valid node type in AST" % name) bandit-1.4.0/bandit/formatters/000077500000000000000000000000001303375232700164115ustar00rootroot00000000000000bandit-1.4.0/bandit/formatters/__init__.py000066400000000000000000000000001303375232700205100ustar00rootroot00000000000000bandit-1.4.0/bandit/formatters/csv.py000066400000000000000000000046151303375232700175640ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ============= CSV Formatter ============= This formatter outputs the issues in a comma separated values format. :Example: .. code-block:: none filename,test_name,test_id,issue_severity,issue_confidence,issue_text, line_number,line_range examples/yaml_load.py,blacklist_calls,B301,MEDIUM,HIGH,"Use of unsafe yaml load. Allows instantiation of arbitrary objects. Consider yaml.safe_load(). ",5,[5] .. versionadded:: 0.11.0 """ # Necessary for this formatter to work when imported on Python 2. Importing # the standard library's csv module conflicts with the name of this module. from __future__ import absolute_import import csv import logging import sys LOG = logging.getLogger(__name__) def report(manager, fileobj, sev_level, conf_level, lines=-1): '''Prints issues in CSV format :param manager: the bandit manager object :param fileobj: The output file object, which may be sys.stdout :param sev_level: Filtering severity level :param conf_level: Filtering confidence level :param lines: Number of lines to report, -1 for all ''' results = manager.get_issue_list(sev_level=sev_level, conf_level=conf_level) with fileobj: fieldnames = ['filename', 'test_name', 'test_id', 'issue_severity', 'issue_confidence', 'issue_text', 'line_number', 'line_range'] writer = csv.DictWriter(fileobj, fieldnames=fieldnames, extrasaction='ignore') writer.writeheader() for result in results: writer.writerow(result.as_dict(with_code=False)) if fileobj.name != sys.stdout.name: LOG.info("CSV output written to file: %s", fileobj.name) bandit-1.4.0/bandit/formatters/html.py000066400000000000000000000214371303375232700177360ustar00rootroot00000000000000# Copyright (c) 2015 Rackspace, Inc. # Copyright (c) 2015 Hewlett Packard Enterprise # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ============== HTML formatter ============== This formatter outputs the issues as HTML. :Example: .. code-block:: html Bandit Report
Metrics:
Total lines of code: 9
Total lines skipped (#nosec): 0

yaml_load: Use of unsafe yaml load. Allows instantiation of arbitrary objects. Consider yaml.safe_load().
Test ID: B506
Severity: MEDIUM
Confidence: HIGH
File: examples/yaml_load.py
More info: http://docs.openstack.org/developer/bandit/plugins/yaml_load.html
    5       ystr = yaml.dump({'a' : 1, 'b' : 2, 'c' : 3})
    6       y = yaml.load(ystr)
    7       yaml.dump(y)
    
.. versionadded:: 0.14.0 """ import cgi import logging import sys from bandit.core import docs_utils from bandit.core import test_properties from bandit.formatters import utils LOG = logging.getLogger(__name__) @test_properties.accepts_baseline def report(manager, fileobj, sev_level, conf_level, lines=-1): """Writes issues to 'fileobj' in HTML format :param manager: the bandit manager object :param fileobj: The output file object, which may be sys.stdout :param sev_level: Filtering severity level :param conf_level: Filtering confidence level :param lines: Number of lines to report, -1 for all """ header_block = u""" Bandit Report """ report_block = u""" {metrics} {skipped}
{results}
""" issue_block = u"""
{test_name}: {test_text}
Test ID: {test_id}
Severity: {severity}
Confidence: {confidence}
File: {path}
More info: {url}
{code} {candidates}
""" code_block = u"""
{code}
""" candidate_block = u"""

Candidates: {candidate_list}
""" candidate_issue = u"""
{code}
""" skipped_block = u"""
Skipped files:

{files_list}
""" metrics_block = u"""
Metrics:
Total lines of code: {loc}
Total lines skipped (#nosec): {nosec}
""" issues = manager.get_issue_list(sev_level=sev_level, conf_level=conf_level) baseline = not isinstance(issues, list) # build the skipped string to insert in the report skipped_str = ''.join('%s reason: %s
' % (fname, reason) for fname, reason in manager.get_skipped()) if skipped_str: skipped_text = skipped_block.format(files_list=skipped_str) else: skipped_text = '' # build the results string to insert in the report results_str = '' for index, issue in enumerate(issues): if not baseline or len(issues[issue]) == 1: candidates = '' safe_code = cgi.escape(issue.get_code(lines, True). strip('\n').lstrip(' ')) code = code_block.format(code=safe_code) else: candidates_str = '' code = '' for candidate in issues[issue]: candidate_code = cgi.escape(candidate.get_code(lines, True). strip('\n').lstrip(' ')) candidates_str += candidate_issue.format(code=candidate_code) candidates = candidate_block.format(candidate_list=candidates_str) url = docs_utils.get_url(issue.test_id) results_str += issue_block.format(issue_no=index, issue_class='issue-sev-{}'. format(issue.severity.lower()), test_name=issue.test, test_id=issue.test_id, test_text=issue.text, severity=issue.severity, confidence=issue.confidence, path=issue.fname, code=code, candidates=candidates, url=url) # build the metrics string to insert in the report metrics_summary = metrics_block.format( loc=manager.metrics.data['_totals']['loc'], nosec=manager.metrics.data['_totals']['nosec']) # build the report and output it report_contents = report_block.format(metrics=metrics_summary, skipped=skipped_text, results=results_str) with fileobj: wrapped_file = utils.wrap_file_object(fileobj) wrapped_file.write(utils.convert_file_contents(header_block)) wrapped_file.write(utils.convert_file_contents(report_contents)) if fileobj.name != sys.stdout.name: LOG.info("HTML output written to file: %s", fileobj.name) bandit-1.4.0/bandit/formatters/json.py000066400000000000000000000104341303375232700177360ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ============== JSON formatter ============== This formatter outputs the issues in JSON. :Example: .. code-block:: javascript { "errors": [], "generated_at": "2015-12-16T22:27:34Z", "metrics": { "_totals": { "CONFIDENCE.HIGH": 1, "CONFIDENCE.LOW": 0, "CONFIDENCE.MEDIUM": 0, "CONFIDENCE.UNDEFINED": 0, "SEVERITY.HIGH": 0, "SEVERITY.LOW": 0, "SEVERITY.MEDIUM": 1, "SEVERITY.UNDEFINED": 0, "loc": 5, "nosec": 0 }, "examples/yaml_load.py": { "CONFIDENCE.HIGH": 1, "CONFIDENCE.LOW": 0, "CONFIDENCE.MEDIUM": 0, "CONFIDENCE.UNDEFINED": 0, "SEVERITY.HIGH": 0, "SEVERITY.LOW": 0, "SEVERITY.MEDIUM": 1, "SEVERITY.UNDEFINED": 0, "loc": 5, "nosec": 0 } }, "results": [ { "code": "4 ystr = yaml.dump({'a' : 1, 'b' : 2, 'c' : 3})\n5 y = yaml.load(ystr)\n6 yaml.dump(y)\n", "filename": "examples/yaml_load.py", "issue_confidence": "HIGH", "issue_severity": "MEDIUM", "issue_text": "Use of unsafe yaml load. Allows instantiation of arbitrary objects. Consider yaml.safe_load().\n", "line_number": 5, "line_range": [ 5 ], "test_name": "blacklist_calls", "test_id": "B301" } ] } .. versionadded:: 0.10.0 """ # Necessary so we can import the standard library json module while continuing # to name this file json.py. (Python 2 only) from __future__ import absolute_import import datetime import json import logging import operator import sys from bandit.core import test_properties LOG = logging.getLogger(__name__) @test_properties.accepts_baseline def report(manager, fileobj, sev_level, conf_level, lines=-1): '''''Prints issues in JSON format :param manager: the bandit manager object :param fileobj: The output file object, which may be sys.stdout :param sev_level: Filtering severity level :param conf_level: Filtering confidence level :param lines: Number of lines to report, -1 for all ''' machine_output = {'results': [], 'errors': []} for (fname, reason) in manager.get_skipped(): machine_output['errors'].append({'filename': fname, 'reason': reason}) results = manager.get_issue_list(sev_level=sev_level, conf_level=conf_level) baseline = not isinstance(results, list) if baseline: collector = [] for r in results: d = r.as_dict() if len(results[r]) > 1: d['candidates'] = [c.as_dict() for c in results[r]] collector.append(d) else: collector = [r.as_dict() for r in results] itemgetter = operator.itemgetter if manager.agg_type == 'vuln': machine_output['results'] = sorted(collector, key=itemgetter('test_name')) else: machine_output['results'] = sorted(collector, key=itemgetter('filename')) machine_output['metrics'] = manager.metrics.data # timezone agnostic format TS_FORMAT = "%Y-%m-%dT%H:%M:%SZ" time_string = datetime.datetime.utcnow().strftime(TS_FORMAT) machine_output['generated_at'] = time_string result = json.dumps(machine_output, sort_keys=True, indent=2, separators=(',', ': ')) with fileobj: fileobj.write(result) if fileobj.name != sys.stdout.name: LOG.info("JSON output written to file: %s", fileobj.name) bandit-1.4.0/bandit/formatters/screen.py000066400000000000000000000135331303375232700202470ustar00rootroot00000000000000# Copyright (c) 2015 Hewlett Packard Enterprise # -*- coding:utf-8 -*- # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ================ Screen formatter ================ This formatter outputs the issues as color coded text. :Example: .. code-block:: none >> Issue: [B301:blacklist_calls] Use of unsafe yaml load. Allows instantiation of arbitrary objects. Consider yaml.safe_load(). Severity: Medium Confidence: High Location: examples/yaml_load.py:5 4 ystr = yaml.dump({'a' : 1, 'b' : 2, 'c' : 3}) 5 y = yaml.load(ystr) 6 yaml.dump(y) .. versionadded:: 0.9.0 """ from __future__ import print_function import datetime import logging import sys from bandit.core import constants from bandit.core import test_properties LOG = logging.getLogger(__name__) COLOR = { 'DEFAULT': '\033[0m', 'HEADER': '\033[95m', 'LOW': '\033[94m', 'MEDIUM': '\033[93m', 'HIGH': '\033[91m', } def header(text, *args): return u'%s%s%s' % (COLOR['HEADER'], (text % args), COLOR['DEFAULT']) def get_verbose_details(manager): bits = [] bits.append(header(u'Files in scope (%i):', len(manager.files_list))) tpl = u"\t%s (score: {SEVERITY: %i, CONFIDENCE: %i})" bits.extend([tpl % (item, sum(score['SEVERITY']), sum(score['CONFIDENCE'])) for (item, score) in zip(manager.files_list, manager.scores)]) bits.append(header(u'Files excluded (%i):', len(manager.excluded_files))) bits.extend([u"\t%s" % fname for fname in manager.excluded_files]) return '\n'.join([str(bit) for bit in bits]) def get_metrics(manager): bits = [] bits.append(header("\nRun metrics:")) for (criteria, default) in constants.CRITERIA: bits.append("\tTotal issues (by %s):" % (criteria.lower())) for rank in constants.RANKING: bits.append("\t\t%s: %s" % ( rank.capitalize(), manager.metrics.data['_totals']['%s.%s' % (criteria, rank)])) return '\n'.join([str(bit) for bit in bits]) def _output_issue_str(issue, indent, show_lineno=True, show_code=True, lines=-1): # returns a list of lines that should be added to the existing lines list bits = [] bits.append("%s%s>> Issue: [%s:%s] %s" % ( indent, COLOR[issue.severity], issue.test_id, issue.test, issue.text)) bits.append("%s Severity: %s Confidence: %s" % ( indent, issue.severity.capitalize(), issue.confidence.capitalize())) bits.append("%s Location: %s:%s%s" % ( indent, issue.fname, issue.lineno if show_lineno else "", COLOR['DEFAULT'])) if show_code: bits.extend([indent + l for l in issue.get_code(lines, True).split('\n')]) return '\n'.join([bit for bit in bits]) def get_results(manager, sev_level, conf_level, lines): bits = [] issues = manager.get_issue_list(sev_level, conf_level) baseline = not isinstance(issues, list) candidate_indent = ' ' * 10 if not len(issues): return u"\tNo issues identified." for issue in issues: # if not a baseline or only one candidate we know the issue if not baseline or len(issues[issue]) == 1: bits.append(_output_issue_str(issue, "", lines=lines)) # otherwise show the finding and the candidates else: bits.append(_output_issue_str(issue, "", show_lineno=False, show_code=False)) bits.append(u'\n-- Candidate Issues --') for candidate in issues[issue]: bits.append(_output_issue_str(candidate, candidate_indent, lines=lines)) bits.append('\n') bits.append(u'-' * 50) return '\n'.join([bit for bit in bits]) def do_print(bits): # needed so we can mock this stuff print('\n'.join([bit for bit in bits])) @test_properties.accepts_baseline def report(manager, fileobj, sev_level, conf_level, lines=-1): """Prints discovered issues formatted for screen reading This makes use of VT100 terminal codes for colored text. :param manager: the bandit manager object :param fileobj: The output file object, which may be sys.stdout :param sev_level: Filtering severity level :param conf_level: Filtering confidence level :param lines: Number of lines to report, -1 for all """ bits = [] bits.append(header("Run started:%s", datetime.datetime.utcnow())) if manager.verbose: bits.append(get_verbose_details(manager)) bits.append(header("\nTest results:")) bits.append(get_results(manager, sev_level, conf_level, lines)) bits.append(header("\nCode scanned:")) bits.append('\tTotal lines of code: %i' % (manager.metrics.data['_totals']['loc'])) bits.append('\tTotal lines skipped (#nosec): %i' % (manager.metrics.data['_totals']['nosec'])) bits.append(get_metrics(manager)) skipped = manager.get_skipped() bits.append(header("Files skipped (%i):", len(skipped))) bits.extend(["\t%s (%s)" % skip for skip in skipped]) do_print(bits) if fileobj.name != sys.stdout.name: LOG.info("Screen formatter output was not written to file: %s, " "consider '-f txt'", fileobj.name) bandit-1.4.0/bandit/formatters/text.py000066400000000000000000000127101303375232700177500ustar00rootroot00000000000000# Copyright (c) 2015 Hewlett Packard Enterprise # -*- coding:utf-8 -*- # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ============== Text Formatter ============== This formatter outputs the issues as plain text. :Example: .. code-block:: none >> Issue: [B301:blacklist_calls] Use of unsafe yaml load. Allows instantiation of arbitrary objects. Consider yaml.safe_load(). Severity: Medium Confidence: High Location: examples/yaml_load.py:5 4 ystr = yaml.dump({'a' : 1, 'b' : 2, 'c' : 3}) 5 y = yaml.load(ystr) 6 yaml.dump(y) .. versionadded:: 0.9.0 """ from __future__ import print_function import datetime import logging import sys from bandit.core import constants from bandit.core import test_properties from bandit.formatters import utils LOG = logging.getLogger(__name__) def get_verbose_details(manager): bits = [] bits.append(u'Files in scope (%i):' % len(manager.files_list)) tpl = u"\t%s (score: {SEVERITY: %i, CONFIDENCE: %i})" bits.extend([tpl % (item, sum(score['SEVERITY']), sum(score['CONFIDENCE'])) for (item, score) in zip(manager.files_list, manager.scores)]) bits.append(u'Files excluded (%i):' % len(manager.excluded_files)) bits.extend([u"\t%s" % fname for fname in manager.excluded_files]) return '\n'.join([bit for bit in bits]) def get_metrics(manager): bits = [] bits.append("\nRun metrics:") for (criteria, default) in constants.CRITERIA: bits.append("\tTotal issues (by %s):" % (criteria.lower())) for rank in constants.RANKING: bits.append("\t\t%s: %s" % ( rank.capitalize(), manager.metrics.data['_totals']['%s.%s' % (criteria, rank)])) return '\n'.join([bit for bit in bits]) def _output_issue_str(issue, indent, show_lineno=True, show_code=True, lines=-1): # returns a list of lines that should be added to the existing lines list bits = [] bits.append("%s>> Issue: [%s:%s] %s" % ( indent, issue.test_id, issue.test, issue.text)) bits.append("%s Severity: %s Confidence: %s" % ( indent, issue.severity.capitalize(), issue.confidence.capitalize())) bits.append("%s Location: %s:%s" % ( indent, issue.fname, issue.lineno if show_lineno else "")) if show_code: bits.extend([indent + l for l in issue.get_code(lines, True).split('\n')]) return '\n'.join([bit for bit in bits]) def get_results(manager, sev_level, conf_level, lines): bits = [] issues = manager.get_issue_list(sev_level, conf_level) baseline = not isinstance(issues, list) candidate_indent = ' ' * 10 if not len(issues): return u"\tNo issues identified." for issue in issues: # if not a baseline or only one candidate we know the issue if not baseline or len(issues[issue]) == 1: bits.append(_output_issue_str(issue, "", lines=lines)) # otherwise show the finding and the candidates else: bits.append(_output_issue_str(issue, "", show_lineno=False, show_code=False)) bits.append(u'\n-- Candidate Issues --') for candidate in issues[issue]: bits.append(_output_issue_str(candidate, candidate_indent, lines=lines)) bits.append('\n') bits.append(u'-' * 50) return '\n'.join([bit for bit in bits]) @test_properties.accepts_baseline def report(manager, fileobj, sev_level, conf_level, lines=-1): """Prints discovered issues in the text format :param manager: the bandit manager object :param fileobj: The output file object, which may be sys.stdout :param sev_level: Filtering severity level :param conf_level: Filtering confidence level :param lines: Number of lines to report, -1 for all """ bits = [] bits.append("Run started:%s" % datetime.datetime.utcnow()) if manager.verbose: bits.append(get_verbose_details(manager)) bits.append("\nTest results:") bits.append(get_results(manager, sev_level, conf_level, lines)) bits.append("\nCode scanned:") bits.append('\tTotal lines of code: %i' % (manager.metrics.data['_totals']['loc'])) bits.append('\tTotal lines skipped (#nosec): %i' % (manager.metrics.data['_totals']['nosec'])) skipped = manager.get_skipped() bits.append(get_metrics(manager)) bits.append("Files skipped (%i):" % len(skipped)) bits.extend(["\t%s (%s)" % skip for skip in skipped]) result = '\n'.join([bit for bit in bits]) + '\n' with fileobj: wrapped_file = utils.wrap_file_object(fileobj) wrapped_file.write(utils.convert_file_contents(result)) if fileobj.name != sys.stdout.name: LOG.info("Text output written to file: %s", fileobj.name) bandit-1.4.0/bandit/formatters/utils.py000066400000000000000000000030651303375232700201270ustar00rootroot00000000000000# Copyright (c) 2016 Rackspace, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Utility functions for formatting plugins for Bandit.""" import io import six def wrap_file_object(fileobj): """Handle differences in Python 2 and 3 around writing bytes.""" # If it's not an instance of IOBase, we're probably using Python 2 and # that is less finnicky about writing text versus bytes to a file. if not isinstance(fileobj, io.IOBase): return fileobj # At this point we're using Python 3 and that will mangle text written to # a file written in bytes mode. So, let's check if the file can handle # text as opposed to bytes. if isinstance(fileobj, io.TextIOBase): return fileobj # Finally, we've determined that the fileobj passed in cannot handle text, # so we use TextIOWrapper to handle the conversion for us. return io.TextIOWrapper(fileobj) def convert_file_contents(text): """Convert text to built-in strings on Python 2.""" if not six.PY2: return text return str(text.encode('utf-8')) bandit-1.4.0/bandit/formatters/xml.py000066400000000000000000000056361303375232700175750ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ============= XML Formatter ============= This formatter outputs the issues as XML. :Example: .. code-block:: xml Test ID: B301 Severity: MEDIUM Confidence: HIGH Use of unsafe yaml load. Allows instantiation of arbitrary objects. Consider yaml.safe_load(). Location examples/yaml_load.py:5 .. versionadded:: 0.12.0 """ # This future import is necessary here due to the xml import below on Python # 2.7 from __future__ import absolute_import import logging import sys from xml.etree import cElementTree as ET import six LOG = logging.getLogger(__name__) def report(manager, fileobj, sev_level, conf_level, lines=-1): '''Prints issues in XML format :param manager: the bandit manager object :param fileobj: The output file object, which may be sys.stdout :param sev_level: Filtering severity level :param conf_level: Filtering confidence level :param lines: Number of lines to report, -1 for all ''' issues = manager.get_issue_list(sev_level=sev_level, conf_level=conf_level) root = ET.Element('testsuite', name='bandit', tests=str(len(issues))) for issue in issues: test = issue.test testcase = ET.SubElement(root, 'testcase', classname=issue.fname, name=test) text = 'Test ID: %s Severity: %s Confidence: %s\n%s\nLocation %s:%s' text = text % (issue.test_id, issue.severity, issue.confidence, issue.text, issue.fname, issue.lineno) ET.SubElement(testcase, 'error', type=issue.severity, message=issue.text).text = text tree = ET.ElementTree(root) if fileobj.name == sys.stdout.name: if six.PY2: fileobj = sys.stdout else: fileobj = sys.stdout.buffer elif fileobj.mode == 'w': fileobj.close() fileobj = open(fileobj.name, "wb") with fileobj: tree.write(fileobj, encoding='utf-8', xml_declaration=True) if fileobj.name != sys.stdout.name: LOG.info("XML output written to file: %s", fileobj.name) bandit-1.4.0/bandit/plugins/000077500000000000000000000000001303375232700157045ustar00rootroot00000000000000bandit-1.4.0/bandit/plugins/__init__.py000066400000000000000000000000001303375232700200030ustar00rootroot00000000000000bandit-1.4.0/bandit/plugins/app_debug.py000066400000000000000000000047521303375232700202140ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2015 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ====================================================== B201: Test for use of flask app with debug set to true ====================================================== Running Flask applications in debug mode results in the Werkzeug debugger being enabled. This includes a feature that allows arbitrary code execution. Documentation for both Flask [1]_ and Werkzeug [2]_ strongly suggests that debug mode should never be enabled on production systems. Operating a production server with debug mode enabled was the probable cause of the Patreon breach in 2015 [3]_. :Example: .. code-block:: none >> Issue: A Flask app appears to be run with debug=True, which exposes the Werkzeug debugger and allows the execution of arbitrary code. Severity: High Confidence: High Location: examples/flask_debug.py:10 9 #bad 10 app.run(debug=True) 11 .. seealso:: .. [1] http://flask.pocoo.org/docs/0.10/quickstart/#debug-mode .. [2] http://werkzeug.pocoo.org/docs/0.10/debug/ .. [3] http://labs.detectify.com/post/130332638391/how-patreon-got-hacked-publicly-exposed-werkzeug # noqa .. versionadded:: 0.15.0 """ import bandit from bandit.core import test_properties as test @test.test_id('B201') @test.checks('Call') def flask_debug_true(context): if context.is_module_imported_like('flask'): if context.call_function_name_qual.endswith('.run'): if context.check_call_arg_value('debug', 'True'): return bandit.Issue( severity=bandit.HIGH, confidence=bandit.MEDIUM, text="A Flask app appears to be run with debug=True, " "which exposes the Werkzeug debugger and allows " "the execution of arbitrary code.", lineno=context.get_lineno_for_call_arg('debug'), ) bandit-1.4.0/bandit/plugins/asserts.py000066400000000000000000000040571303375232700177500ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ============================ B101: Test for use of assert ============================ This plugin test checks for the use of the Python ``assert`` keyword. It was discovered that some projects used assert to enforce interface constraints. However, assert is removed with compiling to optimised byte code (python -o producing \*.pyo files). This caused various protections to be removed. The use of assert is also considered as general bad practice in OpenStack codebases. Please see https://docs.python.org/2/reference/simple_stmts.html#the-assert-statement for more info on ``assert`` :Example: .. code-block:: none >> Issue: Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Severity: Low Confidence: High Location: ./examples/assert.py:1 1 assert logged_in 2 display_assets() .. seealso:: - https://bugs.launchpad.net/juniperopenstack/+bug/1456193 - https://bugs.launchpad.net/heat/+bug/1397883 - https://docs.python.org/2/reference/simple_stmts.html#the-assert-statement .. versionadded:: 0.11.0 """ import bandit from bandit.core import test_properties as test @test.test_id('B101') @test.checks('Assert') def assert_used(context): return bandit.Issue( severity=bandit.LOW, confidence=bandit.HIGH, text=("Use of assert detected. The enclosed code " "will be removed when compiling to optimised byte code.") ) bandit-1.4.0/bandit/plugins/crypto_request_no_cert_validation.py000066400000000000000000000051631303375232700252760ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ============================================= B501: Test for missing certificate validation ============================================= Encryption in general is typically critical to the security of many applications. Using TLS can greatly increase security by guaranteeing the identity of the party you are communicating with. This is accomplished by one or both parties presenting trusted certificates during the connection initialization phase of TLS. When request methods are used certificates are validated automatically which is the desired behavior. If certificate validation is explicitly turned off Bandit will return a HIGH severity error. :Example: .. code-block:: none >> Issue: [request_with_no_cert_validation] Requests call with verify=False disabling SSL certificate checks, security issue. Severity: High Confidence: High Location: examples/requests-ssl-verify-disabled.py:4 3 requests.get('https://gmail.com', verify=True) 4 requests.get('https://gmail.com', verify=False) 5 requests.post('https://gmail.com', verify=True) .. seealso:: - https://security.openstack.org/guidelines/dg_move-data-securely.html - https://security.openstack.org/guidelines/dg_validate-certificates.html .. versionadded:: 0.9.0 """ import bandit from bandit.core import test_properties as test @test.checks('Call') @test.test_id('B501') def request_with_no_cert_validation(context): http_verbs = ('get', 'options', 'head', 'post', 'put', 'patch', 'delete') if ('requests' in context.call_function_name_qual and context.call_function_name in http_verbs): if context.check_call_arg_value('verify', 'False'): issue = bandit.Issue( severity=bandit.HIGH, confidence=bandit.HIGH, text="Requests call with verify=False disabling SSL " "certificate checks, security issue.", lineno=context.get_lineno_for_call_arg('verify'), ) return issue bandit-1.4.0/bandit/plugins/exec.py000066400000000000000000000033301303375232700172010ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ============================== B102: Test for the use of exec ============================== This plugin test checks for the use of Python's `exec` method or keyword. The Python docs succinctly describe why the use of `exec` is risky. :Example: .. code-block:: none >> Issue: Use of exec detected. Severity: Medium Confidence: High Location: ./examples/exec-py2.py:2 1 exec("do evil") 2 exec "do evil" .. seealso:: - https://docs.python.org/2.0/ref/exec.html - TODO: add info on exec and similar to sec best practice and link here .. versionadded:: 0.9.0 """ import six import bandit from bandit.core import test_properties as test def exec_issue(): return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.HIGH, text="Use of exec detected." ) if six.PY2: @test.checks('Exec') @test.test_id('B102') def exec_used(context): return exec_issue() else: @test.checks('Call') @test.test_id('B102') def exec_used(context): if context.call_function_name_qual == 'exec': return exec_issue() bandit-1.4.0/bandit/plugins/exec_as_root.py000066400000000000000000000062111303375232700207300ustar00rootroot00000000000000# Copyright (c) 2015 VMware, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ================================================== B111: Test for the use of rootwrap running as root ================================================== Running commands as root dramatically increase their potential risk. Running commands with restricted user privileges provides defense in depth against command injection attacks, or developer and configuration error. This plugin test checks for specific methods being called with a keyword parameter `run_as_root` set to True, a common OpenStack idiom. **Config Options:** This test plugin takes a similarly named configuration block, `execute_with_run_as_root_equals_true`, providing a list, `function_names`, of function names. A call to any of these named functions will be checked for a `run_as_root` keyword parameter, and if True, will report a Low severity issue. .. code-block:: yaml execute_with_run_as_root_equals_true: function_names: - ceilometer.utils.execute - cinder.utils.execute - neutron.agent.linux.utils.execute - nova.utils.execute - nova.utils.trycmd :Example: .. code-block:: none >> Issue: Execute with run_as_root=True identified, possible security issue. Severity: Low Confidence: Medium Location: ./examples/exec-as-root.py:26 25 nova_utils.trycmd('gcc --version') 26 nova_utils.trycmd('gcc --version', run_as_root=True) 27 .. seealso:: - https://security.openstack.org/guidelines/dg_rootwrap-recommendations-and-plans.html # noqa - https://security.openstack.org/guidelines/dg_use-oslo-rootwrap-securely.html .. versionadded:: 0.10.0 """ import bandit from bandit.core import test_properties as test def gen_config(name): if name == 'execute_with_run_as_root_equals_true': return {'function_names': ['ceilometer.utils.execute', 'cinder.utils.execute', 'neutron.agent.linux.utils.execute', 'nova.utils.execute', 'nova.utils.trycmd']} @test.takes_config @test.checks('Call') @test.test_id('B111') def execute_with_run_as_root_equals_true(context, config): if (context.call_function_name_qual in config['function_names']): if context.check_call_arg_value('run_as_root', 'True'): return bandit.Issue( severity=bandit.LOW, confidence=bandit.MEDIUM, text="Execute with run_as_root=True identified, possible " "security issue.", lineno=context.get_lineno_for_call_arg('run_as_root'), ) bandit-1.4.0/bandit/plugins/general_bad_file_permissions.py000066400000000000000000000062621303375232700241410ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ================================================== B103: Test for setting permissive file permissions ================================================== POSIX based operating systems utilize a permissions model to protect access to parts of the file system. This model supports three roles "owner", "group" and "world" each role may have a combination of "read", "write" or "execute" flags sets. Python provides ``chmod`` to manipulate POSIX style permissions. This plugin test looks for the use of ``chmod`` and will alert when it is used to set particularly permissive control flags. A MEDIUM warning is generated if a file is set to group executable and a HIGH warning is reported if a file is set world writable. Warnings are given with HIGH confidence. :Example: .. code-block:: none >> Issue: Probable insecure usage of temp file/directory. Severity: Medium Confidence: Medium Location: ./examples/os-chmod-py2.py:15 14 os.chmod('/etc/hosts', 0o777) 15 os.chmod('/tmp/oh_hai', 0x1ff) 16 os.chmod('/etc/passwd', stat.S_IRWXU) >> Issue: Chmod setting a permissive mask 0777 on file (key_file). Severity: High Confidence: High Location: ./examples/os-chmod-py2.py:17 16 os.chmod('/etc/passwd', stat.S_IRWXU) 17 os.chmod(key_file, 0o777) 18 .. seealso:: - https://security.openstack.org/guidelines/dg_apply-restrictive-file-permissions.html # noqa - https://en.wikipedia.org/wiki/File_system_permissions - https://security.openstack.org .. versionadded:: 0.9.0 """ import stat import bandit from bandit.core import test_properties as test @test.checks('Call') @test.test_id('B103') def set_bad_file_permissions(context): if 'chmod' in context.call_function_name: if context.call_args_count == 2: mode = context.get_call_arg_at_position(1) if (mode is not None and isinstance(mode, int) and (mode & stat.S_IWOTH or mode & stat.S_IXGRP)): # world writable is an HIGH, group executable is a MEDIUM if mode & stat.S_IWOTH: sev_level = bandit.HIGH else: sev_level = bandit.MEDIUM filename = context.get_call_arg_at_position(0) if filename is None: filename = 'NOT PARSED' return bandit.Issue( severity=sev_level, confidence=bandit.HIGH, text="Chmod setting a permissive mask %s on file (%s)." % (oct(mode), filename) ) bandit-1.4.0/bandit/plugins/general_bind_all_interfaces.py000066400000000000000000000034431303375232700237260ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ======================================== B104: Test for binding to all interfaces ======================================== Binding to all network interfaces can potentially open up a service to traffic on unintended interfaces, that may not be properly documented or secured. This plugin test looks for a string pattern "0.0.0.0" that may indicate a hardcoded binding to all network interfaces. :Example: .. code-block:: none >> Issue: Possible binding to all interfaces. Severity: Medium Confidence: Medium Location: ./examples/binding.py:4 3 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 4 s.bind(('0.0.0.0', 31137)) 5 s.bind(('192.168.0.1', 8080)) .. seealso:: - __TODO__ : add best practice info on binding to all interfaces, and link here. .. versionadded:: 0.9.0 """ import bandit from bandit.core import test_properties as test @test.checks('Str') @test.test_id('B104') def hardcoded_bind_all_interfaces(context): if context.string_val == '0.0.0.0': return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.MEDIUM, text="Possible binding to all interfaces." ) bandit-1.4.0/bandit/plugins/general_hardcoded_password.py000066400000000000000000000143401303375232700236140ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import ast import sys import bandit from bandit.core import test_properties as test CANDIDATES = set(["password", "pass", "passwd", "pwd", "secret", "token", "secrete"]) def _report(value): return bandit.Issue( severity=bandit.LOW, confidence=bandit.MEDIUM, text=("Possible hardcoded password: '%s'" % value)) @test.checks('Str') @test.test_id('B105') def hardcoded_password_string(context): """**B105: Test for use of hard-coded password strings** The use of hard-coded passwords increases the possibility of password guessing tremendously. This plugin test looks for all string literals and checks the following conditions: - assigned to a variable that looks like a password - assigned to a dict key that looks like a password - used in a comparison with a variable that looks like a password Variables are considered to look like a password if they have match any one of: - "password" - "pass" - "passwd" - "pwd" - "secret" - "token" - "secrete" Note: this can be noisy and may generate false positives. **Config Options:** None :Example: .. code-block:: none >> Issue: Possible hardcoded password '(root)' Severity: Low Confidence: Low Location: ./examples/hardcoded-passwords.py:5 4 def someFunction2(password): 5 if password == "root": 6 print("OK, logged in") .. seealso:: - https://www.owasp.org/index.php/Use_of_hard-coded_password .. versionadded:: 0.9.0 """ node = context.node if isinstance(node.parent, ast.Assign): # looks for "candidate='some_string'" for targ in node.parent.targets: if isinstance(targ, ast.Name) and targ.id in CANDIDATES: return _report(node.s) elif isinstance(node.parent, ast.Index) and node.s in CANDIDATES: # looks for "dict[candidate]='some_string'" # assign -> subscript -> index -> string assign = node.parent.parent.parent if isinstance(assign, ast.Assign) and isinstance(assign.value, ast.Str): return _report(assign.value.s) elif isinstance(node.parent, ast.Compare): # looks for "candidate == 'some_string'" comp = node.parent if isinstance(comp.left, ast.Name) and comp.left.id in CANDIDATES: if isinstance(comp.comparators[0], ast.Str): return _report(comp.comparators[0].s) @test.checks('Call') @test.test_id('B106') def hardcoded_password_funcarg(context): """**B106: Test for use of hard-coded password function arguments** The use of hard-coded passwords increases the possibility of password guessing tremendously. This plugin test looks for all function calls being passed a keyword argument that is a string literal. It checks that the assigned local variable does not look like a password. Variables are considered to look like a password if they have match any one of: - "password" - "pass" - "passwd" - "pwd" - "secret" - "token" - "secrete" Note: this can be noisy and may generate false positives. **Config Options:** None :Example: .. code-block:: none >> Issue: [B106:hardcoded_password_funcarg] Possible hardcoded password: 'blerg' Severity: Low Confidence: Medium Location: ./examples/hardcoded-passwords.py:16 15 16 doLogin(password="blerg") .. seealso:: - https://www.owasp.org/index.php/Use_of_hard-coded_password .. versionadded:: 0.9.0 """ # looks for "function(candidate='some_string')" for kw in context.node.keywords: if isinstance(kw.value, ast.Str) and kw.arg in CANDIDATES: return _report(kw.value.s) @test.checks('FunctionDef') @test.test_id('B107') def hardcoded_password_default(context): """**B107: Test for use of hard-coded password argument defaults** The use of hard-coded passwords increases the possibility of password guessing tremendously. This plugin test looks for all function definitions that specify a default string literal for some argument. It checks that the argument does not look like a password. Variables are considered to look like a password if they have match any one of: - "password" - "pass" - "passwd" - "pwd" - "secret" - "token" - "secrete" Note: this can be noisy and may generate false positives. **Config Options:** None :Example: .. code-block:: none >> Issue: [B107:hardcoded_password_default] Possible hardcoded password: 'Admin' Severity: Low Confidence: Medium Location: ./examples/hardcoded-passwords.py:1 1 def someFunction(user, password="Admin"): 2 print("Hi " + user) .. seealso:: - https://www.owasp.org/index.php/Use_of_hard-coded_password .. versionadded:: 0.9.0 """ # looks for "def function(candidate='some_string')" # this pads the list of default values with "None" if nothing is given defs = [None] * (len(context.node.args.args) - len(context.node.args.defaults)) defs.extend(context.node.args.defaults) # go through all (param, value)s and look for candidates for key, val in zip(context.node.args.args, defs): if isinstance(key, ast.Name): check = key.arg if sys.version_info.major > 2 else key.id # Py3 if isinstance(val, ast.Str) and check in CANDIDATES: return _report(val.s) bandit-1.4.0/bandit/plugins/general_hardcoded_tmp.py000066400000000000000000000047711303375232700225610ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" =================================================== B108: Test for insecure usage of tmp file/directory =================================================== Safely creating a temporary file or directory means following a number of rules (see the references for more details). This plugin test looks for strings starting with (configurable) commonly used temporary paths, for example: - /tmp - /var/tmp - /dev/shm - etc **Config Options:** This test plugin takes a similarly named config block, `hardcoded_tmp_directory`. The config block provides a Python list, `tmp_dirs`, that lists string fragments indicating possible temporary file paths. Any string starting with one of these fragments will report a MEDIUM confidence issue. .. code-block:: yaml hardcoded_tmp_directory: tmp_dirs: ['/tmp', '/var/tmp', '/dev/shm'] :Example: .. code-block: none >> Issue: Probable insecure usage of temp file/directory. Severity: Medium Confidence: Medium Location: ./examples/hardcoded-tmp.py:1 1 f = open('/tmp/abc', 'w') 2 f.write('def') .. seealso:: - https://security.openstack.org/guidelines/dg_using-temporary-files-securely.html # noqa .. versionadded:: 0.9.0 """ import bandit from bandit.core import test_properties as test def gen_config(name): if name == 'hardcoded_tmp_directory': return {'tmp_dirs': ['/tmp', '/var/tmp', '/dev/shm']} @test.takes_config @test.checks('Str') @test.test_id('B108') def hardcoded_tmp_directory(context, config): if config is not None and 'tmp_dirs' in config: tmp_dirs = config['tmp_dirs'] else: tmp_dirs = ['/tmp', '/var/tmp', '/dev/shm'] if any(context.string_val.startswith(s) for s in tmp_dirs): return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.MEDIUM, text="Probable insecure usage of temp file/directory." ) bandit-1.4.0/bandit/plugins/injection_paramiko.py000066400000000000000000000051231303375232700221240ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ============================================== B601: Test for shell injection within Paramiko ============================================== Paramiko is a Python library designed to work with the SSH2 protocol for secure (encrypted and authenticated) connections to remote machines. It is intended to run commands on a remote host. These commands are run within a shell on the target and are thus vulnerable to various shell injection attacks. Bandit reports a MEDIUM issue when it detects the use of Paramiko's "exec_command" or "invoke_shell" methods advising the user to check inputs are correctly sanitized. :Example: .. code-block:: none >> Issue: Possible shell injection via Paramiko call, check inputs are properly sanitized. Severity: Medium Confidence: Medium Location: ./examples/paramiko_injection.py:4 3 # this is not safe 4 paramiko.exec_command('something; reallly; unsafe') 5 >> Issue: Possible shell injection via Paramiko call, check inputs are properly sanitized. Severity: Medium Confidence: Medium Location: ./examples/paramiko_injection.py:10 9 # this is not safe 10 SSHClient.invoke_shell('something; bad; here\n') 11 .. seealso:: - https://security.openstack.org - https://github.com/paramiko/paramiko - https://www.owasp.org/index.php/Command_Injection .. versionadded:: 0.12.0 """ import bandit from bandit.core import test_properties as test @test.checks('Call') @test.test_id('B601') def paramiko_calls(context): issue_text = ('Possible shell injection via Paramiko call, check inputs ' 'are properly sanitized.') for module in ['paramiko']: if context.is_module_imported_like(module): if context.call_function_name in ['exec_command', 'invoke_shell']: return bandit.Issue(severity=bandit.MEDIUM, confidence=bandit.MEDIUM, text=issue_text) bandit-1.4.0/bandit/plugins/injection_shell.py000066400000000000000000000574011303375232700214360ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import ast import re import bandit from bandit.core import test_properties as test # yuck, regex: starts with a windows drive letter (eg C:) # or one of our path delimeter characters (/, \, .) full_path_match = re.compile(r'^(?:[A-Za-z](?=\:)|[\\\/\.])') def _evaluate_shell_call(context): no_formatting = isinstance(context.node.args[0], ast.Str) if no_formatting: return bandit.LOW else: return bandit.HIGH def gen_config(name): if name == 'shell_injection': return { # Start a process using the subprocess module, or one of its # wrappers. 'subprocess': ['subprocess.Popen', 'subprocess.call', 'subprocess.check_call', 'subprocess.check_output', 'utils.execute', 'utils.execute_with_timeout'], # Start a process with a function vulnerable to shell injection. 'shell': ['os.system', 'os.popen', 'os.popen2', 'os.popen3', 'os.popen4', 'popen2.popen2', 'popen2.popen3', 'popen2.popen4', 'popen2.Popen3', 'popen2.Popen4', 'commands.getoutput', 'commands.getstatusoutput'], # Start a process with a function that is not vulnerable to shell # injection. 'no_shell': ['os.execl', 'os.execle', 'os.execlp', 'os.execlpe', 'os.execv', 'os.execve', 'os.execvp', 'os.execvpe', 'os.spawnl', 'os.spawnle', 'os.spawnlp', 'os.spawnlpe', 'os.spawnv', 'os.spawnve', 'os.spawnvp', 'os.spawnvpe', 'os.startfile'] } @test.takes_config('shell_injection') @test.checks('Call') @test.test_id('B602') def subprocess_popen_with_shell_equals_true(context, config): """**B602: Test for use of popen with shell equals true** Python possesses many mechanisms to invoke an external executable. However, doing so may present a security issue if appropriate care is not taken to sanitize any user provided or variable input. This plugin test is part of a family of tests built to check for process spawning and warn appropriately. Specifically, this test looks for the spawning of a subprocess using a command shell. This type of subprocess invocation is dangerous as it is vulnerable to various shell injection attacks. Great care should be taken to sanitize all input in order to mitigate this risk. Calls of this type are identified by a parameter of 'shell=True' being given. Additionally, this plugin scans the command string given and adjusts its reported severity based on how it is presented. If the command string is a simple static string containing no special shell characters, then the resulting issue has low severity. If the string is static, but contains shell formatting characters or wildcards, then the reported issue is medium. Finally, if the string is computed using Python's string manipulation or formatting operations, then the reported issue has high severity. These severity levels reflect the likelihood that the code is vulnerable to injection. See also: - :doc:`../plugins/linux_commands_wildcard_injection` - :doc:`../plugins/subprocess_without_shell_equals_true` - :doc:`../plugins/start_process_with_no_shell` - :doc:`../plugins/start_process_with_a_shell` - :doc:`../plugins/start_process_with_partial_path` **Config Options:** This plugin test shares a configuration with others in the same family, namely `shell_injection`. This configuration is divided up into three sections, `subprocess`, `shell` and `no_shell`. They each list Python calls that spawn subprocesses, invoke commands within a shell, or invoke commands without a shell (by replacing the calling process) respectively. This plugin specifically scans for methods listed in `subprocess` section that have shell=True specified. .. code-block:: yaml shell_injection: # Start a process using the subprocess module, or one of its wrappers. subprocess: - subprocess.Popen - subprocess.call :Example: .. code-block:: none >> Issue: subprocess call with shell=True seems safe, but may be changed in the future, consider rewriting without shell Severity: Low Confidence: High Location: ./examples/subprocess_shell.py:21 20 subprocess.check_call(['/bin/ls', '-l'], shell=False) 21 subprocess.check_call('/bin/ls -l', shell=True) 22 >> Issue: call with shell=True contains special shell characters, consider moving extra logic into Python code Severity: Medium Confidence: High Location: ./examples/subprocess_shell.py:26 25 26 subprocess.Popen('/bin/ls *', shell=True) 27 subprocess.Popen('/bin/ls %s' % ('something',), shell=True) >> Issue: subprocess call with shell=True identified, security issue. Severity: High Confidence: High Location: ./examples/subprocess_shell.py:27 26 subprocess.Popen('/bin/ls *', shell=True) 27 subprocess.Popen('/bin/ls %s' % ('something',), shell=True) 28 subprocess.Popen('/bin/ls {}'.format('something'), shell=True) .. seealso:: - https://security.openstack.org - https://docs.python.org/2/library/subprocess.html#frequently-used-arguments # noqa - https://security.openstack.org/guidelines/dg_use-subprocess-securely.html - https://security.openstack.org/guidelines/dg_avoid-shell-true.html .. versionadded:: 0.9.0 """ if config and context.call_function_name_qual in config['subprocess']: if context.check_call_arg_value('shell', 'True'): if len(context.call_args) > 0: sev = _evaluate_shell_call(context) if sev == bandit.LOW: return bandit.Issue( severity=bandit.LOW, confidence=bandit.HIGH, text='subprocess call with shell=True seems safe, but ' 'may be changed in the future, consider ' 'rewriting without shell', lineno=context.get_lineno_for_call_arg('shell'), ) else: return bandit.Issue( severity=bandit.HIGH, confidence=bandit.HIGH, text='subprocess call with shell=True identified, ' 'security issue.', lineno=context.get_lineno_for_call_arg('shell'), ) @test.takes_config('shell_injection') @test.checks('Call') @test.test_id('B603') def subprocess_without_shell_equals_true(context, config): """**B603: Test for use of subprocess with shell equals true** Python possesses many mechanisms to invoke an external executable. However, doing so may present a security issue if appropriate care is not taken to sanitize any user provided or variable input. This plugin test is part of a family of tests built to check for process spawning and warn appropriately. Specifically, this test looks for the spawning of a subprocess without the use of a command shell. This type of subprocess invocation is not vulnerable to shell injection attacks, but care should still be taken to ensure validity of input. Because this is a lesser issue than that described in `subprocess_popen_with_shell_equals_true` a LOW severity warning is reported. See also: - :doc:`../plugins/linux_commands_wildcard_injection` - :doc:`../plugins/subprocess_popen_with_shell_equals_true` - :doc:`../plugins/start_process_with_no_shell` - :doc:`../plugins/start_process_with_a_shell` - :doc:`../plugins/start_process_with_partial_path` **Config Options:** This plugin test shares a configuration with others in the same family, namely `shell_injection`. This configuration is divided up into three sections, `subprocess`, `shell` and `no_shell`. They each list Python calls that spawn subprocesses, invoke commands within a shell, or invoke commands without a shell (by replacing the calling process) respectively. This plugin specifically scans for methods listed in `subprocess` section that have shell=False specified. .. code-block:: yaml shell_injection: # Start a process using the subprocess module, or one of its wrappers. subprocess: - subprocess.Popen - subprocess.call :Example: .. code-block:: none >> Issue: subprocess call - check for execution of untrusted input. Severity: Low Confidence: High Location: ./examples/subprocess_shell.py:23 22 23 subprocess.check_output(['/bin/ls', '-l']) 24 .. seealso:: - https://security.openstack.org - https://docs.python.org/2/library/subprocess.html#frequently-used-arguments # noqa - https://security.openstack.org/guidelines/dg_avoid-shell-true.html - https://security.openstack.org/guidelines/dg_use-subprocess-securely.html .. versionadded:: 0.9.0 """ if config and context.call_function_name_qual in config['subprocess']: if not context.check_call_arg_value('shell', 'True'): return bandit.Issue( severity=bandit.LOW, confidence=bandit.HIGH, text='subprocess call - check for execution of untrusted ' 'input.', lineno=context.get_lineno_for_call_arg('shell'), ) @test.takes_config('shell_injection') @test.checks('Call') @test.test_id('B604') def any_other_function_with_shell_equals_true(context, config): """**B604: Test for any function with shell equals true** Python possesses many mechanisms to invoke an external executable. However, doing so may present a security issue if appropriate care is not taken to sanitize any user provided or variable input. This plugin test is part of a family of tests built to check for process spawning and warn appropriately. Specifically, this plugin test interrogates method calls for the presence of a keyword parameter `shell` equalling true. It is related to detection of shell injection issues and is intended to catch custom wrappers to vulnerable methods that may have been created. See also: - :doc:`../plugins/linux_commands_wildcard_injection` - :doc:`../plugins/subprocess_popen_with_shell_equals_true` - :doc:`../plugins/subprocess_without_shell_equals_true` - :doc:`../plugins/start_process_with_no_shell` - :doc:`../plugins/start_process_with_a_shell` - :doc:`../plugins/start_process_with_partial_path` **Config Options:** This plugin test shares a configuration with others in the same family, namely `shell_injection`. This configuration is divided up into three sections, `subprocess`, `shell` and `no_shell`. They each list Python calls that spawn subprocesses, invoke commands within a shell, or invoke commands without a shell (by replacing the calling process) respectively. Specifically, this plugin excludes those functions listed under the subprocess section, these methods are tested in a separate specific test plugin and this exclusion prevents duplicate issue reporting. .. code-block:: yaml shell_injection: # Start a process using the subprocess module, or one of its wrappers. subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output, utils.execute, utils.execute_with_timeout] :Example: .. code-block:: none >> Issue: Function call with shell=True parameter identified, possible security issue. Severity: Medium Confidence: High Location: ./examples/subprocess_shell.py:9 8 pop('/bin/gcc --version', shell=True) 9 Popen('/bin/gcc --version', shell=True) 10 .. seealso:: - https://security.openstack.org/guidelines/dg_avoid-shell-true.html - https://security.openstack.org/guidelines/dg_use-subprocess-securely.html # noqa .. versionadded:: 0.9.0 """ if config and context.call_function_name_qual not in config['subprocess']: if context.check_call_arg_value('shell', 'True'): return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.LOW, text='Function call with shell=True parameter identified, ' 'possible security issue.', lineno=context.get_lineno_for_call_arg('shell'), ) @test.takes_config('shell_injection') @test.checks('Call') @test.test_id('B605') def start_process_with_a_shell(context, config): """**B605: Test for starting a process with a shell** Python possesses many mechanisms to invoke an external executable. However, doing so may present a security issue if appropriate care is not taken to sanitize any user provided or variable input. This plugin test is part of a family of tests built to check for process spawning and warn appropriately. Specifically, this test looks for the spawning of a subprocess using a command shell. This type of subprocess invocation is dangerous as it is vulnerable to various shell injection attacks. Great care should be taken to sanitize all input in order to mitigate this risk. Calls of this type are identified by the use of certain commands which are known to use shells. Bandit will report a MEDIUM severity warning. See also: - :doc:`../plugins/linux_commands_wildcard_injection` - :doc:`../plugins/subprocess_without_shell_equals_true` - :doc:`../plugins/start_process_with_no_shell` - :doc:`../plugins/start_process_with_partial_path` - :doc:`../plugins/subprocess_popen_with_shell_equals_true` **Config Options:** This plugin test shares a configuration with others in the same family, namely `shell_injection`. This configuration is divided up into three sections, `subprocess`, `shell` and `no_shell`. They each list Python calls that spawn subprocesses, invoke commands within a shell, or invoke commands without a shell (by replacing the calling process) respectively. This plugin specifically scans for methods listed in `shell` section. .. code-block:: yaml shell_injection: shell: - os.system - os.popen - os.popen2 - os.popen3 - os.popen4 - popen2.popen2 - popen2.popen3 - popen2.popen4 - popen2.Popen3 - popen2.Popen4 - commands.getoutput - commands.getstatusoutput :Example: .. code-block:: none >> Issue: Starting a process with a shell: check for injection. Severity: Medium Confidence: Medium Location: examples/os_system.py:3 2 3 os.system('/bin/echo hi') .. seealso:: - https://security.openstack.org - https://docs.python.org/2/library/os.html#os.system - https://docs.python.org/2/library/subprocess.html#frequently-used-arguments # noqa - https://security.openstack.org/guidelines/dg_use-subprocess-securely.html .. versionadded:: 0.10.0 """ if config and context.call_function_name_qual in config['shell']: if len(context.call_args) > 0: sev = _evaluate_shell_call(context) if sev == bandit.LOW: return bandit.Issue( severity=bandit.LOW, confidence=bandit.HIGH, text='Starting a process with a shell: ' 'Seems safe, but may be changed in the future, ' 'consider rewriting without shell' ) else: return bandit.Issue( severity=bandit.HIGH, confidence=bandit.HIGH, text='Starting a process with a shell, possible injection' ' detected, security issue.' ) @test.takes_config('shell_injection') @test.checks('Call') @test.test_id('B606') def start_process_with_no_shell(context, config): """**B606: Test for starting a process with no shell** Python possesses many mechanisms to invoke an external executable. However, doing so may present a security issue if appropriate care is not taken to sanitize any user provided or variable input. This plugin test is part of a family of tests built to check for process spawning and warn appropriately. Specifically, this test looks for the spawning of a subprocess in a way that doesn't use a shell. Although this is generally safe, it maybe useful for penetration testing workflows to track where external system calls are used. As such a LOW severity message is generated. See also: - :doc:`../plugins/linux_commands_wildcard_injection` - :doc:`../plugins/subprocess_without_shell_equals_true` - :doc:`../plugins/start_process_with_a_shell` - :doc:`../plugins/start_process_with_partial_path` - :doc:`../plugins/subprocess_popen_with_shell_equals_true` **Config Options:** This plugin test shares a configuration with others in the same family, namely `shell_injection`. This configuration is divided up into three sections, `subprocess`, `shell` and `no_shell`. They each list Python calls that spawn subprocesses, invoke commands within a shell, or invoke commands without a shell (by replacing the calling process) respectively. This plugin specifically scans for methods listed in `no_shell` section. .. code-block:: yaml shell_injection: no_shell: - os.execl - os.execle - os.execlp - os.execlpe - os.execv - os.execve - os.execvp - os.execvpe - os.spawnl - os.spawnle - os.spawnlp - os.spawnlpe - os.spawnv - os.spawnve - os.spawnvp - os.spawnvpe - os.startfile :Example: .. code-block:: none >> Issue: [start_process_with_no_shell] Starting a process without a shell. Severity: Low Confidence: Medium Location: examples/os-spawn.py:8 7 os.spawnv(mode, path, args) 8 os.spawnve(mode, path, args, env) 9 os.spawnvp(mode, file, args) .. seealso:: - https://security.openstack.org - https://docs.python.org/2/library/os.html#os.system - https://docs.python.org/2/library/subprocess.html#frequently-used-arguments # noqa - https://security.openstack.org/guidelines/dg_use-subprocess-securely.html .. versionadded:: 0.10.0 """ if config and context.call_function_name_qual in config['no_shell']: return bandit.Issue( severity=bandit.LOW, confidence=bandit.MEDIUM, text='Starting a process without a shell.' ) @test.takes_config('shell_injection') @test.checks('Call') @test.test_id('B607') def start_process_with_partial_path(context, config): """**B607: Test for starting a process with a partial path** Python possesses many mechanisms to invoke an external executable. If the desired executable path is not fully qualified relative to the filesystem root then this may present a potential security risk. In POSIX environments, the `PATH` environment variable is used to specify a set of standard locations that will be searched for the first matching named executable. While convenient, this behavior may allow a malicious actor to exert control over a system. If they are able to adjust the contents of the `PATH` variable, or manipulate the file system, then a bogus executable may be discovered in place of the desired one. This executable will be invoked with the user privileges of the Python process that spawned it, potentially a highly privileged user. This test will scan the parameters of all configured Python methods, looking for paths that do not start at the filesystem root, that is, do not have a leading '/' character. **Config Options:** This plugin test shares a configuration with others in the same family, namely `shell_injection`. This configuration is divided up into three sections, `subprocess`, `shell` and `no_shell`. They each list Python calls that spawn subprocesses, invoke commands within a shell, or invoke commands without a shell (by replacing the calling process) respectively. This test will scan parameters of all methods in all sections. Note that methods are fully qualified and de-aliased prior to checking. .. code-block:: yaml shell_injection: # Start a process using the subprocess module, or one of its wrappers. subprocess: - subprocess.Popen - subprocess.call # Start a process with a function vulnerable to shell injection. shell: - os.system - os.popen - popen2.Popen3 - popen2.Popen4 - commands.getoutput - commands.getstatusoutput # Start a process with a function that is not vulnerable to shell injection. no_shell: - os.execl - os.execle :Example: .. code-block:: none >> Issue: Starting a process with a partial executable path Severity: Low Confidence: High Location: ./examples/partial_path_process.py:3 2 from subprocess import Popen as pop 3 pop('gcc --version', shell=False) .. seealso:: - https://security.openstack.org - https://docs.python.org/2/library/os.html#process-management .. versionadded:: 0.13.0 """ if config and len(context.call_args): if(context.call_function_name_qual in config['subprocess'] or context.call_function_name_qual in config['shell'] or context.call_function_name_qual in config['no_shell']): node = context.node.args[0] # some calls take an arg list, check the first part if isinstance(node, ast.List): node = node.elts[0] # make sure the param is a string literal and not a var name if isinstance(node, ast.Str) and not full_path_match.match(node.s): return bandit.Issue( severity=bandit.LOW, confidence=bandit.HIGH, text='Starting a process with a partial executable path' ) bandit-1.4.0/bandit/plugins/injection_sql.py000066400000000000000000000063761303375232700211330ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ============================ B608: Test for SQL injection ============================ An SQL injection attack consists of insertion or "injection" of a SQL query via the input data given to an application. It is a very common attack vector. This plugin test looks for strings that resemble SQL statements that are involved in some form of string building operation. For example: - "SELECT %s FROM derp;" % var - "SELECT thing FROM " + tab - "SELECT " + val + " FROM " + tab + ... Unless care is taken to sanitize and control the input data when building such SQL statement strings, an injection attack becomes possible. If strings of this nature are discovered, a LOW confidence issue is reported. In order to boost result confidence, this plugin test will also check to see if the discovered string is in use with standard Python DBAPI calls `execute` or `executemany`. If so, a MEDIUM issue is reported. For example: - cursor.execute("SELECT %s FROM derp;" % var) :Example: .. code-block:: none >> Issue: Possible SQL injection vector through string-based query construction. Severity: Medium Confidence: Low Location: ./examples/sql_statements_without_sql_alchemy.py:4 3 query = "DELETE FROM foo WHERE id = '%s'" % identifier 4 query = "UPDATE foo SET value = 'b' WHERE id = '%s'" % identifier 5 .. seealso:: - https://www.owasp.org/index.php/SQL_Injection - https://security.openstack.org/guidelines/dg_parameterize-database-queries.html # noqa .. versionadded:: 0.9.0 """ import ast import bandit from bandit.core import test_properties as test from bandit.core import utils def _check_string(data): val = data.lower() return ((val.startswith('select ') and ' from ' in val) or val.startswith('insert into') or (val.startswith('update ') and ' set ' in val) or val.startswith('delete from ')) def _evaluate_ast(node): if not isinstance(node.parent, ast.BinOp): return (False, "") out = utils.concat_string(node, node.parent) if isinstance(out[0].parent, ast.Call): # wrapped in "execute" call? names = ['execute', 'executemany'] name = utils.get_called_name(out[0].parent) return (name in names, out[1]) return (False, out[1]) @test.checks('Str') @test.test_id('B608') def hardcoded_sql_expressions(context): val = _evaluate_ast(context.node) if _check_string(val[1]): return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.MEDIUM if val[0] else bandit.LOW, text="Possible SQL injection vector through string-based " "query construction." ) bandit-1.4.0/bandit/plugins/injection_wildcard.py000066400000000000000000000121721303375232700221140ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ======================================== B609: Test for use of wildcard injection ======================================== Python provides a number of methods that emulate the behavior of standard Linux command line utilities. Like their Linux counterparts, these commands may take a wildcard "\*" character in place of a file system path. This is interpreted to mean "any and all files or folders" and can be used to build partially qualified paths, such as "/home/user/\*". The use of partially qualified paths may result in unintended consequences if an unexpected file or symlink is placed into the path location given. This becomes particularly dangerous when combined with commands used to manipulate file permissions or copy data off of a system. This test plugin looks for usage of the following commands in conjunction with wild card parameters: - 'chown' - 'chmod' - 'tar' - 'rsync' As well as any method configured in the shell or subprocess injection test configurations. **Config Options:** This plugin test shares a configuration with others in the same family, namely `shell_injection`. This configuration is divided up into three sections, `subprocess`, `shell` and `no_shell`. They each list Python calls that spawn subprocesses, invoke commands within a shell, or invoke commands without a shell (by replacing the calling process) respectively. This test will scan parameters of all methods in all sections. Note that methods are fully qualified and de-aliased prior to checking. .. code-block:: yaml shell_injection: # Start a process using the subprocess module, or one of its wrappers. subprocess: - subprocess.Popen - subprocess.call # Start a process with a function vulnerable to shell injection. shell: - os.system - os.popen - popen2.Popen3 - popen2.Popen4 - commands.getoutput - commands.getstatusoutput # Start a process with a function that is not vulnerable to shell injection. no_shell: - os.execl - os.execle :Example: .. code-block:: none >> Issue: Possible wildcard injection in call: subprocess.Popen Severity: High Confidence: Medium Location: ./examples/wildcard-injection.py:8 7 o.popen2('/bin/chmod *') 8 subp.Popen('/bin/chown *', shell=True) 9 >> Issue: subprocess call - check for execution of untrusted input. Severity: Low Confidence: High Location: ./examples/wildcard-injection.py:11 10 # Not vulnerable to wildcard injection 11 subp.Popen('/bin/rsync *') 12 subp.Popen("/bin/chmod *") .. seealso:: - https://security.openstack.org - https://en.wikipedia.org/wiki/Wildcard_character - http://www.defensecode.com/public/DefenseCode_Unix_WildCards_Gone_Wild.txt .. versionadded:: 0.9.0 """ import bandit from bandit.core import test_properties as test from bandit.plugins import injection_shell # NOTE(tkelsey): shared config gen_config = injection_shell.gen_config @test.takes_config('shell_injection') @test.checks('Call') @test.test_id('B609') def linux_commands_wildcard_injection(context, config): if not ('shell' in config and 'subprocess' in config): return vulnerable_funcs = ['chown', 'chmod', 'tar', 'rsync'] if context.call_function_name_qual in config['shell'] or ( context.call_function_name_qual in config['subprocess'] and context.check_call_arg_value('shell', 'True')): if context.call_args_count >= 1: call_argument = context.get_call_arg_at_position(0) argument_string = '' if isinstance(call_argument, list): for li in call_argument: argument_string = argument_string + ' %s' % li elif isinstance(call_argument, str): argument_string = call_argument if argument_string != '': for vulnerable_func in vulnerable_funcs: if( vulnerable_func in argument_string and '*' in argument_string ): return bandit.Issue( severity=bandit.HIGH, confidence=bandit.MEDIUM, text="Possible wildcard injection in call: %s" % context.call_function_name_qual, lineno=context.get_lineno_for_call_arg('shell'), ) bandit-1.4.0/bandit/plugins/insecure_ssl_tls.py000066400000000000000000000240211303375232700216350ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import bandit from bandit.core import test_properties as test def get_bad_proto_versions(config): return config['bad_protocol_versions'] def gen_config(name): if name == 'ssl_with_bad_version': return {'bad_protocol_versions': ['PROTOCOL_SSLv2', 'SSLv2_METHOD', 'SSLv23_METHOD', 'PROTOCOL_SSLv3', # strict option 'PROTOCOL_TLSv1', # strict option 'SSLv3_METHOD', # strict option 'TLSv1_METHOD']} # strict option @test.takes_config @test.checks('Call') @test.test_id('B502') def ssl_with_bad_version(context, config): """**B502: Test for SSL use with bad version used** Several highly publicized exploitable flaws have been discovered in all versions of SSL and early versions of TLS. It is strongly recommended that use of the following known broken protocol versions be avoided: - SSL v2 - SSL v3 - TLS v1 - TLS v1.1 This plugin test scans for calls to Python methods with parameters that indicate the used broken SSL/TLS protocol versions. Currently, detection supports methods using Python's native SSL/TLS support and the pyOpenSSL module. A HIGH severity warning will be reported whenever known broken protocol versions are detected. It is worth noting that native support for TLS 1.2 is only available in more recent Python versions, specifically 2.7.9 and up, and 3.x See also: - :doc:`../plugins/ssl_with_bad_defaults` - :doc:`../plugins/ssl_with_no_version` A note on 'SSLv23': Amongst the available SSL/TLS versions provided by Python/pyOpenSSL there exists the option to use SSLv23. This very poorly named option actually means "use the highest version of SSL/TLS supported by both the server and client". This may (and should be) a version well in advance of SSL v2 or v3. Bandit can scan for the use of SSLv23 if desired, but its detection does not necessarily indicate a problem. When using SSLv23 it is important to also provide flags to explicitly exclude bad versions of SSL/TLS from the protocol versions considered. Both the Python native and pyOpenSSL modules provide the ``OP_NO_SSLv2`` and ``OP_NO_SSLv3`` flags for this purpose. **Config Options:** .. code-block:: yaml ssl_with_bad_version: bad_protocol_versions: - PROTOCOL_SSLv2 - SSLv2_METHOD - SSLv23_METHOD - PROTOCOL_SSLv3 # strict option - PROTOCOL_TLSv1 # strict option - SSLv3_METHOD # strict option - TLSv1_METHOD # strict option :Example: .. code-block:: none >> Issue: ssl.wrap_socket call with insecure SSL/TLS protocol version identified, security issue. Severity: High Confidence: High Location: ./examples/ssl-insecure-version.py:13 12 # strict tests 13 ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv3) 14 ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1) .. seealso:: - http://heartbleed.com/ - https://poodlebleed.com/ - https://security.openstack.org/ - https://security.openstack.org/guidelines/dg_move-data-securely.html .. versionadded:: 0.9.0 """ bad_ssl_versions = get_bad_proto_versions(config) if context.call_function_name_qual == 'ssl.wrap_socket': if context.check_call_arg_value('ssl_version', bad_ssl_versions): return bandit.Issue( severity=bandit.HIGH, confidence=bandit.HIGH, text="ssl.wrap_socket call with insecure SSL/TLS protocol " "version identified, security issue.", lineno=context.get_lineno_for_call_arg('ssl_version'), ) elif context.call_function_name_qual == 'pyOpenSSL.SSL.Context': if context.check_call_arg_value('method', bad_ssl_versions): return bandit.Issue( severity=bandit.HIGH, confidence=bandit.HIGH, text="SSL.Context call with insecure SSL/TLS protocol " "version identified, security issue.", lineno=context.get_lineno_for_call_arg('method'), ) elif (context.call_function_name_qual != 'ssl.wrap_socket' and context.call_function_name_qual != 'pyOpenSSL.SSL.Context'): if (context.check_call_arg_value('method', bad_ssl_versions) or context.check_call_arg_value('ssl_version', bad_ssl_versions)): lineno = (context.get_lineno_for_call_arg('method') or context.get_lineno_for_call_arg('ssl_version')) return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.MEDIUM, text="Function call with insecure SSL/TLS protocol " "identified, possible security issue.", lineno=lineno, ) @test.takes_config("ssl_with_bad_version") @test.checks('FunctionDef') @test.test_id('B503') def ssl_with_bad_defaults(context, config): """**B503: Test for SSL use with bad defaults specified** This plugin is part of a family of tests that detect the use of known bad versions of SSL/TLS, please see :doc:`../plugins/ssl_with_bad_version` for a complete discussion. Specifically, this plugin test scans for Python methods with default parameter values that specify the use of broken SSL/TLS protocol versions. Currently, detection supports methods using Python's native SSL/TLS support and the pyOpenSSL module. A MEDIUM severity warning will be reported whenever known broken protocol versions are detected. See also: - :doc:`../plugins/ssl_with_bad_version` - :doc:`../plugins/ssl_with_no_version` **Config Options:** This test shares the configuration provided for the standard :doc:`../plugins/ssl_with_bad_version` test, please refer to its documentation. :Example: .. code-block:: none >> Issue: Function definition identified with insecure SSL/TLS protocol version by default, possible security issue. Severity: Medium Confidence: Medium Location: ./examples/ssl-insecure-version.py:28 27 28 def open_ssl_socket(version=SSL.SSLv2_METHOD): 29 pass .. seealso:: - http://heartbleed.com/ - https://poodlebleed.com/ - https://security.openstack.org/ - https://security.openstack.org/guidelines/dg_move-data-securely.html .. versionadded:: 0.9.0 """ bad_ssl_versions = get_bad_proto_versions(config) for default in context.function_def_defaults_qual: val = default.split(".")[-1] if val in bad_ssl_versions: return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.MEDIUM, text="Function definition identified with insecure SSL/TLS " "protocol version by default, possible security " "issue." ) @test.checks('Call') @test.test_id('B504') def ssl_with_no_version(context): """**B504: Test for SSL use with no version specified** This plugin is part of a family of tests that detect the use of known bad versions of SSL/TLS, please see :doc:`../plugins/ssl_with_bad_version` for a complete discussion. Specifically, This plugin test scans for specific methods in Python's native SSL/TLS support and the pyOpenSSL module that configure the version of SSL/TLS protocol to use. These methods are known to provide default value that maximize compatibility, but permit use of the aforementioned broken protocol versions. A LOW severity warning will be reported whenever this is detected. See also: - :doc:`../plugins/ssl_with_bad_version` - :doc:`../plugins/ssl_with_bad_defaults` **Config Options:** This test shares the configuration provided for the standard :doc:`../plugins/ssl_with_bad_version` test, please refer to its documentation. :Example: .. code-block:: none >> Issue: ssl.wrap_socket call with no SSL/TLS protocol version specified, the default SSLv23 could be insecure, possible security issue. Severity: Low Confidence: Medium Location: ./examples/ssl-insecure-version.py:23 22 23 ssl.wrap_socket() 24 .. seealso:: - http://heartbleed.com/ - https://poodlebleed.com/ - https://security.openstack.org/ - https://security.openstack.org/guidelines/dg_move-data-securely.html .. versionadded:: 0.9.0 """ if context.call_function_name_qual == 'ssl.wrap_socket': if context.check_call_arg_value('ssl_version') is None: # check_call_arg_value() returns False if the argument is found # but does not match the supplied value (or the default None). # It returns None if the arg_name passed doesn't exist. This # tests for that (ssl_version is not specified). return bandit.Issue( severity=bandit.LOW, confidence=bandit.MEDIUM, text="ssl.wrap_socket call with no SSL/TLS protocol version " "specified, the default SSLv23 could be insecure, " "possible security issue.", lineno=context.get_lineno_for_call_arg('ssl_version'), ) bandit-1.4.0/bandit/plugins/jinja2_templates.py000066400000000000000000000117231303375232700215150ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ========================================== B701: Test for not auto escaping in jinja2 ========================================== Jinja2 is a Python HTML templating system. It is typically used to build web applications, though appears in other places well, notably the Ansible automation system. When configuring the Jinja2 environment, the option to use autoescaping on input can be specified. When autoescaping is enabled, Jinja2 will filter input strings to escape any HTML content submitted via template variables. Without escaping HTML input the application becomes vulnerable to Cross Site Scripting (XSS) attacks. Unfortunately, autoescaping is False by default. Thus this plugin test will warn on omission of an autoescape setting, as well as an explicit setting of false. A HIGH severity warning is generated in either of these scenarios. :Example: .. code-block:: none >> Issue: Using jinja2 templates with autoescape=False is dangerous and can lead to XSS. Use autoescape=True to mitigate XSS vulnerabilities. Severity: High Confidence: High Location: ./examples/jinja2_templating.py:11 10 templateEnv = jinja2.Environment(autoescape=False, loader=templateLoader) 11 Environment(loader=templateLoader, 12 load=templateLoader, 13 autoescape=False) 14 >> Issue: By default, jinja2 sets autoescape to False. Consider using autoescape=True to mitigate XSS vulnerabilities. Severity: High Confidence: High Location: ./examples/jinja2_templating.py:15 14 15 Environment(loader=templateLoader, 16 load=templateLoader) 17 .. seealso:: - `OWASP XSS `_ - https://realpython.com/blog/python/primer-on-jinja-templating/ - http://jinja.pocoo.org/docs/dev/api/#autoescaping - https://security.openstack.org - https://security.openstack.org/guidelines/dg_cross-site-scripting-xss.html .. versionadded:: 0.10.0 """ import ast import bandit from bandit.core import test_properties as test @test.checks('Call') @test.test_id('B701') def jinja2_autoescape_false(context): # check type just to be safe if isinstance(context.call_function_name_qual, str): qualname_list = context.call_function_name_qual.split('.') func = qualname_list[-1] if 'jinja2' in qualname_list and func == 'Environment': for node in ast.walk(context.node): if isinstance(node, ast.keyword): # definite autoescape = False if (getattr(node, 'arg', None) == 'autoescape' and (getattr(node.value, 'id', None) == 'False' or getattr(node.value, 'value', None) is False)): return bandit.Issue( severity=bandit.HIGH, confidence=bandit.HIGH, text="Using jinja2 templates with autoescape=" "False is dangerous and can lead to XSS. " "Use autoescape=True to mitigate XSS " "vulnerabilities." ) # found autoescape if getattr(node, 'arg', None) == 'autoescape': if (getattr(node.value, 'id', None) == 'True' or getattr(node.value, 'value', None) is True): return else: return bandit.Issue( severity=bandit.HIGH, confidence=bandit.MEDIUM, text="Using jinja2 templates with autoescape=" "False is dangerous and can lead to XSS. " "Ensure autoescape=True to mitigate XSS " "vulnerabilities." ) # We haven't found a keyword named autoescape, indicating default # behavior return bandit.Issue( severity=bandit.HIGH, confidence=bandit.HIGH, text="By default, jinja2 sets autoescape to False. Consider " "using autoescape=True to mitigate XSS vulnerabilities." ) bandit-1.4.0/bandit/plugins/mako_templates.py000066400000000000000000000055301303375232700212660ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ==================================== B702: Test for use of mako templates ==================================== Mako is a Python templating system often used to build web applications. It is the default templating system used in Pylons and Pyramid. Unlike Jinja2 (an alternative templating system), Mako has no environment wide variable escaping mechanism. Because of this, all input variables must be carefully escaped before use to prevent possible vulnerabilities to Cross Site Scripting (XSS) attacks. :Example: .. code-block:: none >> Issue: Mako templates allow HTML/JS rendering by default and are inherently open to XSS attacks. Ensure variables in all templates are properly sanitized via the 'n', 'h' or 'x' flags (depending on context). For example, to HTML escape the variable 'data' do ${ data |h }. Severity: Medium Confidence: High Location: ./examples/mako_templating.py:10 9 10 mako.template.Template("hern") 11 template.Template("hern") .. seealso:: - http://www.makotemplates.org/ - `OWASP XSS `_ - https://security.openstack.org - https://security.openstack.org/guidelines/dg_cross-site-scripting-xss.html .. versionadded:: 0.10.0 """ import bandit from bandit.core import test_properties as test @test.checks('Call') @test.test_id('B702') def use_of_mako_templates(context): # check type just to be safe if isinstance(context.call_function_name_qual, str): qualname_list = context.call_function_name_qual.split('.') func = qualname_list[-1] if 'mako' in qualname_list and func == 'Template': # unlike Jinja2, mako does not have a template wide autoescape # feature and thus each variable must be carefully sanitized. return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.HIGH, text="Mako templates allow HTML/JS rendering by default and " "are inherently open to XSS attacks. Ensure variables " "in all templates are properly sanitized via the 'n', " "'h' or 'x' flags (depending on context). For example, " "to HTML escape the variable 'data' do ${ data |h }." ) bandit-1.4.0/bandit/plugins/secret_config_option.py000066400000000000000000000103711303375232700224620ustar00rootroot00000000000000# Copyright (c) 2015 VMware, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" =============================================================== B109: Test for a password based config option not marked secret =============================================================== Passwords are sensitive and must be protected appropriately. In OpenStack Oslo there is an option to mark options "secret" which will ensure that they are not logged. This plugin detects usages of oslo configuration functions that appear to deal with strings ending in 'password' and flag usages where they have not been marked secret. If such a value is found a MEDIUM severity error is generated. If 'False' or 'None' are explicitly set, Bandit will return a MEDIUM confidence issue. If Bandit can't determine the value of secret it will return a LOW confidence issue. **Config Options:** .. code-block:: yaml password_config_option_not_marked_secret: function_names: - oslo.config.cfg.StrOpt - oslo_config.cfg.StrOpt :Example: .. code-block:: none >> Issue: [password_config_option_not_marked_secret] oslo config option possibly not marked secret=True identified. Severity: Medium Confidence: Low Location: examples/secret-config-option.py:12 11 help="User's password"), 12 cfg.StrOpt('nova_password', 13 secret=secret, 14 help="Nova user password"), 15 ] >> Issue: [password_config_option_not_marked_secret] oslo config option not marked secret=True identified, security issue. Severity: Medium Confidence: Medium Location: examples/secret-config-option.py:21 20 help="LDAP ubind ser name"), 21 cfg.StrOpt('ldap_password', 22 help="LDAP bind user password"), 23 cfg.StrOpt('ldap_password_attribute', .. seealso:: - https://security.openstack.org/guidelines/dg_protect-sensitive-data-in-files.html # noqa .. versionadded:: 0.10.0 """ import bandit from bandit.core import constants from bandit.core import test_properties as test def gen_config(name): if name == 'password_config_option_not_marked_secret': return {'function_names': ['oslo.config.cfg.StrOpt', 'oslo_config.cfg.StrOpt']} @test.takes_config @test.checks('Call') @test.test_id('B109') def password_config_option_not_marked_secret(context, config): if(context.call_function_name_qual in config['function_names'] and context.get_call_arg_at_position(0) is not None and context.get_call_arg_at_position(0).endswith('password')): # Checks whether secret=False or secret is not set (None). # Returns True if argument found, and matches supplied values # and None if argument not found at all. if context.check_call_arg_value('secret', constants.FALSE_VALUES) in [ True, None]: return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.MEDIUM, text="oslo config option not marked secret=True " "identified, security issue.", lineno=context.get_lineno_for_call_arg('secret'), ) # Checks whether secret is not True, for example when its set to a # variable, secret=secret. elif not context.check_call_arg_value('secret', 'True'): return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.LOW, text="oslo config option possibly not marked secret=True " "identified.", lineno=context.get_lineno_for_call_arg('secret'), ) bandit-1.4.0/bandit/plugins/try_except_continue.py000066400000000000000000000063361303375232700223600ustar00rootroot00000000000000# Copyright 2016 IBM Corp. # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ============================================= B112: Test for a continue in the except block ============================================= Errors in Python code bases are typically communicated using ``Exceptions``. An exception object is 'raised' in the event of an error and can be 'caught' at a later point in the program, typically some error handling or logging action will then be performed. However, it is possible to catch an exception and silently ignore it while in a loop. This is illustrated with the following example .. code-block:: python while keep_going: try: do_some_stuff() except Exception: continue This pattern is considered bad practice in general, but also represents a potential security issue. A larger than normal volume of errors from a service can indicate an attempt is being made to disrupt or interfere with it. Thus errors should, at the very least, be logged. There are rare situations where it is desirable to suppress errors, but this is typically done with specific exception types, rather than the base Exception class (or no type). To accommodate this, the test may be configured to ignore 'try, except, continue' where the exception is typed. For example, the following would not generate a warning if the configuration option ``checked_typed_exception`` is set to False: .. code-block:: python while keep_going: try: do_some_stuff() except ZeroDivisionError: continue **Config Options:** .. code-block:: yaml try_except_continue: check_typed_exception: True :Example: .. code-block:: none >> Issue: Try, Except, Continue detected. Severity: Low Confidence: High Location: ./examples/try_except_continue.py:5 4 a = i 5 except: 6 continue .. seealso:: - https://security.openstack.org .. versionadded:: 1.0.0 """ import ast import bandit from bandit.core import test_properties as test def gen_config(name): if name == 'try_except_continue': return {'check_typed_exception': False} @test.takes_config @test.checks('ExceptHandler') @test.test_id('B112') def try_except_continue(context, config): node = context.node if len(node.body) == 1: if (not config['check_typed_exception'] and node.type is not None and getattr(node.type, 'id', None) != 'Exception'): return if isinstance(node.body[0], ast.Continue): return bandit.Issue( severity=bandit.LOW, confidence=bandit.HIGH, text=("Try, Except, Continue detected.")) bandit-1.4.0/bandit/plugins/try_except_pass.py000066400000000000000000000061321303375232700214740ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ========================================= B110: Test for a pass in the except block ========================================= Errors in Python code bases are typically communicated using ``Exceptions``. An exception object is 'raised' in the event of an error and can be 'caught' at a later point in the program, typically some error handling or logging action will then be performed. However, it is possible to catch an exception and silently ignore it. This is illustrated with the following example .. code-block:: python try: do_some_stuff() except Exception: pass This pattern is considered bad practice in general, but also represents a potential security issue. A larger than normal volume of errors from a service can indicate an attempt is being made to disrupt or interfere with it. Thus errors should, at the very least, be logged. There are rare situations where it is desirable to suppress errors, but this is typically done with specific exception types, rather than the base Exception class (or no type). To accommodate this, the test may be configured to ignore 'try, except, pass' where the exception is typed. For example, the following would not generate a warning if the configuration option ``checked_typed_exception`` is set to False: .. code-block:: python try: do_some_stuff() except ZeroDivisionError: pass **Config Options:** .. code-block:: yaml try_except_pass: check_typed_exception: True :Example: .. code-block:: none >> Issue: Try, Except, Pass detected. Severity: Low Confidence: High Location: ./examples/try_except_pass.py:4 3 a = 1 4 except: 5 pass .. seealso:: - https://security.openstack.org .. versionadded:: 0.13.0 """ import ast import bandit from bandit.core import test_properties as test def gen_config(name): if name == 'try_except_pass': return {'check_typed_exception': False} @test.takes_config @test.checks('ExceptHandler') @test.test_id('B110') def try_except_pass(context, config): node = context.node if len(node.body) == 1: if (not config['check_typed_exception'] and node.type is not None and getattr(node.type, 'id', None) != 'Exception'): return if isinstance(node.body[0], ast.Pass): return bandit.Issue( severity=bandit.LOW, confidence=bandit.HIGH, text=("Try, Except, Pass detected.") ) bandit-1.4.0/bandit/plugins/weak_cryptographic_key.py000066400000000000000000000104241303375232700230140ustar00rootroot00000000000000# Copyright (c) 2015 VMware, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" ========================================= B505: Test for weak cryptographic key use ========================================= As computational power increases, so does the ability to break ciphers with smaller key lengths. The recommended key length size for RSA and DSA algorithms is 2048 and higher. 1024 bits and below are now considered breakable. EC key length sizes are recommended to be 224 and higher with 160 and below considered breakable. This plugin test checks for use of any key less than those limits and returns a high severity error if lower than the lower threshold and a medium severity error for those lower than the higher threshold. :Example: .. code-block:: none >> Issue: DSA key sizes below 1024 bits are considered breakable. Severity: High Confidence: High Location: examples/weak_cryptographic_key_sizes.py:36 35 # Also incorrect: without keyword args 36 dsa.generate_private_key(512, 37 backends.default_backend()) 38 rsa.generate_private_key(3, .. seealso:: - http://csrc.nist.gov/publications/nistpubs/800-131A/sp800-131A.pdf - https://security.openstack.org/guidelines/dg_strong-crypto.html .. versionadded:: 0.14.0 """ import bandit from bandit.core import test_properties as test def _classify_key_size(key_type, key_size): if isinstance(key_size, str): # size provided via a variable - can't process it at the moment return key_sizes = { 'DSA': [(1024, bandit.HIGH), (2048, bandit.MEDIUM)], 'RSA': [(1024, bandit.HIGH), (2048, bandit.MEDIUM)], 'EC': [(160, bandit.HIGH), (224, bandit.MEDIUM)], } for size, level in key_sizes[key_type]: if key_size < size: return bandit.Issue( severity=level, confidence=bandit.HIGH, text='%s key sizes below %d bits are considered breakable. ' % (key_type, size)) def _weak_crypto_key_size_cryptography_io(context): func_key_type = { 'cryptography.hazmat.primitives.asymmetric.dsa.' 'generate_private_key': 'DSA', 'cryptography.hazmat.primitives.asymmetric.rsa.' 'generate_private_key': 'RSA', 'cryptography.hazmat.primitives.asymmetric.ec.' 'generate_private_key': 'EC', } arg_position = { 'DSA': 0, 'RSA': 1, 'EC': 0, } key_type = func_key_type.get(context.call_function_name_qual) if key_type in ['DSA', 'RSA']: key_size = (context.get_call_arg_value('key_size') or context.get_call_arg_at_position(arg_position[key_type]) or 2048) return _classify_key_size(key_type, key_size) elif key_type == 'EC': curve_key_sizes = { 'SECP192R1': 192, 'SECT163K1': 163, 'SECT163R2': 163, } curve = (context.get_call_arg_value('curve') or context.call_args[arg_position[key_type]]) key_size = curve_key_sizes[curve] if curve in curve_key_sizes else 224 return _classify_key_size(key_type, key_size) def _weak_crypto_key_size_pycrypto(context): func_key_type = { 'Crypto.PublicKey.DSA.generate': 'DSA', 'Crypto.PublicKey.RSA.generate': 'RSA', } key_type = func_key_type.get(context.call_function_name_qual) if key_type: key_size = (context.get_call_arg_value('bits') or context.get_call_arg_at_position(0) or 2048) return _classify_key_size(key_type, key_size) @test.checks('Call') @test.test_id('B505') def weak_cryptographic_key(context): return (_weak_crypto_key_size_cryptography_io(context) or _weak_crypto_key_size_pycrypto(context)) bandit-1.4.0/bandit/plugins/yaml_load.py000066400000000000000000000045161303375232700202250ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright (c) 2016 Rackspace, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. r""" =============================== B506: Test for use of yaml load =============================== This plugin test checks for the unsafe usage of the ``yaml.load`` function from the PyYAML package. The yaml.load function provides the ability to construct an arbitrary Python object, which may be dangerous if you receive a YAML document from an untrusted source. The function yaml.safe_load limits this ability to simple Python objects like integers or lists. Please see http://pyyaml.org/wiki/PyYAMLDocumentation#LoadingYAML for more information on ``yaml.load`` and yaml.safe_load :Example: >> Issue: [yaml_load] Use of unsafe yaml load. Allows instantiation of arbitrary objects. Consider yaml.safe_load(). Severity: Medium Confidence: High Location: examples/yaml_load.py:5 4 ystr = yaml.dump({'a' : 1, 'b' : 2, 'c' : 3}) 5 y = yaml.load(ystr) 6 yaml.dump(y) .. seealso:: - http://pyyaml.org/wiki/PyYAMLDocumentation#LoadingYAML .. versionadded:: 1.0.0 """ import bandit from bandit.core import test_properties as test @test.test_id('B506') @test.checks('Call') def yaml_load(context): if isinstance(context.call_function_name_qual, str): qualname_list = context.call_function_name_qual.split('.') func = qualname_list[-1] if 'yaml' in qualname_list and func == 'load': if not context.check_call_arg_value('Loader', 'SafeLoader'): return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.HIGH, text="Use of unsafe yaml load. Allows instantiation of" " arbitrary objects. Consider yaml.safe_load().", lineno=context.node.lineno, ) bandit-1.4.0/doc/000077500000000000000000000000001303375232700135275ustar00rootroot00000000000000bandit-1.4.0/doc/source/000077500000000000000000000000001303375232700150275ustar00rootroot00000000000000bandit-1.4.0/doc/source/blacklists/000077500000000000000000000000001303375232700171625ustar00rootroot00000000000000bandit-1.4.0/doc/source/blacklists/blacklist_calls.rst000066400000000000000000000001311303375232700230350ustar00rootroot00000000000000--------------- blacklist_calls --------------- .. automodule:: bandit.blacklists.calls bandit-1.4.0/doc/source/blacklists/blacklist_imports.rst000066400000000000000000000001411303375232700234350ustar00rootroot00000000000000----------------- blacklist_imports ----------------- .. automodule:: bandit.blacklists.imports bandit-1.4.0/doc/source/blacklists/index.rst000066400000000000000000000060761303375232700210340ustar00rootroot00000000000000Bandit Blacklist Plugins ======================== Bandit supports built in functionality to implement blacklisting of imports and function calls, this functionality is provided by built in test 'B001'. This test may be filtered as per normal plugin filtering rules. The exact calls and imports that are blacklisted, and the issues reported, are controlled by plugin methods with the entry point 'bandit.blacklists' and can be extended by third party plugins if desired. Blacklist plugins will be discovered by Bandit at startup and called. The returned results are combined into the final data set, subject to Bandit's normal test include/exclude rules allowing for fine grained control over blacklisted items. By convention, blacklisted calls should have IDs in the B3xx range and imports should have IDs in the B4xx range. Plugin functions should return a dictionary mapping AST node types to lists of blacklist data. Currently the following node types are supported: - Call, used for blacklisting calls. - Import, used for blacklisting module imports (this also implicitly tests ImportFrom and Call nodes where the invoked function is Pythons built in '__import__()' method). Items in the data lists are Python dictionaries with the following structure: +-------------+----------------------------------------------------+ | key | data meaning | +=============+====================================================+ | 'name' | The issue name string. | +-------------+----------------------------------------------------+ | 'id' | The bandit ID of the check, this must be unique | | | and is used for filtering blacklist checks. | +-------------+----------------------------------------------------+ | 'qualnames' | A Python list of fully qualified name strings. | +-------------+----------------------------------------------------+ | 'message' | The issue message reported, this is a string that | | | may contain the token '{name}' that will be | | | substituted with the matched qualname in the final | | | report. | +-------------+----------------------------------------------------+ | 'level' | The severity level reported. | +-------------+----------------------------------------------------+ A utility method bandit.blacklists.utils.build_conf_dict is provided to aid building these dictionaries. :Example: .. code-block:: none >> Issue: [B317:blacklist] Using xml.sax.parse to parse untrusted XML data is known to be vulnerable to XML attacks. Replace xml.sax.parse with its defusedxml equivalent function. Severity: Medium Confidence: High Location: ./examples/xml_sax.py:24 23 sax.parseString(xmlString, ExampleContentHandler()) 24 sax.parse('notaxmlfilethatexists.xml', ExampleContentHandler) 25 Complete Plugin Listing ----------------------- .. toctree:: :maxdepth: 1 :glob: * .. versionadded:: 0.17.0 bandit-1.4.0/doc/source/conf.py000066400000000000000000000054351303375232700163350ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import os import sys sys.path.insert(0, os.path.abspath('../..')) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', # 'sphinx.ext.intersphinx', 'oslosphinx' ] # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. # execute "export SPHINX_DEBUG=1" in your terminal to disable # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Bandit' copyright = u'2016, OpenStack Foundation' # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' modindex_common_prefix = ['bandit.'] #-- Options for man page output -------------------------------------------- # Grouping the document tree for man pages. # List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual' man_pages = [ ('man/bandit', 'bandit', u'Python source code security analyzer', [u'OpenStack Security Group'], 1) ] # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. # html_theme_path = ["."] # html_theme = '_theme' # html_static_path = ['static'] html_theme_options = {} # Output file base name for HTML help builder. htmlhelp_basename = '%sdoc' % project # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ('index', '%s.tex' % project, u'%s Documentation' % project, u'OpenStack Foundation', 'manual'), ] # Example configuration for intersphinx: refer to the Python standard library. # intersphinx_mapping = {'http://docs.python.org/': None} bandit-1.4.0/doc/source/config.rst000066400000000000000000000075301303375232700170330ustar00rootroot00000000000000Configuration ============= Bandit is designed to be configurable and cover a wide range of needs, it may be used as either a local developer utility or as part of a full CI/CD pipeline. To provide for these various usage scenarios bandit can be configured via a `YAML `_ file. This file is completely optional and in many cases not needed, it may be specified on the command line by using `-c`. A bandit configuration file may choose the specific test plugins to run and override the default configurations of those tests. An example config might look like the following: .. code-block:: yaml ### profile may optionally select or skip tests # (optional) list included tests here: tests: ['B201', 'B301'] # (optional) list skipped tests here: skips: ['B101', 'B601'] ### override settings - used to set settings for plugins to non-default values any_other_function_with_shell_equals_true: no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp, os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe, os.startfile] shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3, popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput] subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output, utils.execute, utils.execute_with_timeout] If you require several sets of tests for specific tasks, then you should create several config files and pick from them using `-c`. If you only wish to control the specific tests that are to be run (and not their parameters) then using `-s` or `-t` on the command line may be more appropriate. Skipping Tests -------------- The bandit config may contain optional lists of test IDs to either include (`tests`) or exclude (`skips`). These lists are equivalent to using `-t` and `-s` on the command line. If only `tests` is given then bandit will include only those tests, effectively excluding all other tests. If only `skips` is given then bandit will include all tests not in the skips list. If both are given then bandit will include only tests in `tests` and then remove `skips` from that set. It is an error to include the same test ID in both `tests` and `skips`. Note that command line options `-t`/`-s` can still be used in conjunction with `tests` and `skips` given in a config. The result is to concatenate `-t` with `tests` and likewise for `-s` and `skips` before working out the tests to run. Generating a Config ------------------- Bandit ships the tool `bandit-config-generator` designed to take the leg work out of configuration. This tool can generate a configuration file automatically. The generated configuration will include default config blocks for all detected test and blacklist plugins. This data can then be deleted or edited as needed to produce a minimal config as desired. The config generator supports `-t` and `-s` command line options to specify a list of test IDs that should be included or excluded respectively. If no options are given then the generated config will not include `tests` or `skips` sections (but will provide a complete list of all test IDs for reference when editing). Configuring Test Plugins ------------------------ Bandit's configuration file is written in `YAML `_ and options for each plugin test are provided under a section named to match the test method. For example, given a test plugin called 'try_except_pass' its configuration section might look like the following: .. code-block:: yaml try_except_pass: check_typed_exception: True The specific content of the configuration block is determined by the plugin test itself. See the `plugin test list `_ for complete information on configuring each one. bandit-1.4.0/doc/source/formatters/000077500000000000000000000000001303375232700172155ustar00rootroot00000000000000bandit-1.4.0/doc/source/formatters/csv.rst000066400000000000000000000000631303375232700205410ustar00rootroot00000000000000--- csv --- .. automodule:: bandit.formatters.csv bandit-1.4.0/doc/source/formatters/html.rst000066400000000000000000000000671303375232700207160ustar00rootroot00000000000000---- html ---- .. automodule:: bandit.formatters.html bandit-1.4.0/doc/source/formatters/index.rst000066400000000000000000000021351303375232700210570ustar00rootroot00000000000000Bandit Report Formatters ======================== Bandit supports many different formatters to output various security issues in python code. These formatters are created as plugins and new ones can be created to extend the functionality offered by bandit today. Example Formatter ----------------- .. code-block:: python def report(manager, fileobj, sev_level, conf_level, lines=-1): result = bson.dumps(issues) with fileobj: fileobj.write(result) To register your plugin, you have two options: 1. If you're using setuptools directly, add something like the following to your `setup` call:: # If you have an imaginary bson formatter in the bandit_bson module # and a function called `formatter`. entry_points={'bandit.formatters': ['bson = bandit_bson:formatter']} 2. If you're using pbr, add something like the following to your `setup.cfg` file:: [entry_points] bandit.formatters = bson = bandit_bson:formatter Complete Formatter Listing ---------------------------- .. toctree:: :maxdepth: 1 :glob: * bandit-1.4.0/doc/source/formatters/json.rst000066400000000000000000000000671303375232700207230ustar00rootroot00000000000000---- json ---- .. automodule:: bandit.formatters.json bandit-1.4.0/doc/source/formatters/screen.rst000066400000000000000000000000771303375232700212320ustar00rootroot00000000000000------ screen ------ .. automodule:: bandit.formatters.screen bandit-1.4.0/doc/source/formatters/text.rst000066400000000000000000000000671303375232700207360ustar00rootroot00000000000000---- text ---- .. automodule:: bandit.formatters.text bandit-1.4.0/doc/source/formatters/xml.rst000066400000000000000000000000631303375232700205460ustar00rootroot00000000000000--- xml --- .. automodule:: bandit.formatters.xml bandit-1.4.0/doc/source/index.rst000066400000000000000000000012231303375232700166660ustar00rootroot00000000000000Welcome to Bandit's developer documentation! ============================================ Bandit is a tool designed to find common security issues in Python code. To do this, Bandit processes each file, builds an AST from it, and runs appropriate plugins against the AST nodes. Once Bandit has finished scanning all the files, it generates a report. This documentation is generated by the Sphinx toolkit and lives in the source tree. Getting Started =============== .. toctree:: :maxdepth: 1 config plugins/index blacklists/index formatters/index Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` bandit-1.4.0/doc/source/man/000077500000000000000000000000001303375232700156025ustar00rootroot00000000000000bandit-1.4.0/doc/source/man/bandit.rst000066400000000000000000000070151303375232700176000ustar00rootroot00000000000000====== bandit ====== SYNOPSIS ======== bandit [-h] [-r] [-a {file,vuln}] [-n CONTEXT_LINES] [-c CONFIG_FILE] [-p PROFILE] [-t TESTS] [-s SKIPS] [-l] [-i] [-f {csv,html,json,screen,txt,xml}] [-o OUTPUT_FILE] [-v] [-d] [--ignore-nosec] [-x EXCLUDED_PATHS] [-b BASELINE] [--ini INI_PATH] [--version] targets [targets ...] DESCRIPTION =========== ``bandit`` is a tool designed to find common security issues in Python code. To do this Bandit processes each file, builds an AST from it, and runs appropriate plugins against the AST nodes. Once Bandit has finished scanning all the files it generates a report. OPTIONS ======= -h, --help show this help message and exit -r, --recursive find and process files in subdirectories -a {file,vuln}, --aggregate {file,vuln} aggregate output by vulnerability (default) or by filename -n CONTEXT_LINES, --number CONTEXT_LINES maximum number of code lines to output for each issue -c CONFIG_FILE, --configfile CONFIG_FILE optional config file to use for selecting plugins and overriding defaults -p PROFILE, --profile PROFILE profile to use (defaults to executing all tests) -t TESTS, --tests TESTS comma-separated list of test IDs to run -s SKIPS, --skip SKIPS comma-separated list of test IDs to skip -l, --level report only issues of a given severity level or higher (-l for LOW, -ll for MEDIUM, -lll for HIGH) -i, --confidence report only issues of a given confidence level or higher (-i for LOW, -ii for MEDIUM, -iii for HIGH) -f {csv,html,json,screen,txt,xml}, --format {csv,html,json,screen,txt,xml} specify output format -o OUTPUT_FILE, --output OUTPUT_FILE write report to filename -v, --verbose output extra information like excluded and included files -d, --debug turn on debug mode --ignore-nosec do not skip lines with # nosec comments -x EXCLUDED_PATHS, --exclude EXCLUDED_PATHS comma-separated list of paths to exclude from scan (note that these are in addition to the excluded paths provided in the config file) -b BASELINE, --baseline BASELINE path of a baseline report to compare against (only JSON-formatted files are accepted) --ini INI_PATH path to a .bandit file that supplies command line arguments --version show program's version number and exit FILES ===== .bandit file that supplies command line arguments /etc/bandit/bandit.yaml legacy bandit configuration file EXAMPLES ======== Example usage across a code tree:: bandit -r ~/openstack-repo/keystone Example usage across the ``examples/`` directory, showing three lines of context and only reporting on the high-severity issues:: bandit examples/*.py -n 3 -lll Bandit can be run with profiles. To run Bandit against the examples directory using only the plugins listed in the ShellInjection profile:: bandit examples/*.py -p ShellInjection Bandit also supports passing lines of code to scan using standard input. To run Bandit with standard input:: cat examples/imports.py | bandit - SEE ALSO ======== pylint(1) bandit-1.4.0/doc/source/plugins/000077500000000000000000000000001303375232700165105ustar00rootroot00000000000000bandit-1.4.0/doc/source/plugins/any_other_function_with_shell_equals_true.rst000066400000000000000000000004151303375232700277720ustar00rootroot00000000000000----------------------------------------------- B604: any_other_function_with_shell_equals_true ----------------------------------------------- .. currentmodule:: bandit.plugins.injection_shell .. autofunction:: any_other_function_with_shell_equals_true :noindex: bandit-1.4.0/doc/source/plugins/assert_used.rst000066400000000000000000000001361303375232700215630ustar00rootroot00000000000000----------------- B101: assert_used ----------------- .. automodule:: bandit.plugins.asserts bandit-1.4.0/doc/source/plugins/exec_used.rst000066400000000000000000000001251303375232700212040ustar00rootroot00000000000000--------------- B102: exec_used --------------- .. automodule:: bandit.plugins.exec bandit-1.4.0/doc/source/plugins/execute_with_run_as_root_equals_true.rst000066400000000000000000000002561303375232700267650ustar00rootroot00000000000000------------------------------------------ B111: execute_with_run_as_root_equals_true ------------------------------------------ .. automodule:: bandit.plugins.exec_as_root bandit-1.4.0/doc/source/plugins/flask_debug_true.rst000066400000000000000000000001571303375232700225520ustar00rootroot00000000000000---------------------- B201: flask_debug_true ---------------------- .. automodule:: bandit.plugins.app_debug bandit-1.4.0/doc/source/plugins/hardcoded_bind_all_interfaces.rst000066400000000000000000000002501303375232700252030ustar00rootroot00000000000000----------------------------------- B104: hardcoded_bind_all_interfaces ----------------------------------- .. automodule:: bandit.plugins.general_bind_all_interfaces bandit-1.4.0/doc/source/plugins/hardcoded_password_funcarg.rst000066400000000000000000000003341303375232700246060ustar00rootroot00000000000000-------------------------------- B106: hardcoded_password_funcarg -------------------------------- .. currentmodule:: bandit.plugins.general_hardcoded_password .. autofunction:: hardcoded_password_funcarg :noindex: bandit-1.4.0/doc/source/plugins/hardcoded_password_funcdef.rst000066400000000000000000000003341303375232700245730ustar00rootroot00000000000000-------------------------------- B107: hardcoded_password_default -------------------------------- .. currentmodule:: bandit.plugins.general_hardcoded_password .. autofunction:: hardcoded_password_default :noindex: bandit-1.4.0/doc/source/plugins/hardcoded_password_string.rst000066400000000000000000000003301303375232700244630ustar00rootroot00000000000000------------------------------- B105: hardcoded_password_string ------------------------------- .. currentmodule:: bandit.plugins.general_hardcoded_password .. autofunction:: hardcoded_password_string :noindex: bandit-1.4.0/doc/source/plugins/hardcoded_sql_expressions.rst000066400000000000000000000002161303375232700244770ustar00rootroot00000000000000------------------------------- B608: hardcoded_sql_expressions ------------------------------- .. automodule:: bandit.plugins.injection_sql bandit-1.4.0/doc/source/plugins/hardcoded_tmp_directory.rst000066400000000000000000000002201303375232700241150ustar00rootroot00000000000000----------------------------- B108: hardcoded_tmp_directory ----------------------------- .. automodule:: bandit.plugins.general_hardcoded_tmp bandit-1.4.0/doc/source/plugins/index.rst000066400000000000000000000100031303375232700203430ustar00rootroot00000000000000Bandit Test Plugins =================== Bandit supports many different tests to detect various security issues in python code. These tests are created as plugins and new ones can be created to extend the functionality offered by bandit today. Writing Tests ------------- To write a test: - Identify a vulnerability to build a test for, and create a new file in examples/ that contains one or more cases of that vulnerability. - Create a new Python source file to contain your test, you can reference existing tests for examples. - Consider the vulnerability you're testing for, mark the function with one or more of the appropriate decorators: - @checks('Call') - @checks('Import', 'ImportFrom') - @checks('Str') - Register your plugin using the `bandit.plugins` entry point, see example. - The function that you create should take a parameter "context" which is an instance of the context class you can query for information about the current element being examined. You can also get the raw AST node for more advanced use cases. Please see the `context.py` file for more. - Extend your Bandit configuration file as needed to support your new test. - Execute Bandit against the test file you defined in `examples/` and ensure that it detects the vulnerability. Consider variations on how this vulnerability might present itself and extend the example file and the test function accordingly. Config Generation ----------------- In Bandit 1.0+ config files are optional. Plugins that need config settings are required to implement a module global `gen_config` function. This function is called with a single parameter, the test plugin name. It should return a dictionary with keys being the config option names and values being the default settings for each option. An example `gen_config` might look like the following: .. code-block:: python def gen_config(name): if name == 'try_except_continue': return {'check_typed_exception': False} When no config file is specified, or when the chosen file has no section pertaining to a given plugin, `gen_config` will be called to provide defaults. The config file generation tool `bandit-config-generator` will also call `gen_config` on all discovered plugins to produce template config blocks. If the defaults are acceptable then these blocks may be deleted to create a minimal configuration, or otherwise edited as needed. The above example would produce the following config snippet. .. code-block:: yaml try_except_continue: {check_typed_exception: false} Example Test Plugin ------------------- .. code-block:: python @bandit.checks('Call') def prohibit_unsafe_deserialization(context): if 'unsafe_load' in context.call_function_name_qual: return bandit.Issue( severity=bandit.HIGH, confidence=bandit.HIGH, text="Unsafe deserialization detected." ) To register your plugin, you have two options: 1. If you're using setuptools directly, add something like the following to your `setup` call:: # If you have an imaginary bson formatter in the bandit_bson module # and a function called `formatter`. entry_points={'bandit.formatters': ['bson = bandit_bson:formatter']} # Or a check for using mako templates in bandit_mako that entry_points={'bandit.plugins': ['mako = bandit_mako']} 2. If you're using pbr, add something like the following to your `setup.cfg` file:: [entry_points] bandit.formatters = bson = bandit_bson:formatter bandit.plugins = mako = bandit_mako Plugin ID Groupings ------------------- ======= =========== ID Description ======= =========== B1xx misc tests B2xx application/framework misconfiguration B3xx blacklists (calls) B4xx blacklists (imports) B5xx cryptography B6xx injection B7xx XSS ======= =========== Complete Test Plugin Listing ---------------------------- .. toctree:: :maxdepth: 1 :glob: * bandit-1.4.0/doc/source/plugins/jinja2_autoescape_false.rst000066400000000000000000000002131303375232700237760ustar00rootroot00000000000000----------------------------- B701: jinja2_autoescape_false ----------------------------- .. automodule:: bandit.plugins.jinja2_templates bandit-1.4.0/doc/source/plugins/linux_commands_wildcard_injection.rst000066400000000000000000000002531303375232700261750ustar00rootroot00000000000000--------------------------------------- B609: linux_commands_wildcard_injection --------------------------------------- .. automodule:: bandit.plugins.injection_wildcard bandit-1.4.0/doc/source/plugins/paramiko_calls.rst000066400000000000000000000001621303375232700222220ustar00rootroot00000000000000-------------------- B601: paramiko_calls -------------------- .. automodule:: bandit.plugins.injection_paramiko bandit-1.4.0/doc/source/plugins/password_config_option_not_marked_secret.rst000066400000000000000000000003021303375232700275640ustar00rootroot00000000000000---------------------------------------------- B109: password_config_option_not_marked_secret ---------------------------------------------- .. automodule:: bandit.plugins.secret_config_option bandit-1.4.0/doc/source/plugins/request_with_no_cert_validation.rst000066400000000000000000000002641303375232700257120ustar00rootroot00000000000000------------------------------------- B501: request_with_no_cert_validation ------------------------------------- .. automodule:: bandit.plugins.crypto_request_no_cert_validation bandit-1.4.0/doc/source/plugins/set_bad_file_permissions.rst000066400000000000000000000002321303375232700242720ustar00rootroot00000000000000------------------------------ B103: set_bad_file_permissions ------------------------------ .. automodule:: bandit.plugins.general_bad_file_permissions bandit-1.4.0/doc/source/plugins/ssl_with_bad_defaults.rst000066400000000000000000000002761303375232700236000ustar00rootroot00000000000000--------------------------- B503: ssl_with_bad_defaults --------------------------- .. currentmodule:: bandit.plugins.insecure_ssl_tls .. autofunction:: ssl_with_bad_defaults :noindex: bandit-1.4.0/doc/source/plugins/ssl_with_bad_version.rst000066400000000000000000000002721303375232700234520ustar00rootroot00000000000000-------------------------- B502: ssl_with_bad_version -------------------------- .. currentmodule:: bandit.plugins.insecure_ssl_tls .. autofunction:: ssl_with_bad_version :noindex: bandit-1.4.0/doc/source/plugins/ssl_with_no_version.rst000066400000000000000000000002661303375232700233430ustar00rootroot00000000000000------------------------- B504: ssl_with_no_version ------------------------- .. currentmodule:: bandit.plugins.insecure_ssl_tls .. autofunction:: ssl_with_no_version :noindex: bandit-1.4.0/doc/source/plugins/start_process_with_a_shell.rst000066400000000000000000000003211303375232700246530ustar00rootroot00000000000000-------------------------------- B605: start_process_with_a_shell -------------------------------- .. currentmodule:: bandit.plugins.injection_shell .. autofunction:: start_process_with_a_shell :noindex: bandit-1.4.0/doc/source/plugins/start_process_with_no_shell.rst000066400000000000000000000003251303375232700250530ustar00rootroot00000000000000--------------------------------- B606: start_process_with_no_shell --------------------------------- .. currentmodule:: bandit.plugins.injection_shell .. autofunction:: start_process_with_no_shell :noindex: bandit-1.4.0/doc/source/plugins/start_process_with_partial_path.rst000066400000000000000000000003451303375232700257220ustar00rootroot00000000000000------------------------------------- B607: start_process_with_partial_path ------------------------------------- .. currentmodule:: bandit.plugins.injection_shell .. autofunction:: start_process_with_partial_path :noindex: bandit-1.4.0/doc/source/plugins/subprocess_popen_with_shell_equals_true.rst000066400000000000000000000004051303375232700274650ustar00rootroot00000000000000--------------------------------------------- B602: subprocess_popen_with_shell_equals_true --------------------------------------------- .. currentmodule:: bandit.plugins.injection_shell .. autofunction:: subprocess_popen_with_shell_equals_true :noindex: bandit-1.4.0/doc/source/plugins/subprocess_without_shell_equals_true.rst000066400000000000000000000003711303375232700270160ustar00rootroot00000000000000------------------------------------------ B603: subprocess_without_shell_equals_true ------------------------------------------ .. currentmodule:: bandit.plugins.injection_shell .. autofunction:: subprocess_without_shell_equals_true :noindex: bandit-1.4.0/doc/source/plugins/try_except_continue.rst000066400000000000000000000002021303375232700233260ustar00rootroot00000000000000------------------------- B112: try_except_continue ------------------------- .. automodule:: bandit.plugins.try_except_continue bandit-1.4.0/doc/source/plugins/try_except_pass.rst000066400000000000000000000001621303375232700224550ustar00rootroot00000000000000--------------------- B110: try_except_pass --------------------- .. automodule:: bandit.plugins.try_except_pass bandit-1.4.0/doc/source/plugins/use_of_mako_templates.rst000066400000000000000000000002031303375232700236020ustar00rootroot00000000000000--------------------------- B702: use_of_mako_templates --------------------------- .. automodule:: bandit.plugins.mako_templates bandit-1.4.0/doc/source/plugins/weak_cryptographic_key.rst000066400000000000000000000002161303375232700237760ustar00rootroot00000000000000---------------------------- B505: weak_cryptographic_key ---------------------------- .. automodule:: bandit.plugins.weak_cryptographic_key bandit-1.4.0/doc/source/plugins/yaml_load.rst000066400000000000000000000001321303375232700211770ustar00rootroot00000000000000--------------- B506: yaml_load --------------- .. automodule:: bandit.plugins.yaml_load bandit-1.4.0/examples/000077500000000000000000000000001303375232700146005ustar00rootroot00000000000000bandit-1.4.0/examples/assert.py000066400000000000000000000000141303375232700164460ustar00rootroot00000000000000assert True bandit-1.4.0/examples/binding.py000066400000000000000000000001761303375232700165700ustar00rootroot00000000000000import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('0.0.0.0', 31137)) s.bind(('192.168.0.1', 8080)) bandit-1.4.0/examples/cipher-modes.py000066400000000000000000000004011303375232700175240ustar00rootroot00000000000000from cryptography.hazmat.primitives.ciphers.modes import CBC from cryptography.hazmat.primitives.ciphers.modes import ECB # Insecure mode mode = ECB(iv) # Secure cipher and mode cipher = AES.new(key, blockalgo.MODE_CTR, iv) # Secure mode mode = CBC(iv) bandit-1.4.0/examples/ciphers.py000066400000000000000000000037061303375232700166150ustar00rootroot00000000000000from Crypto.Cipher import ARC2 from Crypto.Cipher import ARC4 from Crypto.Cipher import Blowfish from Crypto.Cipher import DES from Crypto.Cipher import XOR from Crypto.Hash import SHA from Crypto import Random from Crypto.Util import Counter from cryptography.hazmat.primitives.ciphers import Cipher from cryptography.hazmat.primitives.ciphers import algorithms from cryptography.hazmat.primitives.ciphers import modes from cryptography.hazmat.backends import default_backend from struct import pack key = b'Sixteen byte key' iv = Random.new().read(ARC2.block_size) cipher = ARC2.new(key, ARC2.MODE_CFB, iv) msg = iv + cipher.encrypt(b'Attack at dawn') key = b'Very long and confidential key' nonce = Random.new().read(16) tempkey = SHA.new(key+nonce).digest() cipher = ARC4.new(tempkey) msg = nonce + cipher.encrypt(b'Open the pod bay doors, HAL') bs = Blowfish.block_size key = b'An arbitrarily long key' iv = Random.new().read(bs) cipher = Blowfish.new(key, Blowfish.MODE_CBC, iv) plaintext = b'docendo discimus ' plen = bs - divmod(len(plaintext),bs)[1] padding = [plen]*plen padding = pack('b'*plen, *padding) msg = iv + cipher.encrypt(plaintext + padding) key = b'-8B key-' nonce = Random.new().read(DES.block_size/2) ctr = Counter.new(DES.block_size*8/2, prefix=nonce) cipher = DES.new(key, DES.MODE_CTR, counter=ctr) plaintext = b'We are no longer the knights who say ni!' msg = nonce + cipher.encrypt(plaintext) key = b'Super secret key' cipher = XOR.new(key) plaintext = b'Encrypt me' msg = cipher.encrypt(plaintext) cipher = Cipher(algorithms.ARC4(key), mode=None, backend=default_backend()) encryptor = cipher.encryptor() ct = encryptor.update(b"a secret message") cipher = Cipher(algorithms.Blowfish(key), mode=None, backend=default_backend()) encryptor = cipher.encryptor() ct = encryptor.update(b"a secret message") cipher = Cipher(algorithms.IDEA(key), mode=None, backend=default_backend()) encryptor = cipher.encryptor() ct = encryptor.update(b"a secret message") bandit-1.4.0/examples/crypto-md5.py000066400000000000000000000005751303375232700171640ustar00rootroot00000000000000from cryptography.hazmat.primitives import hashes from Crypto.Hash import MD2 as pycrypto_md2 from Crypto.Hash import MD4 as pycrypto_md4 from Crypto.Hash import MD5 as pycrypto_md5 import hashlib hashlib.md5(1) hashlib.md5(1).hexdigest() abc = str.replace(hashlib.md5("1"), "###") print(hashlib.md5("1")) pycrypto_md2.new() pycrypto_md4.new() pycrypto_md5.new() hashes.MD5() bandit-1.4.0/examples/eval.py000066400000000000000000000004331303375232700161010ustar00rootroot00000000000000import os print(eval("1+1")) print(eval("os.getcwd()")) print(eval("os.chmod('%s', 0777)" % 'test.txt')) # A user-defined method named "eval" should not get flagged. class Test(object): def eval(self): print("hi") def foo(self): self.eval() Test().eval() bandit-1.4.0/examples/exec-as-root.py000066400000000000000000000016361303375232700174660ustar00rootroot00000000000000from ceilometer import utils as ceilometer_utils from cinder import utils as cinder_utils from neutron.agent.linux import utils as neutron_utils from nova import utils as nova_utils # Ceilometer ceilometer_utils.execute('gcc --version') ceilometer_utils.execute('gcc --version', run_as_root=False) ceilometer_utils.execute('gcc --version', run_as_root=True) # Cinder cinder_utils.execute('gcc --version') cinder_utils.execute('gcc --version', run_as_root=False) cinder_utils.execute('gcc --version', run_as_root=True) # Neutron neutron_utils.execute('gcc --version') neutron_utils.execute('gcc --version', run_as_root=False) neutron_utils.execute('gcc --version', run_as_root=True) # Nova nova_utils.execute('gcc --version') nova_utils.execute('gcc --version', run_as_root=False) nova_utils.execute('gcc --version', run_as_root=True) nova_utils.trycmd('gcc --version') nova_utils.trycmd('gcc --version', run_as_root=True) bandit-1.4.0/examples/exec-py2.py000066400000000000000000000000361303375232700166050ustar00rootroot00000000000000exec("do evil") exec "do evil"bandit-1.4.0/examples/exec-py3.py000066400000000000000000000000201303375232700165770ustar00rootroot00000000000000exec("do evil") bandit-1.4.0/examples/flask_debug.py000066400000000000000000000003031303375232700174140ustar00rootroot00000000000000from flask import Flask app = Flask(__name__) @app.route('/') def main(): raise #bad app.run(debug=True) #okay app.run() app.run(debug=False) #unrelated run() run(debug=True) run(debug) bandit-1.4.0/examples/ftplib.py000066400000000000000000000001641303375232700164330ustar00rootroot00000000000000from ftplib import FTP ftp = FTP('ftp.debian.org') ftp.login() ftp.cwd('debian') ftp.retrlines('LIST') ftp.quit()bandit-1.4.0/examples/hardcoded-passwords.py000066400000000000000000000006641303375232700211200ustar00rootroot00000000000000def someFunction(user, password="Admin"): print("Hi " + user) def someFunction2(password): if password == "root": print("OK, logged in") def noMatch(password): if password == '': print("No password!") def NoMatch2(password): if password == "ajklawejrkl42348swfgkg": print("Nice password!") doLogin(password="blerg") password = "blerg" d["password"] = "blerg" def NoMatch3((a, b)): pass bandit-1.4.0/examples/hardcoded-tmp.py000066400000000000000000000004461303375232700176710ustar00rootroot00000000000000f = open('/tmp/abc', 'w') f.write('def') f.close() # ok f = open('/abc/tmp', 'w') f.write('def') f.close() f = open('/var/tmp/123', 'w') f.write('def') f.close() f = open('/dev/shm/unit/test', 'w') f.write('def') f.close() # Negative test f = open('/foo/bar', 'w') f.write('def') f.close() bandit-1.4.0/examples/httplib_https.py000066400000000000000000000002761303375232700200470ustar00rootroot00000000000000import httplib c = httplib.HTTPSConnection("example.com") import http.client c = http.client.HTTPSConnection("example.com") import six six.moves.http_client.HTTPSConnection("example.com") bandit-1.4.0/examples/httpoxy_cgihandler.py000066400000000000000000000004701303375232700210520ustar00rootroot00000000000000import requests import wsgiref.handlers def application(environ, start_response): r = requests.get('https://192.168.0.42/private/api/foobar') start_response('200 OK', [('Content-Type', 'text/plain')]) return [r.content] if __name__ == '__main__': wsgiref.handlers.CGIHandler().run(application) bandit-1.4.0/examples/httpoxy_twisted_directory.py000066400000000000000000000003501303375232700225160ustar00rootroot00000000000000from twisted.internet import reactor from twisted.web import static, server, twcgi root = static.File("/root") root.putChild("cgi-bin", twcgi.CGIDirectory("/var/www/cgi-bin")) reactor.listenTCP(80, server.Site(root)) reactor.run() bandit-1.4.0/examples/httpoxy_twisted_script.py000066400000000000000000000003601303375232700220170ustar00rootroot00000000000000from twisted.internet import reactor from twisted.web import static, server, twcgi root = static.File("/root") root.putChild("login.cgi", twcgi.CGIScript("/var/www/cgi-bin/login.py")) reactor.listenTCP(80, server.Site(root)) reactor.run() bandit-1.4.0/examples/imports-aliases.py000066400000000000000000000004471303375232700202730ustar00rootroot00000000000000from subprocess import Popen as pop import hashlib as h import hashlib as hh import hashlib as hhh import hashlib as hhhh from pickle import loads as lp import pickle as p pop('/bin/gcc --version', shell=True) h.md5('1') hh.md5('2') hhh.md5('3').hexdigest() hhhh.md5('4') lp({'key': 'value'}) bandit-1.4.0/examples/imports-from.py000066400000000000000000000002221303375232700176040ustar00rootroot00000000000000from subprocess import Popen from ..foo import sys from . import sys from .. import sys from .. import subprocess from ..subprocess import Popen bandit-1.4.0/examples/imports-function.py000066400000000000000000000005401303375232700204710ustar00rootroot00000000000000os = __import__("os") pickle = __import__("pickle") sys = __import__("sys") subprocess = __import__("subprocess") # this has been reported in the wild, though it's invalid python # see bug https://bugs.launchpad.net/bandit/+bug/1396333 __import__() # TODO(??): bandit can not find this one unfortunatly (no symbol tab) a = 'subprocess' __import__(a) bandit-1.4.0/examples/imports.py000066400000000000000000000000651303375232700166500ustar00rootroot00000000000000import os import pickle import sys import subprocess bandit-1.4.0/examples/input.py000066400000000000000000000000101303375232700163000ustar00rootroot00000000000000input() bandit-1.4.0/examples/jinja2_templating.py000066400000000000000000000011231303375232700205500ustar00rootroot00000000000000import jinja2 from jinja2 import Environment templateLoader = jinja2.FileSystemLoader( searchpath="/" ) something = '' Environment(loader=templateLoader, load=templateLoader, autoescape=True) templateEnv = jinja2.Environment(autoescape=True, loader=templateLoader ) Environment(loader=templateLoader, load=templateLoader, autoescape=something) templateEnv = jinja2.Environment(autoescape=False, loader=templateLoader ) Environment(loader=templateLoader, load=templateLoader, autoescape=False) Environment(loader=templateLoader, load=templateLoader) bandit-1.4.0/examples/mako_templating.py000066400000000000000000000004441303375232700203270ustar00rootroot00000000000000from mako.template import Template import mako from mako import template Template("hello") # XXX(fletcher): for some reason, bandit is missing the one below. keeping it # in for now so that if it gets fixed inadvertitently we know. mako.template.Template("hern") template.Template("hern") bandit-1.4.0/examples/mark_safe.py000066400000000000000000000001461303375232700171030ustar00rootroot00000000000000from django.utils import safestring mystr = 'Hello World' mystr = safestring.mark_safe(mystr) bandit-1.4.0/examples/marshal_deserialize.py000066400000000000000000000003551303375232700211640ustar00rootroot00000000000000import marshal import tempfile serialized = marshal.dumps({'a': 1}) print(marshal.loads(serialized)) file_obj = tempfile.TemporaryFile() marshal.dump(range(5), file_obj) file_obj.seek(0) print(marshal.load(file_obj)) file_obj.close() bandit-1.4.0/examples/mktemp.py000066400000000000000000000002301303375232700164420ustar00rootroot00000000000000from tempfile import mktemp import tempfile.mktemp as mt import tempfile as tmp foo = 'hi' mktemp(foo) tempfile.mktemp('foo') mt(foo) tmp.mktemp(foo) bandit-1.4.0/examples/multiline_statement.py000066400000000000000000000002611303375232700212370ustar00rootroot00000000000000import subprocess subprocess.check_output("/some_command", "args", shell=True, universal_newlines=True) bandit-1.4.0/examples/new_candidates-all.py000066400000000000000000000012321303375232700206660ustar00rootroot00000000000000import xml import yaml def subprocess_shell_cmd(): # sample function with known subprocess shell cmd candidates # candidate #1 subprocess.Popen('/bin/ls *', shell=True) # candidate #2 subprocess.Popen('/bin/ls *', shell=True) # nosec def yaml_load(): # sample function with known yaml.load candidates temp_str = yaml.dump({'a': '1', 'b': '2'}) # candidate #3 y = yaml.load(temp_str) # candidate #4 y = yaml.load(temp_str) # nosec def xml_sax_make_parser(): # sample function with known xml.sax.make_parser candidates # candidate #5 xml.sax.make_parser() # candidate #6 xml.sax.make_parser() # nosec bandit-1.4.0/examples/new_candidates-none.py000066400000000000000000000004011303375232700210520ustar00rootroot00000000000000def subprocess_shell_cmd(): # sample function with known subprocess shell cmd candidates def yaml_load(): # sample function with known yaml.load candidates def xml_sax_make_parser(): # sample function with known xml.sax.make_parser candidates bandit-1.4.0/examples/new_candidates-nosec.py000066400000000000000000000007751303375232700212400ustar00rootroot00000000000000import xml import yaml def subprocess_shell_cmd(): # sample function with known subprocess shell cmd candidates # candidate #2 subprocess.Popen('/bin/ls *', shell=True) # nosec def yaml_load(): # sample function with known yaml.load candidates temp_str = yaml.dump({'a': '1', 'b': '2'}) # candidate #4 y = yaml.load(temp_str) # nosec def xml_sax_make_parser(): # sample function with known xml.sax.make_parser candidates # candidate #6 xml.sax.make_parser() # nosec bandit-1.4.0/examples/new_candidates-some.py000066400000000000000000000010761303375232700210670ustar00rootroot00000000000000import xml import yaml def subprocess_shell_cmd(): # sample function with known subprocess shell cmd candidates # candidate #1 subprocess.Popen('/bin/ls *', shell=True) # candidate #2 subprocess.Popen('/bin/ls *', shell=True) # nosec def yaml_load(): # sample function with known yaml.load candidates temp_str = yaml.dump({'a': '1', 'b': '2'}) # candidate #4 y = yaml.load(temp_str) # nosec def xml_sax_make_parser(): # sample function with known xml.sax.make_parser candidates # candidate #6 xml.sax.make_parser() # nosec bandit-1.4.0/examples/nonsense.py000066400000000000000000000000101303375232700167710ustar00rootroot00000000000000test(hi bandit-1.4.0/examples/nonsense2.py000066400000000000000000000000501303375232700170570ustar00rootroot00000000000000‹(æ Wnonsense.py+I-.ÑÈÈä˜>úbbandit-1.4.0/examples/nosec.py000066400000000000000000000004011303375232700162540ustar00rootroot00000000000000subprocess.Popen('/bin/ls *', shell=True) #nosec (on the line) subprocess.Popen('/bin/ls *', #nosec (at the start of function call) shell=True) subprocess.Popen('/bin/ls *', shell=True) #nosec (on the specific kwarg line) bandit-1.4.0/examples/okay.py000066400000000000000000000000531303375232700161130ustar00rootroot00000000000000print('hopefully no vulnerabilities here') bandit-1.4.0/examples/os-chmod-py2.py000066400000000000000000000006231303375232700173740ustar00rootroot00000000000000import os import stat keyfile = 'foo' os.chmod('/etc/passwd', 0227) os.chmod('/etc/passwd', 07) os.chmod('/etc/passwd', 0664) os.chmod('/etc/passwd', 0777) os.chmod('/etc/passwd', 0o770) os.chmod('/etc/passwd', 0o776) os.chmod('/etc/passwd', 0o760) os.chmod('~/.bashrc', 511) os.chmod('/etc/hosts', 0o777) os.chmod('/tmp/oh_hai', 0x1ff) os.chmod('/etc/passwd', stat.S_IRWXU) os.chmod(key_file, 0o777) bandit-1.4.0/examples/os-chmod-py3.py000066400000000000000000000006271303375232700174010ustar00rootroot00000000000000import os import stat keyfile = 'foo' os.chmod('/etc/passwd', 0o227) os.chmod('/etc/passwd', 0o7) os.chmod('/etc/passwd', 0o664) os.chmod('/etc/passwd', 0o777) os.chmod('/etc/passwd', 0o770) os.chmod('/etc/passwd', 0o776) os.chmod('/etc/passwd', 0o760) os.chmod('~/.bashrc', 511) os.chmod('/etc/hosts', 0o777) os.chmod('/tmp/oh_hai', 0x1ff) os.chmod('/etc/passwd', stat.S_IRWXU) os.chmod(key_file, 0o777) bandit-1.4.0/examples/os-exec.py000066400000000000000000000003501303375232700165130ustar00rootroot00000000000000import os os.execl(path, arg0, arg1) os.execle(path, arg0, arg1, env) os.execlp(file, arg0, arg1) os.execlpe(file, arg0, arg1, env) os.execv(path, args) os.execve(path, args, env) os.execvp(file, args) os.execvpe(file, args, env) bandit-1.4.0/examples/os-popen.py000066400000000000000000000004751303375232700167200ustar00rootroot00000000000000import os from os import popen import os as o from os import popen as pos os.popen('/bin/uname -av') popen('/bin/uname -av') o.popen('/bin/uname -av') pos('/bin/uname -av') os.popen2('/bin/uname -av') os.popen3('/bin/uname -av') os.popen4('/bin/uname -av') os.popen4('/bin/uname -av; rm -rf /') os.popen4(some_var) bandit-1.4.0/examples/os-spawn.py000066400000000000000000000003571303375232700167260ustar00rootroot00000000000000import os os.spawnl(mode, path) os.spawnle(mode, path, env) os.spawnlp(mode, file) os.spawnlpe(mode, file, env) os.spawnv(mode, path, args) os.spawnve(mode, path, args, env) os.spawnvp(mode, file, args) os.spawnvpe(mode, file, args, env) bandit-1.4.0/examples/os-startfile.py000066400000000000000000000001441303375232700175650ustar00rootroot00000000000000import os os.startfile('/bin/foo.docx') os.startfile('/bin/bad.exe') os.startfile('/bin/text.txt') bandit-1.4.0/examples/os_system.py000066400000000000000000000000451303375232700171760ustar00rootroot00000000000000import os os.system('/bin/echo hi') bandit-1.4.0/examples/paramiko_injection.py000066400000000000000000000003121303375232700210130ustar00rootroot00000000000000import paramiko # this is not safe paramiko.exec_command('something; really; unsafe') # this is safe paramiko.connect('somehost') # this is not safe SSHClient.invoke_shell('something; bad; here\n') bandit-1.4.0/examples/partial_path_process.py000066400000000000000000000004721303375232700213630ustar00rootroot00000000000000from subprocess import Popen as pop pop('gcc --version', shell=False) pop('/bin/gcc --version', shell=False) pop(var, shell=False) pop(['ls', '-l'], shell=False) pop(['/bin/ls', '-l'], shell=False) pop('../ls -l', shell=False) pop('c:\hello\something', shell=False) pop('c:/hello/something_else', shell=False) bandit-1.4.0/examples/pickle_deserialize.py000066400000000000000000000010421303375232700207760ustar00rootroot00000000000000import cPickle import pickle import StringIO # pickle pick = pickle.dumps({'a': 'b', 'c': 'd'}) print(pickle.loads(pick)) file_obj = StringIO.StringIO() pickle.dump([1, 2, '3'], file_obj) file_obj.seek(0) print(pickle.load(file_obj)) file_obj.seek(0) print(pickle.Unpickler(file_obj).load()) # cPickle serialized = cPickle.dumps({(): []}) print(cPickle.loads(serialized)) file_obj = StringIO.StringIO() cPickle.dump((1,), file_obj) file_obj.seek(0) print(cPickle.load(file_obj)) file_obj.seek(0) print(cPickle.Unpickler(file_obj).load()) bandit-1.4.0/examples/popen_wrappers.py000066400000000000000000000010031303375232700202100ustar00rootroot00000000000000import commands import popen2 print(commands.getstatusoutput('/bin/echo / | xargs ls')) print(commands.getoutput('/bin/echo / | xargs ls')) # This one is safe. print(commands.getstatus('/bin/echo / | xargs ls')) print(popen2.popen2('/bin/echo / | xargs ls')[0].read()) print(popen2.popen3('/bin/echo / | xargs ls')[0].read()) print(popen2.popen4('/bin/echo / | xargs ls')[0].read()) print(popen2.Popen3('/bin/echo / | xargs ls').fromchild.read()) print(popen2.Popen4('/bin/echo / | xargs ls').fromchild.read()) bandit-1.4.0/examples/random_module.py000066400000000000000000000004301303375232700177740ustar00rootroot00000000000000import random import os import somelib bad = random.random() bad = random.randrange() bad = random.randint() bad = random.choice() bad = random.uniform() bad = random.triangular() good = os.urandom() good = random.SystemRandom() unknown = random() unknown = somelib.a.random() bandit-1.4.0/examples/requests-ssl-verify-disabled.py000066400000000000000000000013001303375232700226650ustar00rootroot00000000000000import requests requests.get('https://gmail.com', verify=True) requests.get('https://gmail.com', verify=False) requests.post('https://gmail.com', verify=True) requests.post('https://gmail.com', verify=False) requests.put('https://gmail.com', verify=True) requests.put('https://gmail.com', verify=False) requests.delete('https://gmail.com', verify=True) requests.delete('https://gmail.com', verify=False) requests.patch('https://gmail.com', verify=True) requests.patch('https://gmail.com', verify=False) requests.options('https://gmail.com', verify=True) requests.options('https://gmail.com', verify=False) requests.head('https://gmail.com', verify=True) requests.head('https://gmail.com', verify=False) bandit-1.4.0/examples/secret-config-option.py000066400000000000000000000013641303375232700212140ustar00rootroot00000000000000from oslo_config import cfg # Correct secret = True opts = [ cfg.StrOpt('admin_user', help="User's name"), cfg.StrOpt('admin_password', secret=True, help="User's password"), cfg.StrOpt('nova_password', secret=secret, help="Nova user password"), ] # Incorrect: password not marked secret ldap_opts = [ cfg.StrOpt('ldap_user', help="LDAP bind user name"), cfg.StrOpt('ldap_password', help="LDAP bind user password"), cfg.StrOpt('ldap_password_attribute', help="LDAP password attribute (default userPassword"), cfg.StrOpt('user_password', secret=False, help="User password"), ] bandit-1.4.0/examples/skip.py000066400000000000000000000004211303375232700161150ustar00rootroot00000000000000subprocess.call(["/bin/ls", "-l"]) subprocess.call(["/bin/ls", "-l"]) #noqa subprocess.call(["/bin/ls", "-l"]) # noqa subprocess.call(["/bin/ls", "-l"]) # nosec subprocess.call(["/bin/ls", "-l"]) subprocess.call(["/bin/ls", "-l"]) #nosec subprocess.call(["/bin/ls", "-l"]) bandit-1.4.0/examples/sql_statements.py000066400000000000000000000022721303375232700202230ustar00rootroot00000000000000import sqlalchemy # bad query = "SELECT * FROM foo WHERE id = '%s'" % identifier query = "INSERT INTO foo VALUES ('a', 'b', '%s')" % value query = "DELETE FROM foo WHERE id = '%s'" % identifier query = "UPDATE foo SET value = 'b' WHERE id = '%s'" % identifier # bad cur.execute("SELECT * FROM foo WHERE id = '%s'" % identifier) cur.execute("INSERT INTO foo VALUES ('a', 'b', '%s')" % value) cur.execute("DELETE FROM foo WHERE id = '%s'" % identifier) cur.execute("UPDATE foo SET value = 'b' WHERE id = '%s'" % identifier) # good cur.execute("SELECT * FROM foo WHERE id = '%s'", identifier) cur.execute("INSERT INTO foo VALUES ('a', 'b', '%s')", value) cur.execute("DELETE FROM foo WHERE id = '%s'", identifier) cur.execute("UPDATE foo SET value = 'b' WHERE id = '%s'", identifier) # bad query = "SELECT " + val + " FROM " + val +" WHERE id = " + val # bad cur.execute("SELECT " + val + " FROM " + val +" WHERE id = " + val) # bug: https://bugs.launchpad.net/bandit/+bug/1479625 def a(): def b(): pass return b a()("SELECT %s FROM foo" % val) # real world false positives choices=[('server_list', _("Select from active instances"))] print("delete from the cache as the first argument") bandit-1.4.0/examples/ssl-insecure-version.py000066400000000000000000000015741303375232700212600ustar00rootroot00000000000000import ssl from pyOpenSSL import SSL ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv2) SSL.Context(method=SSL.SSLv2_METHOD) SSL.Context(method=SSL.SSLv23_METHOD) herp_derp(ssl_version=ssl.PROTOCOL_SSLv2) herp_derp(method=SSL.SSLv2_METHOD) herp_derp(method=SSL.SSLv23_METHOD) # strict tests ssl.wrap_socket(ssl_version=ssl.PROTOCOL_SSLv3) ssl.wrap_socket(ssl_version=ssl.PROTOCOL_TLSv1) SSL.Context(method=SSL.SSLv3_METHOD) SSL.Context(method=SSL.TLSv1_METHOD) herp_derp(ssl_version=ssl.PROTOCOL_SSLv3) herp_derp(ssl_version=ssl.PROTOCOL_TLSv1) herp_derp(method=SSL.SSLv3_METHOD) herp_derp(method=SSL.TLSv1_METHOD) ssl.wrap_socket() def open_ssl_socket(version=ssl.PROTOCOL_SSLv2): pass def open_ssl_socket(version=SSL.SSLv2_METHOD): pass def open_ssl_socket(version=SSL.SSLv23_METHOD): pass # this one will pass ok def open_ssl_socket(version=SSL.TLSv1_1_METHOD): pass bandit-1.4.0/examples/subprocess_shell.py000066400000000000000000000016641303375232700205400ustar00rootroot00000000000000import subprocess from subprocess import Popen as pop def Popen(*args, **kwargs): print('hi') pop('/bin/gcc --version', shell=True) Popen('/bin/gcc --version', shell=True) subprocess.Popen('/bin/gcc --version', shell=True) subprocess.Popen(['/bin/gcc', '--version'], shell=False) subprocess.Popen(['/bin/gcc', '--version']) subprocess.call(["/bin/ls", "-l" ]) subprocess.call('/bin/ls -l', shell=True) subprocess.check_call(['/bin/ls', '-l'], shell=False) subprocess.check_call('/bin/ls -l', shell=True) subprocess.check_output(['/bin/ls', '-l']) subprocess.check_output('/bin/ls -l', shell=True) subprocess.Popen('/bin/ls *', shell=True) subprocess.Popen('/bin/ls %s' % ('something',), shell=True) subprocess.Popen('/bin/ls {}'.format('something'), shell=True) command = "/bin/ls" + unknown_function() subprocess.Popen(command, shell=True) subprocess.Popen('/bin/ls && cat /etc/passwd', shell=True) bandit-1.4.0/examples/telnetlib.py000066400000000000000000000005121303375232700171320ustar00rootroot00000000000000import telnetlib import getpass host = sys.argv[1] username = raw_input('Username:') password = getpass.getpass() tn = telnetlib.Telnet(host) tn.read_until("login: ") tn.write(username + "\n") if password: tn.read_until("Password: ") tn.write(password + "\n") tn.write("ls\n") tn.write("exit\n") print(tn.read_all()) bandit-1.4.0/examples/try_except_continue.py000066400000000000000000000005501303375232700212440ustar00rootroot00000000000000# bad for i in {0,1}: try: a = i except: continue # bad while keep_trying: try: a = 1 except Exception: continue # bad for i in {0,2}: try: a = i except ZeroDivisionError: continue except: a = 2 # good while keep_trying: try: a = 1 except: a = 2 bandit-1.4.0/examples/try_except_pass.py000066400000000000000000000004121303375232700203630ustar00rootroot00000000000000# bad try: a = 1 except: pass # bad try: a = 1 except Exception: pass # bad try: a = 1 except ZeroDivisionError: pass except: a = 2 # good try: a = 1 except: a = 2 # silly, but ok try: a = 1 except: pass a = 2 bandit-1.4.0/examples/urlopen.py000066400000000000000000000035271303375232700166450ustar00rootroot00000000000000''' Example dangerous usage of urllib[2] opener functions The urllib and urllib2 opener functions and object can open http, ftp, and file urls. Often, the ability to open file urls is overlooked leading to code that can unexpectedly open files on the local server. This could be used by an attacker to leak information about the server. ''' import urllib import urllib2 # Python 3 import urllib.request # Six import six def test_urlopen(): # urllib url = urllib.quote('file:///bin/ls') urllib.urlopen(url, 'blah', 32) urllib.urlretrieve('file:///bin/ls', '/bin/ls2') opener = urllib.URLopener() opener.open('file:///bin/ls') opener.retrieve('file:///bin/ls') opener = urllib.FancyURLopener() opener.open('file:///bin/ls') opener.retrieve('file:///bin/ls') # urllib2 handler = urllib2.HTTPBasicAuthHandler() handler.add_password(realm='test', uri='http://mysite.com', user='bob') opener = urllib2.build_opener(handler) urllib2.install_opener(opener) urllib2.urlopen('file:///bin/ls') urllib2.Request('file:///bin/ls') # Python 3 urllib.request.urlopen('file:///bin/ls') urllib.request.urlretrieve('file:///bin/ls', '/bin/ls2') opener = urllib.request.URLopener() opener.open('file:///bin/ls') opener.retrieve('file:///bin/ls') opener = urllib.request.FancyURLopener() opener.open('file:///bin/ls') opener.retrieve('file:///bin/ls') # Six six.moves.urllib.request.urlopen('file:///bin/ls') six.moves.urllib.request.urlretrieve('file:///bin/ls', '/bin/ls2') opener = six.moves.urllib.request.URLopener() opener.open('file:///bin/ls') opener.retrieve('file:///bin/ls') opener = six.moves.urllib.request.FancyURLopener() opener.open('file:///bin/ls') opener.retrieve('file:///bin/ls') bandit-1.4.0/examples/utils-shell.py000066400000000000000000000004651303375232700174240ustar00rootroot00000000000000import utils import utils as u u.execute('/bin/gcc --version', shell=True) utils.execute('/bin/gcc --version', shell=True) u.execute_with_timeout('/bin/gcc --version', shell=True) utils.execute_with_timeout('/bin/gcc --version', shell=True) utils.execute_with_timeout(['/bin/gcc', '--version'], shell=False) bandit-1.4.0/examples/weak_cryptographic_key_sizes.py000066400000000000000000000037761303375232700231410ustar00rootroot00000000000000from cryptography.hazmat import backends from cryptography.hazmat.primitives.asymmetric import dsa from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric import rsa from Crypto.PublicKey import DSA from Crypto.PublicKey import RSA # Correct dsa.generate_private_key(key_size=2048, backend=backends.default_backend()) ec.generate_private_key(curve=ec.SECP384R1, backend=backends.default_backend()) rsa.generate_private_key(public_exponent=65537, key_size=2048, backend=backends.default_backend()) DSA.generate(bits=2048) RSA.generate(bits=2048) # Also correct: without keyword args dsa.generate_private_key(4096, backends.default_backend()) ec.generate_private_key(ec.SECP256K1, backends.default_backend()) rsa.generate_private_key(3, 4096, backends.default_backend()) DSA.generate(4096) RSA.generate(4096) # Incorrect: weak key sizes dsa.generate_private_key(key_size=1024, backend=backends.default_backend()) ec.generate_private_key(curve=ec.SECT163R2, backend=backends.default_backend()) rsa.generate_private_key(public_exponent=65537, key_size=1024, backend=backends.default_backend()) DSA.generate(bits=1024) RSA.generate(bits=1024) # Also incorrect: without keyword args dsa.generate_private_key(512, backends.default_backend()) ec.generate_private_key(ec.SECT163R2, backends.default_backend()) rsa.generate_private_key(3, 512, backends.default_backend()) DSA.generate(512) RSA.generate(512) # Don't crash when the size is variable rsa.generate_private_key(public_exponent=65537, key_size=some_key_size, backend=backends.default_backend()) bandit-1.4.0/examples/wildcard-injection.py000066400000000000000000000007371303375232700207320ustar00rootroot00000000000000import os as o import subprocess as subp # Vulnerable to wildcard injection o.system("/bin/tar xvzf *") o.system('/bin/chown *') o.popen2('/bin/chmod *') subp.Popen('/bin/chown *', shell=True) # Not vulnerable to wildcard injection subp.Popen('/bin/rsync *') subp.Popen("/bin/chmod *") subp.Popen(['/bin/chown', '*']) subp.Popen(["/bin/chmod", sys.argv[1], "*"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) o.spawnvp(os.P_WAIT, 'tar', ['tar', 'xvzf', '*']) bandit-1.4.0/examples/xml_etree_celementtree.py000066400000000000000000000010361303375232700216720ustar00rootroot00000000000000import xml.etree.cElementTree as badET import defusedxml.cElementTree as goodET xmlString = "\nTove\nJani\nReminder\nDon't forget me this weekend!\n" # unsafe tree = badET.fromstring(xmlString) print(tree) badET.parse('filethatdoesntexist.xml') badET.iterparse('filethatdoesntexist.xml') a = badET.XMLParser() # safe tree = goodET.fromstring(xmlString) print(tree) goodET.parse('filethatdoesntexist.xml') goodET.iterparse('filethatdoesntexist.xml') a = goodET.XMLParser() bandit-1.4.0/examples/xml_etree_elementtree.py000066400000000000000000000010341303375232700215250ustar00rootroot00000000000000import xml.etree.ElementTree as badET import defusedxml.ElementTree as goodET xmlString = "\nTove\nJani\nReminder\nDon't forget me this weekend!\n" # unsafe tree = badET.fromstring(xmlString) print(tree) badET.parse('filethatdoesntexist.xml') badET.iterparse('filethatdoesntexist.xml') a = badET.XMLParser() # safe tree = goodET.fromstring(xmlString) print(tree) goodET.parse('filethatdoesntexist.xml') goodET.iterparse('filethatdoesntexist.xml') a = goodET.XMLParser() bandit-1.4.0/examples/xml_expatbuilder.py000066400000000000000000000005301303375232700205200ustar00rootroot00000000000000import xml.dom.expatbuilder as bad import defusedxml.expatbuilder as good bad.parse('filethatdoesntexist.xml') good.parse('filethatdoesntexist.xml') xmlString = "\nTove\nJani\nReminder\nDon't forget me this weekend!\n" bad.parseString(xmlString) good.parseString(xmlString) bandit-1.4.0/examples/xml_expatreader.py000066400000000000000000000001721303375232700203360ustar00rootroot00000000000000import xml.sax.expatreader as bad import defusedxml.expatreader as good p = bad.create_parser() b = good.create_parser() bandit-1.4.0/examples/xml_lxml.py000066400000000000000000000005221303375232700170050ustar00rootroot00000000000000import lxml.etree import lxml from lxml import etree from defusedxml.lxml import fromstring from defuxedxml import lxml as potatoe xmlString = "\nTove\nJani\nReminder\nDon't forget me this weekend!\n" root = lxml.etree.fromstring(xmlString) root = fromstring(xmlString) bandit-1.4.0/examples/xml_minidom.py000066400000000000000000000007231303375232700174700ustar00rootroot00000000000000from xml.dom.minidom import parseString as badParseString from defusedxml.minidom import parseString as goodParseString a = badParseString("Some data some more data") print(a) b = goodParseString("Some data some more data") print(b) from xml.dom.minidom import parse as badParse from defusedxml.minidom import parse as goodParse a = badParse("somfilethatdoesntexist.xml") print(a) b = goodParse("somefilethatdoesntexist.xml") print(b) bandit-1.4.0/examples/xml_pulldom.py000066400000000000000000000007231303375232700175100ustar00rootroot00000000000000from xml.dom.pulldom import parseString as badParseString from defusedxml.pulldom import parseString as goodParseString a = badParseString("Some data some more data") print(a) b = goodParseString("Some data some more data") print(b) from xml.dom.pulldom import parse as badParse from defusedxml.pulldom import parse as goodParse a = badParse("somfilethatdoesntexist.xml") print(a) b = goodParse("somefilethatdoesntexist.xml") print(b) bandit-1.4.0/examples/xml_sax.py000066400000000000000000000020231303375232700166220ustar00rootroot00000000000000import xml.sax from xml import sax import defusedxml.sax class ExampleContentHandler(xml.sax.ContentHandler): def __init__(self): xml.sax.ContentHandler.__init__(self) def startElement(self, name, attrs): print('start:', name) def endElement(self, name): print('end:', name) def characters(self, content): print('chars:', content) def main(): xmlString = "\nTove\nJani\nReminder\nDon't forget me this weekend!\n" # bad xml.sax.parseString(xmlString, ExampleContentHandler()) xml.sax.parse('notaxmlfilethatexists.xml', ExampleContentHandler()) sax.parseString(xmlString, ExampleContentHandler()) sax.parse('notaxmlfilethatexists.xml', ExampleContentHandler) # good defusedxml.sax.parseString(xmlString, ExampleContentHandler()) # bad xml.sax.make_parser() sax.make_parser() print('nothing') # good defusedxml.sax.make_parser() if __name__ == "__main__": main() bandit-1.4.0/examples/xml_xmlrpc.py000066400000000000000000000004011303375232700173320ustar00rootroot00000000000000import xmlrpclib from SimpleXMLRPCServer import SimpleXMLRPCServer def is_even(n): return n%2 == 0 server = SimpleXMLRPCServer(("localhost", 8000)) print("Listening on port 8000...") server.register_function(is_even, "is_even") server.serve_forever() bandit-1.4.0/examples/yaml_load.py000066400000000000000000000004101303375232700171060ustar00rootroot00000000000000import json import yaml def test_yaml_load(): ystr = yaml.dump({'a' : 1, 'b' : 2, 'c' : 3}) y = yaml.load(ystr) yaml.dump(y) y = yaml.load(ystr, Loader=yaml.SafeLoader) def test_json_load(): # no issue should be found j = json.load("{}") bandit-1.4.0/pylintrc000066400000000000000000000042561303375232700145600ustar00rootroot00000000000000# The format of this file isn't really documented; just use --generate-rcfile [Messages Control] # C0111: Don't require docstrings on every method # C0301: Handled by pep8 # C0325: Parens are required on print in py3x # F0401: Imports are check by other linters # W0511: TODOs in code comments are fine. # W0142: *args and **kwargs are fine. # W0622: Redefining id is fine. # TODO(browne): fix these in the future # C0103: invalid-name # E1101: no-member # R0204: redefined-variable-type # R0902: too-many-instance-attributes # R0912: too-many-branches # R0913: too-many-arguments # R0914: too-many-locals # R0915: too-many-statements # W0110: deprecated-lambda # W0141: bad-builtin # W0201: attribute-defined-outside-init # W0212: protected-access # W0401: wildcard-import # W0603: global-statement # W0612: unused-variable # W0613: unused-argument # W0621: redefined-outer-name # W0703: broad-except disable=C0111,C0301,C0325,F0401,W0511,W0142,W0622,C0103,E1101,R0204,R0902,R0912,R0913,R0914,R0915,W0110,W0141,W0201,W0401,W0603,W0212,W0612,W0613,W0621,W0703 [Basic] # Variable names can be 1 to 31 characters long, with lowercase and underscores variable-rgx=[a-z_][a-z0-9_]{0,30}$ # Argument names can be 2 to 31 characters long, with lowercase and underscores argument-rgx=[a-z_][a-z0-9_]{1,30}$ # Method names should be at least 3 characters long # and be lowecased with underscores method-rgx=([a-z_][a-z0-9_]{2,50}|setUp|tearDown)$ # Module names matching manila-* are ok (files in bin/) module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+)|(manila-[a-z0-9_-]+))$ # Don't require docstrings on tests. no-docstring-rgx=((__.*__)|([tT]est.*)|setUp|tearDown)$ [Design] max-public-methods=100 min-public-methods=0 max-args=6 [Variables] # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. # _ is used by our localization additional-builtins=_ [Similarities] # Minimum lines number of a similarity. min-similarity-lines=10 # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes # Ignore imports when computing similarities. ignore-imports=yes bandit-1.4.0/releasenotes/000077500000000000000000000000001303375232700154535ustar00rootroot00000000000000bandit-1.4.0/releasenotes/notes/000077500000000000000000000000001303375232700166035ustar00rootroot00000000000000bandit-1.4.0/releasenotes/notes/add_reno-b8585fc3ffe775cb.yaml000066400000000000000000000000721303375232700236370ustar00rootroot00000000000000--- other: - Switch to reno for managing release notes. bandit-1.4.0/releasenotes/source/000077500000000000000000000000001303375232700167535ustar00rootroot00000000000000bandit-1.4.0/releasenotes/source/_static/000077500000000000000000000000001303375232700204015ustar00rootroot00000000000000bandit-1.4.0/releasenotes/source/_static/.placeholder000066400000000000000000000000001303375232700226520ustar00rootroot00000000000000bandit-1.4.0/releasenotes/source/_templates/000077500000000000000000000000001303375232700211105ustar00rootroot00000000000000bandit-1.4.0/releasenotes/source/_templates/.placeholder000066400000000000000000000000001303375232700233610ustar00rootroot00000000000000bandit-1.4.0/releasenotes/source/conf.py000066400000000000000000000215711303375232700202600ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'oslosphinx', 'reno.sphinxext', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Bandit Release Notes' copyright = u'2016, Bandit Developers' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. import pbr.version bandit_version = pbr.version.VersionInfo('bandit') # The full version, including alpha/beta/rc tags. release = bandit_version.version_string_with_vcs() # The short X.Y version. version = bandit_version.canonical_version_string() # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'banditReleaseNotesDoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'banditReleaseNotes.tex', u'Bandit Release Notes Documentation', u'Bandit Developers', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'banditReleaseNotes', u'Bandit Release Notes Documentation', [u'Bandit Developers'], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'banditReleaseNotes', u'Bandit Release Notes Documentation', u'Bandit Developers', 'banditReleaseNotes', 'Python source code security analyzer', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False # -- Options for Internationalization output ------------------------------ locale_dirs = ['locale/'] bandit-1.4.0/releasenotes/source/index.rst000066400000000000000000000001571303375232700206170ustar00rootroot00000000000000==================== Bandit Release Notes ==================== .. toctree:: :maxdepth: 1 unreleased bandit-1.4.0/releasenotes/source/unreleased.rst000066400000000000000000000001441303375232700216330ustar00rootroot00000000000000========================== Unreleased Release Notes ========================== .. release-notes:: bandit-1.4.0/requirements.txt000066400000000000000000000005011303375232700162420ustar00rootroot00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. GitPython>=1.0.1 # BSD License (3 clause) PyYAML>=3.10.0 # MIT six>=1.9.0 # MIT stevedore>=1.17.1 # Apache-2.0 bandit-1.4.0/scripts/000077500000000000000000000000001303375232700144515ustar00rootroot00000000000000bandit-1.4.0/scripts/integration-test.sh000066400000000000000000000024061303375232700203070ustar00rootroot00000000000000#!/bin/bash # Usage: integration-test.sh {organization} {project} {path-to-clone} # Example usage: # $ integration-test.sh openstack barbican # $ integration-test.sh openstack keystone # $ integration-test.sh openstack keystonemiddleware # $ integration-test.sh openstack sahara # $ integration-test.sh openstack python-keystoneclient \ # /opt/openstack/python-keystoneclient set -x set -e if [[ $# -lt 2 ]]; then echo "Script requires at least two arguments to run." echo "Usage: $0 organization project [path-to-clone]" exit 1 fi REPO_ROOT=${REPO_ROOT:-git://git.openstack.org} org=$1 project=$2 if [[ $# -eq 3 ]] ; then projectdir=$3 clone=0 else projectdir=$project clone=1 fi workdir="$(pwd)" if [[ $clone -eq 1 ]] ; then tempdir="$(mktemp -d)" trap "rm -rf $tempdir" EXIT pushd $tempdir git clone $REPO_ROOT/$org/$project --depth=1 fi pushd $projectdir # --notest allows us to create the tox-managed virtualenv without # running any tests. tox -e bandit --notest # We then install our local version of bandit into the virtualenv .tox/bandit/bin/pip install --force-reinstall -U $workdir # And now we actually run the tests tox -e bandit popd if [[ $clone -eq 1 ]] ; then popd fi bandit-1.4.0/scripts/main.py000066400000000000000000000014051303375232700157470ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding:utf-8 -*- # Copyright 2014 Hewlett-Packard Development Company, L.P. # #    Licensed under the Apache License, Version 2.0 (the "License"); you may #    not use this file except in compliance with the License. You may obtain #    a copy of the License at # #         http://www.apache.org/licenses/LICENSE-2.0 # #    Unless required by applicable law or agreed to in writing, software #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the #    License for the specific language governing permissions and limitations #    under the License. from bandit import bandit if __name__ == '__main__': bandit.main() bandit-1.4.0/setup.cfg000066400000000000000000000120421303375232700146020ustar00rootroot00000000000000[metadata] name = bandit summary = Security oriented static analyser for python code. description-file = README.rst author = OpenStack Security Group author-email = openstack-dev@lists.openstack.org home-page = https://wiki.openstack.org/wiki/Security/Projects/Bandit classifier = Environment :: OpenStack Intended Audience :: Information Technology Intended Audience :: System Administrators Intended Audience :: Developers License :: OSI Approved :: Apache Software License Operating System :: POSIX :: Linux Operating System :: MacOS :: MacOS X Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 Topic :: Security [entry_points] console_scripts = bandit = bandit.cli.main:main bandit-config-generator = bandit.cli.config_generator:main bandit-baseline = bandit.cli.baseline:main bandit.blacklists = calls = bandit.blacklists.calls:gen_blacklist imports = bandit.blacklists.imports:gen_blacklist bandit.formatters = csv = bandit.formatters.csv:report json = bandit.formatters.json:report txt = bandit.formatters.text:report xml = bandit.formatters.xml:report html = bandit.formatters.html:report screen = bandit.formatters.screen:report bandit.plugins = # bandit/plugins/app_debug.py flask_debug_true = bandit.plugins.app_debug:flask_debug_true # bandit/plugins/asserts.py assert_used = bandit.plugins.asserts:assert_used # bandit/plugins/crypto_request_no_cert_validation.py request_with_no_cert_validation = bandit.plugins.crypto_request_no_cert_validation:request_with_no_cert_validation # bandit/plugins/exec_as_root.py execute_with_run_as_root_equals_true = bandit.plugins.exec_as_root:execute_with_run_as_root_equals_true # bandit/plugins/exec.py exec_used = bandit.plugins.exec:exec_used # bandit/plugins/general_bad_File_permissions.py set_bad_file_permissions = bandit.plugins.general_bad_file_permissions:set_bad_file_permissions # bandit/plugins/general_bind_all_interfaces.py hardcoded_bind_all_interfaces = bandit.plugins.general_bind_all_interfaces:hardcoded_bind_all_interfaces # bandit/plugins/general_hardcoded_password.py hardcoded_password_string = bandit.plugins.general_hardcoded_password:hardcoded_password_string hardcoded_password_funcarg = bandit.plugins.general_hardcoded_password:hardcoded_password_funcarg hardcoded_password_default = bandit.plugins.general_hardcoded_password:hardcoded_password_default # bandit/plugins/general_hardcoded_tmp.py hardcoded_tmp_directory = bandit.plugins.general_hardcoded_tmp:hardcoded_tmp_directory # bandit/plugins/injection_paramiko.py paramiko_calls = bandit.plugins.injection_paramiko:paramiko_calls # bandit/plugins/injection_shell.py subprocess_popen_with_shell_equals_true = bandit.plugins.injection_shell:subprocess_popen_with_shell_equals_true subprocess_without_shell_equals_true = bandit.plugins.injection_shell:subprocess_without_shell_equals_true any_other_function_with_shell_equals_true = bandit.plugins.injection_shell:any_other_function_with_shell_equals_true start_process_with_a_shell = bandit.plugins.injection_shell:start_process_with_a_shell start_process_with_no_shell = bandit.plugins.injection_shell:start_process_with_no_shell start_process_with_partial_path = bandit.plugins.injection_shell:start_process_with_partial_path # bandit/plugins/injection_sql.py hardcoded_sql_expressions = bandit.plugins.injection_sql:hardcoded_sql_expressions # bandit/plugins/injection_wildcard.py linux_commands_wildcard_injection = bandit.plugins.injection_wildcard:linux_commands_wildcard_injection # bandit/plugins/insecure_ssl_tls.py ssl_with_bad_version = bandit.plugins.insecure_ssl_tls:ssl_with_bad_version ssl_with_bad_defaults = bandit.plugins.insecure_ssl_tls:ssl_with_bad_defaults ssl_with_no_version = bandit.plugins.insecure_ssl_tls:ssl_with_no_version # bandit/plugins/jinja2_templates.py jinja2_autoescape_false = bandit.plugins.jinja2_templates:jinja2_autoescape_false # bandit/plugins/mako_templates.py use_of_mako_templates = bandit.plugins.mako_templates:use_of_mako_templates # bandit/plugins/secret_config_options.py password_config_option_not_marked_secret = bandit.plugins.secret_config_option:password_config_option_not_marked_secret # bandit/plugins/try_except_continue.py try_except_continue = bandit.plugins.try_except_continue:try_except_continue # bandit/plugins/try_except_pass.py try_except_pass = bandit.plugins.try_except_pass:try_except_pass # bandit/plugins/weak_cryptographic_key.py weak_cryptographic_key = bandit.plugins.weak_cryptographic_key:weak_cryptographic_key # bandit/plugins/yaml_load.py yaml_load = bandit.plugins.yaml_load:yaml_load [build_sphinx] all_files = 1 build-dir = doc/build source-dir = doc/source [pbr] autodoc_tree_index_modules = True [bdist_wheel] universal = 1 bandit-1.4.0/setup.py000066400000000000000000000020041303375232700144700ustar00rootroot00000000000000# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools # In python < 2.7.4, a lazy loading of package `pbr` will break # setuptools if some other modules registered functions in `atexit`. # solution from: http://bugs.python.org/issue15881#msg170215 try: import multiprocessing # noqa except ImportError: pass setuptools.setup( setup_requires=['pbr>=1.8'], pbr=True) bandit-1.4.0/test-requirements.txt000066400000000000000000000011421303375232700172210ustar00rootroot00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. coverage>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD hacking<0.10,>=0.9.2 mock>=2.0 # BSD python-subunit>=0.0.18 # Apache-2.0/BSD testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT oslotest>=1.10.0 # Apache-2.0 sphinx!=1.3b1,<1.4,>=1.2.1 # BSD oslosphinx>=4.7.0 # Apache-2.0 beautifulsoup4 # MIT reno>=1.8.0 # Apache-2.0 pylint==1.4.5 # GPLv2 bandit-1.4.0/tests/000077500000000000000000000000001303375232700141245ustar00rootroot00000000000000bandit-1.4.0/tests/__init__.py000066400000000000000000000000001303375232700162230ustar00rootroot00000000000000bandit-1.4.0/tests/functional/000077500000000000000000000000001303375232700162665ustar00rootroot00000000000000bandit-1.4.0/tests/functional/__init__.py000066400000000000000000000000001303375232700203650ustar00rootroot00000000000000bandit-1.4.0/tests/functional/test_baseline.py000066400000000000000000000365721303375232700214760ustar00rootroot00000000000000# Copyright 2016 IBM Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import shutil import subprocess import fixtures import testtools new_candidates_all_total_lines = "Total lines of code: 12" new_candidates_some_total_lines = "Total lines of code: 9" new_candidates_no_nosec_lines = "Total lines skipped (#nosec): 0" new_candidates_skip_nosec_lines = "Total lines skipped (#nosec): 3" baseline_no_skipped_files = "Files skipped (0):" baseline_no_issues_found = "No issues identified." xml_sax_issue_id = "Issue: [B317:blacklist]" yaml_load_issue_id = "Issue: [B506:yaml_load]" shell_issue_id = "Issue: [B602:subprocess_popen_with_shell_equals_true]" candidate_example_one = "subprocess.Popen('/bin/ls *', shell=True)" candidate_example_two = "subprocess.Popen('/bin/ls *', shell=True) # nosec" candidate_example_three = "y = yaml.load(temp_str)" candidate_example_four = "y = yaml.load(temp_str) # nosec" candidate_example_five = "xml.sax.make_parser()" candidate_example_six = "xml.sax.make_parser() # nosec" class BaselineFunctionalTests(testtools.TestCase): '''Functional tests for Bandit baseline. This set of tests is used to verify that the baseline comparison handles finding and comparing results appropriately. The only comparison is the number of candidates per file, meaning that any candidates found may already exist in the baseline. In this case, all candidates are flagged and a user will need to investigate the candidates related to that file. ''' def setUp(self): super(BaselineFunctionalTests, self).setUp() self.examples_path = 'examples' self.baseline_commands = ['bandit', '-r'] self.baseline_report_file = "baseline_report.json" def _run_bandit_baseline(self, target_directory, baseline_file): '''A helper method to run bandit baseline This method will run the bandit baseline test provided an existing baseline report and the target directory containing the content to be tested. :param target_directory: Directory containing content to be compared :param baseline_file: File containing an existing baseline report :return The baseline test results and return code ''' cmds = self.baseline_commands + ['-b', baseline_file, target_directory] process = subprocess.Popen(cmds, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) stdout, stderr = process.communicate() return (stdout.decode('utf-8'), process.poll()) def _create_baseline(self, baseline_paired_files): '''A helper method to create a baseline to use during baseline test This method will run bandit to create an initial baseline that can then be used during the bandit baseline test. Since the file contents of the baseline report can be extremely dynamic and difficult to create ahead of time, we do this at runtime to reduce the risk of missing something. To do this, we must temporary replace the file contents with different code which will produce the proper baseline results to be used during the baseline test. :param baseline_paired_files A dictionary based set of files for which to create the baseline report with. For each key file, a value file is provided, which contains content to use in place of the key file when the baseline report is created initially. :return The target directory for the baseline test and the return code of the bandit run to help determine whether the baseline report was populated ''' target_directory = self.useFixture(fixtures.TempDir()).path baseline_results = os.path.join(target_directory, self.baseline_report_file) for key_file, value_file in baseline_paired_files.items(): shutil.copy(os.path.join(self.examples_path, value_file), os.path.join(target_directory, key_file)) cmds = self.baseline_commands + ['-f', 'json', '-o', baseline_results, target_directory] process = subprocess.Popen(cmds, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) stdout, stderr = process.communicate() return_code = process.poll() for key_file, value_file in baseline_paired_files.items(): shutil.copy(os.path.join(self.examples_path, key_file), os.path.join(target_directory, key_file)) return (target_directory, return_code) def test_no_new_candidates(self): '''Tests when there are no new candidates Test that bandit returns no issues found, as there are no new candidates found compared with those found from the baseline. ''' baseline_report_files = {"new_candidates-all.py": "new_candidates-all.py"} target_directory, baseline_code = (self._create_baseline( baseline_report_files)) # assert the initial baseline found results self.assertEqual(1, baseline_code) baseline_report = os.path.join(target_directory, self.baseline_report_file) return_value, return_code = (self._run_bandit_baseline( target_directory, baseline_report)) # assert there were no results (no candidates found) self.assertEqual(0, return_code) self.assertIn(new_candidates_all_total_lines, return_value) self.assertIn(new_candidates_skip_nosec_lines, return_value) self.assertIn(baseline_no_skipped_files, return_value) self.assertIn(baseline_no_issues_found, return_value) def test_no_existing_no_new_candidates(self): '''Tests when there are no new or existing candidates Test file with no existing candidates from baseline and no new candidates. ''' baseline_report_files = {"okay.py": "okay.py"} target_directory, baseline_code = (self._create_baseline( baseline_report_files)) # assert the initial baseline found nothing self.assertEqual(0, baseline_code) baseline_report = os.path.join(target_directory, self.baseline_report_file) return_value, return_code = (self._run_bandit_baseline( target_directory, baseline_report)) # assert there were no results (no candidates found) self.assertEqual(0, return_code) self.assertIn("Total lines of code: 1", return_value) self.assertIn(new_candidates_no_nosec_lines, return_value) self.assertIn(baseline_no_skipped_files, return_value) self.assertIn(baseline_no_issues_found, return_value) def test_no_existing_with_new_candidates(self): '''Tests when there are new candidates and no existing candidates Test that bandit returns issues found in file that had no existing candidates from baseline but now contain candidates. ''' baseline_report_files = {"new_candidates-all.py": "new_candidates-none.py"} target_directory, baseline_code = (self._create_baseline( baseline_report_files)) # assert the initial baseline found nothing self.assertEqual(0, baseline_code) baseline_report = os.path.join(target_directory, self.baseline_report_file) return_value, return_code = (self._run_bandit_baseline( target_directory, baseline_report)) # assert there were results (candidates found) self.assertEqual(1, return_code) self.assertIn(new_candidates_all_total_lines, return_value) self.assertIn(new_candidates_skip_nosec_lines, return_value) self.assertIn(baseline_no_skipped_files, return_value) self.assertIn(xml_sax_issue_id, return_value) self.assertIn(yaml_load_issue_id, return_value) self.assertIn(shell_issue_id, return_value) # candidate #1 self.assertIn(candidate_example_one, return_value) # candidate #3 self.assertIn(candidate_example_three, return_value) # candidate #5 self.assertIn(candidate_example_five, return_value) def test_existing_and_new_candidates(self): '''Tests when tere are new candidates and existing candidates Test that bandit returns issues found in file with existing candidates. The new candidates should be returned in this case. ''' baseline_report_files = {"new_candidates-all.py": "new_candidates-some.py"} target_directory, baseline_code = (self._create_baseline( baseline_report_files)) # assert the initial baseline found results self.assertEqual(1, baseline_code) baseline_report = os.path.join(target_directory, self.baseline_report_file) return_value, return_code = (self._run_bandit_baseline( target_directory, baseline_report)) # assert there were results (candidates found) self.assertEqual(1, return_code) self.assertIn(new_candidates_all_total_lines, return_value) self.assertIn(new_candidates_skip_nosec_lines, return_value) self.assertIn(baseline_no_skipped_files, return_value) self.assertIn(xml_sax_issue_id, return_value) self.assertIn(yaml_load_issue_id, return_value) # candidate #3 self.assertIn(candidate_example_three, return_value) # candidate #5 self.assertIn(candidate_example_five, return_value) def test_no_new_candidates_include_nosec(self): '''Test to check nosec references with no new candidates Test that nosec references are included during a baseline test, which would normally be ignored. In this test case, there are no new candidates even while including the nosec references. ''' self.baseline_commands.append('--ignore-nosec') baseline_report_files = {"new_candidates-all.py": "new_candidates-all.py"} target_directory, baseline_code = (self._create_baseline( baseline_report_files)) # assert the intial baseline found results self.assertEqual(1, baseline_code) baseline_report = os.path.join(target_directory, self.baseline_report_file) return_value, return_code = (self._run_bandit_baseline( target_directory, baseline_report)) # assert there were no results (candidates found) self.assertEqual(0, return_code) self.assertIn(new_candidates_all_total_lines, return_value) self.assertIn(new_candidates_no_nosec_lines, return_value) self.assertIn(baseline_no_skipped_files, return_value) self.assertIn(baseline_no_issues_found, return_value) def test_new_candidates_include_nosec_only_nosecs(self): '''Test to check nosec references with new only nosec candidates Test that nosec references are included during a baseline test, which would normally be ignored. In this test case, there are new candidates which are specifically nosec references. ''' self.baseline_commands.append('--ignore-nosec') baseline_report_files = {"new_candidates-nosec.py": "new_candidates-none.py"} target_directory, baseline_code = (self._create_baseline( baseline_report_files)) # assert the initial baseline found nothing self.assertEqual(0, baseline_code) baseline_report = os.path.join(target_directory, self.baseline_report_file) return_value, return_code = (self._run_bandit_baseline( target_directory, baseline_report)) # assert there were results (candidates found) self.assertEqual(1, return_code) self.assertIn(new_candidates_some_total_lines, return_value) self.assertIn(new_candidates_no_nosec_lines, return_value) self.assertIn(baseline_no_skipped_files, return_value) self.assertIn(xml_sax_issue_id, return_value) self.assertIn(yaml_load_issue_id, return_value) self.assertIn(shell_issue_id, return_value) # candidate #2 self.assertIn(candidate_example_two, return_value) # candidate #4 self.assertIn(candidate_example_four, return_value) # candidate #6 self.assertIn(candidate_example_six, return_value) def test_new_candidates_include_nosec_new_nosecs(self): '''Test to check nosec references with new candidates, including nosecs Test that nosec references are included during a baseline test, which would normally be ignored. In this test case, there are new candidates that also includes new nosec references as well. ''' self.baseline_commands.append('--ignore-nosec') baseline_report_files = {"new_candidates-all.py": "new_candidates-none.py"} target_directory, baseline_code = (self._create_baseline( baseline_report_files)) # assert the initial baseline found nothing self.assertEqual(0, baseline_code) baseline_report = os.path.join(target_directory, self.baseline_report_file) return_value, return_code = (self._run_bandit_baseline( target_directory, baseline_report)) # assert there were results (candidates found) self.assertEqual(1, return_code) self.assertIn(new_candidates_all_total_lines, return_value) self.assertIn(new_candidates_no_nosec_lines, return_value) self.assertIn(baseline_no_skipped_files, return_value) self.assertIn(xml_sax_issue_id, return_value) self.assertIn(yaml_load_issue_id, return_value) self.assertIn(shell_issue_id, return_value) # candidate #1 self.assertIn(candidate_example_one, return_value) # candidate #2 self.assertIn(candidate_example_two, return_value) # candidate #3 self.assertIn(candidate_example_three, return_value) # candidate #4 self.assertIn(candidate_example_four, return_value) # candidate #5 self.assertIn(candidate_example_five, return_value) # candidate #6 self.assertIn(candidate_example_six, return_value) bandit-1.4.0/tests/functional/test_functional.py000066400000000000000000000513011303375232700220410ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import six import testtools from bandit.core import config as b_config from bandit.core import constants as C from bandit.core import manager as b_manager from bandit.core import metrics from bandit.core import test_set as b_test_set class FunctionalTests(testtools.TestCase): '''Functional tests for bandit test plugins. This set of tests runs bandit against each example file in turn and records the score returned. This is compared to a known good value. When new tests are added to an example the expected result should be adjusted to match. ''' def setUp(self): super(FunctionalTests, self).setUp() # NOTE(tkelsey): bandit is very sensitive to paths, so stitch # them up here for the testing environment. # path = os.path.join(os.getcwd(), 'bandit', 'plugins') b_conf = b_config.BanditConfig() self.b_mgr = b_manager.BanditManager(b_conf, 'file') self.b_mgr.b_conf._settings['plugins_dir'] = path self.b_mgr.b_ts = b_test_set.BanditTestSet(config=b_conf) def run_example(self, example_script, ignore_nosec=False): '''A helper method to run the specified test This method runs the test, which populates the self.b_mgr.scores value. Call this directly if you need to run a test, but do not need to test the resulting scores against specified values. :param example_script: Filename of an example script to test ''' path = os.path.join(os.getcwd(), 'examples', example_script) self.b_mgr.ignore_nosec = ignore_nosec self.b_mgr.discover_files([path], True) self.b_mgr.run_tests() def check_example(self, example_script, expect, ignore_nosec=False): '''A helper method to test the scores for example scripts. :param example_script: Filename of an example script to test :param expect: dict with expected counts of issue types ''' # reset scores for subsequent calls to check_example self.b_mgr.scores = [] self.run_example(example_script, ignore_nosec=ignore_nosec) expected = 0 result = 0 for test_scores in self.b_mgr.scores: for score_type in test_scores: self.assertIn(score_type, expect) for rating in expect[score_type]: expected += ( expect[score_type][rating] * C.RANKING_VALUES[rating] ) result += sum(test_scores[score_type]) self.assertEqual(expected, result) def check_metrics(self, example_script, expect): '''A helper method to test the metrics being returned. :param example_script: Filename of an example script to test :param expect: dict with expected values of metrics ''' self.b_mgr.metrics = metrics.Metrics() self.b_mgr.scores = [] self.run_example(example_script) # test general metrics (excludes issue counts) m = self.b_mgr.metrics.data for k in expect: if k != 'issues': self.assertEqual(expect[k], m['_totals'][k]) # test issue counts if 'issues' in expect: for (criteria, default) in C.CRITERIA: for rank in C.RANKING: label = '{0}.{1}'.format(criteria, rank) expected = 0 if expect['issues'].get(criteria, None).get(rank, None): expected = expect['issues'][criteria][rank] self.assertEqual(expected, m['_totals'][label]) def test_binding(self): '''Test the bind-to-0.0.0.0 example.''' expect = {'SEVERITY': {'MEDIUM': 1}, 'CONFIDENCE': {'MEDIUM': 1}} self.check_example('binding.py', expect) def test_crypto_md5(self): '''Test the `hashlib.md5` example.''' expect = {'SEVERITY': {'MEDIUM': 8}, 'CONFIDENCE': {'HIGH': 8}} self.check_example('crypto-md5.py', expect) def test_ciphers(self): '''Test the `Crypto.Cipher` example.''' expect = {'SEVERITY': {'HIGH': 8}, 'CONFIDENCE': {'HIGH': 8}} self.check_example('ciphers.py', expect) def test_cipher_modes(self): '''Test for insecure cipher modes.''' expect = {'SEVERITY': {'MEDIUM': 1}, 'CONFIDENCE': {'HIGH': 1}} self.check_example('cipher-modes.py', expect) def test_eval(self): '''Test the `eval` example.''' expect = {'SEVERITY': {'MEDIUM': 3}, 'CONFIDENCE': {'HIGH': 3}} self.check_example('eval.py', expect) def test_mark_safe(self): '''Test the `mark_safe` example.''' expect = {'SEVERITY': {'MEDIUM': 1}, 'CONFIDENCE': {'HIGH': 1}} self.check_example('mark_safe.py', expect) def test_exec(self): '''Test the `exec` example.''' filename = 'exec-{}.py' if six.PY2: filename = filename.format('py2') expect = {'SEVERITY': {'MEDIUM': 2}, 'CONFIDENCE': {'HIGH': 2}} else: filename = filename.format('py3') expect = {'SEVERITY': {'MEDIUM': 1}, 'CONFIDENCE': {'HIGH': 1}} self.check_example(filename, expect) def test_exec_as_root(self): '''Test for the `run_as_root=True` keyword argument.''' expect = {'SEVERITY': {'LOW': 5}, 'CONFIDENCE': {'MEDIUM': 5}} self.check_example('exec-as-root.py', expect) def test_hardcoded_passwords(self): '''Test for hard-coded passwords.''' expect = {'SEVERITY': {'LOW': 7}, 'CONFIDENCE': {'MEDIUM': 7}} self.check_example('hardcoded-passwords.py', expect) def test_hardcoded_tmp(self): '''Test for hard-coded /tmp, /var/tmp, /dev/shm.''' expect = {'SEVERITY': {'MEDIUM': 3}, 'CONFIDENCE': {'MEDIUM': 3}} self.check_example('hardcoded-tmp.py', expect) def test_httplib_https(self): '''Test for `httplib.HTTPSConnection`.''' expect = {'SEVERITY': {'MEDIUM': 3}, 'CONFIDENCE': {'HIGH': 3}} self.check_example('httplib_https.py', expect) def test_imports_aliases(self): '''Test the `import X as Y` syntax.''' expect = { 'SEVERITY': {'LOW': 4, 'MEDIUM': 5, 'HIGH': 0}, 'CONFIDENCE': {'HIGH': 9} } self.check_example('imports-aliases.py', expect) def test_imports_from(self): '''Test the `from X import Y` syntax.''' expect = {'SEVERITY': {'LOW': 3}, 'CONFIDENCE': {'HIGH': 3}} self.check_example('imports-from.py', expect) def test_imports_function(self): '''Test the `__import__` function.''' expect = {'SEVERITY': {'LOW': 2}, 'CONFIDENCE': {'HIGH': 2}} self.check_example('imports-function.py', expect) def test_telnet_usage(self): '''Test for `import telnetlib` and Telnet.* calls.''' expect = {'SEVERITY': {'HIGH': 2}, 'CONFIDENCE': {'HIGH': 2}} self.check_example('telnetlib.py', expect) def test_ftp_usage(self): '''Test for `import ftplib` and FTP.* calls.''' expect = {'SEVERITY': {'HIGH': 2}, 'CONFIDENCE': {'HIGH': 2}} self.check_example('ftplib.py', expect) def test_imports(self): '''Test for dangerous imports.''' expect = {'SEVERITY': {'LOW': 2}, 'CONFIDENCE': {'HIGH': 2}} self.check_example('imports.py', expect) def test_mktemp(self): '''Test for `tempfile.mktemp`.''' expect = {'SEVERITY': {'MEDIUM': 4}, 'CONFIDENCE': {'HIGH': 4}} self.check_example('mktemp.py', expect) def test_nonsense(self): '''Test that a syntactically invalid module is skipped.''' self.run_example('nonsense.py') self.assertEqual(1, len(self.b_mgr.skipped)) def test_okay(self): '''Test a vulnerability-free file.''' expect = {'SEVERITY': {}, 'CONFIDENCE': {}} self.check_example('okay.py', expect) def test_os_chmod(self): '''Test setting file permissions.''' filename = 'os-chmod-{}.py' if six.PY2: filename = filename.format('py2') else: filename = filename.format('py3') expect = { 'SEVERITY': {'MEDIUM': 2, 'HIGH': 8}, 'CONFIDENCE': {'MEDIUM': 1, 'HIGH': 9} } self.check_example(filename, expect) def test_os_exec(self): '''Test for `os.exec*`.''' expect = {'SEVERITY': {'LOW': 8}, 'CONFIDENCE': {'MEDIUM': 8}} self.check_example('os-exec.py', expect) def test_os_popen(self): '''Test for `os.popen`.''' expect = {'SEVERITY': {'LOW': 8, 'MEDIUM': 0, 'HIGH': 1}, 'CONFIDENCE': {'HIGH': 9}} self.check_example('os-popen.py', expect) def test_os_spawn(self): '''Test for `os.spawn*`.''' expect = {'SEVERITY': {'LOW': 8}, 'CONFIDENCE': {'MEDIUM': 8}} self.check_example('os-spawn.py', expect) def test_os_startfile(self): '''Test for `os.startfile`.''' expect = {'SEVERITY': {'LOW': 3}, 'CONFIDENCE': {'MEDIUM': 3}} self.check_example('os-startfile.py', expect) def test_os_system(self): '''Test for `os.system`.''' expect = {'SEVERITY': {'LOW': 1}, 'CONFIDENCE': {'HIGH': 1}} self.check_example('os_system.py', expect) def test_pickle(self): '''Test for the `pickle` module.''' expect = { 'SEVERITY': {'LOW': 2, 'MEDIUM': 6}, 'CONFIDENCE': {'HIGH': 8} } self.check_example('pickle_deserialize.py', expect) def test_popen_wrappers(self): '''Test the `popen2` and `commands` modules.''' expect = {'SEVERITY': {'LOW': 7}, 'CONFIDENCE': {'HIGH': 7}} self.check_example('popen_wrappers.py', expect) def test_random_module(self): '''Test for the `random` module.''' expect = {'SEVERITY': {'LOW': 6}, 'CONFIDENCE': {'HIGH': 6}} self.check_example('random_module.py', expect) def test_requests_ssl_verify_disabled(self): '''Test for the `requests` library skipping verification.''' expect = {'SEVERITY': {'HIGH': 7}, 'CONFIDENCE': {'HIGH': 7}} self.check_example('requests-ssl-verify-disabled.py', expect) def test_skip(self): '''Test `#nosec` and `#noqa` comments.''' expect = {'SEVERITY': {'LOW': 5}, 'CONFIDENCE': {'HIGH': 5}} self.check_example('skip.py', expect) def test_ignore_skip(self): '''Test --ignore-nosec flag.''' expect = {'SEVERITY': {'LOW': 7}, 'CONFIDENCE': {'HIGH': 7}} self.check_example('skip.py', expect, ignore_nosec=True) def test_sql_statements(self): '''Test for SQL injection through string building.''' expect = { 'SEVERITY': {'MEDIUM': 11}, 'CONFIDENCE': {'LOW': 6, 'MEDIUM': 5}} self.check_example('sql_statements.py', expect) def test_ssl_insecure_version(self): '''Test for insecure SSL protocol versions.''' expect = { 'SEVERITY': {'LOW': 1, 'MEDIUM': 10, 'HIGH': 7}, 'CONFIDENCE': {'LOW': 0, 'MEDIUM': 11, 'HIGH': 7} } self.check_example('ssl-insecure-version.py', expect) def test_subprocess_shell(self): '''Test for `subprocess.Popen` with `shell=True`.''' expect = { 'SEVERITY': {'HIGH': 3, 'MEDIUM': 1, 'LOW': 14}, 'CONFIDENCE': {'HIGH': 17, 'LOW': 1} } self.check_example('subprocess_shell.py', expect) def test_urlopen(self): '''Test for dangerous URL opening.''' expect = {'SEVERITY': {'MEDIUM': 14}, 'CONFIDENCE': {'HIGH': 14}} self.check_example('urlopen.py', expect) def test_utils_shell(self): '''Test for `utils.execute*` with `shell=True`.''' expect = { 'SEVERITY': {'LOW': 5}, 'CONFIDENCE': {'HIGH': 5} } self.check_example('utils-shell.py', expect) def test_wildcard_injection(self): '''Test for wildcard injection in shell commands.''' expect = { 'SEVERITY': {'HIGH': 4, 'MEDIUM': 0, 'LOW': 10}, 'CONFIDENCE': {'MEDIUM': 5, 'HIGH': 9} } self.check_example('wildcard-injection.py', expect) def test_yaml(self): '''Test for `yaml.load`.''' expect = {'SEVERITY': {'MEDIUM': 1}, 'CONFIDENCE': {'HIGH': 1}} self.check_example('yaml_load.py', expect) def test_jinja2_templating(self): '''Test jinja templating for potential XSS bugs.''' expect = { 'SEVERITY': {'HIGH': 4}, 'CONFIDENCE': {'HIGH': 3, 'MEDIUM': 1} } self.check_example('jinja2_templating.py', expect) def test_secret_config_option(self): '''Test for `secret=True` in Oslo's config.''' expect = { 'SEVERITY': {'LOW': 1, 'MEDIUM': 2}, 'CONFIDENCE': {'MEDIUM': 3} } self.check_example('secret-config-option.py', expect) def test_mako_templating(self): '''Test Mako templates for XSS.''' expect = {'SEVERITY': {'MEDIUM': 3}, 'CONFIDENCE': {'HIGH': 3}} self.check_example('mako_templating.py', expect) def test_xml(self): '''Test xml vulnerabilities.''' expect = {'SEVERITY': {'LOW': 1, 'HIGH': 4}, 'CONFIDENCE': {'HIGH': 1, 'MEDIUM': 4}} self.check_example('xml_etree_celementtree.py', expect) expect = {'SEVERITY': {'LOW': 1, 'HIGH': 2}, 'CONFIDENCE': {'HIGH': 1, 'MEDIUM': 2}} self.check_example('xml_expatbuilder.py', expect) expect = {'SEVERITY': {'LOW': 3, 'HIGH': 1}, 'CONFIDENCE': {'HIGH': 3, 'MEDIUM': 1}} self.check_example('xml_lxml.py', expect) expect = {'SEVERITY': {'LOW': 2, 'HIGH': 2}, 'CONFIDENCE': {'HIGH': 2, 'MEDIUM': 2}} self.check_example('xml_pulldom.py', expect) expect = {'SEVERITY': {'HIGH': 1}, 'CONFIDENCE': {'HIGH': 1}} self.check_example('xml_xmlrpc.py', expect) expect = {'SEVERITY': {'LOW': 1, 'HIGH': 4}, 'CONFIDENCE': {'HIGH': 1, 'MEDIUM': 4}} self.check_example('xml_etree_elementtree.py', expect) expect = {'SEVERITY': {'LOW': 1, 'HIGH': 1}, 'CONFIDENCE': {'HIGH': 1, 'MEDIUM': 1}} self.check_example('xml_expatreader.py', expect) expect = {'SEVERITY': {'LOW': 2, 'HIGH': 2}, 'CONFIDENCE': {'HIGH': 2, 'MEDIUM': 2}} self.check_example('xml_minidom.py', expect) expect = {'SEVERITY': {'LOW': 2, 'HIGH': 6}, 'CONFIDENCE': {'HIGH': 2, 'MEDIUM': 6}} self.check_example('xml_sax.py', expect) def test_httpoxy(self): '''Test httpoxy vulnerability.''' expect = {'SEVERITY': {'HIGH': 1}, 'CONFIDENCE': {'HIGH': 1}} self.check_example('httpoxy_cgihandler.py', expect) self.check_example('httpoxy_twisted_script.py', expect) self.check_example('httpoxy_twisted_directory.py', expect) def test_asserts(self): '''Test catching the use of assert.''' expect = {'SEVERITY': {'LOW': 1}, 'CONFIDENCE': {'HIGH': 1}} self.check_example('assert.py', expect) def test_paramiko_injection(self): '''Test paramiko command execution.''' expect = {'SEVERITY': {'MEDIUM': 2}, 'CONFIDENCE': {'MEDIUM': 2}} self.check_example('paramiko_injection.py', expect) def test_partial_path(self): '''Test process spawning with partial file paths.''' expect = {'SEVERITY': {'LOW': 11}, 'CONFIDENCE': {'HIGH': 11}} self.check_example('partial_path_process.py', expect) def test_try_except_continue(self): '''Test try, except, continue detection.''' test = next((x for x in self.b_mgr.b_ts.tests['ExceptHandler'] if x.__name__ == 'try_except_continue')) test._config = {'check_typed_exception': True} expect = {'SEVERITY': {'LOW': 3}, 'CONFIDENCE': {'HIGH': 3}} self.check_example('try_except_continue.py', expect) test._config = {'check_typed_exception': False} expect = {'SEVERITY': {'LOW': 2}, 'CONFIDENCE': {'HIGH': 2}} self.check_example('try_except_continue.py', expect) def test_try_except_pass(self): '''Test try, except pass detection.''' test = next((x for x in self.b_mgr.b_ts.tests['ExceptHandler'] if x.__name__ == 'try_except_pass')) test._config = {'check_typed_exception': True} expect = {'SEVERITY': {'LOW': 3}, 'CONFIDENCE': {'HIGH': 3}} self.check_example('try_except_pass.py', expect) test._config = {'check_typed_exception': False} expect = {'SEVERITY': {'LOW': 2}, 'CONFIDENCE': {'HIGH': 2}} self.check_example('try_except_pass.py', expect) def test_metric_gathering(self): expect = { 'nosec': 2, 'loc': 7, 'issues': {'CONFIDENCE': {'HIGH': 5}, 'SEVERITY': {'LOW': 5}} } self.check_metrics('skip.py', expect) expect = { 'nosec': 0, 'loc': 4, 'issues': {'CONFIDENCE': {'HIGH': 2}, 'SEVERITY': {'LOW': 2}} } self.check_metrics('imports.py', expect) def test_weak_cryptographic_key(self): '''Test for weak key sizes.''' expect = { 'SEVERITY': {'MEDIUM': 6, 'HIGH': 4}, 'CONFIDENCE': {'HIGH': 10} } self.check_example('weak_cryptographic_key_sizes.py', expect) def test_multiline_code(self): '''Test issues in multiline statements return code as expected.''' self.run_example('multiline_statement.py') self.assertEqual(0, len(self.b_mgr.skipped)) self.assertEqual(1, len(self.b_mgr.files_list)) self.assertTrue(self.b_mgr.files_list[0].endswith( 'multiline_statement.py')) issues = self.b_mgr.get_issue_list() self.assertEqual(2, len(issues)) self.assertTrue( issues[0].fname.endswith('examples/multiline_statement.py') ) self.assertEqual(1, issues[0].lineno) self.assertEqual(list(range(1, 3)), issues[0].linerange) self.assertIn('subprocess', issues[0].get_code()) self.assertEqual(5, issues[1].lineno) self.assertEqual(list(range(3, 6 + 1)), issues[1].linerange) self.assertIn('shell=True', issues[1].get_code()) def test_code_line_numbers(self): self.run_example('binding.py') issues = self.b_mgr.get_issue_list() code_lines = issues[0].get_code().splitlines() lineno = issues[0].lineno self.assertEqual("%i " % (lineno - 1), code_lines[0][:2]) self.assertEqual("%i " % (lineno), code_lines[1][:2]) self.assertEqual("%i " % (lineno + 1), code_lines[2][:2]) def test_flask_debug_true(self): expect = { 'SEVERITY': {'HIGH': 1}, 'CONFIDENCE': {'MEDIUM': 1} } self.check_example('flask_debug.py', expect) def test_nosec(self): expect = { 'SEVERITY': {}, 'CONFIDENCE': {} } self.check_example('nosec.py', expect) def test_baseline_filter(self): issue_text = ('A Flask app appears to be run with debug=True, which ' 'exposes the Werkzeug debugger and allows the execution ' 'of arbitrary code.') json = """{ "results": [ { "code": "...", "filename": "%s/examples/flask_debug.py", "issue_confidence": "MEDIUM", "issue_severity": "HIGH", "issue_text": "%s", "line_number": 10, "line_range": [ 10 ], "test_name": "flask_debug_true", "test_id": "B201" } ] } """ % (os.getcwd(), issue_text) self.b_mgr.populate_baseline(json) self.run_example('flask_debug.py') self.assertEqual(1, len(self.b_mgr.baseline)) self.assertEqual({}, self.b_mgr.get_issue_list()) def test_blacklist_input(self): expect = { 'SEVERITY': {'HIGH': 1}, 'CONFIDENCE': {'HIGH': 1} } self.check_example('input.py', expect) bandit-1.4.0/tests/functional/test_runtime.py000066400000000000000000000122221303375232700213610ustar00rootroot00000000000000# Copyright (c) 2015 VMware, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import subprocess import six import testtools class RuntimeTests(testtools.TestCase): def _test_runtime(self, cmdlist, infile=None): process = subprocess.Popen( cmdlist, stdin=infile if infile else subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True ) stdout, stderr = process.communicate() retcode = process.poll() return (retcode, stdout.decode('utf-8')) def _test_example(self, cmdlist, targets): for t in targets: cmdlist.append(os.path.join(os.getcwd(), 'examples', t)) return self._test_runtime(cmdlist) def test_no_arguments(self): (retcode, output) = self._test_runtime(['bandit', ]) self.assertEqual(2, retcode) if six.PY2: self.assertIn("error: too few arguments", output) else: self.assertIn("arguments are required: targets", output) def test_piped_input(self): with open('examples/imports.py', 'r') as infile: (retcode, output) = self._test_runtime(['bandit', '-'], infile) self.assertEqual(1, retcode) self.assertIn("Total lines of code: 4", output) self.assertIn("Low: 2", output) self.assertIn("High: 2", output) self.assertIn("Files skipped (0):", output) self.assertIn("Issue: [B403:blacklist] Consider possible", output) self.assertIn(":2", output) self.assertIn(":4", output) def test_nonexistent_config(self): (retcode, output) = self._test_runtime([ 'bandit', '-c', 'nonexistent.yml', 'xx.py' ]) self.assertEqual(2, retcode) self.assertIn("nonexistent.yml : Could not read config file.", output) def test_help_arg(self): (retcode, output) = self._test_runtime(['bandit', '-h']) self.assertEqual(0, retcode) self.assertIn( "Bandit - a Python source code security analyzer", output ) self.assertIn("usage: bandit [-h]", output) self.assertIn("positional arguments:", output) self.assertIn("optional arguments:", output) self.assertIn("tests were discovered and loaded:", output) def test_help_in_readme(self): replace_list = [' ', '\t'] (retcode, output) = self._test_runtime(['bandit', '-h']) for i in replace_list: output = output.replace(i, '') output = output.replace("'", "\'") with open('README.rst') as f: readme = f.read() for i in replace_list: readme = readme.replace(i, '') self.assertIn(output, readme) # test examples (use _test_example() to wrap in config location argument def test_example_nonexistent(self): (retcode, output) = self._test_example( ['bandit', ], ['nonexistent.py', ] ) self.assertEqual(0, retcode) self.assertIn("Files skipped (1):", output) self.assertIn("nonexistent.py (No such file or directory", output) def test_example_okay(self): (retcode, output) = self._test_example(['bandit', ], ['okay.py', ]) self.assertEqual(0, retcode) self.assertIn("Total lines of code: 1", output) self.assertIn("Files skipped (0):", output) self.assertIn("No issues identified.", output) def test_example_nonsense(self): (retcode, output) = self._test_example(['bandit', ], ['nonsense.py', ]) self.assertEqual(0, retcode) self.assertIn("Files skipped (1):", output) self.assertIn("nonsense.py (syntax error while parsing AST", output) def test_example_nonsense2(self): (retcode, output) = self._test_example( ['bandit', ], ['nonsense2.py', ] ) self.assertEqual(0, retcode) self.assertIn( "Exception occurred when executing tests against", output ) self.assertIn("Files skipped (1):", output) self.assertIn("nonsense2.py (exception while scanning file)", output) def test_example_imports(self): (retcode, output) = self._test_example(['bandit', ], ['imports.py', ]) self.assertEqual(1, retcode) self.assertIn("Total lines of code: 4", output) self.assertIn("Low: 2", output) self.assertIn("High: 2", output) self.assertIn("Files skipped (0):", output) self.assertIn("Issue: [B403:blacklist] Consider possible", output) self.assertIn("imports.py:2", output) self.assertIn("imports.py:4", output) bandit-1.4.0/tests/unit/000077500000000000000000000000001303375232700151035ustar00rootroot00000000000000bandit-1.4.0/tests/unit/__init__.py000066400000000000000000000000001303375232700172020ustar00rootroot00000000000000bandit-1.4.0/tests/unit/cli/000077500000000000000000000000001303375232700156525ustar00rootroot00000000000000bandit-1.4.0/tests/unit/cli/__init__.py000066400000000000000000000000001303375232700177510ustar00rootroot00000000000000bandit-1.4.0/tests/unit/cli/test_baseline.py000066400000000000000000000252411303375232700210510ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2015 Hewlett-Packard Enterprise # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import subprocess import fixtures import git import mock import testtools import bandit.cli.baseline as baseline config = """ include: - '*.py' - '*.pyw' profiles: test: include: - start_process_with_a_shell shell_injection: subprocess: [] no_shell: [] shell: - os.system """ class BanditBaselineToolTests(testtools.TestCase): @classmethod def setUpClass(cls): # Set up prior to running test class # read in content used for temporary file contents with open('examples/mktemp.py') as fd: cls.temp_file_contents = fd.read() def setUp(self): # Set up prior to run each test case super(BanditBaselineToolTests, self).setUp() self.current_directory = os.getcwd() def tearDown(self): # Tear down after running each test case super(BanditBaselineToolTests, self).tearDown() os.chdir(self.current_directory) def test_bandit_baseline(self): # Tests running bandit via the CLI (baseline) with benign and malicious # content repo_directory = self.useFixture(fixtures.TempDir()).path # get benign and findings examples with open('examples/okay.py') as fd: benign_contents = fd.read() with open('examples/os_system.py') as fd: malicious_contents = fd.read() contents = {'benign_one.py': benign_contents, 'benign_two.py': benign_contents, 'malicious.py': malicious_contents} # init git repo, change directory to it git_repo = git.Repo.init(repo_directory) git_repo.index.commit('Initial commit') os.chdir(repo_directory) with open('bandit.yaml', 'wt') as fd: fd.write(config) # create three branches, first has only benign, second adds malicious, # third adds benign branches = [{'name': 'benign1', 'files': ['benign_one.py'], 'expected_return': 0}, {'name': 'malicious', 'files': ['benign_one.py', 'malicious.py'], 'expected_return': 1}, {'name': 'benign2', 'files': ['benign_one.py', 'malicious.py', 'benign_two.py'], 'expected_return': 0}] baseline_command = ['bandit-baseline', '-c', 'bandit.yaml', '-r', '.', '-p', 'test'] for branch in branches: branch['branch'] = git_repo.create_head(branch['name']) git_repo.head.reference = branch['branch'] git_repo.head.reset(working_tree=True) for f in branch['files']: with open(f, 'wt') as fd: fd.write(contents[f]) git_repo.index.add(branch['files']) git_repo.index.commit(branch['name']) self.assertEqual(branch['expected_return'], subprocess.call(baseline_command)) def test_main_non_repo(self): # Test that bandit gracefully exits when there is no git repository # when calling main repo_dir = self.useFixture(fixtures.TempDir()).path os.chdir(repo_dir) # assert the system exits with code 2 self.assertRaisesRegex(SystemExit, '2', baseline.main) def test_main_git_command_failure(self): # Test that bandit does not run when the Git command fails repo_directory = self.useFixture(fixtures.TempDir()).path git_repo = git.Repo.init(repo_directory) git_repo.index.commit('Initial Commit') os.chdir(repo_directory) additional_content = 'additional_file.py' with open(additional_content, 'wt') as fd: fd.write(self.temp_file_contents) git_repo.index.add([additional_content]) git_repo.index.commit('Additional Content') with mock.patch('git.Repo.commit') as mock_git_repo_commit: mock_git_repo_commit.side_effect = git.exc.GitCommandError( 'commit', '') # assert the system exits with code 2 self.assertRaisesRegex(SystemExit, '2', baseline.main) def test_main_no_parent_commit(self): # Test that bandit exits when there is no parent commit detected when # calling main repo_directory = self.useFixture(fixtures.TempDir()).path git_repo = git.Repo.init(repo_directory) git_repo.index.commit('Initial Commit') os.chdir(repo_directory) # assert the system exits with code 2 self.assertRaisesRegex(SystemExit, '2', baseline.main) def test_main_subprocess_error(self): # Test that bandit handles a CalledProcessError when attempting to run # bandit baseline via a subprocess repo_directory = self.useFixture(fixtures.TempDir()).path git_repo = git.Repo.init(repo_directory) git_repo.index.commit('Initial Commit') os.chdir(repo_directory) additional_content = 'additional_file.py' with open(additional_content, 'wt') as fd: fd.write(self.temp_file_contents) git_repo.index.add([additional_content]) git_repo.index.commit('Additional Content') with mock.patch('subprocess.check_output') as mock_check_output: mock_bandit_cmd = 'bandit_mock -b temp_file.txt' mock_check_output.side_effect = ( subprocess.CalledProcessError('3', mock_bandit_cmd) ) # assert the system exits with code 3 (returned from # CalledProcessError) self.assertRaisesRegex(SystemExit, '3', baseline.main) def test_init_logger(self): # Test whether the logger was initialized when calling init_logger baseline.init_logger() logger = baseline.LOG # verify that logger was initialized self.assertIsNotNone(logger) def test_initialize_no_repo(self): # Test that bandit does not run when there is no current git # repository when calling initialize repo_directory = self.useFixture(fixtures.TempDir()).path os.chdir(repo_directory) return_value = baseline.initialize() # assert bandit did not run due to no git repo self.assertEqual((None, None, None), return_value) def test_initialize_git_command_failure(self): # Test that bandit does not run when the Git command fails repo_directory = self.useFixture(fixtures.TempDir()).path git_repo = git.Repo.init(repo_directory) git_repo.index.commit('Initial Commit') os.chdir(repo_directory) additional_content = 'additional_file.py' with open(additional_content, 'wt') as fd: fd.write(self.temp_file_contents) git_repo.index.add([additional_content]) git_repo.index.commit('Additional Content') with mock.patch('git.Repo') as mock_git_repo: mock_git_repo.side_effect = git.exc.GitCommandNotFound('clone', '') return_value = baseline.initialize() # assert bandit did not run due to git command failure self.assertEqual((None, None, None), return_value) def test_initialize_dirty_repo(self): # Test that bandit does not run when the current git repository is # 'dirty' when calling the initialize method repo_directory = self.useFixture(fixtures.TempDir()).path git_repo = git.Repo.init(repo_directory) git_repo.index.commit('Initial Commit') os.chdir(repo_directory) # make the git repo 'dirty' with open('dirty_file.py', 'wt') as fd: fd.write(self.temp_file_contents) git_repo.index.add(['dirty_file.py']) return_value = baseline.initialize() # assert bandit did not run due to dirty repo self.assertEqual((None, None, None), return_value) @mock.patch('sys.argv', ['bandit', '-f', 'txt', 'test']) def test_initialize_existing_report_file(self): # Test that bandit does not run when the output file exists (and the # provided output format does not match the default format) when # calling the initialize method repo_directory = self.useFixture(fixtures.TempDir()).path git_repo = git.Repo.init(repo_directory) git_repo.index.commit('Initial Commit') os.chdir(repo_directory) # create an existing version of output report file existing_report = "{}.{}".format(baseline.report_basename, 'txt') with open(existing_report, 'wt') as fd: fd.write(self.temp_file_contents) return_value = baseline.initialize() # assert bandit did not run due to existing report file self.assertEqual((None, None, None), return_value) @mock.patch('bandit.cli.baseline.bandit_args', ['-o', 'bandit_baseline_result']) def test_initialize_with_output_argument(self): # Test that bandit does not run when the '-o' (output) argument is # specified repo_directory = self.useFixture(fixtures.TempDir()).path git_repo = git.Repo.init(repo_directory) git_repo.index.commit('Initial Commit') os.chdir(repo_directory) return_value = baseline.initialize() # assert bandit did not run due to provided -o (--ouput) argument self.assertEqual((None, None, None), return_value) def test_initialize_existing_temp_file(self): # Test that bandit does not run when the temporary output file exists # when calling the initialize method repo_directory = self.useFixture(fixtures.TempDir()).path git_repo = git.Repo.init(repo_directory) git_repo.index.commit('Initial Commit') os.chdir(repo_directory) # create an existing version of temporary output file existing_temp_file = baseline.baseline_tmp_file with open(existing_temp_file, 'wt') as fd: fd.write(self.temp_file_contents) return_value = baseline.initialize() # assert bandit did not run due to existing temporary report file self.assertEqual((None, None, None), return_value) bandit-1.4.0/tests/unit/cli/test_config_generator.py000066400000000000000000000072621303375232700226050ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2016 Hewlett-Packard Enterprise # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import importlib import logging import mock import testtools import yaml from bandit.cli import config_generator from bandit.core import extension_loader from bandit.core import test_properties as test def gen_config(name): return {"test": "test data"} @test.takes_config('test') @test.checks('Str') def _test_plugin(context, conf): pass class BanditConfigGeneratorLoggerTests(testtools.TestCase): def setUp(self): super(BanditConfigGeneratorLoggerTests, self).setUp() self.logger = logging.getLogger(config_generator.__name__) self.original_logger_handlers = self.logger.handlers self.original_logger_level = self.logger.level self.logger.handlers = [] def tearDown(self): super(BanditConfigGeneratorLoggerTests, self).tearDown() self.logger.handlers = self.original_logger_handlers self.logger.level = self.original_logger_level def test_init_logger(self): # Test that a logger was properly initialized config_generator.init_logger() self.assertIsNotNone(self.logger) self.assertNotEqual([], self.logger.handlers) self.assertEqual(logging.INFO, self.logger.level) class BanditConfigGeneratorTests(testtools.TestCase): @mock.patch('sys.argv', ['bandit-config-generator']) def test_parse_args_no_defaults(self): # Without arguments, the generator should just show help and exit self.assertRaises(SystemExit, config_generator.parse_args) @mock.patch('sys.argv', ['bandit-config-generator', '--show-defaults']) def test_parse_args_show_defaults(self): # Test that the config generator does show default plugin settings return_value = config_generator.parse_args() self.assertTrue(return_value.show_defaults) @mock.patch('sys.argv', ['bandit-config-generator', '--out', 'dummyfile']) def test_parse_args_out_file(self): # Test config generator get proper output file when specified return_value = config_generator.parse_args() self.assertEqual('dummyfile', return_value.output_file) def test_get_config_settings(self): config = {} for plugin in extension_loader.MANAGER.plugins: function = plugin.plugin if hasattr(plugin.plugin, '_takes_config'): module = importlib.import_module(function.__module__) config[plugin.name] = module.gen_config( function._takes_config) settings = config_generator.get_config_settings() self.assertEqual(yaml.safe_dump(config), settings) @mock.patch('sys.argv', ['bandit-config-generator', '--show-defaults']) def test_main_show_defaults(self): # Test that the config generator does show defaults and returns 0 with mock.patch('bandit.cli.config_generator.get_config_settings' ) as mock_config_settings: return_value = config_generator.main() # The get_config_settings function should have been called self.assertTrue(mock_config_settings.called) self.assertEqual(0, return_value) bandit-1.4.0/tests/unit/cli/test_main.py000066400000000000000000000277651303375232700202300ustar00rootroot00000000000000# Copyright 2016 IBM Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import logging import os import fixtures import mock import testtools from bandit.cli import main as bandit from bandit.core import extension_loader as ext_loader from bandit.core import utils bandit_config_content = """ include: - '*.py' - '*.pyw' profiles: test: include: - start_process_with_a_shell shell_injection: subprocess: shell: - os.system """ bandit_baseline_content = """{ "results": [ { "code": "some test code", "filename": "test_example.py", "issue_severity": "low", "issue_confidence": "low", "issue_text": "test_issue", "test_name": "some_test", "test_id": "x", "line_number": "n", "line_range": "n-m" } ] } """ class BanditCLIMainLoggerTests(testtools.TestCase): def setUp(self): super(BanditCLIMainLoggerTests, self).setUp() self.logger = logging.getLogger() self.original_logger_handlers = self.logger.handlers self.original_logger_level = self.logger.level self.logger.handlers = [] def tearDown(self): super(BanditCLIMainLoggerTests, self).tearDown() self.logger.handlers = self.original_logger_handlers self.logger.level = self.original_logger_level def test_init_logger(self): # Test that a logger was properly initialized bandit._init_logger(False) self.assertIsNotNone(self.logger) self.assertNotEqual(self.logger.handlers, []) self.assertEqual(logging.INFO, self.logger.level) def test_init_logger_debug_mode(self): # Test that the logger's level was set at 'DEBUG' bandit._init_logger(True) self.assertEqual(logging.DEBUG, self.logger.level) class BanditCLIMainTests(testtools.TestCase): def setUp(self): super(BanditCLIMainTests, self).setUp() self.current_directory = os.getcwd() def tearDown(self): super(BanditCLIMainTests, self).tearDown() os.chdir(self.current_directory) def test_get_options_from_ini_no_ini_path_no_target(self): # Test that no config options are loaded when no ini path or target # directory are provided self.assertIsNone(bandit._get_options_from_ini(None, [])) def test_get_options_from_ini_empty_directory_no_target(self): # Test that no config options are loaded when an empty directory is # provided as the ini path and no target directory is provided ini_directory = self.useFixture(fixtures.TempDir()).path self.assertIsNone(bandit._get_options_from_ini(ini_directory, [])) def test_get_options_from_ini_no_ini_path_no_bandit_files(self): # Test that no config options are loaded when no ini path is provided # and the target directory contains no bandit config files (.bandit) target_directory = self.useFixture(fixtures.TempDir()).path self.assertIsNone(bandit._get_options_from_ini(None, [target_directory])) def test_get_options_from_ini_no_ini_path_multi_bandit_files(self): # Test that bandit exits when no ini path is provided and the target # directory(s) contain multiple bandit config files (.bandit) target_directory = self.useFixture(fixtures.TempDir()).path second_config = 'second_config_directory' os.mkdir(os.path.join(target_directory, second_config)) bandit_config_one = os.path.join(target_directory, '.bandit') bandit_config_two = os.path.join(target_directory, second_config, '.bandit') bandit_files = [bandit_config_one, bandit_config_two] for bandit_file in bandit_files: with open(bandit_file, 'wt') as fd: fd.write(bandit_config_content) self.assertRaisesRegex(SystemExit, '2', bandit._get_options_from_ini, None, [target_directory]) def test_init_extensions(self): # Test that an extension loader manager is returned self.assertEqual(ext_loader.MANAGER, bandit._init_extensions()) def test_log_option_source_arg_val(self): # Test that the command argument value is returned when provided arg_val = 'file' ini_val = 'vuln' option_name = 'aggregate' self.assertEqual(arg_val, bandit._log_option_source(arg_val, ini_val, option_name)) def test_log_option_source_ini_value(self): # Test that the ini value is returned when no command argument is # provided ini_val = 'vuln' option_name = 'aggregate' self.assertEqual(ini_val, bandit._log_option_source(None, ini_val, option_name)) def test_log_option_source_no_values(self): # Test that None is returned when no command arguement or ini value are # provided option_name = 'aggregate' self.assertIsNone(bandit._log_option_source(None, None, option_name)) @mock.patch('sys.argv', ['bandit', '-c', 'bandit.yaml', 'test']) def test_main_config_unopenable(self): # Test that bandit exits when a config file cannot be opened with mock.patch('bandit.core.config.__init__') as mock_bandit_config: mock_bandit_config.side_effect = utils.ConfigError('', '') # assert a SystemExit with code 2 self.assertRaisesRegex(SystemExit, '2', bandit.main) @mock.patch('sys.argv', ['bandit', '-c', 'bandit.yaml', 'test']) def test_main_invalid_config(self): # Test that bandit exits when a config file contains invalid YAML # content with mock.patch('bandit.core.config.BanditConfig.__init__' ) as mock_bandit_config: mock_bandit_config.side_effect = utils.ConfigError('', '') # assert a SystemExit with code 2 self.assertRaisesRegex(SystemExit, '2', bandit.main) @mock.patch('sys.argv', ['bandit', '-c', 'bandit.yaml', 'test']) def test_main_handle_ini_options(self): # Test that bandit handles cmdline args from a bandit.yaml file temp_directory = self.useFixture(fixtures.TempDir()).path os.chdir(temp_directory) with open('bandit.yaml', 'wt') as fd: fd.write(bandit_config_content) with mock.patch('bandit.cli.main._get_options_from_ini' ) as mock_get_opts: mock_get_opts.return_value = {"exclude": "/tmp", "skips": "skip_test", "tests": "some_test"} with mock.patch('bandit.cli.main.LOG.error') as err_mock: # SystemExit with code 2 when test not found in profile self.assertRaisesRegex(SystemExit, '2', bandit.main) self.assertEqual(str(err_mock.call_args[0][0]), 'Unknown test found in profile: some_test') @mock.patch('sys.argv', ['bandit', '-c', 'bandit.yaml', '-t', 'badID', 'test']) def test_main_unknown_tests(self): # Test that bandit exits when an invalid test ID is provided temp_directory = self.useFixture(fixtures.TempDir()).path os.chdir(temp_directory) with open('bandit.yaml', 'wt') as fd: fd.write(bandit_config_content) # assert a SystemExit with code 2 self.assertRaisesRegex(SystemExit, '2', bandit.main) @mock.patch('sys.argv', ['bandit', '-c', 'bandit.yaml', '-s', 'badID', 'test']) def test_main_unknown_skip_tests(self): # Test that bandit exits when an invalid test ID is provided to skip temp_directory = self.useFixture(fixtures.TempDir()).path os.chdir(temp_directory) with open('bandit.yaml', 'wt') as fd: fd.write(bandit_config_content) # assert a SystemExit with code 2 self.assertRaisesRegex(SystemExit, '2', bandit.main) @mock.patch('sys.argv', ['bandit', '-c', 'bandit.yaml', '-p', 'bad', 'test']) def test_main_profile_not_found(self): # Test that bandit exits when an invalid profile name is provided temp_directory = self.useFixture(fixtures.TempDir()).path os.chdir(temp_directory) with open('bandit.yaml', 'wt') as fd: fd.write(bandit_config_content) # assert a SystemExit with code 2 with mock.patch('bandit.cli.main.LOG.error') as err_mock: self.assertRaisesRegex(SystemExit, '2', bandit.main) self.assertEqual( str(err_mock.call_args[0][0]), 'Unable to find profile (bad) in config file: bandit.yaml') @mock.patch('sys.argv', ['bandit', '-c', 'bandit.yaml', '-b', 'base.json', 'test']) def test_main_baseline_ioerror(self): # Test that bandit exits when encountering an IOError while reading # baseline data temp_directory = self.useFixture(fixtures.TempDir()).path os.chdir(temp_directory) with open('bandit.yaml', 'wt') as fd: fd.write(bandit_config_content) with open('base.json', 'wt') as fd: fd.write(bandit_baseline_content) with mock.patch('bandit.core.manager.BanditManager.populate_baseline' ) as mock_mgr_pop_bl: mock_mgr_pop_bl.side_effect = IOError # assert a SystemExit with code 2 self.assertRaisesRegex(SystemExit, '2', bandit.main) @mock.patch('sys.argv', ['bandit', '-c', 'bandit.yaml', '-b', 'base.json', '-f', 'csv', 'test']) def test_main_invalid_output_format(self): # Test that bandit exits when an invalid output format is selected temp_directory = self.useFixture(fixtures.TempDir()).path os.chdir(temp_directory) with open('bandit.yaml', 'wt') as fd: fd.write(bandit_config_content) with open('base.json', 'wt') as fd: fd.write(bandit_baseline_content) # assert a SystemExit with code 2 self.assertRaisesRegex(SystemExit, '2', bandit.main) @mock.patch('sys.argv', ['bandit', '-c', 'bandit.yaml', 'test', '-o', 'output']) def test_main_exit_with_results(self): # Test that bandit exits when there are results temp_directory = self.useFixture(fixtures.TempDir()).path os.chdir(temp_directory) with open('bandit.yaml', 'wt') as fd: fd.write(bandit_config_content) with mock.patch('bandit.core.manager.BanditManager.results_count' ) as mock_mgr_results_ct: mock_mgr_results_ct.return_value = 1 # assert a SystemExit with code 1 self.assertRaisesRegex(SystemExit, '1', bandit.main) @mock.patch('sys.argv', ['bandit', '-c', 'bandit.yaml', 'test', '-o', 'output']) def test_main_exit_with_no_results(self): # Test that bandit exits when there are no results temp_directory = self.useFixture(fixtures.TempDir()).path os.chdir(temp_directory) with open('bandit.yaml', 'wt') as fd: fd.write(bandit_config_content) with mock.patch('bandit.core.manager.BanditManager.results_count' ) as mock_mgr_results_ct: mock_mgr_results_ct.return_value = 0 # assert a SystemExit with code 0 self.assertRaisesRegex(SystemExit, '0', bandit.main) bandit-1.4.0/tests/unit/core/000077500000000000000000000000001303375232700160335ustar00rootroot00000000000000bandit-1.4.0/tests/unit/core/__init__.py000066400000000000000000000000001303375232700201320ustar00rootroot00000000000000bandit-1.4.0/tests/unit/core/test_blacklisting.py000066400000000000000000000033131303375232700221120ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2016 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from bandit.core import blacklisting import testtools class BlacklistingTests(testtools.TestCase): def test_report_issue(self): data = {'level': 'HIGH', 'message': 'test {name}', 'id': 'B000'} issue = blacklisting.report_issue(data, 'name') issue_dict = issue.as_dict(with_code=False) self.assertIsInstance(issue_dict, dict) self.assertEqual('B000', issue_dict['test_id']) self.assertEqual('HIGH', issue_dict['issue_severity']) self.assertEqual('HIGH', issue_dict['issue_confidence']) self.assertEqual('test name', issue_dict['issue_text']) def test_report_issue_defaults(self): data = {'message': 'test {name}'} issue = blacklisting.report_issue(data, 'name') issue_dict = issue.as_dict(with_code=False) self.assertIsInstance(issue_dict, dict) self.assertEqual('LEGACY', issue_dict['test_id']) self.assertEqual('MEDIUM', issue_dict['issue_severity']) self.assertEqual('HIGH', issue_dict['issue_confidence']) self.assertEqual('test name', issue_dict['issue_text']) bandit-1.4.0/tests/unit/core/test_config.py000066400000000000000000000213261303375232700207150ustar00rootroot00000000000000# Copyright 2015 IBM Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import tempfile import textwrap import uuid import fixtures import mock import testtools from bandit.core import config from bandit.core import utils class TempFile(fixtures.Fixture): def __init__(self, contents=None): super(TempFile, self).__init__() self.contents = contents def setUp(self): super(TempFile, self).setUp() with tempfile.NamedTemporaryFile(mode='wt', delete=False) as f: if self.contents: f.write(self.contents) self.addCleanup(os.unlink, f.name) self.name = f.name class TestInit(testtools.TestCase): def test_settings(self): # Can initialize a BanditConfig. example_key = uuid.uuid4().hex example_value = self.getUniqueString() contents = '%s: %s' % (example_key, example_value) f = self.useFixture(TempFile(contents)) b_config = config.BanditConfig(f.name) # After initialization, can get settings. self.assertEqual('*.py', b_config.get_setting('plugin_name_pattern')) self.assertEqual({example_key: example_value}, b_config.config) self.assertEqual(example_value, b_config.get_option(example_key)) def test_file_does_not_exist(self): # When the config file doesn't exist, ConfigFileUnopenable is raised. cfg_file = os.path.join(os.getcwd(), 'notafile') self.assertRaisesRegex(utils.ConfigError, cfg_file, config.BanditConfig, cfg_file) def test_yaml_invalid(self): # When the config yaml file isn't valid, sys.exit(2) is called. # The following is invalid because it starts a sequence and doesn't # end it. invalid_yaml = '- [ something' f = self.useFixture(TempFile(invalid_yaml)) self.assertRaisesRegex( utils.ConfigError, f.name, config.BanditConfig, f.name) class TestGetOption(testtools.TestCase): def setUp(self): super(TestGetOption, self).setUp() self.example_key = uuid.uuid4().hex self.example_subkey = uuid.uuid4().hex self.example_subvalue = uuid.uuid4().hex sample_yaml = textwrap.dedent(""" %s: %s: %s """ % (self.example_key, self.example_subkey, self.example_subvalue)) f = self.useFixture(TempFile(sample_yaml)) self.b_config = config.BanditConfig(f.name) def test_levels(self): # get_option with .-separated string. sample_option_name = '%s.%s' % (self.example_key, self.example_subkey) self.assertEqual(self.example_subvalue, self.b_config.get_option(sample_option_name)) def test_levels_not_exist(self): # get_option when option name doesn't exist returns None. sample_option_name = '%s.%s' % (uuid.uuid4().hex, uuid.uuid4().hex) self.assertIsNone(self.b_config.get_option(sample_option_name)) class TestGetSetting(testtools.TestCase): def setUp(self): super(TestGetSetting, self).setUp() test_yaml = 'key: value' f = self.useFixture(TempFile(test_yaml)) self.b_config = config.BanditConfig(f.name) def test_not_exist(self): # get_setting() when the name doesn't exist returns None sample_setting_name = uuid.uuid4().hex self.assertIsNone(self.b_config.get_setting(sample_setting_name)) class TestConfigCompat(testtools.TestCase): sample_yaml = textwrap.dedent(""" profiles: test_1: include: - any_other_function_with_shell_equals_true - assert_used exclude: test_2: include: - blacklist_calls test_3: include: - blacklist_imports test_4: exclude: - assert_used test_5: exclude: - blacklist_calls - blacklist_imports test_6: include: - blacklist_calls exclude: - blacklist_imports blacklist_calls: bad_name_sets: - pickle: qualnames: [pickle.loads] message: "{func} library appears to be in use." blacklist_imports: bad_import_sets: - telnet: imports: [telnetlib] level: HIGH message: "{module} is considered insecure." """) def setUp(self): super(TestConfigCompat, self).setUp() f = self.useFixture(TempFile(self.sample_yaml)) self.config = config.BanditConfig(f.name) def test_converted_include(self): profiles = self.config.get_option('profiles') test = profiles['test_1'] data = {'blacklist': {}, 'exclude': set(), 'include': set(['B101', 'B604'])} self.assertEqual(data, test) def test_converted_exclude(self): profiles = self.config.get_option('profiles') test = profiles['test_4'] self.assertEqual(set(['B101']), test['exclude']) def test_converted_blacklist_call_data(self): profiles = self.config.get_option('profiles') test = profiles['test_2'] data = {'Call': [{'qualnames': ['telnetlib'], 'level': 'HIGH', 'message': '{name} is considered insecure.', 'name': 'telnet'}]} self.assertEqual(data, test['blacklist']) def test_converted_blacklist_import_data(self): profiles = self.config.get_option('profiles') test = profiles['test_3'] data = [{'message': '{name} library appears to be in use.', 'name': 'pickle', 'qualnames': ['pickle.loads']}] self.assertEqual(data, test['blacklist']['Call']) self.assertEqual(data, test['blacklist']['Import']) self.assertEqual(data, test['blacklist']['ImportFrom']) def test_converted_blacklist_call_test(self): profiles = self.config.get_option('profiles') test = profiles['test_2'] self.assertEqual(set(['B001']), test['include']) def test_converted_blacklist_import_test(self): profiles = self.config.get_option('profiles') test = profiles['test_3'] self.assertEqual(set(['B001']), test['include']) def test_converted_exclude_blacklist(self): profiles = self.config.get_option('profiles') test = profiles['test_5'] self.assertEqual(set(['B001']), test['exclude']) def test_deprecation_message(self): msg = ("Config file '%s' contains deprecated legacy config data. " "Please consider upgrading to the new config format. The tool " "'bandit-config-generator' can help you with this. Support for " "legacy configs will be removed in a future bandit version.") with mock.patch('bandit.core.config.LOG.warning') as m: self.config._config = {"profiles": {}} self.config.validate('') self.assertEqual((msg, ''), m.call_args_list[0][0]) def test_blacklist_error(self): msg = (" : Config file has an include or exclude reference to legacy " "test '%s' but no configuration data for it. Configuration " "data is required for this test. Please consider switching to " "the new config file format, the tool " "'bandit-config-generator' can help you with this.") for name in ["blacklist_call", "blacklist_imports", "blacklist_imports_func"]: self.config._config = ( {"profiles": {"test": {"include": [name]}}}) try: self.config.validate('') except utils.ConfigError as e: self.assertEqual(msg % name, e.message) def test_bad_yaml(self): f = self.useFixture(TempFile("[]")) try: self.config = config.BanditConfig(f.name) except utils.ConfigError as e: self.assertIn("Error parsing file.", e.message) bandit-1.4.0/tests/unit/core/test_context.py000066400000000000000000000253101303375232700211310ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2015 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import ast import mock import six import testtools from bandit.core import context class ContextTests(testtools.TestCase): def test_context_create(self): ref_context = mock.Mock() new_context = context.Context(context_object=ref_context) self.assertEqual(ref_context, new_context._context) new_context = context.Context() self.assertIsInstance(new_context._context, dict) def test_repr(self): ref_object = dict(spam='eggs') expected_repr = ''.format(ref_object) new_context = context.Context(context_object=ref_object) self.assertEqual(expected_repr, repr(new_context)) @mock.patch('bandit.core.context.Context._get_literal_value') def test_call_args(self, get_literal_value): get_literal_value.return_value = 'eggs' ref_call = mock.Mock() ref_call.args = [mock.Mock(attr='spam'), 'eggs'] ref_context = dict(call=ref_call) new_context = context.Context(context_object=ref_context) expected_args = ['spam', 'eggs'] self.assertListEqual(expected_args, new_context.call_args) def test_call_args_count(self): ref_call = mock.Mock() ref_call.args = ['spam', 'eggs'] ref_context = dict(call=ref_call) new_context = context.Context(context_object=ref_context) self.assertEqual(len(ref_call.args), new_context.call_args_count) ref_context = dict(call={}) new_context = context.Context(context_object=ref_context) self.assertIsNone(new_context.call_args_count) new_context = context.Context() self.assertIsNone(new_context.call_args_count) def test_call_function_name(self): expected_string = 'spam' ref_context = dict(name=expected_string) new_context = context.Context(context_object=ref_context) self.assertEqual(expected_string, new_context.call_function_name) new_context = context.Context() self.assertIsNone(new_context.call_function_name) def test_call_function_name_qual(self): expected_string = 'spam' ref_context = dict(qualname=expected_string) new_context = context.Context(context_object=ref_context) self.assertEqual(expected_string, new_context.call_function_name_qual) new_context = context.Context() self.assertIsNone(new_context.call_function_name_qual) @mock.patch('bandit.core.context.Context._get_literal_value') def test_call_keywords(self, get_literal_value): get_literal_value.return_value = 'eggs' ref_keyword1 = mock.Mock(arg='arg1', value=mock.Mock(attr='spam')) ref_keyword2 = mock.Mock(arg='arg2', value='eggs') ref_call = mock.Mock() ref_call.keywords = [ref_keyword1, ref_keyword2] ref_context = dict(call=ref_call) new_context = context.Context(context_object=ref_context) expected_dict = dict(arg1='spam', arg2='eggs') self.assertDictEqual(expected_dict, new_context.call_keywords) ref_context = dict(call=None) new_context = context.Context(context_object=ref_context) self.assertIsNone(new_context.call_keywords) new_context = context.Context() self.assertIsNone(new_context.call_keywords) def test_node(self): expected_node = 'spam' ref_context = dict(node=expected_node) new_context = context.Context(context_object=ref_context) self.assertEqual(expected_node, new_context.node) new_context = context.Context() self.assertIsNone(new_context.node) def test_string_val(self): expected_string = 'spam' ref_context = dict(str=expected_string) new_context = context.Context(context_object=ref_context) self.assertEqual(expected_string, new_context.string_val) new_context = context.Context() self.assertIsNone(new_context.string_val) def test_statement(self): expected_string = 'spam' ref_context = dict(statement=expected_string) new_context = context.Context(context_object=ref_context) self.assertEqual(expected_string, new_context.statement) new_context = context.Context() self.assertIsNone(new_context.statement) @mock.patch('bandit.core.utils.get_qual_attr') def test_function_def_defaults_qual(self, get_qual_attr): get_qual_attr.return_value = 'spam' ref_node = mock.Mock(args=mock.Mock(defaults=['spam'])) ref_context = dict(node=ref_node, import_aliases=None) new_context = context.Context(context_object=ref_context) self.assertListEqual(['spam'], new_context.function_def_defaults_qual) ref_node = mock.Mock(args=mock.Mock(defaults=[])) ref_context = dict(node=ref_node, import_aliases=None) new_context = context.Context(context_object=ref_context) self.assertListEqual([], new_context.function_def_defaults_qual) new_context = context.Context() self.assertListEqual([], new_context.function_def_defaults_qual) def test__get_literal_value(self): new_context = context.Context() value = ast.Num(42) expected = value.n self.assertEqual(expected, new_context._get_literal_value(value)) value = ast.Str('spam') expected = value.s self.assertEqual(expected, new_context._get_literal_value(value)) value = ast.List([ast.Str('spam'), ast.Num(42)], ast.Load()) expected = [ast.Str('spam').s, ast.Num(42).n] self.assertListEqual(expected, new_context._get_literal_value(value)) value = ast.Tuple([ast.Str('spam'), ast.Num(42)], ast.Load()) expected = (ast.Str('spam').s, ast.Num(42).n) self.assertTupleEqual(expected, new_context._get_literal_value(value)) value = ast.Set([ast.Str('spam'), ast.Num(42)]) expected = set([ast.Str('spam').s, ast.Num(42).n]) self.assertSetEqual(expected, new_context._get_literal_value(value)) value = ast.Dict(['spam', 'eggs'], [42, 'foo']) expected = dict(spam=42, eggs='foo') self.assertDictEqual(expected, new_context._get_literal_value(value)) value = ast.Ellipsis() self.assertIsNone(new_context._get_literal_value(value)) value = ast.Name('spam', ast.Load()) expected = value.id self.assertEqual(expected, new_context._get_literal_value(value)) if six.PY3: value = ast.NameConstant(True) expected = str(value.value) self.assertEqual(expected, new_context._get_literal_value(value)) if six.PY3: value = ast.Bytes(b'spam') expected = value.s self.assertEqual(expected, new_context._get_literal_value(value)) self.assertIsNone(new_context._get_literal_value(None)) @mock.patch('bandit.core.context.Context.call_keywords', new_callable=mock.PropertyMock) def test_check_call_arg_value(self, call_keywords): new_context = context.Context() call_keywords.return_value = dict(spam='eggs') self.assertTrue(new_context.check_call_arg_value('spam', 'eggs')) self.assertTrue(new_context.check_call_arg_value('spam', ['spam', 'eggs'])) self.assertFalse(new_context.check_call_arg_value('spam', 'spam')) self.assertFalse(new_context.check_call_arg_value('spam')) self.assertFalse(new_context.check_call_arg_value('eggs')) new_context = context.Context() self.assertIsNone(new_context.check_call_arg_value(None)) @mock.patch('bandit.core.context.Context.node', new_callable=mock.PropertyMock) def test_get_lineno_for_call_arg(self, node): expected_lineno = 42 keyword1 = mock.Mock(arg='spam', value=mock.Mock(lineno=expected_lineno)) node.return_value = mock.Mock(keywords=[keyword1]) new_context = context.Context() actual_lineno = new_context.get_lineno_for_call_arg('spam') self.assertEqual(expected_lineno, actual_lineno) new_context = context.Context() missing_lineno = new_context.get_lineno_for_call_arg('eggs') self.assertIsNone(missing_lineno) def test_get_call_arg_at_position(self): expected_arg = 'spam' ref_call = mock.Mock() ref_call.args = [ast.Str(expected_arg)] ref_context = dict(call=ref_call) new_context = context.Context(context_object=ref_context) self.assertEqual(expected_arg, new_context.get_call_arg_at_position(0)) self.assertIsNone(new_context.get_call_arg_at_position(1)) ref_call = mock.Mock() ref_call.args = [] ref_context = dict(call=ref_call) new_context = context.Context(context_object=ref_context) self.assertIsNone(new_context.get_call_arg_at_position(0)) new_context = context.Context() self.assertIsNone(new_context.get_call_arg_at_position(0)) def test_is_module_being_imported(self): ref_context = dict(module='spam') new_context = context.Context(context_object=ref_context) self.assertTrue(new_context.is_module_being_imported('spam')) self.assertFalse(new_context.is_module_being_imported('eggs')) new_context = context.Context() self.assertFalse(new_context.is_module_being_imported('spam')) def test_is_module_imported_exact(self): ref_context = dict(imports=['spam']) new_context = context.Context(context_object=ref_context) self.assertTrue(new_context.is_module_imported_exact('spam')) self.assertFalse(new_context.is_module_imported_exact('eggs')) new_context = context.Context() self.assertFalse(new_context.is_module_being_imported('spam')) def test_is_module_imported_like(self): ref_context = dict(imports=[['spam'], ['eggs']]) new_context = context.Context(context_object=ref_context) self.assertTrue(new_context.is_module_imported_like('spam')) self.assertFalse(new_context.is_module_imported_like('bacon')) new_context = context.Context() self.assertFalse(new_context.is_module_imported_like('spam')) bandit-1.4.0/tests/unit/core/test_issue.py000066400000000000000000000110051303375232700205710ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2015 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import mock import testtools import bandit from bandit.core import constants from bandit.core import issue class IssueTests(testtools.TestCase): def test_issue_create(self): new_issue = _get_issue_instance() self.assertIsInstance(new_issue, issue.Issue) def test_issue_str(self): test_issue = _get_issue_instance() self.assertEqual( ("Issue: 'Test issue' from B999:bandit_plugin: Severity: MEDIUM " "Confidence: MEDIUM at code.py:1"), str(test_issue) ) def test_issue_as_dict(self): test_issue = _get_issue_instance() test_issue_dict = test_issue.as_dict(with_code=False) self.assertIsInstance(test_issue_dict, dict) self.assertEqual('code.py', test_issue_dict['filename']) self.assertEqual('bandit_plugin', test_issue_dict['test_name']) self.assertEqual('B999', test_issue_dict['test_id']) self.assertEqual('MEDIUM', test_issue_dict['issue_severity']) self.assertEqual('MEDIUM', test_issue_dict['issue_confidence']) self.assertEqual('Test issue', test_issue_dict['issue_text']) self.assertEqual(1, test_issue_dict['line_number']) self.assertEqual([], test_issue_dict['line_range']) def test_issue_filter_severity(self): levels = [bandit.LOW, bandit.MEDIUM, bandit.HIGH] issues = [_get_issue_instance(l, bandit.HIGH) for l in levels] for level in levels: rank = constants.RANKING.index(level) for i in issues: test = constants.RANKING.index(i.severity) result = i.filter(level, bandit.UNDEFINED) self.assertTrue((test >= rank) == result) def test_issue_filter_confidence(self): levels = [bandit.LOW, bandit.MEDIUM, bandit.HIGH] issues = [_get_issue_instance(bandit.HIGH, l) for l in levels] for level in levels: rank = constants.RANKING.index(level) for i in issues: test = constants.RANKING.index(i.confidence) result = i.filter(bandit.UNDEFINED, level) self.assertTrue((test >= rank) == result) def test_matches_issue(self): issue_a = _get_issue_instance() issue_b = _get_issue_instance(severity=bandit.HIGH) issue_c = _get_issue_instance(confidence=bandit.LOW) issue_d = _get_issue_instance() issue_d.text = 'ABCD' issue_e = _get_issue_instance() issue_e.fname = 'file1.py' issue_f = issue_a issue_g = _get_issue_instance() issue_g.test = 'ZZZZ' issue_h = issue_a issue_h.lineno = 12345 # positive tests self.assertEqual(issue_a, issue_a) self.assertEqual(issue_a, issue_f) self.assertEqual(issue_f, issue_a) # severity doesn't match self.assertNotEqual(issue_a, issue_b) # confidence doesn't match self.assertNotEqual(issue_a, issue_c) # text doesn't match self.assertNotEqual(issue_a, issue_d) # filename doesn't match self.assertNotEqual(issue_a, issue_e) # plugin name doesn't match self.assertNotEqual(issue_a, issue_g) # line number doesn't match but should pass because we don't test that self.assertEqual(issue_a, issue_h) @mock.patch('linecache.getline') def test_get_code(self, getline): getline.return_value = b'\x08\x30' new_issue = issue.Issue(bandit.MEDIUM, lineno=1) try: new_issue.get_code() except UnicodeDecodeError: self.fail('Bytes not properly decoded in issue.get_code()') def _get_issue_instance(severity=bandit.MEDIUM, confidence=bandit.MEDIUM): new_issue = issue.Issue(severity, confidence, 'Test issue') new_issue.fname = 'code.py' new_issue.test = 'bandit_plugin' new_issue.test_id = 'B999' new_issue.lineno = 1 return new_issue bandit-1.4.0/tests/unit/core/test_manager.py000066400000000000000000000307441303375232700210660ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2015 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import sys import fixtures import mock import testtools from bandit.core import config from bandit.core import constants from bandit.core import issue from bandit.core import manager class ManagerTests(testtools.TestCase): def _get_issue_instance(self, sev=constants.MEDIUM, conf=constants.MEDIUM): new_issue = issue.Issue(sev, conf, 'Test issue') new_issue.fname = 'code.py' new_issue.test = 'bandit_plugin' new_issue.lineno = 1 return new_issue def setUp(self): super(ManagerTests, self).setUp() self.profile = {} self.profile['include'] = { 'any_other_function_with_shell_equals_true', 'assert_used'} self.config = config.BanditConfig() self.manager = manager.BanditManager(config=self.config, agg_type='file', debug=False, verbose=False) def test_create_manager(self): # make sure we can create a manager self.assertEqual(self.manager.debug, False) self.assertEqual(self.manager.verbose, False) self.assertEqual(self.manager.agg_type, 'file') def test_create_manager_with_profile(self): # make sure we can create a manager m = manager.BanditManager(config=self.config, agg_type='file', debug=False, verbose=False, profile=self.profile) self.assertEqual(m.debug, False) self.assertEqual(m.verbose, False) self.assertEqual(m.agg_type, 'file') def test_matches_globlist(self): self.assertTrue(manager._matches_glob_list('test', ['*tes*'])) self.assertFalse(manager._matches_glob_list('test', ['*fes*'])) def test_is_file_included(self): a = manager._is_file_included(path='a.py', included_globs=['*.py'], excluded_path_strings='', enforce_glob=True) b = manager._is_file_included(path='a.dd', included_globs=['*.py'], excluded_path_strings='', enforce_glob=False) c = manager._is_file_included(path='a.py', included_globs=['*.py'], excluded_path_strings='a.py', enforce_glob=True) d = manager._is_file_included(path='a.dd', included_globs=['*.py'], excluded_path_strings='', enforce_glob=True) self.assertTrue(a) self.assertTrue(b) self.assertFalse(c) self.assertFalse(d) @mock.patch('os.walk') def test_get_files_from_dir(self, os_walk): os_walk.return_value = [ ('/', ('a'), ()), ('/a', (), ('a.py', 'b.py', 'c.ww')) ] inc, exc = manager._get_files_from_dir(files_dir='', included_globs=['*.py'], excluded_path_strings=None) self.assertEqual(exc, set(['/a/c.ww'])) self.assertEqual(inc, set(['/a/a.py', '/a/b.py'])) def test_populate_baseline_success(self): # Test populate_baseline with valid JSON baseline_data = """{ "results": [ { "code": "test code", "filename": "example_file.py", "issue_severity": "low", "issue_confidence": "low", "issue_text": "test issue", "test_name": "some_test", "test_id": "x", "line_number": "n", "line_range": "n-m" } ] } """ issue_dictionary = {"code": "test code", "filename": "example_file.py", "issue_severity": "low", "issue_confidence": "low", "issue_text": "test issue", "test_name": "some_test", "test_id": "x", "line_number": "n", "line_range": "n-m"} baseline_items = [issue.issue_from_dict(issue_dictionary)] self.manager.populate_baseline(baseline_data) self.assertEqual(baseline_items, self.manager.baseline) @mock.patch('logging.Logger.warning') def test_populate_baseline_invalid_json(self, mock_logger_warning): # Test populate_baseline with invalid JSON content baseline_data = """{"data": "bad"}""" self.manager.populate_baseline(baseline_data) # Default value for manager.baseline is [] self.assertEqual([], self.manager.baseline) self.assertTrue(mock_logger_warning.called) def test_results_count(self): levels = [constants.LOW, constants.MEDIUM, constants.HIGH] self.manager.results = ( [issue.Issue(severity=l, confidence=l) for l in levels]) r = [self.manager.results_count(sev_filter=l, conf_filter=l) for l in levels] self.assertEqual([3, 2, 1], r) def test_output_results_invalid_format(self): # Test that output_results succeeds given an invalid format temp_directory = self.useFixture(fixtures.TempDir()).path lines = 5 sev_level = constants.LOW conf_level = constants.LOW output_filename = os.path.join(temp_directory, "_temp_output") output_format = "invalid" tmp_file = open(output_filename, 'w') self.manager.output_results(lines, sev_level, conf_level, tmp_file, output_format) if sys.stdout.isatty(): self.assertFalse(os.path.isfile(output_filename)) else: self.assertTrue(os.path.isfile(output_filename)) def test_output_results_valid_format(self): # Test that output_results succeeds given a valid format temp_directory = self.useFixture(fixtures.TempDir()).path lines = 5 sev_level = constants.LOW conf_level = constants.LOW output_filename = os.path.join(temp_directory, "_temp_output.txt") output_format = "txt" tmp_file = open(output_filename, 'w') self.manager.output_results(lines, sev_level, conf_level, tmp_file, output_format) self.assertTrue(os.path.isfile(output_filename)) @mock.patch('os.path.isdir') def test_discover_files_recurse_skip(self, isdir): isdir.return_value = True self.manager.discover_files(['thing'], False) self.assertEqual(self.manager.files_list, []) self.assertEqual(self.manager.excluded_files, []) @mock.patch('os.path.isdir') def test_discover_files_recurse_files(self, isdir): isdir.return_value = True with mock.patch.object(manager, '_get_files_from_dir') as m: m.return_value = (set(['files']), set(['excluded'])) self.manager.discover_files(['thing'], True) self.assertEqual(self.manager.files_list, ['files']) self.assertEqual(self.manager.excluded_files, ['excluded']) @mock.patch('os.path.isdir') def test_discover_files_exclude(self, isdir): isdir.return_value = False with mock.patch.object(manager, '_is_file_included') as m: m.return_value = False self.manager.discover_files(['thing'], True) self.assertEqual(self.manager.files_list, []) self.assertEqual(self.manager.excluded_files, ['thing']) @mock.patch('os.path.isdir') def test_discover_files_exclude_cmdline(self, isdir): isdir.return_value = False with mock.patch.object(manager, '_is_file_included') as m: self.manager.discover_files(['a', 'b', 'c'], True, excluded_paths='a,b') m.assert_called_with('c', ['*.py', '*.pyw'], ['a', 'b'], enforce_glob=False) @mock.patch('os.path.isdir') def test_discover_files_include(self, isdir): isdir.return_value = False with mock.patch.object(manager, '_is_file_included') as m: m.return_value = True self.manager.discover_files(['thing'], True) self.assertEqual(self.manager.files_list, ['thing']) self.assertEqual(self.manager.excluded_files, []) def test_run_tests_keyboardinterrupt(self): # Test that bandit manager exits when there is a keyboard interrupt temp_directory = self.useFixture(fixtures.TempDir()).path some_file = os.path.join(temp_directory, 'some_code_file.py') with open(some_file, 'wt') as fd: fd.write('some_code = x + 1') self.manager.files_list = [some_file] with mock.patch('bandit.core.metrics.Metrics.count_issues' ) as mock_count_issues: mock_count_issues.side_effect = KeyboardInterrupt # assert a SystemExit with code 2 self.assertRaisesRegex(SystemExit, '2', self.manager.run_tests) def test_run_tests_ioerror(self): # Test that a file name is skipped and added to the manager.skipped # list when there is an IOError attempting to open/read the file temp_directory = self.useFixture(fixtures.TempDir()).path no_such_file = os.path.join(temp_directory, 'no_such_file.py') self.manager.files_list = [no_such_file] self.manager.run_tests() # since the file name and the IOError.strerror text are added to # manager.skipped, we convert skipped to str to find just the file name # since IOError is not constant self.assertIn(no_such_file, str(self.manager.skipped)) def test_compare_baseline(self): issue_a = self._get_issue_instance() issue_a.fname = 'file1.py' issue_b = self._get_issue_instance() issue_b.fname = 'file2.py' issue_c = self._get_issue_instance(sev=constants.HIGH) issue_c.fname = 'file1.py' # issue c is in results, not in baseline self.assertEqual( [issue_c], manager._compare_baseline_results([issue_a, issue_b], [issue_a, issue_b, issue_c])) # baseline and results are the same self.assertEqual( [], manager._compare_baseline_results([issue_a, issue_b, issue_c], [issue_a, issue_b, issue_c])) # results are better than baseline self.assertEqual( [], manager._compare_baseline_results([issue_a, issue_b, issue_c], [issue_a, issue_b])) def test_find_candidate_matches(self): issue_a = self._get_issue_instance() issue_b = self._get_issue_instance() issue_c = self._get_issue_instance() issue_c.fname = 'file1.py' # issue a and b are the same, both should be returned as candidates self.assertEqual({issue_a: [issue_a, issue_b]}, manager._find_candidate_matches([issue_a], [issue_a, issue_b])) # issue a and c are different, only a should be returned self.assertEqual({issue_a: [issue_a]}, manager._find_candidate_matches([issue_a], [issue_a, issue_c])) # c doesn't match a, empty list should be returned self.assertEqual({issue_a: []}, manager._find_candidate_matches([issue_a], [issue_c])) # a and b match, a and b should both return a and b candidates self.assertEqual( {issue_a: [issue_a, issue_b], issue_b: [issue_a, issue_b]}, manager._find_candidate_matches([issue_a, issue_b], [issue_a, issue_b, issue_c])) bandit-1.4.0/tests/unit/core/test_meta_ast.py000066400000000000000000000027061303375232700212460ustar00rootroot00000000000000# Copyright (c) 2015 VMware, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import six import testtools from bandit.core import meta_ast class BanditMetaAstTests(testtools.TestCase): def setUp(self): super(BanditMetaAstTests, self).setUp() self.b_meta_ast = meta_ast.BanditMetaAst() self.node = 'fake_node' self.parent_id = 'fake_parent_id' self.depth = 1 self.b_meta_ast.add_node(self.node, self.parent_id, self.depth) self.node_id = hex(id(self.node)) def test_add_node(self): expected = {'raw': self.node, 'parent_id': self.parent_id, 'depth': self.depth} self.assertEqual(expected, self.b_meta_ast.nodes[self.node_id]) def test_str(self): node = self.b_meta_ast.nodes[self.node_id] expected = 'Node: %s\n\t%s\nLength: 1\n' % (self.node_id, node) self.assertEqual(expected, six.text_type(self.b_meta_ast)) bandit-1.4.0/tests/unit/core/test_test_set.py000066400000000000000000000143641303375232700213060ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright (c) 2016 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import mock from stevedore import extension import testtools from bandit.blacklists import utils from bandit.core import extension_loader from bandit.core import test_properties as test from bandit.core import test_set @test.checks('Str') @test.test_id('B000') def test_plugin(): sets = [] sets.append(utils.build_conf_dict( 'telnet', 'B401', ['telnetlib'], 'A telnet-related module is being imported. Telnet is ' 'considered insecure. Use SSH or some other encrypted protocol.', 'HIGH' )) sets.append(utils.build_conf_dict( 'marshal', 'B302', ['marshal.load', 'marshal.loads'], 'Deserialization with the marshal module is possibly dangerous.' )) return {'Import': sets, 'ImportFrom': sets, 'Call': sets} class BanditTestSetTests(testtools.TestCase): def _make_test_manager(self, plugin): return extension.ExtensionManager.make_test_instance( [extension.Extension('test_plugin', None, test_plugin, None)]) def setUp(self): super(BanditTestSetTests, self).setUp() mngr = self._make_test_manager(mock.Mock) self.patchExtMan = mock.patch('stevedore.extension.ExtensionManager') self.mockExtMan = self.patchExtMan.start() self.mockExtMan.return_value = mngr self.old_ext_man = extension_loader.MANAGER extension_loader.MANAGER = extension_loader.Manager() self.config = mock.MagicMock() self.config.get_setting.return_value = None def tearDown(self): self.patchExtMan.stop() super(BanditTestSetTests, self).tearDown() extension_loader.MANAGER = self.old_ext_man def test_has_defaults(self): ts = test_set.BanditTestSet(self.config) self.assertEqual(len(ts.get_tests('Str')), 1) def test_profile_include_id(self): profile = {'include': ['B000']} ts = test_set.BanditTestSet(self.config, profile) self.assertEqual(len(ts.get_tests('Str')), 1) def test_profile_exclude_id(self): profile = {'exclude': ['B000']} ts = test_set.BanditTestSet(self.config, profile) self.assertEqual(len(ts.get_tests('Str')), 0) def test_profile_include_none(self): profile = {'include': []} # same as no include ts = test_set.BanditTestSet(self.config, profile) self.assertEqual(len(ts.get_tests('Str')), 1) def test_profile_exclude_none(self): profile = {'exclude': []} # same as no exclude ts = test_set.BanditTestSet(self.config, profile) self.assertEqual(len(ts.get_tests('Str')), 1) def test_profile_has_builtin_blacklist(self): ts = test_set.BanditTestSet(self.config) self.assertEqual(len(ts.get_tests('Import')), 1) self.assertEqual(len(ts.get_tests('ImportFrom')), 1) self.assertEqual(len(ts.get_tests('Call')), 1) def test_profile_exclude_builtin_blacklist(self): profile = {'exclude': ['B001']} ts = test_set.BanditTestSet(self.config, profile) self.assertEqual(len(ts.get_tests('Import')), 0) self.assertEqual(len(ts.get_tests('ImportFrom')), 0) self.assertEqual(len(ts.get_tests('Call')), 0) def test_profile_exclude_builtin_blacklist_specific(self): profile = {'exclude': ['B302', 'B401']} ts = test_set.BanditTestSet(self.config, profile) self.assertEqual(len(ts.get_tests('Import')), 0) self.assertEqual(len(ts.get_tests('ImportFrom')), 0) self.assertEqual(len(ts.get_tests('Call')), 0) def test_profile_filter_blacklist_none(self): ts = test_set.BanditTestSet(self.config) blacklist = ts.get_tests('Import')[0] self.assertEqual(len(blacklist._config['Import']), 2) self.assertEqual(len(blacklist._config['ImportFrom']), 2) self.assertEqual(len(blacklist._config['Call']), 2) def test_profile_filter_blacklist_one(self): profile = {'exclude': ['B401']} ts = test_set.BanditTestSet(self.config, profile) blacklist = ts.get_tests('Import')[0] self.assertEqual(len(blacklist._config['Import']), 1) self.assertEqual(len(blacklist._config['ImportFrom']), 1) self.assertEqual(len(blacklist._config['Call']), 1) def test_profile_filter_blacklist_include(self): profile = {'include': ['B001', 'B401']} ts = test_set.BanditTestSet(self.config, profile) blacklist = ts.get_tests('Import')[0] self.assertEqual(len(blacklist._config['Import']), 1) self.assertEqual(len(blacklist._config['ImportFrom']), 1) self.assertEqual(len(blacklist._config['Call']), 1) def test_profile_filter_blacklist_all(self): profile = {'exclude': ['B401', 'B302']} ts = test_set.BanditTestSet(self.config, profile) # if there is no blacklist data for a node type then we wont add a # blacklist test to it, as this would be pointless. self.assertEqual(len(ts.get_tests('Import')), 0) self.assertEqual(len(ts.get_tests('ImportFrom')), 0) self.assertEqual(len(ts.get_tests('Call')), 0) def test_profile_blacklist_compat(self): data = [utils.build_conf_dict( 'marshal', 'B302', ['marshal.load', 'marshal.loads'], ('Deserialization with the marshal module is possibly ' 'dangerous.'))] profile = {'include': ['B001'], 'blacklist': {'Call': data}} ts = test_set.BanditTestSet(self.config, profile) blacklist = ts.get_tests('Call')[0] self.assertNotIn('Import', blacklist._config) self.assertNotIn('ImportFrom', blacklist._config) self.assertEqual(len(blacklist._config['Call']), 1) bandit-1.4.0/tests/unit/core/test_util.py000066400000000000000000000275511303375232700204330ustar00rootroot00000000000000# -*- coding:utf-8 -*- # # Copyright 2014 Hewlett-Packard Development Company, L.P. # Copyright 2015 Nebula, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import ast import os import shutil import sys import tempfile import testtools from bandit.core import utils as b_utils def _touch(path): '''Create an empty file at ``path``.''' newf = open(path, 'w') newf.close() class UtilTests(testtools.TestCase): '''This set of tests exercises bandit.core.util functions.''' def setUp(self): super(UtilTests, self).setUp() self._setup_get_module_qualname_from_path() def _setup_get_module_qualname_from_path(self): '''Setup a fake directory for testing get_module_qualname_from_path(). Create temporary directory and then create fake .py files within directory structure. We setup test cases for a typical module, a path misssing a middle __init__.py, no __init__.py anywhere in path, symlinking .py files. ''' self.tempdir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, self.tempdir) self.reltempdir = os.path.relpath(self.tempdir) # good/a/b/c/test_typical.py os.makedirs(os.path.join( self.tempdir, 'good', 'a', 'b', 'c'), 0o755) _touch(os.path.join(self.tempdir, 'good', '__init__.py')) _touch(os.path.join(self.tempdir, 'good', 'a', '__init__.py')) _touch(os.path.join( self.tempdir, 'good', 'a', 'b', '__init__.py')) _touch(os.path.join( self.tempdir, 'good', 'a', 'b', 'c', '__init__.py')) _touch(os.path.join( self.tempdir, 'good', 'a', 'b', 'c', 'test_typical.py')) # missingmid/a/b/c/test_missingmid.py os.makedirs(os.path.join( self.tempdir, 'missingmid', 'a', 'b', 'c'), 0o755) _touch(os.path.join(self.tempdir, 'missingmid', '__init__.py')) # no missingmid/a/__init__.py _touch(os.path.join( self.tempdir, 'missingmid', 'a', 'b', '__init__.py')) _touch(os.path.join( self.tempdir, 'missingmid', 'a', 'b', 'c', '__init__.py')) _touch(os.path.join( self.tempdir, 'missingmid', 'a', 'b', 'c', 'test_missingmid.py')) # missingend/a/b/c/test_missingend.py os.makedirs(os.path.join( self.tempdir, 'missingend', 'a', 'b', 'c'), 0o755) _touch(os.path.join( self.tempdir, 'missingend', '__init__.py')) _touch(os.path.join( self.tempdir, 'missingend', 'a', 'b', '__init__.py')) # no missingend/a/b/c/__init__.py _touch(os.path.join( self.tempdir, 'missingend', 'a', 'b', 'c', 'test_missingend.py')) # syms/a/bsym/c/test_typical.py os.makedirs(os.path.join(self.tempdir, 'syms', 'a'), 0o755) _touch(os.path.join(self.tempdir, 'syms', '__init__.py')) _touch(os.path.join(self.tempdir, 'syms', 'a', '__init__.py')) os.symlink(os.path.join(self.tempdir, 'good', 'a', 'b'), os.path.join(self.tempdir, 'syms', 'a', 'bsym')) def test_get_module_qualname_from_path_abs_typical(self): '''Test get_module_qualname_from_path with typical absolute paths.''' name = b_utils.get_module_qualname_from_path(os.path.join( self.tempdir, 'good', 'a', 'b', 'c', 'test_typical.py')) self.assertEqual('good.a.b.c.test_typical', name) def test_get_module_qualname_from_path_with_dot(self): '''Test get_module_qualname_from_path with a "." .''' name = b_utils.get_module_qualname_from_path(os.path.join( '.', '__init__.py')) self.assertEqual('__init__', name) def test_get_module_qualname_from_path_abs_missingmid(self): # Test get_module_qualname_from_path with missing module # __init__.py name = b_utils.get_module_qualname_from_path(os.path.join( self.tempdir, 'missingmid', 'a', 'b', 'c', 'test_missingmid.py')) self.assertEqual('b.c.test_missingmid', name) def test_get_module_qualname_from_path_abs_missingend(self): # Test get_module_qualname_from_path with no __init__.py # last dir''' name = b_utils.get_module_qualname_from_path(os.path.join( self.tempdir, 'missingend', 'a', 'b', 'c', 'test_missingend.py')) self.assertEqual('test_missingend', name) def test_get_module_qualname_from_path_abs_syms(self): '''Test get_module_qualname_from_path with symlink in path.''' name = b_utils.get_module_qualname_from_path(os.path.join( self.tempdir, 'syms', 'a', 'bsym', 'c', 'test_typical.py')) self.assertEqual('syms.a.bsym.c.test_typical', name) def test_get_module_qualname_from_path_rel_typical(self): '''Test get_module_qualname_from_path with typical relative paths.''' name = b_utils.get_module_qualname_from_path(os.path.join( self.reltempdir, 'good', 'a', 'b', 'c', 'test_typical.py')) self.assertEqual('good.a.b.c.test_typical', name) def test_get_module_qualname_from_path_rel_missingmid(self): # Test get_module_qualname_from_path with module __init__.py # missing and relative paths name = b_utils.get_module_qualname_from_path(os.path.join( self.reltempdir, 'missingmid', 'a', 'b', 'c', 'test_missingmid.py')) self.assertEqual('b.c.test_missingmid', name) def test_get_module_qualname_from_path_rel_missingend(self): # Test get_module_qualname_from_path with __init__.py missing from # last dir and using relative paths name = b_utils.get_module_qualname_from_path(os.path.join( self.reltempdir, 'missingend', 'a', 'b', 'c', 'test_missingend.py')) self.assertEqual('test_missingend', name) def test_get_module_qualname_from_path_rel_syms(self): '''Test get_module_qualname_from_path with symbolic relative paths.''' name = b_utils.get_module_qualname_from_path(os.path.join( self.reltempdir, 'syms', 'a', 'bsym', 'c', 'test_typical.py')) self.assertEqual('syms.a.bsym.c.test_typical', name) def test_get_module_qualname_from_path_sys(self): '''Test get_module_qualname_from_path with system module paths.''' name = b_utils.get_module_qualname_from_path(os.__file__) self.assertEqual('os', name) # This will fail because of magic for os.path. Not sure how to fix. # name = b_utils.get_module_qualname_from_path(os.path.__file__) # self.assertEqual(name, 'os.path') def test_get_module_qualname_from_path_invalid_path(self): '''Test get_module_qualname_from_path with invalid path.''' name = b_utils.get_module_qualname_from_path('/a/b/c/d/e.py') self.assertEqual('e', name) def test_get_module_qualname_from_path_dir(self): '''Test get_module_qualname_from_path with dir path.''' self.assertRaises(b_utils.InvalidModulePath, b_utils.get_module_qualname_from_path, '/tmp/') def test_namespace_path_join(self): p = b_utils.namespace_path_join('base1.base2', 'name') self.assertEqual('base1.base2.name', p) def test_namespace_path_split(self): (head, tail) = b_utils.namespace_path_split('base1.base2.name') self.assertEqual('base1.base2', head) self.assertEqual('name', tail) def test_get_call_name1(self): '''Gets a qualified call name.''' tree = ast.parse('a.b.c.d(x,y)').body[0].value name = b_utils.get_call_name(tree, {}) self.assertEqual('a.b.c.d', name) def test_get_call_name2(self): '''Gets qualified call name and resolves aliases.''' tree = ast.parse('a.b.c.d(x,y)').body[0].value name = b_utils.get_call_name(tree, {'a': 'alias.x.y'}) self.assertEqual('alias.x.y.b.c.d', name) name = b_utils.get_call_name(tree, {'a.b': 'alias.x.y'}) self.assertEqual('alias.x.y.c.d', name) name = b_utils.get_call_name(tree, {'a.b.c.d': 'alias.x.y'}) self.assertEqual('alias.x.y', name) def test_get_call_name3(self): '''Getting name for a complex call.''' tree = ast.parse('a.list[0](x,y)').body[0].value name = b_utils._get_attr_qual_name(tree, {}) self.assertEqual('', name) # TODO(ljfisher) At best we might be able to get: # self.assertEqual(name, 'a.list[0]') def test_linerange(self): self.test_file = open("./examples/jinja2_templating.py") self.tree = ast.parse(self.test_file.read()) # Check linerange returns corrent number of lines line = self.tree.body[8] lrange = b_utils.linerange(line) # line 9 should be three lines long self.assertEqual(3, len(lrange)) # the range should be the correct line numbers self.assertEqual([11, 12, 13], list(lrange)) def test_path_for_function(self): path = b_utils.get_path_for_function(b_utils.get_path_for_function) self.assertEqual(path, b_utils.__file__) def test_path_for_function_no_file(self): self.assertIsNone(b_utils.get_path_for_function(sys.settrace)) def test_path_for_function_no_module(self): self.assertIsNone(b_utils.get_path_for_function(1)) def test_escaped_representation_simple(self): res = b_utils.escaped_bytes_representation(b"ascii") self.assertEqual(res, b"ascii") def test_escaped_representation_valid_not_printable(self): res = b_utils.escaped_bytes_representation(b"\u0000") self.assertEqual(res, b"\\x00") def test_escaped_representation_invalid(self): res = b_utils.escaped_bytes_representation(b"\uffff") self.assertEqual(res, b"\\uffff") def test_escaped_representation_mixed(self): res = b_utils.escaped_bytes_representation(b"ascii\u0000\uffff") self.assertEqual(res, b"ascii\\x00\\uffff") def test_deepgetattr(self): a = type('', (), {}) a.b = type('', (), {}) a.b.c = type('', (), {}) a.b.c.d = 'deep value' a.b.c.d2 = 'deep value 2' a.b.c.e = 'a.b.c' self.assertEqual('deep value', b_utils.deepgetattr(a.b.c, 'd')) self.assertEqual('deep value 2', b_utils.deepgetattr(a.b.c, 'd2')) self.assertEqual('a.b.c', b_utils.deepgetattr(a.b.c, 'e')) self.assertEqual('deep value', b_utils.deepgetattr(a, 'b.c.d')) self.assertEqual('deep value 2', b_utils.deepgetattr(a, 'b.c.d2')) self.assertRaises(AttributeError, b_utils.deepgetattr, a.b, 'z') def test_parse_ini_file(self): tests = [{'content': "[bandit]\nexclude=/abc,/def", 'expected': {'exclude': '/abc,/def'}}, {'content': '[Blabla]\nsomething=something', 'expected': None}] with tempfile.NamedTemporaryFile('r+') as t: for test in tests: f = open(t.name, 'w') f.write(test['content']) f.close() self.assertEqual(b_utils.parse_ini_file(t.name), test['expected']) def test_check_ast_node_good(self): node = b_utils.check_ast_node("Call") self.assertEqual("Call", node) def test_check_ast_node_bad_node(self): self.assertRaises(TypeError, b_utils.check_ast_node, 'Derp') def test_check_ast_node_bad_type(self): self.assertRaises(TypeError, b_utils.check_ast_node, 'walk') bandit-1.4.0/tests/unit/formatters/000077500000000000000000000000001303375232700172715ustar00rootroot00000000000000bandit-1.4.0/tests/unit/formatters/__init__.py000066400000000000000000000000001303375232700213700ustar00rootroot00000000000000bandit-1.4.0/tests/unit/formatters/test_csv.py000066400000000000000000000050121303375232700214730ustar00rootroot00000000000000# Copyright (c) 2015 VMware, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import csv import tempfile import six import testtools import bandit from bandit.core import config from bandit.core import issue from bandit.core import manager from bandit.formatters import csv as b_csv class CsvFormatterTests(testtools.TestCase): def setUp(self): super(CsvFormatterTests, self).setUp() conf = config.BanditConfig() self.manager = manager.BanditManager(conf, 'file') (tmp_fd, self.tmp_fname) = tempfile.mkstemp() self.context = {'filename': self.tmp_fname, 'lineno': 4, 'linerange': [4]} self.check_name = 'hardcoded_bind_all_interfaces' self.issue = issue.Issue(bandit.MEDIUM, bandit.MEDIUM, 'Possible binding to all interfaces.') self.manager.out_file = self.tmp_fname self.issue.fname = self.context['filename'] self.issue.lineno = self.context['lineno'] self.issue.linerange = self.context['linerange'] self.issue.test = self.check_name self.manager.results.append(self.issue) def test_report(self): tmp_file = open(self.tmp_fname, 'w') b_csv.report(self.manager, tmp_file, self.issue.severity, self.issue.confidence) with open(self.tmp_fname) as f: reader = csv.DictReader(f) data = six.next(reader) self.assertEqual(self.tmp_fname, data['filename']) self.assertEqual(self.issue.severity, data['issue_severity']) self.assertEqual(self.issue.confidence, data['issue_confidence']) self.assertEqual(self.issue.text, data['issue_text']) self.assertEqual(six.text_type(self.context['lineno']), data['line_number']) self.assertEqual(six.text_type(self.context['linerange']), data['line_range']) self.assertEqual(self.check_name, data['test_name']) bandit-1.4.0/tests/unit/formatters/test_html.py000066400000000000000000000140611303375232700216500ustar00rootroot00000000000000# Copyright (c) 2015 Rackspace, Inc. # Copyright (c) 2015 Hewlett Packard Enterprise # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections import tempfile import bs4 import mock import testtools import bandit from bandit.core import config from bandit.core import issue from bandit.core import manager from bandit.formatters import html as b_html class HtmlFormatterTests(testtools.TestCase): def setUp(self): super(HtmlFormatterTests, self).setUp() conf = config.BanditConfig() self.manager = manager.BanditManager(conf, 'file') (tmp_fd, self.tmp_fname) = tempfile.mkstemp() self.manager.out_file = self.tmp_fname def test_report_with_skipped(self): self.manager.skipped = [('abc.py', 'File is bad')] tmp_file = open(self.tmp_fname, 'w') b_html.report( self.manager, tmp_file, bandit.LOW, bandit.LOW) with open(self.tmp_fname) as f: soup = bs4.BeautifulSoup(f.read(), 'html.parser') skipped = soup.find_all('div', id='skipped')[0] self.assertEqual(1, len(soup.find_all('div', id='skipped'))) self.assertIn('abc.py', skipped.text) self.assertIn('File is bad', skipped.text) @mock.patch('bandit.core.issue.Issue.get_code') @mock.patch('bandit.core.manager.BanditManager.get_issue_list') def test_report_contents(self, get_issue_list, get_code): self.manager.metrics.data['_totals'] = {'loc': 1000, 'nosec': 50} issue_a = _get_issue_instance(severity=bandit.LOW) issue_a.fname = 'abc.py' issue_a.test = 'AAAAAAA' issue_a.text = 'BBBBBBB' issue_a.confidence = 'CCCCCCC' # don't need to test severity, it determines the color which we're # testing separately issue_b = _get_issue_instance(severity=bandit.MEDIUM) issue_c = _get_issue_instance(severity=bandit.HIGH) issue_x = _get_issue_instance() get_code.return_value = 'some code' issue_y = _get_issue_instance() get_issue_list.return_value = collections.OrderedDict( [(issue_a, [issue_x, issue_y]), (issue_b, [issue_x]), (issue_c, [issue_y])]) tmp_file = open(self.tmp_fname, 'w') b_html.report( self.manager, tmp_file, bandit.LOW, bandit.LOW) with open(self.tmp_fname) as f: soup = bs4.BeautifulSoup(f.read(), 'html.parser') self.assertEqual('1000', soup.find_all('span', id='loc')[0].text) self.assertEqual('50', soup.find_all('span', id='nosec')[0].text) issue1 = soup.find_all('div', id='issue-0')[0] issue2 = soup.find_all('div', id='issue-1')[0] issue3 = soup.find_all('div', id='issue-2')[0] # make sure the class has been applied properly self.assertEqual(1, len(issue1.find_all( 'div', {'class': 'issue-sev-low'}))) self.assertEqual(1, len(issue2.find_all( 'div', {'class': 'issue-sev-medium'}))) self.assertEqual(1, len(issue3.find_all( 'div', {'class': 'issue-sev-high'}))) # issue1 has a candidates section with 2 candidates in it self.assertEqual(1, len(issue1.find_all('div', {'class': 'candidates'}))) self.assertEqual(2, len(issue1.find_all('div', {'class': 'candidate'}))) # issue2 doesn't have candidates self.assertEqual(0, len(issue2.find_all('div', {'class': 'candidates'}))) self.assertEqual(0, len(issue2.find_all('div', {'class': 'candidate'}))) # issue1 doesn't have code issue 2 and 3 do self.assertEqual(0, len(issue1.find_all('div', {'class': 'code'}))) self.assertEqual(1, len(issue2.find_all('div', {'class': 'code'}))) self.assertEqual(1, len(issue3.find_all('div', {'class': 'code'}))) # issue2 code and issue1 first candidate have code element1 = issue1.find_all('div', {'class': 'candidate'}) self.assertIn('some code', element1[0].text) element2 = issue2.find_all('div', {'class': 'code'}) self.assertIn('some code', element2[0].text) # make sure correct things are being output in issues self.assertIn('AAAAAAA:', issue1.text) self.assertIn('BBBBBBB', issue1.text) self.assertIn('CCCCCCC', issue1.text) self.assertIn('abc.py', issue1.text) @mock.patch('bandit.core.issue.Issue.get_code') @mock.patch('bandit.core.manager.BanditManager.get_issue_list') def test_escaping(self, get_issue_list, get_code): self.manager.metrics.data['_totals'] = {'loc': 1000, 'nosec': 50} marker = '' issue_a = _get_issue_instance() issue_x = _get_issue_instance() get_code.return_value = marker get_issue_list.return_value = {issue_a: [issue_x]} tmp_file = open(self.tmp_fname, 'w') b_html.report( self.manager, tmp_file, bandit.LOW, bandit.LOW) with open(self.tmp_fname) as f: contents = f.read() self.assertNotIn(marker, contents) def _get_issue_instance(severity=bandit.MEDIUM, confidence=bandit.MEDIUM): new_issue = issue.Issue(severity, confidence, 'Test issue') new_issue.fname = 'code.py' new_issue.test = 'bandit_plugin' new_issue.lineno = 1 return new_issue bandit-1.4.0/tests/unit/formatters/test_json.py000066400000000000000000000075651303375232700216700ustar00rootroot00000000000000# Copyright (c) 2015 VMware, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections import json import tempfile import mock import testtools import bandit from bandit.core import config from bandit.core import constants from bandit.core import issue from bandit.core import manager from bandit.core import metrics from bandit.formatters import json as b_json class JsonFormatterTests(testtools.TestCase): def setUp(self): super(JsonFormatterTests, self).setUp() conf = config.BanditConfig() self.manager = manager.BanditManager(conf, 'file') (tmp_fd, self.tmp_fname) = tempfile.mkstemp() self.context = {'filename': self.tmp_fname, 'lineno': 4, 'linerange': [4]} self.check_name = 'hardcoded_bind_all_interfaces' self.issue = issue.Issue(bandit.MEDIUM, bandit.MEDIUM, 'Possible binding to all interfaces.') self.candidates = [issue.Issue(bandit.LOW, bandit.LOW, 'Candidate A', lineno=1), issue.Issue(bandit.HIGH, bandit.HIGH, 'Candiate B', lineno=2)] self.manager.out_file = self.tmp_fname self.issue.fname = self.context['filename'] self.issue.lineno = self.context['lineno'] self.issue.linerange = self.context['linerange'] self.issue.test = self.check_name self.manager.results.append(self.issue) self.manager.metrics = metrics.Metrics() # mock up the metrics for key in ['_totals', 'binding.py']: self.manager.metrics.data[key] = {'loc': 4, 'nosec': 2} for (criteria, default) in constants.CRITERIA: for rank in constants.RANKING: self.manager.metrics.data[key]['{0}.{1}'.format( criteria, rank )] = 0 @mock.patch('bandit.core.manager.BanditManager.get_issue_list') def test_report(self, get_issue_list): self.manager.files_list = ['binding.py'] self.manager.scores = [{'SEVERITY': [0] * len(constants.RANKING), 'CONFIDENCE': [0] * len(constants.RANKING)}] get_issue_list.return_value = collections.OrderedDict( [(self.issue, self.candidates)]) tmp_file = open(self.tmp_fname, 'w') b_json.report(self.manager, tmp_file, self.issue.severity, self.issue.confidence) with open(self.tmp_fname) as f: data = json.loads(f.read()) self.assertIsNotNone(data['generated_at']) self.assertEqual(self.tmp_fname, data['results'][0]['filename']) self.assertEqual(self.issue.severity, data['results'][0]['issue_severity']) self.assertEqual(self.issue.confidence, data['results'][0]['issue_confidence']) self.assertEqual(self.issue.text, data['results'][0]['issue_text']) self.assertEqual(self.context['lineno'], data['results'][0]['line_number']) self.assertEqual(self.context['linerange'], data['results'][0]['line_range']) self.assertEqual(self.check_name, data['results'][0]['test_name']) self.assertIn('candidates', data['results'][0]) bandit-1.4.0/tests/unit/formatters/test_screen.py000066400000000000000000000205201303375232700221600ustar00rootroot00000000000000# Copyright (c) 2015 VMware, Inc. # Copyright (c) 2015 Hewlett Packard Enterprise # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections import tempfile import mock import testtools import bandit from bandit.core import config from bandit.core import issue from bandit.core import manager from bandit.formatters import screen class ScreenFormatterTests(testtools.TestCase): def setUp(self): super(ScreenFormatterTests, self).setUp() @mock.patch('bandit.core.issue.Issue.get_code') def test_output_issue(self, get_code): issue = _get_issue_instance() get_code.return_value = 'DDDDDDD' indent_val = 'CCCCCCC' def _template(_issue, _indent_val, _code, _color): return_val = ["{}{}>> Issue: [{}:{}] {}". format(_indent_val, _color, _issue.test_id, _issue.test, _issue.text), "{} Severity: {} Confidence: {}". format(_indent_val, _issue.severity.capitalize(), _issue.confidence.capitalize()), "{} Location: {}:{}{}". format(_indent_val, _issue.fname, _issue.lineno, screen.COLOR['DEFAULT'])] if _code: return_val.append("{}{}".format(_indent_val, _code)) return '\n'.join(return_val) issue_text = screen._output_issue_str(issue, indent_val) expected_return = _template(issue, indent_val, 'DDDDDDD', screen.COLOR['MEDIUM']) self.assertEqual(expected_return, issue_text) issue_text = screen._output_issue_str(issue, indent_val, show_code=False) expected_return = _template(issue, indent_val, '', screen.COLOR['MEDIUM']) self.assertEqual(expected_return, issue_text) issue.lineno = '' issue_text = screen._output_issue_str(issue, indent_val, show_lineno=False) expected_return = _template(issue, indent_val, 'DDDDDDD', screen.COLOR['MEDIUM']) self.assertEqual(expected_return, issue_text) @mock.patch('bandit.core.manager.BanditManager.get_issue_list') def test_no_issues(self, get_issue_list): conf = config.BanditConfig() self.manager = manager.BanditManager(conf, 'file') (tmp_fd, self.tmp_fname) = tempfile.mkstemp() self.manager.out_file = self.tmp_fname get_issue_list.return_value = collections.OrderedDict() with mock.patch('bandit.formatters.screen.do_print') as m: tmp_file = open(self.tmp_fname, 'w') screen.report(self.manager, tmp_file, bandit.LOW, bandit.LOW, lines=5) self.assertIn('No issues identified.', '\n'.join([str(a) for a in m.call_args])) @mock.patch('bandit.core.manager.BanditManager.get_issue_list') def test_report_nobaseline(self, get_issue_list): conf = config.BanditConfig() self.manager = manager.BanditManager(conf, 'file') (tmp_fd, self.tmp_fname) = tempfile.mkstemp() self.manager.out_file = self.tmp_fname self.manager.verbose = True self.manager.files_list = ['binding.py'] self.manager.scores = [{'SEVERITY': [0, 0, 0, 1], 'CONFIDENCE': [0, 0, 0, 1]}] self.manager.skipped = [('abc.py', 'File is bad')] self.manager.excluded_files = ['def.py'] issue_a = _get_issue_instance() issue_b = _get_issue_instance() get_issue_list.return_value = [issue_a, issue_b] self.manager.metrics.data['_totals'] = {'loc': 1000, 'nosec': 50} for category in ['SEVERITY', 'CONFIDENCE']: for level in ['UNDEFINED', 'LOW', 'MEDIUM', 'HIGH']: self.manager.metrics.data['_totals']['%s.%s' % (category, level)] = 1 # Validate that we're outputting the correct issues output_str_fn = 'bandit.formatters.screen._output_issue_str' with mock.patch(output_str_fn) as output_str: output_str.return_value = 'ISSUE_OUTPUT_TEXT' tmp_file = open(self.tmp_fname, 'w') screen.report(self.manager, tmp_file, bandit.LOW, bandit.LOW, lines=5) calls = [mock.call(issue_a, '', lines=5), mock.call(issue_b, '', lines=5)] output_str.assert_has_calls(calls, any_order=True) # Validate that we're outputting all of the expected fields and the # correct values with mock.patch('bandit.formatters.screen.do_print') as m: tmp_file = open(self.tmp_fname, 'w') screen.report(self.manager, tmp_file, bandit.LOW, bandit.LOW, lines=5) data = '\n'.join([str(a) for a in m.call_args[0][0]]) expected = 'Run started' self.assertIn(expected, data) expected_items = [ screen.header('Files in scope (1):'), '\n\tbinding.py (score: {SEVERITY: 1, CONFIDENCE: 1})'] for item in expected_items: self.assertIn(item, data) expected = screen.header('Files excluded (1):') + '\n\tdef.py' self.assertIn(expected, data) expected = ('Total lines of code: 1000\n\tTotal lines skipped ' '(#nosec): 50') self.assertIn(expected, data) expected = ('Total issues (by severity):\n\t\tUndefined: 1\n\t\t' 'Low: 1\n\t\tMedium: 1\n\t\tHigh: 1') self.assertIn(expected, data) expected = ('Total issues (by confidence):\n\t\tUndefined: 1\n\t\t' 'Low: 1\n\t\tMedium: 1\n\t\tHigh: 1') self.assertIn(expected, data) expected = (screen.header('Files skipped (1):') + '\n\tabc.py (File is bad)') self.assertIn(expected, data) @mock.patch('bandit.core.manager.BanditManager.get_issue_list') def test_report_baseline(self, get_issue_list): conf = config.BanditConfig() self.manager = manager.BanditManager(conf, 'file') (tmp_fd, self.tmp_fname) = tempfile.mkstemp() self.manager.out_file = self.tmp_fname issue_a = _get_issue_instance() issue_b = _get_issue_instance() issue_x = _get_issue_instance() issue_x.fname = 'x' issue_y = _get_issue_instance() issue_y.fname = 'y' issue_z = _get_issue_instance() issue_z.fname = 'z' get_issue_list.return_value = collections.OrderedDict( [(issue_a, [issue_x]), (issue_b, [issue_y, issue_z])]) # Validate that we're outputting the correct issues indent_val = ' ' * 10 output_str_fn = 'bandit.formatters.screen._output_issue_str' with mock.patch(output_str_fn) as output_str: output_str.return_value = 'ISSUE_OUTPUT_TEXT' tmp_file = open(self.tmp_fname, 'w') screen.report(self.manager, tmp_file, bandit.LOW, bandit.LOW, lines=5) calls = [mock.call(issue_a, '', lines=5), mock.call(issue_b, '', show_code=False, show_lineno=False), mock.call(issue_y, indent_val, lines=5), mock.call(issue_z, indent_val, lines=5)] output_str.assert_has_calls(calls, any_order=True) def _get_issue_instance(severity=bandit.MEDIUM, confidence=bandit.MEDIUM): new_issue = issue.Issue(severity, confidence, 'Test issue') new_issue.fname = 'code.py' new_issue.test = 'bandit_plugin' new_issue.lineno = 1 return new_issue bandit-1.4.0/tests/unit/formatters/test_text.py000066400000000000000000000173611303375232700216760ustar00rootroot00000000000000# Copyright (c) 2015 VMware, Inc. # Copyright (c) 2015 Hewlett Packard Enterprise # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections import tempfile import mock import testtools import bandit from bandit.core import config from bandit.core import issue from bandit.core import manager from bandit.formatters import text as b_text class TextFormatterTests(testtools.TestCase): def setUp(self): super(TextFormatterTests, self).setUp() @mock.patch('bandit.core.issue.Issue.get_code') def test_output_issue(self, get_code): issue = _get_issue_instance() get_code.return_value = 'DDDDDDD' indent_val = 'CCCCCCC' def _template(_issue, _indent_val, _code): return_val = ["{}>> Issue: [{}:{}] {}". format(_indent_val, _issue.test_id, _issue.test, _issue.text), "{} Severity: {} Confidence: {}". format(_indent_val, _issue.severity.capitalize(), _issue.confidence.capitalize()), "{} Location: {}:{}". format(_indent_val, _issue.fname, _issue.lineno)] if _code: return_val.append("{}{}".format(_indent_val, _code)) return '\n'.join(return_val) issue_text = b_text._output_issue_str(issue, indent_val) expected_return = _template(issue, indent_val, 'DDDDDDD') self.assertEqual(expected_return, issue_text) issue_text = b_text._output_issue_str(issue, indent_val, show_code=False) expected_return = _template(issue, indent_val, '') self.assertEqual(expected_return, issue_text) issue.lineno = '' issue_text = b_text._output_issue_str(issue, indent_val, show_lineno=False) expected_return = _template(issue, indent_val, 'DDDDDDD') self.assertEqual(expected_return, issue_text) @mock.patch('bandit.core.manager.BanditManager.get_issue_list') def test_no_issues(self, get_issue_list): conf = config.BanditConfig() self.manager = manager.BanditManager(conf, 'file') (tmp_fd, self.tmp_fname) = tempfile.mkstemp() self.manager.out_file = self.tmp_fname get_issue_list.return_value = collections.OrderedDict() tmp_file = open(self.tmp_fname, 'w') b_text.report(self.manager, tmp_file, bandit.LOW, bandit.LOW, lines=5) with open(self.tmp_fname) as f: data = f.read() self.assertIn('No issues identified.', data) @mock.patch('bandit.core.manager.BanditManager.get_issue_list') def test_report_nobaseline(self, get_issue_list): conf = config.BanditConfig() self.manager = manager.BanditManager(conf, 'file') (tmp_fd, self.tmp_fname) = tempfile.mkstemp() self.manager.out_file = self.tmp_fname self.manager.verbose = True self.manager.files_list = ['binding.py'] self.manager.scores = [{'SEVERITY': [0, 0, 0, 1], 'CONFIDENCE': [0, 0, 0, 1]}] self.manager.skipped = [('abc.py', 'File is bad')] self.manager.excluded_files = ['def.py'] issue_a = _get_issue_instance() issue_b = _get_issue_instance() get_issue_list.return_value = [issue_a, issue_b] self.manager.metrics.data['_totals'] = {'loc': 1000, 'nosec': 50} for category in ['SEVERITY', 'CONFIDENCE']: for level in ['UNDEFINED', 'LOW', 'MEDIUM', 'HIGH']: self.manager.metrics.data['_totals']['%s.%s' % (category, level)] = 1 # Validate that we're outputting the correct issues output_str_fn = 'bandit.formatters.text._output_issue_str' with mock.patch(output_str_fn) as output_str: output_str.return_value = 'ISSUE_OUTPUT_TEXT' tmp_file = open(self.tmp_fname, 'w') b_text.report(self.manager, tmp_file, bandit.LOW, bandit.LOW, lines=5) calls = [mock.call(issue_a, '', lines=5), mock.call(issue_b, '', lines=5)] output_str.assert_has_calls(calls, any_order=True) # Validate that we're outputting all of the expected fields and the # correct values tmp_file = open(self.tmp_fname, 'w') b_text.report(self.manager, tmp_file, bandit.LOW, bandit.LOW, lines=5) with open(self.tmp_fname) as f: data = f.read() expected_items = ['Run started', 'Files in scope (1)', 'binding.py (score: ', "CONFIDENCE: 1", "SEVERITY: 1", 'Files excluded (1):', 'def.py', 'Undefined: 1', 'Low: 1', 'Medium: 1', 'High: 1', 'Total lines skipped ', '(#nosec): 50', 'Total issues (by severity)', 'Total issues (by confidence)', 'Files skipped (1)', 'abc.py (File is bad)' ] for item in expected_items: self.assertIn(item, data) @mock.patch('bandit.core.manager.BanditManager.get_issue_list') def test_report_baseline(self, get_issue_list): conf = config.BanditConfig() self.manager = manager.BanditManager(conf, 'file') (tmp_fd, self.tmp_fname) = tempfile.mkstemp() self.manager.out_file = self.tmp_fname issue_a = _get_issue_instance() issue_b = _get_issue_instance() issue_x = _get_issue_instance() issue_x.fname = 'x' issue_y = _get_issue_instance() issue_y.fname = 'y' issue_z = _get_issue_instance() issue_z.fname = 'z' get_issue_list.return_value = collections.OrderedDict( [(issue_a, [issue_x]), (issue_b, [issue_y, issue_z])]) # Validate that we're outputting the correct issues indent_val = ' ' * 10 output_str_fn = 'bandit.formatters.text._output_issue_str' with mock.patch(output_str_fn) as output_str: output_str.return_value = 'ISSUE_OUTPUT_TEXT' tmp_file = open(self.tmp_fname, 'w') b_text.report(self.manager, tmp_file, bandit.LOW, bandit.LOW, lines=5) calls = [mock.call(issue_a, '', lines=5), mock.call(issue_b, '', show_code=False, show_lineno=False), mock.call(issue_y, indent_val, lines=5), mock.call(issue_z, indent_val, lines=5)] output_str.assert_has_calls(calls, any_order=True) def _get_issue_instance(severity=bandit.MEDIUM, confidence=bandit.MEDIUM): new_issue = issue.Issue(severity, confidence, 'Test issue') new_issue.fname = 'code.py' new_issue.test = 'bandit_plugin' new_issue.lineno = 1 return new_issue bandit-1.4.0/tests/unit/formatters/test_xml.py000066400000000000000000000060731303375232700215100ustar00rootroot00000000000000# Copyright (c) 2015 VMware, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections import tempfile from xml.etree import cElementTree as ET import six import testtools import bandit from bandit.core import config from bandit.core import issue from bandit.core import manager from bandit.formatters import xml as b_xml class XmlFormatterTests(testtools.TestCase): def setUp(self): super(XmlFormatterTests, self).setUp() conf = config.BanditConfig() self.manager = manager.BanditManager(conf, 'file') (tmp_fd, self.tmp_fname) = tempfile.mkstemp() self.context = {'filename': self.tmp_fname, 'lineno': 4, 'linerange': [4]} self.check_name = 'hardcoded_bind_all_interfaces' self.issue = issue.Issue(bandit.MEDIUM, bandit.MEDIUM, 'Possible binding to all interfaces.') self.manager.out_file = self.tmp_fname self.issue.fname = self.context['filename'] self.issue.lineno = self.context['lineno'] self.issue.linerange = self.context['linerange'] self.issue.test = self.check_name self.manager.results.append(self.issue) def _xml_to_dict(self, t): d = {t.tag: {} if t.attrib else None} children = list(t) if children: dd = collections.defaultdict(list) for dc in map(self._xml_to_dict, children): for k, v in six.iteritems(dc): dd[k].append(v) d = {t.tag: {k: v[0] if len(v) == 1 else v for k, v in six.iteritems(dd)}} if t.attrib: d[t.tag].update(('@' + k, v) for k, v in six.iteritems(t.attrib)) if t.text: text = t.text.strip() if children or t.attrib: if text: d[t.tag]['#text'] = text else: d[t.tag] = text return d def test_report(self): tmp_file = open(self.tmp_fname, 'wb') b_xml.report(self.manager, tmp_file, self.issue.severity, self.issue.confidence) with open(self.tmp_fname) as f: data = self._xml_to_dict(ET.XML(f.read())) self.assertEqual(self.tmp_fname, data['testsuite']['testcase']['@classname']) self.assertEqual( self.issue.text, data['testsuite']['testcase']['error']['@message']) self.assertEqual(self.check_name, data['testsuite']['testcase']['@name']) bandit-1.4.0/tools/000077500000000000000000000000001303375232700141225ustar00rootroot00000000000000bandit-1.4.0/tools/openstack_coverage.py000077500000000000000000000214341303375232700203450ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright 2015 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tool for reporting Bandit coverage over OpenStack. Intended for execution against specific Jenkins and Zuul configuration files within the openstack-infra/project-config repository. Parses out Bandit jobs and tests as defined within these configurations. Prints the summary of results. If the '-t' (test) option is provided, this tool will attempt to git clone any project that defines a Bandit job. Once cloned, it will use tox to run the defined Bandit job and capture logs for any failures. TODO: Add detection / handling of bandit.yaml for each project. TODO: Deal with different branch definitions in the Zuul layout.yaml. """ import argparse import datetime import os import requests import subprocess import yaml BASE_URL = "https://git.openstack.org/cgit/" GIT_BASE = "https://git.openstack.org/" PATH_INFRA = "openstack-infra/project-config/plain/" PATH_JENKINS = "jenkins/jobs/projects.yaml" PATH_PROJECT_LIST = "openstack/governance/plain/reference/projects.yaml" PATH_ZUUL = "zuul/layout.yaml" TITLE = "OpenStack Bandit Coverage Report -- {0} UTC".format( datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S') ) TEST_TYPES = ['experimental', 'check', 'gate'] def get_yaml(url): r = requests.get(url) if r.status_code == 200: data = yaml.load(r.content) return(data) raise SystemError( "Could not obtain valid YAML from specified source ({0})" .format(url) ) def list_projects(conf_jenkins): data = get_yaml("{0}{1}{2}".format(BASE_URL, PATH_INFRA, conf_jenkins)) # parse data bandit_projects = [] for project in data: project_name = project['project']['name'] project_jobs = project['project']['jobs'] for job in project_jobs: if type(job) == dict and 'gate-{name}-tox-{envlist}' in job: if 'bandit' in job['gate-{name}-tox-{envlist}']['envlist']: bandit_projects.append(project_name) # output results print("Bandit jobs have been defined in the following OpenStack projects:") for project in sorted(bandit_projects): print(" - {0}".format(project)) print("\n(Configuration from {0}{1}{2})\n".format( BASE_URL, PATH_INFRA, conf_jenkins )) return bandit_projects def coverage_zuul(conf_zuul): data = get_yaml("{0}{1}{2}".format(BASE_URL, PATH_INFRA, conf_zuul)) # parse data bandit_jobs = {} bandit_tests = {key: set() for key in TEST_TYPES} for job in data['jobs']: if 'bandit' in job['name']: if job.get('voting', True) is False: bandit_jobs[job['name']] = False else: bandit_jobs[job['name']] = True for project in data['projects']: project_name = project['name'] for test_type in bandit_tests.keys(): for test in project.get(test_type, []): if str(test).endswith('bandit'): voting = bandit_jobs.get(test, False) bandit_tests[test_type].add((project_name, voting)) # output results for test_type in bandit_tests: print( "\n{0} tests exist for the following OpenStack projects:" .format(test_type.capitalize()) ) for project in sorted(bandit_tests[test_type]): if project[1] is False: print(" - {0}".format(project[0])) else: print(" - {0} (VOTING)".format(project[0])) print("\n(Configuration from {0}{1}{2})\n".format( BASE_URL, PATH_INFRA, conf_zuul )) def _print_title(): print("{0}\n{1}\n{0}\n".format( "=" * len(TITLE), TITLE, "=" * len(TITLE) )) def _parse_args(): parser = argparse.ArgumentParser() parser.add_argument('-t', '--test', dest='do_test', action='store_true', help='Test upstream project Bandit gates. This will ' 'clone each upstream project, run Bandit as ' 'configured in the tox environment, display pass ' 'status, and save output.') parser.set_defaults(do_test=False) return parser.parse_args() def _get_repo_names(project_list): # take a list of project names, like ['anchor', 'barbican'], get the # corresponding repos for each. Return a dictionary with the project # as the key and the repo as the value. project_repos = {key: None for key in project_list} yaml_data = get_yaml("{0}{1}".format(BASE_URL, PATH_PROJECT_LIST)) for project in yaml_data: try: # if one of the projects we're looking for is listed as a # deliverable for this project, look for the first listed repo # for that deliverable for deliverable in yaml_data[project]['deliverables']: if deliverable in project_list: # the deliverable name is the project we're looking for, # store the listed repo name for it project_repos[deliverable] = (yaml_data[project] ['deliverables'] [deliverable]['repos'][0]) except (KeyError, IndexError): # improperly formatted entry, keep going pass return project_repos def clone_projects(project_list): # clone all of the projects, return the directory name they are cloned in project_locations = _get_repo_names(project_list) orig_dir = os.path.abspath(os.getcwd()) # create directory for projects try: dir_name = 'project-source-{}'.format(datetime.datetime.utcnow(). strftime('%Y-%m-%d-%H-%M-%S')) os.mkdir(dir_name) os.chdir(dir_name) except OSError: print("Unable to create directory for cloning projects") return None for project in project_locations: print '=' * len(TITLE) print("Cloning project: {} from repo {} into {}". format(project, project_locations[project], dir_name)) try: subprocess.check_call(['git', 'clone', GIT_BASE + project_locations[project]]) except subprocess.CalledProcessError: print("Unable to clone project from repo: {}". format(project_locations[project])) os.chdir(orig_dir) return os.path.abspath(dir_name) def run_bandit(source_dir): # go through each source directory in the directory which contains source, # run Bandit with the established tox job, save results orig_dir = os.path.abspath(os.getcwd()) try: fail_results_dir = os.path.abspath('fail_results') os.mkdir(fail_results_dir) except OSError: print("Unable to make results directory") os.chdir(source_dir) run_success = {} for d in os.listdir(os.getcwd()): os.chdir(d) print '=' * len(TITLE) print 'Running tox Bandit in directory {}'.format(d) try: subprocess.check_output(['tox', '-e', 'bandit'], stderr=subprocess.STDOUT) except subprocess.CalledProcessError as exc: run_success[d] = False # write log containing the process output fail_log_path = fail_results_dir + '/' + d with open(fail_log_path, 'w') as f: f.write(exc.output) print("Bandit tox failed, wrote failure log to {}". format(fail_log_path)) else: run_success[d] = True os.chdir(source_dir) os.chdir(orig_dir) return run_success def main(): _print_title() args = _parse_args() project_list = list_projects(PATH_JENKINS) coverage_zuul(PATH_ZUUL) print("=" * len(TITLE)) if args.do_test: source_dir = clone_projects(project_list) if source_dir: results = run_bandit(source_dir) # output results table print "-" * 50 print "{:40s}{:10s}".format("Project", "Passed") print "-" * 50 for project in results: print "{:40s}{:10s}".format(project, str(results[project])) if __name__ == "__main__": main() bandit-1.4.0/tools/tox_install.sh000077500000000000000000000020361303375232700170220ustar00rootroot00000000000000#!/usr/bin/env bash # Client constraint file contains this client version pin that is in conflict # with installing the client from source. We should remove the version pin in # the constraints file before applying it for from-source installation. CONSTRAINTS_FILE="$1" shift 1 set -e # NOTE(tonyb): Place this in the tox enviroment's log dir so it will get # published to logs.openstack.org for easy debugging. localfile="$VIRTUAL_ENV/log/upper-constraints.txt" if [[ "$CONSTRAINTS_FILE" != http* ]]; then CONSTRAINTS_FILE="file://$CONSTRAINTS_FILE" fi # NOTE(tonyb): need to add curl to bindep.txt if the project supports bindep curl "$CONSTRAINTS_FILE" --insecure --progress-bar --output "$localfile" pip install -c"$localfile" openstack-requirements # This is the main purpose of the script: Allow local installation of # the current repo. It is listed in constraints file and thus any # install will be constrained and we need to unconstrain it. edit-constraints "$localfile" -- "$CLIENT_NAME" pip install -c"$localfile" -U "$@" exit $? bandit-1.4.0/tox.ini000066400000000000000000000040161303375232700142760ustar00rootroot00000000000000[tox] minversion = 2.0 envlist = py35,py27,pep8 skipsdist = True [testenv] usedevelop = True install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} setenv = VIRTUAL_ENV={envdir} BRANCH_NAME=master CLIENT_NAME=bandit VIRTUAL_ENV={envdir} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = coverage erase python setup.py testr --coverage --slowest --testr-args='{posargs}' coverage report -m passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY [testenv:debug] commands = oslo_debug_helper -t tests {posargs} [testenv:linters] usedevelop = False deps = {[testenv:pep8]deps} commands = flake8 {posargs} bandit flake8 {posargs} tests bandit-baseline -r bandit -ll -ii [testenv:pep8] usedevelop = False deps = {[testenv]deps} . commands = flake8 {posargs} bandit flake8 {posargs} tests {[testenv:pylint]commands} bandit-baseline -r bandit -ll -ii [testenv:venv] commands = {posargs} [testenv:codesec] usedevelop = False deps = {[testenv]deps} . commands = bandit-baseline -r bandit -ll -ii [testenv:cover] deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = python setup.py testr --coverage --testr-args='{posargs}' [testenv:openstack_coverage] deps = PyYAML>=3.1.0 requests>=2.7.0 commands = python tools/openstack_coverage.py [testenv:integration] passenv = REPO_ROOT whitelist_externals = bash commands = bash scripts/integration-test.sh {posargs} [testenv:docs] commands= python setup.py build_sphinx [flake8] show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,releasenotes [testenv:releasenotes] commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [testenv:pylint] commands = pylint --rcfile=pylintrc bandit