Repository: PyCQA/bandit Branch: main Commit: b46fa3a27236 Files: 291 Total size: 778.0 KB Directory structure: gitextract_siphom_i/ ├── .github/ │ ├── CODEOWNERS │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── Feature_request.md │ │ ├── bug-report.yml │ │ └── config.yml │ ├── dependabot.yml │ └── workflows/ │ ├── build-publish-image.yml │ ├── dependency-review.yml │ ├── publish-to-pypi.yml │ ├── publish-to-test-pypi.yml │ └── pythonpackage.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pre-commit-hooks.yaml ├── .readthedocs.yaml ├── .stestr.conf ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.rst ├── SECURITY.md ├── bandit/ │ ├── __init__.py │ ├── __main__.py │ ├── blacklists/ │ │ ├── __init__.py │ │ ├── calls.py │ │ ├── imports.py │ │ └── utils.py │ ├── cli/ │ │ ├── __init__.py │ │ ├── baseline.py │ │ ├── config_generator.py │ │ └── main.py │ ├── core/ │ │ ├── __init__.py │ │ ├── blacklisting.py │ │ ├── config.py │ │ ├── constants.py │ │ ├── context.py │ │ ├── docs_utils.py │ │ ├── extension_loader.py │ │ ├── issue.py │ │ ├── manager.py │ │ ├── meta_ast.py │ │ ├── metrics.py │ │ ├── node_visitor.py │ │ ├── test_properties.py │ │ ├── test_set.py │ │ ├── tester.py │ │ └── utils.py │ ├── formatters/ │ │ ├── __init__.py │ │ ├── csv.py │ │ ├── custom.py │ │ ├── html.py │ │ ├── json.py │ │ ├── sarif.py │ │ ├── screen.py │ │ ├── text.py │ │ ├── utils.py │ │ ├── xml.py │ │ └── yaml.py │ └── plugins/ │ ├── __init__.py │ ├── app_debug.py │ ├── asserts.py │ ├── crypto_request_no_cert_validation.py │ ├── django_sql_injection.py │ ├── django_xss.py │ ├── exec.py │ ├── general_bad_file_permissions.py │ ├── general_bind_all_interfaces.py │ ├── general_hardcoded_password.py │ ├── general_hardcoded_tmp.py │ ├── hashlib_insecure_functions.py │ ├── huggingface_unsafe_download.py │ ├── injection_paramiko.py │ ├── injection_shell.py │ ├── injection_sql.py │ ├── injection_wildcard.py │ ├── insecure_ssl_tls.py │ ├── jinja2_templates.py │ ├── logging_config_insecure_listen.py │ ├── mako_templates.py │ ├── markupsafe_markup_xss.py │ ├── pytorch_load.py │ ├── request_without_timeout.py │ ├── snmp_security_check.py │ ├── ssh_no_host_key_verification.py │ ├── tarfile_unsafe_members.py │ ├── trojansource.py │ ├── try_except_continue.py │ ├── try_except_pass.py │ ├── weak_cryptographic_key.py │ └── yaml_load.py ├── doc/ │ ├── requirements.txt │ └── source/ │ ├── blacklists/ │ │ ├── blacklist_calls.rst │ │ ├── blacklist_imports.rst │ │ └── index.rst │ ├── ci-cd/ │ │ ├── github-actions.rst │ │ └── index.rst │ ├── conf.py │ ├── config.rst │ ├── faq.rst │ ├── formatters/ │ │ ├── csv.rst │ │ ├── custom.rst │ │ ├── html.rst │ │ ├── index.rst │ │ ├── json.rst │ │ ├── sarif.rst │ │ ├── screen.rst │ │ ├── text.rst │ │ ├── xml.rst │ │ └── yaml.rst │ ├── index.rst │ ├── integrations.rst │ ├── man/ │ │ └── bandit.rst │ ├── plugins/ │ │ ├── b101_assert_used.rst │ │ ├── b102_exec_used.rst │ │ ├── b103_set_bad_file_permissions.rst │ │ ├── b104_hardcoded_bind_all_interfaces.rst │ │ ├── b105_hardcoded_password_string.rst │ │ ├── b106_hardcoded_password_funcarg.rst │ │ ├── b107_hardcoded_password_default.rst │ │ ├── b108_hardcoded_tmp_directory.rst │ │ ├── b109_password_config_option_not_marked_secret.rst │ │ ├── b110_try_except_pass.rst │ │ ├── b111_execute_with_run_as_root_equals_true.rst │ │ ├── b112_try_except_continue.rst │ │ ├── b113_request_without_timeout.rst │ │ ├── b201_flask_debug_true.rst │ │ ├── b202_tarfile_unsafe_members.rst │ │ ├── b324_hashlib.rst │ │ ├── b501_request_with_no_cert_validation.rst │ │ ├── b502_ssl_with_bad_version.rst │ │ ├── b503_ssl_with_bad_defaults.rst │ │ ├── b504_ssl_with_no_version.rst │ │ ├── b505_weak_cryptographic_key.rst │ │ ├── b506_yaml_load.rst │ │ ├── b507_ssh_no_host_key_verification.rst │ │ ├── b508_snmp_insecure_version.rst │ │ ├── b509_snmp_weak_cryptography.rst │ │ ├── b601_paramiko_calls.rst │ │ ├── b602_subprocess_popen_with_shell_equals_true.rst │ │ ├── b603_subprocess_without_shell_equals_true.rst │ │ ├── b604_any_other_function_with_shell_equals_true.rst │ │ ├── b605_start_process_with_a_shell.rst │ │ ├── b606_start_process_with_no_shell.rst │ │ ├── b607_start_process_with_partial_path.rst │ │ ├── b608_hardcoded_sql_expressions.rst │ │ ├── b609_linux_commands_wildcard_injection.rst │ │ ├── b610_django_extra_used.rst │ │ ├── b611_django_rawsql_used.rst │ │ ├── b612_logging_config_insecure_listen.rst │ │ ├── b613_trojansource.rst │ │ ├── b614_pytorch_load.rst │ │ ├── b615_huggingface_unsafe_download.rst │ │ ├── b701_jinja2_autoescape_false.rst │ │ ├── b702_use_of_mako_templates.rst │ │ ├── b703_django_mark_safe.rst │ │ ├── b704_markupsafe_markup_xss.rst │ │ └── index.rst │ └── start.rst ├── docker/ │ └── Dockerfile ├── examples/ │ ├── __init__.py │ ├── assert.py │ ├── binding.py │ ├── cipher-modes.py │ ├── ciphers.py │ ├── crypto-md5.py │ ├── dill.py │ ├── django_sql_injection_extra.py │ ├── django_sql_injection_raw.py │ ├── eval.py │ ├── exec.py │ ├── flask_debug.py │ ├── ftplib.py │ ├── hardcoded-passwords.py │ ├── hardcoded-tmp.py │ ├── hashlib_new_insecure_functions.py │ ├── httpoxy_cgihandler.py │ ├── httpoxy_twisted_directory.py │ ├── httpoxy_twisted_script.py │ ├── huggingface_unsafe_download.py │ ├── imports-aliases.py │ ├── imports-from.py │ ├── imports-function.py │ ├── imports-with-importlib.py │ ├── imports.py │ ├── init-py-test/ │ │ ├── __init__.py │ │ └── subdirectory-okay.py │ ├── jinja2_templating.py │ ├── jsonpickle.py │ ├── logging_config_insecure_listen.py │ ├── long_set.py │ ├── mako_templating.py │ ├── mark_safe.py │ ├── mark_safe_insecure.py │ ├── mark_safe_secure.py │ ├── markupsafe_markup_xss.py │ ├── markupsafe_markup_xss_allowed_calls.py │ ├── markupsafe_markup_xss_extend_markup_names.py │ ├── marshal_deserialize.py │ ├── mktemp.py │ ├── multiline_statement.py │ ├── new_candidates-all.py │ ├── new_candidates-none.py │ ├── new_candidates-nosec.py │ ├── new_candidates-some.py │ ├── no_host_key_verification.py │ ├── nonsense.py │ ├── nonsense2.py │ ├── nosec.py │ ├── okay.py │ ├── os-chmod.py │ ├── os-exec.py │ ├── os-popen.py │ ├── os-spawn.py │ ├── os-startfile.py │ ├── os_system.py │ ├── pandas_read_pickle.py │ ├── paramiko_injection.py │ ├── partial_path_process.py │ ├── pickle_deserialize.py │ ├── popen_wrappers.py │ ├── pycrypto.py │ ├── pycryptodome.py │ ├── pyghmi.py │ ├── pytorch_load.py │ ├── random_module.py │ ├── requests-missing-timeout.py │ ├── requests-ssl-verify-disabled.py │ ├── shelve_open.py │ ├── skip.py │ ├── snmp.py │ ├── sql_multiline_statements.py │ ├── sql_statements.py │ ├── ssl-insecure-version.py │ ├── subprocess_shell.py │ ├── tarfile_extractall.py │ ├── telnetlib.py │ ├── trojansource.py │ ├── trojansource_latin1.py │ ├── try_except_continue.py │ ├── try_except_pass.py │ ├── unverified_context.py │ ├── urlopen.py │ ├── weak_cryptographic_key_sizes.py │ ├── wildcard-injection.py │ ├── xml_etree_celementtree.py │ ├── xml_etree_elementtree.py │ ├── xml_expatbuilder.py │ ├── xml_expatreader.py │ ├── xml_minidom.py │ ├── xml_pulldom.py │ ├── xml_sax.py │ ├── xml_xmlrpc.py │ └── yaml_load.py ├── funding.json ├── pylintrc ├── requirements.txt ├── scripts/ │ └── main.py ├── setup.cfg ├── setup.py ├── test-requirements.txt ├── tests/ │ ├── __init__.py │ ├── functional/ │ │ ├── __init__.py │ │ ├── test_baseline.py │ │ ├── test_functional.py │ │ └── test_runtime.py │ └── unit/ │ ├── __init__.py │ ├── cli/ │ │ ├── __init__.py │ │ ├── test_baseline.py │ │ ├── test_config_generator.py │ │ └── test_main.py │ ├── core/ │ │ ├── __init__.py │ │ ├── test_blacklisting.py │ │ ├── test_config.py │ │ ├── test_context.py │ │ ├── test_docs_util.py │ │ ├── test_issue.py │ │ ├── test_manager.py │ │ ├── test_meta_ast.py │ │ ├── test_test_set.py │ │ └── test_util.py │ └── formatters/ │ ├── __init__.py │ ├── test_csv.py │ ├── test_custom.py │ ├── test_html.py │ ├── test_json.py │ ├── test_sarif.py │ ├── test_screen.py │ ├── test_text.py │ ├── test_xml.py │ └── test_yaml.py └── tox.ini ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CODEOWNERS ================================================ * @ericwb @lukehinds @sigmavirus24 ================================================ FILE: .github/FUNDING.yml ================================================ custom: ["https://psfmember.org/civicrm/contribute/transact/?reset=1&id=42"] github: [ericwb] tidelift: pypi/bandit ================================================ FILE: .github/ISSUE_TEMPLATE/Feature_request.md ================================================ --- name: "\U0001F680 Feature request" about: Suggest an idea for this project title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. Love this idea? Give it a 👍. We prioritize fulfilling features with the most 👍. ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.yml ================================================ name: 🐛 Bug report description: Create a report to help us improve labels: bug body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! - type: textarea id: describe-bug attributes: label: Describe the bug description: A clear and concise description of what the bug is. validations: required: true - type: textarea id: reproduction-steps attributes: label: Reproduction steps description: Steps to reproduce the behavior value: | 1. 2. 3. ... render: bash validations: required: true - type: textarea id: expected-behavior attributes: label: Expected behavior description: A clear and concise description of what you expected to happen. validations: required: true - type: dropdown id: bandit-version attributes: label: Bandit version description: Run "bandit --version" if unsure of version number options: - 1.9.1 (Default) - 1.9.0 - 1.8.6 - 1.8.5 - 1.8.4 - 1.8.3 - 1.8.2 - 1.8.1 - 1.8.0 - 1.7.10 - 1.7.9 - 1.7.8 - 1.7.7 - 1.7.6 validations: required: true - type: dropdown id: python-version attributes: label: Python version description: Run "bandit --version" if unsure of version number options: - "3.14 (Default)" - "3.13" - "3.12" - "3.11" - "3.10" - "3.9" validations: required: true - type: textarea id: additional-context attributes: label: Additional context description: Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: ❓ Ask a question url: https://github.com/PyCQA/bandit/discussions about: Please post questions in discussions. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" ================================================ FILE: .github/workflows/build-publish-image.yml ================================================ name: Build and Publish Bandit Images on: release: types: [created] schedule: - cron: '0 0 * * 0' # Every Sunday at midnight workflow_dispatch: jobs: build-and-publish: runs-on: ubuntu-latest permissions: contents: read packages: write id-token: write steps: - name: Get latest release tag if: github.event_name != 'release' id: get-latest-tag run: | TAG=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r .tag_name) echo "Latest tag is $TAG" echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV - name: Check out the repo uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6 with: ref: ${{ github.event_name == 'release' && github.ref || env.RELEASE_TAG }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Log in to GitHub Container Registry uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Install Cosign uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 with: cosign-release: 'v2.2.2' - name: Downcase github.repository value run: | echo "IMAGE_NAME=`echo ${{github.repository}} | tr '[:upper:]' '[:lower:]'`" >>${GITHUB_ENV} - name: Build and push Docker image id: build-and-push uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: . file: ./docker/Dockerfile push: true tags: ghcr.io/${{ env.IMAGE_NAME }}/bandit:latest platforms: linux/amd64, linux/arm64, linux/arm/v7, linux/arm/v8 - name: Sign the image env: TAGS: ghcr.io/${{ env.IMAGE_NAME }}/bandit:latest DIGEST: ${{ steps.build-and-push.outputs.digest }} run: | echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} ================================================ FILE: .github/workflows/dependency-review.yml ================================================ name: 'Dependency Review' on: [pull_request] permissions: contents: read jobs: dependency-review: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6 - name: 'Dependency Review' uses: actions/dependency-review-action@v4 ================================================ FILE: .github/workflows/publish-to-pypi.yml ================================================ name: Publish to PyPI on: workflow_dispatch jobs: build-n-publish: name: Build and publish to PyPI runs-on: ubuntu-latest permissions: # IMPORTANT: this permission is mandatory for trusted publishing id-token: write steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Python 3.10 uses: actions/setup-python@v6 with: python-version: "3.10" - name: Install dependencies run: pip install tox wheel - name: Build man page if not present run: | if [ ! -f doc/build/man/bandit.1 ]; then tox run -e manpage fi - name: Build a binary wheel and a source tarball run: | python setup.py sdist bdist_wheel - name: Publish distribution to PyPI if: startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@release/v1 ================================================ FILE: .github/workflows/publish-to-test-pypi.yml ================================================ name: Publish to Test PyPI on: workflow_dispatch jobs: build-n-publish: name: Build and publish to Test PyPI runs-on: ubuntu-latest permissions: # IMPORTANT: this permission is mandatory for trusted publishing id-token: write steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Python 3.10 uses: actions/setup-python@v6 with: python-version: "3.10" - name: Install dependencies run: pip install tox wheel - name: Build man page if not present run: | if [ ! -f doc/build/man/bandit.1 ]; then tox run -e manpage fi - name: Build a binary wheel and a source tarball run: | python setup.py sdist bdist_wheel - name: Publish distribution to Test PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ ================================================ FILE: .github/workflows/pythonpackage.yml ================================================ name: Build and Test Bandit on: [push, pull_request] jobs: format: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.10"] steps: - name: Checkout repository uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install tox - name: Run tox run: tox run -e format pep8: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.10"] steps: - name: Checkout repository uses: actions/checkout@v6 with: fetch-depth: 2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install tox - name: Run tox run: tox run -e pep8 tests: strategy: matrix: python-version: [ ["3.10", "310"], ["3.11", "311"], ["3.12", "312"], ["3.13", "313"], ["3.14", "314"], ] os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} name: ${{ matrix.os }} (${{ matrix.python-version[0] }}) steps: - name: Checkout repository uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version[0] }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version[0] }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install tox - name: Run tox run: tox run -e py${{ matrix.python-version[1] }} ================================================ FILE: .gitignore ================================================ env* venv* .python-version *.pyc .DS_Store *.egg *.egg-info .eggs/ .idea/ .vscode/ .tox .stestr build/* cover/* .coverage* doc/build/* ChangeLog doc/source/api .*.sw? AUTHORS ================================================ FILE: .pre-commit-config.yaml ================================================ exclude: ^(examples|tools|doc) repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: check-yaml - id: debug-statements - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/asottile/reorder-python-imports rev: v3.16.0 hooks: - id: reorder-python-imports args: [--application-directories, '.:src', --py38-plus] - repo: https://github.com/psf/black-pre-commit-mirror rev: 25.12.0 hooks: - id: black args: [--line-length=79, --target-version=py38] - repo: https://github.com/asottile/pyupgrade rev: v3.21.2 hooks: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/jorisroovers/gitlint rev: v0.19.1 hooks: - id: gitlint #- repo: https://github.com/pre-commit/mirrors-mypy # rev: v0.910-1 # hooks: # - id: mypy # exclude: ^(docs/|example-plugin/) ================================================ FILE: .pre-commit-hooks.yaml ================================================ - id: bandit name: bandit description: 'Bandit is a tool for finding common security issues in Python code' entry: bandit language: python language_version: python3 types: [python] require_serial: true ================================================ FILE: .readthedocs.yaml ================================================ version: 2 build: os: ubuntu-lts-latest tools: python: "3.10" sphinx: configuration: doc/source/conf.py python: install: - requirements: requirements.txt - requirements: doc/requirements.txt - method: pip path: . extra_requirements: - sarif ================================================ FILE: .stestr.conf ================================================ [DEFAULT] test_path=./tests top_dir=./ parallel_class=True ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at Ian Stapleton Cordasco , Ian Lee or Florian Bruhin . All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Bandit Thanks for considering to take part in the improvement of the Bandit project. Contributions are always welcome! Here are guidelines and rules that can be helpful if you plan to want to get involved in the project. #### Table Of Contents [Code of Conduct](#code-of-conduct) [How Can I Contribute?](#how-can-i-contribute) * [Reporting Bugs](#reporting-bugs) * [Suggesting Enhancements](#suggesting-enhancements) * [Your First Code Contribution](#your-first-code-contribution) * [Pull Requests](#pull-requests) * [Commit Message Guidelines](#commit-message-guidelines) * [Squash Commits](#squash-commits) * [Things You Should Know Before Getting Started](#things-you-should-know-before-getting-started) * [Vulnerability Tests](#vulnerability-tests) * [Writing Tests](#writing-tests) * [Extending Bandit](#extending-bandit) ## Code of Conduct Everyone who participates in this project is governed by the PyCQA [Code of Conduct](https://github.com/PyCQA/bandit/blob/main/CODE_OF_CONDUCT.md#contributor-covenant-code-of-conduct). ## Reporting Bugs If you encounter a bug, please let us know about it. See the guide here [GitHub issues](https://guides.github.com/features/issues/). **Before submitting a new issue** you might want to check for an [existing issue](https://github.com/PyCQA/bandit/issues) to know if there is already a reported issue. If an issue is already open please feel free to add a comment to the existing issue instead of creating a new one. ### Submitting your first issue We encourage using the issue template to improve quality of reported issues. Navigate to the issues tab and select `New issue`, then select the **Bug report** template and fill out the form. To submit a good bug report keep in mind to: * Use a descriptive title so other people can understand what the issue is about. * Be specific about the details, for example, what command did you use, what version of Bandit did you use, and in what environment you observed the bug (CI or development). ## Suggesting Enhancements If you want to suggest an enhancement, open a new issue and use the **Feature request** template. **Before submitting an enhancement** please check for existing [feature requests](https://github.com/PyCQA/bandit/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement). Useful things to point out in your feature request: * Explain your feature request in a way that everyone can understand * Please try to explain how this feature will improve the Bandit project ## Your First Code Contribution You can start contributing to Bandit project by picking [bug issues](https://github.com/PyCQA/bandit/issues?q=is%3Aopen+is%3Aissue+label%3Abug) These issues can be easier to resolve rather than a feature request and can get you up and running with the code base. ## Pull Requests The best way to get started with Bandit is to grab the source: Fork the repository into one with your username ```shell script git clone https://github.com//bandit.git ``` Create you own branch to start writing code: ```shell script git switch -c mybranch git add git commit -S git push origin mybranch ``` You can test any changes with tox: ```shell script pip install tox tox run -e pep8 tox run -e format tox run -e py310 tox run -e docs tox run -e cover ``` If everything is done, proceed with [opening a new pull request](https://help.github.com/en/desktop/contributing-to-projects/creating-a-pull-request) ### Commit Message Guidelines We follow the commit formatting recommendations found on [Chris Beams' How to Write a Git Commit Message article](https://chris.beams.io/posts/git-commit/). Well formed commit messages not only help reviewers understand the nature of the Pull Request, but also assists the release process where commit messages are used to generate release notes. A good example of a commit message would be as follows: ``` Summarize changes in around 50 characters or less More detailed explanatory text, if necessary. Wrap it to about 72 characters or so. In some contexts, the first line is treated as the subject of the commit and the rest of the text as the body. The blank line separating the summary from the body is critical (unless you omit the body entirely); various tools like `log`, `shortlog` and `rebase` can get confused if you run the two together. Explain the problem that this commit is solving. Focus on why you are making this change as opposed to how (the code explains that). Are there side effects or other unintuitive consequences of this change? Here's the place to explain them. Further paragraphs come after blank lines. - Bullet points are okay, too - Typically a hyphen or asterisk is used for the bullet, preceded by a single space, with blank lines in between, but conventions vary here If you use an issue tracker, put references to them at the bottom, like this: Resolves: #123 See also: #456, #789 ``` Note the `Resolves #123` tag, this references the issue raised and allows us to ensure issues are associated and closed when a pull request is merged. Please refer to [the github help page on message types](https://help.github.com/articles/closing-issues-using-keywords/) for a complete list of issue references. ### Squash Commits Should your pull request consist of more than one commit (perhaps due to a change being requested during the review cycle), please perform a git squash once a reviewer has approved your pull request. A squash can be performed as follows. Let's say you have the following commits: initial commit second commit final commit Run the command below with the number set to the total commits you wish to squash (in our case 3 commits): git rebase -i HEAD~3 You default text editor will then open up and you will see the following:: pick eb36612 initial commit pick 9ac8968 second commit pick a760569 final commit # Rebase eb1429f..a760569 onto eb1429f (3 commands) We want to rebase on top of our first commit, so we change the other two commits to `squash`: pick eb36612 initial commit squash 9ac8968 second commit squash a760569 final commit After this, should you wish to update your commit message to better summarise all of your pull request, run: git commit --amend You will then need to force push (assuming your initial commit(s) were posted to github): git push origin your-branch --force ## Things You Should Know Before Getting Started ### 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 managed in the ``Manager`` and aggregated for output at the completion of a test run through the method `output_result` from ``Manager`` instance. ### 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 5 things: - `manager`: an instance of `bandit manager` - `fileobj`: the output file object, which may be sys.stdout - `sev_level` : Filtering severity level - `conf_level`: Filtering confidence level - `lines=-1`: number of lines to report 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 ## Creating and Publishing a Release (Maintainers) ### Create the GitHub Release 1. Navigate to the [Releases](https://github.com/PyCQA/bandit/releases) page 2. Click on `Draft a new release` 3. Under `Choose a tag` enter a new release version (typically increment the patch number) and select `Create new tag: on publish` 4. Click on `Generate release notes` 5. Click on `Publish release` ### Publish the Release to Test PyPI 1. Go to `Actions` tab 2. Click on the `Publish to Test PyPI` action 3. Click on `Run workflow` 4. Select `Use workflow from`, then `Tags` tab, and select `` 5. Click on `Run workflow` ### Publish the Release to PyPI 1. Go to `Actions` tab 2. Click on the `Publish to PyPI` action 3. Click on `Run workflow` 4. Select `Use workflow from`, then `Tags` tab, and select `` 5. Click on `Run workflow` ================================================ FILE: LICENSE ================================================ 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. ================================================ FILE: README.rst ================================================ .. image:: https://raw.githubusercontent.com/pycqa/bandit/main/logo/logotype-sm.png :alt: Bandit ====== .. image:: https://github.com/PyCQA/bandit/actions/workflows/pythonpackage.yml/badge.svg?branch=main :target: https://github.com/PyCQA/bandit/actions?query=workflow%3A%22Build+and+Test+Bandit%22+branch%3Amain :alt: Build Status .. image:: https://readthedocs.org/projects/bandit/badge/?version=latest :target: https://readthedocs.org/projects/bandit/ :alt: Docs Status .. image:: https://img.shields.io/pypi/v/bandit.svg :target: https://pypi.org/project/bandit/ :alt: Latest Version .. image:: https://img.shields.io/pypi/pyversions/bandit.svg :target: https://pypi.org/project/bandit/ :alt: Python Versions .. image:: https://img.shields.io/pypi/format/bandit.svg :target: https://pypi.org/project/bandit/ :alt: Format .. image:: https://img.shields.io/badge/license-Apache%202-blue.svg :target: https://github.com/PyCQA/bandit/blob/main/LICENSE :alt: License .. image:: https://img.shields.io/discord/825463413634891776.svg :target: https://discord.gg/qYxpadCgkx :alt: Discord A security linter from PyCQA * Free software: Apache license * Documentation: https://bandit.readthedocs.io/en/latest/ * Source: https://github.com/PyCQA/bandit * Bugs: https://github.com/PyCQA/bandit/issues * Contributing: https://github.com/PyCQA/bandit/blob/main/CONTRIBUTING.md 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. Bandit was originally developed within the OpenStack Security Project and later rehomed to PyCQA. .. image:: https://raw.githubusercontent.com/pycqa/bandit/main/bandit-terminal.png :alt: Bandit Example Screen Shot Show Your Style --------------- .. image:: https://img.shields.io/badge/security-bandit-yellow.svg :target: https://github.com/PyCQA/bandit :alt: Security Status Use our badge in your project's README! using Markdown:: [![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://github.com/PyCQA/bandit) using RST:: .. image:: https://img.shields.io/badge/security-bandit-yellow.svg :target: https://github.com/PyCQA/bandit :alt: Security Status References ---------- Python AST module documentation: https://docs.python.org/3/library/ast.html Green Tree Snakes - the missing Python AST docs: https://greentreesnakes.readthedocs.org/en/latest/ Documentation of the various types of AST nodes that Bandit currently covers or could be extended to cover: https://greentreesnakes.readthedocs.org/en/latest/nodes.html Container Images ---------------- Bandit is available as a container image, built within the bandit repository using GitHub Actions. The image is available on ghcr.io: .. code-block:: console docker pull ghcr.io/pycqa/bandit/bandit The image is built for the following architectures: * amd64 * arm64 * armv7 * armv8 To pull a specific architecture, use the following format: .. code-block:: console docker pull --platform= ghcr.io/pycqa/bandit/bandit:latest Every image is signed with sigstore cosign and it is possible to verify the source of origin using the following cosign command: .. code-block:: console cosign verify ghcr.io/pycqa/bandit/bandit:latest \ --certificate-identity https://github.com/pycqa/bandit/.github/workflows/build-publish-image.yml@refs/tags/ \ --certificate-oidc-issuer https://token.actions.githubusercontent.com Where `` is the release version of Bandit. Sponsors -------- The development of Bandit is made possible by the following sponsors: .. list-table:: :width: 100% :class: borderless * - .. image:: https://avatars.githubusercontent.com/u/34240465?s=200&v=4 :target: https://opensource.mercedes-benz.com/ :alt: Mercedes-Benz :width: 88 - .. image:: https://github.githubassets.com/assets/tidelift-8cea37dea8fc.svg :target: https://tidelift.com/lifter/search/pypi/bandit :alt: Tidelift :width: 88 - .. image:: https://avatars.githubusercontent.com/u/110237746?s=200&v=4 :target: https://stacklok.com/ :alt: Stacklok :width: 88 If you also ❤️ Bandit, please consider sponsoring. ================================================ FILE: SECURITY.md ================================================ # Security Policy Bandit is a tool designed to find security issues, so every effort is made that Bandit itself is also free of those issues. However, if you believe you have found a security vulnerability in this repository please open it privately via the [Report a security vulnerability](https://github.com/PyCQA/bandit/security/advisories/new) link in the Issues tab. **Please do not report security vulnerabilities through public issues, discussions, or pull requests.** Please also inform the [Tidelift security](https://tidelift.com/security). Tidelift will help coordinate the fix and disclosure. ================================================ FILE: bandit/__init__.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 from importlib import metadata 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 __author__ = metadata.metadata("bandit")["Author"] __version__ = metadata.version("bandit") ================================================ FILE: bandit/__main__.py ================================================ #!/usr/bin/env python # SPDX-License-Identifier: Apache-2.0 """Bandit is a tool designed to find common security issues in Python code. 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. Bandit was originally developed within the OpenStack Security Project and later rehomed to PyCQA. https://bandit.readthedocs.io/ """ from bandit.cli import main main.main() ================================================ FILE: bandit/blacklists/__init__.py ================================================ ================================================ FILE: bandit/blacklists/calls.py ================================================ # # Copyright 2016 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 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 encountered in the scanned code base, triggered by encountering ast.Call nodes. B301: pickle ------------ Pickle and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue. +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | B301 | pickle | - pickle.loads | Medium | | | | - pickle.load | | | | | - pickle.Unpickler | | | | | - dill.loads | | | | | - dill.load | | | | | - dill.Unpickler | | | | | - shelve.open | | | | | - shelve.DbfilenameShelf | | | | | - jsonpickle.decode | | | | | - jsonpickle.unpickler.decode | | | | | - jsonpickle.unpickler.Unpickler | | | | | - pandas.read_pickle | | +------+---------------------+------------------------------------+-----------+ 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, MD5, or SHA1 hash function. +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | B303 | md5 | - hashlib.md5 | Medium | | | | - hashlib.sha1 | | | | | - Crypto.Hash.MD2.new | | | | | - Crypto.Hash.MD4.new | | | | | - Crypto.Hash.MD5.new | | | | | - Crypto.Hash.SHA.new | | | | | - Cryptodome.Hash.MD2.new | | | | | - Cryptodome.Hash.MD4.new | | | | | - Cryptodome.Hash.MD5.new | | | | | - Cryptodome.Hash.SHA.new | | | | | - cryptography.hazmat.primitives | | | | | .hashes.MD5 | | | | | - cryptography.hazmat.primitives | | | | | .hashes.SHA1 | | +------+---------------------+------------------------------------+-----------+ 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 | | | | | - Cryptodome.Cipher.ARC2.new | | | | | - Cryptodome.Cipher.ARC4.new | | | | | - Cryptodome.Cipher.Blowfish.new | | | | | - Cryptodome.Cipher.DES.new | | | | | - Cryptodome.Cipher.XOR.new | | | | | - cryptography.hazmat.primitives | | | | | .ciphers.algorithms.ARC4 | | | | | - cryptography.hazmat.primitives | | | | | .ciphers.algorithms.Blowfish | | | | | - cryptography.hazmat.primitives | | | | | .ciphers.algorithms.IDEA | | | | | - cryptography.hazmat.primitives | | | | | .ciphers.algorithms.CAST5 | | | | | - cryptography.hazmat.primitives | | | | | .ciphers.algorithms.SEED | | | | | - cryptography.hazmat.primitives | | | | | .ciphers.algorithms.TripleDES | | +------+---------------------+------------------------------------+-----------+ | 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 --------------------- The check for this call has been removed. Use of HTTPSConnection on older versions of Python prior to 2.7.9 and 3.4.3 do 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. Consider using the secrets module instead: https://docs.python.org/library/secrets.html +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | B311 | random | - random.Random | Low | | | | - random.random | | | | | - random.randrange | | | | | - random.randint | | | | | - random.choice | | | | | - random.choices | | | | | - random.uniform | | | | | - random.triangular | | | | | - random.randbytes | | | | | - random.randrange | | | | | - random.sample | | | | | - random.getrandbits | | +------+---------------------+------------------------------------+-----------+ 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 - B319: XML ---------------- Most of this is based off of Christian Heimes' work on defusedxml: https://pypi.org/project/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 | | +------+---------------------+------------------------------------+-----------+ B320: xml_bad_etree ------------------- The check for this call has been removed. +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | 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 check for this call has been removed. 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, than using eval. On Python 2, use raw_input instead, input is safe in Python 3. +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | B322 | input | - input | High | +------+---------------------+------------------------------------+-----------+ B323: unverified_context ------------------------ By default, Python will create a secure, verified ssl context for use in such classes as HTTPSConnection. However, it still allows using an insecure context via the _create_unverified_context that reverts to the previous behavior that does not validate certificates or perform hostname checks. +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | B323 | unverified_context | - ssl._create_unverified_context | Medium | +------+---------------------+------------------------------------+-----------+ B325: tempnam -------------- The check for this call has been removed. Use of os.tempnam() and os.tmpnam() is vulnerable to symlink attacks. Consider using tmpfile() instead. For further information: https://docs.python.org/2.7/library/os.html#os.tempnam https://docs.python.org/3/whatsnew/3.0.html?highlight=tempnam https://bugs.python.org/issue17880 +------+---------------------+------------------------------------+-----------+ | ID | Name | Calls | Severity | +======+=====================+====================================+===========+ | B325 | tempnam | - os.tempnam | Medium | | | | - os.tmpnam | | +------+---------------------+------------------------------------+-----------+ """ from bandit.blacklists import utils from bandit.core import issue 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", issue.Cwe.DESERIALIZATION_OF_UNTRUSTED_DATA, [ "pickle.loads", "pickle.load", "pickle.Unpickler", "dill.loads", "dill.load", "dill.Unpickler", "shelve.open", "shelve.DbfilenameShelf", "jsonpickle.decode", "jsonpickle.unpickler.decode", "jsonpickle.unpickler.Unpickler", "pandas.read_pickle", ], "Pickle and modules that wrap it can be unsafe when used to " "deserialize untrusted data, possible security issue.", ) ) sets.append( utils.build_conf_dict( "marshal", "B302", issue.Cwe.DESERIALIZATION_OF_UNTRUSTED_DATA, ["marshal.load", "marshal.loads"], "Deserialization with the marshal module is possibly dangerous.", ) ) sets.append( utils.build_conf_dict( "md5", "B303", issue.Cwe.BROKEN_CRYPTO, [ "Crypto.Hash.MD2.new", "Crypto.Hash.MD4.new", "Crypto.Hash.MD5.new", "Crypto.Hash.SHA.new", "Cryptodome.Hash.MD2.new", "Cryptodome.Hash.MD4.new", "Cryptodome.Hash.MD5.new", "Cryptodome.Hash.SHA.new", "cryptography.hazmat.primitives.hashes.MD5", "cryptography.hazmat.primitives.hashes.SHA1", ], "Use of insecure MD2, MD4, MD5, or SHA1 hash function.", ) ) sets.append( utils.build_conf_dict( "ciphers", "B304", issue.Cwe.BROKEN_CRYPTO, [ "Crypto.Cipher.ARC2.new", "Crypto.Cipher.ARC4.new", "Crypto.Cipher.Blowfish.new", "Crypto.Cipher.DES.new", "Crypto.Cipher.XOR.new", "Cryptodome.Cipher.ARC2.new", "Cryptodome.Cipher.ARC4.new", "Cryptodome.Cipher.Blowfish.new", "Cryptodome.Cipher.DES.new", "Cryptodome.Cipher.XOR.new", "cryptography.hazmat.primitives.ciphers.algorithms.ARC4", "cryptography.hazmat.primitives.ciphers.algorithms.Blowfish", "cryptography.hazmat.primitives.ciphers.algorithms.CAST5", "cryptography.hazmat.primitives.ciphers.algorithms.IDEA", "cryptography.hazmat.primitives.ciphers.algorithms.SEED", "cryptography.hazmat.primitives.ciphers.algorithms.TripleDES", ], "Use of insecure cipher {name}. Replace with a known secure" " cipher such as AES.", "HIGH", ) ) sets.append( utils.build_conf_dict( "cipher_modes", "B305", issue.Cwe.BROKEN_CRYPTO, ["cryptography.hazmat.primitives.ciphers.modes.ECB"], "Use of insecure cipher mode {name}.", ) ) sets.append( utils.build_conf_dict( "mktemp_q", "B306", issue.Cwe.INSECURE_TEMP_FILE, ["tempfile.mktemp"], "Use of insecure and deprecated function (mktemp).", ) ) sets.append( utils.build_conf_dict( "eval", "B307", issue.Cwe.OS_COMMAND_INJECTION, ["eval"], "Use of possibly insecure function - consider using safer " "ast.literal_eval.", ) ) sets.append( utils.build_conf_dict( "mark_safe", "B308", issue.Cwe.XSS, ["django.utils.safestring.mark_safe"], "Use of mark_safe() may expose cross-site scripting " "vulnerabilities and should be reviewed.", ) ) # skipped B309 as the check for a call to httpsconnection has been removed sets.append( utils.build_conf_dict( "urllib_urlopen", "B310", issue.Cwe.PATH_TRAVERSAL, [ "urllib.request.urlopen", "urllib.request.urlretrieve", "urllib.request.URLopener", "urllib.request.FancyURLopener", "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", issue.Cwe.INSUFFICIENT_RANDOM_VALUES, [ "random.Random", "random.random", "random.randrange", "random.randint", "random.choice", "random.choices", "random.uniform", "random.triangular", "random.randbytes", "random.sample", "random.randrange", "random.getrandbits", ], "Standard pseudo-random generators are not suitable for " "security/cryptographic purposes.", "LOW", ) ) sets.append( utils.build_conf_dict( "telnetlib", "B312", issue.Cwe.CLEARTEXT_TRANSMISSION, ["telnetlib.Telnet"], "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.org/project/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", issue.Cwe.IMPROPER_INPUT_VALIDATION, [ "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", issue.Cwe.IMPROPER_INPUT_VALIDATION, [ "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", issue.Cwe.IMPROPER_INPUT_VALIDATION, ["xml.sax.expatreader.create_parser"], xml_msg, ) ) sets.append( utils.build_conf_dict( "xml_bad_expatbuilder", "B316", issue.Cwe.IMPROPER_INPUT_VALIDATION, ["xml.dom.expatbuilder.parse", "xml.dom.expatbuilder.parseString"], xml_msg, ) ) sets.append( utils.build_conf_dict( "xml_bad_sax", "B317", issue.Cwe.IMPROPER_INPUT_VALIDATION, ["xml.sax.parse", "xml.sax.parseString", "xml.sax.make_parser"], xml_msg, ) ) sets.append( utils.build_conf_dict( "xml_bad_minidom", "B318", issue.Cwe.IMPROPER_INPUT_VALIDATION, ["xml.dom.minidom.parse", "xml.dom.minidom.parseString"], xml_msg, ) ) sets.append( utils.build_conf_dict( "xml_bad_pulldom", "B319", issue.Cwe.IMPROPER_INPUT_VALIDATION, ["xml.dom.pulldom.parse", "xml.dom.pulldom.parseString"], xml_msg, ) ) # skipped B320 as the check for a call to lxml.etree has been removed # end of XML tests sets.append( utils.build_conf_dict( "ftplib", "B321", issue.Cwe.CLEARTEXT_TRANSMISSION, ["ftplib.FTP"], "FTP-related functions are being called. FTP is considered " "insecure. Use SSH/SFTP/SCP or some other encrypted protocol.", "HIGH", ) ) # skipped B322 as the check for a call to input() has been removed sets.append( utils.build_conf_dict( "unverified_context", "B323", issue.Cwe.IMPROPER_CERT_VALIDATION, ["ssl._create_unverified_context"], "By default, Python will create a secure, verified ssl context for" " use in such classes as HTTPSConnection. However, it still allows" " using an insecure context via the _create_unverified_context " "that reverts to the previous behavior that does not validate " "certificates or perform hostname checks.", ) ) # skipped B324 (used in bandit/plugins/hashlib_new_insecure_functions.py) # skipped B325 as the check for a call to os.tempnam and os.tmpnam have # been removed return {"Call": sets} ================================================ FILE: bandit/blacklists/imports.py ================================================ # # Copyright 2016 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 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.org/project/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 | import_ftplib | - ftplib | high | +------+---------------------+------------------------------------+-----------+ B403: import_pickle ------------------- Consider possible security implications associated with these modules. +------+---------------------+------------------------------------+-----------+ | ID | Name | Imports | Severity | +======+=====================+====================================+===========+ | B403 | import_pickle | - pickle | low | | | | - cPickle | | | | | - dill | | | | | - shelve | | +------+---------------------+------------------------------------+-----------+ 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 ----------------- This import blacklist has been removed. The information here has been left for historical purposes. 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 defusedxml.xmlrpc.monkey_patch() function to monkey-patch xmlrpclib and mitigate remote XML attacks. +------+---------------------+------------------------------------+-----------+ | ID | Name | Imports | Severity | +======+=====================+====================================+===========+ | B411 | import_xmlrpclib | - xmlrpc | 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 | | +------+---------------------+------------------------------------+-----------+ B413: import_pycrypto --------------------- pycrypto library is known to have publicly disclosed buffer overflow vulnerability https://github.com/dlitz/pycrypto/issues/176. It is no longer actively maintained and has been deprecated in favor of pyca/cryptography library. +------+---------------------+------------------------------------+-----------+ | ID | Name | Imports | Severity | +======+=====================+====================================+===========+ | B413 | import_pycrypto | - Crypto.Cipher | high | | | | - Crypto.Hash | | | | | - Crypto.IO | | | | | - Crypto.Protocol | | | | | - Crypto.PublicKey | | | | | - Crypto.Random | | | | | - Crypto.Signature | | | | | - Crypto.Util | | +------+---------------------+------------------------------------+-----------+ B414: import_pycryptodome ------------------------- This import blacklist has been removed. The information here has been left for historical purposes. pycryptodome is a direct fork of pycrypto that has not fully addressed the issues inherent in PyCrypto. It seems to exist, mainly, as an API compatible continuation of pycrypto and should be deprecated in favor of pyca/cryptography which has more support among the Python community. +------+---------------------+------------------------------------+-----------+ | ID | Name | Imports | Severity | +======+=====================+====================================+===========+ | B414 | import_pycryptodome | - Cryptodome.Cipher | high | | | | - Cryptodome.Hash | | | | | - Cryptodome.IO | | | | | - Cryptodome.Protocol | | | | | - Cryptodome.PublicKey | | | | | - Cryptodome.Random | | | | | - Cryptodome.Signature | | | | | - Cryptodome.Util | | +------+---------------------+------------------------------------+-----------+ B415: import_pyghmi ------------------- An IPMI-related module is being imported. IPMI is considered insecure. Use an encrypted protocol. +------+---------------------+------------------------------------+-----------+ | ID | Name | Imports | Severity | +======+=====================+====================================+===========+ | B415 | import_pyghmi | - pyghmi | high | +------+---------------------+------------------------------------+-----------+ """ from bandit.blacklists import utils from bandit.core import issue 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", issue.Cwe.CLEARTEXT_TRANSMISSION, ["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", issue.Cwe.CLEARTEXT_TRANSMISSION, ["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", issue.Cwe.DESERIALIZATION_OF_UNTRUSTED_DATA, ["pickle", "cPickle", "dill", "shelve"], "Consider possible security implications associated with " "{name} module.", "LOW", ) ) sets.append( utils.build_conf_dict( "import_subprocess", "B404", issue.Cwe.OS_COMMAND_INJECTION, ["subprocess"], "Consider possible security implications associated with the " "subprocess module.", "LOW", ) ) # Most of this is based off of Christian Heimes' work on defusedxml: # https://pypi.org/project/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." ) sets.append( utils.build_conf_dict( "import_xml_etree", "B405", issue.Cwe.IMPROPER_INPUT_VALIDATION, ["xml.etree.cElementTree", "xml.etree.ElementTree"], xml_msg, "LOW", ) ) sets.append( utils.build_conf_dict( "import_xml_sax", "B406", issue.Cwe.IMPROPER_INPUT_VALIDATION, ["xml.sax"], xml_msg, "LOW", ) ) sets.append( utils.build_conf_dict( "import_xml_expat", "B407", issue.Cwe.IMPROPER_INPUT_VALIDATION, ["xml.dom.expatbuilder"], xml_msg, "LOW", ) ) sets.append( utils.build_conf_dict( "import_xml_minidom", "B408", issue.Cwe.IMPROPER_INPUT_VALIDATION, ["xml.dom.minidom"], xml_msg, "LOW", ) ) sets.append( utils.build_conf_dict( "import_xml_pulldom", "B409", issue.Cwe.IMPROPER_INPUT_VALIDATION, ["xml.dom.pulldom"], xml_msg, "LOW", ) ) # skipped B410 as the check for import_lxml has been removed sets.append( utils.build_conf_dict( "import_xmlrpclib", "B411", issue.Cwe.IMPROPER_INPUT_VALIDATION, ["xmlrpc"], "Using {name} to parse untrusted XML data is known to be " "vulnerable to XML attacks. Use defusedxml.xmlrpc.monkey_patch() " "function to monkey-patch xmlrpclib and mitigate XML " "vulnerabilities.", "HIGH", ) ) sets.append( utils.build_conf_dict( "import_httpoxy", "B412", issue.Cwe.IMPROPER_ACCESS_CONTROL, [ "wsgiref.handlers.CGIHandler", "twisted.web.twcgi.CGIScript", "twisted.web.twcgi.CGIDirectory", ], "Consider possible security implications associated with " "{name} module.", "HIGH", ) ) sets.append( utils.build_conf_dict( "import_pycrypto", "B413", issue.Cwe.BROKEN_CRYPTO, [ "Crypto.Cipher", "Crypto.Hash", "Crypto.IO", "Crypto.Protocol", "Crypto.PublicKey", "Crypto.Random", "Crypto.Signature", "Crypto.Util", ], "The pyCrypto library and its module {name} are no longer actively" " maintained and have been deprecated. " "Consider using pyca/cryptography library.", "HIGH", ) ) sets.append( utils.build_conf_dict( "import_pyghmi", "B415", issue.Cwe.CLEARTEXT_TRANSMISSION, ["pyghmi"], "An IPMI-related module is being imported. IPMI is considered " "insecure. Use an encrypted protocol.", "HIGH", ) ) return {"Import": sets, "ImportFrom": sets, "Call": sets} ================================================ FILE: bandit/blacklists/utils.py ================================================ # # Copyright 2016 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 r"""Utils module.""" def build_conf_dict(name, bid, cwe, qualnames, message, level="MEDIUM"): """Build and return a blacklist configuration dict.""" return { "name": name, "id": bid, "cwe": cwe, "message": message, "qualnames": qualnames, "level": level, } ================================================ FILE: bandit/cli/__init__.py ================================================ ================================================ FILE: bandit/cli/baseline.py ================================================ # # Copyright 2015 Hewlett-Packard Enterprise # # SPDX-License-Identifier: Apache-2.0 # ############################################################################# # 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. # ############################################################################# """Bandit is a tool designed to find common security issues in Python code.""" import argparse import contextlib import logging import os import shutil import subprocess # nosec: B404 import sys import tempfile try: import git except ImportError: git = None 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"] """baseline.py""" def main(): """Execute Bandit.""" # 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 = f"{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) # nosec: B603 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(): """Baseline setup by creating temp folder and resetting repo.""" 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(): """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(): """Initialize arguments and output formats.""" 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.", ) if sys.version_info >= (3, 14): parser.suggest_on_error = True parser.color = False 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, _ = 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 = f"{report_basename}.{output_format}" # #################### Check Requirements ################################# if git is None: LOG.error("Git not available, reinstall with baseline extra") valid = False return (None, None, None) 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() ================================================ FILE: bandit/cli/config_generator.py ================================================ # Copyright 2015 Red Hat Inc. # # SPDX-License-Identifier: Apache-2.0 """Bandit is a tool designed to find common security issues in Python code.""" import argparse import importlib import logging import os import sys 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(): """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(): """Parse arguments.""" 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, ) if sys.version_info >= (3, 14): parser.suggest_on_error = True parser.color = False 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(): """Get configuration 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, default_flow_style=False) def main(): """Config generator to write configuration file.""" 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(f"unknown ID in skips: {skip}") for test in tests: if not extension_loader.MANAGER.check_id(test): raise RuntimeError(f"unknown ID in tests: {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 ( extension_loader.MANAGER.blacklist_by_id.items() ) ] 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 OSError: 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()) ================================================ FILE: bandit/cli/main.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 """Bandit is a tool designed to find common security issues in Python code.""" import argparse import fnmatch import logging import os import sys import textwrap 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(log_level=logging.INFO, log_format=None): """Initialize the logger. :param debug: Whether to enable debug mode :return: An instantiated logging instance """ LOG.handlers = [] 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, _, 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(default_val, arg_val, ini_val, option_name): """It's useful to show the source of each option.""" # When default value is not defined, arg_val and ini_val is deterministic if default_val is None: if arg_val: LOG.info("Using command line arg for %s", option_name) return arg_val elif ini_val: LOG.info("Using ini file for %s", option_name) return ini_val else: return None # No value passed to command line and default value is used elif default_val == arg_val: return ini_val if ini_val else arg_val # Certainly a value is passed to command line else: return arg_val 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(): """Bandit CLI.""" # bring our logging stuff up as early as possible debug = ( logging.DEBUG if "-d" in sys.argv or "--debug" in sys.argv else logging.INFO ) _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, ) if sys.version_info >= (3, 14): parser.suggest_on_error = True parser.color = False 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", ) severity_group = parser.add_mutually_exclusive_group(required=False) severity_group.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)", ) severity_group.add_argument( "--severity-level", dest="severity_string", action="store", help="report only issues of a given severity level or higher." ' "all" and "low" are likely to produce the same results, but it' " is possible for rules to be undefined which will" ' not be listed in "low".', choices=["all", "low", "medium", "high"], ) confidence_group = parser.add_mutually_exclusive_group(required=False) confidence_group.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)", ) confidence_group.add_argument( "--confidence-level", dest="confidence_string", action="store", help="report only issues of a given confidence level or higher." ' "all" and "low" are likely to produce the same results, but it' " is possible for rules to be undefined which will" ' not be listed in "low".', choices=["all", "low", "medium", "high"], ) output_format = ( "screen" if ( sys.stdout.isatty() and os.getenv("NO_COLOR") is None and os.getenv("TERM") != "dumb" ) 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( "--msg-template", action="store", default=None, help="specify output message template" " (only usable with --format custom)," " see CUSTOM FORMAT section" " for list of available values", ) parser.add_argument( "-o", "--output", dest="output_file", action="store", nargs="?", type=argparse.FileType("w", encoding="utf-8"), default=sys.stdout, help="write report to filename", ) group = parser.add_mutually_exclusive_group(required=False) group.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", ) group.add_argument( "-q", "--quiet", "--silent", dest="quiet", action="store_true", help="only show output in the case of an error", ) 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=",".join(constants.EXCLUDE), help="comma-separated list of paths (glob patterns " "supported) to exclude from scan " "(note that these are in addition to the excluded " "paths provided in the config file) (default: " + ",".join(constants.EXCLUDE) + ")", ) 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( "--exit-zero", action="store_true", dest="exit_zero", default=False, help="exit with 0, " "even with results found", ) python_ver = sys.version.replace("\n", "") parser.add_argument( "--version", action="version", version=f"%(prog)s {bandit.__version__}\n" f" python version = {python_ver}", ) parser.set_defaults(debug=False) parser.set_defaults(verbose=False) parser.set_defaults(quiet=False) parser.set_defaults(ignore_nosec=False) plugin_info = [ f"{a[0]}\t{a[1].name}" for a in extension_mgr.plugins_by_id.items() ] blacklist_info = [] for a in extension_mgr.blacklist.items(): for b in a[1]: blacklist_info.append(f"{b['id']}\t{b['name']}") plugin_list = "\n\t".join(sorted(set(plugin_info + blacklist_info))) dedent_text = textwrap.dedent( """ CUSTOM FORMATTING ----------------- Available tags: {abspath}, {relpath}, {line}, {col}, {test_id}, {severity}, {msg}, {confidence}, {range} Example usage: Default template: bandit -r examples/ --format custom --msg-template \\ "{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}" Provides same output as: bandit -r examples/ --format custom Tags can also be formatted in python string.format() style: bandit -r examples/ --format custom --msg-template \\ "{relpath:20.20s}: {line:03}: {test_id:^8}: DEFECT: {msg:>20}" See python documentation for more information about formatting style: https://docs.python.org/3/library/string.html The following tests were discovered and loaded: ----------------------------------------------- """ ) parser.epilog = dedent_text + f"\t{plugin_list}" # setup work - parse arguments, and initialize BanditManager args = parser.parse_args() # Check if `--msg-template` is not present without custom formatter if args.output_format != "custom" and args.msg_template is not None: parser.error("--msg-template can only be used with --format=custom") # Check if confidence or severity level have been specified with strings if args.severity_string is not None: if args.severity_string == "all": args.severity = 1 elif args.severity_string == "low": args.severity = 2 elif args.severity_string == "medium": args.severity = 3 elif args.severity_string == "high": args.severity = 4 # Other strings will be blocked by argparse if args.confidence_string is not None: if args.confidence_string == "all": args.confidence = 1 elif args.confidence_string == "low": args.confidence = 2 elif args.confidence_string == "medium": args.confidence = 3 elif args.confidence_string == "high": args.confidence = 4 # Other strings will be blocked by argparse # 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.config_file = _log_option_source( parser.get_default("configfile"), args.config_file, ini_options.get("configfile"), "config file", ) args.excluded_paths = _log_option_source( parser.get_default("excluded_paths"), args.excluded_paths, ini_options.get("exclude"), "excluded paths", ) args.skips = _log_option_source( parser.get_default("skips"), args.skips, ini_options.get("skips"), "skipped tests", ) args.tests = _log_option_source( parser.get_default("tests"), args.tests, ini_options.get("tests"), "selected tests", ) ini_targets = ini_options.get("targets") if ini_targets: ini_targets = ini_targets.split(",") args.targets = _log_option_source( parser.get_default("targets"), args.targets, ini_targets, "selected targets", ) # TODO(tmcpeak): any other useful options to pass from .bandit? args.recursive = _log_option_source( parser.get_default("recursive"), args.recursive, ini_options.get("recursive"), "recursive scan", ) args.agg_type = _log_option_source( parser.get_default("agg_type"), args.agg_type, ini_options.get("aggregate"), "aggregate output type", ) args.context_lines = _log_option_source( parser.get_default("context_lines"), args.context_lines, int(ini_options.get("number") or 0) or None, "max code lines output for issue", ) args.profile = _log_option_source( parser.get_default("profile"), args.profile, ini_options.get("profile"), "profile", ) args.severity = _log_option_source( parser.get_default("severity"), args.severity, ini_options.get("level"), "severity level", ) args.confidence = _log_option_source( parser.get_default("confidence"), args.confidence, ini_options.get("confidence"), "confidence level", ) args.output_format = _log_option_source( parser.get_default("output_format"), args.output_format, ini_options.get("format"), "output format", ) args.msg_template = _log_option_source( parser.get_default("msg_template"), args.msg_template, ini_options.get("msg-template"), "output message template", ) args.output_file = _log_option_source( parser.get_default("output_file"), args.output_file, ini_options.get("output"), "output file", ) args.verbose = _log_option_source( parser.get_default("verbose"), args.verbose, ini_options.get("verbose"), "output extra information", ) args.debug = _log_option_source( parser.get_default("debug"), args.debug, ini_options.get("debug"), "debug mode", ) args.quiet = _log_option_source( parser.get_default("quiet"), args.quiet, ini_options.get("quiet"), "silent mode", ) args.ignore_nosec = _log_option_source( parser.get_default("ignore_nosec"), args.ignore_nosec, ini_options.get("ignore-nosec"), "do not skip lines with # nosec", ) args.baseline = _log_option_source( parser.get_default("baseline"), args.baseline, ini_options.get("baseline"), "path of a baseline report", ) try: b_conf = b_config.BanditConfig(config_file=args.config_file) except utils.ConfigError as e: LOG.error(e) sys.exit(2) if not args.targets: parser.print_usage() sys.exit(2) # 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(log_level=logging.DEBUG, log_format=log_format) if args.quiet: _init_logger(log_level=logging.WARN) 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, quiet=args.quiet, 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 OSError: 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, args.msg_template, ) if ( b_mgr.results_count(sev_filter=sev_level, conf_filter=conf_level) > 0 and not args.exit_zero ): sys.exit(1) else: sys.exit(0) if __name__ == "__main__": main() ================================================ FILE: bandit/core/__init__.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 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 ================================================ FILE: bandit/core/blacklisting.py ================================================ # # Copyright 2016 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 import ast from bandit.core import issue def report_issue(check, name): return issue.Issue( severity=check.get("level", "MEDIUM"), confidence="HIGH", cwe=check.get("cwe", issue.Cwe.NOTSET), 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.Constant ) and isinstance(context.node.args[0].value, str): name = context.node.args[0].value else: # TODO(??): import through a variable, need symbol tab name = "UNKNOWN" else: name = "" # handle '__import__()' else: name = context.call_function_name_qual # In the case the Call is an importlib.import, treat the first # argument name as an actual import module name. # Will produce None if argument is not a literal or identifier if name in ["importlib.import_module", "importlib.__import__"]: if context.call_args_count > 0: name = context.call_args[0] else: name = context.call_keywords["name"] for check in blacklists[node_type]: for qn in check["qualnames"]: if name is not None and 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) ================================================ FILE: bandit/core/config.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 import logging import sys import yaml if sys.version_info >= (3, 11): import tomllib else: try: import tomli as tomllib except ImportError: tomllib = None from bandit.core import constants from bandit.core import extension_loader from bandit.core import utils LOG = logging.getLogger(__name__) class BanditConfig: 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, "rb") except OSError: raise utils.ConfigError( "Could not read config file.", config_file ) if config_file.endswith(".toml"): if tomllib is None: raise utils.ConfigError( "toml parser not available, reinstall with toml extra", config_file, ) try: with f: self._config = ( tomllib.load(f).get("tool", {}).get("bandit", {}) ) except tomllib.TOMLDecodeError as err: LOG.error(err) raise utils.ConfigError("Error parsing file.", config_file) else: try: with f: self._config = yaml.safe_load(f) except yaml.YAMLError as err: LOG.error(err) raise utils.ConfigError("Error parsing file.", config_file) self.validate(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 (self.get_option("profiles") or {}).items(): # NOTE(tkelsey): can't use default of get() because value is # sometimes explicitly 'None', for example when the list is given # in yaml but not populated with any values. include = { (extman.get_test_id(i) or i) for i in (profile.get("include") or []) } exclude = { (extman.get_test_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 item.items(): 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 item.items(): 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 profiles.items(): 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, ) ================================================ FILE: bandit/core/constants.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 # default plugin name pattern plugin_name_pattern = "*.py" 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/3/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" # Directories to exclude by default EXCLUDE = ( ".svn", "CVS", ".bzr", ".hg", ".git", "__pycache__", ".tox", ".eggs", "*.egg", ) ================================================ FILE: bandit/core/context.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 import ast from bandit.core import utils class Context: 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 f"" @property def call_args(self): """Get a list of function args :return: A list of function args """ args = [] if "call" in self._context and hasattr(self._context["call"], "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 or None """ 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 """ return self._context.get("name") @property def call_function_name_qual(self): """Get the FQ name of a function call :return: The FQ name of a function call """ return self._context.get("qualname") @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 """ return self._context.get("node") @property def string_val(self): """Get the value of a standalone unicode or string object :return: value of a standalone unicode or string object """ return self._context.get("str") @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 """ return self._context.get("statement") @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 and hasattr(self._context["node"], "args") and hasattr(self._context["node"].args, "defaults") ): 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.Constant): if isinstance(literal.value, bool): literal_value = str(literal.value) elif literal.value is None: literal_value = str(literal.value) else: literal_value = literal.value 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 += (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.Name): literal_value = literal.id 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 """ if hasattr(self.node, "keywords"): 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 """ max_args = self.call_args_count if max_args and position_num < max_args: arg = self._context["call"].args[position_num] return getattr(arg, "attr", None) or self._get_literal_value(arg) 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 self._context.get("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 module in self._context.get("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 @property def filename(self): return self._context.get("filename") @property def file_data(self): return self._context.get("file_data") @property def import_aliases(self): return self._context.get("import_aliases") ================================================ FILE: bandit/core/docs_utils.py ================================================ # # Copyright 2016 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 import bandit def get_url(bid): # where our docs are hosted base_url = f"https://bandit.readthedocs.io/en/{bandit.__version__}/" # 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) if info is not None: return f"{base_url}plugins/{bid.lower()}_{info.plugin.__name__}.html" info = extension_loader.MANAGER.blacklist_by_id.get(bid) if info is not None: template = "blacklists/blacklist_{kind}.html#{id}-{name}" info["name"] = info["name"].replace("_", "-") if info["id"].startswith("B3"): # B3XX # Some of the links are combined, so we have exception cases if info["id"] in ["B304", "B305"]: info = info.copy() info["id"] = "b304-b305" info["name"] = "ciphers-and-modes" elif info["id"] in [ "B313", "B314", "B315", "B316", "B317", "B318", "B319", "B320", ]: info = info.copy() info["id"] = "b313-b320" 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 ================================================ FILE: bandit/core/extension_loader.py ================================================ # # SPDX-License-Identifier: Apache-2.0 import logging import sys from stevedore import extension from bandit.core import utils LOG = logging.getLogger(__name__) class Manager: # 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( f"WARNING: Test '{plugin.name}' has no ID, skipping.", 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_test_id(self, test_name): if test_name in self.plugins_by_name: return self.plugins_by_name[test_name].plugin._test_id if test_name in self.blacklist_by_name: return self.blacklist_by_name[test_name]["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 self.blacklist.values(): 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): LOG.warning(f"Unknown test found in profile: {inc}") for exc in profile["exclude"]: if not self.check_id(exc): LOG.warning(f"Unknown test found in profile: {exc}") union = set(profile["include"]) & set(profile["exclude"]) if len(union) > 0: raise ValueError( f"Non-exclusive include/exclude test sets: {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() ================================================ FILE: bandit/core/issue.py ================================================ # # Copyright 2015 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 import linecache from bandit.core import constants class Cwe: NOTSET = 0 IMPROPER_INPUT_VALIDATION = 20 PATH_TRAVERSAL = 22 OS_COMMAND_INJECTION = 78 XSS = 79 BASIC_XSS = 80 SQL_INJECTION = 89 CODE_INJECTION = 94 IMPROPER_WILDCARD_NEUTRALIZATION = 155 HARD_CODED_PASSWORD = 259 IMPROPER_ACCESS_CONTROL = 284 IMPROPER_CERT_VALIDATION = 295 CLEARTEXT_TRANSMISSION = 319 INADEQUATE_ENCRYPTION_STRENGTH = 326 BROKEN_CRYPTO = 327 INSUFFICIENT_RANDOM_VALUES = 330 INSECURE_TEMP_FILE = 377 UNCONTROLLED_RESOURCE_CONSUMPTION = 400 DOWNLOAD_OF_CODE_WITHOUT_INTEGRITY_CHECK = 494 DESERIALIZATION_OF_UNTRUSTED_DATA = 502 MULTIPLE_BINDS = 605 IMPROPER_CHECK_OF_EXCEPT_COND = 703 INCORRECT_PERMISSION_ASSIGNMENT = 732 INAPPROPRIATE_ENCODING_FOR_OUTPUT_CONTEXT = 838 MITRE_URL_PATTERN = "https://cwe.mitre.org/data/definitions/%s.html" def __init__(self, id=NOTSET): self.id = id def link(self): if self.id == Cwe.NOTSET: return "" return Cwe.MITRE_URL_PATTERN % str(self.id) def __str__(self): if self.id == Cwe.NOTSET: return "" return "CWE-%i (%s)" % (self.id, self.link()) def as_dict(self): return ( {"id": self.id, "link": self.link()} if self.id != Cwe.NOTSET else {} ) def as_jsons(self): return str(self.as_dict()) def from_dict(self, data): if "id" in data: self.id = int(data["id"]) else: self.id = Cwe.NOTSET def __eq__(self, other): return self.id == other.id def __ne__(self, other): return self.id != other.id def __hash__(self): return id(self) class Issue: def __init__( self, severity, cwe=0, confidence=constants.CONFIDENCE_DEFAULT, text="", ident=None, lineno=None, test_id="", col_offset=-1, end_col_offset=0, ): self.severity = severity self.cwe = Cwe(cwe) self.confidence = confidence if isinstance(text, bytes): text = text.decode("utf-8") self.text = text self.ident = ident self.fname = "" self.fdata = None self.test = "" self.test_id = test_id self.lineno = lineno self.col_offset = col_offset self.end_col_offset = end_col_offset self.linerange = [] def __str__(self): return ( "Issue: '%s' from %s:%s: CWE: %s, Severity: %s Confidence: " "%s at %s:%i:%i" ) % ( self.text, self.test_id, (self.ident or self.test), str(self.cwe), self.severity, self.confidence, self.fname, self.lineno, self.col_offset, ) 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", "cwe", "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 if self.fname == "": self.fdata.seek(0) for line_num in range(1, lmin): self.fdata.readline() tmplt = "%i\t%s" if tabbed else "%i %s" for line in range(lmin, lmax): if self.fname == "": text = self.fdata.readline() else: 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, max_lines=3): """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_cwe": self.cwe.as_dict(), "issue_confidence": self.confidence, "issue_text": self.text.encode("utf-8").decode("utf-8"), "line_number": self.lineno, "line_range": self.linerange, "col_offset": self.col_offset, "end_col_offset": self.end_col_offset, } if with_code: out["code"] = self.get_code(max_lines=max_lines) return out def from_dict(self, data, with_code=True): self.code = data["code"] self.fname = data["filename"] self.severity = data["issue_severity"] self.cwe = cwe_from_dict(data["issue_cwe"]) 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"] self.col_offset = data.get("col_offset", 0) self.end_col_offset = data.get("end_col_offset", 0) def cwe_from_dict(data): cwe = Cwe() cwe.from_dict(data) return cwe def issue_from_dict(data): i = Issue(severity=data["issue_severity"]) i.from_dict(data) return i ================================================ FILE: bandit/core/manager.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 import collections import fnmatch import io import json import logging import os import re import sys import tokenize import traceback from rich import progress 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__) NOSEC_COMMENT = re.compile(r"#\s*nosec:?\s*(?P[^#]+)?#?") NOSEC_COMMENT_TESTS = re.compile(r"(?:(B\d+|[a-z\d_]+),?)+", re.IGNORECASE) PROGRESS_THRESHOLD = 50 class BanditManager: scope = [] def __init__( self, config, agg_type, debug=False, verbose=False, quiet=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 quiet: Whether to only show output in the case of an error :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 self.quiet = quiet 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) 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, template=None, ): """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 :param template: Output template with non-terminal tags (default: {abspath}:{line}: {test_id}[bandit]: {severity}: {msg}) :return: - """ try: formatters_mgr = extension_loader.MANAGER.formatters_mgr if output_format not in formatters_mgr: output_format = ( "screen" if ( sys.stdout.isatty() and os.getenv("NO_COLOR") is None and os.getenv("TERM") != "dumb" ) else "txt" ) formatter = formatters_mgr[output_format] report_func = formatter.plugin if output_format == "custom": report_func( self, fileobj=output_file, sev_level=sev_level, conf_level=conf_level, template=template, ) else: report_func( self, fileobj=output_file, sev_level=sev_level, conf_level=conf_level, lines=lines, ) except Exception as e: raise RuntimeError( f"Unable to output report using " f"'{output_format}' formatter: {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 maintain a list of files which are added, and ones which have # been explicitly excluded files_list = set() excluded_files = set() excluded_path_globs = 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(","): if os.path.isdir(path): path = os.path.join(path, "*") excluded_path_globs.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_globs, ) 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_globs, enforce_glob=False, ): if fname != "-": fname = os.path.join(".", fname) 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: - """ # 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) if ( len(self.files_list) > PROGRESS_THRESHOLD and LOG.getEffectiveLevel() <= logging.INFO ): files = progress.track(self.files_list) else: files = self.files_list for count, fname in enumerate(files): LOG.debug("working on file : %s", fname) try: if fname == "-": open_fd = os.fdopen(sys.stdin.fileno(), "rb", 0) fdata = io.BytesIO(open_fd.read()) new_files_list = [ "" if x == "-" else x for x in new_files_list ] self._parse_file("", fdata, new_files_list) else: with open(fname, "rb") as fdata: self._parse_file(fname, fdata, new_files_list) except OSError as e: self.skipped.append((fname, e.strerror)) new_files_list.remove(fname) # 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) # nosec_lines is a dict of line number -> set of tests to ignore # for the line nosec_lines = dict() try: fdata.seek(0) tokens = tokenize.tokenize(fdata.readline) if not self.ignore_nosec: for toktype, tokval, (lineno, _), _, _ in tokens: if toktype == tokenize.COMMENT: nosec_lines[lineno] = _parse_nosec_comment(tokval) except tokenize.TokenError: pass score = self._execute_ast_visitor(fname, fdata, data, nosec_lines) self.scores.append(score) self.metrics.count_issues([score]) except KeyboardInterrupt: sys.exit(2) except SyntaxError: 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.", fname ) if not LOG.isEnabledFor(logging.DEBUG): LOG.error( 'Run "bandit --debug %s" to see the full traceback.', 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, fdata, 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, fdata, 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, _, 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 (globbing supported) 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 _matches_glob_list(path, excluded_path_strings) and 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: main 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 def _find_test_id_from_nosec_string(extman, match): test_id = extman.check_id(match) if test_id: return match # Finding by short_id didn't work, let's check the test name test_id = extman.get_test_id(match) if not test_id: # Name and short id didn't work: LOG.warning( "Test in comment: %s is not a test name or id, ignoring", match ) return test_id # We want to return None or the string here regardless def _parse_nosec_comment(comment): found_no_sec_comment = NOSEC_COMMENT.search(comment) if not found_no_sec_comment: # there was no nosec comment return None matches = found_no_sec_comment.groupdict() nosec_tests = matches.get("tests", set()) # empty set indicates that there was a nosec comment without specific # test ids or names test_ids = set() if nosec_tests: extman = extension_loader.MANAGER # lookup tests by short code or name for test in NOSEC_COMMENT_TESTS.finditer(nosec_tests): test_match = test.group(1) test_id = _find_test_id_from_nosec_string(extman, test_match) if test_id: test_ids.add(test_id) return test_ids ================================================ FILE: bandit/core/meta_ast.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 import collections import logging LOG = logging.getLogger(__name__) class BanditMetaAst: 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 += f"Node: {k}\n" tmpstr += f"\t{str(v)}\n" tmpstr += f"Length: {len(self.nodes)}\n" return tmpstr ================================================ FILE: bandit/core/metrics.py ================================================ # # Copyright 2015 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 import collections from bandit.core import constants class Metrics: """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, "skipped_tests": 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"][f"{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, "skipped_tests": 0, } self.current = self.data[fname] def note_nosec(self, num=1): """Note a "nosec" comment. Increment the currently active metrics nosec count. :param num: number of nosecs seen, defaults to 1 """ self.current["nosec"] += num def note_skipped_test(self, num=1): """Note a "nosec BXXX, BYYY, ..." comment. Increment the currently active metrics skipped_tests count. :param num: number of skipped_tests seen, defaults to 1 """ self.current["skipped_tests"] += 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, _ in constants.CRITERIA: for i, rank in enumerate(constants.RANKING): label = f"{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 ================================================ FILE: bandit/core/node_visitor.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 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: def __init__( self, fname, fdata, metaast, testset, debug, nosec_lines, metrics ): self.debug = debug self.nosec_lines = nosec_lines self.scores = { "SEVERITY": [0] * len(constants.RANKING), "CONFIDENCE": [0] * len(constants.RANKING), } self.depth = 0 self.fname = fname self.fdata = fdata self.metaast = metaast self.testset = testset self.imports = set() self.import_aliases = {} self.tester = b_tester.BanditTester( self.testset, self.debug, nosec_lines, metrics ) # 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.warning( "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_Constant(self, node): """Visitor for AST Constant nodes call the appropriate method for the node type. this maintains compatibility with <3.6 and 3.8+ This code is heavily influenced by Anthony Sottile (@asottile) here: https://bugs.python.org/msg342486 :param node: The node that is being inspected :return: - """ if isinstance(node.value, str): self.visit_Str(node) elif isinstance(node.value, bytes): self.visit_Bytes(node) 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.value if not isinstance(node._bandit_parent, ast.Expr): # docstring self.context["linerange"] = b_utils.linerange(node._bandit_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.value if not isinstance(node._bandit_parent, ast.Expr): # docstring self.context["linerange"] = b_utils.linerange(node._bandit_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 hasattr(node, "col_offset"): self.context["col_offset"] = node.col_offset if hasattr(node, "end_col_offset"): self.context["end_col_offset"] = node.end_col_offset self.context["node"] = node self.context["linerange"] = b_utils.linerange(node) self.context["filename"] = self.fname self.context["file_data"] = self.fdata 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, 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: item._bandit_sibling = value[idx + 1] else: item._bandit_sibling = None item._bandit_parent = node if self.pre_visit(item): self.visit(item) self.generic_visit(item) self.post_visit(item) elif isinstance(value, ast.AST): value._bandit_sibling = None value._bandit_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) # Run tests that do not require access to the AST, # but only to the whole file source: self.context = { "file_data": self.fdata, "filename": self.fname, "lineno": 0, "linerange": [0, 1], "col_offset": 0, } self.update_scores(self.tester.run_tests(self.context, "File")) return self.scores ================================================ FILE: bandit/core/test_properties.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 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 = [] for arg in args: if arg == "File": func._checks.append("File") else: func._checks.append(utils.check_ast_node(arg)) 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]) ================================================ FILE: bandit/core/test_set.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 import importlib import logging from bandit.core import blacklisting from bandit.core import extension_loader LOG = logging.getLogger(__name__) class BanditTestSet: 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 _, tests in extman.blacklist.items(): 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: 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 extman.blacklist.items(): 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. blacklisting.blacklist._test_id = "B001" blacklisting.blacklist._checks = blacklist.keys() 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) targeting %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 [] ================================================ FILE: bandit/core/tester.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 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: def __init__(self, testset, debug, nosec_lines, metrics): self.results = [] self.testset = testset self.last_result = None self.debug = debug self.nosec_lines = nosec_lines self.metrics = metrics 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 :return: a score based on the number and type of test results with extra metrics about nosec comments """ 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 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 result is not None: nosec_tests_to_skip = self._get_nosecs_from_contexts( temp_context, test_result=result ) if isinstance(temp_context["filename"], bytes): result.fname = temp_context["filename"].decode("utf-8") else: result.fname = temp_context["filename"] result.fdata = temp_context["file_data"] if result.lineno is None: result.lineno = temp_context["lineno"] if result.linerange == []: result.linerange = temp_context["linerange"] if result.col_offset == -1: result.col_offset = temp_context["col_offset"] result.end_col_offset = temp_context.get( "end_col_offset", 0 ) result.test = name if result.test_id == "": result.test_id = test._test_id # don't skip the test if there was no nosec comment if nosec_tests_to_skip is not None: # If the set is empty then it means that nosec was # used without test number -> update nosecs counter. # If the test id is in the set of tests to skip, # log and increment the skip by test count. if not nosec_tests_to_skip: LOG.debug("skipped, nosec without test number") self.metrics.note_nosec() continue if result.test_id in nosec_tests_to_skip: LOG.debug( f"skipped, nosec for test {result.test_id}" ) self.metrics.note_skipped_test() continue 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 else: nosec_tests_to_skip = self._get_nosecs_from_contexts( temp_context ) if ( nosec_tests_to_skip and test._test_id in nosec_tests_to_skip ): LOG.warning( f"nosec encountered ({test._test_id}), but no " f"failed test on file " f"{temp_context['filename']}:" f"{temp_context['lineno']}" ) except Exception as e: self.report_error(name, context, e) if self.debug: raise LOG.debug("Returning scores: %s", scores) return scores def _get_nosecs_from_contexts(self, context, test_result=None): """Use context and optional test result to get set of tests to skip. :param context: temp context :param test_result: optional test result :return: set of tests to skip for the line based on contexts """ nosec_tests_to_skip = set() base_tests = ( self.nosec_lines.get(test_result.lineno, None) if test_result else None ) context_tests = utils.get_nosec(self.nosec_lines, context) # if both are none there were no comments # this is explicitly different from being empty. # empty set indicates blanket nosec comment without # individual test names or ids if base_tests is None and context_tests is None: nosec_tests_to_skip = None # combine tests from current line and context line if base_tests is not None: nosec_tests_to_skip.update(base_tests) if context_tests is not None: nosec_tests_to_skip.update(context_tests) return nosec_tests_to_skip @staticmethod def report_error(test, context, error): what = "Bandit internal error running: " what += f"{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) ================================================ FILE: bandit/core/utils.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 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 = f"{_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): 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. prefix = "" return f"{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 = f"{config_file} : {message}" super().__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 ({}) in config file: {}".format( self.profile, self.config_file, ) super().__init__(message) def warnings_formatter( message, category=UserWarning, filename="", lineno=-1, line="" ): """Monkey patch for warnings.warn to suppress cruft output.""" return f"{message}\n" 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( f'Invalid python file path: "{path}" Missing path or file name' ) 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 f"{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 calc_linerange(node): """Calculate linerange for subtree""" if hasattr(node, "_bandit_linerange"): return node._bandit_linerange lines_min = 9999999999 lines_max = -1 if hasattr(node, "lineno"): lines_min = node.lineno lines_max = node.lineno for n in ast.iter_child_nodes(node): lines_minmax = calc_linerange(n) lines_min = min(lines_min, lines_minmax[0]) lines_max = max(lines_max, lines_minmax[1]) node._bandit_linerange = (lines_min, lines_max) return (lines_min, lines_max) def linerange(node): """Get line number range from a node.""" if hasattr(node, "lineno"): return list(range(node.lineno, node.end_lineno + 1)) else: if hasattr(node, "_bandit_linerange_stripped"): lines_minmax = node._bandit_linerange_stripped return list(range(lines_minmax[0], lines_minmax[1] + 1)) 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 if hasattr(node, "lineno"): lines_min = node.lineno lines_max = node.lineno for n in ast.iter_child_nodes(node): lines_minmax = calc_linerange(n) lines_min = min(lines_min, lines_minmax[0]) lines_max = max(lines_max, lines_minmax[1]) for key in strip.keys(): if strip[key] is not None: setattr(node, key, strip[key]) if lines_max == -1: lines_min = 0 lines_max = 1 node._bandit_linerange_stripped = (lines_min, lines_max) lines = list(range(lines_min, lines_max + 1)) """Try and work around a known Python bug with multi-line strings.""" # deal with multiline strings lineno behavior (Python issue #16806) if hasattr(node, "_bandit_sibling") and hasattr( node._bandit_sibling, "lineno" ): start = min(lines) delta = node._bandit_sibling.lineno - start if delta > 1: return list(range(start, node._bandit_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.Constant 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.Constant or ast.BinOp) The node to process :param stop: (ast.Constant 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._bandit_parent, ast.BinOp): node = node._bandit_parent if isinstance(node, ast.BinOp): _get(node, bits, stop) return ( node, " ".join( [ x.value for x in bits if isinstance(x, ast.Constant) and isinstance(x.value, 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: # These ast Node types were deprecated in Python 3.12 and removed # in Python 3.14, but plugins may still check on them. if sys.version_info >= (3, 12) and name in ( "Num", "Str", "Ellipsis", "NameConstant", "Bytes", ): return name node = getattr(ast, name) if issubclass(node, ast.AST): return name except AttributeError: # nosec(tkelsey): catching expected exception pass raise TypeError(f"Error: {name} is not a valid node type in AST") def get_nosec(nosec_lines, context): for lineno in context["linerange"]: nosec = nosec_lines.get(lineno, None) if nosec is not None: return nosec return None ================================================ FILE: bandit/formatters/__init__.py ================================================ ================================================ FILE: bandit/formatters/csv.py ================================================ # # SPDX-License-Identifier: Apache-2.0 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_cwe, issue_text,line_number,line_range,more_info examples/yaml_load.py,blacklist_calls,B301,MEDIUM,HIGH, https://cwe.mitre.org/data/definitions/20.html,"Use of unsafe yaml load. Allows instantiation of arbitrary objects. Consider yaml.safe_load(). ",5,[5],https://bandit.readthedocs.io/en/latest/ .. versionadded:: 0.11.0 .. versionchanged:: 1.5.0 New field `more_info` added to output .. versionchanged:: 1.7.3 New field `CWE` added to output """ # 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. import csv import logging import sys from bandit.core import docs_utils 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_cwe", "issue_text", "line_number", "col_offset", "end_col_offset", "line_range", "more_info", ] writer = csv.DictWriter( fileobj, fieldnames=fieldnames, extrasaction="ignore" ) writer.writeheader() for result in results: r = result.as_dict(with_code=False) r["issue_cwe"] = r["issue_cwe"]["link"] r["more_info"] = docs_utils.get_url(r["test_id"]) writer.writerow(r) if fileobj.name != sys.stdout.name: LOG.info("CSV output written to file: %s", fileobj.name) ================================================ FILE: bandit/formatters/custom.py ================================================ # # Copyright (c) 2017 Hewlett Packard Enterprise # # SPDX-License-Identifier: Apache-2.0 """ ================ Custom Formatter ================ This formatter outputs the issues in custom machine-readable format. default template: ``{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}`` :Example: .. code-block:: none /usr/lib/python3.6/site-packages/openlp/core/utils/__init__.py:\ 405: B310[bandit]: MEDIUM: Audit url open for permitted schemes. \ Allowing use of file:/ or custom schemes is often unexpected. .. versionadded:: 1.5.0 .. versionchanged:: 1.7.3 New field `CWE` added to output """ import logging import os import re import string import sys from bandit.core import test_properties LOG = logging.getLogger(__name__) class SafeMapper(dict): """Safe mapper to handle format key errors""" @classmethod # To prevent PEP8 warnings in the test suite def __missing__(cls, key): return "{%s}" % key @test_properties.accepts_baseline def report(manager, fileobj, sev_level, conf_level, template=None): """Prints issues in custom 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 template: Output template with non-terminal tags (default: '{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}') """ 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 ) msg_template = template if template is None: msg_template = "{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}" # Dictionary of non-terminal tags that will be expanded tag_mapper = { "abspath": lambda issue: os.path.abspath(issue.fname), "relpath": lambda issue: os.path.relpath(issue.fname), "line": lambda issue: issue.lineno, "col": lambda issue: issue.col_offset, "end_col": lambda issue: issue.end_col_offset, "test_id": lambda issue: issue.test_id, "severity": lambda issue: issue.severity, "msg": lambda issue: issue.text, "confidence": lambda issue: issue.confidence, "range": lambda issue: issue.linerange, "cwe": lambda issue: issue.cwe, } # Create dictionary with tag sets to speed up search for similar tags tag_sim_dict = {tag: set(tag) for tag, _ in tag_mapper.items()} # Parse the format_string template and check the validity of tags try: parsed_template_orig = list(string.Formatter().parse(msg_template)) # of type (literal_text, field_name, fmt_spec, conversion) # Check the format validity only, ignore keys string.Formatter().vformat(msg_template, (), SafeMapper(line=0)) except ValueError as e: LOG.error("Template is not in valid format: %s", e.args[0]) sys.exit(2) tag_set = {t[1] for t in parsed_template_orig if t[1] is not None} if not tag_set: LOG.error("No tags were found in the template. Are you missing '{}'?") sys.exit(2) def get_similar_tag(tag): similarity_list = [ (len(set(tag) & t_set), t) for t, t_set in tag_sim_dict.items() ] return sorted(similarity_list)[-1][1] tag_blacklist = [] for tag in tag_set: # check if the tag is in dictionary if tag not in tag_mapper: similar_tag = get_similar_tag(tag) LOG.warning( "Tag '%s' was not recognized and will be skipped, " "did you mean to use '%s'?", tag, similar_tag, ) tag_blacklist += [tag] # Compose the message template back with the valid values only msg_parsed_template_list = [] for literal_text, field_name, fmt_spec, conversion in parsed_template_orig: if literal_text: # if there is '{' or '}', double it to prevent expansion literal_text = re.sub("{", "{{", literal_text) literal_text = re.sub("}", "}}", literal_text) msg_parsed_template_list.append(literal_text) if field_name is not None: if field_name in tag_blacklist: msg_parsed_template_list.append(field_name) continue # Append the fmt_spec part params = [field_name, fmt_spec, conversion] markers = ["", ":", "!"] msg_parsed_template_list.append( ["{"] + [f"{m + p}" if p else "" for m, p in zip(markers, params)] + ["}"] ) msg_parsed_template = ( "".join([item for lst in msg_parsed_template_list for item in lst]) + "\n" ) with fileobj: for defect in results: evaluated_tags = SafeMapper( (k, v(defect)) for k, v in tag_mapper.items() ) output = msg_parsed_template.format(**evaluated_tags) fileobj.write(output) if fileobj.name != sys.stdout.name: LOG.info("Result written to file: %s", fileobj.name) ================================================ FILE: bandit/formatters/html.py ================================================ # Copyright (c) 2015 Rackspace, Inc. # Copyright (c) 2015 Hewlett Packard Enterprise # # SPDX-License-Identifier: Apache-2.0 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
CWE: CWE-20 (https://cwe.mitre.org/data/definitions/20.html)
File: examples/yaml_load.py
More info: https://bandit.readthedocs.io/en/latest/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 .. versionchanged:: 1.5.0 New field `more_info` added to output .. versionchanged:: 1.7.3 New field `CWE` added to output """ import logging import sys from html import escape as html_escape 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 = """ Bandit Report """ report_block = """ {metrics} {skipped}
{results}
""" issue_block = """
{test_name}: {test_text}
Test ID: {test_id}
Severity: {severity}
Confidence: {confidence}
CWE: CWE-{cwe.id}
File: {path}
Line number: {line_number}
More info: {url}
{code} {candidates}
""" code_block = """
{code}
""" candidate_block = """

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

{files_list}
""" metrics_block = """
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( f"{fname} reason: {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 = html_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 = html_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=f"issue-sev-{issue.severity.lower()}", test_name=issue.test, test_id=issue.test_id, test_text=issue.text, severity=issue.severity, confidence=issue.confidence, cwe=issue.cwe, cwe_link=issue.cwe.link(), path=issue.fname, code=code, candidates=candidates, url=url, line_number=issue.lineno, ) # 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(header_block) wrapped_file.write(report_contents) if fileobj.name != sys.stdout.name: LOG.info("HTML output written to file: %s", fileobj.name) ================================================ FILE: bandit/formatters/json.py ================================================ # # SPDX-License-Identifier: Apache-2.0 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_cwe": { "id": 20, "link": "https://cwe.mitre.org/data/definitions/20.html" }, "issue_text": "Use of unsafe yaml load. Allows instantiation of arbitrary objects. Consider yaml.safe_load().\n", "line_number": 5, "line_range": [ 5 ], "more_info": "https://bandit.readthedocs.io/en/latest/", "test_name": "blacklist_calls", "test_id": "B301" } ] } .. versionadded:: 0.10.0 .. versionchanged:: 1.5.0 New field `more_info` added to output .. versionchanged:: 1.7.3 New field `CWE` added to output """ # Necessary so we can import the standard library json module while continuing # to name this file json.py. (Python 2 only) import datetime import json import logging import operator import sys from bandit.core import docs_utils 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(max_lines=lines) d["more_info"] = docs_utils.get_url(d["test_id"]) if len(results[r]) > 1: d["candidates"] = [ c.as_dict(max_lines=lines) for c in results[r] ] collector.append(d) else: collector = [r.as_dict(max_lines=lines) for r in results] for elem in collector: elem["more_info"] = docs_utils.get_url(elem["test_id"]) 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.now(datetime.timezone.utc).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) ================================================ FILE: bandit/formatters/sarif.py ================================================ # Copyright (c) Microsoft. All Rights Reserved. # # SPDX-License-Identifier: Apache-2.0 # # Note: this code mostly incorporated from # https://github.com/microsoft/bandit-sarif-formatter # r""" =============== SARIF formatter =============== This formatter outputs the issues in SARIF formatted JSON. :Example: .. code-block:: javascript { "runs": [ { "tool": { "driver": { "name": "Bandit", "organization": "PyCQA", "rules": [ { "id": "B101", "name": "assert_used", "properties": { "tags": [ "security", "external/cwe/cwe-703" ], "precision": "high" }, "helpUri": "https://bandit.readthedocs.io/en/1.7.8/plugins/b101_assert_used.html" } ], "version": "1.7.8", "semanticVersion": "1.7.8" } }, "invocations": [ { "executionSuccessful": true, "endTimeUtc": "2024-03-05T03:28:48Z" } ], "properties": { "metrics": { "_totals": { "loc": 1, "nosec": 0, "skipped_tests": 0, "SEVERITY.UNDEFINED": 0, "CONFIDENCE.UNDEFINED": 0, "SEVERITY.LOW": 1, "CONFIDENCE.LOW": 0, "SEVERITY.MEDIUM": 0, "CONFIDENCE.MEDIUM": 0, "SEVERITY.HIGH": 0, "CONFIDENCE.HIGH": 1 }, "./examples/assert.py": { "loc": 1, "nosec": 0, "skipped_tests": 0, "SEVERITY.UNDEFINED": 0, "SEVERITY.LOW": 1, "SEVERITY.MEDIUM": 0, "SEVERITY.HIGH": 0, "CONFIDENCE.UNDEFINED": 0, "CONFIDENCE.LOW": 0, "CONFIDENCE.MEDIUM": 0, "CONFIDENCE.HIGH": 1 } } }, "results": [ { "message": { "text": "Use of assert detected. The enclosed code will be removed when compiling to optimised byte code." }, "level": "note", "locations": [ { "physicalLocation": { "region": { "snippet": { "text": "assert True\n" }, "endColumn": 11, "endLine": 1, "startColumn": 0, "startLine": 1 }, "artifactLocation": { "uri": "examples/assert.py" }, "contextRegion": { "snippet": { "text": "assert True\n" }, "endLine": 1, "startLine": 1 } } } ], "properties": { "issue_confidence": "HIGH", "issue_severity": "LOW" }, "ruleId": "B101", "ruleIndex": 0 } ] } ], "version": "2.1.0", "$schema": "https://json.schemastore.org/sarif-2.1.0.json" } .. versionadded:: 1.7.8 """ # noqa: E501 import datetime import logging import pathlib import sys import urllib.parse as urlparse import sarif_om as om from jschema_to_python.to_json import to_json import bandit from bandit.core import docs_utils LOG = logging.getLogger(__name__) SCHEMA_URI = "https://json.schemastore.org/sarif-2.1.0.json" SCHEMA_VER = "2.1.0" TS_FORMAT = "%Y-%m-%dT%H:%M:%SZ" def report(manager, fileobj, sev_level, conf_level, lines=-1): """Prints issues in SARIF 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 """ log = om.SarifLog( schema_uri=SCHEMA_URI, version=SCHEMA_VER, runs=[ om.Run( tool=om.Tool( driver=om.ToolComponent( name="Bandit", organization=bandit.__author__, semantic_version=bandit.__version__, version=bandit.__version__, ) ), invocations=[ om.Invocation( end_time_utc=datetime.datetime.now( datetime.timezone.utc ).strftime(TS_FORMAT), execution_successful=True, ) ], properties={"metrics": manager.metrics.data}, ) ], ) run = log.runs[0] invocation = run.invocations[0] skips = manager.get_skipped() add_skipped_file_notifications(skips, invocation) issues = manager.get_issue_list(sev_level=sev_level, conf_level=conf_level) add_results(issues, run) serializedLog = to_json(log) with fileobj: fileobj.write(serializedLog) if fileobj.name != sys.stdout.name: LOG.info("SARIF output written to file: %s", fileobj.name) def add_skipped_file_notifications(skips, invocation): if skips is None or len(skips) == 0: return if invocation.tool_configuration_notifications is None: invocation.tool_configuration_notifications = [] for skip in skips: (file_name, reason) = skip notification = om.Notification( level="error", message=om.Message(text=reason), locations=[ om.Location( physical_location=om.PhysicalLocation( artifact_location=om.ArtifactLocation( uri=to_uri(file_name) ) ) ) ], ) invocation.tool_configuration_notifications.append(notification) def add_results(issues, run): if run.results is None: run.results = [] rules = {} rule_indices = {} for issue in issues: result = create_result(issue, rules, rule_indices) run.results.append(result) if len(rules) > 0: run.tool.driver.rules = list(rules.values()) def create_result(issue, rules, rule_indices): issue_dict = issue.as_dict() rule, rule_index = create_or_find_rule(issue_dict, rules, rule_indices) physical_location = om.PhysicalLocation( artifact_location=om.ArtifactLocation( uri=to_uri(issue_dict["filename"]) ) ) add_region_and_context_region( physical_location, issue_dict["line_range"], issue_dict["col_offset"], issue_dict["end_col_offset"], issue_dict["code"], ) return om.Result( rule_id=rule.id, rule_index=rule_index, message=om.Message(text=issue_dict["issue_text"]), level=level_from_severity(issue_dict["issue_severity"]), locations=[om.Location(physical_location=physical_location)], properties={ "issue_confidence": issue_dict["issue_confidence"], "issue_severity": issue_dict["issue_severity"], }, ) def level_from_severity(severity): if severity == "HIGH": return "error" elif severity == "MEDIUM": return "warning" elif severity == "LOW": return "note" else: return "warning" def add_region_and_context_region( physical_location, line_range, col_offset, end_col_offset, code ): if code: first_line_number, snippet_lines = parse_code(code) snippet_line = snippet_lines[line_range[0] - first_line_number] snippet = om.ArtifactContent(text=snippet_line) else: snippet = None physical_location.region = om.Region( start_line=line_range[0], end_line=line_range[1] if len(line_range) > 1 else line_range[0], start_column=col_offset + 1, end_column=end_col_offset + 1, snippet=snippet, ) if code: physical_location.context_region = om.Region( start_line=first_line_number, end_line=first_line_number + len(snippet_lines) - 1, snippet=om.ArtifactContent(text="".join(snippet_lines)), ) def parse_code(code): code_lines = code.split("\n") # The last line from the split has nothing in it; it's an artifact of the # last "real" line ending in a newline. Unless, of course, it doesn't: last_line = code_lines[len(code_lines) - 1] last_real_line_ends_in_newline = False if len(last_line) == 0: code_lines.pop() last_real_line_ends_in_newline = True snippet_lines = [] first_line_number = 0 first = True for code_line in code_lines: number_and_snippet_line = code_line.split(" ", 1) if first: first_line_number = int(number_and_snippet_line[0]) first = False snippet_line = number_and_snippet_line[1] + "\n" snippet_lines.append(snippet_line) if not last_real_line_ends_in_newline: last_line = snippet_lines[len(snippet_lines) - 1] snippet_lines[len(snippet_lines) - 1] = last_line[: len(last_line) - 1] return first_line_number, snippet_lines def create_or_find_rule(issue_dict, rules, rule_indices): rule_id = issue_dict["test_id"] if rule_id in rules: return rules[rule_id], rule_indices[rule_id] rule = om.ReportingDescriptor( id=rule_id, name=issue_dict["test_name"], help_uri=docs_utils.get_url(rule_id), properties={ "tags": [ "security", f"external/cwe/cwe-{issue_dict['issue_cwe'].get('id')}", ], "precision": issue_dict["issue_confidence"].lower(), }, ) index = len(rules) rules[rule_id] = rule rule_indices[rule_id] = index return rule, index def to_uri(file_path): pure_path = pathlib.PurePath(file_path) if pure_path.is_absolute(): return pure_path.as_uri() else: # Replace backslashes with slashes. posix_path = pure_path.as_posix() # %-encode special characters. return urlparse.quote(posix_path) ================================================ FILE: bandit/formatters/screen.py ================================================ # Copyright (c) 2015 Hewlett Packard Enterprise # # SPDX-License-Identifier: Apache-2.0 r""" ================ Screen formatter ================ This formatter outputs the issues as color coded text to screen. :Example: .. code-block:: none >> Issue: [B506: yaml_load] Use of unsafe yaml load. Allows instantiation of arbitrary objects. Consider yaml.safe_load(). Severity: Medium Confidence: High CWE: CWE-20 (https://cwe.mitre.org/data/definitions/20.html) More Info: https://bandit.readthedocs.io/en/latest/ 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 .. versionchanged:: 1.5.0 New field `more_info` added to output .. versionchanged:: 1.7.3 New field `CWE` added to output """ import datetime import logging import sys from bandit.core import constants from bandit.core import docs_utils from bandit.core import test_properties IS_WIN_PLATFORM = sys.platform.startswith("win32") COLORAMA = False # This fixes terminal colors not displaying properly on Windows systems. # Colorama will intercept any ANSI escape codes and convert them to the # proper Windows console API calls to change text color. if IS_WIN_PLATFORM: try: import colorama except ImportError: pass else: COLORAMA = True 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 f"{COLOR['HEADER']}{text % args}{COLOR['DEFAULT']}" def get_verbose_details(manager): bits = [] bits.append(header("Files in scope (%i):", len(manager.files_list))) tpl = "\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("Files excluded (%i):", len(manager.excluded_files))) bits.extend([f"\t{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, _ in constants.CRITERIA: bits.append(f"\tTotal issues (by {criteria.lower()}):") for rank in constants.RANKING: bits.append( "\t\t%s: %s" % ( rank.capitalize(), manager.metrics.data["_totals"][f"{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(f"{indent} CWE: {str(issue.cwe)}") bits.append(f"{indent} More Info: {docs_utils.get_url(issue.test_id)}") bits.append( "%s Location: %s:%s:%s%s" % ( indent, issue.fname, issue.lineno if show_lineno else "", issue.col_offset if show_lineno else "", COLOR["DEFAULT"], ) ) if show_code: bits.extend( [indent + line for line 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 "\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("\n-- Candidate Issues --") for candidate in issues[issue]: bits.append( _output_issue_str(candidate, candidate_indent, lines=lines) ) bits.append("\n") bits.append("-" * 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 """ if IS_WIN_PLATFORM and COLORAMA: colorama.init() bits = [] if not manager.quiet or manager.results_count(sev_level, conf_level): bits.append( header( "Run started:%s", datetime.datetime.now(datetime.timezone.utc) ) ) 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, ) if IS_WIN_PLATFORM and COLORAMA: colorama.deinit() ================================================ FILE: bandit/formatters/text.py ================================================ # Copyright (c) 2015 Hewlett Packard Enterprise # # SPDX-License-Identifier: Apache-2.0 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 CWE: CWE-20 (https://cwe.mitre.org/data/definitions/20.html) More Info: https://bandit.readthedocs.io/en/latest/ 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 .. versionchanged:: 1.5.0 New field `more_info` added to output .. versionchanged:: 1.7.3 New field `CWE` added to output """ import datetime import logging import sys from bandit.core import constants from bandit.core import docs_utils from bandit.core import test_properties from bandit.formatters import utils LOG = logging.getLogger(__name__) def get_verbose_details(manager): bits = [] bits.append(f"Files in scope ({len(manager.files_list)}):") tpl = "\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(f"Files excluded ({len(manager.excluded_files)}):") bits.extend([f"\t{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, _ in constants.CRITERIA: bits.append(f"\tTotal issues (by {criteria.lower()}):") for rank in constants.RANKING: bits.append( "\t\t%s: %s" % ( rank.capitalize(), manager.metrics.data["_totals"][f"{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( f"{indent}>> Issue: [{issue.test_id}:{issue.test}] {issue.text}" ) bits.append( "%s Severity: %s Confidence: %s" % ( indent, issue.severity.capitalize(), issue.confidence.capitalize(), ) ) bits.append(f"{indent} CWE: {str(issue.cwe)}") bits.append(f"{indent} More Info: {docs_utils.get_url(issue.test_id)}") bits.append( "%s Location: %s:%s:%s" % ( indent, issue.fname, issue.lineno if show_lineno else "", issue.col_offset if show_lineno else "", ) ) if show_code: bits.extend( [indent + line for line 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 "\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("\n-- Candidate Issues --") for candidate in issues[issue]: bits.append( _output_issue_str(candidate, candidate_indent, lines=lines) ) bits.append("\n") bits.append("-" * 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 = [] if not manager.quiet or manager.results_count(sev_level, conf_level): bits.append( f"Run started:{datetime.datetime.now(datetime.timezone.utc)}" ) 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"]) ) bits.append( "\tTotal potential issues skipped due to specifically being " "disabled (e.g., #nosec BXXX): %i" % (manager.metrics.data["_totals"]["skipped_tests"]) ) skipped = manager.get_skipped() bits.append(get_metrics(manager)) bits.append(f"Files skipped ({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(result) if fileobj.name != sys.stdout.name: LOG.info("Text output written to file: %s", fileobj.name) ================================================ FILE: bandit/formatters/utils.py ================================================ # Copyright (c) 2016 Rackspace, Inc. # # SPDX-License-Identifier: Apache-2.0 """Utility functions for formatting plugins for Bandit.""" import io def wrap_file_object(fileobj): """If the fileobj passed in cannot handle text, use TextIOWrapper to handle the conversion. """ if isinstance(fileobj, io.TextIOBase): return fileobj return io.TextIOWrapper(fileobj) ================================================ FILE: bandit/formatters/xml.py ================================================ # # SPDX-License-Identifier: Apache-2.0 r""" ============= XML Formatter ============= This formatter outputs the issues as XML. :Example: .. code-block:: xml Test ID: B301 Severity: MEDIUM Confidence: HIGH CWE: CWE-20 (https://cwe.mitre.org/data/definitions/20.html) Use of unsafe yaml load. Allows instantiation of arbitrary objects. Consider yaml.safe_load(). Location examples/yaml_load.py:5 .. versionadded:: 0.12.0 .. versionchanged:: 1.5.0 New field `more_info` added to output .. versionchanged:: 1.7.3 New field `CWE` added to output """ import logging import sys from xml.etree import ElementTree as ET # nosec: B405 from bandit.core import docs_utils 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\nCWE: %s\n%s\n" "Location %s:%s" ) text %= ( issue.test_id, issue.severity, issue.confidence, issue.cwe, issue.text, issue.fname, issue.lineno, ) ET.SubElement( testcase, "error", more_info=docs_utils.get_url(issue.test_id), type=issue.severity, message=issue.text, ).text = text tree = ET.ElementTree(root) if fileobj.name == sys.stdout.name: 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) ================================================ FILE: bandit/formatters/yaml.py ================================================ # Copyright (c) 2017 VMware, Inc. # # SPDX-License-Identifier: Apache-2.0 r""" ============== YAML Formatter ============== This formatter outputs the issues in a yaml format. :Example: .. code-block:: none errors: [] generated_at: '2017-03-09T22:29:30Z' 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: 9 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: 9 nosec: 0 results: - code: '5 ystr = yaml.dump({''a'' : 1, ''b'' : 2, ''c'' : 3})\n 6 y = yaml.load(ystr)\n7 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(). line_number: 6 line_range: - 6 more_info: https://bandit.readthedocs.io/en/latest/ test_id: B506 test_name: yaml_load .. versionadded:: 1.5.0 .. versionchanged:: 1.7.3 New field `CWE` added to output """ # Necessary for this formatter to work when imported on Python 2. Importing # the standard library's yaml module conflicts with the name of this module. import datetime import logging import operator import sys import yaml from bandit.core import docs_utils LOG = logging.getLogger(__name__) def report(manager, fileobj, sev_level, conf_level, lines=-1): """Prints issues in YAML 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 ) collector = [r.as_dict(max_lines=lines) for r in results] for elem in collector: elem["more_info"] = docs_utils.get_url(elem["test_id"]) 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 for result in machine_output["results"]: if "code" in result: code = result["code"].replace("\n", "\\n") result["code"] = code # timezone agnostic format TS_FORMAT = "%Y-%m-%dT%H:%M:%SZ" time_string = datetime.datetime.now(datetime.timezone.utc).strftime( TS_FORMAT ) machine_output["generated_at"] = time_string yaml.safe_dump(machine_output, fileobj, default_flow_style=False) if fileobj.name != sys.stdout.name: LOG.info("YAML output written to file: %s", fileobj.name) ================================================ FILE: bandit/plugins/__init__.py ================================================ ================================================ FILE: bandit/plugins/app_debug.py ================================================ # # Copyright 2015 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 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 CWE: CWE-94 (https://cwe.mitre.org/data/definitions/94.html) Location: examples/flask_debug.py:10 9 #bad 10 app.run(debug=True) 11 .. seealso:: .. [1] https://flask.palletsprojects.com/en/1.1.x/quickstart/#debug-mode .. [2] https://werkzeug.palletsprojects.com/en/1.0.x/debug/ .. [3] https://labs.detectify.com/2015/10/02/how-patreon-got-hacked-publicly-exposed-werkzeug-debugger/ .. https://cwe.mitre.org/data/definitions/94.html .. versionadded:: 0.15.0 .. versionchanged:: 1.7.3 CWE information added """ # noqa: E501 import bandit from bandit.core import issue 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, cwe=issue.Cwe.CODE_INJECTION, 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"), ) ================================================ FILE: bandit/plugins/asserts.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 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 \*.opt-1.pyc files). This caused various protections to be removed. Consider raising a semantically meaningful error or ``AssertionError`` instead. Please see https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement for more info on ``assert``. **Config Options:** You can configure files that skip this check. This is often useful when you use assert statements in test cases. .. code-block:: yaml assert_used: skips: ['*_test.py', '*test_*.py'] :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 CWE: CWE-703 (https://cwe.mitre.org/data/definitions/703.html) 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/3/reference/simple_stmts.html#the-assert-statement - https://cwe.mitre.org/data/definitions/703.html .. versionadded:: 0.11.0 .. versionchanged:: 1.7.3 CWE information added """ import fnmatch import bandit from bandit.core import issue from bandit.core import test_properties as test def gen_config(name): if name == "assert_used": return {"skips": []} @test.takes_config @test.test_id("B101") @test.checks("Assert") def assert_used(context, config): for skip in config.get("skips", []): if fnmatch.fnmatch(context.filename, skip): return None return bandit.Issue( severity=bandit.LOW, confidence=bandit.HIGH, cwe=issue.Cwe.IMPROPER_CHECK_OF_EXCEPT_COND, text=( "Use of assert detected. The enclosed code " "will be removed when compiling to optimised byte code." ), ) ================================================ FILE: bandit/plugins/crypto_request_no_cert_validation.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 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 HTTPS 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] Call to requests with verify=False disabling SSL certificate checks, security issue. Severity: High Confidence: High CWE: CWE-295 (https://cwe.mitre.org/data/definitions/295.html) 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 - https://cwe.mitre.org/data/definitions/295.html .. versionadded:: 0.9.0 .. versionchanged:: 1.7.3 CWE information added .. versionchanged:: 1.7.5 Added check for httpx module """ import bandit from bandit.core import issue 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"} HTTPX_ATTRS = {"request", "stream", "Client", "AsyncClient"} | HTTP_VERBS qualname = context.call_function_name_qual.split(".")[0] if ( qualname == "requests" and context.call_function_name in HTTP_VERBS or qualname == "httpx" and context.call_function_name in HTTPX_ATTRS ): if context.check_call_arg_value("verify", "False"): return bandit.Issue( severity=bandit.HIGH, confidence=bandit.HIGH, cwe=issue.Cwe.IMPROPER_CERT_VALIDATION, text=f"Call to {qualname} with verify=False disabling SSL " "certificate checks, security issue.", lineno=context.get_lineno_for_call_arg("verify"), ) ================================================ FILE: bandit/plugins/django_sql_injection.py ================================================ # # Copyright (C) 2018 [Victor Torre](https://github.com/ehooo) # # SPDX-License-Identifier: Apache-2.0 import ast import bandit from bandit.core import issue from bandit.core import test_properties as test def keywords2dict(keywords): kwargs = {} for node in keywords: if isinstance(node, ast.keyword): kwargs[node.arg] = node.value return kwargs @test.checks("Call") @test.test_id("B610") def django_extra_used(context): """**B610: Potential SQL injection on extra function** :Example: .. code-block:: none >> Issue: [B610:django_extra_used] Use of extra potential SQL attack vector. Severity: Medium Confidence: Medium CWE: CWE-89 (https://cwe.mitre.org/data/definitions/89.html) Location: examples/django_sql_injection_extra.py:29:0 More Info: https://bandit.readthedocs.io/en/latest/plugins/b610_django_extra_used.html 28 tables_str = 'django_content_type" WHERE "auth_user"."username"="admin' 29 User.objects.all().extra(tables=[tables_str]).distinct() .. seealso:: - https://docs.djangoproject.com/en/dev/topics/security/\ #sql-injection-protection - https://cwe.mitre.org/data/definitions/89.html .. versionadded:: 1.5.0 .. versionchanged:: 1.7.3 CWE information added """ # noqa: E501 description = "Use of extra potential SQL attack vector." if context.call_function_name == "extra": kwargs = keywords2dict(context.node.keywords) args = context.node.args if args: if len(args) >= 1: kwargs["select"] = args[0] if len(args) >= 2: kwargs["where"] = args[1] if len(args) >= 3: kwargs["params"] = args[2] if len(args) >= 4: kwargs["tables"] = args[3] if len(args) >= 5: kwargs["order_by"] = args[4] if len(args) >= 6: kwargs["select_params"] = args[5] insecure = False for key in ["where", "tables"]: if key in kwargs: if isinstance(kwargs[key], ast.List): for val in kwargs[key].elts: if not ( isinstance(val, ast.Constant) and isinstance(val.value, str) ): insecure = True break else: insecure = True break if not insecure and "select" in kwargs: if isinstance(kwargs["select"], ast.Dict): for k in kwargs["select"].keys: if not ( isinstance(k, ast.Constant) and isinstance(k.value, str) ): insecure = True break if not insecure: for v in kwargs["select"].values: if not ( isinstance(v, ast.Constant) and isinstance(v.value, str) ): insecure = True break else: insecure = True if insecure: return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.MEDIUM, cwe=issue.Cwe.SQL_INJECTION, text=description, ) @test.checks("Call") @test.test_id("B611") def django_rawsql_used(context): """**B611: Potential SQL injection on RawSQL function** :Example: .. code-block:: none >> Issue: [B611:django_rawsql_used] Use of RawSQL potential SQL attack vector. Severity: Medium Confidence: Medium CWE: CWE-89 (https://cwe.mitre.org/data/definitions/89.html) Location: examples/django_sql_injection_raw.py:11:26 More Info: https://bandit.readthedocs.io/en/latest/plugins/b611_django_rawsql_used.html 10 ' WHERE "username"="admin" OR 1=%s --' 11 User.objects.annotate(val=RawSQL(raw, [0])) .. seealso:: - https://docs.djangoproject.com/en/dev/topics/security/\ #sql-injection-protection - https://cwe.mitre.org/data/definitions/89.html .. versionadded:: 1.5.0 .. versionchanged:: 1.7.3 CWE information added """ # noqa: E501 description = "Use of RawSQL potential SQL attack vector." if context.is_module_imported_like("django.db.models"): if context.call_function_name == "RawSQL": if context.node.args: sql = context.node.args[0] else: kwargs = keywords2dict(context.node.keywords) sql = kwargs["sql"] if not ( isinstance(sql, ast.Constant) and isinstance(sql.value, str) ): return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.MEDIUM, cwe=issue.Cwe.SQL_INJECTION, text=description, ) ================================================ FILE: bandit/plugins/django_xss.py ================================================ # # Copyright 2018 Victor Torre # # SPDX-License-Identifier: Apache-2.0 import ast import bandit from bandit.core import issue from bandit.core import test_properties as test class DeepAssignation: def __init__(self, var_name, ignore_nodes=None): self.var_name = var_name self.ignore_nodes = ignore_nodes def is_assigned_in(self, items): assigned = [] for ast_inst in items: new_assigned = self.is_assigned(ast_inst) if new_assigned: if isinstance(new_assigned, (list, tuple)): assigned.extend(new_assigned) else: assigned.append(new_assigned) return assigned def is_assigned(self, node): assigned = False if self.ignore_nodes: if isinstance(self.ignore_nodes, (list, tuple, object)): if isinstance(node, self.ignore_nodes): return assigned if isinstance(node, ast.Expr): assigned = self.is_assigned(node.value) elif isinstance(node, ast.FunctionDef): for name in node.args.args: if isinstance(name, ast.Name): if name.id == self.var_name.id: # If is param the assignations are not affected return assigned assigned = self.is_assigned_in(node.body) elif isinstance(node, ast.With): for withitem in node.items: var_id = getattr(withitem.optional_vars, "id", None) if var_id == self.var_name.id: assigned = node else: assigned = self.is_assigned_in(node.body) elif isinstance(node, ast.Try): assigned = [] assigned.extend(self.is_assigned_in(node.body)) assigned.extend(self.is_assigned_in(node.handlers)) assigned.extend(self.is_assigned_in(node.orelse)) assigned.extend(self.is_assigned_in(node.finalbody)) elif isinstance(node, ast.ExceptHandler): assigned = [] assigned.extend(self.is_assigned_in(node.body)) elif isinstance(node, (ast.If, ast.For, ast.While)): assigned = [] assigned.extend(self.is_assigned_in(node.body)) assigned.extend(self.is_assigned_in(node.orelse)) elif isinstance(node, ast.AugAssign): if isinstance(node.target, ast.Name): if node.target.id == self.var_name.id: assigned = node.value elif isinstance(node, ast.Assign) and node.targets: target = node.targets[0] if isinstance(target, ast.Name): if target.id == self.var_name.id: assigned = node.value elif isinstance(target, ast.Tuple) and isinstance( node.value, ast.Tuple ): pos = 0 for name in target.elts: if name.id == self.var_name.id: assigned = node.value.elts[pos] break pos += 1 return assigned def evaluate_var(xss_var, parent, until, ignore_nodes=None): secure = False if isinstance(xss_var, ast.Name): if isinstance(parent, ast.FunctionDef): for name in parent.args.args: if name.arg == xss_var.id: return False # Params are not secure analyser = DeepAssignation(xss_var, ignore_nodes) for node in parent.body: if node.lineno >= until: break to = analyser.is_assigned(node) if to: if isinstance(to, ast.Constant) and isinstance(to.value, str): secure = True elif isinstance(to, ast.Name): secure = evaluate_var(to, parent, to.lineno, ignore_nodes) elif isinstance(to, ast.Call): secure = evaluate_call(to, parent, ignore_nodes) elif isinstance(to, (list, tuple)): num_secure = 0 for some_to in to: if isinstance(some_to, ast.Constant) and isinstance( some_to.value, str ): num_secure += 1 elif isinstance(some_to, ast.Name): if evaluate_var( some_to, parent, node.lineno, ignore_nodes ): num_secure += 1 else: break else: break if num_secure == len(to): secure = True else: secure = False break else: secure = False break return secure def evaluate_call(call, parent, ignore_nodes=None): secure = False evaluate = False if isinstance(call, ast.Call) and isinstance(call.func, ast.Attribute): if ( isinstance(call.func.value, ast.Constant) and call.func.attr == "format" ): evaluate = True if call.keywords: evaluate = False # TODO(??) get support for this if evaluate: args = list(call.args) num_secure = 0 for arg in args: if isinstance(arg, ast.Constant) and isinstance(arg.value, str): num_secure += 1 elif isinstance(arg, ast.Name): if evaluate_var(arg, parent, call.lineno, ignore_nodes): num_secure += 1 else: break elif isinstance(arg, ast.Call): if evaluate_call(arg, parent, ignore_nodes): num_secure += 1 else: break elif isinstance(arg, ast.Starred) and isinstance( arg.value, (ast.List, ast.Tuple) ): args.extend(arg.value.elts) num_secure += 1 else: break secure = num_secure == len(args) return secure def transform2call(var): if isinstance(var, ast.BinOp): is_mod = isinstance(var.op, ast.Mod) is_left_str = isinstance(var.left, ast.Constant) and isinstance( var.left.value, str ) if is_mod and is_left_str: new_call = ast.Call() new_call.args = [] new_call.args = [] new_call.keywords = None new_call.lineno = var.lineno new_call.func = ast.Attribute() new_call.func.value = var.left new_call.func.attr = "format" if isinstance(var.right, ast.Tuple): new_call.args = var.right.elts else: new_call.args = [var.right] return new_call def check_risk(node): description = "Potential XSS on mark_safe function." xss_var = node.args[0] secure = False if isinstance(xss_var, ast.Name): # Check if the var are secure parent = node._bandit_parent while not isinstance(parent, (ast.Module, ast.FunctionDef)): parent = parent._bandit_parent is_param = False if isinstance(parent, ast.FunctionDef): for name in parent.args.args: if name.arg == xss_var.id: is_param = True break if not is_param: secure = evaluate_var(xss_var, parent, node.lineno) elif isinstance(xss_var, ast.Call): parent = node._bandit_parent while not isinstance(parent, (ast.Module, ast.FunctionDef)): parent = parent._bandit_parent secure = evaluate_call(xss_var, parent) elif isinstance(xss_var, ast.BinOp): is_mod = isinstance(xss_var.op, ast.Mod) is_left_str = isinstance(xss_var.left, ast.Constant) and isinstance( xss_var.left.value, str ) if is_mod and is_left_str: parent = node._bandit_parent while not isinstance(parent, (ast.Module, ast.FunctionDef)): parent = parent._bandit_parent new_call = transform2call(xss_var) secure = evaluate_call(new_call, parent) if not secure: return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.HIGH, cwe=issue.Cwe.BASIC_XSS, text=description, ) @test.checks("Call") @test.test_id("B703") def django_mark_safe(context): """**B703: Potential XSS on mark_safe function** :Example: .. code-block:: none >> Issue: [B703:django_mark_safe] Potential XSS on mark_safe function. Severity: Medium Confidence: High CWE: CWE-80 (https://cwe.mitre.org/data/definitions/80.html) Location: examples/mark_safe_insecure.py:159:4 More Info: https://bandit.readthedocs.io/en/latest/plugins/b703_django_mark_safe.html 158 str_arg = 'could be insecure' 159 safestring.mark_safe(str_arg) .. seealso:: - https://docs.djangoproject.com/en/dev/topics/security/\ #cross-site-scripting-xss-protection - https://docs.djangoproject.com/en/dev/ref/utils/\ #module-django.utils.safestring - https://docs.djangoproject.com/en/dev/ref/utils/\ #django.utils.html.format_html - https://cwe.mitre.org/data/definitions/80.html .. versionadded:: 1.5.0 .. versionchanged:: 1.7.3 CWE information added """ # noqa: E501 if context.is_module_imported_like("django.utils.safestring"): affected_functions = [ "mark_safe", "SafeText", "SafeUnicode", "SafeString", "SafeBytes", ] if context.call_function_name in affected_functions: xss = context.node.args[0] if not ( isinstance(xss, ast.Constant) and isinstance(xss.value, str) ): return check_risk(context.node) ================================================ FILE: bandit/plugins/exec.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 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 CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html) Location: ./examples/exec.py:2 1 exec("do evil") .. seealso:: - https://docs.python.org/3/library/functions.html#exec - https://www.python.org/dev/peps/pep-0551/#background - https://www.python.org/dev/peps/pep-0578/#suggested-audit-hook-locations - https://cwe.mitre.org/data/definitions/78.html .. versionadded:: 0.9.0 .. versionchanged:: 1.7.3 CWE information added """ import bandit from bandit.core import issue from bandit.core import test_properties as test def exec_issue(): return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.HIGH, cwe=issue.Cwe.OS_COMMAND_INJECTION, text="Use of exec detected.", ) @test.checks("Call") @test.test_id("B102") def exec_used(context): if context.call_function_name_qual == "exec": return exec_issue() ================================================ FILE: bandit/plugins/general_bad_file_permissions.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 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 write or executable and a HIGH warning is reported if a file is set world write or executable. Warnings are given with HIGH confidence. :Example: .. code-block:: none >> Issue: Probable insecure usage of temp file/directory. Severity: Medium Confidence: Medium CWE: CWE-732 (https://cwe.mitre.org/data/definitions/732.html) Location: ./examples/os-chmod.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 CWE: CWE-732 (https://cwe.mitre.org/data/definitions/732.html) Location: ./examples/os-chmod.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 - https://en.wikipedia.org/wiki/File_system_permissions - https://security.openstack.org - https://cwe.mitre.org/data/definitions/732.html .. versionadded:: 0.9.0 .. versionchanged:: 1.7.3 CWE information added .. versionchanged:: 1.7.5 Added checks for S_IWGRP and S_IXOTH """ # noqa: E501 import stat import bandit from bandit.core import issue from bandit.core import test_properties as test def _stat_is_dangerous(mode): return ( mode & stat.S_IWOTH or mode & stat.S_IWGRP or mode & stat.S_IXGRP or mode & stat.S_IXOTH ) @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 _stat_is_dangerous(mode) ): # 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, cwe=issue.Cwe.INCORRECT_PERMISSION_ASSIGNMENT, text="Chmod setting a permissive mask %s on file (%s)." % (oct(mode), filename), ) ================================================ FILE: bandit/plugins/general_bind_all_interfaces.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 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 CWE: CWE-605 (https://cwe.mitre.org/data/definitions/605.html) 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:: - https://nvd.nist.gov/vuln/detail/CVE-2018-1281 - https://cwe.mitre.org/data/definitions/605.html .. versionadded:: 0.9.0 .. versionchanged:: 1.7.3 CWE information added """ import bandit from bandit.core import issue 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": # nosec: B104 return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.MEDIUM, cwe=issue.Cwe.MULTIPLE_BINDS, text="Possible binding to all interfaces.", ) ================================================ FILE: bandit/plugins/general_hardcoded_password.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 import ast import re import bandit from bandit.core import issue from bandit.core import test_properties as test RE_WORDS = "(pas+wo?r?d|pass(phrase)?|pwd|token|secrete?)" RE_CANDIDATES = re.compile( "(^{0}$|_{0}_|^{0}_|_{0}$)".format(RE_WORDS), re.IGNORECASE ) def _report(value, lineno=None): return bandit.Issue( severity=bandit.LOW, confidence=bandit.MEDIUM, cwe=issue.Cwe.HARD_CODED_PASSWORD, text=f"Possible hardcoded password: '{value}'", lineno=lineno, ) @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 - assigned to a class attribute 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 CWE: CWE-259 (https://cwe.mitre.org/data/definitions/259.html) 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 - https://cwe.mitre.org/data/definitions/259.html .. versionadded:: 0.9.0 .. versionchanged:: 1.7.3 CWE information added """ node = context.node if isinstance(node._bandit_parent, ast.Assign): # looks for "candidate='some_string'" for targ in node._bandit_parent.targets: if isinstance(targ, ast.Name) and RE_CANDIDATES.search(targ.id): return _report(node.value) elif isinstance(targ, ast.Attribute) and RE_CANDIDATES.search( targ.attr ): return _report(node.value) elif ( isinstance(node._bandit_parent, ast.Dict) and node in node._bandit_parent.keys and RE_CANDIDATES.search(node.value) ): # looks for "{'candidate': 'some_string'}" dict_node = node._bandit_parent pos = dict_node.keys.index(node) value_node = dict_node.values[pos] if isinstance(value_node, ast.Constant): return _report(value_node.value) elif isinstance( node._bandit_parent, ast.Subscript ) and RE_CANDIDATES.search(node.value): # Py39+: looks for "dict[candidate]='some_string'" # subscript -> index -> string assign = node._bandit_parent._bandit_parent if ( isinstance(assign, ast.Assign) and isinstance(assign.value, ast.Constant) and isinstance(assign.value.value, str) ): return _report(assign.value.value) elif isinstance(node._bandit_parent, ast.Index) and RE_CANDIDATES.search( node.value ): # looks for "dict[candidate]='some_string'" # assign -> subscript -> index -> string assign = node._bandit_parent._bandit_parent._bandit_parent if ( isinstance(assign, ast.Assign) and isinstance(assign.value, ast.Constant) and isinstance(assign.value.value, str) ): return _report(assign.value.value) elif isinstance(node._bandit_parent, ast.Compare): # looks for "candidate == 'some_string'" comp = node._bandit_parent if isinstance(comp.left, ast.Name): if RE_CANDIDATES.search(comp.left.id): if isinstance( comp.comparators[0], ast.Constant ) and isinstance(comp.comparators[0].value, str): return _report(comp.comparators[0].value) elif isinstance(comp.left, ast.Attribute): if RE_CANDIDATES.search(comp.left.attr): if isinstance( comp.comparators[0], ast.Constant ) and isinstance(comp.comparators[0].value, str): return _report(comp.comparators[0].value) @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 CWE: CWE-259 (https://cwe.mitre.org/data/definitions/259.html) Location: ./examples/hardcoded-passwords.py:16 15 16 doLogin(password="blerg") .. seealso:: - https://www.owasp.org/index.php/Use_of_hard-coded_password - https://cwe.mitre.org/data/definitions/259.html .. versionadded:: 0.9.0 .. versionchanged:: 1.7.3 CWE information added """ # looks for "function(candidate='some_string')" for kw in context.node.keywords: if ( isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str) and RE_CANDIDATES.search(kw.arg) ): return _report(kw.value.value, lineno=kw.value.lineno) @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. We do not report on None values which can be legitimately used as a default value, when initializing a function or class. **Config Options:** None :Example: .. code-block:: none >> Issue: [B107:hardcoded_password_default] Possible hardcoded password: 'Admin' Severity: Low Confidence: Medium CWE: CWE-259 (https://cwe.mitre.org/data/definitions/259.html) 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 - https://cwe.mitre.org/data/definitions/259.html .. versionadded:: 0.9.0 .. versionchanged:: 1.7.3 CWE information added """ # 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, ast.arg)): # Skip if the default value is None if val is None or ( isinstance(val, ast.Constant) and val.value is None ): continue if ( isinstance(val, ast.Constant) and isinstance(val.value, str) and RE_CANDIDATES.search(key.arg) ): return _report(val.value) ================================================ FILE: bandit/plugins/general_hardcoded_tmp.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 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 **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 CWE: CWE-377 (https://cwe.mitre.org/data/definitions/377.html) 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 - https://cwe.mitre.org/data/definitions/377.html .. versionadded:: 0.9.0 .. versionchanged:: 1.7.3 CWE information added """ # noqa: E501 import bandit from bandit.core import issue 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"]} # nosec: B108 @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"] # nosec: B108 if any(context.string_val.startswith(s) for s in tmp_dirs): return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.MEDIUM, cwe=issue.Cwe.INSECURE_TEMP_FILE, text="Probable insecure usage of temp file/directory.", ) ================================================ FILE: bandit/plugins/hashlib_insecure_functions.py ================================================ # # SPDX-License-Identifier: Apache-2.0 r""" ====================================================================== B324: Test use of insecure md4, md5, or sha1 hash functions in hashlib ====================================================================== This plugin checks for the usage of the insecure MD4, MD5, or SHA1 hash functions in ``hashlib`` and ``crypt``. The ``hashlib.new`` function provides the ability to construct a new hashing object using the named algorithm. This can be used to create insecure hash functions like MD4 and MD5 if they are passed as algorithm names to this function. This check does additional checking for usage of keyword usedforsecurity on all function variations of hashlib. Similar to ``hashlib``, this plugin also checks for usage of one of the ``crypt`` module's weak hashes. ``crypt`` also permits MD5 among other weak hash variants. :Example: .. code-block:: none >> Issue: [B324:hashlib] Use of weak MD4, MD5, or SHA1 hash for security. Consider usedforsecurity=False Severity: High Confidence: High CWE: CWE-327 (https://cwe.mitre.org/data/definitions/327.html) Location: examples/hashlib_new_insecure_functions.py:3:0 More Info: https://bandit.readthedocs.io/en/latest/plugins/b324_hashlib.html 2 3 hashlib.new('md5') 4 .. seealso:: - https://cwe.mitre.org/data/definitions/327.html .. versionadded:: 1.5.0 .. versionchanged:: 1.7.3 CWE information added .. versionchanged:: 1.7.6 Added check for the crypt module weak hashes """ # noqa: E501 import bandit from bandit.core import issue from bandit.core import test_properties as test WEAK_HASHES = ("md4", "md5", "sha", "sha1") WEAK_CRYPT_HASHES = ("METHOD_CRYPT", "METHOD_MD5", "METHOD_BLOWFISH") def _hashlib_func(context, func): keywords = context.call_keywords if func in WEAK_HASHES: if keywords.get("usedforsecurity", "True") == "True": return bandit.Issue( severity=bandit.HIGH, confidence=bandit.HIGH, cwe=issue.Cwe.BROKEN_CRYPTO, text=f"Use of weak {func.upper()} hash for security. " "Consider usedforsecurity=False", lineno=context.node.lineno, ) elif func == "new": args = context.call_args name = args[0] if args else keywords.get("name", None) if isinstance(name, str) and name.lower() in WEAK_HASHES: if keywords.get("usedforsecurity", "True") == "True": return bandit.Issue( severity=bandit.HIGH, confidence=bandit.HIGH, cwe=issue.Cwe.BROKEN_CRYPTO, text=f"Use of weak {name.upper()} hash for " "security. Consider usedforsecurity=False", lineno=context.node.lineno, ) def _crypt_crypt(context, func): args = context.call_args keywords = context.call_keywords if func == "crypt": name = args[1] if len(args) > 1 else keywords.get("salt", None) if isinstance(name, str) and name in WEAK_CRYPT_HASHES: return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.HIGH, cwe=issue.Cwe.BROKEN_CRYPTO, text=f"Use of insecure crypt.{name.upper()} hash function.", lineno=context.node.lineno, ) elif func == "mksalt": name = args[0] if args else keywords.get("method", None) if isinstance(name, str) and name in WEAK_CRYPT_HASHES: return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.HIGH, cwe=issue.Cwe.BROKEN_CRYPTO, text=f"Use of insecure crypt.{name.upper()} hash function.", lineno=context.node.lineno, ) @test.test_id("B324") @test.checks("Call") def hashlib(context): if isinstance(context.call_function_name_qual, str): qualname_list = context.call_function_name_qual.split(".") func = qualname_list[-1] if "hashlib" in qualname_list: return _hashlib_func(context, func) elif "crypt" in qualname_list and func in ("crypt", "mksalt"): return _crypt_crypt(context, func) ================================================ FILE: bandit/plugins/huggingface_unsafe_download.py ================================================ # SPDX-License-Identifier: Apache-2.0 r""" ================================================ B615: Test for unsafe Hugging Face Hub downloads ================================================ This plugin checks for unsafe downloads from Hugging Face Hub without proper integrity verification. Downloading models, datasets, or files without specifying a revision based on an immmutable revision (commit) can lead to supply chain attacks where malicious actors could replace model files and use an existing tag or branch name to serve malicious content. The secure approach is to: 1. Pin to specific revisions/commits when downloading models, files or datasets Common unsafe patterns: - ``AutoModel.from_pretrained("org/model-name")`` - ``AutoModel.from_pretrained("org/model-name", revision="main")`` - ``AutoModel.from_pretrained("org/model-name", revision="v1.0.0")`` - ``load_dataset("org/dataset-name")`` without revision - ``load_dataset("org/dataset-name", revision="main")`` - ``load_dataset("org/dataset-name", revision="v1.0")`` - ``AutoTokenizer.from_pretrained("org/model-name")`` - ``AutoTokenizer.from_pretrained("org/model-name", revision="main")`` - ``AutoTokenizer.from_pretrained("org/model-name", revision="v3.3.0")`` - ``hf_hub_download(repo_id="org/model_name", filename="file_name")`` - ``hf_hub_download(repo_id="org/model_name", filename="file_name", revision="main" )`` - ``hf_hub_download(repo_id="org/model_name", filename="file_name", revision="v2.0.0" )`` - ``snapshot_download(repo_id="org/model_name")`` - ``snapshot_download(repo_id="org/model_name", revision="main")`` - ``snapshot_download(repo_id="org/model_name", revision="refs/pr/1")`` :Example: .. code-block:: none >> Issue: Unsafe Hugging Face Hub download without revision pinning Severity: Medium Confidence: High CWE: CWE-494 (https://cwe.mitre.org/data/definitions/494.html) Location: examples/huggingface_unsafe_download.py:8 7 # Unsafe: no revision specified 8 model = AutoModel.from_pretrained("org/model_name") 9 .. seealso:: - https://cwe.mitre.org/data/definitions/494.html - https://huggingface.co/docs/huggingface_hub/en/guides/download .. versionadded:: 1.8.6 """ import ast import string import bandit from bandit.core import issue from bandit.core import test_properties as test @test.checks("Call") @test.test_id("B615") def huggingface_unsafe_download(context): """ This plugin checks for unsafe artifact download from Hugging Face Hub without immutable/reproducible revision pinning. """ # Check if any HuggingFace-related modules are imported hf_modules = [ "transformers", "datasets", "huggingface_hub", ] # Check if any HF modules are imported hf_imported = any( context.is_module_imported_like(module) for module in hf_modules ) if not hf_imported: return qualname = context.call_function_name_qual if not isinstance(qualname, str): return unsafe_patterns = { # transformers library patterns "from_pretrained": ["transformers"], # datasets library patterns "load_dataset": ["datasets"], # huggingface_hub patterns "hf_hub_download": ["huggingface_hub"], "snapshot_download": ["huggingface_hub"], "repository_id": ["huggingface_hub"], } qualname_parts = qualname.split(".") func_name = qualname_parts[-1] if func_name not in unsafe_patterns: return required_modules = unsafe_patterns[func_name] if not any(module in qualname_parts for module in required_modules): return # Check for revision parameter (the key security control). # First, check the raw AST to see if a revision/commit_id keyword was # passed as a non-literal expression (variable, attribute, subscript, # function call, etc.). In those cases we cannot statically determine # the value, so we give the user the benefit of the doubt. call_node = context._context.get("call") if call_node is not None: for kw in getattr(call_node, "keywords", []): if kw.arg in ("revision", "commit_id") and not isinstance( kw.value, ast.Constant ): return revision_value = context.get_call_arg_value("revision") commit_id_value = context.get_call_arg_value("commit_id") # Check if a revision or commit_id is specified revision_to_check = revision_value or commit_id_value if revision_to_check is not None: # Check if it's a secure revision (looks like a commit hash) # Commit hashes: 40 chars (full SHA) or 7+ chars (short SHA) if isinstance(revision_to_check, str): # Remove quotes if present revision_str = str(revision_to_check).strip("\"'") # Check if it looks like a commit hash (hexadecimal string) # Must be at least 7 characters and all hexadecimal is_hex = all(c in string.hexdigits for c in revision_str) if len(revision_str) >= 7 and is_hex: # This looks like a commit hash, which is secure return # Edge case: check if this is a local path (starts with ./ or /) first_arg = context.get_call_arg_at_position(0) if first_arg and isinstance(first_arg, str): if first_arg.startswith(("./", "/", "../")): # Local paths are generally safer return return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.HIGH, text=( f"Unsafe Hugging Face Hub download without revision pinning " f"in {func_name}()" ), cwe=issue.Cwe.DOWNLOAD_OF_CODE_WITHOUT_INTEGRITY_CHECK, lineno=context.get_lineno_for_call_arg(func_name), ) ================================================ FILE: bandit/plugins/injection_paramiko.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 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" method 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 CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html) Location: ./examples/paramiko_injection.py:4 3 # this is not safe 4 paramiko.exec_command('something; really; unsafe') 5 .. seealso:: - https://security.openstack.org - https://github.com/paramiko/paramiko - https://www.owasp.org/index.php/Command_Injection - https://cwe.mitre.org/data/definitions/78.html .. versionadded:: 0.12.0 .. versionchanged:: 1.7.3 CWE information added """ import bandit from bandit.core import issue 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"]: return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.MEDIUM, cwe=issue.Cwe.OS_COMMAND_INJECTION, text=issue_text, ) ================================================ FILE: bandit/plugins/injection_shell.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 import ast import re import bandit from bandit.core import issue from bandit.core import test_properties as test # yuck, regex: starts with a windows drive letter (eg C:) # or one of our path delimiter characters (/, \, .) full_path_match = re.compile(r"^(?:[A-Za-z](?=\:)|[\\\/\.])") def _evaluate_shell_call(context): no_formatting = isinstance( context.node.args[0], ast.Constant ) and isinstance(context.node.args[0].value, 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", "subprocess.run", ], # 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", "subprocess.getoutput", "subprocess.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", ], } def has_shell(context): keywords = context.node.keywords result = False if "shell" in context.call_keywords: for key in keywords: if key.arg == "shell": val = key.value if isinstance(val, ast.Constant) and ( isinstance(val.value, int) or isinstance(val.value, float) or isinstance(val.value, complex) ): result = bool(val.value) elif isinstance(val, ast.List): result = bool(val.elts) elif isinstance(val, ast.Dict): result = bool(val.keys) elif isinstance(val, ast.Name) and val.id in ["False", "None"]: result = False elif isinstance(val, ast.Constant): result = val.value else: result = True return result @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 CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html) 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 CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html) 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 CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html) 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/3/library/subprocess.html#frequently-used-arguments - https://security.openstack.org/guidelines/dg_use-subprocess-securely.html - https://security.openstack.org/guidelines/dg_avoid-shell-true.html - https://cwe.mitre.org/data/definitions/78.html .. versionadded:: 0.9.0 .. versionchanged:: 1.7.3 CWE information added """ # noqa: E501 if config and context.call_function_name_qual in config["subprocess"]: if has_shell(context): if len(context.call_args) > 0: sev = _evaluate_shell_call(context) if sev == bandit.LOW: return bandit.Issue( severity=bandit.LOW, confidence=bandit.HIGH, cwe=issue.Cwe.OS_COMMAND_INJECTION, 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, cwe=issue.Cwe.OS_COMMAND_INJECTION, 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 without 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 CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html) Location: ./examples/subprocess_shell.py:23 22 23 subprocess.check_output(['/bin/ls', '-l']) 24 .. seealso:: - https://security.openstack.org - https://docs.python.org/3/library/subprocess.html#frequently-used-arguments - https://security.openstack.org/guidelines/dg_avoid-shell-true.html - https://security.openstack.org/guidelines/dg_use-subprocess-securely.html - https://cwe.mitre.org/data/definitions/78.html .. versionadded:: 0.9.0 .. versionchanged:: 1.7.3 CWE information added """ # noqa: E501 if config and context.call_function_name_qual in config["subprocess"]: if not has_shell(context): return bandit.Issue( severity=bandit.LOW, confidence=bandit.HIGH, cwe=issue.Cwe.OS_COMMAND_INJECTION, 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 execute_with_timeout] :Example: .. code-block:: none >> Issue: Function call with shell=True parameter identified, possible security issue. Severity: Medium Confidence: High CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html) 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 - https://cwe.mitre.org/data/definitions/78.html .. versionadded:: 0.9.0 .. versionchanged:: 1.7.3 CWE information added """ # noqa: E501 if config and context.call_function_name_qual not in config["subprocess"]: if has_shell(context): return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.LOW, cwe=issue.Cwe.OS_COMMAND_INJECTION, 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 LOW 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 - subprocess.getoutput - subprocess.getstatusoutput :Example: .. code-block:: none >> Issue: Starting a process with a shell: check for injection. Severity: Low Confidence: Medium CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html) Location: examples/os_system.py:3 2 3 os.system('/bin/echo hi') .. seealso:: - https://security.openstack.org - https://docs.python.org/3/library/os.html#os.system - https://docs.python.org/3/library/subprocess.html#frequently-used-arguments - https://security.openstack.org/guidelines/dg_use-subprocess-securely.html - https://cwe.mitre.org/data/definitions/78.html .. versionadded:: 0.10.0 .. versionchanged:: 1.7.3 CWE information added """ # noqa: E501 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, cwe=issue.Cwe.OS_COMMAND_INJECTION, 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, cwe=issue.Cwe.OS_COMMAND_INJECTION, 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 CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html) 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/3/library/os.html#os.system - https://docs.python.org/3/library/subprocess.html#frequently-used-arguments - https://security.openstack.org/guidelines/dg_use-subprocess-securely.html - https://cwe.mitre.org/data/definitions/78.html .. versionadded:: 0.10.0 .. versionchanged:: 1.7.3 CWE information added """ # noqa: E501 if config and context.call_function_name_qual in config["no_shell"]: return bandit.Issue( severity=bandit.LOW, confidence=bandit.MEDIUM, cwe=issue.Cwe.OS_COMMAND_INJECTION, 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 CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html) 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/3/library/os.html#process-management - https://cwe.mitre.org/data/definitions/78.html .. versionadded:: 0.13.0 .. versionchanged:: 1.7.3 CWE information added """ 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) and node.elts: node = node.elts[0] # make sure the param is a string literal and not a var name if ( isinstance(node, ast.Constant) and isinstance(node.value, str) and not full_path_match.match(node.value) ): return bandit.Issue( severity=bandit.LOW, confidence=bandit.HIGH, cwe=issue.Cwe.OS_COMMAND_INJECTION, text="Starting a process with a partial executable path", ) ================================================ FILE: bandit/plugins/injection_sql.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 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 + ... - "SELECT {} FROM derp;".format(var) - f"SELECT foo FROM bar WHERE id = {product}" 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) Use of str.replace in the string construction can also be dangerous. For example: - "SELECT * FROM foo WHERE id = '[VALUE]'".replace("[VALUE]", identifier) However, such cases are always reported with LOW confidence to compensate for false positives, since valid uses of str.replace can be common. :Example: .. code-block:: none >> Issue: Possible SQL injection vector through string-based query construction. Severity: Medium Confidence: Low CWE: CWE-89 (https://cwe.mitre.org/data/definitions/89.html) Location: ./examples/sql_statements.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 - https://cwe.mitre.org/data/definitions/89.html .. versionadded:: 0.9.0 .. versionchanged:: 1.7.3 CWE information added .. versionchanged:: 1.7.7 Flag when str.replace is used in the string construction """ # noqa: E501 import ast import re import bandit from bandit.core import issue from bandit.core import test_properties as test from bandit.core import utils SIMPLE_SQL_RE = re.compile( r"(select\s.*from\s|" r"delete\s+from\s|" r"insert\s+into\s.*values[\s(]|" r"update\s.*set\s)", re.IGNORECASE | re.DOTALL, ) def _check_string(data): return SIMPLE_SQL_RE.search(data) is not None def _evaluate_ast(node): wrapper = None statement = "" str_replace = False if isinstance(node._bandit_parent, ast.BinOp): out = utils.concat_string(node, node._bandit_parent) wrapper = out[0]._bandit_parent statement = out[1] elif isinstance( node._bandit_parent, ast.Attribute ) and node._bandit_parent.attr in ("format", "replace"): statement = node.value # Hierarchy for "".format() is Wrapper -> Call -> Attribute -> Str wrapper = node._bandit_parent._bandit_parent._bandit_parent if node._bandit_parent.attr == "replace": str_replace = True elif hasattr(ast, "JoinedStr") and isinstance( node._bandit_parent, ast.JoinedStr ): substrings = [ child for child in node._bandit_parent.values if isinstance(child, ast.Constant) and isinstance(child.value, str) ] # JoinedStr consists of list of Constant and FormattedValue # instances. Let's perform one test for the whole string # and abandon all parts except the first one to raise one # failed test instead of many for the same SQL statement. if substrings and node == substrings[0]: statement = "".join([str(child.value) for child in substrings]) wrapper = node._bandit_parent._bandit_parent if isinstance(wrapper, ast.Call): # wrapped in "execute" call? names = ["execute", "executemany"] name = utils.get_called_name(wrapper) return (name in names, statement, str_replace) else: return (False, statement, str_replace) @test.checks("Str") @test.test_id("B608") def hardcoded_sql_expressions(context): execute_call, statement, str_replace = _evaluate_ast(context.node) if _check_string(statement): return bandit.Issue( severity=bandit.MEDIUM, confidence=( bandit.MEDIUM if execute_call and not str_replace else bandit.LOW ), cwe=issue.Cwe.SQL_INJECTION, text="Possible SQL injection vector through string-based " "query construction.", ) ================================================ FILE: bandit/plugins/injection_wildcard.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 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 CWE-78 (https://cwe.mitre.org/data/definitions/78.html) 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 CWE-78 (https://cwe.mitre.org/data/definitions/78.html) 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 - https://www.defensecode.com/public/DefenseCode_Unix_WildCards_Gone_Wild.txt - https://cwe.mitre.org/data/definitions/78.html .. versionadded:: 0.9.0 .. versionchanged:: 1.7.3 CWE information added """ import bandit from bandit.core import issue 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 += f" {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, cwe=issue.Cwe.IMPROPER_WILDCARD_NEUTRALIZATION, text="Possible wildcard injection in call: %s" % context.call_function_name_qual, lineno=context.get_lineno_for_call_arg("shell"), ) ================================================ FILE: bandit/plugins/insecure_ssl_tls.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 import bandit from bandit.core import issue 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", "PROTOCOL_TLSv1_1", "TLSv1_1_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 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 CWE: CWE-327 (https://cwe.mitre.org/data/definitions/327.html) 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:: - :func:`ssl_with_bad_defaults` - :func:`ssl_with_no_version` - https://heartbleed.com/ - https://en.wikipedia.org/wiki/POODLE - https://security.openstack.org/guidelines/dg_move-data-securely.html - https://cwe.mitre.org/data/definitions/327.html .. versionadded:: 0.9.0 .. versionchanged:: 1.7.3 CWE information added .. versionchanged:: 1.7.5 Added TLS 1.1 """ 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, cwe=issue.Cwe.BROKEN_CRYPTO, 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, cwe=issue.Cwe.BROKEN_CRYPTO, 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, cwe=issue.Cwe.BROKEN_CRYPTO, 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. **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 CWE: CWE-327 (https://cwe.mitre.org/data/definitions/327.html) Location: ./examples/ssl-insecure-version.py:28 27 28 def open_ssl_socket(version=SSL.SSLv2_METHOD): 29 pass .. seealso:: - :func:`ssl_with_bad_version` - :func:`ssl_with_no_version` - https://heartbleed.com/ - https://en.wikipedia.org/wiki/POODLE - https://security.openstack.org/guidelines/dg_move-data-securely.html .. versionadded:: 0.9.0 .. versionchanged:: 1.7.3 CWE information added .. versionchanged:: 1.7.5 Added TLS 1.1 """ 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, cwe=issue.Cwe.BROKEN_CRYPTO, 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. **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 CWE: CWE-327 (https://cwe.mitre.org/data/definitions/327.html) Location: ./examples/ssl-insecure-version.py:23 22 23 ssl.wrap_socket() 24 .. seealso:: - :func:`ssl_with_bad_version` - :func:`ssl_with_bad_defaults` - https://heartbleed.com/ - https://en.wikipedia.org/wiki/POODLE - https://security.openstack.org/guidelines/dg_move-data-securely.html .. versionadded:: 0.9.0 .. versionchanged:: 1.7.3 CWE information added """ 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, cwe=issue.Cwe.BROKEN_CRYPTO, 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"), ) ================================================ FILE: bandit/plugins/jinja2_templates.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 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 CWE: CWE-94 (https://cwe.mitre.org/data/definitions/94.html) 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 or use the select_autoescape function to mitigate XSS vulnerabilities. Severity: High Confidence: High CWE: CWE-94 (https://cwe.mitre.org/data/definitions/94.html) Location: ./examples/jinja2_templating.py:15 14 15 Environment(loader=templateLoader, 16 load=templateLoader) 17 18 Environment(autoescape=select_autoescape(['html', 'htm', 'xml']), 19 loader=templateLoader) .. seealso:: - `OWASP XSS `__ - https://realpython.com/primer-on-jinja-templating/ - https://jinja.palletsprojects.com/en/2.11.x/api/#autoescaping - https://security.openstack.org/guidelines/dg_cross-site-scripting-xss.html - https://cwe.mitre.org/data/definitions/94.html .. versionadded:: 0.10.0 .. versionchanged:: 1.7.3 CWE information added """ import ast import bandit from bandit.core import issue 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, cwe=issue.Cwe.CODE_INJECTION, text="Using jinja2 templates with autoescape=" "False is dangerous and can lead to XSS. " "Use autoescape=True or use the " "select_autoescape function to mitigate XSS " "vulnerabilities.", ) # found autoescape if getattr(node, "arg", None) == "autoescape": value = getattr(node, "value", None) if ( getattr(value, "id", None) == "True" or getattr(value, "value", None) is True ): return # Check if select_autoescape function is used. elif isinstance(value, ast.Call) and ( getattr(value.func, "attr", None) == "select_autoescape" or getattr(value.func, "id", None) == "select_autoescape" ): return else: return bandit.Issue( severity=bandit.HIGH, confidence=bandit.MEDIUM, cwe=issue.Cwe.CODE_INJECTION, text="Using jinja2 templates with autoescape=" "False is dangerous and can lead to XSS. " "Ensure autoescape=True or use the " "select_autoescape function to mitigate " "XSS vulnerabilities.", ) # We haven't found a keyword named autoescape, indicating default # behavior return bandit.Issue( severity=bandit.HIGH, confidence=bandit.HIGH, cwe=issue.Cwe.CODE_INJECTION, text="By default, jinja2 sets autoescape to False. Consider " "using autoescape=True or use the select_autoescape " "function to mitigate XSS vulnerabilities.", ) ================================================ FILE: bandit/plugins/logging_config_insecure_listen.py ================================================ # Copyright (c) 2022 Rajesh Pangare # # SPDX-License-Identifier: Apache-2.0 r""" ==================================================== B612: Test for insecure use of logging.config.listen ==================================================== This plugin test checks for the unsafe usage of the ``logging.config.listen`` function. The logging.config.listen function provides the ability to listen for external configuration files on a socket server. Because portions of the configuration are passed through eval(), use of this function may open its users to a security risk. While the function only binds to a socket on localhost, and so does not accept connections from remote machines, there are scenarios where untrusted code could be run under the account of the process which calls listen(). logging.config.listen provides the ability to verify bytes received across the socket with signature verification or encryption/decryption. :Example: .. code-block:: none >> Issue: [B612:logging_config_listen] Use of insecure logging.config.listen detected. Severity: Medium Confidence: High CWE: CWE-94 (https://cwe.mitre.org/data/definitions/94.html) Location: examples/logging_config_insecure_listen.py:3:4 2 3 t = logging.config.listen(9999) .. seealso:: - https://docs.python.org/3/library/logging.config.html#logging.config.listen .. versionadded:: 1.7.5 """ import bandit from bandit.core import issue from bandit.core import test_properties as test @test.checks("Call") @test.test_id("B612") def logging_config_insecure_listen(context): if ( context.call_function_name_qual == "logging.config.listen" and "verify" not in context.call_keywords ): return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.HIGH, cwe=issue.Cwe.CODE_INJECTION, text="Use of insecure logging.config.listen detected.", ) ================================================ FILE: bandit/plugins/mako_templates.py ================================================ # # SPDX-License-Identifier: Apache-2.0 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 CWE: CWE-80 (https://cwe.mitre.org/data/definitions/80.html) Location: ./examples/mako_templating.py:10 9 10 mako.template.Template("hern") 11 template.Template("hern") .. seealso:: - https://www.makotemplates.org/ - `OWASP XSS `__ - https://security.openstack.org/guidelines/dg_cross-site-scripting-xss.html - https://cwe.mitre.org/data/definitions/80.html .. versionadded:: 0.10.0 .. versionchanged:: 1.7.3 CWE information added """ import bandit from bandit.core import issue 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, cwe=issue.Cwe.BASIC_XSS, 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 }.", ) ================================================ FILE: bandit/plugins/markupsafe_markup_xss.py ================================================ # Copyright (c) 2025 David Salvisberg # # SPDX-License-Identifier: Apache-2.0 r""" ============================================ B704: Potential XSS on markupsafe.Markup use ============================================ ``markupsafe.Markup`` does not perform any escaping, so passing dynamic content, like f-strings, variables or interpolated strings will potentially lead to XSS vulnerabilities, especially if that data was submitted by users. Instead you should interpolate the resulting ``markupsafe.Markup`` object, which will perform escaping, or use ``markupsafe.escape``. **Config Options:** This plugin allows you to specify additional callable that should be treated like ``markupsafe.Markup``. By default we recognize ``flask.Markup`` as an alias, but there are other subclasses or similar classes in the wild that you may wish to treat the same. Additionally there is a whitelist for callable names, whose result may be safely passed into ``markupsafe.Markup``. This is useful for escape functions like e.g. ``bleach.clean`` which don't themselves return ``markupsafe.Markup``, so they need to be wrapped. Take care when using this setting, since incorrect use may introduce false negatives. These two options can be set in a shared configuration section `markupsafe_xss`. .. code-block:: yaml markupsafe_xss: # Recognize additional aliases extend_markup_names: - webhelpers.html.literal - my_package.Markup # Allow the output of these functions to pass into Markup allowed_calls: - bleach.clean - my_package.sanitize :Example: .. code-block:: none >> Issue: [B704:markupsafe_markup_xss] Potential XSS with ``markupsafe.Markup`` detected. Do not use ``Markup`` on untrusted data. Severity: Medium Confidence: High CWE: CWE-79 (https://cwe.mitre.org/data/definitions/79.html) Location: ./examples/markupsafe_markup_xss.py:5:0 4 content = "" 5 Markup(f"unsafe {content}") 6 flask.Markup("unsafe {}".format(content)) .. seealso:: - https://pypi.org/project/MarkupSafe/ - https://markupsafe.palletsprojects.com/en/stable/escaping/#markupsafe.Markup - https://cwe.mitre.org/data/definitions/79.html .. versionadded:: 1.8.3 """ import ast import bandit from bandit.core import issue from bandit.core import test_properties as test from bandit.core.utils import get_call_name def gen_config(name): if name == "markupsafe_xss": return { "extend_markup_names": [], "allowed_calls": [], } @test.takes_config("markupsafe_xss") @test.checks("Call") @test.test_id("B704") def markupsafe_markup_xss(context, config): qualname = context.call_function_name_qual if qualname not in ("markupsafe.Markup", "flask.Markup"): if qualname not in config.get("extend_markup_names", []): # not a Markup call return None args = context.node.args if not args or isinstance(args[0], ast.Constant): # both no arguments and a constant are fine return None allowed_calls = config.get("allowed_calls", []) if ( allowed_calls and isinstance(args[0], ast.Call) and get_call_name(args[0], context.import_aliases) in allowed_calls ): # the argument contains a whitelisted call return None return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.HIGH, cwe=issue.Cwe.XSS, text=f"Potential XSS with ``{qualname}`` detected. Do " f"not use ``{context.call_function_name}`` on untrusted data.", ) ================================================ FILE: bandit/plugins/pytorch_load.py ================================================ # Copyright (c) 2024 Stacklok, Inc. # # SPDX-License-Identifier: Apache-2.0 r""" ================================== B614: Test for unsafe PyTorch load ================================== This plugin checks for unsafe use of `torch.load` and `torch.serialization.load`. Using `torch.load` or `torch.serialization.load` with untrusted data can lead to arbitrary code execution. There are two safe alternatives: 1. Use `torch.load` with `weights_only=True` where only tensor data is extracted, and no arbitrary Python objects are deserialized 2. Use the `safetensors` library from huggingface, which provides a safe deserialization mechanism With `weights_only=True`, PyTorch enforces a strict type check, ensuring that only torch.Tensor objects are loaded. :Example: .. code-block:: none >> Issue: Use of unsafe PyTorch load Severity: Medium Confidence: High CWE: CWE-502 (https://cwe.mitre.org/data/definitions/502.html) Location: examples/pytorch_load_save.py:8 7 loaded_model.load_state_dict(torch.load('model_weights.pth')) 8 another_model.load_state_dict(torch.load('model_weights.pth', map_location='cpu')) 9 10 print("Model loaded successfully!") .. seealso:: - https://cwe.mitre.org/data/definitions/502.html - https://pytorch.org/docs/stable/generated/torch.load.html#torch.load - https://github.com/huggingface/safetensors .. versionadded:: 1.7.10 """ import bandit from bandit.core import issue from bandit.core import test_properties as test @test.checks("Call") @test.test_id("B614") def pytorch_load(context): """ This plugin checks for unsafe use of `torch.load` and `torch.serialization.load`. Using `torch.load` or `torch.serialization.load` with untrusted data can lead to arbitrary code execution. The safe alternative is to use `weights_only=True` or the safetensors library. """ imported = context.is_module_imported_exact("torch") qualname = context.call_function_name_qual if not imported and isinstance(qualname, str): return if qualname in {"torch.load", "torch.serialization.load"}: # For torch.load, check if weights_only=True is specified weights_only = context.get_call_arg_value("weights_only") if weights_only == "True" or weights_only is True: return return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.HIGH, text="Use of unsafe PyTorch load", cwe=issue.Cwe.DESERIALIZATION_OF_UNTRUSTED_DATA, lineno=context.get_lineno_for_call_arg("load"), ) ================================================ FILE: bandit/plugins/request_without_timeout.py ================================================ # SPDX-License-Identifier: Apache-2.0 r""" ======================================= B113: Test for missing requests timeout ======================================= This plugin test checks for ``requests`` or ``httpx`` calls without a timeout specified. Nearly all production code should use this parameter in nearly all requests, Failure to do so can cause your program to hang indefinitely. When request methods are used without the timeout parameter set, Bandit will return a MEDIUM severity error. :Example: .. code-block:: none >> Issue: [B113:request_without_timeout] Call to requests without timeout Severity: Medium Confidence: Low CWE: CWE-400 (https://cwe.mitre.org/data/definitions/400.html) More Info: https://bandit.readthedocs.io/en/latest/plugins/b113_request_without_timeout.html Location: examples/requests-missing-timeout.py:3:0 2 3 requests.get('https://gmail.com') 4 requests.get('https://gmail.com', timeout=None) -------------------------------------------------- >> Issue: [B113:request_without_timeout] Call to requests with timeout set to None Severity: Medium Confidence: Low CWE: CWE-400 (https://cwe.mitre.org/data/definitions/400.html) More Info: https://bandit.readthedocs.io/en/latest/plugins/b113_request_without_timeout.html Location: examples/requests-missing-timeout.py:4:0 3 requests.get('https://gmail.com') 4 requests.get('https://gmail.com', timeout=None) 5 requests.get('https://gmail.com', timeout=5) .. seealso:: - https://requests.readthedocs.io/en/latest/user/advanced/#timeouts .. versionadded:: 1.7.5 .. versionchanged:: 1.7.10 Added check for httpx module """ # noqa: E501 import bandit from bandit.core import issue from bandit.core import test_properties as test @test.checks("Call") @test.test_id("B113") def request_without_timeout(context): HTTP_VERBS = {"get", "options", "head", "post", "put", "patch", "delete"} HTTPX_ATTRS = {"request", "stream", "Client", "AsyncClient"} | HTTP_VERBS qualname = context.call_function_name_qual.split(".")[0] if qualname == "requests" and context.call_function_name in HTTP_VERBS: # check for missing timeout if context.check_call_arg_value("timeout") is None: return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.LOW, cwe=issue.Cwe.UNCONTROLLED_RESOURCE_CONSUMPTION, text=f"Call to {qualname} without timeout", ) if ( qualname == "requests" and context.call_function_name in HTTP_VERBS or qualname == "httpx" and context.call_function_name in HTTPX_ATTRS ): # check for timeout=None if context.check_call_arg_value("timeout", "None"): return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.LOW, cwe=issue.Cwe.UNCONTROLLED_RESOURCE_CONSUMPTION, text=f"Call to {qualname} with timeout set to None", ) ================================================ FILE: bandit/plugins/snmp_security_check.py ================================================ # # Copyright (c) 2018 SolarWinds, Inc. # # SPDX-License-Identifier: Apache-2.0 import bandit from bandit.core import issue from bandit.core import test_properties as test @test.checks("Call") @test.test_id("B508") def snmp_insecure_version_check(context): """**B508: Checking for insecure SNMP versions** This test is for checking for the usage of insecure SNMP version like v1, v2c Please update your code to use more secure versions of SNMP. :Example: .. code-block:: none >> Issue: [B508:snmp_insecure_version_check] The use of SNMPv1 and SNMPv2 is insecure. You should use SNMPv3 if able. Severity: Medium Confidence: High CWE: CWE-319 (https://cwe.mitre.org/data/definitions/319.html) Location: examples/snmp.py:4:4 More Info: https://bandit.readthedocs.io/en/latest/plugins/b508_snmp_insecure_version_check.html 3 # SHOULD FAIL 4 a = CommunityData('public', mpModel=0) 5 # SHOULD FAIL .. seealso:: - http://snmplabs.com/pysnmp/examples/hlapi/asyncore/sync/manager/cmdgen/snmp-versions.html - https://cwe.mitre.org/data/definitions/319.html .. versionadded:: 1.7.2 .. versionchanged:: 1.7.3 CWE information added """ # noqa: E501 if context.call_function_name_qual == "pysnmp.hlapi.CommunityData": # We called community data. Lets check our args if context.check_call_arg_value( "mpModel", 0 ) or context.check_call_arg_value("mpModel", 1): return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.HIGH, cwe=issue.Cwe.CLEARTEXT_TRANSMISSION, text="The use of SNMPv1 and SNMPv2 is insecure. " "You should use SNMPv3 if able.", lineno=context.get_lineno_for_call_arg("CommunityData"), ) @test.checks("Call") @test.test_id("B509") def snmp_crypto_check(context): """**B509: Checking for weak cryptography** This test is for checking for the usage of insecure SNMP cryptography: v3 using noAuthNoPriv. Please update your code to use more secure versions of SNMP. For example: Instead of: `CommunityData('public', mpModel=0)` Use (Defaults to usmHMACMD5AuthProtocol and usmDESPrivProtocol `UsmUserData("securityName", "authName", "privName")` :Example: .. code-block:: none >> Issue: [B509:snmp_crypto_check] You should not use SNMPv3 without encryption. noAuthNoPriv & authNoPriv is insecure Severity: Medium CWE: CWE-319 (https://cwe.mitre.org/data/definitions/319.html) Confidence: High Location: examples/snmp.py:6:11 More Info: https://bandit.readthedocs.io/en/latest/plugins/b509_snmp_crypto_check.html 5 # SHOULD FAIL 6 insecure = UsmUserData("securityName") 7 # SHOULD FAIL .. seealso:: - http://snmplabs.com/pysnmp/examples/hlapi/asyncore/sync/manager/cmdgen/snmp-versions.html - https://cwe.mitre.org/data/definitions/319.html .. versionadded:: 1.7.2 .. versionchanged:: 1.7.3 CWE information added """ # noqa: E501 if context.call_function_name_qual == "pysnmp.hlapi.UsmUserData": if context.call_args_count < 3: return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.HIGH, cwe=issue.Cwe.CLEARTEXT_TRANSMISSION, text="You should not use SNMPv3 without encryption. " "noAuthNoPriv & authNoPriv is insecure", lineno=context.get_lineno_for_call_arg("UsmUserData"), ) ================================================ FILE: bandit/plugins/ssh_no_host_key_verification.py ================================================ # Copyright (c) 2018 VMware, Inc. # # SPDX-License-Identifier: Apache-2.0 r""" ========================================== B507: Test for missing host key validation ========================================== Encryption in general is typically critical to the security of many applications. Using SSH 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 host keys during the connection initialization phase of SSH. When paramiko methods are used, host keys are verified by default. If host key verification is disabled, Bandit will return a HIGH severity error. :Example: .. code-block:: none >> Issue: [B507:ssh_no_host_key_verification] Paramiko call with policy set to automatically trust the unknown host key. Severity: High Confidence: Medium CWE: CWE-295 (https://cwe.mitre.org/data/definitions/295.html) Location: examples/no_host_key_verification.py:4 3 ssh_client = client.SSHClient() 4 ssh_client.set_missing_host_key_policy(client.AutoAddPolicy) 5 ssh_client.set_missing_host_key_policy(client.WarningPolicy) .. versionadded:: 1.5.1 .. versionchanged:: 1.7.3 CWE information added """ import ast import bandit from bandit.core import issue from bandit.core import test_properties as test @test.checks("Call") @test.test_id("B507") def ssh_no_host_key_verification(context): if ( context.is_module_imported_like("paramiko") and context.call_function_name == "set_missing_host_key_policy" and context.node.args ): policy_argument = context.node.args[0] policy_argument_value = None if isinstance(policy_argument, ast.Attribute): policy_argument_value = policy_argument.attr elif isinstance(policy_argument, ast.Name): policy_argument_value = policy_argument.id elif isinstance(policy_argument, ast.Call): if isinstance(policy_argument.func, ast.Attribute): policy_argument_value = policy_argument.func.attr elif isinstance(policy_argument.func, ast.Name): policy_argument_value = policy_argument.func.id if policy_argument_value in ["AutoAddPolicy", "WarningPolicy"]: return bandit.Issue( severity=bandit.HIGH, confidence=bandit.MEDIUM, cwe=issue.Cwe.IMPROPER_CERT_VALIDATION, text="Paramiko call with policy set to automatically trust " "the unknown host key.", lineno=context.get_lineno_for_call_arg( "set_missing_host_key_policy" ), ) ================================================ FILE: bandit/plugins/tarfile_unsafe_members.py ================================================ # # SPDX-License-Identifier: Apache-2.0 # r""" ================================= B202: Test for tarfile.extractall ================================= This plugin will look for usage of ``tarfile.extractall()`` Severity are set as follows: * ``tarfile.extractall(members=function(tarfile))`` - LOW * ``tarfile.extractall(members=?)`` - member is not a function - MEDIUM * ``tarfile.extractall()`` - members from the archive is trusted - HIGH Use ``tarfile.extractall(members=function_name)`` and define a function that will inspect each member. Discard files that contain a directory traversal sequences such as ``../`` or ``\..`` along with all special filetypes unless you explicitly need them. :Example: .. code-block:: none >> Issue: [B202:tarfile_unsafe_members] tarfile.extractall used without any validation. You should check members and discard dangerous ones Severity: High Confidence: High CWE: CWE-22 (https://cwe.mitre.org/data/definitions/22.html) Location: examples/tarfile_extractall.py:8 More Info: https://bandit.readthedocs.io/en/latest/plugins/b202_tarfile_unsafe_members.html 7 tar = tarfile.open(filename) 8 tar.extractall(path=tempfile.mkdtemp()) 9 tar.close() .. seealso:: - https://docs.python.org/3/library/tarfile.html#tarfile.TarFile.extractall - https://docs.python.org/3/library/tarfile.html#tarfile.TarInfo .. versionadded:: 1.7.5 .. versionchanged:: 1.7.8 Added check for filter parameter """ import ast import bandit from bandit.core import issue from bandit.core import test_properties as test def exec_issue(level, members=""): if level == bandit.LOW: return bandit.Issue( severity=bandit.LOW, confidence=bandit.LOW, cwe=issue.Cwe.PATH_TRAVERSAL, text="Usage of tarfile.extractall(members=function(tarfile)). " "Make sure your function properly discards dangerous members " "{members}).".format(members=members), ) elif level == bandit.MEDIUM: return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.MEDIUM, cwe=issue.Cwe.PATH_TRAVERSAL, text="Found tarfile.extractall(members=?) but couldn't " "identify the type of members. " "Check if the members were properly validated " "{members}).".format(members=members), ) else: return bandit.Issue( severity=bandit.HIGH, confidence=bandit.HIGH, cwe=issue.Cwe.PATH_TRAVERSAL, text="tarfile.extractall used without any validation. " "Please check and discard dangerous members.", ) def get_members_value(context): for keyword in context.node.keywords: if keyword.arg == "members": arg = keyword.value if isinstance(arg, ast.Call): return {"Function": arg.func.id} else: value = arg.id if isinstance(arg, ast.Name) else arg return {"Other": value} def is_filter_data(context): for keyword in context.node.keywords: if keyword.arg == "filter": arg = keyword.value return isinstance(arg, ast.Constant) and arg.value == "data" @test.test_id("B202") @test.checks("Call") def tarfile_unsafe_members(context): if all( [ context.is_module_imported_exact("tarfile"), "extractall" in context.call_function_name, ] ): if "filter" in context.call_keywords and is_filter_data(context): return None if "members" in context.call_keywords: members = get_members_value(context) if "Function" in members: return exec_issue(bandit.LOW, members) else: return exec_issue(bandit.MEDIUM, members) return exec_issue(bandit.HIGH) ================================================ FILE: bandit/plugins/trojansource.py ================================================ # # SPDX-License-Identifier: Apache-2.0 r""" ===================================================== B613: TrojanSource - Bidirectional control characters ===================================================== This plugin checks for the presence of unicode bidirectional control characters in Python source files. Those characters can be embedded in comments and strings to reorder source code characters in a way that changes its logic. :Example: .. code-block:: none >> Issue: [B613:trojansource] A Python source file contains bidirectional control characters ('\u202e'). Severity: High Confidence: Medium CWE: CWE-838 (https://cwe.mitre.org/data/definitions/838.html) More Info: https://bandit.readthedocs.io/en/1.7.5/plugins/b113_trojansource.html Location: examples/trojansource.py:4:25 3 access_level = "user" 4 if access_level != 'none‮⁦': # Check if admin ⁩⁦' and access_level != 'user 5 print("You are an admin.\n") .. seealso:: - https://trojansource.codes/ - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-42574 .. versionadded:: 1.7.10 """ # noqa: E501 from tokenize import detect_encoding import bandit from bandit.core import issue from bandit.core import test_properties as test BIDI_CHARACTERS = ( "\u202a", "\u202b", "\u202c", "\u202d", "\u202e", "\u2066", "\u2067", "\u2068", "\u2069", "\u200f", ) @test.test_id("B613") @test.checks("File") def trojansource(context): src_data = context.file_data src_data.seek(0) encoding, _ = detect_encoding(src_data.readline) src_data.seek(0) for lineno, line in enumerate( src_data.read().decode(encoding).splitlines(), start=1 ): for char in BIDI_CHARACTERS: try: col_offset = line.index(char) + 1 except ValueError: continue text = ( "A Python source file contains bidirectional" " control characters (%r)." % char ) b_issue = bandit.Issue( severity=bandit.HIGH, confidence=bandit.MEDIUM, cwe=issue.Cwe.INAPPROPRIATE_ENCODING_FOR_OUTPUT_CONTEXT, text=text, lineno=lineno, col_offset=col_offset, ) b_issue.linerange = [lineno] return b_issue ================================================ FILE: bandit/plugins/try_except_continue.py ================================================ # Copyright 2016 IBM Corp. # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 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 CWE: CWE-703 (https://cwe.mitre.org/data/definitions/703.html) Location: ./examples/try_except_continue.py:5 4 a = i 5 except: 6 continue .. seealso:: - https://security.openstack.org - https://cwe.mitre.org/data/definitions/703.html .. versionadded:: 1.0.0 .. versionchanged:: 1.7.3 CWE information added """ import ast import bandit from bandit.core import issue 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, cwe=issue.Cwe.IMPROPER_CHECK_OF_EXCEPT_COND, text=("Try, Except, Continue detected."), ) ================================================ FILE: bandit/plugins/try_except_pass.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 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 CWE: CWE-703 (https://cwe.mitre.org/data/definitions/703.html) Location: ./examples/try_except_pass.py:4 3 a = 1 4 except: 5 pass .. seealso:: - https://security.openstack.org - https://cwe.mitre.org/data/definitions/703.html .. versionadded:: 0.13.0 .. versionchanged:: 1.7.3 CWE information added """ import ast import bandit from bandit.core import issue 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, cwe=issue.Cwe.IMPROPER_CHECK_OF_EXCEPT_COND, text=("Try, Except, Pass detected."), ) ================================================ FILE: bandit/plugins/weak_cryptographic_key.py ================================================ # Copyright (c) 2015 VMware, Inc. # # SPDX-License-Identifier: Apache-2.0 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 CWE: CWE-326 (https://cwe.mitre.org/data/definitions/326.html) 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:: - https://csrc.nist.gov/publications/detail/sp/800-131a/rev-2/final - https://security.openstack.org/guidelines/dg_strong-crypto.html - https://cwe.mitre.org/data/definitions/326.html .. versionadded:: 0.14.0 .. versionchanged:: 1.7.3 CWE information added """ import bandit from bandit.core import issue from bandit.core import test_properties as test def gen_config(name): if name == "weak_cryptographic_key": return { "weak_key_size_dsa_high": 1024, "weak_key_size_dsa_medium": 2048, "weak_key_size_rsa_high": 1024, "weak_key_size_rsa_medium": 2048, "weak_key_size_ec_high": 160, "weak_key_size_ec_medium": 224, } def _classify_key_size(config, 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": [ (config["weak_key_size_dsa_high"], bandit.HIGH), (config["weak_key_size_dsa_medium"], bandit.MEDIUM), ], "RSA": [ (config["weak_key_size_rsa_high"], bandit.HIGH), (config["weak_key_size_rsa_medium"], bandit.MEDIUM), ], "EC": [ (config["weak_key_size_ec_high"], bandit.HIGH), (config["weak_key_size_ec_medium"], bandit.MEDIUM), ], } for size, level in key_sizes[key_type]: if key_size < size: return bandit.Issue( severity=level, confidence=bandit.HIGH, cwe=issue.Cwe.INADEQUATE_ENCRYPTION_STRENGTH, text="%s key sizes below %d bits are considered breakable. " % (key_type, size), ) def _weak_crypto_key_size_cryptography_io(context, config): 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(config, key_type, key_size) elif key_type == "EC": curve_key_sizes = { "SECT571K1": 571, "SECT571R1": 570, "SECP521R1": 521, "BrainpoolP512R1": 512, "SECT409K1": 409, "SECT409R1": 409, "BrainpoolP384R1": 384, "SECP384R1": 384, "SECT283K1": 283, "SECT283R1": 283, "BrainpoolP256R1": 256, "SECP256K1": 256, "SECP256R1": 256, "SECT233K1": 233, "SECT233R1": 233, "SECP224R1": 224, "SECP192R1": 192, "SECT163K1": 163, "SECT163R2": 163, } curve = context.get_call_arg_value("curve") or ( len(context.call_args) > arg_position[key_type] and 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(config, key_type, key_size) def _weak_crypto_key_size_pycrypto(context, config): func_key_type = { "Crypto.PublicKey.DSA.generate": "DSA", "Crypto.PublicKey.RSA.generate": "RSA", "Cryptodome.PublicKey.DSA.generate": "DSA", "Cryptodome.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(config, key_type, key_size) @test.takes_config @test.checks("Call") @test.test_id("B505") def weak_cryptographic_key(context, config): return _weak_crypto_key_size_cryptography_io( context, config ) or _weak_crypto_key_size_pycrypto(context, config) ================================================ FILE: bandit/plugins/yaml_load.py ================================================ # # Copyright (c) 2016 Rackspace, Inc. # # SPDX-License-Identifier: Apache-2.0 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 https://pyyaml.org/wiki/PyYAMLDocumentation#LoadingYAML for more information on ``yaml.load`` and yaml.safe_load :Example: .. code-block:: none >> Issue: [yaml_load] Use of unsafe yaml load. Allows instantiation of arbitrary objects. Consider yaml.safe_load(). Severity: Medium Confidence: High CWE: CWE-20 (https://cwe.mitre.org/data/definitions/20.html) 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:: - https://pyyaml.org/wiki/PyYAMLDocumentation#LoadingYAML - https://cwe.mitre.org/data/definitions/20.html .. versionadded:: 1.0.0 .. versionchanged:: 1.7.3 CWE information added """ import bandit from bandit.core import issue from bandit.core import test_properties as test @test.test_id("B506") @test.checks("Call") def yaml_load(context): imported = context.is_module_imported_exact("yaml") qualname = context.call_function_name_qual if not imported and isinstance(qualname, str): return qualname_list = qualname.split(".") func = qualname_list[-1] if all( [ "yaml" in qualname_list, func == "load", not context.check_call_arg_value("Loader", "SafeLoader"), not context.check_call_arg_value("Loader", "CSafeLoader"), not context.get_call_arg_at_position(1) == "SafeLoader", not context.get_call_arg_at_position(1) == "CSafeLoader", ] ): return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.HIGH, cwe=issue.Cwe.IMPROPER_INPUT_VALIDATION, text="Use of unsafe yaml load. Allows instantiation of" " arbitrary objects. Consider yaml.safe_load().", lineno=context.node.lineno, ) ================================================ FILE: doc/requirements.txt ================================================ # 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. sphinx>=4.0.0 # BSD sphinx-rtd-theme>=0.3.0 sphinx-copybutton ================================================ FILE: doc/source/blacklists/blacklist_calls.rst ================================================ --------------- blacklist_calls --------------- .. automodule:: bandit.blacklists.calls :no-index: ================================================ FILE: doc/source/blacklists/blacklist_imports.rst ================================================ ----------------- blacklist_imports ----------------- .. automodule:: bandit.blacklists.imports :no-index: ================================================ FILE: doc/source/blacklists/index.rst ================================================ 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 ================================================ FILE: doc/source/ci-cd/github-actions.rst ================================================ GitHub Actions Workflow for Bandit ================================== This document provides a minimal complete example workflow for setting up a Code Scanning action using Bandit through GitHub Actions. It leverages PyCQA's `bandit-action `_ for seamless integration. Example YAML Code for GitHub Actions Pipeline --------------------------------------------- Below is an example configuration for the GitHub Actions pipeline: .. code-block:: yaml name: Bandit on: workflow_dispatch: jobs: analyze: runs-on: ubuntu-latest permissions: # Required for all workflows security-events: write # Only required for workflows in private repositories actions: read contents: read steps: - name: Perform Bandit Analysis uses: PyCQA/bandit-action@v1 Inputs ====== Below is a list of available inputs for the `bandit-action` and their descriptions: .. list-table:: :header-rows: 1 :widths: 20 50 10 20 * - Name - Description - Required - Default Value * - ``configfile`` - Config file to use for selecting plugins and overriding defaults. - False - ``DEFAULT`` * - ``profile`` - Profile to use (defaults to executing all tests). - False - ``DEFAULT`` * - ``tests`` - Comma-separated list of test IDs to run. - False - ``DEFAULT`` * - ``skips`` - Comma-separated list of test IDs to skip. - False - ``DEFAULT`` * - ``severity`` - Report only issues of a given severity level or higher. Options include ``all``, ``high``, ``medium``, ``low``. Note: ``all`` and ``low`` may produce similar results, but undefined rules will not be listed under ``low``. - False - ``DEFAULT`` * - ``confidence`` - Report only issues of a given confidence level or higher. Options include ``all``, ``high``, ``medium``, ``low``. Note: ``all`` and ``low`` may produce similar results, but undefined rules will not be listed under ``low``. - False - ``DEFAULT`` * - ``exclude`` - Comma-separated list of paths (glob patterns supported) to exclude from the scan. These are in addition to excluded paths provided in the config file. - False - ``.svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg`` * - ``baseline`` - Path of a baseline report to compare against (only JSON-formatted files are accepted). - False - ``DEFAULT`` * - ``ini`` - Path to a ``.bandit`` file that supplies command-line arguments. - False - ``DEFAULT`` * - ``targets`` - Source file(s) or directory(s) to be tested. - False - ``.`` ================================================ FILE: doc/source/ci-cd/index.rst ================================================ .. _ci-cd: Continuous Integration and Deployment (CI/CD) ============================================= This section provides documentation for setting up Continuous Integration and Deployment (CI/CD) pipelines for automated security scanning and quality assurance in this project. Supported CI/CD Platforms ------------------------- The following CI/CD platforms are covered: - **GitHub Actions**: Example workflows for security scanning and quality checks. Available Documentation ----------------------- .. toctree:: :maxdepth: 1 github-actions More CI/CD platforms and configurations may be added over time. Contributions and improvements to these configurations are welcome. ================================================ FILE: doc/source/conf.py ================================================ # SPDX-License-Identifier: Apache-2.0 from datetime import datetime import os import sys sys.path.insert(0, os.path.abspath(os.path.join("..", ".."))) # -- 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.doctest", "sphinx.ext.coverage", "sphinx.ext.viewcode", "sphinx_copybutton", ] # 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 root toctree document. root_doc = "index" # General information about the project. project = "Bandit" copyright = f"{datetime.now():%Y}, Bandit Developers" # 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", "Python source code security analyzer", ["PyCQA"], 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 = "sphinx_rtd_theme" # html_static_path = ['static'] html_theme_options = {} # Output file base name for HTML help builder. htmlhelp_basename = f"{project}doc" # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ( "index", f"{project}.tex", f"{project} Documentation", "PyCQA", "manual", ), ] # Example configuration for intersphinx: refer to the Python standard library. # intersphinx_mapping = {'http://docs.python.org/': None} ================================================ FILE: doc/source/config.rst ================================================ Configuration ============= --------------- Bandit Settings --------------- Projects may include an INI file named `.bandit`, which specifies command line arguments that should be supplied for that project. In addition or alternatively, you can use a YAML or TOML file, which however needs to be explicitly specified using the `-c` option. The currently supported arguments are: ``targets`` comma separated list of target dirs/files to run bandit on ``exclude`` comma separated list of excluded paths -- *INI only* ``exclude_dirs`` comma separated list of excluded paths (directories or files) -- *YAML and TOML only* ``skips`` comma separated list of tests to skip ``tests`` comma separated list of tests to run To use this, put an INI file named `.bandit` in your project's directory. Command line arguments must be in `[bandit]` section. For example: .. code-block:: ini # FILE: .bandit [bandit] exclude = tests,path/to/file tests = B201,B301 skips = B101,B601 Alternatively, put a YAML or TOML file anywhere, and use the `-c` option. For example: .. code-block:: yaml # FILE: bandit.yaml exclude_dirs: ['tests', 'path/to/file'] tests: ['B201', 'B301'] skips: ['B101', 'B601'] .. code-block:: toml # FILE: pyproject.toml [tool.bandit] exclude_dirs = ["tests", "path/to/file"] tests = ["B201", "B301"] skips = ["B101", "B601"] Then run bandit like this: .. code-block:: console bandit -c bandit.yaml -r . .. code-block:: console bandit -c pyproject.toml -r . Note that Bandit will look for `.bandit` file only if it is invoked with `-r` option. If you do not use `-r` or the INI file's name is not `.bandit`, you can specify the file's path explicitly with `--ini` option, e.g. .. code-block:: console bandit --ini tox.ini If Bandit is used via `pre-commit`_ and a config file, you have to specify the config file and optional additional dependencies in the `pre-commit`_ configuration: .. code-block:: yaml repos: - repo: https://github.com/PyCQA/bandit rev: '' # Update me! hooks: - id: bandit args: ["-c", "pyproject.toml"] additional_dependencies: ["bandit[toml]"] 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: .. code-block:: python self.process = subprocess.Popen('/bin/echo', shell=True) # nosec Because multiple issues can be reported for the same line, specific tests may be provided to suppress those reports. This will cause other issues not included to be reported. This can be useful in preventing situations where a nosec comment is used, but a separate vulnerability may be added to the line later causing the new vulnerability to be ignored. For example, this will suppress the report of B602 and B607: .. code-block:: python self.process = subprocess.Popen('/bin/ls *', shell=True) # nosec B602, B607 Full test names rather than the test ID may also be used. For example, this will suppress the report of B101 and continue to report B506 as an issue. .. code-block:: python assert yaml.load("{}") == [] # nosec assert_used ----------------- Scanning Behavior ----------------- 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 exclude_dirs: ['tests', 'path/to/file'] # (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] Run with: .. code-block:: console bandit -c bandit.yaml -r . 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. Also, you can configure bandit via a `pyproject.toml file`_. In this case you would explicitly specify the path to configuration via `-c`, too. For example: .. code-block:: toml [tool.bandit] exclude_dirs = ["tests", "path/to/file"] tests = ["B201", "B301"] skips = ["B101", "B601"] [tool.bandit.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" ] Run with: .. code-block:: console bandit -c pyproject.toml -r . .. _YAML file: https://yaml.org/ .. _pyproject.toml file: https://www.python.org/dev/peps/pep-0518/ 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. Suppressing Individual Lines ---------------------------- If you have lines in your code triggering vulnerability errors and you are certain that this is acceptable, they can be individually silenced by appending ``# nosec`` to the line: .. code-block:: python # The following hash is not used in any security context. It is only used # to generate unique values, collisions are acceptable and "data" is not # coming from user-generated input the_hash = md5(data).hexdigest() # nosec In such cases, it is good practice to add a comment explaining *why* a given line was excluded from security checks. 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. .. _YAML: https://yaml.org/ .. _plugin test list: plugins/index.html .. _pre-commit: https://pre-commit.com/ ================================================ FILE: doc/source/faq.rst ================================================ Frequently Asked Questions ========================== 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 3.9, you should install Bandit to run under Python 3.9. If your project is only compatible with Python 3.10, then use 3.10 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. ================================================ FILE: doc/source/formatters/csv.rst ================================================ --- csv --- .. automodule:: bandit.formatters.csv :no-index: ================================================ FILE: doc/source/formatters/custom.rst ================================================ ------ custom ------ .. automodule:: bandit.formatters.custom :no-index: ================================================ FILE: doc/source/formatters/html.rst ================================================ ---- html ---- .. automodule:: bandit.formatters.html :no-index: ================================================ FILE: doc/source/formatters/index.rst ================================================ 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: * ================================================ FILE: doc/source/formatters/json.rst ================================================ ---- json ---- .. automodule:: bandit.formatters.json :no-index: ================================================ FILE: doc/source/formatters/sarif.rst ================================================ ----- sarif ----- .. automodule:: bandit.formatters.sarif :no-index: ================================================ FILE: doc/source/formatters/screen.rst ================================================ ------ screen ------ .. automodule:: bandit.formatters.screen :no-index: ================================================ FILE: doc/source/formatters/text.rst ================================================ ---- text ---- .. automodule:: bandit.formatters.text :no-index: ================================================ FILE: doc/source/formatters/xml.rst ================================================ --- xml --- .. automodule:: bandit.formatters.xml :no-index: ================================================ FILE: doc/source/formatters/yaml.rst ================================================ ---- yaml ---- .. automodule:: bandit.formatters.yaml :no-index: ================================================ FILE: doc/source/index.rst ================================================ Welcome to Bandit ================= 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. Using and Extending Bandit ========================== .. toctree:: :maxdepth: 1 start config integrations plugins/index blacklists/index formatters/index ci-cd/index faq Contributing ============ * `Source code`_ * `Issue tracker`_ * Join us on `Discord`_ .. _`Source code`: https://github.com/PyCQA/bandit .. _`Issue tracker`: https://github.com/PyCQA/bandit/issues .. _`Discord`: https://discord.gg/qYxpadCgkx Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` License ======= The ``bandit`` library is provided under the terms and conditions of the `Apache License 2.0 `_ ================================================ FILE: doc/source/integrations.rst ================================================ Integrations ============ Bandit can be integrated into a wide variety of developer tools, editors, CI/CD systems, and code quality pipelines. This page outlines popular integrations to help you seamlessly incorporate Bandit into your development workflow. IDE Integrations ---------------- .. list-table:: :widths: 30 70 * - Visual Studio Code - `Bandit by PyCQA `_ * - Sublime Text - `SublimeLinter-bandit `_ * - Vim/Neovim - `Asynchronous Lint Engine `_ * - Emacs - `flycheck-pycheckers `_ CI/CD Integrations ------------------ .. list-table:: :widths: 30 70 * - GitHub Action - `Bandit by PyCQA `_ * - Hudson/Jenkins - `Bandit Plugin `_ Linters ------- .. list-table:: :widths: 30 70 * - Ruff - `flake8-bandit (S) `_ * - Flake8 - `flake8-bandit `_ Packages -------- .. list-table:: :widths: 30 70 * - Ubuntu - `bandit `_ * - Homebrew - `bandit `_ * - FreeBSD - `py-bandit `_ 🙌 Contributions Welcome If you’ve integrated Bandit into another platform or tool, feel free to open a PR and update this page! ================================================ FILE: doc/source/man/bandit.rst ================================================ ====== bandit ====== SYNOPSIS ======== bandit [-h] [-r] [-a {file,vuln}] [-n CONTEXT_LINES] [-c CONFIG_FILE] [-p PROFILE] [-t TESTS] [-s SKIPS] [-l] [-i] [-f {csv,custom,html,json,screen,txt,xml,yaml}] [--msg-template MSG_TEMPLATE] [-o [OUTPUT_FILE]] [-v] [-d] [-q] [--ignore-nosec] [-x EXCLUDED_PATHS] [-b BASELINE] [--ini INI_PATH] [--exit-zero] [--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) -l, --severity-level={all,high,medium,low} report only issues of a given severity level or higher. "all" and "low" are likely to produce the same results, but it is possible for rules to be undefined which will not be listed in "low". -i, --confidence report only issues of a given confidence level or higher (-i for LOW, -ii for MEDIUM, -iii for HIGH) -l, --confidence-level={all,high,medium,low} report only issues of a given confidence level or higher. "all" and "low" are likely to produce the same results, but it is possible for rules to be undefined which will not be listed in "low". -f {csv,custom,html,json,sarif,screen,txt,xml,yaml}, --format {csv,custom,html,json,sarif,screen,txt,xml,yaml} specify output format --msg-template MSG_TEMPLATE specify output message template (only usable with --format custom), see CUSTOM FORMAT section for list of available values -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 -q, --quiet, --silent only show output in the case of an error --ignore-nosec do not skip lines with # nosec comments -x EXCLUDED_PATHS, --exclude EXCLUDED_PATHS comma-separated list of paths (glob patterns supported) to exclude from scan (note that these are in addition to the excluded paths provided in the config file) (default: .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.eggs,*.egg) -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 --exit-zero exit with 0, even with results found --version show program's version number and exit CUSTOM FORMATTING ----------------- Available tags: {abspath}, {relpath}, {line}, {test_id}, {severity}, {msg}, {confidence}, {range} Example usage: Default template: bandit -r examples/ --format custom --msg-template \ "{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}" Provides same output as: bandit -r examples/ --format custom Tags can also be formatted in python string.format() style: bandit -r examples/ --format custom --msg-template \ "{relpath:20.20s}: {line:03}: {test_id:^8}: DEFECT: {msg:>20}" See python documentation for more information about formatting style: https://docs.python.org/3/library/string.html 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 ~/your-repos/project Example usage across the ``examples/`` directory, showing three lines of context and only reporting on the high-severity issues:: bandit examples/*.py -n 3 --severity-level=high 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) ================================================ FILE: doc/source/plugins/b101_assert_used.rst ================================================ ----------------- B101: assert_used ----------------- .. automodule:: bandit.plugins.asserts :no-index: ================================================ FILE: doc/source/plugins/b102_exec_used.rst ================================================ --------------- B102: exec_used --------------- .. automodule:: bandit.plugins.exec :no-index: ================================================ FILE: doc/source/plugins/b103_set_bad_file_permissions.rst ================================================ ------------------------------ B103: set_bad_file_permissions ------------------------------ .. automodule:: bandit.plugins.general_bad_file_permissions :no-index: ================================================ FILE: doc/source/plugins/b104_hardcoded_bind_all_interfaces.rst ================================================ ----------------------------------- B104: hardcoded_bind_all_interfaces ----------------------------------- .. automodule:: bandit.plugins.general_bind_all_interfaces :no-index: ================================================ FILE: doc/source/plugins/b105_hardcoded_password_string.rst ================================================ ------------------------------- B105: hardcoded_password_string ------------------------------- .. currentmodule:: bandit.plugins.general_hardcoded_password .. autofunction:: hardcoded_password_string :noindex: ================================================ FILE: doc/source/plugins/b106_hardcoded_password_funcarg.rst ================================================ -------------------------------- B106: hardcoded_password_funcarg -------------------------------- .. currentmodule:: bandit.plugins.general_hardcoded_password .. autofunction:: hardcoded_password_funcarg :noindex: ================================================ FILE: doc/source/plugins/b107_hardcoded_password_default.rst ================================================ -------------------------------- B107: hardcoded_password_default -------------------------------- .. currentmodule:: bandit.plugins.general_hardcoded_password .. autofunction:: hardcoded_password_default :noindex: ================================================ FILE: doc/source/plugins/b108_hardcoded_tmp_directory.rst ================================================ ----------------------------- B108: hardcoded_tmp_directory ----------------------------- .. automodule:: bandit.plugins.general_hardcoded_tmp :no-index: ================================================ FILE: doc/source/plugins/b109_password_config_option_not_marked_secret.rst ================================================ ---------------------------------------------- B109: password_config_option_not_marked_secret ---------------------------------------------- This plugin has been removed. 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 .. versionadded:: 0.10.0 .. deprecated:: 1.5.0 This plugin was removed ================================================ FILE: doc/source/plugins/b110_try_except_pass.rst ================================================ --------------------- B110: try_except_pass --------------------- .. automodule:: bandit.plugins.try_except_pass :no-index: ================================================ FILE: doc/source/plugins/b111_execute_with_run_as_root_equals_true.rst ================================================ ------------------------------------------ B111: execute_with_run_as_root_equals_true ------------------------------------------ This plugin has been removed. 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 - https://security.openstack.org/guidelines/dg_use-oslo-rootwrap-securely.html .. versionadded:: 0.10.0 .. deprecated:: 1.5.0 This plugin was removed ================================================ FILE: doc/source/plugins/b112_try_except_continue.rst ================================================ ------------------------- B112: try_except_continue ------------------------- .. automodule:: bandit.plugins.try_except_continue :no-index: ================================================ FILE: doc/source/plugins/b113_request_without_timeout.rst ================================================ ----------------------------- B113: request_without_timeout ----------------------------- .. automodule:: bandit.plugins.request_without_timeout :no-index: ================================================ FILE: doc/source/plugins/b201_flask_debug_true.rst ================================================ ---------------------- B201: flask_debug_true ---------------------- .. automodule:: bandit.plugins.app_debug :no-index: ================================================ FILE: doc/source/plugins/b202_tarfile_unsafe_members.rst ================================================ ---------------------------- B202: tarfile_unsafe_members ---------------------------- .. automodule:: bandit.plugins.tarfile_unsafe_members :no-index: ================================================ FILE: doc/source/plugins/b324_hashlib.rst ================================================ ------------- B324: hashlib ------------- .. automodule:: bandit.plugins.hashlib_insecure_functions :no-index: ================================================ FILE: doc/source/plugins/b501_request_with_no_cert_validation.rst ================================================ ------------------------------------- B501: request_with_no_cert_validation ------------------------------------- .. automodule:: bandit.plugins.crypto_request_no_cert_validation :no-index: ================================================ FILE: doc/source/plugins/b502_ssl_with_bad_version.rst ================================================ -------------------------- B502: ssl_with_bad_version -------------------------- .. currentmodule:: bandit.plugins.insecure_ssl_tls .. autofunction:: ssl_with_bad_version :noindex: ================================================ FILE: doc/source/plugins/b503_ssl_with_bad_defaults.rst ================================================ --------------------------- B503: ssl_with_bad_defaults --------------------------- .. currentmodule:: bandit.plugins.insecure_ssl_tls .. autofunction:: ssl_with_bad_defaults :noindex: ================================================ FILE: doc/source/plugins/b504_ssl_with_no_version.rst ================================================ ------------------------- B504: ssl_with_no_version ------------------------- .. currentmodule:: bandit.plugins.insecure_ssl_tls .. autofunction:: ssl_with_no_version :noindex: ================================================ FILE: doc/source/plugins/b505_weak_cryptographic_key.rst ================================================ ---------------------------- B505: weak_cryptographic_key ---------------------------- .. automodule:: bandit.plugins.weak_cryptographic_key :no-index: ================================================ FILE: doc/source/plugins/b506_yaml_load.rst ================================================ --------------- B506: yaml_load --------------- .. automodule:: bandit.plugins.yaml_load :no-index: ================================================ FILE: doc/source/plugins/b507_ssh_no_host_key_verification.rst ================================================ ---------------------------------- B507: ssh_no_host_key_verification ---------------------------------- .. automodule:: bandit.plugins.ssh_no_host_key_verification :no-index: ================================================ FILE: doc/source/plugins/b508_snmp_insecure_version.rst ================================================ --------------------------- B508: snmp_insecure_version --------------------------- .. currentmodule:: bandit.plugins.snmp_security_check .. autofunction:: snmp_insecure_version_check :noindex: ================================================ FILE: doc/source/plugins/b509_snmp_weak_cryptography.rst ================================================ ---------------------------- B509: snmp_weak_cryptography ---------------------------- .. currentmodule:: bandit.plugins.snmp_security_check .. autofunction:: snmp_crypto_check :noindex: ================================================ FILE: doc/source/plugins/b601_paramiko_calls.rst ================================================ -------------------- B601: paramiko_calls -------------------- .. automodule:: bandit.plugins.injection_paramiko :no-index: ================================================ FILE: doc/source/plugins/b602_subprocess_popen_with_shell_equals_true.rst ================================================ --------------------------------------------- B602: subprocess_popen_with_shell_equals_true --------------------------------------------- .. currentmodule:: bandit.plugins.injection_shell .. autofunction:: subprocess_popen_with_shell_equals_true :noindex: ================================================ FILE: doc/source/plugins/b603_subprocess_without_shell_equals_true.rst ================================================ ------------------------------------------ B603: subprocess_without_shell_equals_true ------------------------------------------ .. currentmodule:: bandit.plugins.injection_shell .. autofunction:: subprocess_without_shell_equals_true :noindex: ================================================ FILE: doc/source/plugins/b604_any_other_function_with_shell_equals_true.rst ================================================ ----------------------------------------------- B604: any_other_function_with_shell_equals_true ----------------------------------------------- .. currentmodule:: bandit.plugins.injection_shell .. autofunction:: any_other_function_with_shell_equals_true :noindex: ================================================ FILE: doc/source/plugins/b605_start_process_with_a_shell.rst ================================================ -------------------------------- B605: start_process_with_a_shell -------------------------------- .. currentmodule:: bandit.plugins.injection_shell .. autofunction:: start_process_with_a_shell :noindex: ================================================ FILE: doc/source/plugins/b606_start_process_with_no_shell.rst ================================================ --------------------------------- B606: start_process_with_no_shell --------------------------------- .. currentmodule:: bandit.plugins.injection_shell .. autofunction:: start_process_with_no_shell :noindex: ================================================ FILE: doc/source/plugins/b607_start_process_with_partial_path.rst ================================================ ------------------------------------- B607: start_process_with_partial_path ------------------------------------- .. currentmodule:: bandit.plugins.injection_shell .. autofunction:: start_process_with_partial_path :noindex: ================================================ FILE: doc/source/plugins/b608_hardcoded_sql_expressions.rst ================================================ ------------------------------- B608: hardcoded_sql_expressions ------------------------------- .. automodule:: bandit.plugins.injection_sql :no-index: ================================================ FILE: doc/source/plugins/b609_linux_commands_wildcard_injection.rst ================================================ --------------------------------------- B609: linux_commands_wildcard_injection --------------------------------------- .. automodule:: bandit.plugins.injection_wildcard :no-index: ================================================ FILE: doc/source/plugins/b610_django_extra_used.rst ================================================ ----------------------- B610: django_extra_used ----------------------- .. currentmodule:: bandit.plugins.django_sql_injection .. autofunction:: django_extra_used :noindex: ================================================ FILE: doc/source/plugins/b611_django_rawsql_used.rst ================================================ ------------------------ B611: django_rawsql_used ------------------------ .. currentmodule:: bandit.plugins.django_sql_injection .. autofunction:: django_rawsql_used :noindex: ================================================ FILE: doc/source/plugins/b612_logging_config_insecure_listen.rst ================================================ ------------------------------------ B612: logging_config_insecure_listen ------------------------------------ .. automodule:: bandit.plugins.logging_config_insecure_listen :no-index: ================================================ FILE: doc/source/plugins/b613_trojansource.rst ================================================ ------------------ B613: trojansource ------------------ .. automodule:: bandit.plugins.trojansource :no-index: ================================================ FILE: doc/source/plugins/b614_pytorch_load.rst ================================================ ------------------ B614: pytorch_load ------------------ .. automodule:: bandit.plugins.pytorch_load :no-index: ================================================ FILE: doc/source/plugins/b615_huggingface_unsafe_download.rst ================================================ --------------------------------- B615: huggingface_unsafe_download --------------------------------- .. automodule:: bandit.plugins.huggingface_unsafe_download :no-index: ================================================ FILE: doc/source/plugins/b701_jinja2_autoescape_false.rst ================================================ ----------------------------- B701: jinja2_autoescape_false ----------------------------- .. automodule:: bandit.plugins.jinja2_templates :no-index: ================================================ FILE: doc/source/plugins/b702_use_of_mako_templates.rst ================================================ --------------------------- B702: use_of_mako_templates --------------------------- .. automodule:: bandit.plugins.mako_templates :no-index: ================================================ FILE: doc/source/plugins/b703_django_mark_safe.rst ================================================ ---------------------- B703: django_mark_safe ---------------------- .. currentmodule:: bandit.plugins.django_xss .. autofunction:: django_mark_safe :noindex: ================================================ FILE: doc/source/plugins/b704_markupsafe_markup_xss.rst ================================================ --------------------------- B704: markupsafe_markup_xss --------------------------- .. automodule:: bandit.plugins.markupsafe_markup_xss :no-index: ================================================ FILE: doc/source/plugins/index.rst ================================================ 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: * ================================================ FILE: doc/source/start.rst ================================================ Getting Started =============== Installation ------------ Bandit is distributed on PyPI. The best way to install it is with pip. Create a virtual environment and activate it using `virtualenv` (optional): .. code-block:: console virtualenv bandit-env source bandit-env/bin/activate Alternatively, use `venv` instead of `virtualenv` (optional): .. code-block:: console python3 -m venv bandit-env source bandit-env/bin/activate Install Bandit: .. code-block:: console pip install bandit If you want to include TOML support, install it with the `toml` extras: .. code-block:: console pip install bandit[toml] If you want to use the bandit-baseline CLI, install it with the `baseline` extras: .. code-block:: console pip install bandit[baseline] If you want to include SARIF output formatter support, install it with the `sarif` extras: .. code-block:: console pip install bandit[sarif] Run Bandit: .. code-block:: console bandit -r path/to/your/code Bandit can also be installed from source. To do so, either clone the repository or download the source tarball from PyPI, then install it: .. code-block:: console python setup.py install Alternatively, let pip do the downloading for you, like this: .. code-block:: console pip install git+https://github.com/PyCQA/bandit#egg=bandit Usage ----- Example usage across a code tree: .. code-block:: console bandit -r ~/your_repos/project Two examples of usage across the ``examples/`` directory, showing three lines of context and only reporting on the high-severity issues: .. code-block:: console bandit examples/*.py -n 3 --severity-level=high .. code-block:: console 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: .. code-block:: console bandit examples/*.py -p ShellInjection Bandit also supports passing lines of code to scan using standard input. To run Bandit with standard input: .. code-block:: console cat examples/imports.py | bandit - For more usage information: .. code-block:: console bandit -h Baseline -------- Bandit allows specifying the path of a baseline report to compare against using the base line argument (i.e. ``-b BASELINE`` or ``--baseline BASELINE``). .. code-block:: console bandit -b BASELINE This is useful for ignoring known vulnerabilities that you believe are non-issues (e.g. a cleartext password in a unit test). To generate a baseline report simply run Bandit with the output format set to ``json`` (only JSON-formatted files are accepted as a baseline) and output file path specified: .. code-block:: console bandit -f json -o PATH_TO_OUTPUT_FILE Version control integration --------------------------- Use `pre-commit`_. Once you `have it installed`_, add this to the ``.pre-commit-config.yaml`` in your repository (be sure to update `rev` to point to a `real git tag/revision`_!): .. code-block:: yaml repos: - repo: https://github.com/PyCQA/bandit rev: '' # Update me! hooks: - id: bandit Then run ``pre-commit install`` and you're ready to go. .. _pre-commit: https://pre-commit.com/ .. _have it installed: https://pre-commit.com/#install .. _`real git tag/revision`: https://github.com/PyCQA/bandit/releases ================================================ FILE: docker/Dockerfile ================================================ FROM python:3.12-alpine # Install Git (required for pbr versioning) RUN apk add --no-cache git # Copy the source code into the container COPY . /bandit # Set the working directory WORKDIR /bandit # Install Bandit from the source code using pip RUN pip install . # Define entrypoint and default command ENTRYPOINT ["bandit"] ================================================ FILE: examples/__init__.py ================================================ ================================================ FILE: examples/assert.py ================================================ assert True ================================================ FILE: examples/binding.py ================================================ import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('0.0.0.0', 31137)) s.bind(('192.168.0.1', 8080)) ================================================ FILE: examples/cipher-modes.py ================================================ from 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) ================================================ FILE: examples/ciphers.py ================================================ from Crypto.Cipher import ARC2 as pycrypto_arc2 from Crypto.Cipher import ARC4 as pycrypto_arc4 from Crypto.Cipher import Blowfish as pycrypto_blowfish from Crypto.Cipher import DES as pycrypto_des from Crypto.Cipher import XOR as pycrypto_xor from Cryptodome.Cipher import ARC2 as pycryptodomex_arc2 from Cryptodome.Cipher import ARC4 as pycryptodomex_arc4 from Cryptodome.Cipher import Blowfish as pycryptodomex_blowfish from Cryptodome.Cipher import DES as pycryptodomex_des from Cryptodome.Cipher import XOR as pycryptodomex_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(pycrypto_arc2.block_size) cipher = pycrypto_arc2.new(key, pycrypto_arc2.MODE_CFB, iv) msg = iv + cipher.encrypt(b'Attack at dawn') cipher = pycryptodomex_arc2.new(key, pycryptodomex_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 = pycrypto_arc4.new(tempkey) msg = nonce + cipher.encrypt(b'Open the pod bay doors, HAL') cipher = pycryptodomex_arc4.new(tempkey) msg = nonce + cipher.encrypt(b'Open the pod bay doors, HAL') iv = Random.new().read(bs) key = b'An arbitrarily long key' plaintext = b'docendo discimus ' plen = bs - divmod(len(plaintext),bs)[1] padding = [plen]*plen padding = pack('b'*plen, *padding) bs = pycrypto_blowfish.block_size cipher = pycrypto_blowfish.new(key, pycrypto_blowfish.MODE_CBC, iv) msg = iv + cipher.encrypt(plaintext + padding) bs = pycryptodomex_blowfish.block_size cipher = pycryptodomex_blowfish.new(key, pycryptodomex_blowfish.MODE_CBC, iv) msg = iv + cipher.encrypt(plaintext + padding) key = b'-8B key-' plaintext = b'We are no longer the knights who say ni!' nonce = Random.new().read(pycrypto_des.block_size/2) ctr = Counter.new(pycrypto_des.block_size*8/2, prefix=nonce) cipher = pycrypto_des.new(key, pycrypto_des.MODE_CTR, counter=ctr) msg = nonce + cipher.encrypt(plaintext) nonce = Random.new().read(pycryptodomex_des.block_size/2) ctr = Counter.new(pycryptodomex_des.block_size*8/2, prefix=nonce) cipher = pycryptodomex_des.new(key, pycryptodomex_des.MODE_CTR, counter=ctr) msg = nonce + cipher.encrypt(plaintext) key = b'Super secret key' plaintext = b'Encrypt me' cipher = pycrypto_xor.new(key) msg = cipher.encrypt(plaintext) cipher = pycryptodomex_xor.new(key) 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.CAST5(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") cipher = Cipher(algorithms.SEED(key), mode=None, backend=default_backend()) encryptor = cipher.encryptor() ct = encryptor.update(b"a secret message") cipher = Cipher(algorithms.TripleDES(key), mode=None, backend=default_backend()) encryptor = cipher.encryptor() ct = encryptor.update(b"a secret message") ================================================ FILE: examples/crypto-md5.py ================================================ from 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 from Crypto.Hash import SHA as pycrypto_sha from Cryptodome.Hash import MD2 as pycryptodomex_md2 from Cryptodome.Hash import MD4 as pycryptodomex_md4 from Cryptodome.Hash import MD5 as pycryptodomex_md5 from Cryptodome.Hash import SHA as pycryptodomex_sha import hashlib import crypt hashlib.md5(1) hashlib.md5(1).hexdigest() abc = str.replace(hashlib.md5("1"), "###") print(hashlib.md5("1")) hashlib.sha1(1) hashlib.sha1(usedforsecurity=False) pycrypto_md2.new() pycrypto_md4.new() pycrypto_md5.new() pycrypto_sha.new() pycryptodomex_md2.new() pycryptodomex_md4.new() pycryptodomex_md5.new() pycryptodomex_sha.new() hashes.MD5() hashes.SHA1() crypt.crypt("asdfasdfasdfasdf", salt=crypt.METHOD_CRYPT) crypt.crypt("asdfasdfasdfasdf", salt=crypt.METHOD_MD5) crypt.crypt("asdfasdfasdfasdf", salt=crypt.METHOD_BLOWFISH) crypt.crypt("asdfasdfasdfasdf") crypt.crypt("asdfasdfasdfasdf", salt=crypt.METHOD_SHA256) crypt.crypt("asdfasdfasdfasdf", salt=crypt.METHOD_SHA512) crypt.mksalt(crypt.METHOD_CRYPT) crypt.mksalt(crypt.METHOD_MD5) crypt.mksalt(crypt.METHOD_BLOWFISH) crypt.mksalt() crypt.mksalt(crypt.METHOD_SHA256) crypt.mksalt(crypt.METHOD_SHA512) ================================================ FILE: examples/dill.py ================================================ import dill import io # dill pick = dill.dumps({'a': 'b', 'c': 'd'}) print(dill.loads(pick)) file_obj = io.BytesIO() dill.dump([1, 2, '3'], file_obj) file_obj.seek(0) print(dill.load(file_obj)) file_obj.seek(0) print(dill.Unpickler(file_obj).load()) ================================================ FILE: examples/django_sql_injection_extra.py ================================================ from django.contrib.auth.models import User User.objects.filter(username='admin').extra( select={'test': 'secure'}, where=['secure'], tables=['secure'] ) User.objects.filter(username='admin').extra({'test': 'secure'}) User.objects.filter(username='admin').extra(select={'test': 'secure'}) User.objects.filter(username='admin').extra(where=['secure']) User.objects.filter(username='admin').extra(dict(could_be='insecure')) User.objects.filter(username='admin').extra(select=dict(could_be='insecure')) query = '"username") AS "username", * FROM "auth_user" WHERE 1=1 OR "username"=? --' User.objects.filter(username='admin').extra(select={'test': query}) User.objects.filter(username='admin').extra(select={'test': '%secure' % 'nos'}) User.objects.filter(username='admin').extra(select={'test': '{}secure'.format('nos')}) where_var = ['1=1) OR 1=1 AND (1=1'] User.objects.filter(username='admin').extra(where=where_var) where_str = '1=1) OR 1=1 AND (1=1' User.objects.filter(username='admin').extra(where=[where_str]) User.objects.filter(username='admin').extra(where=['%secure' % 'nos']) User.objects.filter(username='admin').extra(where=['{}secure'.format('no')]) tables_var = ['django_content_type" WHERE "auth_user"."username"="admin'] User.objects.all().extra(tables=tables_var).distinct() tables_str = 'django_content_type" WHERE "auth_user"."username"="admin' User.objects.all().extra(tables=[tables_str]).distinct() ================================================ FILE: examples/django_sql_injection_raw.py ================================================ from django.db.models.expressions import RawSQL from django.contrib.auth.models import User User.objects.annotate(val=RawSQL('secure', [])) User.objects.annotate(val=RawSQL('%secure' % 'nos', [])) User.objects.annotate(val=RawSQL('{}secure'.format('no'), [])) raw = '"username") AS "val" FROM "auth_user" WHERE "username"="admin" --' User.objects.annotate(val=RawSQL(raw, [])) raw = '"username") AS "val" FROM "auth_user"' \ ' WHERE "username"="admin" OR 1=%s --' User.objects.annotate(val=RawSQL(raw, [0])) User.objects.annotate(val=RawSQL(sql='{}secure'.format('no'), params=[])) User.objects.annotate(val=RawSQL(params=[], sql='{}secure'.format('no'))) ================================================ FILE: examples/eval.py ================================================ import 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() ================================================ FILE: examples/exec.py ================================================ exec("do evil") ================================================ FILE: examples/flask_debug.py ================================================ from 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) ================================================ FILE: examples/ftplib.py ================================================ from ftplib import FTP from ftplib import FTP_TLS # bad ftp = FTP('ftp.debian.org') ftp.login() ftp.cwd('debian') ftp.retrlines('LIST') ftp.quit() # okay ftp = ftplib.FTP_TLS( "ftp.us.debian.org", context=ssl.create_default_context(), ) ftp.login() ftp.cwd("debian") ftp.retrlines("LIST") ftp.quit() ================================================ FILE: examples/hardcoded-passwords.py ================================================ # Possible hardcoded password: 'class_password' # Severity: Low Confidence: Medium class SomeClass: password = "class_password" # Possible hardcoded password: 'Admin' # Severity: Low Confidence: Medium def someFunction(user, password="Admin"): print("Hi " + user) def someFunction2(password): # Possible hardcoded password: 'root' # Severity: Low Confidence: Medium if password == "root": print("OK, logged in") def noMatch(password): # Possible hardcoded password: '' # Severity: Low Confidence: Medium if password == '': print("No password!") def NoMatch2(password): # Possible hardcoded password: 'ajklawejrkl42348swfgkg' # Severity: Low Confidence: Medium if password == "ajklawejrkl42348swfgkg": print("Nice password!") def noMatchObject(): obj = SomeClass() # Possible hardcoded password: 'this cool password' # Severity: Low Confidence: Medium if obj.password == "this cool password": print(obj.password) # Possible hardcoded password: 'blerg' # Severity: Low Confidence: Medium def doLogin(password="blerg"): pass def NoMatch3(a, b): pass # Possible hardcoded password: 'blerg' # Severity: Low Confidence: Medium doLogin(password="blerg") # Possible hardcoded password: 'blerg' # Severity: Low Confidence: Medium password = "blerg" # Possible hardcoded password: 'blerg' # Severity: Low Confidence: Medium password["password"] = "blerg" # Possible hardcoded password: 'secret' # Severity: Low Confidence: Medium EMAIL_PASSWORD = "secret" # Possible hardcoded password: 'emails_secret' # Severity: Low Confidence: Medium email_pwd = 'emails_secret' # Possible hardcoded password: 'd6s$f9g!j8mg7hw?n&2' # Severity: Low Confidence: Medium my_secret_password_for_email = 'd6s$f9g!j8mg7hw?n&2' # Possible hardcoded password: '1234' # Severity: Low Confidence: Medium passphrase='1234' # Possible hardcoded password: None # Severity: High Confidence: High def __init__(self, auth_scheme, auth_token=None, auth_username=None, auth_password=None, auth_link=None, **kwargs): self.auth_scheme = auth_scheme self.auth_token = auth_token self.auth_username = auth_username self.auth_password = auth_password self.auth_link = auth_link self.kwargs = kwargs # Possible hardcoded password: None # Severity: High Confidence: High from oslo_config import cfg cfg.StrOpt( 'metadata_proxy_shared_secret', default='', secret=True, ) # Possible hardcoded password: 'pass' # Severity: Low Confidence: Medium # https://github.com/PyCQA/bandit/issues/313 log({"server": server, "password": 'pass', "user": user}) # ... but not: log({"server": server, "password": password, "user": user}) # Possible hardcoded password: '12345' # Severity: Low Confidence: Medium # https://github.com/PyCQA/bandit/issues/1267 info = {"password": "12345"} # ... but not: info = {"password": password} ================================================ FILE: examples/hardcoded-tmp.py ================================================ with open('/tmp/abc', 'w') as f: f.write('def') # ok with open('/abc/tmp', 'w') as f: f.write('def') with open('/var/tmp/123', 'w') as f: f.write('def') with open('/dev/shm/unit/test', 'w') as f: f.write('def') # Negative test with open('/foo/bar', 'w') as f: f.write('def') ================================================ FILE: examples/hashlib_new_insecure_functions.py ================================================ import hashlib hashlib.new('md5') hashlib.new('md4', b'test') hashlib.new(name='md5', data=b'test') hashlib.new('MD4', data=b'test') hashlib.new('sha1') hashlib.new('sha1', data=b'test') hashlib.new('sha', data=b'test') hashlib.new(name='SHA', data=b'test') hashlib.new('sha1', usedforsecurity=True) # Test that plugin does not flag valid hash functions. hashlib.new('sha256') hashlib.new('SHA512') hashlib.new(name='sha1', usedforsecurity=False) ================================================ FILE: examples/httpoxy_cgihandler.py ================================================ import requests import wsgiref.handlers def application(environ, start_response): r = requests.get('https://192.168.0.42/private/api/foobar', timeout=30) start_response('200 OK', [('Content-Type', 'text/plain')]) return [r.content] if __name__ == '__main__': wsgiref.handlers.CGIHandler().run(application) ================================================ FILE: examples/httpoxy_twisted_directory.py ================================================ from 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() ================================================ FILE: examples/httpoxy_twisted_script.py ================================================ from 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() ================================================ FILE: examples/huggingface_unsafe_download.py ================================================ from datasets import load_dataset from huggingface_hub import hf_hub_download, snapshot_download from transformers import AutoModel, AutoTokenizer # UNSAFE USAGE # AutoModel (Model Loading) # Example #1: No revision (defaults to floating 'main') unsafe_model_no_revision = AutoModel.from_pretrained("org/model_name") # Example #2: Floating revision: 'main' unsafe_model_main = AutoModel.from_pretrained( "org/model_name", revision="main" ) # Example #3: Floating tag revision: 'v1.0.0' unsafe_model_tag = AutoModel.from_pretrained( "org/model_name", revision="v1.0.0" ) # AutoTokenizer (Tokenizer Loading) # Example #4: No revision unsafe_tokenizer_no_revision = AutoTokenizer.from_pretrained("org/model_name") # Example #5: Floating revision: 'main' unsafe_tokenizer_main = AutoTokenizer.from_pretrained( "org/model_name", revision="main" ) # Example #6: Floating tag revision: 'v1.0.0' unsafe_tokenizer_tag = AutoTokenizer.from_pretrained( "org/model_name", revision="v1.0.0" ) # Example #7: load_dataset (Dataset Loading) # Example #8: No revision unsafe_dataset_no_revision = load_dataset("org_dataset") # Example #9: Floating revision: 'main' unsafe_dataset_main = load_dataset("org_dataset", revision="main") # Example #10: Floating tag revision: 'v1.0.0' unsafe_dataset_tag = load_dataset("org_dataset", revision="v1.0.0") # f_hub_download (File Download) # Example #11: No revision unsafe_file_no_revision = hf_hub_download( repo_id="org/model_name", filename="config.json" ) # Example #12: Floating revision: 'main' unsafe_file_main = hf_hub_download( repo_id="org/model_name", filename="config.json", revision="main" ) # Example #13: Floating tag revision: 'v1.0.0' unsafe_file_tag = hf_hub_download( repo_id="org/model_name", filename="config.json", revision="v1.0.0" ) # snapshot_download (Repo Snapshot) # Example #14: No revision unsafe_snapshot_no_revision = snapshot_download(repo_id="org/model_name") # Example #15: Floating revision: 'main' unsafe_snapshot_main = snapshot_download( repo_id="org/model_name", revision="main" ) # Example #16: Floating tag revision: 'v1.0.0' unsafe_snapshot_tag = snapshot_download( repo_id="org/model_name", revision="v1.0.0" ) # ------------------------------- # SAFE USAGE # ------------------------------- # AutoModel # Example #17: Pinned commit hash safe_model_commit = AutoModel.from_pretrained( "org/model_name", revision="5d0f2e8a7f1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d" ) # Example #18: Local path safe_model_local = AutoModel.from_pretrained("./local_model") safe_model_local_abs = AutoModel.from_pretrained("/path/to/model") # AutoTokenizer # Example #19: Pinned commit hash safe_tokenizer_commit = AutoTokenizer.from_pretrained( "org/model_name", revision="5d0f2e8a7f1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d" ) # Example #20: Local path safe_tokenizer_local = AutoTokenizer.from_pretrained("./local_tokenizer") # load_dataset # Example #21: Pinned commit hash safe_dataset_commit = load_dataset( "org_dataset", revision="5d0f2e8a7f1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d" ) # hf_hub_download # Example #22: Pinned commit hash safe_file_commit = hf_hub_download( repo_id="org/model_name", filename="config.json", revision="5d0f2e8a7f1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d" ) # snapshot_download # Example #23: Pinned commit hash safe_snapshot_commit = snapshot_download( repo_id="org/model_name", revision="5d0f2e8a7f1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d" ) # Example #24: Revision passed as a variable (can't be statically checked) MODEL_REVISION = "548fc3543a" safe_model_variable = AutoModel.from_pretrained( "org/model_name", revision=MODEL_REVISION ) # Example #25: Revision from a dict/subscript access config = {"revision": "abc1234567"} safe_model_subscript = AutoModel.from_pretrained( "org/model_name", revision=config["revision"] ) ================================================ FILE: examples/imports-aliases.py ================================================ from 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'}) ================================================ FILE: examples/imports-from.py ================================================ from subprocess import Popen from ..foo import sys from . import sys from .. import sys from .. import subprocess from ..subprocess import Popen ================================================ FILE: examples/imports-function.py ================================================ os = __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 unfortunately (no symbol tab) a = 'subprocess' __import__(a) ================================================ FILE: examples/imports-with-importlib.py ================================================ import importlib a = importlib.import_module('os') b = importlib.import_module('pickle') c = importlib.__import__('sys') d = importlib.__import__('subprocess') # Do not crash when target is an expression e = importlib.import_module(MODULE_MAP[key]) f = importlib.__import__(MODULE_MAP[key]) # Do not crash when target is a named argument g = importlib.import_module(name='sys') h = importlib.__import__(name='subprocess') i = importlib.import_module(name='subprocess', package='bar.baz') j = importlib.__import__(name='sys', package='bar.baz') ================================================ FILE: examples/imports.py ================================================ import os import pickle import sys import subprocess ================================================ FILE: examples/init-py-test/__init__.py ================================================ ================================================ FILE: examples/init-py-test/subdirectory-okay.py ================================================ # A sample test file in a subdirectory and its parents both containing # an __init__.py file outlined in bug/1743042. print('hopefully no vulnerabilities here') ================================================ FILE: examples/jinja2_templating.py ================================================ import jinja2 from jinja2 import Environment, select_autoescape 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) Environment(loader=templateLoader, autoescape=select_autoescape()) Environment(loader=templateLoader, autoescape=select_autoescape(['html', 'htm', 'xml'])) Environment(loader=templateLoader, autoescape=jinja2.select_autoescape(['html', 'htm', 'xml'])) def fake_func(): return 'foobar' Environment(loader=templateLoader, autoescape=fake_func()) ================================================ FILE: examples/jsonpickle.py ================================================ import jsonpickle pick = jsonpickle.encode({'a': 'b', 'c': 'd'}) print(jsonpickle.decode(pick)) print(jsonpickle.unpickler.decode(pick)) print(jsonpickle.unpickler.Unpickler().restore(pick)) ================================================ FILE: examples/logging_config_insecure_listen.py ================================================ import logging.config t = logging.config.listen(9999) ================================================ FILE: examples/long_set.py ================================================ # This file contains a single long_set with 7276 'a' elements long_set = { 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a' } ================================================ FILE: examples/mako_templating.py ================================================ from 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") ================================================ FILE: examples/mark_safe.py ================================================ from django.utils import safestring mystr = 'Hello World' mystr = safestring.mark_safe(mystr) ================================================ FILE: examples/mark_safe_insecure.py ================================================ import os from django.utils import safestring def insecure_function(text, cls=''): return '

{text}

'.format(text=text, cls=cls) my_insecure_str = insecure_function('insecure', cls='" onload="alert(\'xss\')') safestring.mark_safe(my_insecure_str) safestring.SafeText(my_insecure_str) safestring.SafeUnicode(my_insecure_str) safestring.SafeString(my_insecure_str) safestring.SafeBytes(my_insecure_str) def try_insecure(cls='" onload="alert(\'xss\')'): try: my_insecure_str = insecure_function('insecure', cls=cls) except Exception: my_insecure_str = 'Secure' safestring.mark_safe(my_insecure_str) def except_insecure(cls='" onload="alert(\'xss\')'): try: my_insecure_str = 'Secure' except Exception: my_insecure_str = insecure_function('insecure', cls=cls) safestring.mark_safe(my_insecure_str) def try_else_insecure(cls='" onload="alert(\'xss\')'): try: if 1 == random.randint(0, 1): # nosec raise Exception except Exception: my_insecure_str = 'Secure' else: my_insecure_str = insecure_function('insecure', cls=cls) safestring.mark_safe(my_insecure_str) def finally_insecure(cls='" onload="alert(\'xss\')'): try: if 1 == random.randint(0, 1): # nosec raise Exception except Exception: print("Exception") else: print("No Exception") finally: my_insecure_str = insecure_function('insecure', cls=cls) safestring.mark_safe(my_insecure_str) def format_arg_insecure(cls='" onload="alert(\'xss\')'): my_insecure_str = insecure_function('insecure', cls=cls) safestring.mark_safe('{} {}'.format(my_insecure_str, 'STR')) def format_startarg_insecure(cls='" onload="alert(\'xss\')'): my_insecure_str = insecure_function('insecure', cls=cls) safestring.mark_safe('{}'.format(*[my_insecure_str])) def format_keywords_insecure(cls='" onload="alert(\'xss\')'): my_insecure_str = insecure_function('insecure', cls=cls) safestring.mark_safe('{b}'.format(b=my_insecure_str)) def format_kwargs_insecure(cls='" onload="alert(\'xss\')'): my_insecure_str = insecure_function('insecure', cls=cls) safestring.mark_safe('{b}'.format(**{'b': my_insecure_str})) def percent_insecure(cls='" onload="alert(\'xss\')'): my_insecure_str = insecure_function('insecure', cls=cls) safestring.mark_safe('%s' % my_insecure_str) def percent_list_insecure(cls='" onload="alert(\'xss\')'): my_insecure_str = insecure_function('insecure', cls=cls) safestring.mark_safe('%s %s' % (my_insecure_str, 'b')) def percent_dict_insecure(cls='" onload="alert(\'xss\')'): my_insecure_str = insecure_function('insecure', cls=cls) safestring.mark_safe('%(b)s' % {'b': my_insecure_str}) def import_insecure(): import sre_constants safestring.mark_safe(sre_constants.ANY) def import_as_insecure(): import sre_constants.ANY as any_str safestring.mark_safe(any_str) def from_import_insecure(): from sre_constants import ANY safestring.mark_safe(ANY) def from_import_as_insecure(): from sre_constants import ANY as any_str safestring.mark_safe(any_str) def with_insecure(path): with open(path) as f: safestring.mark_safe(f.read()) def also_with_insecure(path): with open(path) as f: safestring.mark_safe(f) def for_insecure(): my_secure_str = '' for i in range(random.randint(0, 1)): # nosec my_secure_str += insecure_function('insecure', cls='" onload="alert(\'xss\')') safestring.mark_safe(my_secure_str) def while_insecure(): my_secure_str = '' while ord(os.urandom(1)) % 2 == 0: my_secure_str += insecure_function('insecure', cls='" onload="alert(\'xss\')') safestring.mark_safe(my_secure_str) def some_insecure_case(): if ord(os.urandom(1)) % 2 == 0: my_secure_str = insecure_function('insecure', cls='" onload="alert(\'xss\')') elif ord(os.urandom(1)) % 2 == 0: my_secure_str = 'Secure' else: my_secure_str = 'Secure' safestring.mark_safe(my_secure_str) mystr = 'insecure' def test_insecure_shadow(): # var assigned out of scope safestring.mark_safe(mystr) def test_insecure(str_arg): safestring.mark_safe(str_arg) def test_insecure_with_assign(str_arg=None): if not str_arg: str_arg = 'could be insecure' safestring.mark_safe(str_arg) def test_insecure_tuple_assign(): HTML_CHOICES = ( (_('Donate'), 'https://example.org/donate/'), (_('More info'), 'https://example.org/'), ) text, url = choice(HTML_CHOICES) safestring.mark_safe('{1}'.format(url, text)) ================================================ FILE: examples/mark_safe_secure.py ================================================ import os from django.utils import safestring safestring.mark_safe('secure') safestring.SafeText('secure') safestring.SafeUnicode('secure') safestring.SafeString('secure') safestring.SafeBytes('secure') my_secure_str = 'Hello World' safestring.mark_safe(my_secure_str) my_secure_str, _ = ('Hello World', '') safestring.mark_safe(my_secure_str) also_secure_str = my_secure_str safestring.mark_safe(also_secure_str) def try_secure(): try: my_secure_str = 'Secure' except Exception: my_secure_str = 'Secure' else: my_secure_str = 'Secure' finally: my_secure_str = 'Secure' safestring.mark_safe(my_secure_str) def format_secure(): safestring.mark_safe('{}'.format('secure')) my_secure_str = 'secure' safestring.mark_safe('{}'.format(my_secure_str)) safestring.mark_safe('{} {}'.format(my_secure_str, 'a')) safestring.mark_safe('{} {}'.format(*[my_secure_str, 'a'])) safestring.mark_safe('{b}'.format(b=my_secure_str)) # nosec TODO safestring.mark_safe('{b}'.format(**{'b': my_secure_str})) # nosec TODO my_secure_str = '{}'.format(my_secure_str) safestring.mark_safe(my_secure_str) def percent_secure(): safestring.mark_safe('%s' % 'secure') my_secure_str = 'secure' safestring.mark_safe('%s' % my_secure_str) safestring.mark_safe('%s %s' % (my_secure_str, 'a')) safestring.mark_safe('%(b)s' % {'b': my_secure_str}) # nosec TODO def with_secure(path): with open(path) as f: safestring.mark_safe('Secure') def loop_secure(): my_secure_str = '' for i in range(ord(os.urandom(1))): my_secure_str += ' Secure' safestring.mark_safe(my_secure_str) while ord(os.urandom(1)) % 2 == 0: my_secure_str += ' Secure' safestring.mark_safe(my_secure_str) def all_secure_case(): if ord(os.urandom(1)) % 2 == 0: my_secure_str = 'Secure' elif ord(os.urandom(1)) % 2 == 0: my_secure_str = 'Secure' else: my_secure_str = 'Secure' safestring.mark_safe(my_secure_str) ================================================ FILE: examples/markupsafe_markup_xss.py ================================================ import flask from markupsafe import Markup, escape content = "" Markup(f"unsafe {content}") # B704 flask.Markup("unsafe {}".format(content)) # B704 Markup("safe {}").format(content) flask.Markup(b"safe {}", encoding='utf-8').format(content) escape(content) Markup(content) # B704 flask.Markup("unsafe %s" % content) # B704 Markup(object="safe") Markup(object="unsafe {}".format(content)) # Not currently detected ================================================ FILE: examples/markupsafe_markup_xss_allowed_calls.py ================================================ from bleach import clean from markupsafe import Markup content = "" Markup(clean(content)) # indirect assignments are currently not supported cleaned = clean(content) Markup(cleaned) ================================================ FILE: examples/markupsafe_markup_xss_extend_markup_names.py ================================================ from markupsafe import Markup from webhelpers.html import literal content = "" Markup(f"unsafe {content}") literal(f"unsafe {content}") ================================================ FILE: examples/marshal_deserialize.py ================================================ import 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() ================================================ FILE: examples/mktemp.py ================================================ from tempfile import mktemp import tempfile.mktemp as mt import tempfile as tmp foo = 'hi' mktemp(foo) tempfile.mktemp('foo') mt(foo) tmp.mktemp(foo) ================================================ FILE: examples/multiline_statement.py ================================================ import subprocess subprocess.check_output("/some_command", "args", shell=True, universal_newlines=True) subprocess.check_output( "/some_command", "args", shell=True, universal_newlines=True ) ================================================ FILE: examples/new_candidates-all.py ================================================ import 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 ================================================ FILE: examples/new_candidates-none.py ================================================ def 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 ================================================ FILE: examples/new_candidates-nosec.py ================================================ import 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 ================================================ FILE: examples/new_candidates-some.py ================================================ import 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 ================================================ FILE: examples/no_host_key_verification.py ================================================ from paramiko import client from paramiko import AutoAddPolicy from paramiko import WarningPolicy ssh_client = client.SSHClient() ssh_client.set_missing_host_key_policy(client.AutoAddPolicy) ssh_client.set_missing_host_key_policy(client.WarningPolicy) ssh_client.set_missing_host_key_policy(client.AutoAddPolicy()) ssh_client.set_missing_host_key_policy(client.WarningPolicy()) ssh_client.set_missing_host_key_policy(AutoAddPolicy) ssh_client.set_missing_host_key_policy(WarningPolicy) ssh_client.set_missing_host_key_policy(AutoAddPolicy()) ssh_client.set_missing_host_key_policy(WarningPolicy()) ================================================ FILE: examples/nonsense.py ================================================ test(hi ================================================ FILE: examples/nosec.py ================================================ import subprocess # nosec: import_subprocess from cryptography.hazmat.primitives import hashes hashes.SHA1() # nosec: md5 subprocess.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) subprocess.Popen('#nosec', shell=True) subprocess.Popen('/bin/ls *', shell=True) # type: ... # nosec # noqa: E501 ; pylint: disable=line-too-long subprocess.Popen('/bin/ls *', shell=True) # type: ... # nosec B607 # noqa: E501 ; pylint: disable=line-too-long subprocess.Popen('/bin/ls *', shell=True) #nosec subprocess_popen_with_shell_equals_true (on the line) subprocess.Popen('#nosec', shell=True) # nosec B607, B602 subprocess.Popen('#nosec', shell=True) # nosec B607 B602 subprocess.Popen('/bin/ls *', shell=True) # nosec subprocess_popen_with_shell_equals_true start_process_with_partial_path subprocess.Popen('/bin/ls *', shell=True) # type: ... # noqa: E501 ; pylint: disable=line-too-long # nosec subprocess.Popen('#nosec', shell=True) # nosec B607, B101 subprocess.Popen('#nosec', shell=True) # nosec B602, subprocess_popen_with_shell_equals_true ================================================ FILE: examples/okay.py ================================================ print('hopefully no vulnerabilities here') ================================================ FILE: examples/os-chmod.py ================================================ import 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(keyfile, 0o777) os.chmod('~/hidden_exec', stat.S_IXGRP) os.chmod('~/hidden_exec', stat.S_IXOTH) ================================================ FILE: examples/os-exec.py ================================================ import 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) ================================================ FILE: examples/os-popen.py ================================================ import 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) ================================================ FILE: examples/os-spawn.py ================================================ import 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) ================================================ FILE: examples/os-startfile.py ================================================ import os os.startfile('/bin/foo.docx') os.startfile('/bin/bad.exe') os.startfile('/bin/text.txt') ================================================ FILE: examples/os_system.py ================================================ import os os.system('/bin/echo hi') ================================================ FILE: examples/pandas_read_pickle.py ================================================ import pickle import pandas as pd df = pd.DataFrame( { "col_A": [1, 2] } ) pick = pickle.dumps(df) print(pd.read_pickle(pick)) ================================================ FILE: examples/paramiko_injection.py ================================================ import paramiko client = paramiko.client.SSHClient() # this is not safe client.exec_command('something; really; unsafe') # this is safe client.connect('somehost') ================================================ FILE: examples/partial_path_process.py ================================================ from 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) ================================================ FILE: examples/pickle_deserialize.py ================================================ import io import pickle # pickle pick = pickle.dumps({'a': 'b', 'c': 'd'}) print(pickle.loads(pick)) file_obj = io.BytesIO() 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()) ================================================ FILE: examples/popen_wrappers.py ================================================ import 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()) ================================================ FILE: examples/pycrypto.py ================================================ from Crypto.Cipher import AES from Crypto import Random from . import CryptoMaterialsCacheEntry def test_pycrypto(): key = b'Sixteen byte key' iv = Random.new().read(AES.block_size) cipher = pycrypto_arc2.new(key, AES.MODE_CFB, iv) factory = CryptoMaterialsCacheEntry() ================================================ FILE: examples/pycryptodome.py ================================================ from Cryptodome.Cipher import AES from Cryptodome import Random from . import CryptoMaterialsCacheEntry def test_pycrypto(): key = b'Sixteen byte key' iv = Random.new().read(AES.block_size) cipher = pycrypto_arc2.new(key, AES.MODE_CFB, iv) factory = CryptoMaterialsCacheEntry() ================================================ FILE: examples/pyghmi.py ================================================ from pyghmi.ipmi import command cmd = command.Command(bmc="bmc", userid="userid", password="ZjE4ZjI0NTE4YmI2NGJjZDliOGY3ZmJiY2UyN2IzODQK") ================================================ FILE: examples/pytorch_load.py ================================================ import torch import torchvision.models as models # Example of saving a model model = models.resnet18(pretrained=True) torch.save(model.state_dict(), 'model_weights.pth') # Example of loading the model weights in an insecure way (should trigger B614) loaded_model = models.resnet18() loaded_model.load_state_dict(torch.load('model_weights.pth')) # Example of loading with weights_only=True (should NOT trigger B614) safe_model = models.resnet18() safe_model.load_state_dict(torch.load('model_weights.pth', weights_only=True)) # Example of loading with weights_only=False (should trigger B614) unsafe_model = models.resnet18() unsafe_model.load_state_dict(torch.load('model_weights.pth', weights_only=False)) # Example of loading with map_location but no weights_only (should trigger B614) cpu_model = models.resnet18() cpu_model.load_state_dict(torch.load('model_weights.pth', map_location='cpu')) # Example of loading with both map_location and weights_only=True (should NOT trigger B614) safe_cpu_model = models.resnet18() safe_cpu_model.load_state_dict(torch.load('model_weights.pth', map_location='cpu', weights_only=True)) # Example of a torch.*.load call that should NOT trigger B614 # Only pickle deserializers should trigger B614 torch.utils.cpp_extension.load(name="example_ext", sources=[]) ================================================ FILE: examples/random_module.py ================================================ import random import os import somelib bad = random.Random() bad = random.random() bad = random.randrange() bad = random.randint() bad = random.choice() bad = random.choices() bad = random.uniform() bad = random.triangular() bad = random.randbytes() bad = random.sample() bad = random.randrange() bad = random.getrandbits() good = os.urandom() good = random.SystemRandom() unknown = random() unknown = somelib.a.random() ================================================ FILE: examples/requests-missing-timeout.py ================================================ import httpx import requests import not_requests # Errors requests.get('https://gmail.com') requests.get('https://gmail.com', timeout=None) requests.post('https://gmail.com') requests.post('https://gmail.com', timeout=None) requests.put('https://gmail.com') requests.put('https://gmail.com', timeout=None) requests.delete('https://gmail.com') requests.delete('https://gmail.com', timeout=None) requests.patch('https://gmail.com') requests.patch('https://gmail.com', timeout=None) requests.options('https://gmail.com') requests.options('https://gmail.com', timeout=None) requests.head('https://gmail.com') requests.head('https://gmail.com', timeout=None) httpx.get('https://gmail.com') httpx.get('https://gmail.com', timeout=None) httpx.post('https://gmail.com') httpx.post('https://gmail.com', timeout=None) httpx.put('https://gmail.com') httpx.put('https://gmail.com', timeout=None) httpx.delete('https://gmail.com') httpx.delete('https://gmail.com', timeout=None) httpx.patch('https://gmail.com') httpx.patch('https://gmail.com', timeout=None) httpx.options('https://gmail.com') httpx.options('https://gmail.com', timeout=None) httpx.head('https://gmail.com') httpx.head('https://gmail.com', timeout=None) httpx.Client() httpx.Client(timeout=None) httpx.AsyncClient() httpx.AsyncClient(timeout=None) with httpx.Client() as client: client.get('https://gmail.com') with httpx.Client(timeout=None) as client: client.get('https://gmail.com') async with httpx.AsyncClient() as client: await client.get('https://gmail.com') async with httpx.AsyncClient(timeout=None) as client: await client.get('https://gmail.com') # Okay not_requests.get('https://gmail.com') requests.get('https://gmail.com', timeout=5) requests.post('https://gmail.com', timeout=5) requests.put('https://gmail.com', timeout=5) requests.delete('https://gmail.com', timeout=5) requests.patch('https://gmail.com', timeout=5) requests.options('https://gmail.com', timeout=5) requests.head('https://gmail.com', timeout=5) httpx.get('https://gmail.com', timeout=5) httpx.post('https://gmail.com', timeout=5) httpx.put('https://gmail.com', timeout=5) httpx.delete('https://gmail.com', timeout=5) httpx.patch('https://gmail.com', timeout=5) httpx.options('https://gmail.com', timeout=5) httpx.head('https://gmail.com', timeout=5) httpx.Client(timeout=5) httpx.AsyncClient(timeout=5) with httpx.Client(timeout=5) as client: client.get('https://gmail.com') async with httpx.AsyncClient(timeout=5) as client: await client.get('https://gmail.com') ================================================ FILE: examples/requests-ssl-verify-disabled.py ================================================ import httpx import requests # Errors requests.get('https://gmail.com', timeout=30, verify=True) requests.get('https://gmail.com', timeout=30, verify=False) requests.post('https://gmail.com', timeout=30, verify=True) requests.post('https://gmail.com', timeout=30, verify=False) requests.put('https://gmail.com', timeout=30, verify=True) requests.put('https://gmail.com', timeout=30, verify=False) requests.delete('https://gmail.com', timeout=30, verify=True) requests.delete('https://gmail.com', timeout=30, verify=False) requests.patch('https://gmail.com', timeout=30, verify=True) requests.patch('https://gmail.com', timeout=30, verify=False) requests.options('https://gmail.com', timeout=30, verify=True) requests.options('https://gmail.com', timeout=30, verify=False) requests.head('https://gmail.com', timeout=30, verify=True) requests.head('https://gmail.com', timeout=30, verify=False) # Okay httpx.request('GET', 'https://gmail.com', timeout=30, verify=True) httpx.request('GET', 'https://gmail.com', timeout=30, verify=False) httpx.get('https://gmail.com', timeout=30, verify=True) httpx.get('https://gmail.com', timeout=30, verify=False) httpx.options('https://gmail.com', timeout=30, verify=True) httpx.options('https://gmail.com', timeout=30, verify=False) httpx.head('https://gmail.com', timeout=30, verify=True) httpx.head('https://gmail.com', timeout=30, verify=False) httpx.post('https://gmail.com', timeout=30, verify=True) httpx.post('https://gmail.com', timeout=30, verify=False) httpx.put('https://gmail.com', timeout=30, verify=True) httpx.put('https://gmail.com', timeout=30, verify=False) httpx.patch('https://gmail.com', timeout=30, verify=True) httpx.patch('https://gmail.com', timeout=30, verify=False) httpx.delete('https://gmail.com', timeout=30, verify=True) httpx.delete('https://gmail.com', timeout=30, verify=False) httpx.stream('https://gmail.com', timeout=30, verify=True) httpx.stream('https://gmail.com', timeout=30, verify=False) httpx.Client(timeout=30) httpx.Client(timeout=30, verify=False) httpx.AsyncClient(timeout=30) httpx.AsyncClient(timeout=30, verify=False) ================================================ FILE: examples/shelve_open.py ================================================ import os import shelve import tempfile with tempfile.TemporaryDirectory() as d: filename = os.path.join(d, 'shelf') with shelve.open(filename) as db: db['spam'] = {'eggs': 'ham'} with shelve.open(filename) as db: print(db['spam']) ================================================ FILE: examples/skip.py ================================================ subprocess.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"]) ================================================ FILE: examples/snmp.py ================================================ from pysnmp.hlapi import CommunityData, UsmUserData # SHOULD FAIL a = CommunityData('public', mpModel=0) # SHOULD FAIL insecure = UsmUserData("securityName") # SHOULD FAIL auth_no_priv = UsmUserData("securityName","authName") # SHOULD PASS less_insecure = UsmUserData("securityName","authName","privName") ================================================ FILE: examples/sql_multiline_statements.py ================================================ import 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 query = """WITH cte AS (SELECT x FROM foo) SELECT x FROM cte WHERE x = '%s'""" % identifier # bad alternate forms query = """SELECT * FROM foo WHERE id = '""" + identifier + "'" query = """SELECT * FROM foo WHERE id = '{}'""".format(identifier) query = f""" SELECT * FROM foo WHERE id = {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) # bad alternate forms cur.execute("""SELECT * FROM foo WHERE id = '""" + identifier + "'") cur.execute("""SELECT * FROM foo WHERE id = '{}'""".format(identifier)) # bad with f-string query = f""" SELECT * FROM foo WHERE id = {identifier} """ query = f""" SELECT * FROM foo WHERE id = {identifier} """ query = f""" SELECT * FROM foo WHERE id = {identifier}""" query = f""" SELECT * FROM foo WHERE id = {identifier}""" cur.execute(f""" SELECT {column_name} FROM foo WHERE id = 1""") cur.execute(f""" SELECT {a + b} FROM foo WHERE id = 1""") cur.execute(f""" INSERT INTO {table_name} VALUES (1)""") cur.execute(f""" UPDATE {table_name} SET id = 1""") # implicit concatenation mixed with f-strings cur.execute("SELECT " f"{column_name} " "FROM foo " "WHERE id = 1" ) cur.execute("INSERT INTO " f"{table_name} " "VALUES (1)") cur.execute(f"UPDATE {table_name} " "SET id = 1") # 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) # bug: https://bugs.launchpad.net/bandit/+bug/1479625 def a(): def b(): pass return b a()("""SELECT %s FROM foo""" % val) # skip query = """SELECT * FROM foo WHERE id = '%s'""" % identifier # nosec query = """SELECT * FROM foo WHERE id = '%s'""" % identifier # nosec B608 query = """ SELECT * FROM foo WHERE id = '%s' """ % identifier # nosec B608 query = f""" SELECT * FROM foo WHERE id = {identifier} """ # nosec query = f""" SELECT * FROM foo WHERE id = {identifier} """ # nosec B608 query = f""" SELECT * FROM foo WHERE id = {identifier}""" # nosec query = f""" SELECT * FROM foo WHERE id = {identifier}""" # nosec B608 cur.execute("SELECT * " # nosec "FROM foo " f"WHERE id = {identifier}") cur.execute("SELECT * " # nosec B608 "FROM foo " f"WHERE id = {identifier}") query = ("SELECT * " # nosec "FROM foo " f"WHERE id = {identifier}") query = ("SELECT * " # nosec B608 "FROM foo " f"WHERE id = {identifier}") # nosec is not recognized for the 4 below cases in python 3.7 query = ("SELECT * " "FROM foo " # nosec f"WHERE id = {identifier}") query = ("SELECT * " "FROM foo " # nosec B608 f"WHERE id = {identifier}") query = ("SELECT * " "FROM foo " f"WHERE id = {identifier}") # nosec query = ("SELECT * " "FROM foo " f"WHERE id = {identifier}") # nosec B608 ================================================ FILE: examples/sql_statements.py ================================================ import sqlalchemy # bad query = "SELECT * FROM foo WHERE id = '%s'" % identifier query = "INSERT INTO foo VALUES ('a', 'b', '%s')" % value 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 query = """WITH cte AS (SELECT x FROM foo) SELECT x FROM cte WHERE x = '%s'""" % identifier # bad alternate forms query = "SELECT * FROM foo WHERE id = '" + identifier + "'" query = "SELECT * FROM foo WHERE id = '{}'".format(identifier) query = "SELECT * FROM foo WHERE id = '[VALUE]'".replace("[VALUE]", identifier) # bad cur.execute("SELECT * FROM foo WHERE id = '%s'" % identifier) cur.execute("INSERT INTO foo VALUES ('a', 'b', '%s')" % value) 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 alternate forms cur.execute("SELECT * FROM foo WHERE id = '" + identifier + "'") cur.execute("SELECT * FROM foo WHERE id = '{}'".format(identifier)) cur.execute("SELECT * FROM foo WHERE id = '[VALUE]'".replace("[VALUE]", identifier)) # bad f-strings cur.execute(f"SELECT {column_name} FROM foo WHERE id = 1") cur.execute(f"SELECT {a + b} FROM foo WHERE id = 1") cur.execute(f"INSERT INTO {table_name} VALUES (1)") cur.execute(f"INSERT INTO {table_name} VALUES(1)") cur.execute(f"UPDATE {table_name} SET id = 1") # good cur.execute("SELECT * FROM foo WHERE id = '%s'", identifier) cur.execute("INSERT INTO foo VALUES ('a', 'b', '%s')", value) 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) # 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") ================================================ FILE: examples/ssl-insecure-version.py ================================================ import 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(ssl_version=ssl.PROTOCOL_TLSv1_1) SSL.Context(method=SSL.TLSv1_1_METHOD) herp_derp(ssl_version=ssl.PROTOCOL_TLSv1_1) herp_derp(method=SSL.TLSv1_1_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 def open_ssl_socket(version=SSL.TLSv1_1_METHOD): pass # this one will pass ok def open_ssl_socket(version=SSL.TLSv1_2_METHOD): pass ================================================ FILE: examples/subprocess_shell.py ================================================ import subprocess from subprocess import Popen as pop def Popen(*args, **kwargs): print('hi') def __len__(self): return 0 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.check_output([], stdout=None) subprocess.getoutput('/bin/ls -l') subprocess.getstatusoutput('/bin/ls -l') subprocess.run(['/bin/ls', '-l']) subprocess.run('/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) command = 'pwd' subprocess.call(command, shell='True') subprocess.call(command, shell='False') subprocess.call(command, shell='None') subprocess.call(command, shell=1) subprocess.call(command, shell=Popen()) subprocess.call(command, shell=[True]) subprocess.call(command, shell={'IS': 'True'}) subprocess.call(command, shell=command) subprocess.call(command, shell=False) subprocess.call(command, shell=0) subprocess.call(command, shell=[]) subprocess.call(command, shell={}) subprocess.call(command, shell=None) ================================================ FILE: examples/tarfile_extractall.py ================================================ import sys import tarfile import tempfile def unsafe_archive_handler(filename): tar = tarfile.open(filename) tar.extractall(path=tempfile.mkdtemp()) tar.close() def managed_members_archive_handler(filename): tar = tarfile.open(filename) tar.extractall(path=tempfile.mkdtemp(), members=members_filter(tar)) tar.close() def filter_data_archive_handler(filename): tar = tarfile.open(filename) tar.extractall(path=tempfile.mkdtemp(), filter="data") tar.close() def filter_fully_trusted_archive_handler(filename): tar = tarfile.open(filename) tar.extractall(path=tempfile.mkdtemp(), filter="fully_trusted") tar.close() def list_members_archive_handler(filename): tar = tarfile.open(filename) tar.extractall(path=tempfile.mkdtemp(), members=[]) tar.close() def provided_members_archive_handler(filename): tar = tarfile.open(filename) tarfile.extractall(path=tempfile.mkdtemp(), members=tar) tar.close() def members_filter(tarfile): result = [] for member in tarfile.getmembers(): if '../' in member.name: print('Member name container directory traversal sequence') continue elif (member.issym() or member.islnk()) and ('../' in member.linkname): print('Symlink to external resource') continue result.append(member) return result if __name__ == "__main__": if len(sys.argv) > 1: filename = sys.argv[1] unsafe_archive_handler(filename) managed_members_archive_handler(filename) filter_data_archive_handler(filename) filter_fully_trusted_archive_handler(filename) ================================================ FILE: examples/telnetlib.py ================================================ import 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()) ================================================ FILE: examples/trojansource.py ================================================ #!/usr/bin/env python3 # cf. https://trojansource.codes/ & https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-42574 access_level = "user" if access_level != 'none‮⁦': # Check if admin ⁩⁦' and access_level != 'user print("You are an admin.\n") ================================================ FILE: examples/trojansource_latin1.py ================================================ #!/usr/bin/env python3 # -*- coding: latin-1 -*- # cf. https://trojansource.codes & https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-42574 # Some special characters: access_level = "user" if access_level != 'none??': # Check if admin ??' and access_level != 'user print("You are an admin.\n") ================================================ FILE: examples/try_except_continue.py ================================================ # 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 ================================================ FILE: examples/try_except_pass.py ================================================ # 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 ================================================ FILE: examples/unverified_context.py ================================================ import ssl # Correct context = ssl.create_default_context() # Incorrect: unverified context context = ssl._create_unverified_context() ================================================ FILE: examples/urlopen.py ================================================ ''' Example dangerous usage of urllib.request opener functions The urllib.request 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. ''' # Python 3 import urllib.request # Six import six def test_urlopen(): # 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') ================================================ FILE: examples/weak_cryptographic_key_sizes.py ================================================ from 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 as pycrypto_dsa from Crypto.PublicKey import RSA as pycrypto_rsa from Cryptodome.PublicKey import DSA as pycryptodomex_dsa from Cryptodome.PublicKey import RSA as pycryptodomex_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()) pycrypto_dsa.generate(bits=2048) pycrypto_rsa.generate(bits=2048) pycryptodomex_dsa.generate(bits=2048) pycryptodomex_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()) pycrypto_dsa.generate(4096) pycrypto_rsa.generate(4096) pycryptodomex_dsa.generate(4096) pycryptodomex_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()) pycrypto_dsa.generate(bits=1024) pycrypto_rsa.generate(bits=1024) pycryptodomex_dsa.generate(bits=1024) pycryptodomex_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()) pycrypto_dsa.generate(512) pycrypto_rsa.generate(512) pycryptodomex_dsa.generate(512) pycryptodomex_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()) # Can't reliably know which curve was passed, in some cases like below ec.generate_private_key( curve=curves[self.curve]['create'](self.size), backend=backends.default_backend() ) ================================================ FILE: examples/wildcard-injection.py ================================================ import 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', '*']) ================================================ FILE: examples/xml_etree_celementtree.py ================================================ import 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() ================================================ FILE: examples/xml_etree_elementtree.py ================================================ import 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() ================================================ FILE: examples/xml_expatbuilder.py ================================================ import 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) ================================================ FILE: examples/xml_expatreader.py ================================================ import xml.sax.expatreader as bad import defusedxml.expatreader as good p = bad.create_parser() b = good.create_parser() ================================================ FILE: examples/xml_minidom.py ================================================ from 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) ================================================ FILE: examples/xml_pulldom.py ================================================ from 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) ================================================ FILE: examples/xml_sax.py ================================================ import 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() ================================================ FILE: examples/xml_xmlrpc.py ================================================ import xmlrpc 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() ================================================ FILE: examples/yaml_load.py ================================================ import json import yaml from yaml import CSafeLoader from yaml import SafeLoader def test_yaml_load(): ystr = yaml.dump({'a': 1, 'b': 2, 'c': 3}) y = yaml.load(ystr) yaml.dump(y) try: y = yaml.load(ystr, Loader=yaml.CSafeLoader) except AttributeError: # CSafeLoader only exists if you build yaml with LibYAML y = yaml.load(ystr, Loader=yaml.SafeLoader) def test_json_load(): # no issue should be found j = json.load("{}") yaml.load("{}", Loader=yaml.Loader) # no issue should be found yaml.load("{}", SafeLoader) yaml.load("{}", yaml.SafeLoader) yaml.load("{}", CSafeLoader) yaml.load("{}", yaml.CSafeLoader) ================================================ FILE: funding.json ================================================ { "version": "v1.0.0", "entity": { "type": "individual", "role": "maintainer", "name": "Eric Brown", "email": "eric_wade_brown@yahoo.com", "phone": "", "description": "I’m passionate about developing tools that empower engineers to produce secure, hardened code, reducing vulnerabilities and strengthening software integrity. With a focus on security automation, I aim to make secure coding practices more accessible and integrated into development workflows.", "webpageUrl": { "url": "https://github.com" } }, "projects": [{ "guid": "bandit", "name": "Bandit", "description": " Bandit is a tool designed to find common security issues in Python code.", "webpageUrl": { "url": "https://github.com/PyCQA/bandit" }, "repositoryUrl": { "url": "https://github.com/PyCQA/bandit" }, "licenses": ["spdx:Apache-2.0"], "tags": ["python", "static-code-analysis", "security", "security-tools"] }], "funding": { "channels": [ { "guid": "github", "type": "payment-provider", "address": "https://github.com/sponsors/ericwb", "description": "Pay with your credit card through this gateway and setup recurring subscriptions." }, { "guid": "psf", "type": "payment-provider", "address": "https://psfmember.org/civicrm/contribute/transact/?reset=1&id=42", "description": "Pay with your credit card through this gateway and setup recurring subscriptions." } ], "plans": [ { "guid": "developer-time", "status": "active", "name": "Developer compensation", "description": "This will cover the cost of one developer working part-time on the projects.", "amount": 1000, "currency": "USD", "frequency": "monthly", "channels": ["github", "psf"] }, { "guid": "angel-plan", "status": "active", "name": "Goodwill plan", "description": "Pay anything you wish to show your goodwill for the project.", "amount": 0, "currency": "USD", "frequency": "one-time", "channels": ["psf"] } ] } } ================================================ FILE: pylintrc ================================================ # 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. # W0622: Redefining id is fine. # TODO(browne): fix these in the future # C0103: invalid-name # C0114: Missing module docstring # C0115: Missing class docstring # C0116: Missing function or method docstring # C0201: consider-iterating-dictionary # C0206: Consider iterating with .items() # C0209: Foramtting a regular string which could be an f-string # C0413: wrong-import-position # C0415: Import outside toplevel # C1802: Do not use `len(SEQUENCE)` without comparison to determine if a sequence is empty # E0611: No name in module # E1101: no-member # R0801: Similar lines in 2 files # R0902: too-many-instance-attributes # R0912: too-many-branches # R0913: too-many-arguments # R0914: too-many-locals # R0915: too-many-statements # R1702: too-many-nested-blocks # R1705: no-else-return # R1710: inconsistent-return-statements # R1714: Consider merging these comparisons with 'in' # R1721: Unnecessary use of a comprehension # R1732: Consider using 'with' for resource-allocating operations # R1734: Consider using [] instead of list() # R1735: use-dict-literal # W0105: String statement has no effect # W0201: attribute-defined-outside-init # W0212: protected-access # W0246: Useless parent or super() delegation # W0603: global-statement # W0612: Unused variable # W0613: unused-argument # W0621: redefined-outer-name # W0707: Consider explicitly re-raising # W0718: Catching too general exception Exception # W1201: logging-not-lazy # W1203: Use lazy % or % formatting in logging functions # W1404: Implicit string concatenation found in call # W1514: Using open without explicitly specifying an encoding disable=C0103,C0114,C0115,C0116,C0201,C0206,C0209,C0301,C0413,C0415,C1802,F0401,W0511,W0622,E0611,E1101,R0801,R0902,R0912,R0913,R0914,R0915,R1702,R1705,R1710,R1714,R1721,R1732,R1734,R1735,W0105,W0201,W0212,W0246,W0603,W0612,W0613,W0621,W0707,W0718,W1201,W1203,W1404,W1514 [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 # We don't need to do pylint on the examples, too many false positives ignore-paths=examples # Ignore docstrings when computing similarities. ignore-docstrings=yes # Ignore imports when computing similarities. ignore-imports=yes ================================================ FILE: requirements.txt ================================================ # 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. PyYAML>=5.3.1 # MIT stevedore>=1.20.0 # Apache-2.0 colorama>=0.3.9;platform_system=="Windows" # BSD License (3 clause) rich # MIT ================================================ FILE: scripts/main.py ================================================ #!/usr/bin/env python # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 from bandit import bandit if __name__ == "__main__": bandit.main() ================================================ FILE: setup.cfg ================================================ [metadata] name = bandit summary = Security oriented static analyser for python code. description_file = README.rst author = PyCQA author_email = code-quality@python.org home_page = https://bandit.readthedocs.io/ license = Apache-2.0 classifiers = Development Status :: 5 - Production/Stable Environment :: Console Intended Audience :: Information Technology Intended Audience :: System Administrators Intended Audience :: Developers Operating System :: POSIX :: Linux Operating System :: MacOS :: MacOS X Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Programming Language :: Python :: 3.13 Programming Language :: Python :: 3.14 Programming Language :: Python :: 3 :: Only Topic :: Security project_urls = Documentation = https://bandit.readthedocs.io/ Release Notes = https://github.com/PyCQA/bandit/releases Source Code = https://github.com/PyCQA/bandit Issue Tracker = https://github.com/PyCQA/bandit/issues Discord = https://discord.gg/qYxpadCgkx Sponsor = https://psfmember.org/civicrm/contribute/transact/?reset=1&id=42 [extras] yaml = PyYAML toml = tomli>=1.1.0; python_version < "3.11" baseline = GitPython>=3.1.30 sarif = sarif-om>=1.0.4 jschema-to-python>=1.2.3 [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 sarif = bandit.formatters.sarif:report screen = bandit.formatters.screen:report yaml = bandit.formatters.yaml:report custom = bandit.formatters.custom: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/request_without_timeout.py request_without_timeout = bandit.plugins.request_without_timeout:request_without_timeout # 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/hashlib_insecure_functions.py hashlib_insecure_functions = bandit.plugins.hashlib_insecure_functions:hashlib # bandit/plugins/injection_wildcard.py linux_commands_wildcard_injection = bandit.plugins.injection_wildcard:linux_commands_wildcard_injection # bandit/plugins/django_sql_injection.py django_extra_used = bandit.plugins.django_sql_injection:django_extra_used django_rawsql_used = bandit.plugins.django_sql_injection:django_rawsql_used # 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/django_xss.py django_mark_safe = bandit.plugins.django_xss:django_mark_safe # 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 # bandit/plugins/ssh_no_host_key_verification.py ssh_no_host_key_verification = bandit.plugins.ssh_no_host_key_verification:ssh_no_host_key_verification # bandit/plugins/snmp_security_check.py snmp_insecure_version = bandit.plugins.snmp_security_check:snmp_insecure_version_check snmp_weak_cryptography = bandit.plugins.snmp_security_check:snmp_crypto_check # bandit/plugins/logging_config_insecure_listen.py logging_config_insecure_listen = bandit.plugins.logging_config_insecure_listen:logging_config_insecure_listen #bandit/plugins/tarfile_unsafe_members.py tarfile_unsafe_members = bandit.plugins.tarfile_unsafe_members:tarfile_unsafe_members #bandit/plugins/pytorch_load.py pytorch_load = bandit.plugins.pytorch_load:pytorch_load # bandit/plugins/trojansource.py trojansource = bandit.plugins.trojansource:trojansource # bandit/plugins/markupsafe_markup_xss.py markupsafe_markup_xss = bandit.plugins.markupsafe_markup_xss:markupsafe_markup_xss # bandit/plugins/huggingface_unsafe_download.py huggingface_unsafe_download = bandit.plugins.huggingface_unsafe_download:huggingface_unsafe_download [build_sphinx] all_files = 1 build-dir = doc/build source-dir = doc/source [pbr] autodoc_tree_index_modules = True autodoc_tree_excludes = examples* ================================================ FILE: setup.py ================================================ # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 import os import setuptools data_files = [] man_path = "doc/build/man/bandit.1" if os.path.isfile(man_path): data_files.append(("share/man/man1", [man_path])) setuptools.setup( python_requires=">=3.10", setup_requires=["pbr>=2.0.0"], pbr=True, data_files=data_files, ) ================================================ FILE: test-requirements.txt ================================================ # 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.5.4 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD flake8>=4.0.0 # Apache-2.0 stestr>=2.5.0 # Apache-2.0 testscenarios>=0.5.0 # Apache-2.0/BSD testtools>=2.3.0 # MIT beautifulsoup4>=4.8.0 # MIT pylint==1.9.4 # GPLv2 ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/functional/__init__.py ================================================ ================================================ FILE: tests/functional/test_baseline.py ================================================ # Copyright 2016 IBM Corp. # # SPDX-License-Identifier: Apache-2.0 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().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 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 (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) ================================================ FILE: tests/functional/test_functional.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 import os from contextlib import contextmanager 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().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) @contextmanager def with_test_set(self, ts): """A helper context manager to change the test set without side-effects for any follow-up tests. """ orig_ts = self.b_mgr.b_ts self.b_mgr.b_ts = ts try: yield finally: self.b_mgr.b_ts = orig_ts 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) result = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 0}, } for test_scores in self.b_mgr.scores: for score_type in test_scores: self.assertIn(score_type, expect) for idx, rank in enumerate(C.RANKING): result[score_type][rank] = ( test_scores[score_type][idx] // C.RANKING_VALUES[rank] ) self.assertDictEqual(expect, 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 = f"{criteria}.{rank}" expected = 0 if expect["issues"].get(criteria).get(rank): 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": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 0}, } self.check_example("binding.py", expect) def test_crypto_md5(self): """Test the `hashlib.md5` example.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 16, "HIGH": 9}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 25}, } self.check_example("crypto-md5.py", expect) def test_ciphers(self): """Test the `Crypto.Cipher` example.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 24}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 25}, } self.check_example("ciphers.py", expect) def test_cipher_modes(self): """Test for insecure cipher modes.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 1}, } self.check_example("cipher-modes.py", expect) def test_eval(self): """Test the `eval` example.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 3, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 3}, } self.check_example("eval.py", expect) def test_mark_safe(self): """Test the `mark_safe` example.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 1}, } self.check_example("mark_safe.py", expect) def test_exec(self): """Test the `exec` example.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 1}, } self.check_example("exec.py", expect) def test_hardcoded_passwords(self): """Test for hard-coded passwords.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 16, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 16, "HIGH": 0}, } self.check_example("hardcoded-passwords.py", expect) def test_hardcoded_tmp(self): """Test for hard-coded /tmp, /var/tmp, /dev/shm.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 3, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 3, "HIGH": 0}, } self.check_example("hardcoded-tmp.py", expect) def test_imports_aliases(self): """Test the `import X as Y` syntax.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 4, "MEDIUM": 1, "HIGH": 4}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 9}, } self.check_example("imports-aliases.py", expect) def test_imports_from(self): """Test the `from X import Y` syntax.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 3, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 3}, } self.check_example("imports-from.py", expect) def test_imports_function(self): """Test the `__import__` function.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 2, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 2}, } self.check_example("imports-function.py", expect) def test_telnet_usage(self): """Test for `import telnetlib` and Telnet.* calls.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 2}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 2}, } self.check_example("telnetlib.py", expect) def test_ftp_usage(self): """Test for `import ftplib` and FTP.* calls.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 3}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 3}, } self.check_example("ftplib.py", expect) def test_imports(self): """Test for dangerous imports.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 2, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 2}, } self.check_example("imports.py", expect) def test_imports_using_importlib(self): """Test for dangerous imports using importlib.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 4, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 4}, } self.check_example("imports-with-importlib.py", expect) def test_mktemp(self): """Test for `tempfile.mktemp`.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 4, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "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": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 0}, } self.check_example("okay.py", expect) def test_subdirectory_okay(self): """Test a vulnerability-free file under a subdirectory.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 0}, } self.check_example("init-py-test/subdirectory-okay.py", expect) def test_os_chmod(self): """Test setting file permissions.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 4, "HIGH": 8}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 11}, } self.check_example("os-chmod.py", expect) def test_os_exec(self): """Test for `os.exec*`.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 8, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 8, "HIGH": 0}, } self.check_example("os-exec.py", expect) def test_os_popen(self): """Test for `os.popen`.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 8, "MEDIUM": 0, "HIGH": 1}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 9}, } self.check_example("os-popen.py", expect) def test_os_spawn(self): """Test for `os.spawn*`.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 8, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 8, "HIGH": 0}, } self.check_example("os-spawn.py", expect) def test_os_startfile(self): """Test for `os.startfile`.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 3, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 3, "HIGH": 0}, } self.check_example("os-startfile.py", expect) def test_os_system(self): """Test for `os.system`.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 1}, } self.check_example("os_system.py", expect) def test_pickle(self): """Test for the `pickle` module.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 3, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 4}, } self.check_example("pickle_deserialize.py", expect) def test_dill(self): """Test for the `dill` module.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 3, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 4}, } self.check_example("dill.py", expect) def test_shelve(self): """Test for the `shelve` module.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 2, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 3}, } self.check_example("shelve_open.py", expect) def test_jsonpickle(self): """Test for the `jsonpickle` module.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 3, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 3}, } self.check_example("jsonpickle.py", expect) def test_pandas_read_pickle(self): """Test for the `pandas.read_pickle` module.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 1, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 2}, } self.check_example("pandas_read_pickle.py", expect) def test_popen_wrappers(self): """Test the `popen2` and `commands` modules.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 7, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 7}, } self.check_example("popen_wrappers.py", expect) def test_random_module(self): """Test for the `random` module.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 12, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 12}, } self.check_example("random_module.py", expect) def test_requests_ssl_verify_disabled(self): """Test for the `requests` library skipping verification.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 18}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 18}, } self.check_example("requests-ssl-verify-disabled.py", expect) def test_requests_without_timeout(self): """Test for the `requests` library missing timeouts.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 25, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 25, "MEDIUM": 0, "HIGH": 0}, } self.check_example("requests-missing-timeout.py", expect) def test_skip(self): """Test `#nosec` and `#noqa` comments.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 5, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 5}, } self.check_example("skip.py", expect) def test_ignore_skip(self): """Test --ignore-nosec flag.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 7, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "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": { "UNDEFINED": 0, "LOW": 0, "MEDIUM": 23, "HIGH": 0, }, "CONFIDENCE": { "UNDEFINED": 0, "LOW": 11, "MEDIUM": 12, "HIGH": 0, }, } self.check_example("sql_statements.py", expect) def test_multiline_sql_statements(self): """ Test for SQL injection through string building using multi-line strings. """ example_file = "sql_multiline_statements.py" confidence_low_tests = 13 severity_medium_tests = 26 nosec_tests = 7 skipped_tests = 8 expect = { "SEVERITY": { "UNDEFINED": 0, "LOW": 0, "MEDIUM": severity_medium_tests, "HIGH": 0, }, "CONFIDENCE": { "UNDEFINED": 0, "LOW": confidence_low_tests, "MEDIUM": 13, "HIGH": 0, }, } expect_stats = { "nosec": nosec_tests, "skipped_tests": skipped_tests, } self.check_example(example_file, expect) self.check_metrics(example_file, expect_stats) def test_ssl_insecure_version(self): """Test for insecure SSL protocol versions.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 13, "HIGH": 9}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 14, "HIGH": 9}, } self.check_example("ssl-insecure-version.py", expect) def test_subprocess_shell(self): """Test for `subprocess.Popen` with `shell=True`.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 24, "MEDIUM": 1, "HIGH": 11}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 0, "HIGH": 35}, } self.check_example("subprocess_shell.py", expect) def test_urlopen(self): """Test for dangerous URL opening.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 8, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 8}, } self.check_example("urlopen.py", expect) def test_wildcard_injection(self): """Test for wildcard injection in shell commands.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 10, "MEDIUM": 0, "HIGH": 4}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 5, "HIGH": 9}, } self.check_example("wildcard-injection.py", expect) def test_django_sql_injection(self): """Test insecure extra functions on Django.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 11, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 11, "HIGH": 0}, } self.check_example("django_sql_injection_extra.py", expect) def test_django_sql_injection_raw(self): """Test insecure raw functions on Django.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 6, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 6, "HIGH": 0}, } self.check_example("django_sql_injection_raw.py", expect) def test_yaml(self): """Test for `yaml.load`.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 2, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 2}, } self.check_example("yaml_load.py", expect) def test_host_key_verification(self): """Test for ignoring host key verification.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 8}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 8, "HIGH": 0}, } self.check_example("no_host_key_verification.py", expect) def test_jinja2_templating(self): """Test jinja templating for potential XSS bugs.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 5}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 2, "HIGH": 3}, } self.check_example("jinja2_templating.py", expect) def test_mako_templating(self): """Test Mako templates for XSS.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 3, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 3}, } self.check_example("mako_templating.py", expect) def test_django_xss_secure(self): """Test false positives for Django XSS""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 0}, } with self.with_test_set( b_test_set.BanditTestSet( config=self.b_mgr.b_conf, profile={"exclude": ["B308"]} ) ): self.check_example("mark_safe_secure.py", expect) def test_django_xss_insecure(self): """Test for Django XSS via django.utils.safestring""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 29, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 29}, } with self.with_test_set( b_test_set.BanditTestSet( config=self.b_mgr.b_conf, profile={"exclude": ["B308"]} ) ): self.check_example("mark_safe_insecure.py", expect) def test_xml(self): """Test xml vulnerabilities.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 4, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 5}, } self.check_example("xml_etree_celementtree.py", expect) expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 2, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 3}, } self.check_example("xml_expatbuilder.py", expect) expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 2, "MEDIUM": 2, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 4}, } self.check_example("xml_pulldom.py", expect) expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 1}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 1}, } self.check_example("xml_xmlrpc.py", expect) expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 4, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 5}, } self.check_example("xml_etree_elementtree.py", expect) expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 1, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 2}, } self.check_example("xml_expatreader.py", expect) expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 2, "MEDIUM": 2, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 4}, } self.check_example("xml_minidom.py", expect) expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 2, "MEDIUM": 6, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 8}, } self.check_example("xml_sax.py", expect) def test_httpoxy(self): """Test httpoxy vulnerability.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 1}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "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.""" test = next( x for x in self.b_mgr.b_ts.tests["Assert"] if x.__name__ == "assert_used" ) test._config = {"skips": []} expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 1}, } self.check_example("assert.py", expect) test._config = {"skips": ["*assert.py"]} expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 0}, } self.check_example("assert.py", expect) test._config = {} expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 1}, } self.check_example("assert.py", expect) def test_paramiko_injection(self): """Test paramiko command execution.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 0}, } self.check_example("paramiko_injection.py", expect) def test_partial_path(self): """Test process spawning with partial file paths.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 11, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "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": {"UNDEFINED": 0, "LOW": 3, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 3}, } self.check_example("try_except_continue.py", expect) test._config = {"check_typed_exception": False} expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 2, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "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": {"UNDEFINED": 0, "LOW": 3, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 3}, } self.check_example("try_except_pass.py", expect) test._config = {"check_typed_exception": False} expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 2, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "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": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 8, "HIGH": 8}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 16}, } 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(3, len(issues)) self.assertTrue( issues[0].fname.endswith("examples/multiline_statement.py") ) self.assertEqual(1, issues[0].lineno) self.assertEqual(list(range(1, 2)), 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()) self.assertEqual(11, issues[2].lineno) self.assertEqual(list(range(8, 13 + 1)), issues[2].linerange) self.assertIn("shell=True", issues[2].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": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 1}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 0}, } self.check_example("flask_debug.py", expect) def test_nosec(self): expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 5, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 5}, } 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 = f"""{{ "results": [ {{ "code": "...", "filename": "{os.getcwd()}/examples/flask_debug.py", "issue_confidence": "MEDIUM", "issue_severity": "HIGH", "issue_cwe": {{ "id": 94, "link": "https://cwe.mitre.org/data/definitions/94.html" }}, "issue_text": "{issue_text}", "line_number": 10, "col_offset": 0, "line_range": [ 10 ], "test_name": "flask_debug_true", "test_id": "B201" }} ] }} """ 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_unverified_context(self): """Test for `ssl._create_unverified_context`.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 1}, } self.check_example("unverified_context.py", expect) def test_hashlib_new_insecure_functions(self): """Test insecure hash functions created by `hashlib.new`.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 9}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 9}, } self.check_example("hashlib_new_insecure_functions.py", expect) def test_blacklist_pycrypto(self): """Test importing pycrypto module""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 2}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 2}, } self.check_example("pycrypto.py", expect) def test_no_blacklist_pycryptodome(self): """Test importing pycryptodome module make sure it's no longer blacklisted """ expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 0}, } self.check_example("pycryptodome.py", expect) def test_blacklist_pyghmi(self): """Test calling pyghmi methods""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 0, "HIGH": 1}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 1}, } self.check_example("pyghmi.py", expect) def test_snmp_security_check(self): """Test insecure and weak crypto usage of SNMP.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 3, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 3}, } self.check_example("snmp.py", expect) def test_tarfile_unsafe_members(self): """Test insecure usage of tarfile.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 2, "HIGH": 2}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 2, "HIGH": 2}, } self.check_example("tarfile_extractall.py", expect) def test_pytorch_load(self): """Test insecure usage of torch.load.""" expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 3, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 3}, } self.check_example("pytorch_load.py", expect) def test_trojansource(self): expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 1}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 0}, } self.check_example("trojansource.py", expect) def test_trojansource_latin1(self): expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 0}, } self.check_example("trojansource_latin1.py", expect) def test_markupsafe_markup_xss(self): expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 4, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 4}, } self.check_example("markupsafe_markup_xss.py", expect) def test_markupsafe_markup_xss_extend_markup_names(self): expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 2, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 2}, } b_conf = b_config.BanditConfig() b_conf.config["markupsafe_xss"] = { "extend_markup_names": ["webhelpers.html.literal"] } with self.with_test_set(b_test_set.BanditTestSet(config=b_conf)): self.check_example( "markupsafe_markup_xss_extend_markup_names.py", expect ) def test_markupsafe_markup_xss_allowed_calls(self): expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 1}, } b_conf = b_config.BanditConfig() b_conf.config["markupsafe_xss"] = {"allowed_calls": ["bleach.clean"]} with self.with_test_set(b_test_set.BanditTestSet(config=b_conf)): self.check_example( "markupsafe_markup_xss_allowed_calls.py", expect ) def test_huggingface_unsafe_download(self): expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 15, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 15}, } self.check_example("huggingface_unsafe_download.py", expect) ================================================ FILE: tests/functional/test_runtime.py ================================================ # Copyright (c) 2015 VMware, Inc. # # SPDX-License-Identifier: Apache-2.0 import os import subprocess 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) self.assertIn("usage: bandit [-h]", output) def test_piped_input(self): with open("examples/imports.py") 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("tests were discovered and loaded:", output) # 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("Files skipped (1):", output) self.assertIn("nonsense2.py (syntax error while parsing AST", 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) ================================================ FILE: tests/unit/__init__.py ================================================ ================================================ FILE: tests/unit/cli/__init__.py ================================================ ================================================ FILE: tests/unit/cli/test_baseline.py ================================================ # # Copyright 2015 Hewlett-Packard Enterprise # # SPDX-License-Identifier: Apache-2.0 import os import subprocess from unittest import mock import fixtures import git import testtools from bandit.cli import 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().setUp() self.current_directory = os.getcwd() def tearDown(self): # Tear down after running each test case super().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", "w") 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, "w") 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, "w") 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, "w") 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, "w") 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", "w") 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 = f"{baseline.report_basename}.txt" with open(existing_report, "w") 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, "w") 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) ================================================ FILE: tests/unit/cli/test_config_generator.py ================================================ # # Copyright 2016 Hewlett-Packard Enterprise # # SPDX-License-Identifier: Apache-2.0 import importlib import logging from unittest 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().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().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, default_flow_style=False), 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) ================================================ FILE: tests/unit/cli/test_main.py ================================================ # Copyright 2016 IBM Corp. # # SPDX-License-Identifier: Apache-2.0 import logging import os from unittest import mock import fixtures 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().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().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() 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(logging.DEBUG) self.assertEqual(logging.DEBUG, self.logger.level) class BanditCLIMainTests(testtools.TestCase): def setUp(self): super().setUp() self.current_directory = os.getcwd() def tearDown(self): super().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, "w") 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 # with None or a string default value arg_val = "file" ini_val = "vuln" option_name = "aggregate" for default_val in (None, "default"): self.assertEqual( arg_val, bandit._log_option_source( default_val, 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 default_val = None ini_val = "vuln" option_name = "aggregate" self.assertEqual( ini_val, bandit._log_option_source(default_val, None, ini_val, option_name), ) def test_log_option_source_ini_val_with_str_default_and_no_arg_val(self): # Test that the ini value is returned when no command argument is # provided default_val = "file" arg_val = "file" ini_val = "vuln" option_name = "aggregate" self.assertEqual( ini_val, bandit._log_option_source( default_val, arg_val, ini_val, option_name ), ) def test_log_option_source_no_values(self): # Test that None is returned when no command argument or ini value are # provided option_name = "aggregate" self.assertIsNone( bandit._log_option_source(None, 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", "w") 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]), "No tests would be run, please check the profile.", ) @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", "w") 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", "w") as fd: fd.write(bandit_config_content) with open("base.json", "w") 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", "w") as fd: fd.write(bandit_config_content) with open("base.json", "w") 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", "w") 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", "w") 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) @mock.patch( "sys.argv", ["bandit", "-c", "bandit.yaml", "test", "-o", "output", "--exit-zero"], ) def test_main_exit_with_results_and_with_exit_zero_flag(self): # Test that bandit exits with 0 on results and zero flag temp_directory = self.useFixture(fixtures.TempDir()).path os.chdir(temp_directory) with open("bandit.yaml", "w") 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 self.assertRaisesRegex(SystemExit, "0", bandit.main) ================================================ FILE: tests/unit/core/__init__.py ================================================ ================================================ FILE: tests/unit/core/test_blacklisting.py ================================================ # # Copyright 2016 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 import testtools from bandit.core import blacklisting 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({}, issue_dict["issue_cwe"]) 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({}, issue_dict["issue_cwe"]) self.assertEqual("HIGH", issue_dict["issue_confidence"]) self.assertEqual("test name", issue_dict["issue_text"]) ================================================ FILE: tests/unit/core/test_config.py ================================================ # Copyright 2015 IBM Corp. # # SPDX-License-Identifier: Apache-2.0 import os import tempfile import textwrap import uuid from unittest import mock import fixtures import testtools from bandit.core import config from bandit.core import utils class TempFile(fixtures.Fixture): def __init__(self, contents=None, suffix=".yaml"): super().__init__() self.contents = contents self.suffix = suffix def setUp(self): super().setUp() with tempfile.NamedTemporaryFile( suffix=self.suffix, 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 = f"{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().setUp() self.example_key = uuid.uuid4().hex self.example_subkey = uuid.uuid4().hex self.example_subvalue = uuid.uuid4().hex sample_yaml = textwrap.dedent( f""" {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 = f"{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 = f"{uuid.uuid4().hex}.{uuid.uuid4().hex}" self.assertIsNone(self.b_config.get_option(sample_option_name)) class TestGetSetting(testtools.TestCase): def setUp(self): super().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 = 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." """ ) suffix = ".yaml" def setUp(self): super().setUp() f = self.useFixture(TempFile(self.sample, suffix=self.suffix)) 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": {"B101", "B604"}, } self.assertEqual(data, test) def test_converted_exclude(self): profiles = self.config.get_option("profiles") test = profiles["test_4"] self.assertEqual({"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({"B001"}, test["include"]) def test_converted_blacklist_import_test(self): profiles = self.config.get_option("profiles") test = profiles["test_3"] self.assertEqual({"B001"}, test["include"]) def test_converted_exclude_blacklist(self): profiles = self.config.get_option("profiles") test = profiles["test_5"] self.assertEqual({"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) class TestTomlConfig(TestConfigCompat): sample = textwrap.dedent( """ [tool.bandit.profiles.test_1] include = [ "any_other_function_with_shell_equals_true", "assert_used", ] [tool.bandit.profiles.test_2] include = ["blacklist_calls"] [tool.bandit.profiles.test_3] include = ["blacklist_imports"] [tool.bandit.profiles.test_4] exclude = ["assert_used"] [tool.bandit.profiles.test_5] exclude = ["blacklist_calls", "blacklist_imports"] [tool.bandit.profiles.test_6] include = ["blacklist_calls"] exclude = ["blacklist_imports"] [[tool.bandit.blacklist_calls.bad_name_sets]] [tool.bandit.blacklist_calls.bad_name_sets.pickle] qualnames = ["pickle.loads"] message = "{func} library appears to be in use." [[tool.bandit.blacklist_imports.bad_import_sets]] [tool.bandit.blacklist_imports.bad_import_sets.telnet] imports = ["telnetlib"] level = "HIGH" message = "{module} is considered insecure." """ ) suffix = ".toml" ================================================ FILE: tests/unit/core/test_context.py ================================================ # # Copyright 2015 Red Hat, Inc. # # SPDX-License-Identifier: Apache-2.0 import ast from unittest import mock 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 = f"" 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.Constant(42) expected = value.value self.assertEqual(expected, new_context._get_literal_value(value)) value = ast.Constant("spam") expected = value.value self.assertEqual(expected, new_context._get_literal_value(value)) value = ast.List([ast.Constant("spam"), ast.Constant(42)], ast.Load()) expected = [ast.Constant("spam").value, ast.Constant(42).value] self.assertListEqual(expected, new_context._get_literal_value(value)) value = ast.Tuple([ast.Constant("spam"), ast.Constant(42)], ast.Load()) expected = (ast.Constant("spam").value, ast.Constant(42).value) self.assertTupleEqual(expected, new_context._get_literal_value(value)) value = ast.Set([ast.Constant("spam"), ast.Constant(42)]) expected = {ast.Constant("spam").value, ast.Constant(42).value} 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.Name("spam", ast.Load()) expected = value.id self.assertEqual(expected, new_context._get_literal_value(value)) value = ast.Constant(b"spam") expected = value.value 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.Constant(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")) def test_filename(self): ref_context = dict(filename="spam.py") new_context = context.Context(context_object=ref_context) self.assertEqual(new_context.filename, "spam.py") new_context = context.Context() self.assertIsNone(new_context.filename) ================================================ FILE: tests/unit/core/test_docs_util.py ================================================ # Copyright 2019 Victor Torre # # SPDX-License-Identifier: Apache-2.0 import testtools import bandit from bandit.core.docs_utils import get_url class DocsUtilTests(testtools.TestCase): """This set of tests exercises bandit.core.docs_util functions.""" BASE_URL = f"https://bandit.readthedocs.io/en/{bandit.__version__}/" def test_overwrite_bib_info(self): expected_url = self.BASE_URL + ( "blacklists/blacklist_calls.html" "#b304-b305-ciphers-and-modes" ) self.assertEqual(get_url("B304"), get_url("B305")) self.assertEqual(expected_url, get_url("B304")) def test_plugin_call_bib(self): expected_url = self.BASE_URL + "plugins/b101_assert_used.html" self.assertEqual(expected_url, get_url("B101")) def test_import_call_bib(self): expected_url = self.BASE_URL + ( "blacklists/blacklist_imports.html" "#b413-import-pycrypto" ) self.assertEqual(expected_url, get_url("B413")) ================================================ FILE: tests/unit/core/test_issue.py ================================================ # # Copyright 2015 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 from unittest 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() expect = ( "Issue: 'Test issue' from B999:bandit_plugin:" " CWE: %s," " Severity: MEDIUM " "Confidence: MEDIUM at code.py:1:8" ) self.assertEqual( expect % str(issue.Cwe(issue.Cwe.MULTIPLE_BINDS)), 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( { "id": 605, "link": "https://cwe.mitre.org/data/definitions/605.html", }, test_issue_dict["issue_cwe"], ) 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"]) self.assertEqual(8, test_issue_dict["col_offset"]) self.assertEqual(16, test_issue_dict["end_col_offset"]) def test_issue_filter_severity(self): levels = [bandit.LOW, bandit.MEDIUM, bandit.HIGH] issues = [_get_issue_instance(level, bandit.HIGH) for level 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, level) for level 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, cwe=issue.Cwe.MULTIPLE_BINDS, 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, cwe=issue.Cwe.MULTIPLE_BINDS, confidence=bandit.MEDIUM, ): new_issue = issue.Issue(severity, cwe, confidence, "Test issue") new_issue.fname = "code.py" new_issue.test = "bandit_plugin" new_issue.test_id = "B999" new_issue.lineno = 1 new_issue.col_offset = 8 new_issue.end_col_offset = 16 return new_issue ================================================ FILE: tests/unit/core/test_manager.py ================================================ # # Copyright 2015 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 import os from unittest import mock import fixtures 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, cwe=issue.Cwe.MULTIPLE_BINDS, conf=constants.MEDIUM, ): new_issue = issue.Issue(sev, cwe, conf, "Test issue") new_issue.fname = "code.py" new_issue.test = "bandit_plugin" new_issue.lineno = 1 return new_issue def setUp(self): super().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(False, self.manager.debug) self.assertEqual(False, self.manager.verbose) self.assertEqual("file", self.manager.agg_type) 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(False, m.debug) self.assertEqual(False, m.verbose) self.assertEqual("file", m.agg_type) 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, ) e = manager._is_file_included( path="x_a.py", included_globs=["*.py"], excluded_path_strings=["x_*.py"], enforce_glob=True, ) f = manager._is_file_included( path="x.py", included_globs=["*.py"], excluded_path_strings=["x_*.py"], enforce_glob=True, ) self.assertTrue(a) self.assertTrue(b) self.assertFalse(c) self.assertFalse(d) self.assertFalse(e) self.assertTrue(f) @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({"/a/c.ww"}, exc) self.assertEqual({"/a/a.py", "/a/b.py"}, inc) 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_cwe": { "id": 605, "link": "%s" }, "issue_confidence": "low", "issue_text": "test issue", "test_name": "some_test", "test_id": "x", "line_number": "n", "line_range": "n-m" } ] } """ % ( "https://cwe.mitre.org/data/definitions/605.html" ) issue_dictionary = { "code": "test code", "filename": "example_file.py", "issue_severity": "low", "issue_cwe": issue.Cwe(issue.Cwe.MULTIPLE_BINDS).as_dict(), "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=level, cwe=issue.Cwe.MULTIPLE_BINDS, confidence=level ) for level in levels ] r = [ self.manager.results_count(sev_filter=level, conf_filter=level) for level 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" with open(output_filename, "w") as tmp_file: self.manager.output_results( lines, sev_level, conf_level, tmp_file, output_format ) 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" with open(output_filename, "w") as tmp_file: 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 = ({"files"}, {"excluded"}) self.manager.discover_files(["thing"], True) self.assertEqual(["files"], self.manager.files_list) self.assertEqual(["excluded"], self.manager.excluded_files) @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(["thing"], self.manager.excluded_files) @mock.patch("os.path.isdir") def test_discover_files_exclude_dir(self, isdir): isdir.return_value = False # Test exclude dir using wildcard self.manager.discover_files(["./x/y.py"], True, "./x/*") self.assertEqual([], self.manager.files_list) self.assertEqual(["./x/y.py"], self.manager.excluded_files) # Test exclude dir without wildcard isdir.side_effect = [True, False] self.manager.discover_files(["./x/y.py"], True, "./x/") self.assertEqual([], self.manager.files_list) self.assertEqual(["./x/y.py"], self.manager.excluded_files) # Test exclude dir without wildcard or trailing slash isdir.side_effect = [True, False] self.manager.discover_files(["./x/y.py"], True, "./x") self.assertEqual([], self.manager.files_list) self.assertEqual(["./x/y.py"], self.manager.excluded_files) # Test exclude dir without prefix or suffix isdir.side_effect = [False, False] self.manager.discover_files(["./x/y/z.py"], True, "y") self.assertEqual([], self.manager.files_list) self.assertEqual(["./x/y/z.py"], self.manager.excluded_files) @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_exclude_glob(self, isdir): isdir.return_value = False self.manager.discover_files( ["a.py", "test_a.py", "test.py"], True, excluded_paths="test_*.py" ) self.assertEqual(["./a.py", "./test.py"], self.manager.files_list) self.assertEqual(["test_a.py"], self.manager.excluded_files) @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(["./thing"], self.manager.files_list) 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, "w") 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] ), ) ================================================ FILE: tests/unit/core/test_meta_ast.py ================================================ # Copyright (c) 2015 VMware, Inc. # # SPDX-License-Identifier: Apache-2.0 import testtools from bandit.core import meta_ast class BanditMetaAstTests(testtools.TestCase): def setUp(self): super().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 = f"Node: {self.node_id}\n\t{node}\nLength: 1\n" self.assertEqual(expected, str(self.b_meta_ast)) ================================================ FILE: tests/unit/core/test_test_set.py ================================================ # # Copyright (c) 2016 Hewlett-Packard Development Company, L.P. # # SPDX-License-Identifier: Apache-2.0 from unittest import mock import testtools from stevedore import extension from bandit.blacklists import utils from bandit.core import extension_loader from bandit.core import issue 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", issue.Cwe.CLEARTEXT_TRANSMISSION, ["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", issue.Cwe.DESERIALIZATION_OF_UNTRUSTED_DATA, ["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().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().tearDown() extension_loader.MANAGER = self.old_ext_man def test_has_defaults(self): ts = test_set.BanditTestSet(self.config) self.assertEqual(1, len(ts.get_tests("Str"))) def test_profile_include_id(self): profile = {"include": ["B000"]} ts = test_set.BanditTestSet(self.config, profile) self.assertEqual(1, len(ts.get_tests("Str"))) def test_profile_exclude_id(self): profile = {"exclude": ["B000"]} ts = test_set.BanditTestSet(self.config, profile) self.assertEqual(0, len(ts.get_tests("Str"))) def test_profile_include_none(self): profile = {"include": []} # same as no include ts = test_set.BanditTestSet(self.config, profile) self.assertEqual(1, len(ts.get_tests("Str"))) def test_profile_exclude_none(self): profile = {"exclude": []} # same as no exclude ts = test_set.BanditTestSet(self.config, profile) self.assertEqual(1, len(ts.get_tests("Str"))) def test_profile_has_builtin_blacklist(self): ts = test_set.BanditTestSet(self.config) self.assertEqual(1, len(ts.get_tests("Import"))) self.assertEqual(1, len(ts.get_tests("ImportFrom"))) self.assertEqual(1, len(ts.get_tests("Call"))) def test_profile_exclude_builtin_blacklist(self): profile = {"exclude": ["B001"]} ts = test_set.BanditTestSet(self.config, profile) self.assertEqual(0, len(ts.get_tests("Import"))) self.assertEqual(0, len(ts.get_tests("ImportFrom"))) self.assertEqual(0, len(ts.get_tests("Call"))) def test_profile_exclude_builtin_blacklist_specific(self): profile = {"exclude": ["B302", "B401"]} ts = test_set.BanditTestSet(self.config, profile) self.assertEqual(0, len(ts.get_tests("Import"))) self.assertEqual(0, len(ts.get_tests("ImportFrom"))) self.assertEqual(0, len(ts.get_tests("Call"))) def test_profile_filter_blacklist_none(self): ts = test_set.BanditTestSet(self.config) blacklist = ts.get_tests("Import")[0] self.assertEqual(2, len(blacklist._config["Import"])) self.assertEqual(2, len(blacklist._config["ImportFrom"])) self.assertEqual(2, len(blacklist._config["Call"])) 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(1, len(blacklist._config["Import"])) self.assertEqual(1, len(blacklist._config["ImportFrom"])) self.assertEqual(1, len(blacklist._config["Call"])) 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(1, len(blacklist._config["Import"])) self.assertEqual(1, len(blacklist._config["ImportFrom"])) self.assertEqual(1, len(blacklist._config["Call"])) 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(0, len(ts.get_tests("Import"))) self.assertEqual(0, len(ts.get_tests("ImportFrom"))) self.assertEqual(0, len(ts.get_tests("Call"))) def test_profile_blacklist_compat(self): data = [ utils.build_conf_dict( "marshal", "B302", issue.Cwe.DESERIALIZATION_OF_UNTRUSTED_DATA, ["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(1, len(blacklist._config["Call"])) ================================================ FILE: tests/unit/core/test_util.py ================================================ # # Copyright 2014 Hewlett-Packard Development Company, L.P. # Copyright 2015 Nebula, Inc. # # SPDX-License-Identifier: Apache-2.0 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``.""" open(path, "w").close() class UtilTests(testtools.TestCase): """This set of tests exercises bandit.core.util functions.""" def setUp(self): super().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): with open("./examples/jinja2_templating.py") as test_file: tree = ast.parse(test_file.read()) # Check linerange returns corrent number of lines line = 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: with open(t.name, "w") as f: f.write(test["content"]) 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") ================================================ FILE: tests/unit/formatters/__init__.py ================================================ ================================================ FILE: tests/unit/formatters/test_csv.py ================================================ # Copyright (c) 2015 VMware, Inc. # # SPDX-License-Identifier: Apache-2.0 import csv import tempfile 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().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], "col_offset": 8, "end_col_offset": 16, } self.check_name = "hardcoded_bind_all_interfaces" self.issue = issue.Issue( bandit.MEDIUM, 123, 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.col_offset = self.context["col_offset"] self.issue.end_col_offset = self.context["end_col_offset"] self.issue.test = self.check_name self.manager.results.append(self.issue) def test_report(self): with open(self.tmp_fname, "w") as tmp_file: 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 = 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(str(self.context["lineno"]), data["line_number"]) self.assertEqual( str(self.context["linerange"]), data["line_range"] ) self.assertEqual(self.check_name, data["test_name"]) self.assertIsNotNone(data["more_info"]) self.assertEqual(str(self.issue.col_offset), data["col_offset"]) self.assertEqual( str(self.issue.end_col_offset), data["end_col_offset"] ) ================================================ FILE: tests/unit/formatters/test_custom.py ================================================ # SPDX-License-Identifier: Apache-2.0 import csv import tempfile import testtools import bandit from bandit.core import config from bandit.core import issue from bandit.core import manager from bandit.formatters import custom class CustomFormatterTests(testtools.TestCase): def setUp(self): super().setUp() conf = config.BanditConfig() self.manager = manager.BanditManager(conf, "custom") (tmp_fd, self.tmp_fname) = tempfile.mkstemp() self.context = { "filename": self.tmp_fname, "lineno": 4, "linerange": [4], "col_offset": 30, "end_col_offset": 38, } self.check_name = "hardcoded_bind_all_interfaces" self.issue = issue.Issue( bandit.MEDIUM, bandit.MEDIUM, text="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.col_offset = self.context["col_offset"] self.issue.end_col_offset = self.context["end_col_offset"] self.issue.test = self.check_name self.manager.results.append(self.issue) def test_report(self): with open(self.tmp_fname, "w") as tmp_file: custom.report( self.manager, tmp_file, self.issue.severity, self.issue.confidence, template="{line},{col},{end_col},{severity},{msg}", ) with open(self.tmp_fname) as f: reader = csv.DictReader( f, ["line", "col", "end_col", "severity", "message"] ) data = next(reader) self.assertEqual(str(self.context["lineno"]), data["line"]) self.assertEqual(str(self.context["col_offset"]), data["col"]) self.assertEqual( str(self.context["end_col_offset"]), data["end_col"] ) self.assertEqual(self.issue.severity, data["severity"]) self.assertEqual(self.issue.text, data["message"]) ================================================ FILE: tests/unit/formatters/test_html.py ================================================ # Copyright (c) 2015 Rackspace, Inc. # Copyright (c) 2015 Hewlett Packard Enterprise # # SPDX-License-Identifier: Apache-2.0 import collections import tempfile from unittest import mock import bs4 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().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")] with open(self.tmp_fname, "w") as tmp_file: 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]), ] ) with open(self.tmp_fname, "w") as tmp_file: 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) self.assertIn("Line number: 1", 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]} with open(self.tmp_fname, "w") as tmp_file: 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, cwe=123, confidence=bandit.MEDIUM ): new_issue = issue.Issue(severity, cwe, confidence, "Test issue") new_issue.fname = "code.py" new_issue.test = "bandit_plugin" new_issue.lineno = 1 return new_issue ================================================ FILE: tests/unit/formatters/test_json.py ================================================ # Copyright (c) 2015 VMware, Inc. # # SPDX-License-Identifier: Apache-2.0 import collections import json import tempfile from unittest 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().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, issue.Cwe.MULTIPLE_BINDS, bandit.MEDIUM, "Possible binding to all interfaces.", ) self.candidates = [ issue.Issue( issue.Cwe.MULTIPLE_BINDS, bandit.LOW, bandit.LOW, "Candidate A", lineno=1, ), issue.Issue( bandit.HIGH, issue.Cwe.MULTIPLE_BINDS, 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][f"{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)] ) with open(self.tmp_fname, "w") as tmp_file: 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]) self.assertIn("more_info", data["results"][0]) self.assertIsNotNone(data["results"][0]["more_info"]) ================================================ FILE: tests/unit/formatters/test_sarif.py ================================================ # SPDX-License-Identifier: Apache-2.0 import collections import json import tempfile from unittest 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 sarif class SarifFormatterTests(testtools.TestCase): def setUp(self): super().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], "code": ( "import socket\n\n" "s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n" "s.bind(('0.0.0.0', 31137))" ), } self.check_name = "hardcoded_bind_all_interfaces" self.issue = issue.Issue( severity=bandit.MEDIUM, cwe=issue.Cwe.MULTIPLE_BINDS, confidence=bandit.MEDIUM, text="Possible binding to all interfaces.", test_id="B104", ) self.candidates = [ issue.Issue( issue.Cwe.MULTIPLE_BINDS, bandit.LOW, bandit.LOW, "Candidate A", lineno=1, ), issue.Issue( bandit.HIGH, issue.Cwe.MULTIPLE_BINDS, 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.code = self.context["code"] 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][f"{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)] ) with open(self.tmp_fname, "w") as tmp_file: sarif.report( self.manager, tmp_file, self.issue.severity, self.issue.confidence, ) with open(self.tmp_fname) as f: data = json.loads(f.read()) run = data["runs"][0] self.assertEqual(sarif.SCHEMA_URI, data["$schema"]) self.assertEqual(sarif.SCHEMA_VER, data["version"]) driver = run["tool"]["driver"] self.assertEqual("Bandit", driver["name"]) self.assertEqual(bandit.__author__, driver["organization"]) self.assertEqual(bandit.__version__, driver["semanticVersion"]) self.assertEqual("B104", driver["rules"][0]["id"]) self.assertEqual(self.check_name, driver["rules"][0]["name"]) self.assertIn("security", driver["rules"][0]["properties"]["tags"]) self.assertIn( "external/cwe/cwe-605", driver["rules"][0]["properties"]["tags"], ) self.assertEqual( "medium", driver["rules"][0]["properties"]["precision"] ) invocation = run["invocations"][0] self.assertTrue(invocation["executionSuccessful"]) self.assertIsNotNone(invocation["endTimeUtc"]) result = run["results"][0] # If the level is "warning" like in this case, SARIF will remove # from output, as "warning" is the default value. self.assertIsNone(result.get("level")) self.assertEqual(self.issue.text, result["message"]["text"]) physicalLocation = result["locations"][0]["physicalLocation"] self.assertEqual( self.context["linerange"][0], physicalLocation["region"]["startLine"], ) self.assertEqual( self.context["linerange"][0], physicalLocation["region"]["endLine"], ) self.assertIn( self.tmp_fname, physicalLocation["artifactLocation"]["uri"], ) ================================================ FILE: tests/unit/formatters/test_screen.py ================================================ # Copyright (c) 2015 VMware, Inc. # Copyright (c) 2015 Hewlett Packard Enterprise # # SPDX-License-Identifier: Apache-2.0 import collections import tempfile from unittest import mock import testtools import bandit from bandit.core import config from bandit.core import docs_utils from bandit.core import issue from bandit.core import manager from bandit.formatters import screen class ScreenFormatterTests(testtools.TestCase): def setUp(self): super().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(), ), f"{_indent_val} CWE: {_issue.cwe}", f"{_indent_val} More Info: " f"{docs_utils.get_url(_issue.test_id)}", "{} Location: {}:{}:{}{}".format( _indent_val, _issue.fname, _issue.lineno, _issue.col_offset, screen.COLOR["DEFAULT"], ), ] if _code: return_val.append(f"{_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.col_offset = "" 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: with open(self.tmp_fname, "w") as tmp_file: 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"][f"{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" with open(self.tmp_fname, "w") as tmp_file: 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: with open(self.tmp_fname, "w") as tmp_file: 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" with open(self.tmp_fname, "w") as tmp_file: 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, cwe=123, confidence=bandit.MEDIUM ): new_issue = issue.Issue(severity, cwe, confidence, "Test issue") new_issue.fname = "code.py" new_issue.test = "bandit_plugin" new_issue.lineno = 1 return new_issue ================================================ FILE: tests/unit/formatters/test_text.py ================================================ # Copyright (c) 2015 VMware, Inc. # Copyright (c) 2015 Hewlett Packard Enterprise # # SPDX-License-Identifier: Apache-2.0 import collections import tempfile from unittest import mock import testtools import bandit from bandit.core import config from bandit.core import docs_utils 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().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(), ), f"{_indent_val} CWE: {_issue.cwe}", f"{_indent_val} More Info: " f"{docs_utils.get_url(_issue.test_id)}", "{} Location: {}:{}:{}".format( _indent_val, _issue.fname, _issue.lineno, _issue.col_offset ), ] if _code: return_val.append(f"{_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.col_offset = "" 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() with open(self.tmp_fname, "w") as tmp_file: 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, "skipped_tests": 0, } for category in ["SEVERITY", "CONFIDENCE"]: for level in ["UNDEFINED", "LOW", "MEDIUM", "HIGH"]: self.manager.metrics.data["_totals"][f"{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" with open(self.tmp_fname, "w") as tmp_file: 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 with open(self.tmp_fname, "w") as tmp_file: 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", f"CWE: {str(issue.Cwe(issue.Cwe.MULTIPLE_BINDS))}", "Files excluded (1):", "def.py", "Undefined: 1", "Low: 1", "Medium: 1", "High: 1", "Total lines skipped ", "(#nosec): 50", "Total potential issues skipped due to specifically being ", "disabled (e.g., #nosec BXXX): 0", "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" with open(self.tmp_fname, "w") as tmp_file: 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, cwe=issue.Cwe.MULTIPLE_BINDS, confidence=bandit.MEDIUM, ): new_issue = issue.Issue(severity, cwe, confidence, "Test issue") new_issue.fname = "code.py" new_issue.test = "bandit_plugin" new_issue.lineno = 1 return new_issue ================================================ FILE: tests/unit/formatters/test_xml.py ================================================ # Copyright (c) 2015 VMware, Inc. # # SPDX-License-Identifier: Apache-2.0 import collections import tempfile from xml.etree import ElementTree as ET 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().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, issue.Cwe.MULTIPLE_BINDS, 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 dc.items(): dd[k].append(v) d = {t.tag: {k: v[0] if len(v) == 1 else v for k, v in dd.items()}} if t.attrib: d[t.tag].update(("@" + k, v) for k, v in t.attrib.items()) 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): with open(self.tmp_fname, "wb") as tmp_file: 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"] ) self.assertIsNotNone( data["testsuite"]["testcase"]["error"]["@more_info"] ) ================================================ FILE: tests/unit/formatters/test_yaml.py ================================================ # Copyright (c) 2017 VMware, Inc. # # SPDX-License-Identifier: Apache-2.0 import collections import tempfile from unittest import mock import testtools import yaml 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 YamlFormatterTests(testtools.TestCase): def setUp(self): super().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, 123, bandit.MEDIUM, "Possible binding to all interfaces.", ) self.candidates = [ issue.Issue(bandit.LOW, 123, bandit.LOW, "Candidate A", lineno=1), issue.Issue(bandit.HIGH, 123, 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][f"{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)] ) with open(self.tmp_fname, "w") as tmp_file: b_json.report( self.manager, tmp_file, self.issue.severity, self.issue.confidence, ) with open(self.tmp_fname) as f: data = yaml.load(f.read(), Loader=yaml.SafeLoader) 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]) self.assertIn("more_info", data["results"][0]) self.assertIsNotNone(data["results"][0]["more_info"]) ================================================ FILE: tox.ini ================================================ [tox] minversion = 3.2.0 envlist = py310,pep8 [testenv] usedevelop = True install_command = pip install {opts} {packages} setenv = VIRTUAL_ENV={envdir} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt extras = yaml toml baseline sarif commands = find bandit -type f -name "*.pyc" -delete stestr run {posargs} allowlist_externals = find passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY [testenv:linters] deps = {[testenv:pep8]deps} usedevelop = False commands = flake8 {posargs} bandit flake8 {posargs} tests bandit-baseline -r bandit -ll -ii [testenv:pep8] ignore_errors = true deps = {[testenv]deps} . usedevelop = False commands = flake8 {posargs} bandit flake8 {posargs} tests -{[testenv:pylint]commands} bandit-baseline -r bandit -ll -ii [testenv:venv] commands = {posargs} [testenv:codesec] deps = {[testenv]deps} . usedevelop = False commands = bandit-baseline -r bandit -ll -ii [testenv:cover] setenv = {[testenv]setenv} PYTHON=coverage run --source bandit --parallel-mode commands = coverage erase stestr run '{posargs}' coverage report [testenv:docs] deps = -r{toxinidir}/doc/requirements.txt commands= sphinx-build doc/source doc/build [testenv:manpage] deps = -r{toxinidir}/doc/requirements.txt commands= sphinx-build -b man doc/source doc/build/man [flake8] # [H106] Don't put vim configuration in source files. # [H203] Use assertIs(Not)None to check for None. show-source = True exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build enable-extensions = H106,H203 [testenv:pylint] commands = -pylint --rcfile=pylintrc bandit [testenv:format] skip_install = true deps = pre-commit commands = pre-commit run --all-files --show-diff-on-failure