diff --git a/doc/~$模板:软件需求规格说明书.docx b/doc/~$模板:软件需求规格说明书.docx new file mode 100644 index 0000000..5e61389 Binary files /dev/null and b/doc/~$模板:软件需求规格说明书.docx differ diff --git a/doc/军工软件python编码指南.docx b/doc/军工软件python编码指南.docx new file mode 100644 index 0000000..9910f40 Binary files /dev/null and b/doc/军工软件python编码指南.docx differ diff --git a/doc/文档模板:软件需求规格说明书.docx b/doc/文档模板:软件需求规格说明书.docx new file mode 100644 index 0000000..b02859b Binary files /dev/null and b/doc/文档模板:软件需求规格说明书.docx differ diff --git a/doc/软件需求构思及描述.docx b/doc/软件需求构思及描述.docx new file mode 100644 index 0000000..b5dce09 Binary files /dev/null and b/doc/软件需求构思及描述.docx differ diff --git a/model/模型模板:软件需求模型.vsdx b/model/模型模板:软件需求模型.vsdx new file mode 100644 index 0000000..c5acba6 Binary files /dev/null and b/model/模型模板:软件需求模型.vsdx differ diff --git a/src/.idea/.gitignore b/src/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/src/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/src/.idea/inspectionProfiles/profiles_settings.xml b/src/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/src/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/src/.idea/misc.xml b/src/.idea/misc.xml new file mode 100644 index 0000000..55da0b2 --- /dev/null +++ b/src/.idea/misc.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/.idea/modules.xml b/src/.idea/modules.xml new file mode 100644 index 0000000..f669a0e --- /dev/null +++ b/src/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/.idea/src.iml b/src/.idea/src.iml new file mode 100644 index 0000000..0a88826 --- /dev/null +++ b/src/.idea/src.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/.idea/vcs.xml b/src/.idea/vcs.xml new file mode 100644 index 0000000..ec06c7c --- /dev/null +++ b/src/.idea/vcs.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/bandit-main/bandit-main/.github/CODEOWNERS b/src/bandit-main/bandit-main/.github/CODEOWNERS new file mode 100644 index 0000000..6b61387 --- /dev/null +++ b/src/bandit-main/bandit-main/.github/CODEOWNERS @@ -0,0 +1 @@ +* @ericwb @lukehinds @sigmavirus24 diff --git a/src/bandit-main/bandit-main/.github/FUNDING.yml b/src/bandit-main/bandit-main/.github/FUNDING.yml new file mode 100644 index 0000000..fbf5bae --- /dev/null +++ b/src/bandit-main/bandit-main/.github/FUNDING.yml @@ -0,0 +1,3 @@ +custom: ["https://psfmember.org/civicrm/contribute/transact/?reset=1&id=42"] +github: [ericwb] +tidelift: pypi/bandit diff --git a/src/bandit-main/bandit-main/.github/ISSUE_TEMPLATE/Feature_request.md b/src/bandit-main/bandit-main/.github/ISSUE_TEMPLATE/Feature_request.md new file mode 100644 index 0000000..0d0d3d1 --- /dev/null +++ b/src/bandit-main/bandit-main/.github/ISSUE_TEMPLATE/Feature_request.md @@ -0,0 +1,22 @@ +--- +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 👍. diff --git a/src/bandit-main/bandit-main/.github/ISSUE_TEMPLATE/bug-report.yml b/src/bandit-main/bandit-main/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..1e1baed --- /dev/null +++ b/src/bandit-main/bandit-main/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,83 @@ +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.8.3 (Default) + - 1.8.2 + - 1.8.1 + - 1.8.0 + - 1.7.10 + - 1.7.9 + - 1.7.8 + - 1.7.7 + - 1.7.6 + - 1.7.5 + - 1.7.4 + - 1.7.3 + - 1.7.2 + - 1.7.1 + - 1.7.0 + validations: + required: true + + - type: dropdown + id: python-version + attributes: + label: Python version + description: Run "bandit --version" if unsure of version number + options: + - "3.13 (Default)" + - "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. diff --git a/src/bandit-main/bandit-main/.github/ISSUE_TEMPLATE/config.yml b/src/bandit-main/bandit-main/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..7ac651d --- /dev/null +++ b/src/bandit-main/bandit-main/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: ❓ Ask a question + url: https://github.com/PyCQA/bandit/discussions + about: Please post questions in discussions. diff --git a/src/bandit-main/bandit-main/.github/dependabot.yml b/src/bandit-main/bandit-main/.github/dependabot.yml new file mode 100644 index 0000000..9960204 --- /dev/null +++ b/src/bandit-main/bandit-main/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/src/bandit-main/bandit-main/.github/workflows/build-publish-image.yml b/src/bandit-main/bandit-main/.github/workflows/build-publish-image.yml new file mode 100644 index 0000000..a3980fd --- /dev/null +++ b/src/bandit-main/bandit-main/.github/workflows/build-publish-image.yml @@ -0,0 +1,67 @@ +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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4 + with: + ref: ${{ github.event_name == 'release' && github.ref || env.RELEASE_TAG }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Cosign + uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 + 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@263435318d21b8e681c14492fe198d362a7d2c83 # v6 + 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} diff --git a/src/bandit-main/bandit-main/.github/workflows/dependency-review.yml b/src/bandit-main/bandit-main/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..edc1cad --- /dev/null +++ b/src/bandit-main/bandit-main/.github/workflows/dependency-review.yml @@ -0,0 +1,14 @@ +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + - name: 'Dependency Review' + uses: actions/dependency-review-action@v4 diff --git a/src/bandit-main/bandit-main/.github/workflows/publish-to-pypi.yml b/src/bandit-main/bandit-main/.github/workflows/publish-to-pypi.yml new file mode 100644 index 0000000..94d74c8 --- /dev/null +++ b/src/bandit-main/bandit-main/.github/workflows/publish-to-pypi.yml @@ -0,0 +1,36 @@ +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@v5 + with: + fetch-depth: 0 + - name: Set up Python 3.9 + uses: actions/setup-python@v6 + with: + python-version: 3.9 + + - 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 diff --git a/src/bandit-main/bandit-main/.github/workflows/publish-to-test-pypi.yml b/src/bandit-main/bandit-main/.github/workflows/publish-to-test-pypi.yml new file mode 100644 index 0000000..6d47166 --- /dev/null +++ b/src/bandit-main/bandit-main/.github/workflows/publish-to-test-pypi.yml @@ -0,0 +1,37 @@ +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@v5 + with: + fetch-depth: 0 + - name: Set up Python 3.9 + uses: actions/setup-python@v6 + with: + python-version: 3.9 + + - 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/ diff --git a/src/bandit-main/bandit-main/.github/workflows/pythonpackage.yml b/src/bandit-main/bandit-main/.github/workflows/pythonpackage.yml new file mode 100644 index 0000000..7dea27d --- /dev/null +++ b/src/bandit-main/bandit-main/.github/workflows/pythonpackage.yml @@ -0,0 +1,71 @@ +name: Build and Test Bandit + +on: [push, pull_request] + +jobs: + format: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.9] + steps: + - name: Checkout repository + uses: actions/checkout@v5 + - 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.9] + steps: + - name: Checkout repository + uses: actions/checkout@v5 + 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.9", "39"], + ["3.10", "310"], + ["3.11", "311"], + ["3.12", "312"], + ["3.13", "313"], + ] + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + name: ${{ matrix.os }} (${{ matrix.python-version[0] }}) + steps: + - name: Checkout repository + uses: actions/checkout@v5 + - 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] }} diff --git a/src/bandit-main/bandit-main/.gitignore b/src/bandit-main/bandit-main/.gitignore new file mode 100644 index 0000000..eca3b67 --- /dev/null +++ b/src/bandit-main/bandit-main/.gitignore @@ -0,0 +1,20 @@ +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 diff --git a/src/bandit-main/bandit-main/.pre-commit-config.yaml b/src/bandit-main/bandit-main/.pre-commit-config.yaml new file mode 100644 index 0000000..8ab2408 --- /dev/null +++ b/src/bandit-main/bandit-main/.pre-commit-config.yaml @@ -0,0 +1,33 @@ +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.15.0 + hooks: + - id: reorder-python-imports + args: [--application-directories, '.:src', --py38-plus] +- repo: https://github.com/psf/black-pre-commit-mirror + rev: 25.1.0 + hooks: + - id: black + args: [--line-length=79, --target-version=py38] +- repo: https://github.com/asottile/pyupgrade + rev: v3.20.0 + 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/) diff --git a/src/bandit-main/bandit-main/.pre-commit-hooks.yaml b/src/bandit-main/bandit-main/.pre-commit-hooks.yaml new file mode 100644 index 0000000..44db124 --- /dev/null +++ b/src/bandit-main/bandit-main/.pre-commit-hooks.yaml @@ -0,0 +1,8 @@ +- 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 diff --git a/src/bandit-main/bandit-main/.readthedocs.yaml b/src/bandit-main/bandit-main/.readthedocs.yaml new file mode 100644 index 0000000..46e5911 --- /dev/null +++ b/src/bandit-main/bandit-main/.readthedocs.yaml @@ -0,0 +1,18 @@ +version: 2 + +build: + os: ubuntu-lts-latest + tools: + python: "3.9" + +sphinx: + configuration: doc/source/conf.py + +python: + install: + - requirements: requirements.txt + - requirements: doc/requirements.txt + - method: pip + path: . + extra_requirements: + - sarif diff --git a/src/bandit-main/bandit-main/.stestr.conf b/src/bandit-main/bandit-main/.stestr.conf new file mode 100644 index 0000000..ebc11d4 --- /dev/null +++ b/src/bandit-main/bandit-main/.stestr.conf @@ -0,0 +1,4 @@ +[DEFAULT] +test_path=./tests +top_dir=./ +parallel_class=True diff --git a/src/bandit-main/bandit-main/CODE_OF_CONDUCT.md b/src/bandit-main/bandit-main/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..67e8a09 --- /dev/null +++ b/src/bandit-main/bandit-main/CODE_OF_CONDUCT.md @@ -0,0 +1,129 @@ +# 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. diff --git a/src/bandit-main/bandit-main/CONTRIBUTING.md b/src/bandit-main/bandit-main/CONTRIBUTING.md new file mode 100644 index 0000000..f2280c6 --- /dev/null +++ b/src/bandit-main/bandit-main/CONTRIBUTING.md @@ -0,0 +1,278 @@ +# 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 py39 +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` diff --git a/src/bandit-main/bandit-main/LICENSE b/src/bandit-main/bandit-main/LICENSE new file mode 100644 index 0000000..67db858 --- /dev/null +++ b/src/bandit-main/bandit-main/LICENSE @@ -0,0 +1,175 @@ + + 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. diff --git a/src/bandit-main/bandit-main/README.rst b/src/bandit-main/bandit-main/README.rst new file mode 100644 index 0000000..79615ff --- /dev/null +++ b/src/bandit-main/bandit-main/README.rst @@ -0,0 +1,145 @@ +.. 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. diff --git a/src/bandit-main/bandit-main/SECURITY.md b/src/bandit-main/bandit-main/SECURITY.md new file mode 100644 index 0000000..a627e5f --- /dev/null +++ b/src/bandit-main/bandit-main/SECURITY.md @@ -0,0 +1,9 @@ +# 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. diff --git a/src/bandit-main/bandit-main/bandit-terminal.png b/src/bandit-main/bandit-main/bandit-terminal.png new file mode 100644 index 0000000..f4ac249 Binary files /dev/null and b/src/bandit-main/bandit-main/bandit-terminal.png differ diff --git a/src/bandit-main/bandit-main/bandit/__init__.py b/src/bandit-main/bandit-main/bandit/__init__.py new file mode 100644 index 0000000..7c7bf00 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/__init__.py @@ -0,0 +1,20 @@ +# +# 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") diff --git a/src/bandit-main/bandit-main/bandit/__main__.py b/src/bandit-main/bandit-main/bandit/__main__.py new file mode 100644 index 0000000..f43c06a --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/__main__.py @@ -0,0 +1,17 @@ +#!/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() diff --git a/src/bandit-main/bandit-main/bandit/blacklists/__init__.py b/src/bandit-main/bandit-main/bandit/blacklists/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/bandit-main/bandit-main/bandit/blacklists/calls.py b/src/bandit-main/bandit-main/bandit/blacklists/calls.py new file mode 100644 index 0000000..024e873 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/blacklists/calls.py @@ -0,0 +1,670 @@ +# +# 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} diff --git a/src/bandit-main/bandit-main/bandit/blacklists/imports.py b/src/bandit-main/bandit-main/bandit/blacklists/imports.py new file mode 100644 index 0000000..b15155b --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/blacklists/imports.py @@ -0,0 +1,425 @@ +# +# 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} diff --git a/src/bandit-main/bandit-main/bandit/blacklists/utils.py b/src/bandit-main/bandit-main/bandit/blacklists/utils.py new file mode 100644 index 0000000..fa4a5c9 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/blacklists/utils.py @@ -0,0 +1,17 @@ +# +# 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, + } diff --git a/src/bandit-main/bandit-main/bandit/cli/__init__.py b/src/bandit-main/bandit-main/bandit/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/bandit-main/bandit-main/bandit/cli/baseline.py b/src/bandit-main/bandit-main/bandit/cli/baseline.py new file mode 100644 index 0000000..252ceb1 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/cli/baseline.py @@ -0,0 +1,249 @@ +# +# 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.", + ) + + 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() diff --git a/src/bandit-main/bandit-main/bandit/cli/config_generator.py b/src/bandit-main/bandit-main/bandit/cli/config_generator.py new file mode 100644 index 0000000..5b941cd --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/cli/config_generator.py @@ -0,0 +1,204 @@ +# 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, + ) + + 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()) diff --git a/src/bandit-main/bandit-main/bandit/cli/main.py b/src/bandit-main/bandit-main/bandit/cli/main.py new file mode 100644 index 0000000..0cb0f8d --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/cli/main.py @@ -0,0 +1,697 @@ +# +# 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 commad 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 commad 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, + ) + 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() diff --git a/src/bandit-main/bandit-main/bandit/core/__init__.py b/src/bandit-main/bandit-main/bandit/core/__init__.py new file mode 100644 index 0000000..2efdc4d --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/core/__init__.py @@ -0,0 +1,15 @@ +# +# 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 diff --git a/src/bandit-main/bandit-main/bandit/core/blacklisting.py b/src/bandit-main/bandit-main/bandit/core/blacklisting.py new file mode 100644 index 0000000..2bbb093 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/core/blacklisting.py @@ -0,0 +1,70 @@ +# +# 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.Str): + name = context.node.args[0].s + else: + # TODO(??): import through a variable, need symbol tab + name = "UNKNOWN" + else: + name = "" # handle '__import__()' + else: + name = context.call_function_name_qual + # 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) diff --git a/src/bandit-main/bandit-main/bandit/core/config.py b/src/bandit-main/bandit-main/bandit/core/config.py new file mode 100644 index 0000000..dbc68fb --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/core/config.py @@ -0,0 +1,271 @@ +# +# 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, + ) diff --git a/src/bandit-main/bandit-main/bandit/core/constants.py b/src/bandit-main/bandit-main/bandit/core/constants.py new file mode 100644 index 0000000..dd8ddeb --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/core/constants.py @@ -0,0 +1,40 @@ +# +# 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", +) diff --git a/src/bandit-main/bandit-main/bandit/core/context.py b/src/bandit-main/bandit-main/bandit/core/context.py new file mode 100644 index 0000000..57f293c --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/core/context.py @@ -0,0 +1,324 @@ +# +# 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.Num): + literal_value = literal.n + + elif isinstance(literal, ast.Str): + literal_value = literal.s + + elif isinstance(literal, ast.List): + return_list = list() + for li in literal.elts: + return_list.append(self._get_literal_value(li)) + literal_value = return_list + + elif isinstance(literal, ast.Tuple): + return_tuple = tuple() + for ti in literal.elts: + return_tuple += (self._get_literal_value(ti),) + literal_value = return_tuple + + elif isinstance(literal, ast.Set): + return_set = set() + for si in literal.elts: + return_set.add(self._get_literal_value(si)) + literal_value = return_set + + elif isinstance(literal, ast.Dict): + literal_value = dict(zip(literal.keys, literal.values)) + + elif isinstance(literal, ast.Ellipsis): + # what do we want to do with this? + literal_value = None + + elif isinstance(literal, ast.Name): + literal_value = literal.id + + elif isinstance(literal, ast.NameConstant): + literal_value = str(literal.value) + + elif isinstance(literal, ast.Bytes): + literal_value = literal.s + + else: + literal_value = None + + return literal_value + + def get_call_arg_value(self, argument_name): + """Gets the value of a named argument in a function call. + + :return: named argument value + """ + kwd_values = self.call_keywords + if kwd_values is not None and argument_name in kwd_values: + return kwd_values[argument_name] + + def check_call_arg_value(self, argument_name, argument_values=None): + """Checks for a value of a named argument in a function call. + + Returns none if the specified argument is not found. + :param argument_name: A string - name of the argument to look for + :param argument_values: the value, or list of values to test against + :return: Boolean True if argument found and matched, False if + found and not matched, None if argument not found at all + """ + arg_value = self.get_call_arg_value(argument_name) + if arg_value is not None: + if not isinstance(argument_values, list): + # if passed a single value, or a tuple, convert to a list + argument_values = list((argument_values,)) + for val in argument_values: + if arg_value == val: + return True + return False + else: + # argument name not found, return None to allow testing for this + # eventuality + return None + + def get_lineno_for_call_arg(self, argument_name): + """Get the line number for a specific named argument + + In case the call is split over multiple lines, get the correct one for + the argument. + :param argument_name: A string - name of the argument to look for + :return: Integer - the line number of the found argument, or -1 + """ + 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") diff --git a/src/bandit-main/bandit-main/bandit/core/docs_utils.py b/src/bandit-main/bandit-main/bandit/core/docs_utils.py new file mode 100644 index 0000000..5a5575b --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/core/docs_utils.py @@ -0,0 +1,54 @@ +# +# 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 diff --git a/src/bandit-main/bandit-main/bandit/core/extension_loader.py b/src/bandit-main/bandit-main/bandit/core/extension_loader.py new file mode 100644 index 0000000..ec28a0a --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/core/extension_loader.py @@ -0,0 +1,114 @@ +# +# 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() diff --git a/src/bandit-main/bandit-main/bandit/core/issue.py b/src/bandit-main/bandit-main/bandit/core/issue.py new file mode 100644 index 0000000..b2d9015 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/core/issue.py @@ -0,0 +1,245 @@ +# +# 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 diff --git a/src/bandit-main/bandit-main/bandit/core/manager.py b/src/bandit-main/bandit-main/bandit/core/manager.py new file mode 100644 index 0000000..ffc13ca --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/core/manager.py @@ -0,0 +1,499 @@ +# +# 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 mantain 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 diff --git a/src/bandit-main/bandit-main/bandit/core/meta_ast.py b/src/bandit-main/bandit-main/bandit/core/meta_ast.py new file mode 100644 index 0000000..7bcd7f8 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/core/meta_ast.py @@ -0,0 +1,44 @@ +# +# 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 diff --git a/src/bandit-main/bandit-main/bandit/core/metrics.py b/src/bandit-main/bandit-main/bandit/core/metrics.py new file mode 100644 index 0000000..c212290 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/core/metrics.py @@ -0,0 +1,106 @@ +# +# 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 diff --git a/src/bandit-main/bandit-main/bandit/core/node_visitor.py b/src/bandit-main/bandit-main/bandit/core/node_visitor.py new file mode 100644 index 0000000..938e873 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/core/node_visitor.py @@ -0,0 +1,297 @@ +# +# 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.s + 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.s + 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 diff --git a/src/bandit-main/bandit-main/bandit/core/test_properties.py b/src/bandit-main/bandit-main/bandit/core/test_properties.py new file mode 100644 index 0000000..f6d4da1 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/core/test_properties.py @@ -0,0 +1,83 @@ +# +# 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]) diff --git a/src/bandit-main/bandit-main/bandit/core/test_set.py b/src/bandit-main/bandit-main/bandit/core/test_set.py new file mode 100644 index 0000000..1e7dd0d --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/core/test_set.py @@ -0,0 +1,114 @@ +# +# 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 [] diff --git a/src/bandit-main/bandit-main/bandit/core/tester.py b/src/bandit-main/bandit-main/bandit/core/tester.py new file mode 100644 index 0000000..e92c29f --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/core/tester.py @@ -0,0 +1,166 @@ +# +# 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 line {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) diff --git a/src/bandit-main/bandit-main/bandit/core/utils.py b/src/bandit-main/bandit-main/bandit/core/utils.py new file mode 100644 index 0000000..7fb7753 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/core/utils.py @@ -0,0 +1,378 @@ +# +# 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.Str nodes wrapped in + ast.BinOp nodes. Something like "a" + "b" + "c" or "a %s" % val etc. + The provided node can be any participant in the BinOp chain. + + :param node: (ast.Str or ast.BinOp) The node to process + :param stop: (ast.Str or ast.BinOp) Optional base node to stop at + :returns: (Tuple) the root node of the expression, the string value + """ + + def _get(node, bits, stop=None): + if node != stop: + bits.append( + _get(node.left, bits, stop) + if isinstance(node.left, ast.BinOp) + else node.left + ) + bits.append( + _get(node.right, bits, stop) + if isinstance(node.right, ast.BinOp) + else node.right + ) + + bits = [node] + while isinstance(node._bandit_parent, ast.BinOp): + node = node._bandit_parent + if isinstance(node, ast.BinOp): + _get(node, bits, stop) + return (node, " ".join([x.s for x in bits if isinstance(x, ast.Str)])) + + +def get_called_name(node): + """Get a function name from an ast.Call node. + + An ast.Call node representing a method call with present differently to one + wrapping a function call: thing.call() vs call(). This helper will grab the + unqualified call name correctly in either case. + + :param node: (ast.Call) the call node + :returns: (String) the function name + """ + func = node.func + try: + return func.attr if isinstance(func, ast.Attribute) else func.id + except AttributeError: + return "" + + +def get_path_for_function(f): + """Get the path of the file where the function is defined. + + :returns: the path, or None if one could not be found or f is not a real + function + """ + + if hasattr(f, "__module__"): + module_name = f.__module__ + elif hasattr(f, "im_func"): + module_name = f.im_func.__module__ + else: + LOG.warning("Cannot resolve file where %s is defined", f) + return None + + module = sys.modules[module_name] + if hasattr(module, "__file__"): + return module.__file__ + else: + LOG.warning("Cannot resolve file path for module %s", module_name) + return None + + +def parse_ini_file(f_loc): + config = configparser.ConfigParser() + try: + config.read(f_loc) + return {k: v for k, v in config.items("bandit")} + + except (configparser.Error, KeyError, TypeError): + LOG.warning( + "Unable to parse config file %s or missing [bandit] " "section", + f_loc, + ) + + return None + + +def check_ast_node(name): + "Check if the given name is that of a valid AST node." + try: + node = getattr(ast, name) + if issubclass(node, ast.AST): + return name + except AttributeError: # nosec(tkelsey): catching expected exception + pass + + raise TypeError(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 diff --git a/src/bandit-main/bandit-main/bandit/formatters/__init__.py b/src/bandit-main/bandit-main/bandit/formatters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/bandit-main/bandit-main/bandit/formatters/csv.py b/src/bandit-main/bandit-main/bandit/formatters/csv.py new file mode 100644 index 0000000..6cde187 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/formatters/csv.py @@ -0,0 +1,82 @@ +# +# 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) diff --git a/src/bandit-main/bandit-main/bandit/formatters/custom.py b/src/bandit-main/bandit-main/bandit/formatters/custom.py new file mode 100644 index 0000000..e9381ea --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/formatters/custom.py @@ -0,0 +1,161 @@ +# +# 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) diff --git a/src/bandit-main/bandit-main/bandit/formatters/html.py b/src/bandit-main/bandit-main/bandit/formatters/html.py new file mode 100644 index 0000000..fb09f83 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/formatters/html.py @@ -0,0 +1,394 @@ +# 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) diff --git a/src/bandit-main/bandit-main/bandit/formatters/json.py b/src/bandit-main/bandit-main/bandit/formatters/json.py new file mode 100644 index 0000000..3a954a4 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/formatters/json.py @@ -0,0 +1,155 @@ +# +# 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) diff --git a/src/bandit-main/bandit-main/bandit/formatters/sarif.py b/src/bandit-main/bandit-main/bandit/formatters/sarif.py new file mode 100644 index 0000000..5b06ce7 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/formatters/sarif.py @@ -0,0 +1,374 @@ +# 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) diff --git a/src/bandit-main/bandit-main/bandit/formatters/screen.py b/src/bandit-main/bandit-main/bandit/formatters/screen.py new file mode 100644 index 0000000..7421c3e --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/formatters/screen.py @@ -0,0 +1,244 @@ +# 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() diff --git a/src/bandit-main/bandit-main/bandit/formatters/text.py b/src/bandit-main/bandit-main/bandit/formatters/text.py new file mode 100644 index 0000000..9324918 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/formatters/text.py @@ -0,0 +1,200 @@ +# 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) diff --git a/src/bandit-main/bandit-main/bandit/formatters/utils.py b/src/bandit-main/bandit-main/bandit/formatters/utils.py new file mode 100644 index 0000000..ebe9f92 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/formatters/utils.py @@ -0,0 +1,14 @@ +# 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) diff --git a/src/bandit-main/bandit-main/bandit/formatters/xml.py b/src/bandit-main/bandit-main/bandit/formatters/xml.py new file mode 100644 index 0000000..d2b2067 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/formatters/xml.py @@ -0,0 +1,97 @@ +# +# 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) diff --git a/src/bandit-main/bandit-main/bandit/formatters/yaml.py b/src/bandit-main/bandit-main/bandit/formatters/yaml.py new file mode 100644 index 0000000..4211090 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/formatters/yaml.py @@ -0,0 +1,126 @@ +# 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) diff --git a/src/bandit-main/bandit-main/bandit/plugins/__init__.py b/src/bandit-main/bandit-main/bandit/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/bandit-main/bandit-main/bandit/plugins/app_debug.py b/src/bandit-main/bandit-main/bandit/plugins/app_debug.py new file mode 100644 index 0000000..3b18996 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/app_debug.py @@ -0,0 +1,63 @@ +# +# 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"), + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/asserts.py b/src/bandit-main/bandit-main/bandit/plugins/asserts.py new file mode 100644 index 0000000..b32007c --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/asserts.py @@ -0,0 +1,83 @@ +# +# 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." + ), + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/crypto_request_no_cert_validation.py b/src/bandit-main/bandit-main/bandit/plugins/crypto_request_no_cert_validation.py new file mode 100644 index 0000000..11791ed --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/crypto_request_no_cert_validation.py @@ -0,0 +1,75 @@ +# +# 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"), + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/django_sql_injection.py b/src/bandit-main/bandit-main/bandit/plugins/django_sql_injection.py new file mode 100644 index 0000000..a57ff46 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/django_sql_injection.py @@ -0,0 +1,144 @@ +# +# 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.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.Str): + insecure = True + break + if not insecure: + for v in kwargs["select"].values: + if not isinstance(v, ast.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.Str): + return bandit.Issue( + severity=bandit.MEDIUM, + confidence=bandit.MEDIUM, + cwe=issue.Cwe.SQL_INJECTION, + text=description, + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/django_xss.py b/src/bandit-main/bandit-main/bandit/plugins/django_xss.py new file mode 100644 index 0000000..e96522a --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/django_xss.py @@ -0,0 +1,276 @@ +# +# 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.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.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.Str) 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.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.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.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.Str): + return check_risk(context.node) diff --git a/src/bandit-main/bandit-main/bandit/plugins/exec.py b/src/bandit-main/bandit-main/bandit/plugins/exec.py new file mode 100644 index 0000000..3e46247 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/exec.py @@ -0,0 +1,55 @@ +# +# 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() diff --git a/src/bandit-main/bandit-main/bandit/plugins/general_bad_file_permissions.py b/src/bandit-main/bandit-main/bandit/plugins/general_bad_file_permissions.py new file mode 100644 index 0000000..7d3fce4 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/general_bad_file_permissions.py @@ -0,0 +1,99 @@ +# +# 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), + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/general_bind_all_interfaces.py b/src/bandit-main/bandit-main/bandit/plugins/general_bind_all_interfaces.py new file mode 100644 index 0000000..58b840e --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/general_bind_all_interfaces.py @@ -0,0 +1,52 @@ +# +# 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.", + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/general_hardcoded_password.py b/src/bandit-main/bandit-main/bandit/plugins/general_hardcoded_password.py new file mode 100644 index 0000000..9196beb --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/general_hardcoded_password.py @@ -0,0 +1,254 @@ +# +# 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): + return bandit.Issue( + severity=bandit.LOW, + confidence=bandit.MEDIUM, + cwe=issue.Cwe.HARD_CODED_PASSWORD, + text=f"Possible hardcoded password: '{value}'", + ) + + +@test.checks("Str") +@test.test_id("B105") +def hardcoded_password_string(context): + """**B105: Test for use of hard-coded password strings** + + The use of hard-coded passwords increases the possibility of password + guessing tremendously. This plugin test looks for all string literals and + checks the following conditions: + + - assigned to a variable that looks like a password + - assigned to a dict key that looks like a password + - 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.s) + elif isinstance(targ, ast.Attribute) and RE_CANDIDATES.search( + targ.attr + ): + return _report(node.s) + + elif isinstance( + node._bandit_parent, ast.Subscript + ) and RE_CANDIDATES.search(node.s): + # 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.Str + ): + return _report(assign.value.s) + + elif isinstance(node._bandit_parent, ast.Index) and RE_CANDIDATES.search( + node.s + ): + # 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.Str + ): + return _report(assign.value.s) + + 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.Str): + return _report(comp.comparators[0].s) + elif isinstance(comp.left, ast.Attribute): + if RE_CANDIDATES.search(comp.left.attr): + if isinstance(comp.comparators[0], ast.Str): + return _report(comp.comparators[0].s) + + +@test.checks("Call") +@test.test_id("B106") +def hardcoded_password_funcarg(context): + """**B106: Test for use of hard-coded password function arguments** + + The use of hard-coded passwords increases the possibility of password + guessing tremendously. This plugin test looks for all function calls being + passed a keyword argument that is a string literal. It checks that the + assigned local variable does not look like a password. + + Variables are considered to look like a password if they have match any one + of: + + - "password" + - "pass" + - "passwd" + - "pwd" + - "secret" + - "token" + - "secrete" + + Note: this can be noisy and may generate false positives. + + **Config Options:** + + None + + :Example: + + .. code-block:: none + + >> Issue: [B106:hardcoded_password_funcarg] Possible hardcoded + password: 'blerg' + Severity: Low Confidence: Medium + 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.Str) and RE_CANDIDATES.search(kw.arg): + return _report(kw.value.s) + + +@test.checks("FunctionDef") +@test.test_id("B107") +def hardcoded_password_default(context): + """**B107: Test for use of hard-coded password argument defaults** + + The use of hard-coded passwords increases the possibility of password + guessing tremendously. This plugin test looks for all function definitions + that specify a default string literal for some argument. It checks that + the argument does not look like a password. + + Variables are considered to look like a password if they have match any one + of: + + - "password" + - "pass" + - "passwd" + - "pwd" + - "secret" + - "token" + - "secrete" + + Note: this can be noisy and may generate false positives. 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, ast.NameConstant)) + and val.value is None + ): + continue + if isinstance(val, ast.Str) and RE_CANDIDATES.search(key.arg): + return _report(val.s) diff --git a/src/bandit-main/bandit-main/bandit/plugins/general_hardcoded_tmp.py b/src/bandit-main/bandit-main/bandit/plugins/general_hardcoded_tmp.py new file mode 100644 index 0000000..ecf8995 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/general_hardcoded_tmp.py @@ -0,0 +1,79 @@ +# +# 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.", + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/hashlib_insecure_functions.py b/src/bandit-main/bandit-main/bandit/plugins/hashlib_insecure_functions.py new file mode 100644 index 0000000..626c8ed --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/hashlib_insecure_functions.py @@ -0,0 +1,124 @@ +# +# 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. + +For Python versions prior to 3.9, this check is similar to B303 blacklist +except that this checks for insecure hash functions created using +``hashlib.new`` function. For Python version 3.9 and later, 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) diff --git a/src/bandit-main/bandit-main/bandit/plugins/huggingface_unsafe_download.py b/src/bandit-main/bandit-main/bandit/plugins/huggingface_unsafe_download.py new file mode 100644 index 0000000..e51181a --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/huggingface_unsafe_download.py @@ -0,0 +1,153 @@ +# 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 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) + 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), + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/injection_paramiko.py b/src/bandit-main/bandit-main/bandit/plugins/injection_paramiko.py new file mode 100644 index 0000000..674fe0b --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/injection_paramiko.py @@ -0,0 +1,63 @@ +# +# 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, + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/injection_shell.py b/src/bandit-main/bandit-main/bandit/plugins/injection_shell.py new file mode 100644 index 0000000..2293683 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/injection_shell.py @@ -0,0 +1,696 @@ +# +# 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 delimeter characters (/, \, .) +full_path_match = re.compile(r"^(?:[A-Za-z](?=\:)|[\\\/\.])") + + +def _evaluate_shell_call(context): + no_formatting = isinstance(context.node.args[0], ast.Str) + + if no_formatting: + return bandit.LOW + else: + return bandit.HIGH + + +def gen_config(name): + if name == "shell_injection": + return { + # Start a process using the subprocess module, or one of its + # wrappers. + "subprocess": [ + "subprocess.Popen", + "subprocess.call", + "subprocess.check_call", + "subprocess.check_output", + "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.Num): + result = bool(val.n) + 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.NameConstant): + 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.Str) and not full_path_match.match(node.s): + return bandit.Issue( + severity=bandit.LOW, + confidence=bandit.HIGH, + cwe=issue.Cwe.OS_COMMAND_INJECTION, + text="Starting a process with a partial executable path", + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/injection_sql.py b/src/bandit-main/bandit-main/bandit/plugins/injection_sql.py new file mode 100644 index 0000000..bd7aa92 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/injection_sql.py @@ -0,0 +1,143 @@ +# +# 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.s + # 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.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.s) 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.", + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/injection_wildcard.py b/src/bandit-main/bandit-main/bandit/plugins/injection_wildcard.py new file mode 100644 index 0000000..46f6b5b --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/injection_wildcard.py @@ -0,0 +1,144 @@ +# +# 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"), + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/insecure_ssl_tls.py b/src/bandit-main/bandit-main/bandit/plugins/insecure_ssl_tls.py new file mode 100644 index 0000000..319abcf --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/insecure_ssl_tls.py @@ -0,0 +1,285 @@ +# +# 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"), + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/jinja2_templates.py b/src/bandit-main/bandit-main/bandit/plugins/jinja2_templates.py new file mode 100644 index 0000000..3374205 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/jinja2_templates.py @@ -0,0 +1,134 @@ +# +# 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.", + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/logging_config_insecure_listen.py b/src/bandit-main/bandit-main/bandit/plugins/logging_config_insecure_listen.py new file mode 100644 index 0000000..96815f0 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/logging_config_insecure_listen.py @@ -0,0 +1,58 @@ +# 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.", + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/mako_templates.py b/src/bandit-main/bandit-main/bandit/plugins/mako_templates.py new file mode 100644 index 0000000..21e8151 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/mako_templates.py @@ -0,0 +1,69 @@ +# +# 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 }.", + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/markupsafe_markup_xss.py b/src/bandit-main/bandit-main/bandit/plugins/markupsafe_markup_xss.py new file mode 100644 index 0000000..7eae905 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/markupsafe_markup_xss.py @@ -0,0 +1,118 @@ +# 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.", + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/pytorch_load.py b/src/bandit-main/bandit-main/bandit/plugins/pytorch_load.py new file mode 100644 index 0000000..ef3e49f --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/pytorch_load.py @@ -0,0 +1,81 @@ +# 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`. Using `torch.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-94 (https://cwe.mitre.org/data/definitions/94.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/94.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`. Using `torch.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 + + qualname_list = qualname.split(".") + func = qualname_list[-1] + if all( + [ + "torch" in qualname_list, + func == "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"), + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/request_without_timeout.py b/src/bandit-main/bandit-main/bandit/plugins/request_without_timeout.py new file mode 100644 index 0000000..c643900 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/request_without_timeout.py @@ -0,0 +1,84 @@ +# 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", + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/snmp_security_check.py b/src/bandit-main/bandit-main/bandit/plugins/snmp_security_check.py new file mode 100644 index 0000000..a915ed8 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/snmp_security_check.py @@ -0,0 +1,110 @@ +# +# 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"), + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/ssh_no_host_key_verification.py b/src/bandit-main/bandit-main/bandit/plugins/ssh_no_host_key_verification.py new file mode 100644 index 0000000..51be2eb --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/ssh_no_host_key_verification.py @@ -0,0 +1,76 @@ +# 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" + ), + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/tarfile_unsafe_members.py b/src/bandit-main/bandit-main/bandit/plugins/tarfile_unsafe_members.py new file mode 100644 index 0000000..5ad145c --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/tarfile_unsafe_members.py @@ -0,0 +1,121 @@ +# +# 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.extractalll(members=function(tarfile))`` - LOW +* ``tarfile.extractalll(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.Str) and arg.s == "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) diff --git a/src/bandit-main/bandit-main/bandit/plugins/trojansource.py b/src/bandit-main/bandit-main/bandit/plugins/trojansource.py new file mode 100644 index 0000000..ddf2448 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/trojansource.py @@ -0,0 +1,79 @@ +# +# 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): + with open(context.filename, "rb") as src_file: + encoding, _ = detect_encoding(src_file.readline) + with open(context.filename, encoding=encoding) as src_file: + for lineno, line in enumerate(src_file.readlines(), 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 diff --git a/src/bandit-main/bandit-main/bandit/plugins/try_except_continue.py b/src/bandit-main/bandit-main/bandit/plugins/try_except_continue.py new file mode 100644 index 0000000..c2e3ad4 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/try_except_continue.py @@ -0,0 +1,108 @@ +# 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."), + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/try_except_pass.py b/src/bandit-main/bandit-main/bandit/plugins/try_except_pass.py new file mode 100644 index 0000000..eda0ef8 --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/try_except_pass.py @@ -0,0 +1,106 @@ +# +# 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."), + ) diff --git a/src/bandit-main/bandit-main/bandit/plugins/weak_cryptographic_key.py b/src/bandit-main/bandit-main/bandit/plugins/weak_cryptographic_key.py new file mode 100644 index 0000000..da73ced --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/weak_cryptographic_key.py @@ -0,0 +1,165 @@ +# 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) diff --git a/src/bandit-main/bandit-main/bandit/plugins/yaml_load.py b/src/bandit-main/bandit-main/bandit/plugins/yaml_load.py new file mode 100644 index 0000000..2304c1d --- /dev/null +++ b/src/bandit-main/bandit-main/bandit/plugins/yaml_load.py @@ -0,0 +1,76 @@ +# +# 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, + ) diff --git a/src/bandit-main/bandit-main/doc/requirements.txt b/src/bandit-main/bandit-main/doc/requirements.txt new file mode 100644 index 0000000..0d20747 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/requirements.txt @@ -0,0 +1,6 @@ +# 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 diff --git a/src/bandit-main/bandit-main/doc/source/blacklists/blacklist_calls.rst b/src/bandit-main/bandit-main/doc/source/blacklists/blacklist_calls.rst new file mode 100644 index 0000000..f565c84 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/blacklists/blacklist_calls.rst @@ -0,0 +1,6 @@ +--------------- +blacklist_calls +--------------- + +.. automodule:: bandit.blacklists.calls + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/blacklists/blacklist_imports.rst b/src/bandit-main/bandit-main/doc/source/blacklists/blacklist_imports.rst new file mode 100644 index 0000000..3a817d6 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/blacklists/blacklist_imports.rst @@ -0,0 +1,6 @@ +----------------- +blacklist_imports +----------------- + +.. automodule:: bandit.blacklists.imports + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/blacklists/index.rst b/src/bandit-main/bandit-main/doc/source/blacklists/index.rst new file mode 100644 index 0000000..98385e7 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/blacklists/index.rst @@ -0,0 +1,70 @@ +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 diff --git a/src/bandit-main/bandit-main/doc/source/ci-cd/github-actions.rst b/src/bandit-main/bandit-main/doc/source/ci-cd/github-actions.rst new file mode 100644 index 0000000..a210b38 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/ci-cd/github-actions.rst @@ -0,0 +1,90 @@ +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 + - ``.`` \ No newline at end of file diff --git a/src/bandit-main/bandit-main/doc/source/ci-cd/index.rst b/src/bandit-main/bandit-main/doc/source/ci-cd/index.rst new file mode 100644 index 0000000..7eea55f --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/ci-cd/index.rst @@ -0,0 +1,27 @@ +.. _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. diff --git a/src/bandit-main/bandit-main/doc/source/conf.py b/src/bandit-main/bandit-main/doc/source/conf.py new file mode 100644 index 0000000..f2a991c --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/conf.py @@ -0,0 +1,86 @@ +# 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} diff --git a/src/bandit-main/bandit-main/doc/source/config.rst b/src/bandit-main/bandit-main/doc/source/config.rst new file mode 100644 index 0000000..185d6ae --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/config.rst @@ -0,0 +1,297 @@ +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/ diff --git a/src/bandit-main/bandit-main/doc/source/faq.rst b/src/bandit-main/bandit-main/doc/source/faq.rst new file mode 100644 index 0000000..16fe25f --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/faq.rst @@ -0,0 +1,20 @@ +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. diff --git a/src/bandit-main/bandit-main/doc/source/formatters/csv.rst b/src/bandit-main/bandit-main/doc/source/formatters/csv.rst new file mode 100644 index 0000000..5001ccb --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/formatters/csv.rst @@ -0,0 +1,6 @@ +--- +csv +--- + +.. automodule:: bandit.formatters.csv + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/formatters/custom.rst b/src/bandit-main/bandit-main/doc/source/formatters/custom.rst new file mode 100644 index 0000000..7c11963 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/formatters/custom.rst @@ -0,0 +1,6 @@ +------ +custom +------ + +.. automodule:: bandit.formatters.custom + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/formatters/html.rst b/src/bandit-main/bandit-main/doc/source/formatters/html.rst new file mode 100644 index 0000000..e28154c --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/formatters/html.rst @@ -0,0 +1,6 @@ +---- +html +---- + +.. automodule:: bandit.formatters.html + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/formatters/index.rst b/src/bandit-main/bandit-main/doc/source/formatters/index.rst new file mode 100644 index 0000000..363b022 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/formatters/index.rst @@ -0,0 +1,42 @@ +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: + + * diff --git a/src/bandit-main/bandit-main/doc/source/formatters/json.rst b/src/bandit-main/bandit-main/doc/source/formatters/json.rst new file mode 100644 index 0000000..4eb5b87 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/formatters/json.rst @@ -0,0 +1,6 @@ +---- +json +---- + +.. automodule:: bandit.formatters.json + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/formatters/sarif.rst b/src/bandit-main/bandit-main/doc/source/formatters/sarif.rst new file mode 100644 index 0000000..95b8934 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/formatters/sarif.rst @@ -0,0 +1,6 @@ +----- +sarif +----- + +.. automodule:: bandit.formatters.sarif + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/formatters/screen.rst b/src/bandit-main/bandit-main/doc/source/formatters/screen.rst new file mode 100644 index 0000000..82186af --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/formatters/screen.rst @@ -0,0 +1,6 @@ +------ +screen +------ + +.. automodule:: bandit.formatters.screen + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/formatters/text.rst b/src/bandit-main/bandit-main/doc/source/formatters/text.rst new file mode 100644 index 0000000..7a07df9 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/formatters/text.rst @@ -0,0 +1,6 @@ +---- +text +---- + +.. automodule:: bandit.formatters.text + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/formatters/xml.rst b/src/bandit-main/bandit-main/doc/source/formatters/xml.rst new file mode 100644 index 0000000..8b20282 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/formatters/xml.rst @@ -0,0 +1,6 @@ +--- +xml +--- + +.. automodule:: bandit.formatters.xml + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/formatters/yaml.rst b/src/bandit-main/bandit-main/doc/source/formatters/yaml.rst new file mode 100644 index 0000000..3de2169 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/formatters/yaml.rst @@ -0,0 +1,6 @@ +---- +yaml +---- + +.. automodule:: bandit.formatters.yaml + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/index.rst b/src/bandit-main/bandit-main/doc/source/index.rst new file mode 100644 index 0000000..4f4202b --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/index.rst @@ -0,0 +1,45 @@ +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 `_ diff --git a/src/bandit-main/bandit-main/doc/source/integrations.rst b/src/bandit-main/bandit-main/doc/source/integrations.rst new file mode 100644 index 0000000..83ead90 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/integrations.rst @@ -0,0 +1,63 @@ +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! diff --git a/src/bandit-main/bandit-main/doc/source/man/bandit.rst b/src/bandit-main/bandit-main/doc/source/man/bandit.rst new file mode 100644 index 0000000..23215f8 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/man/bandit.rst @@ -0,0 +1,139 @@ +====== +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) diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b101_assert_used.rst b/src/bandit-main/bandit-main/doc/source/plugins/b101_assert_used.rst new file mode 100644 index 0000000..97077bf --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b101_assert_used.rst @@ -0,0 +1,6 @@ +----------------- +B101: assert_used +----------------- + +.. automodule:: bandit.plugins.asserts + :no-index: \ No newline at end of file diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b102_exec_used.rst b/src/bandit-main/bandit-main/doc/source/plugins/b102_exec_used.rst new file mode 100644 index 0000000..f139828 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b102_exec_used.rst @@ -0,0 +1,6 @@ +--------------- +B102: exec_used +--------------- + +.. automodule:: bandit.plugins.exec + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b103_set_bad_file_permissions.rst b/src/bandit-main/bandit-main/doc/source/plugins/b103_set_bad_file_permissions.rst new file mode 100644 index 0000000..5b51f4d --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b103_set_bad_file_permissions.rst @@ -0,0 +1,6 @@ +------------------------------ +B103: set_bad_file_permissions +------------------------------ + +.. automodule:: bandit.plugins.general_bad_file_permissions + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b104_hardcoded_bind_all_interfaces.rst b/src/bandit-main/bandit-main/doc/source/plugins/b104_hardcoded_bind_all_interfaces.rst new file mode 100644 index 0000000..5306f88 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b104_hardcoded_bind_all_interfaces.rst @@ -0,0 +1,6 @@ +----------------------------------- +B104: hardcoded_bind_all_interfaces +----------------------------------- + +.. automodule:: bandit.plugins.general_bind_all_interfaces + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b105_hardcoded_password_string.rst b/src/bandit-main/bandit-main/doc/source/plugins/b105_hardcoded_password_string.rst new file mode 100644 index 0000000..72dde9c --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b105_hardcoded_password_string.rst @@ -0,0 +1,8 @@ +------------------------------- +B105: hardcoded_password_string +------------------------------- + +.. currentmodule:: bandit.plugins.general_hardcoded_password + +.. autofunction:: hardcoded_password_string + :noindex: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b106_hardcoded_password_funcarg.rst b/src/bandit-main/bandit-main/doc/source/plugins/b106_hardcoded_password_funcarg.rst new file mode 100644 index 0000000..2f668e2 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b106_hardcoded_password_funcarg.rst @@ -0,0 +1,8 @@ +-------------------------------- +B106: hardcoded_password_funcarg +-------------------------------- + +.. currentmodule:: bandit.plugins.general_hardcoded_password + +.. autofunction:: hardcoded_password_funcarg + :noindex: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b107_hardcoded_password_default.rst b/src/bandit-main/bandit-main/doc/source/plugins/b107_hardcoded_password_default.rst new file mode 100644 index 0000000..ba82f40 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b107_hardcoded_password_default.rst @@ -0,0 +1,8 @@ +-------------------------------- +B107: hardcoded_password_default +-------------------------------- + +.. currentmodule:: bandit.plugins.general_hardcoded_password + +.. autofunction:: hardcoded_password_default + :noindex: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b108_hardcoded_tmp_directory.rst b/src/bandit-main/bandit-main/doc/source/plugins/b108_hardcoded_tmp_directory.rst new file mode 100644 index 0000000..166e4bf --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b108_hardcoded_tmp_directory.rst @@ -0,0 +1,6 @@ +----------------------------- +B108: hardcoded_tmp_directory +----------------------------- + +.. automodule:: bandit.plugins.general_hardcoded_tmp + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b109_password_config_option_not_marked_secret.rst b/src/bandit-main/bandit-main/doc/source/plugins/b109_password_config_option_not_marked_secret.rst new file mode 100644 index 0000000..0fb5b33 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b109_password_config_option_not_marked_secret.rst @@ -0,0 +1,60 @@ +---------------------------------------------- +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 diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b110_try_except_pass.rst b/src/bandit-main/bandit-main/doc/source/plugins/b110_try_except_pass.rst new file mode 100644 index 0000000..7eddf08 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b110_try_except_pass.rst @@ -0,0 +1,6 @@ +--------------------- +B110: try_except_pass +--------------------- + +.. automodule:: bandit.plugins.try_except_pass + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b111_execute_with_run_as_root_equals_true.rst b/src/bandit-main/bandit-main/doc/source/plugins/b111_execute_with_run_as_root_equals_true.rst new file mode 100644 index 0000000..bf78a72 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b111_execute_with_run_as_root_equals_true.rst @@ -0,0 +1,54 @@ +------------------------------------------ +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 diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b112_try_except_continue.rst b/src/bandit-main/bandit-main/doc/source/plugins/b112_try_except_continue.rst new file mode 100644 index 0000000..580c084 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b112_try_except_continue.rst @@ -0,0 +1,6 @@ +------------------------- +B112: try_except_continue +------------------------- + +.. automodule:: bandit.plugins.try_except_continue + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b113_request_without_timeout.rst b/src/bandit-main/bandit-main/doc/source/plugins/b113_request_without_timeout.rst new file mode 100644 index 0000000..2be183a --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b113_request_without_timeout.rst @@ -0,0 +1,6 @@ +----------------------------- +B113: request_without_timeout +----------------------------- + +.. automodule:: bandit.plugins.request_without_timeout + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b201_flask_debug_true.rst b/src/bandit-main/bandit-main/doc/source/plugins/b201_flask_debug_true.rst new file mode 100644 index 0000000..2f18aca --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b201_flask_debug_true.rst @@ -0,0 +1,6 @@ +---------------------- +B201: flask_debug_true +---------------------- + +.. automodule:: bandit.plugins.app_debug + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b202_tarfile_unsafe_members.rst b/src/bandit-main/bandit-main/doc/source/plugins/b202_tarfile_unsafe_members.rst new file mode 100644 index 0000000..40b0392 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b202_tarfile_unsafe_members.rst @@ -0,0 +1,6 @@ +---------------------------- +B202: tarfile_unsafe_members +---------------------------- + +.. automodule:: bandit.plugins.tarfile_unsafe_members + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b324_hashlib.rst b/src/bandit-main/bandit-main/doc/source/plugins/b324_hashlib.rst new file mode 100644 index 0000000..b13f12b --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b324_hashlib.rst @@ -0,0 +1,6 @@ +------------- +B324: hashlib +------------- + +.. automodule:: bandit.plugins.hashlib_insecure_functions + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b501_request_with_no_cert_validation.rst b/src/bandit-main/bandit-main/doc/source/plugins/b501_request_with_no_cert_validation.rst new file mode 100644 index 0000000..d26ee07 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b501_request_with_no_cert_validation.rst @@ -0,0 +1,6 @@ +------------------------------------- +B501: request_with_no_cert_validation +------------------------------------- + +.. automodule:: bandit.plugins.crypto_request_no_cert_validation + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b502_ssl_with_bad_version.rst b/src/bandit-main/bandit-main/doc/source/plugins/b502_ssl_with_bad_version.rst new file mode 100644 index 0000000..16b5def --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b502_ssl_with_bad_version.rst @@ -0,0 +1,8 @@ +-------------------------- +B502: ssl_with_bad_version +-------------------------- + +.. currentmodule:: bandit.plugins.insecure_ssl_tls + +.. autofunction:: ssl_with_bad_version + :noindex: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b503_ssl_with_bad_defaults.rst b/src/bandit-main/bandit-main/doc/source/plugins/b503_ssl_with_bad_defaults.rst new file mode 100644 index 0000000..ebdb8bc --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b503_ssl_with_bad_defaults.rst @@ -0,0 +1,8 @@ +--------------------------- +B503: ssl_with_bad_defaults +--------------------------- + +.. currentmodule:: bandit.plugins.insecure_ssl_tls + +.. autofunction:: ssl_with_bad_defaults + :noindex: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b504_ssl_with_no_version.rst b/src/bandit-main/bandit-main/doc/source/plugins/b504_ssl_with_no_version.rst new file mode 100644 index 0000000..2a8247b --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b504_ssl_with_no_version.rst @@ -0,0 +1,8 @@ +------------------------- +B504: ssl_with_no_version +------------------------- + +.. currentmodule:: bandit.plugins.insecure_ssl_tls + +.. autofunction:: ssl_with_no_version + :noindex: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b505_weak_cryptographic_key.rst b/src/bandit-main/bandit-main/doc/source/plugins/b505_weak_cryptographic_key.rst new file mode 100644 index 0000000..366e665 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b505_weak_cryptographic_key.rst @@ -0,0 +1,6 @@ +---------------------------- +B505: weak_cryptographic_key +---------------------------- + +.. automodule:: bandit.plugins.weak_cryptographic_key + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b506_yaml_load.rst b/src/bandit-main/bandit-main/doc/source/plugins/b506_yaml_load.rst new file mode 100644 index 0000000..e9822c7 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b506_yaml_load.rst @@ -0,0 +1,6 @@ +--------------- +B506: yaml_load +--------------- + +.. automodule:: bandit.plugins.yaml_load + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b507_ssh_no_host_key_verification.rst b/src/bandit-main/bandit-main/doc/source/plugins/b507_ssh_no_host_key_verification.rst new file mode 100644 index 0000000..b28587c --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b507_ssh_no_host_key_verification.rst @@ -0,0 +1,6 @@ +---------------------------------- +B507: ssh_no_host_key_verification +---------------------------------- + +.. automodule:: bandit.plugins.ssh_no_host_key_verification + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b508_snmp_insecure_version.rst b/src/bandit-main/bandit-main/doc/source/plugins/b508_snmp_insecure_version.rst new file mode 100644 index 0000000..e3bf6af --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b508_snmp_insecure_version.rst @@ -0,0 +1,8 @@ +--------------------------- +B508: snmp_insecure_version +--------------------------- + +.. currentmodule:: bandit.plugins.snmp_security_check + +.. autofunction:: snmp_insecure_version_check + :noindex: \ No newline at end of file diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b509_snmp_weak_cryptography.rst b/src/bandit-main/bandit-main/doc/source/plugins/b509_snmp_weak_cryptography.rst new file mode 100644 index 0000000..2c838d4 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b509_snmp_weak_cryptography.rst @@ -0,0 +1,8 @@ +---------------------------- +B509: snmp_weak_cryptography +---------------------------- + +.. currentmodule:: bandit.plugins.snmp_security_check + +.. autofunction:: snmp_crypto_check + :noindex: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b601_paramiko_calls.rst b/src/bandit-main/bandit-main/doc/source/plugins/b601_paramiko_calls.rst new file mode 100644 index 0000000..98dfdbd --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b601_paramiko_calls.rst @@ -0,0 +1,6 @@ +-------------------- +B601: paramiko_calls +-------------------- + +.. automodule:: bandit.plugins.injection_paramiko + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b602_subprocess_popen_with_shell_equals_true.rst b/src/bandit-main/bandit-main/doc/source/plugins/b602_subprocess_popen_with_shell_equals_true.rst new file mode 100644 index 0000000..8b60c5d --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b602_subprocess_popen_with_shell_equals_true.rst @@ -0,0 +1,8 @@ +--------------------------------------------- +B602: subprocess_popen_with_shell_equals_true +--------------------------------------------- + +.. currentmodule:: bandit.plugins.injection_shell + +.. autofunction:: subprocess_popen_with_shell_equals_true + :noindex: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b603_subprocess_without_shell_equals_true.rst b/src/bandit-main/bandit-main/doc/source/plugins/b603_subprocess_without_shell_equals_true.rst new file mode 100644 index 0000000..733b505 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b603_subprocess_without_shell_equals_true.rst @@ -0,0 +1,8 @@ +------------------------------------------ +B603: subprocess_without_shell_equals_true +------------------------------------------ + +.. currentmodule:: bandit.plugins.injection_shell + +.. autofunction:: subprocess_without_shell_equals_true + :noindex: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b604_any_other_function_with_shell_equals_true.rst b/src/bandit-main/bandit-main/doc/source/plugins/b604_any_other_function_with_shell_equals_true.rst new file mode 100644 index 0000000..d9af8f7 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b604_any_other_function_with_shell_equals_true.rst @@ -0,0 +1,8 @@ +----------------------------------------------- +B604: any_other_function_with_shell_equals_true +----------------------------------------------- + +.. currentmodule:: bandit.plugins.injection_shell + +.. autofunction:: any_other_function_with_shell_equals_true + :noindex: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b605_start_process_with_a_shell.rst b/src/bandit-main/bandit-main/doc/source/plugins/b605_start_process_with_a_shell.rst new file mode 100644 index 0000000..97667d8 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b605_start_process_with_a_shell.rst @@ -0,0 +1,8 @@ +-------------------------------- +B605: start_process_with_a_shell +-------------------------------- + +.. currentmodule:: bandit.plugins.injection_shell + +.. autofunction:: start_process_with_a_shell + :noindex: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b606_start_process_with_no_shell.rst b/src/bandit-main/bandit-main/doc/source/plugins/b606_start_process_with_no_shell.rst new file mode 100644 index 0000000..65e8ba6 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b606_start_process_with_no_shell.rst @@ -0,0 +1,8 @@ +--------------------------------- +B606: start_process_with_no_shell +--------------------------------- + +.. currentmodule:: bandit.plugins.injection_shell + +.. autofunction:: start_process_with_no_shell + :noindex: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b607_start_process_with_partial_path.rst b/src/bandit-main/bandit-main/doc/source/plugins/b607_start_process_with_partial_path.rst new file mode 100644 index 0000000..36a77bc --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b607_start_process_with_partial_path.rst @@ -0,0 +1,8 @@ +------------------------------------- +B607: start_process_with_partial_path +------------------------------------- + +.. currentmodule:: bandit.plugins.injection_shell + +.. autofunction:: start_process_with_partial_path + :noindex: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b608_hardcoded_sql_expressions.rst b/src/bandit-main/bandit-main/doc/source/plugins/b608_hardcoded_sql_expressions.rst new file mode 100644 index 0000000..d817078 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b608_hardcoded_sql_expressions.rst @@ -0,0 +1,6 @@ +------------------------------- +B608: hardcoded_sql_expressions +------------------------------- + +.. automodule:: bandit.plugins.injection_sql + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b609_linux_commands_wildcard_injection.rst b/src/bandit-main/bandit-main/doc/source/plugins/b609_linux_commands_wildcard_injection.rst new file mode 100644 index 0000000..a83c18b --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b609_linux_commands_wildcard_injection.rst @@ -0,0 +1,6 @@ +--------------------------------------- +B609: linux_commands_wildcard_injection +--------------------------------------- + +.. automodule:: bandit.plugins.injection_wildcard + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b610_django_extra_used.rst b/src/bandit-main/bandit-main/doc/source/plugins/b610_django_extra_used.rst new file mode 100644 index 0000000..f512241 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b610_django_extra_used.rst @@ -0,0 +1,8 @@ +----------------------- +B610: django_extra_used +----------------------- + +.. currentmodule:: bandit.plugins.django_sql_injection + +.. autofunction:: django_extra_used + :noindex: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b611_django_rawsql_used.rst b/src/bandit-main/bandit-main/doc/source/plugins/b611_django_rawsql_used.rst new file mode 100644 index 0000000..588d445 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b611_django_rawsql_used.rst @@ -0,0 +1,8 @@ +------------------------ +B611: django_rawsql_used +------------------------ + +.. currentmodule:: bandit.plugins.django_sql_injection + +.. autofunction:: django_rawsql_used + :noindex: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b612_logging_config_insecure_listen.rst b/src/bandit-main/bandit-main/doc/source/plugins/b612_logging_config_insecure_listen.rst new file mode 100644 index 0000000..c426f7d --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b612_logging_config_insecure_listen.rst @@ -0,0 +1,6 @@ +------------------------------------ +B612: logging_config_insecure_listen +------------------------------------ + +.. automodule:: bandit.plugins.logging_config_insecure_listen + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b613_trojansource.rst b/src/bandit-main/bandit-main/doc/source/plugins/b613_trojansource.rst new file mode 100644 index 0000000..1de4803 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b613_trojansource.rst @@ -0,0 +1,6 @@ +------------------ +B613: trojansource +------------------ + +.. automodule:: bandit.plugins.trojansource + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b614_pytorch_load.rst b/src/bandit-main/bandit-main/doc/source/plugins/b614_pytorch_load.rst new file mode 100644 index 0000000..f87400a --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b614_pytorch_load.rst @@ -0,0 +1,6 @@ +------------------ +B614: pytorch_load +------------------ + +.. automodule:: bandit.plugins.pytorch_load + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b615_huggingface_unsafe_download.rst b/src/bandit-main/bandit-main/doc/source/plugins/b615_huggingface_unsafe_download.rst new file mode 100644 index 0000000..9b8f882 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b615_huggingface_unsafe_download.rst @@ -0,0 +1,6 @@ +--------------------------------- +B615: huggingface_unsafe_download +--------------------------------- + +.. automodule:: bandit.plugins.huggingface_unsafe_download + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b701_jinja2_autoescape_false.rst b/src/bandit-main/bandit-main/doc/source/plugins/b701_jinja2_autoescape_false.rst new file mode 100644 index 0000000..2c40053 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b701_jinja2_autoescape_false.rst @@ -0,0 +1,6 @@ +----------------------------- +B701: jinja2_autoescape_false +----------------------------- + +.. automodule:: bandit.plugins.jinja2_templates + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b702_use_of_mako_templates.rst b/src/bandit-main/bandit-main/doc/source/plugins/b702_use_of_mako_templates.rst new file mode 100644 index 0000000..3db4d20 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b702_use_of_mako_templates.rst @@ -0,0 +1,6 @@ +--------------------------- +B702: use_of_mako_templates +--------------------------- + +.. automodule:: bandit.plugins.mako_templates + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b703_django_mark_safe.rst b/src/bandit-main/bandit-main/doc/source/plugins/b703_django_mark_safe.rst new file mode 100644 index 0000000..e6aebe3 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b703_django_mark_safe.rst @@ -0,0 +1,8 @@ +---------------------- +B703: django_mark_safe +---------------------- + +.. currentmodule:: bandit.plugins.django_xss + +.. autofunction:: django_mark_safe + :noindex: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/b704_markupsafe_markup_xss.rst b/src/bandit-main/bandit-main/doc/source/plugins/b704_markupsafe_markup_xss.rst new file mode 100644 index 0000000..b7b0390 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/b704_markupsafe_markup_xss.rst @@ -0,0 +1,6 @@ +--------------------------- +B704: markupsafe_markup_xss +--------------------------- + +.. automodule:: bandit.plugins.markupsafe_markup_xss + :no-index: diff --git a/src/bandit-main/bandit-main/doc/source/plugins/index.rst b/src/bandit-main/bandit-main/doc/source/plugins/index.rst new file mode 100644 index 0000000..69f4398 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/plugins/index.rst @@ -0,0 +1,120 @@ +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: + + * diff --git a/src/bandit-main/bandit-main/doc/source/start.rst b/src/bandit-main/bandit-main/doc/source/start.rst new file mode 100644 index 0000000..b3408f1 --- /dev/null +++ b/src/bandit-main/bandit-main/doc/source/start.rst @@ -0,0 +1,142 @@ +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 diff --git a/src/bandit-main/bandit-main/docker/Dockerfile b/src/bandit-main/bandit-main/docker/Dockerfile new file mode 100644 index 0000000..ab33e87 --- /dev/null +++ b/src/bandit-main/bandit-main/docker/Dockerfile @@ -0,0 +1,16 @@ +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"] diff --git a/src/bandit-main/bandit-main/examples/__init__.py b/src/bandit-main/bandit-main/examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/bandit-main/bandit-main/examples/assert.py b/src/bandit-main/bandit-main/examples/assert.py new file mode 100644 index 0000000..f8cc332 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/assert.py @@ -0,0 +1 @@ +assert True diff --git a/src/bandit-main/bandit-main/examples/binding.py b/src/bandit-main/bandit-main/examples/binding.py new file mode 100644 index 0000000..fee2487 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/binding.py @@ -0,0 +1,5 @@ +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)) diff --git a/src/bandit-main/bandit-main/examples/cipher-modes.py b/src/bandit-main/bandit-main/examples/cipher-modes.py new file mode 100644 index 0000000..ff1f86d --- /dev/null +++ b/src/bandit-main/bandit-main/examples/cipher-modes.py @@ -0,0 +1,12 @@ +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) diff --git a/src/bandit-main/bandit-main/examples/ciphers.py b/src/bandit-main/bandit-main/examples/ciphers.py new file mode 100644 index 0000000..af30801 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/ciphers.py @@ -0,0 +1,88 @@ +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") diff --git a/src/bandit-main/bandit-main/examples/crypto-md5.py b/src/bandit-main/bandit-main/examples/crypto-md5.py new file mode 100644 index 0000000..b827c70 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/crypto-md5.py @@ -0,0 +1,49 @@ +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) diff --git a/src/bandit-main/bandit-main/examples/dill.py b/src/bandit-main/bandit-main/examples/dill.py new file mode 100644 index 0000000..beb1e57 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/dill.py @@ -0,0 +1,14 @@ +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()) diff --git a/src/bandit-main/bandit-main/examples/django_sql_injection_extra.py b/src/bandit-main/bandit-main/examples/django_sql_injection_extra.py new file mode 100644 index 0000000..b191d11 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/django_sql_injection_extra.py @@ -0,0 +1,29 @@ +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() diff --git a/src/bandit-main/bandit-main/examples/django_sql_injection_raw.py b/src/bandit-main/bandit-main/examples/django_sql_injection_raw.py new file mode 100644 index 0000000..ee42302 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/django_sql_injection_raw.py @@ -0,0 +1,13 @@ +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'))) diff --git a/src/bandit-main/bandit-main/examples/eval.py b/src/bandit-main/bandit-main/examples/eval.py new file mode 100644 index 0000000..72a8174 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/eval.py @@ -0,0 +1,15 @@ +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() diff --git a/src/bandit-main/bandit-main/examples/exec.py b/src/bandit-main/bandit-main/examples/exec.py new file mode 100644 index 0000000..17ac83a --- /dev/null +++ b/src/bandit-main/bandit-main/examples/exec.py @@ -0,0 +1 @@ +exec("do evil") diff --git a/src/bandit-main/bandit-main/examples/flask_debug.py b/src/bandit-main/bandit-main/examples/flask_debug.py new file mode 100644 index 0000000..28429d7 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/flask_debug.py @@ -0,0 +1,19 @@ +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) diff --git a/src/bandit-main/bandit-main/examples/ftplib.py b/src/bandit-main/bandit-main/examples/ftplib.py new file mode 100644 index 0000000..6664ed0 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/ftplib.py @@ -0,0 +1,24 @@ +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() diff --git a/src/bandit-main/bandit-main/examples/hardcoded-passwords.py b/src/bandit-main/bandit-main/examples/hardcoded-passwords.py new file mode 100644 index 0000000..acae7ad --- /dev/null +++ b/src/bandit-main/bandit-main/examples/hardcoded-passwords.py @@ -0,0 +1,80 @@ +# 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 diff --git a/src/bandit-main/bandit-main/examples/hardcoded-tmp.py b/src/bandit-main/bandit-main/examples/hardcoded-tmp.py new file mode 100644 index 0000000..c5befd3 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/hardcoded-tmp.py @@ -0,0 +1,16 @@ +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') diff --git a/src/bandit-main/bandit-main/examples/hashlib_new_insecure_functions.py b/src/bandit-main/bandit-main/examples/hashlib_new_insecure_functions.py new file mode 100644 index 0000000..f7855bd --- /dev/null +++ b/src/bandit-main/bandit-main/examples/hashlib_new_insecure_functions.py @@ -0,0 +1,28 @@ +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') + +# usedforsecurity arg only availabe in Python 3.9+ +hashlib.new('sha1', usedforsecurity=True) + +# Test that plugin does not flag valid hash functions. +hashlib.new('sha256') + +hashlib.new('SHA512') + +# usedforsecurity arg only availabe in Python 3.9+ +hashlib.new(name='sha1', usedforsecurity=False) diff --git a/src/bandit-main/bandit-main/examples/httpoxy_cgihandler.py b/src/bandit-main/bandit-main/examples/httpoxy_cgihandler.py new file mode 100644 index 0000000..b091c29 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/httpoxy_cgihandler.py @@ -0,0 +1,10 @@ +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) diff --git a/src/bandit-main/bandit-main/examples/httpoxy_twisted_directory.py b/src/bandit-main/bandit-main/examples/httpoxy_twisted_directory.py new file mode 100644 index 0000000..9850b55 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/httpoxy_twisted_directory.py @@ -0,0 +1,7 @@ +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() diff --git a/src/bandit-main/bandit-main/examples/httpoxy_twisted_script.py b/src/bandit-main/bandit-main/examples/httpoxy_twisted_script.py new file mode 100644 index 0000000..3de259c --- /dev/null +++ b/src/bandit-main/bandit-main/examples/httpoxy_twisted_script.py @@ -0,0 +1,7 @@ +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() diff --git a/src/bandit-main/bandit-main/examples/huggingface_unsafe_download.py b/src/bandit-main/bandit-main/examples/huggingface_unsafe_download.py new file mode 100644 index 0000000..61f768b --- /dev/null +++ b/src/bandit-main/bandit-main/examples/huggingface_unsafe_download.py @@ -0,0 +1,149 @@ +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" +) diff --git a/src/bandit-main/bandit-main/examples/imports-aliases.py b/src/bandit-main/bandit-main/examples/imports-aliases.py new file mode 100644 index 0000000..97d5342 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/imports-aliases.py @@ -0,0 +1,15 @@ +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'}) diff --git a/src/bandit-main/bandit-main/examples/imports-from.py b/src/bandit-main/bandit-main/examples/imports-from.py new file mode 100644 index 0000000..422ed10 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/imports-from.py @@ -0,0 +1,7 @@ +from subprocess import Popen + +from ..foo import sys +from . import sys +from .. import sys +from .. import subprocess +from ..subprocess import Popen diff --git a/src/bandit-main/bandit-main/examples/imports-function.py b/src/bandit-main/bandit-main/examples/imports-function.py new file mode 100644 index 0000000..bff8aa4 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/imports-function.py @@ -0,0 +1,12 @@ +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) diff --git a/src/bandit-main/bandit-main/examples/imports-with-importlib.py b/src/bandit-main/bandit-main/examples/imports-with-importlib.py new file mode 100644 index 0000000..748fc66 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/imports-with-importlib.py @@ -0,0 +1,15 @@ +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') diff --git a/src/bandit-main/bandit-main/examples/imports.py b/src/bandit-main/bandit-main/examples/imports.py new file mode 100644 index 0000000..6bfde58 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/imports.py @@ -0,0 +1,4 @@ +import os +import pickle +import sys +import subprocess diff --git a/src/bandit-main/bandit-main/examples/init-py-test/__init__.py b/src/bandit-main/bandit-main/examples/init-py-test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/bandit-main/bandit-main/examples/init-py-test/subdirectory-okay.py b/src/bandit-main/bandit-main/examples/init-py-test/subdirectory-okay.py new file mode 100644 index 0000000..8feea28 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/init-py-test/subdirectory-okay.py @@ -0,0 +1,3 @@ +# 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') diff --git a/src/bandit-main/bandit-main/examples/jinja2_templating.py b/src/bandit-main/bandit-main/examples/jinja2_templating.py new file mode 100644 index 0000000..0dce14d --- /dev/null +++ b/src/bandit-main/bandit-main/examples/jinja2_templating.py @@ -0,0 +1,29 @@ +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()) diff --git a/src/bandit-main/bandit-main/examples/jsonpickle.py b/src/bandit-main/bandit-main/examples/jsonpickle.py new file mode 100644 index 0000000..e8aa290 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/jsonpickle.py @@ -0,0 +1,10 @@ +import jsonpickle + + +pick = jsonpickle.encode({'a': 'b', 'c': 'd'}) + +print(jsonpickle.decode(pick)) + +print(jsonpickle.unpickler.decode(pick)) + +print(jsonpickle.unpickler.Unpickler().restore(pick)) diff --git a/src/bandit-main/bandit-main/examples/logging_config_insecure_listen.py b/src/bandit-main/bandit-main/examples/logging_config_insecure_listen.py new file mode 100644 index 0000000..e58cc5a --- /dev/null +++ b/src/bandit-main/bandit-main/examples/logging_config_insecure_listen.py @@ -0,0 +1,3 @@ +import logging.config + +t = logging.config.listen(9999) diff --git a/src/bandit-main/bandit-main/examples/long_set.py b/src/bandit-main/bandit-main/examples/long_set.py new file mode 100644 index 0000000..89f83c9 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/long_set.py @@ -0,0 +1,7279 @@ +# 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' +} diff --git a/src/bandit-main/bandit-main/examples/mako_templating.py b/src/bandit-main/bandit-main/examples/mako_templating.py new file mode 100644 index 0000000..29dd38c --- /dev/null +++ b/src/bandit-main/bandit-main/examples/mako_templating.py @@ -0,0 +1,11 @@ +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") diff --git a/src/bandit-main/bandit-main/examples/mark_safe.py b/src/bandit-main/bandit-main/examples/mark_safe.py new file mode 100644 index 0000000..f5abbd1 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/mark_safe.py @@ -0,0 +1,4 @@ +from django.utils import safestring + +mystr = 'Hello World' +mystr = safestring.mark_safe(mystr) diff --git a/src/bandit-main/bandit-main/examples/mark_safe_insecure.py b/src/bandit-main/bandit-main/examples/mark_safe_insecure.py new file mode 100644 index 0000000..b4e9f6b --- /dev/null +++ b/src/bandit-main/bandit-main/examples/mark_safe_insecure.py @@ -0,0 +1,167 @@ +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)) diff --git a/src/bandit-main/bandit-main/examples/mark_safe_secure.py b/src/bandit-main/bandit-main/examples/mark_safe_secure.py new file mode 100644 index 0000000..33a81ca --- /dev/null +++ b/src/bandit-main/bandit-main/examples/mark_safe_secure.py @@ -0,0 +1,75 @@ +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) diff --git a/src/bandit-main/bandit-main/examples/markupsafe_markup_xss.py b/src/bandit-main/bandit-main/examples/markupsafe_markup_xss.py new file mode 100644 index 0000000..23ee147 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/markupsafe_markup_xss.py @@ -0,0 +1,13 @@ +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 diff --git a/src/bandit-main/bandit-main/examples/markupsafe_markup_xss_allowed_calls.py b/src/bandit-main/bandit-main/examples/markupsafe_markup_xss_allowed_calls.py new file mode 100644 index 0000000..95519ec --- /dev/null +++ b/src/bandit-main/bandit-main/examples/markupsafe_markup_xss_allowed_calls.py @@ -0,0 +1,9 @@ +from bleach import clean +from markupsafe import Markup + +content = "" +Markup(clean(content)) + +# indirect assignments are currently not supported +cleaned = clean(content) +Markup(cleaned) diff --git a/src/bandit-main/bandit-main/examples/markupsafe_markup_xss_extend_markup_names.py b/src/bandit-main/bandit-main/examples/markupsafe_markup_xss_extend_markup_names.py new file mode 100644 index 0000000..2ae44c0 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/markupsafe_markup_xss_extend_markup_names.py @@ -0,0 +1,6 @@ +from markupsafe import Markup +from webhelpers.html import literal + +content = "" +Markup(f"unsafe {content}") +literal(f"unsafe {content}") diff --git a/src/bandit-main/bandit-main/examples/marshal_deserialize.py b/src/bandit-main/bandit-main/examples/marshal_deserialize.py new file mode 100644 index 0000000..2b077d4 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/marshal_deserialize.py @@ -0,0 +1,12 @@ +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() diff --git a/src/bandit-main/bandit-main/examples/mktemp.py b/src/bandit-main/bandit-main/examples/mktemp.py new file mode 100644 index 0000000..9912ad4 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/mktemp.py @@ -0,0 +1,10 @@ +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) diff --git a/src/bandit-main/bandit-main/examples/multiline_statement.py b/src/bandit-main/bandit-main/examples/multiline_statement.py new file mode 100644 index 0000000..278a3e4 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/multiline_statement.py @@ -0,0 +1,13 @@ +import subprocess + +subprocess.check_output("/some_command", + "args", + shell=True, + universal_newlines=True) + +subprocess.check_output( + "/some_command", + "args", + shell=True, + universal_newlines=True +) diff --git a/src/bandit-main/bandit-main/examples/new_candidates-all.py b/src/bandit-main/bandit-main/examples/new_candidates-all.py new file mode 100644 index 0000000..9d7bfc6 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/new_candidates-all.py @@ -0,0 +1,24 @@ +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 diff --git a/src/bandit-main/bandit-main/examples/new_candidates-none.py b/src/bandit-main/bandit-main/examples/new_candidates-none.py new file mode 100644 index 0000000..1ab1809 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/new_candidates-none.py @@ -0,0 +1,8 @@ +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 diff --git a/src/bandit-main/bandit-main/examples/new_candidates-nosec.py b/src/bandit-main/bandit-main/examples/new_candidates-nosec.py new file mode 100644 index 0000000..2d9f659 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/new_candidates-nosec.py @@ -0,0 +1,18 @@ +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 diff --git a/src/bandit-main/bandit-main/examples/new_candidates-some.py b/src/bandit-main/bandit-main/examples/new_candidates-some.py new file mode 100644 index 0000000..f1c3bab --- /dev/null +++ b/src/bandit-main/bandit-main/examples/new_candidates-some.py @@ -0,0 +1,20 @@ +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 diff --git a/src/bandit-main/bandit-main/examples/no_host_key_verification.py b/src/bandit-main/bandit-main/examples/no_host_key_verification.py new file mode 100644 index 0000000..1ac01e6 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/no_host_key_verification.py @@ -0,0 +1,14 @@ +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()) diff --git a/src/bandit-main/bandit-main/examples/nonsense.py b/src/bandit-main/bandit-main/examples/nonsense.py new file mode 100644 index 0000000..19cd186 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/nonsense.py @@ -0,0 +1 @@ +test(hi diff --git a/src/bandit-main/bandit-main/examples/nonsense2.py b/src/bandit-main/bandit-main/examples/nonsense2.py new file mode 100644 index 0000000..e28fc4f Binary files /dev/null and b/src/bandit-main/bandit-main/examples/nonsense2.py differ diff --git a/src/bandit-main/bandit-main/examples/nosec.py b/src/bandit-main/bandit-main/examples/nosec.py new file mode 100644 index 0000000..6ac72a3 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/nosec.py @@ -0,0 +1,18 @@ +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 diff --git a/src/bandit-main/bandit-main/examples/okay.py b/src/bandit-main/bandit-main/examples/okay.py new file mode 100644 index 0000000..9951ff7 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/okay.py @@ -0,0 +1 @@ +print('hopefully no vulnerabilities here') diff --git a/src/bandit-main/bandit-main/examples/os-chmod.py b/src/bandit-main/bandit-main/examples/os-chmod.py new file mode 100644 index 0000000..f7fff85 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/os-chmod.py @@ -0,0 +1,19 @@ +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) diff --git a/src/bandit-main/bandit-main/examples/os-exec.py b/src/bandit-main/bandit-main/examples/os-exec.py new file mode 100644 index 0000000..8cbe391 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/os-exec.py @@ -0,0 +1,11 @@ +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) + diff --git a/src/bandit-main/bandit-main/examples/os-popen.py b/src/bandit-main/bandit-main/examples/os-popen.py new file mode 100644 index 0000000..a944d1c --- /dev/null +++ b/src/bandit-main/bandit-main/examples/os-popen.py @@ -0,0 +1,15 @@ +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) diff --git a/src/bandit-main/bandit-main/examples/os-spawn.py b/src/bandit-main/bandit-main/examples/os-spawn.py new file mode 100644 index 0000000..b6ccc0e --- /dev/null +++ b/src/bandit-main/bandit-main/examples/os-spawn.py @@ -0,0 +1,10 @@ +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) diff --git a/src/bandit-main/bandit-main/examples/os-startfile.py b/src/bandit-main/bandit-main/examples/os-startfile.py new file mode 100644 index 0000000..4df534f --- /dev/null +++ b/src/bandit-main/bandit-main/examples/os-startfile.py @@ -0,0 +1,5 @@ +import os + +os.startfile('/bin/foo.docx') +os.startfile('/bin/bad.exe') +os.startfile('/bin/text.txt') diff --git a/src/bandit-main/bandit-main/examples/os_system.py b/src/bandit-main/bandit-main/examples/os_system.py new file mode 100644 index 0000000..af8bf54 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/os_system.py @@ -0,0 +1,3 @@ +import os + +os.system('/bin/echo hi') diff --git a/src/bandit-main/bandit-main/examples/pandas_read_pickle.py b/src/bandit-main/bandit-main/examples/pandas_read_pickle.py new file mode 100644 index 0000000..61174f6 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/pandas_read_pickle.py @@ -0,0 +1,12 @@ +import pickle +import pandas as pd + + +df = pd.DataFrame( + { + "col_A": [1, 2] + } +) +pick = pickle.dumps(df) + +print(pd.read_pickle(pick)) diff --git a/src/bandit-main/bandit-main/examples/paramiko_injection.py b/src/bandit-main/bandit-main/examples/paramiko_injection.py new file mode 100644 index 0000000..abce4f8 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/paramiko_injection.py @@ -0,0 +1,10 @@ +import paramiko + + +client = paramiko.client.SSHClient() + +# this is not safe +client.exec_command('something; really; unsafe') + +# this is safe +client.connect('somehost') diff --git a/src/bandit-main/bandit-main/examples/partial_path_process.py b/src/bandit-main/bandit-main/examples/partial_path_process.py new file mode 100644 index 0000000..7903048 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/partial_path_process.py @@ -0,0 +1,13 @@ +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) diff --git a/src/bandit-main/bandit-main/examples/pickle_deserialize.py b/src/bandit-main/bandit-main/examples/pickle_deserialize.py new file mode 100644 index 0000000..3c56c80 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/pickle_deserialize.py @@ -0,0 +1,15 @@ +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()) diff --git a/src/bandit-main/bandit-main/examples/popen_wrappers.py b/src/bandit-main/bandit-main/examples/popen_wrappers.py new file mode 100644 index 0000000..5834cbd --- /dev/null +++ b/src/bandit-main/bandit-main/examples/popen_wrappers.py @@ -0,0 +1,15 @@ +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()) diff --git a/src/bandit-main/bandit-main/examples/pycrypto.py b/src/bandit-main/bandit-main/examples/pycrypto.py new file mode 100644 index 0000000..fe8de07 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/pycrypto.py @@ -0,0 +1,11 @@ +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() diff --git a/src/bandit-main/bandit-main/examples/pycryptodome.py b/src/bandit-main/bandit-main/examples/pycryptodome.py new file mode 100644 index 0000000..1fc22f2 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/pycryptodome.py @@ -0,0 +1,11 @@ +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() diff --git a/src/bandit-main/bandit-main/examples/pyghmi.py b/src/bandit-main/bandit-main/examples/pyghmi.py new file mode 100644 index 0000000..44eb197 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/pyghmi.py @@ -0,0 +1,5 @@ +from pyghmi.ipmi import command + +cmd = command.Command(bmc="bmc", + userid="userid", + password="ZjE4ZjI0NTE4YmI2NGJjZDliOGY3ZmJiY2UyN2IzODQK") diff --git a/src/bandit-main/bandit-main/examples/pytorch_load.py b/src/bandit-main/bandit-main/examples/pytorch_load.py new file mode 100644 index 0000000..c5129a0 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/pytorch_load.py @@ -0,0 +1,26 @@ +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)) diff --git a/src/bandit-main/bandit-main/examples/random_module.py b/src/bandit-main/bandit-main/examples/random_module.py new file mode 100644 index 0000000..f0b6d01 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/random_module.py @@ -0,0 +1,22 @@ +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() diff --git a/src/bandit-main/bandit-main/examples/requests-missing-timeout.py b/src/bandit-main/bandit-main/examples/requests-missing-timeout.py new file mode 100644 index 0000000..fa71c4b --- /dev/null +++ b/src/bandit-main/bandit-main/examples/requests-missing-timeout.py @@ -0,0 +1,68 @@ +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') diff --git a/src/bandit-main/bandit-main/examples/requests-ssl-verify-disabled.py b/src/bandit-main/bandit-main/examples/requests-ssl-verify-disabled.py new file mode 100644 index 0000000..c45b9e9 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/requests-ssl-verify-disabled.py @@ -0,0 +1,42 @@ +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) diff --git a/src/bandit-main/bandit-main/examples/shelve_open.py b/src/bandit-main/bandit-main/examples/shelve_open.py new file mode 100644 index 0000000..4345a87 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/shelve_open.py @@ -0,0 +1,12 @@ +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']) diff --git a/src/bandit-main/bandit-main/examples/skip.py b/src/bandit-main/bandit-main/examples/skip.py new file mode 100644 index 0000000..2549485 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/skip.py @@ -0,0 +1,7 @@ +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"]) diff --git a/src/bandit-main/bandit-main/examples/snmp.py b/src/bandit-main/bandit-main/examples/snmp.py new file mode 100644 index 0000000..a24a90b --- /dev/null +++ b/src/bandit-main/bandit-main/examples/snmp.py @@ -0,0 +1,10 @@ +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") diff --git a/src/bandit-main/bandit-main/examples/sql_multiline_statements.py b/src/bandit-main/bandit-main/examples/sql_multiline_statements.py new file mode 100644 index 0000000..663446e --- /dev/null +++ b/src/bandit-main/bandit-main/examples/sql_multiline_statements.py @@ -0,0 +1,182 @@ +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 diff --git a/src/bandit-main/bandit-main/examples/sql_statements.py b/src/bandit-main/bandit-main/examples/sql_statements.py new file mode 100644 index 0000000..53a5306 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/sql_statements.py @@ -0,0 +1,47 @@ +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 = "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("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"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) + +# real world false positives +choices=[('server_list', _("Select from active instances"))] +print("delete from the cache as the first argument") diff --git a/src/bandit-main/bandit-main/examples/ssl-insecure-version.py b/src/bandit-main/bandit-main/examples/ssl-insecure-version.py new file mode 100644 index 0000000..80d6e48 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/ssl-insecure-version.py @@ -0,0 +1,46 @@ +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 diff --git a/src/bandit-main/bandit-main/examples/subprocess_shell.py b/src/bandit-main/bandit-main/examples/subprocess_shell.py new file mode 100644 index 0000000..38944d5 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/subprocess_shell.py @@ -0,0 +1,60 @@ +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) diff --git a/src/bandit-main/bandit-main/examples/tarfile_extractall.py b/src/bandit-main/bandit-main/examples/tarfile_extractall.py new file mode 100644 index 0000000..b32736a --- /dev/null +++ b/src/bandit-main/bandit-main/examples/tarfile_extractall.py @@ -0,0 +1,61 @@ +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) diff --git a/src/bandit-main/bandit-main/examples/telnetlib.py b/src/bandit-main/bandit-main/examples/telnetlib.py new file mode 100644 index 0000000..3b06e8d --- /dev/null +++ b/src/bandit-main/bandit-main/examples/telnetlib.py @@ -0,0 +1,19 @@ +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()) diff --git a/src/bandit-main/bandit-main/examples/trojansource.py b/src/bandit-main/bandit-main/examples/trojansource.py new file mode 100644 index 0000000..40c6055 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/trojansource.py @@ -0,0 +1,5 @@ +#!/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") diff --git a/src/bandit-main/bandit-main/examples/trojansource_latin1.py b/src/bandit-main/bandit-main/examples/trojansource_latin1.py new file mode 100644 index 0000000..dee24e0 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/trojansource_latin1.py @@ -0,0 +1,7 @@ +#!/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") diff --git a/src/bandit-main/bandit-main/examples/try_except_continue.py b/src/bandit-main/bandit-main/examples/try_except_continue.py new file mode 100644 index 0000000..51c232c --- /dev/null +++ b/src/bandit-main/bandit-main/examples/try_except_continue.py @@ -0,0 +1,32 @@ +# 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 diff --git a/src/bandit-main/bandit-main/examples/try_except_pass.py b/src/bandit-main/bandit-main/examples/try_except_pass.py new file mode 100644 index 0000000..2ebda55 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/try_except_pass.py @@ -0,0 +1,36 @@ +# 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 diff --git a/src/bandit-main/bandit-main/examples/unverified_context.py b/src/bandit-main/bandit-main/examples/unverified_context.py new file mode 100644 index 0000000..0f45439 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/unverified_context.py @@ -0,0 +1,7 @@ +import ssl + +# Correct +context = ssl.create_default_context() + +# Incorrect: unverified context +context = ssl._create_unverified_context() diff --git a/src/bandit-main/bandit-main/examples/urlopen.py b/src/bandit-main/bandit-main/examples/urlopen.py new file mode 100644 index 0000000..2a0f8b7 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/urlopen.py @@ -0,0 +1,34 @@ +''' 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') diff --git a/src/bandit-main/bandit-main/examples/weak_cryptographic_key_sizes.py b/src/bandit-main/bandit-main/examples/weak_cryptographic_key_sizes.py new file mode 100644 index 0000000..357a71e --- /dev/null +++ b/src/bandit-main/bandit-main/examples/weak_cryptographic_key_sizes.py @@ -0,0 +1,72 @@ +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() +) diff --git a/src/bandit-main/bandit-main/examples/wildcard-injection.py b/src/bandit-main/bandit-main/examples/wildcard-injection.py new file mode 100644 index 0000000..352c171 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/wildcard-injection.py @@ -0,0 +1,16 @@ +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', '*']) diff --git a/src/bandit-main/bandit-main/examples/xml_etree_celementtree.py b/src/bandit-main/bandit-main/examples/xml_etree_celementtree.py new file mode 100644 index 0000000..d3fc382 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/xml_etree_celementtree.py @@ -0,0 +1,18 @@ +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() diff --git a/src/bandit-main/bandit-main/examples/xml_etree_elementtree.py b/src/bandit-main/bandit-main/examples/xml_etree_elementtree.py new file mode 100644 index 0000000..f7a3775 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/xml_etree_elementtree.py @@ -0,0 +1,18 @@ +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() diff --git a/src/bandit-main/bandit-main/examples/xml_expatbuilder.py b/src/bandit-main/bandit-main/examples/xml_expatbuilder.py new file mode 100644 index 0000000..1deb857 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/xml_expatbuilder.py @@ -0,0 +1,10 @@ +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) diff --git a/src/bandit-main/bandit-main/examples/xml_expatreader.py b/src/bandit-main/bandit-main/examples/xml_expatreader.py new file mode 100644 index 0000000..12e6f96 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/xml_expatreader.py @@ -0,0 +1,5 @@ +import xml.sax.expatreader as bad +import defusedxml.expatreader as good + +p = bad.create_parser() +b = good.create_parser() diff --git a/src/bandit-main/bandit-main/examples/xml_minidom.py b/src/bandit-main/bandit-main/examples/xml_minidom.py new file mode 100644 index 0000000..40e7aea --- /dev/null +++ b/src/bandit-main/bandit-main/examples/xml_minidom.py @@ -0,0 +1,14 @@ +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) diff --git a/src/bandit-main/bandit-main/examples/xml_pulldom.py b/src/bandit-main/bandit-main/examples/xml_pulldom.py new file mode 100644 index 0000000..d310115 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/xml_pulldom.py @@ -0,0 +1,14 @@ +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) diff --git a/src/bandit-main/bandit-main/examples/xml_sax.py b/src/bandit-main/bandit-main/examples/xml_sax.py new file mode 100644 index 0000000..d12eeb6 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/xml_sax.py @@ -0,0 +1,37 @@ +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() diff --git a/src/bandit-main/bandit-main/examples/xml_xmlrpc.py b/src/bandit-main/bandit-main/examples/xml_xmlrpc.py new file mode 100644 index 0000000..d98fda6 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/xml_xmlrpc.py @@ -0,0 +1,10 @@ +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() diff --git a/src/bandit-main/bandit-main/examples/yaml_load.py b/src/bandit-main/bandit-main/examples/yaml_load.py new file mode 100644 index 0000000..a6a2eb1 --- /dev/null +++ b/src/bandit-main/bandit-main/examples/yaml_load.py @@ -0,0 +1,28 @@ +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) diff --git a/src/bandit-main/bandit-main/funding.json b/src/bandit-main/bandit-main/funding.json new file mode 100644 index 0000000..a7831ac --- /dev/null +++ b/src/bandit-main/bandit-main/funding.json @@ -0,0 +1,65 @@ +{ + "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"] + } + ] + } +} diff --git a/src/bandit-main/bandit-main/logo/logo.svg b/src/bandit-main/bandit-main/logo/logo.svg new file mode 100644 index 0000000..0204d32 --- /dev/null +++ b/src/bandit-main/bandit-main/logo/logo.svg @@ -0,0 +1,51 @@ + + + + + + + + + + diff --git a/src/bandit-main/bandit-main/logo/logomark-singlecolor.png b/src/bandit-main/bandit-main/logo/logomark-singlecolor.png new file mode 100644 index 0000000..00c9314 Binary files /dev/null and b/src/bandit-main/bandit-main/logo/logomark-singlecolor.png differ diff --git a/src/bandit-main/bandit-main/logo/logomark.png b/src/bandit-main/bandit-main/logo/logomark.png new file mode 100644 index 0000000..8a6b1be Binary files /dev/null and b/src/bandit-main/bandit-main/logo/logomark.png differ diff --git a/src/bandit-main/bandit-main/logo/logotype-singlecolor.png b/src/bandit-main/bandit-main/logo/logotype-singlecolor.png new file mode 100644 index 0000000..3276c68 Binary files /dev/null and b/src/bandit-main/bandit-main/logo/logotype-singlecolor.png differ diff --git a/src/bandit-main/bandit-main/logo/logotype-sm.png b/src/bandit-main/bandit-main/logo/logotype-sm.png new file mode 100644 index 0000000..85f1d22 Binary files /dev/null and b/src/bandit-main/bandit-main/logo/logotype-sm.png differ diff --git a/src/bandit-main/bandit-main/logo/logotype.png b/src/bandit-main/bandit-main/logo/logotype.png new file mode 100644 index 0000000..3276c68 Binary files /dev/null and b/src/bandit-main/bandit-main/logo/logotype.png differ diff --git a/src/bandit-main/bandit-main/pylintrc b/src/bandit-main/bandit-main/pylintrc new file mode 100644 index 0000000..17952a2 --- /dev/null +++ b/src/bandit-main/bandit-main/pylintrc @@ -0,0 +1,97 @@ +# 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 diff --git a/src/bandit-main/bandit-main/requirements.txt b/src/bandit-main/bandit-main/requirements.txt new file mode 100644 index 0000000..3bc8915 --- /dev/null +++ b/src/bandit-main/bandit-main/requirements.txt @@ -0,0 +1,7 @@ +# 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 diff --git a/src/bandit-main/bandit-main/scripts/main.py b/src/bandit-main/bandit-main/scripts/main.py new file mode 100644 index 0000000..be7223f --- /dev/null +++ b/src/bandit-main/bandit-main/scripts/main.py @@ -0,0 +1,8 @@ +#!/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() diff --git a/src/bandit-main/bandit-main/setup.cfg b/src/bandit-main/bandit-main/setup.cfg new file mode 100644 index 0000000..e1bab07 --- /dev/null +++ b/src/bandit-main/bandit-main/setup.cfg @@ -0,0 +1,177 @@ +[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.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3.13 + 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* diff --git a/src/bandit-main/bandit-main/setup.py b/src/bandit-main/bandit-main/setup.py new file mode 100644 index 0000000..f70f236 --- /dev/null +++ b/src/bandit-main/bandit-main/setup.py @@ -0,0 +1,20 @@ +# 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.9", + setup_requires=["pbr>=2.0.0"], + pbr=True, + data_files=data_files, +) diff --git a/src/bandit-main/bandit-main/test-requirements.txt b/src/bandit-main/bandit-main/test-requirements.txt new file mode 100644 index 0000000..fb00e57 --- /dev/null +++ b/src/bandit-main/bandit-main/test-requirements.txt @@ -0,0 +1,11 @@ +# 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 diff --git a/src/bandit-main/bandit-main/tests/__init__.py b/src/bandit-main/bandit-main/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/bandit-main/bandit-main/tests/functional/__init__.py b/src/bandit-main/bandit-main/tests/functional/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/bandit-main/bandit-main/tests/functional/test_baseline.py b/src/bandit-main/bandit-main/tests/functional/test_baseline.py new file mode 100644 index 0000000..d3df4f5 --- /dev/null +++ b/src/bandit-main/bandit-main/tests/functional/test_baseline.py @@ -0,0 +1,341 @@ +# 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) diff --git a/src/bandit-main/bandit-main/tests/functional/test_functional.py b/src/bandit-main/bandit-main/tests/functional/test_functional.py new file mode 100644 index 0000000..5f21e2d --- /dev/null +++ b/src/bandit-main/bandit-main/tests/functional/test_functional.py @@ -0,0 +1,935 @@ +# +# 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": 14, "MEDIUM": 0, "HIGH": 0}, + "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 14, "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": 20, + "HIGH": 0, + }, + "CONFIDENCE": { + "UNDEFINED": 0, + "LOW": 10, + "MEDIUM": 10, + "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) diff --git a/src/bandit-main/bandit-main/tests/functional/test_runtime.py b/src/bandit-main/bandit-main/tests/functional/test_runtime.py new file mode 100644 index 0000000..a9eb216 --- /dev/null +++ b/src/bandit-main/bandit-main/tests/functional/test_runtime.py @@ -0,0 +1,136 @@ +# 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) diff --git a/src/bandit-main/bandit-main/tests/unit/__init__.py b/src/bandit-main/bandit-main/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/bandit-main/bandit-main/tests/unit/cli/__init__.py b/src/bandit-main/bandit-main/tests/unit/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/bandit-main/bandit-main/tests/unit/cli/test_baseline.py b/src/bandit-main/bandit-main/tests/unit/cli/test_baseline.py new file mode 100644 index 0000000..0f111cb --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/cli/test_baseline.py @@ -0,0 +1,301 @@ +# +# 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) diff --git a/src/bandit-main/bandit-main/tests/unit/cli/test_config_generator.py b/src/bandit-main/bandit-main/tests/unit/cli/test_config_generator.py new file mode 100644 index 0000000..9a2a76f --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/cli/test_config_generator.py @@ -0,0 +1,87 @@ +# +# 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) diff --git a/src/bandit-main/bandit-main/tests/unit/cli/test_main.py b/src/bandit-main/bandit-main/tests/unit/cli/test_main.py new file mode 100644 index 0000000..98b95ec --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/cli/test_main.py @@ -0,0 +1,328 @@ +# 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) diff --git a/src/bandit-main/bandit-main/tests/unit/core/__init__.py b/src/bandit-main/bandit-main/tests/unit/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/bandit-main/bandit-main/tests/unit/core/test_blacklisting.py b/src/bandit-main/bandit-main/tests/unit/core/test_blacklisting.py new file mode 100644 index 0000000..5e17760 --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/core/test_blacklisting.py @@ -0,0 +1,33 @@ +# +# 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"]) diff --git a/src/bandit-main/bandit-main/tests/unit/core/test_config.py b/src/bandit-main/bandit-main/tests/unit/core/test_config.py new file mode 100644 index 0000000..16aed07 --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/core/test_config.py @@ -0,0 +1,319 @@ +# 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" diff --git a/src/bandit-main/bandit-main/tests/unit/core/test_context.py b/src/bandit-main/bandit-main/tests/unit/core/test_context.py new file mode 100644 index 0000000..23b3436 --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/core/test_context.py @@ -0,0 +1,259 @@ +# +# 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.Num(42) + expected = value.n + self.assertEqual(expected, new_context._get_literal_value(value)) + + value = ast.Str("spam") + expected = value.s + self.assertEqual(expected, new_context._get_literal_value(value)) + + value = ast.List([ast.Str("spam"), ast.Num(42)], ast.Load()) + expected = [ast.Str("spam").s, ast.Num(42).n] + self.assertListEqual(expected, new_context._get_literal_value(value)) + + value = ast.Tuple([ast.Str("spam"), ast.Num(42)], ast.Load()) + expected = (ast.Str("spam").s, ast.Num(42).n) + self.assertTupleEqual(expected, new_context._get_literal_value(value)) + + value = ast.Set([ast.Str("spam"), ast.Num(42)]) + expected = {ast.Str("spam").s, ast.Num(42).n} + self.assertSetEqual(expected, new_context._get_literal_value(value)) + + value = ast.Dict(["spam", "eggs"], [42, "foo"]) + expected = dict(spam=42, eggs="foo") + self.assertDictEqual(expected, new_context._get_literal_value(value)) + + value = ast.Ellipsis() + self.assertIsNone(new_context._get_literal_value(value)) + + value = ast.Name("spam", ast.Load()) + expected = value.id + self.assertEqual(expected, new_context._get_literal_value(value)) + + value = ast.Bytes(b"spam") + expected = value.s + self.assertEqual(expected, new_context._get_literal_value(value)) + + self.assertIsNone(new_context._get_literal_value(None)) + + @mock.patch( + "bandit.core.context.Context.call_keywords", + new_callable=mock.PropertyMock, + ) + def test_check_call_arg_value(self, call_keywords): + new_context = context.Context() + call_keywords.return_value = dict(spam="eggs") + self.assertTrue(new_context.check_call_arg_value("spam", "eggs")) + self.assertTrue( + new_context.check_call_arg_value("spam", ["spam", "eggs"]) + ) + self.assertFalse(new_context.check_call_arg_value("spam", "spam")) + self.assertFalse(new_context.check_call_arg_value("spam")) + self.assertFalse(new_context.check_call_arg_value("eggs")) + + new_context = context.Context() + self.assertIsNone(new_context.check_call_arg_value(None)) + + @mock.patch( + "bandit.core.context.Context.node", new_callable=mock.PropertyMock + ) + def test_get_lineno_for_call_arg(self, node): + expected_lineno = 42 + keyword1 = mock.Mock( + arg="spam", value=mock.Mock(lineno=expected_lineno) + ) + node.return_value = mock.Mock(keywords=[keyword1]) + new_context = context.Context() + actual_lineno = new_context.get_lineno_for_call_arg("spam") + self.assertEqual(expected_lineno, actual_lineno) + + new_context = context.Context() + missing_lineno = new_context.get_lineno_for_call_arg("eggs") + self.assertIsNone(missing_lineno) + + def test_get_call_arg_at_position(self): + expected_arg = "spam" + ref_call = mock.Mock() + ref_call.args = [ast.Str(expected_arg)] + ref_context = dict(call=ref_call) + new_context = context.Context(context_object=ref_context) + self.assertEqual(expected_arg, new_context.get_call_arg_at_position(0)) + self.assertIsNone(new_context.get_call_arg_at_position(1)) + + ref_call = mock.Mock() + ref_call.args = [] + ref_context = dict(call=ref_call) + new_context = context.Context(context_object=ref_context) + self.assertIsNone(new_context.get_call_arg_at_position(0)) + + new_context = context.Context() + self.assertIsNone(new_context.get_call_arg_at_position(0)) + + def test_is_module_being_imported(self): + ref_context = dict(module="spam") + new_context = context.Context(context_object=ref_context) + self.assertTrue(new_context.is_module_being_imported("spam")) + self.assertFalse(new_context.is_module_being_imported("eggs")) + + new_context = context.Context() + self.assertFalse(new_context.is_module_being_imported("spam")) + + def test_is_module_imported_exact(self): + ref_context = dict(imports=["spam"]) + new_context = context.Context(context_object=ref_context) + self.assertTrue(new_context.is_module_imported_exact("spam")) + self.assertFalse(new_context.is_module_imported_exact("eggs")) + + new_context = context.Context() + self.assertFalse(new_context.is_module_being_imported("spam")) + + def test_is_module_imported_like(self): + ref_context = dict(imports=[["spam"], ["eggs"]]) + new_context = context.Context(context_object=ref_context) + self.assertTrue(new_context.is_module_imported_like("spam")) + self.assertFalse(new_context.is_module_imported_like("bacon")) + + new_context = context.Context() + self.assertFalse(new_context.is_module_imported_like("spam")) + + 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) diff --git a/src/bandit-main/bandit-main/tests/unit/core/test_docs_util.py b/src/bandit-main/bandit-main/tests/unit/core/test_docs_util.py new file mode 100644 index 0000000..2891f7a --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/core/test_docs_util.py @@ -0,0 +1,30 @@ +# 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")) diff --git a/src/bandit-main/bandit-main/tests/unit/core/test_issue.py b/src/bandit-main/bandit-main/tests/unit/core/test_issue.py new file mode 100644 index 0000000..629c501 --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/core/test_issue.py @@ -0,0 +1,146 @@ +# +# 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 diff --git a/src/bandit-main/bandit-main/tests/unit/core/test_manager.py b/src/bandit-main/bandit-main/tests/unit/core/test_manager.py new file mode 100644 index 0000000..5d20c56 --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/core/test_manager.py @@ -0,0 +1,395 @@ +# +# 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] + ), + ) diff --git a/src/bandit-main/bandit-main/tests/unit/core/test_meta_ast.py b/src/bandit-main/bandit-main/tests/unit/core/test_meta_ast.py new file mode 100644 index 0000000..10f4b1d --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/core/test_meta_ast.py @@ -0,0 +1,30 @@ +# 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)) diff --git a/src/bandit-main/bandit-main/tests/unit/core/test_test_set.py b/src/bandit-main/bandit-main/tests/unit/core/test_test_set.py new file mode 100644 index 0000000..77c5f88 --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/core/test_test_set.py @@ -0,0 +1,169 @@ +# +# 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"])) diff --git a/src/bandit-main/bandit-main/tests/unit/core/test_util.py b/src/bandit-main/bandit-main/tests/unit/core/test_util.py new file mode 100644 index 0000000..2747eef --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/core/test_util.py @@ -0,0 +1,342 @@ +# +# 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") diff --git a/src/bandit-main/bandit-main/tests/unit/formatters/__init__.py b/src/bandit-main/bandit-main/tests/unit/formatters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/bandit-main/bandit-main/tests/unit/formatters/test_csv.py b/src/bandit-main/bandit-main/tests/unit/formatters/test_csv.py new file mode 100644 index 0000000..99f4c4b --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/formatters/test_csv.py @@ -0,0 +1,72 @@ +# 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"] + ) diff --git a/src/bandit-main/bandit-main/tests/unit/formatters/test_custom.py b/src/bandit-main/bandit-main/tests/unit/formatters/test_custom.py new file mode 100644 index 0000000..5481b71 --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/formatters/test_custom.py @@ -0,0 +1,65 @@ +# 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"]) diff --git a/src/bandit-main/bandit-main/tests/unit/formatters/test_html.py b/src/bandit-main/bandit-main/tests/unit/formatters/test_html.py new file mode 100644 index 0000000..07e6bd0 --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/formatters/test_html.py @@ -0,0 +1,159 @@ +# 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 diff --git a/src/bandit-main/bandit-main/tests/unit/formatters/test_json.py b/src/bandit-main/bandit-main/tests/unit/formatters/test_json.py new file mode 100644 index 0000000..821e8e5 --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/formatters/test_json.py @@ -0,0 +1,115 @@ +# 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"]) diff --git a/src/bandit-main/bandit-main/tests/unit/formatters/test_sarif.py b/src/bandit-main/bandit-main/tests/unit/formatters/test_sarif.py new file mode 100644 index 0000000..a5306fa --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/formatters/test_sarif.py @@ -0,0 +1,139 @@ +# 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"], + ) diff --git a/src/bandit-main/bandit-main/tests/unit/formatters/test_screen.py b/src/bandit-main/bandit-main/tests/unit/formatters/test_screen.py new file mode 100644 index 0000000..b560247 --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/formatters/test_screen.py @@ -0,0 +1,244 @@ +# 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 diff --git a/src/bandit-main/bandit-main/tests/unit/formatters/test_text.py b/src/bandit-main/bandit-main/tests/unit/formatters/test_text.py new file mode 100644 index 0000000..a868c34 --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/formatters/test_text.py @@ -0,0 +1,221 @@ +# 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 diff --git a/src/bandit-main/bandit-main/tests/unit/formatters/test_xml.py b/src/bandit-main/bandit-main/tests/unit/formatters/test_xml.py new file mode 100644 index 0000000..ac50397 --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/formatters/test_xml.py @@ -0,0 +1,87 @@ +# 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"] + ) diff --git a/src/bandit-main/bandit-main/tests/unit/formatters/test_yaml.py b/src/bandit-main/bandit-main/tests/unit/formatters/test_yaml.py new file mode 100644 index 0000000..089e1ac --- /dev/null +++ b/src/bandit-main/bandit-main/tests/unit/formatters/test_yaml.py @@ -0,0 +1,103 @@ +# 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"]) diff --git a/src/bandit-main/bandit-main/tox.ini b/src/bandit-main/bandit-main/tox.ini new file mode 100644 index 0000000..60b8b85 --- /dev/null +++ b/src/bandit-main/bandit-main/tox.ini @@ -0,0 +1,91 @@ +[tox] +minversion = 3.2.0 +envlist = py39,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 diff --git a/src/flake8-main/flake8-main/.bandit.yml b/src/flake8-main/flake8-main/.bandit.yml new file mode 100644 index 0000000..759a506 --- /dev/null +++ b/src/flake8-main/flake8-main/.bandit.yml @@ -0,0 +1,5 @@ +skips: +- B101 # Ignore defensive `assert`s (especially useful for mypy) +- B404 # Ignore warnings about importing subprocess +- B603 # Ignore warnings about calling subprocess.Popen without shell=True +- B607 # Ignore warnings about calling subprocess.Popen without a full path to executable diff --git a/src/flake8-main/flake8-main/.github/FUNDING.yml b/src/flake8-main/flake8-main/.github/FUNDING.yml new file mode 100644 index 0000000..1ac2512 --- /dev/null +++ b/src/flake8-main/flake8-main/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: asottile +tidelift: pypi/flake8 diff --git a/src/flake8-main/flake8-main/.github/ISSUE_TEMPLATE/01_bug.yml b/src/flake8-main/flake8-main/.github/ISSUE_TEMPLATE/01_bug.yml new file mode 100644 index 0000000..3005b8b --- /dev/null +++ b/src/flake8-main/flake8-main/.github/ISSUE_TEMPLATE/01_bug.yml @@ -0,0 +1,86 @@ +name: bug report +description: something went wrong +body: + - type: markdown + attributes: + value: > + Please read this brief portion of documentation before going any + further: + https://flake8.pycqa.org/en/latest/internal/contributing.html#filing-a-bug + + - type: markdown + attributes: + value: > + **NOTE: flake8 is a linting framework and does not implement any + checks** + + - type: markdown + attributes: + value: > + _if you are reporting a problem with a particular check, please track + down the plugin which implements that check_ + + - type: textarea + id: install + attributes: + label: how did you install flake8? + description: 'note: this will be rendered as ```console automatically' + placeholder: | + $ pip install flake8 # or `brew install flake8` etc. + Collecting flake8 + ... + Successfully installed flake8... + render: console + validations: + required: true + + - type: markdown + attributes: + value: > + **Note**: Some *nix distributions patch Flake8 arbitrarily to + accommodate incompatible software versions. If you're on one of those + distributions, your issue may be closed and you will be asked to open + an issue with your distribution package maintainers instead. + + - type: textarea + id: bug-report + attributes: + label: unmodified output of `flake8 --bug-report` + description: 'note: this will be rendered as ```json automatically' + placeholder: | + { + "platform": { + "...": "... + } + } + render: json + validations: + required: true + + - type: textarea + id: freeform + attributes: + label: describe the problem + description: > + please provide **sample code** and **directions for reproducing + your problem** including the **commands you ran**, their + **unedited output**, and **what you expected to happen** + value: | + #### what I expected to happen + + ... + + #### sample code + + ```python + print('hello world!') + ``` + + #### commands ran + + ```console + $ flake8 t.py + ... + ``` + validations: + required: true diff --git a/src/flake8-main/flake8-main/.github/ISSUE_TEMPLATE/02_feature.yml b/src/flake8-main/flake8-main/.github/ISSUE_TEMPLATE/02_feature.yml new file mode 100644 index 0000000..1447f06 --- /dev/null +++ b/src/flake8-main/flake8-main/.github/ISSUE_TEMPLATE/02_feature.yml @@ -0,0 +1,27 @@ +name: feature request +description: a new feature! +body: + - type: markdown + attributes: + value: > + Please read this brief portion of documentation before going any + further: + https://flake8.pycqa.org/en/latest/internal/contributing.html#filing-a-bug + + - type: markdown + attributes: + value: '**NOTE: flake8 is a linting framework and does not implement any checks**' + + - type: markdown + attributes: + value: '**NOTE: if you ask about `pyproject.toml` your issue will be closed as a duplicate of [#234](https://github.com/PyCQA/flake8/issues/234)**' + + - type: textarea + id: freeform + attributes: + label: describe the request + description: > + please describe your use case and why the current feature set does + not satisfy your needs + validations: + required: true diff --git a/src/flake8-main/flake8-main/.github/ISSUE_TEMPLATE/config.yml b/src/flake8-main/flake8-main/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..44fec6d --- /dev/null +++ b/src/flake8-main/flake8-main/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: problem with E___ or W___ codes + url: https://github.com/PyCQA/pycodestyle/issues + about: flake8 does not implement any checks, perhaps you want pycodestyle? + - name: problem with F___ codes + url: https://github.com/PyCQA/pyflakes/issues + about: flake8 does not implement any checks, perhaps you want pyflakes? + - name: problem with C___ codes + url: https://github.com/PyCQA/mccabe/issues + about: flake8 does not implement any checks, perhaps you want mccabe? diff --git a/src/flake8-main/flake8-main/.github/SECURITY.md b/src/flake8-main/flake8-main/.github/SECURITY.md new file mode 100644 index 0000000..5885648 --- /dev/null +++ b/src/flake8-main/flake8-main/.github/SECURITY.md @@ -0,0 +1,5 @@ +## security contact information + +to report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. diff --git a/src/flake8-main/flake8-main/.github/workflows/main.yml b/src/flake8-main/flake8-main/.github/workflows/main.yml new file mode 100644 index 0000000..08f54ea --- /dev/null +++ b/src/flake8-main/flake8-main/.github/workflows/main.yml @@ -0,0 +1,54 @@ +name: main + +on: + push: + branches: [main] + tags: + pull_request: + +jobs: + main: + strategy: + matrix: + include: + # linux + - os: ubuntu-latest + python: pypy-3.9 + toxenv: py + - os: ubuntu-latest + python: 3.9 + toxenv: py + - os: ubuntu-latest + python: '3.10' + toxenv: py + - os: ubuntu-latest + python: '3.11' + toxenv: py + - os: ubuntu-latest + python: '3.12' + toxenv: py + - os: ubuntu-latest + python: '3.13' + toxenv: py + # windows + - os: windows-latest + python: 3.9 + toxenv: py + # misc + - os: ubuntu-latest + python: '3.10' + toxenv: docs + - os: ubuntu-latest + python: '3.10' + toxenv: linters + - os: ubuntu-latest + python: '3.10' + toxenv: dogfood + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - run: python -mpip install --upgrade setuptools pip tox virtualenv + - run: tox -e ${{ matrix.toxenv }} diff --git a/src/flake8-main/flake8-main/.gitignore b/src/flake8-main/flake8-main/.gitignore new file mode 100644 index 0000000..c0f628a --- /dev/null +++ b/src/flake8-main/flake8-main/.gitignore @@ -0,0 +1,15 @@ +*.egg +*.egg-info +*.log +*.pyc +*.sw* +*.zip +.cache +.coverage +.coverage.* +.eggs +.tox +/.mypy_cache +build +dist +docs/build/html/* diff --git a/src/flake8-main/flake8-main/.mailmap b/src/flake8-main/flake8-main/.mailmap new file mode 100644 index 0000000..07d8d50 --- /dev/null +++ b/src/flake8-main/flake8-main/.mailmap @@ -0,0 +1,4 @@ +Ian Stapleton Cordasco Ian Cordasco +Ian Stapleton Cordasco Ian Cordasco +Ian Stapleton Cordasco Ian Cordasco +Ian Stapleton Cordasco Ian Cordasco diff --git a/src/flake8-main/flake8-main/.pre-commit-config.yaml b/src/flake8-main/flake8-main/.pre-commit-config.yaml new file mode 100644 index 0000000..30581db --- /dev/null +++ b/src/flake8-main/flake8-main/.pre-commit-config.yaml @@ -0,0 +1,44 @@ +repos: +- repo: https://github.com/asottile/add-trailing-comma + rev: v3.2.0 + hooks: + - id: add-trailing-comma +- 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 + exclude: ^tests/fixtures/ +- repo: https://github.com/asottile/setup-cfg-fmt + rev: v2.8.0 + hooks: + - id: setup-cfg-fmt +- repo: https://github.com/asottile/reorder-python-imports + rev: v3.15.0 + hooks: + - id: reorder-python-imports + args: [ + --application-directories, '.:src', + --py39-plus, + --add-import, 'from __future__ import annotations', + ] +- repo: https://github.com/asottile/pyupgrade + rev: v3.20.0 + hooks: + - id: pyupgrade + args: [--py39-plus] +- repo: https://github.com/hhatto/autopep8 + rev: v2.3.2 + hooks: + - id: autopep8 +- repo: https://github.com/PyCQA/flake8 + rev: 7.3.0 + hooks: + - id: flake8 +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.17.1 + hooks: + - id: mypy + exclude: ^(docs/|example-plugin/) diff --git a/src/flake8-main/flake8-main/.pre-commit-hooks.yaml b/src/flake8-main/flake8-main/.pre-commit-hooks.yaml new file mode 100644 index 0000000..07cf0bf --- /dev/null +++ b/src/flake8-main/flake8-main/.pre-commit-hooks.yaml @@ -0,0 +1,7 @@ +- id: flake8 + name: flake8 + description: '`flake8` is a command-line utility for enforcing style consistency across Python projects.' + entry: flake8 + language: python + types: [python] + require_serial: true diff --git a/src/flake8-main/flake8-main/.pylintrc b/src/flake8-main/flake8-main/.pylintrc new file mode 100644 index 0000000..92ea2a4 --- /dev/null +++ b/src/flake8-main/flake8-main/.pylintrc @@ -0,0 +1,367 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS,.git,flake8.egg-info + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=4 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Allow optimization of some AST trees. This will activate a peephole AST +# optimizer, which will apply various small optimizations. For instance, it can +# be used to obtain the result of joining multiple strings with the addition +# operator. Joining a lot of strings can lead to a maximum recursion error in +# Pylint and this flag can prevent that. It has one side effect, the resulting +# AST will be different than the one from reality. +optimize-ast=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence=INFERENCE_FAILURE + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=intern-builtin,nonzero-method,parameter-unpacking,backtick,raw_input-builtin,dict-view-method,filter-builtin-not-iterating,long-builtin,unichr-builtin,input-builtin,unicode-builtin,file-builtin,map-builtin-not-iterating,delslice-method,apply-builtin,cmp-method,setslice-method,coerce-method,long-suffix,raising-string,import-star-module-level,buffer-builtin,reload-builtin,unpacking-in-except,print-statement,hex-method,old-octal-literal,metaclass-assignment,dict-iter-method,range-builtin-not-iterating,using-cmp-argument,indexing-exception,no-absolute-import,coerce-builtin,getslice-method,suppressed-message,execfile-builtin,round-builtin,useless-suppression,reduce-builtin,old-raise-syntax,zip-builtin-not-iterating,cmp-builtin,xrange-builtin,standarderror-builtin,old-division,oct-method,next-method-called,old-ne-operator,basestring-builtin + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[BASIC] + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=yes + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for argument names +argument-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for attribute names +attr-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for function names +function-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for method names +method-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for variable names +variable-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[ELIF] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=100 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). This supports can work +# with qualified names. +ignored-classes= + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_$|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=20 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=20 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=10 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of boolean expressions in an if statement +max-bool-expr=5 + + +[IMPORTS] +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= diff --git a/src/flake8-main/flake8-main/.readthedocs.yaml b/src/flake8-main/flake8-main/.readthedocs.yaml new file mode 100644 index 0000000..dfa8b9d --- /dev/null +++ b/src/flake8-main/flake8-main/.readthedocs.yaml @@ -0,0 +1,12 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" +python: + install: + - path: . + - requirements: docs/source/requirements.txt +sphinx: + configuration: docs/source/conf.py diff --git a/src/flake8-main/flake8-main/CONTRIBUTING.rst b/src/flake8-main/flake8-main/CONTRIBUTING.rst new file mode 100644 index 0000000..054dfef --- /dev/null +++ b/src/flake8-main/flake8-main/CONTRIBUTING.rst @@ -0,0 +1,3 @@ +Please refer to `Contributing to Flake8 +`_ +on our website. diff --git a/src/flake8-main/flake8-main/CONTRIBUTORS.txt b/src/flake8-main/flake8-main/CONTRIBUTORS.txt new file mode 100644 index 0000000..59c227d --- /dev/null +++ b/src/flake8-main/flake8-main/CONTRIBUTORS.txt @@ -0,0 +1,28 @@ +Project created by Tarek Ziadé. + +Contributors (by order of appearance) : + +- Tamás Gulácsi +- Nicolas Dumazet +- Stefan Scherfke +- Chris Adams +- Ben Bass +- Ask Solem +- Steven Kryskalla +- Gustavo Picon +- Jannis Leidel +- Miki Tebeka +- David Cramer +- Peter Teichman +- Ian Cordasco +- Oleg Broytman +- Marc Labbé +- Bruno Miguel Custódio +- Florent Xicluna +- Austin Morton +- Michael McNeil Forbes +- Christian Long +- Tyrel Souza +- Corey Farwell +- Michael Penkov +- Anthony Sottile diff --git a/src/flake8-main/flake8-main/LICENSE b/src/flake8-main/flake8-main/LICENSE new file mode 100644 index 0000000..e5e3d6f --- /dev/null +++ b/src/flake8-main/flake8-main/LICENSE @@ -0,0 +1,22 @@ +== Flake8 License (MIT) == + +Copyright (C) 2011-2013 Tarek Ziade +Copyright (C) 2012-2016 Ian Cordasco + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/flake8-main/flake8-main/README.rst b/src/flake8-main/flake8-main/README.rst new file mode 100644 index 0000000..1ea6096 --- /dev/null +++ b/src/flake8-main/flake8-main/README.rst @@ -0,0 +1,90 @@ +.. image:: https://github.com/PyCQA/flake8/workflows/main/badge.svg + :target: https://github.com/PyCQA/flake8/actions?query=workflow%3Amain + :alt: build status + +.. image:: https://results.pre-commit.ci/badge/github/PyCQA/flake8/main.svg + :target: https://results.pre-commit.ci/latest/github/PyCQA/flake8/main + :alt: pre-commit.ci status + +.. image:: https://img.shields.io/discord/825463413634891776.svg + :target: https://discord.gg/qYxpadCgkx + :alt: Discord + +======== + Flake8 +======== + +Flake8 is a wrapper around these tools: + +- PyFlakes +- pycodestyle +- Ned Batchelder's McCabe script + +Flake8 runs all the tools by launching the single ``flake8`` command. +It displays the warnings in a per-file, merged output. + +It also adds a few features: + +- files that contain this line are skipped:: + + # flake8: noqa + +- lines that contain a ``# noqa`` comment at the end will not issue warnings. +- you can ignore specific errors on a line with ``# noqa: ``, e.g., + ``# noqa: E234``. Multiple codes can be given, separated by comma. The ``noqa`` token is case insensitive, the colon before the list of codes is required otherwise the part after ``noqa`` is ignored +- Git and Mercurial hooks +- extendable through ``flake8.extension`` and ``flake8.formatting`` entry + points + + +Quickstart +========== + +See our `quickstart documentation +`_ for how to install +and get started with Flake8. + + +Frequently Asked Questions +========================== + +Flake8 maintains an `FAQ `_ in its +documentation. + + +Questions or Feedback +===================== + +If you have questions you'd like to ask the developers, or feedback you'd like +to provide, feel free to use the mailing list: code-quality@python.org + +We would love to hear from you. Additionally, if you have a feature you'd like +to suggest, the mailing list would be the best place for it. + + +Links +===== + +* `Flake8 Documentation `_ + +* `GitHub Project `_ + +* `All (Open and Closed) Issues + `_ + +* `Code-Quality Archives + `_ + +* `Code of Conduct + `_ + +* `Getting Started Contributing + `_ + + +Maintenance +=========== + +Flake8 was created by Tarek Ziadé and is currently maintained by `anthony sottile +`_ and `Ian Cordasco +`_ diff --git a/src/flake8-main/flake8-main/bin/gen-pycodestyle-plugin b/src/flake8-main/flake8-main/bin/gen-pycodestyle-plugin new file mode 100644 index 0000000..c93fbfe --- /dev/null +++ b/src/flake8-main/flake8-main/bin/gen-pycodestyle-plugin @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import inspect +import os.path +from collections.abc import Generator +from typing import Any +from typing import Callable +from typing import NamedTuple + +import pycodestyle + + +def _too_long(s: str) -> str: + if len(s) >= 80: + return f"{s} # noqa: E501" + else: + return s + + +class Call(NamedTuple): + name: str + is_generator: bool + params: tuple[str, ...] + + def to_src(self) -> str: + params_s = ", ".join(self.params) + if self.is_generator: + return _too_long(f" yield from _{self.name}({params_s})") + else: + lines = ( + _too_long(f" ret = _{self.name}({params_s})"), + " if ret is not None:", + " yield ret", + ) + return "\n".join(lines) + + @classmethod + def from_func(cls, func: Callable[..., Any]) -> Call: + spec = inspect.getfullargspec(func) + params = tuple(spec.args) + return cls(func.__name__, inspect.isgeneratorfunction(func), params) + + +def lines() -> Generator[str]: + logical = [] + physical = [] + + logical = [ + Call.from_func(check) for check in pycodestyle._checks["logical_line"] + ] + physical = [ + Call.from_func(check) for check in pycodestyle._checks["physical_line"] + ] + assert not pycodestyle._checks["tree"] + + yield f'"""Generated using ./bin/{os.path.basename(__file__)}."""' + yield "# fmt: off" + yield "from __future__ import annotations" + yield "" + yield "from collections.abc import Generator" + yield "from typing import Any" + yield "" + imports = sorted(call.name for call in logical + physical) + for name in imports: + yield _too_long(f"from pycodestyle import {name} as _{name}") + yield "" + yield "" + + yield "def pycodestyle_logical(" + logical_params = {param for call in logical for param in call.params} + for param in sorted(logical_params): + yield f" {param}: Any," + yield ") -> Generator[tuple[int, str]]:" + yield ' """Run pycodestyle logical checks."""' + for call in sorted(logical): + yield call.to_src() + yield "" + yield "" + + yield "def pycodestyle_physical(" + physical_params = {param for call in physical for param in call.params} + for param in sorted(physical_params): + yield f" {param}: Any," + yield ") -> Generator[tuple[int, str]]:" + yield ' """Run pycodestyle physical checks."""' + for call in sorted(physical): + yield call.to_src() + + +def main() -> int: + for line in lines(): + print(line) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/flake8-main/flake8-main/dev-requirements.txt b/src/flake8-main/flake8-main/dev-requirements.txt new file mode 100644 index 0000000..053148f --- /dev/null +++ b/src/flake8-main/flake8-main/dev-requirements.txt @@ -0,0 +1 @@ +tox diff --git a/src/flake8-main/flake8-main/docs/source/conf.py b/src/flake8-main/flake8-main/docs/source/conf.py new file mode 100644 index 0000000..48f8a52 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/conf.py @@ -0,0 +1,310 @@ +# +# flake8 documentation build configuration file, created by +# sphinx-quickstart on Tue Jan 19 07:14:10 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# sys.path.insert(0, os.path.abspath('.')) +from __future__ import annotations + +import flake8 + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +needs_sphinx = "2.1" + +# 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.extlinks", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.viewcode", + "sphinx_prompt", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = ".rst" + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = "index" + +# General information about the project. +project = "flake8" +copyright = "2016, Ian Stapleton Cordasco" +author = "Ian Stapleton Cordasco" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = flake8.__version__ +# The full version, including alpha/beta/rc tags. +release = flake8.__version__ + +rst_epilog = """ +.. |Flake8| replace:: :program:`Flake8` +""" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = "en" + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = "sphinx_rtd_theme" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = "flake8doc" + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', + # Latex figure (float) alignment + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ( + master_doc, + "flake8.tex", + "flake8 Documentation", + "Ian Stapleton Cordasco", + "manual", + ), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + "index", + "Flake8", + "Flake8 Documentation", + "Tarek Ziade", + "Flake8", + "Code checking using pycodestyle, pyflakes and mccabe", + "Miscellaneous", + ), +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# texinfo_no_detailmenu = False + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "packaging": ("https://packaging.python.org/en/latest/", None), + "setuptools": ("https://setuptools.pypa.io/en/latest/", None), +} + +extlinks = { + "issue": ("https://github.com/pycqa/flake8/issues/%s", "#%s"), + "pull": ("https://github.com/pycqa/flake8/pull/%s", "#%s"), +} + +autodoc_typehints = "description" diff --git a/src/flake8-main/flake8-main/docs/source/faq.rst b/src/flake8-main/flake8-main/docs/source/faq.rst new file mode 100644 index 0000000..e81768f --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/faq.rst @@ -0,0 +1,56 @@ +============================ + Frequently Asked Questions +============================ + +When is Flake8 released? +======================== + +|Flake8| is released *as necessary*. Sometimes there are specific goals and +drives to get to a release. Usually, we release as users report and fix +bugs. + + +How can I help Flake8 release faster? +===================================== + +Look at the next milestone. If there's work you can help us complete, that +will help us get to the next milestone. If there's a show-stopping bug that +needs to be released, let us know but please be kind. |Flake8| is developed +and released entirely on volunteer time. + + +What is the next version of Flake8? +=================================== + +In general we try to use milestones to indicate this. If the last release +on PyPI is 3.1.5 and you see a milestone for 3.2.0 in GitHub, there's a +good chance that 3.2.0 is the next release. + + +Why does Flake8 use ranges for its dependencies? +================================================ + +|Flake8| uses ranges for mccabe, pyflakes, and pycodestyle because each of +those projects tend to add *new* checks in minor releases. It has been an +implicit design goal of |Flake8|'s to make the list of error codes stable in +its own minor releases. That way if you install something from the 2.5 +series today, you will not find new checks in the same series in a month +from now when you install it again. + +|Flake8|'s dependencies tend to avoid new checks in patch versions which is +why |Flake8| expresses its dependencies roughly as:: + + pycodestyle >= 2.0.0, < 2.1.0 + pyflakes >= 0.8.0, != 1.2.0, != 1.2.1, != 1.2.2, < 1.3.0 + mccabe >= 0.5.0, < 0.6.0 + +This allows those projects to release patch versions that fix bugs and for +|Flake8| users to consume those fixes. + + +Should I file an issue when a new version of a dependency is available? +======================================================================= + +**No.** The current Flake8 core team (of one person) is also +a core developer of pycodestyle, pyflakes, and mccabe. They are aware of +these releases. diff --git a/src/flake8-main/flake8-main/docs/source/glossary.rst b/src/flake8-main/flake8-main/docs/source/glossary.rst new file mode 100644 index 0000000..4b77b07 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/glossary.rst @@ -0,0 +1,57 @@ +.. _glossary: + +================================================ + Glossary of Terms Used in Flake8 Documentation +================================================ + +.. glossary:: + :sorted: + + formatter + A :term:`plugin` that augments the output of |Flake8| when passed + to :option:`flake8 --format`. + + plugin + A package that is typically installed from PyPI to augment the + behaviour of |Flake8| either through adding one or more additional + :term:`check`\ s or providing additional :term:`formatter`\ s. + + check + A piece of logic that corresponds to an error code. A check may + be a style check (e.g., check the length of a given line against + the user configured maximum) or a lint check (e.g., checking for + unused imports) or some other check as defined by a plugin. + + error + error code + violation + The symbol associated with a specific :term:`check`. For example, + pycodestyle implements :term:`check`\ s that look for whitespace + around binary operators and will either return an error code of + ``W503`` or ``W504``. + + warning + Typically the ``W`` class of :term:`error code`\ s from pycodestyle. + + class + error class + A larger grouping of related :term:`error code`\ s. For example, + ``W503`` and ``W504`` are two codes related to whitespace. ``W50`` + would be the most specific class of codes relating to whitespace. + ``W`` would be the warning class that subsumes all whitespace + errors. + + pyflakes + The project |Flake8| depends on to lint files (check for unused + imports, variables, etc.). This uses the ``F`` :term:`class` of + :term:`error code`\ s reported by |Flake8|. + + pycodestyle + The project |Flake8| depends on to provide style enforcement. + pycodestyle implements :term:`check`\ s for :pep:`8`. This uses the + ``E`` and ``W`` :term:`class`\ es of :term:`error code`\ s. + + mccabe + The project |Flake8| depends on to calculate the McCabe complexity + of a unit of code (e.g., a function). This uses the ``C`` + :term:`class` of :term:`error code`\ s. diff --git a/src/flake8-main/flake8-main/docs/source/index.rst b/src/flake8-main/flake8-main/docs/source/index.rst new file mode 100644 index 0000000..f4c5938 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/index.rst @@ -0,0 +1,131 @@ +.. flake8 documentation master file, created by + sphinx-quickstart on Tue Jan 19 07:14:10 2016. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +=============================================== + Flake8: Your Tool For Style Guide Enforcement +=============================================== + +Quickstart +========== + +.. _installation-guide: + +Installation +------------ + +To install |Flake8|, open an interactive shell and run: + +.. code:: + + python -m pip install flake8 + +If you want |Flake8| to be installed for your default Python installation, you +can instead use: + +.. code:: + + python -m pip install flake8 + +.. note:: + + It is **very** important to install |Flake8| on the *correct* version of + Python for your needs. If you want |Flake8| to properly parse new language + features in Python 3.5 (for example), you need it to be installed on 3.5 + for |Flake8| to understand those features. In many ways, Flake8 is tied to + the version of Python on which it runs. + +Using Flake8 +------------ + +To start using |Flake8|, open an interactive shell and run: + +.. code:: + + flake8 path/to/code/to/check.py + # or + flake8 path/to/code/ + +.. note:: + + If you have installed |Flake8| on a particular version of Python (or on + several versions), it may be best to instead run ``python -m + flake8``. + +If you only want to see the instances of a specific warning or error, you can +*select* that error like so: + +.. code:: + + flake8 --select E123,W503 path/to/code/ + +Alternatively, if you want to add a specific warning or error to *ignore*: + +.. code:: + + flake8 --extend-ignore E203,W234 path/to/code/ + +Please read our user guide for more information about how to use and configure +|Flake8|. + +FAQ and Glossary +================ + +.. toctree:: + :maxdepth: 2 + + faq + glossary + +User Guide +========== + +All users of |Flake8| should read this portion of the documentation. This +provides examples and documentation around |Flake8|'s assortment of options +and how to specify them on the command-line or in configuration files. + +.. toctree:: + :maxdepth: 2 + + user/index + +Plugin Developer Guide +====================== + +If you're maintaining a plugin for |Flake8| or creating a new one, you should +read this section of the documentation. It explains how you can write your +plugins and distribute them to others. + +.. toctree:: + :maxdepth: 2 + + plugin-development/index + +Contributor Guide +================= + +If you are reading |Flake8|'s source code for fun or looking to contribute, +you should read this portion of the documentation. This is a mix of documenting +the internal-only interfaces |Flake8| and documenting reasoning for Flake8's +design. + +.. toctree:: + :maxdepth: 2 + + internal/index + +Release Notes and History +========================= + +.. toctree:: + :maxdepth: 2 + + release-notes/index + +General Indices +=============== + +* :ref:`genindex` +* :ref:`Index of Documented Public Modules ` +* :ref:`Glossary of terms ` diff --git a/src/flake8-main/flake8-main/docs/source/internal/.keep b/src/flake8-main/flake8-main/docs/source/internal/.keep new file mode 100644 index 0000000..e69de29 diff --git a/src/flake8-main/flake8-main/docs/source/internal/checker.rst b/src/flake8-main/flake8-main/docs/source/internal/checker.rst new file mode 100644 index 0000000..fd571a7 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/internal/checker.rst @@ -0,0 +1,66 @@ +==================== + How Checks are Run +==================== + +In |Flake8| 2.x, |Flake8| delegated check running to pep8. In 3.0 |Flake8| +takes on that responsibility. This has allowed for simpler +handling of the ``--jobs`` parameter (using :mod:`multiprocessing`) and +simplified our fallback if something goes awry with concurrency. +At the lowest level we have a |FileChecker|. Instances of |FileChecker| are +created for *each* file to be analyzed by |Flake8|. Each instance, has a copy +of all of the plugins registered with setuptools in the ``flake8.extension`` +entry-point group. + +The |FileChecker| instances are managed by an instance of |Manager|. The +|Manager| instance handles creating sub-processes with +:mod:`multiprocessing` module and falling back to running checks in serial if +an operating system level error arises. When creating |FileChecker| instances, +the |Manager| is responsible for determining if a particular file has been +excluded. + + +Processing Files +---------------- + +Unfortunately, since |Flake8| took over check running from pep8/pycodestyle, +it also had to take over parsing and processing files for the checkers +to use. Since it couldn't reuse pycodestyle's functionality (since it did not +separate cleanly the processing from check running) that function was isolated +into the :class:`~flake8.processor.FileProcessor` class. We moved +several helper functions into the :mod:`flake8.processor` module (see also +:ref:`Processor Utility Functions `). + + +API Reference +------------- + +.. autoclass:: flake8.checker.FileChecker + :members: + +.. autoclass:: flake8.checker.Manager + :members: + +.. autoclass:: flake8.processor.FileProcessor + :members: + + +.. _processor_utility_functions: + +Utility Functions +````````````````` + +.. autofunction:: flake8.processor.count_parentheses + +.. autofunction:: flake8.processor.expand_indent + +.. autofunction:: flake8.processor.is_eol_token + +.. autofunction:: flake8.processor.is_multiline_string + +.. autofunction:: flake8.processor.mutate_string + +.. autofunction:: flake8.processor.token_is_newline + +.. Substitutions +.. |FileChecker| replace:: :class:`~flake8.checker.FileChecker` +.. |Manager| replace:: :class:`~flake8.checker.Manager` diff --git a/src/flake8-main/flake8-main/docs/source/internal/cli.rst b/src/flake8-main/flake8-main/docs/source/internal/cli.rst new file mode 100644 index 0000000..cbb1fef --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/internal/cli.rst @@ -0,0 +1,26 @@ +Command Line Interface +====================== + +The command line interface of |Flake8| is modeled as an application via +:class:`~flake8.main.cli.Application`. When a user runs ``flake8`` at their +command line, :func:`~flake8.main.cli.main` is run which handles +management of the application. + +User input is parsed *twice* to accommodate logging and verbosity options +passed by the user as early as possible. +This is so as much logging can be produced as possible. + +The default |Flake8| options are registered by +:func:`~flake8.main.options.register_default_options`. Trying to register +these options in plugins will result in errors. + + +API Documentation +----------------- + +.. autofunction:: flake8.main.cli.main + +.. autoclass:: flake8.main.application.Application + :members: + +.. autofunction:: flake8.main.options.register_default_options diff --git a/src/flake8-main/flake8-main/docs/source/internal/contributing.rst b/src/flake8-main/flake8-main/docs/source/internal/contributing.rst new file mode 100644 index 0000000..bd33015 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/internal/contributing.rst @@ -0,0 +1,215 @@ +======================== + Contributing to Flake8 +======================== + +There are many ways to contribute to |Flake8|, and we encourage them all: + +- contributing bug reports and feature requests + +- contributing documentation (and yes that includes this document) + +- reviewing and triaging bugs and merge requests + +Before you go any further, please allow me to reassure you that I do want +*your* contribution. If you think your contribution might not be valuable, I +reassure you that any help you can provide *is* valuable. + + +Code of Conduct +=============== + +|Flake8| adheres to the `Python Code Quality Authority's Code of Conduct`_. +Any violations of the Code of Conduct should be reported to Ian Stapleton +Cordasco (graffatcolmingov [at] gmail [dot] com). + + +Setting Up A Development Environment +==================================== + +To contribute to |Flake8|'s development, you simply need: + +- Python (one of the versions we support) + +- `tox`_ + + We suggest installing this like: + + .. prompt:: bash + + pip install --user tox + + Or + + .. prompt:: bash + + python -m pip install --user tox + +- your favorite editor + + +Filing a Bug +============ + +When filing a bug against |Flake8|, please fill out the issue template as it +is provided to you by `GitHub`_. If your bug is in reference to one of the +checks that |Flake8| reports by default, please do not report them to |Flake8| +unless |Flake8| is doing something to prevent the check from running or you +have some reason to believe |Flake8| is inhibiting the effectiveness of the +check. + +**Please search for closed and open bug reports before opening new ones.** + +All bug reports about checks should go to their respective projects: + +- Error codes starting with ``E`` and ``W`` should be reported to + `pycodestyle`_. + +- Error codes starting with ``F`` should be reported to `pyflakes`_ + +- Error codes starting with ``C`` should be reported to `mccabe`_ + + +Requesting a New Feature +======================== + +When requesting a new feature in |Flake8|, please fill out the issue template. +Please also note if there are any existing alternatives to your new feature +either via plugins, or combining command-line options. Please provide example +use cases. For example, do not ask for a feature like this: + + I need feature frobulate for my job. + +Instead ask: + + I need |Flake8| to frobulate these files because my team expects them to + frobulated but |Flake8| currently does not frobulate them. We tried using + ``--filename`` but we could not create a pattern that worked. + +The more you explain about *why* you need a feature, the more likely we are to +understand your needs and help you to the best of our ability. + + +Contributing Documentation +========================== + +To contribute to |Flake8|'s documentation, you might want to first read a +little about reStructuredText or Sphinx. |Flake8| has a :ref:`guide of best +practices ` when contributing to our documentation. For the most +part, you should be fine following the structure and style of the rest of +|Flake8|'s documentation. + +All of |Flake8|'s documentation is written in reStructuredText and rendered by +Sphinx. The source (reStructuredText) lives in ``docs/source/``. To build +the documentation the way our Continuous Integration does, run: + +.. prompt:: bash + + tox -e docs + +To view the documentation locally, you can also run: + +.. prompt:: bash + + tox -e serve-docs + +You can run the latter in a separate terminal and continuously re-run the +documentation generation and refresh the documentation you're working on. + +.. note:: + + We lint our documentation just like we lint our code. + You should also run: + + .. prompt:: bash + + tox -e linters + + After making changes and before pushing them to ensure that they will + pass our CI tests. + + +Contributing Code +================= + +|Flake8| development happens on `GitHub`_. Code contributions should be +submitted there. + +Merge requests should: + +- Fix one issue and fix it well + + Fix the issue, but do not include extraneous refactoring or code + reformatting. In other words, keep the diff short, but only as short + as is necessary to fix the bug appropriately and add sufficient testing + around it. Long diffs are fine, so long as everything that it includes + is necessary to the purpose of the merge request. + +- Have descriptive titles and descriptions + + Searching old merge requests is made easier when a merge request is well + described. + +- Have commits that follow this style: + + .. code:: + + Create a short title that is 50 characters long + + Ensure the title and commit message use the imperative voice. The + commit and you are doing something. Also, please ensure that the + body of the commit message does not exceed 72 characters. + + The body may have multiple paragraphs as necessary. + + The final line of the body references the issue appropriately. + +- Follow the guidelines in :ref:`writing-code` + +- Avoid having :code:`.gitignore` file in your PR + + Changes to :code:`.gitignore` will rarely be accepted. + + If you need to add files to :code:`.gitignore` you have multiple options + + - Create a global :code:`.gitignore` file + - Create/update :code:`.git/info/exclude` file. + + Both these options are explained in detail `here `_ + + +Reviewing and Triaging Issues and Merge Requests +================================================ + +When reviewing other people's merge requests and issues, please be +**especially** mindful of how the words you choose can be read by someone +else. We strive for professional code reviews that do not insult the +contributor's intelligence or impugn their character. The code review +should be focused on the code, its effectiveness, and whether it is +appropriate for |Flake8|. + +If you have the ability to edit an issue or merge request's labels, please do +so to make search and prioritization easier. + +|Flake8| uses milestones with both issues and merge requests. This provides +direction for other contributors about when an issue or merge request will be +delivered. + + +.. links +.. _Python Code Quality Authority's Code of Conduct: + https://meta.pycqa.org/code-of-conduct.html + +.. _tox: + https://tox.readthedocs.io/ + +.. _GitHub: + https://github.com/pycqa/flake8 + +.. _pycodestyle: + https://github.com/pycqa/pycodestyle + +.. _pyflakes: + https://github.com/pyflakes/pyflakes + +.. _mccabe: + https://github.com/pycqa/mccabe diff --git a/src/flake8-main/flake8-main/docs/source/internal/formatters.rst b/src/flake8-main/flake8-main/docs/source/internal/formatters.rst new file mode 100644 index 0000000..c58189b --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/internal/formatters.rst @@ -0,0 +1,47 @@ +===================== + Built-in Formatters +===================== + +By default |Flake8| has two formatters built-in, ``default`` and ``pylint``. +These correspond to two classes |DefaultFormatter| and |PylintFormatter|. + +In |Flake8| 2.0, pep8 handled formatting of errors and also allowed users to +specify an arbitrary format string as a parameter to ``--format``. In order +to allow for this backwards compatibility, |Flake8| 3.0 made two choices: + +#. To not limit a user's choices for ``--format`` to the format class names + +#. To make the default formatter attempt to use the string provided by the + user if it cannot find a formatter with that name. + +Default Formatter +================= + +The |DefaultFormatter| continues to use the same default format string as +pep8: ``'%(path)s:%(row)d:%(col)d: %(code)s %(text)s'``. + +To provide the default functionality it overrides two methods: + +#. ``after_init`` + +#. ``format`` + +The former allows us to inspect the value provided to ``--format`` by the +user and alter our own format based on that value. The second simply uses +that format string to format the error. + +.. autoclass:: flake8.formatting.default.Default + :members: + +Pylint Formatter +================ + +The |PylintFormatter| simply defines the default Pylint format string from +pep8: ``'%(path)s:%(row)d: [%(code)s] %(text)s'``. + +.. autoclass:: flake8.formatting.default.Pylint + :members: + + +.. |DefaultFormatter| replace:: :class:`~flake8.formatting.default.Default` +.. |PylintFormatter| replace:: :class:`~flake8.formatting.default.Pylint` diff --git a/src/flake8-main/flake8-main/docs/source/internal/index.rst b/src/flake8-main/flake8-main/docs/source/internal/index.rst new file mode 100644 index 0000000..a6a203b --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/internal/index.rst @@ -0,0 +1,28 @@ +============================== + Exploring Flake8's Internals +============================== + +While writing |Flake8| 3.0, the developers attempted to capture some reasoning +and decision information in internal documentation meant for future developers +and maintainers. Most of this information is unnecessary for users and plugin +developers. Some of it, however, is linked to from the plugin development +documentation. + +Keep in mind that not everything will be here and you may need to help pull +information out of the developers' heads and into these documents. Please +pull gently. + +.. toctree:: + :maxdepth: 2 + + contributing + writing-documentation + writing-code + releases + start-to-finish + checker + cli + formatters + option_handling + plugin_handling + utils diff --git a/src/flake8-main/flake8-main/docs/source/internal/option_handling.rst b/src/flake8-main/flake8-main/docs/source/internal/option_handling.rst new file mode 100644 index 0000000..9e8f4fd --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/internal/option_handling.rst @@ -0,0 +1,201 @@ +Option and Configuration Handling +================================= + +Option Management +----------------- + +Command-line options are often also set in configuration files for |Flake8|. +While not all options are meant to be parsed from configuration files, many +default options are also parsed from configuration files as well as +most plugin options. + +In |Flake8| 2, plugins received a :class:`optparse.OptionParser` instance and +called :meth:`optparse.OptionParser.add_option` to register options. If the +plugin author also wanted to have that option parsed from config files they +also had to do something like: + +.. code-block:: python + + parser.config_options.append('my_config_option') + parser.config_options.extend(['config_opt1', 'config_opt2']) + +This was previously undocumented and led to a lot of confusion about why +registered options were not automatically parsed from configuration files. + +Since |Flake8| 3 was rewritten from scratch, we decided to take a different +approach to configuration file parsing. Instead of needing to know about an +undocumented attribute that pep8 looks for, |Flake8| 3 now accepts a parameter +to ``add_option``, specifically ``parse_from_config`` which is a boolean +value. + +|Flake8| does this by creating its own abstractions on top of :mod:`argparse`. +The first abstraction is the :class:`flake8.options.manager.Option` class. The +second is the :class:`flake8.options.manager.OptionManager`. In fact, we add +three new parameters: + +- ``parse_from_config`` + +- ``comma_separated_list`` + +- ``normalize_paths`` + +The last two are not specifically for configuration file handling, but they +do improve that dramatically. We found that there were options that, when +specified in a configuration file, often necessitated being split across +multiple lines and those options were almost always comma-separated. For +example, let's consider a user's list of ignored error codes for a project: + +.. code-block:: ini + + [flake8] + ignore = + # Reasoning + E111, + # Reasoning + E711, + # Reasoning + E712, + # Reasoning + E121, + # Reasoning + E122, + # Reasoning + E123, + # Reasoning + E131, + # Reasoning + E251 + +It makes sense here to allow users to specify the value this way, but, the +standard library's :class:`configparser.RawConfigParser` class does returns a +string that looks like + +.. code-block:: python + + "\nE111, \nE711, \nE712, \nE121, \nE122, \nE123, \nE131, \nE251 " + +This means that a typical call to :meth:`str.split` with ``','`` will not be +sufficient here. Telling |Flake8| that something is a comma-separated list +(e.g., ``comma_separated_list=True``) will handle this for you. |Flake8| will +return: + +.. code-block:: python + + ["E111", "E711", "E712", "E121", "E122", "E123", "E131", "E251"] + +Next let's look at how users might like to specify their ``exclude`` list. +Presently OpenStack's Nova project has this line in their `tox.ini`_: + +.. code-block:: ini + + exclude = .venv,.git,.tox,dist,doc,*openstack/common/*,*lib/python*,*egg,build,tools/xenserver*,releasenotes + +We think we can all agree that this would be easier to read like this: + +.. code-block:: ini + + exclude = + .venv, + .git, + .tox, + dist, + doc, + *openstack/common/*, + *lib/python*, + *egg, + build, + tools/xenserver*, + releasenotes + +In this case, since these are actually intended to be paths, we would specify +both ``comma_separated_list=True`` and ``normalize_paths=True`` because we +want the paths to be provided to us with some consistency (either all absolute +paths or not). + +Now let's look at how this will actually be used. Most plugin developers +will receive an instance of :class:`~flake8.options.manager.OptionManager` so +to ease the transition we kept the same API as the +:class:`optparse.OptionParser` object. The only difference is that +:meth:`~flake8.options.manager.OptionManager.add_option` accepts the three +extra arguments we highlighted above. + +.. _tox.ini: + https://github.com/openstack/nova/blob/3eb190c4cfc0eefddac6c2cc1b94a699fb1687f8/tox.ini#L155 + +Configuration File Management +----------------------------- + +In |Flake8| 2, configuration file discovery and management was handled by +pep8. In pep8's 1.6 release series, it drastically broke how discovery and +merging worked (as a result of trying to improve it). To avoid a dependency +breaking |Flake8| again in the future, we have created our own discovery and +management in 3.0.0. In 4.0.0 we have once again changed how this works and we +removed support for user-level config files. + +- Project files (files stored in the current directory) are read next and + merged on top of the user file. In other words, configuration in project + files takes precedence over configuration in user files. + +- **New in 3.0.0** The user can specify ``--append-config `` + repeatedly to include extra configuration files that should be read and + take precedence over user and project files. + +- **New in 3.0.0** The user can specify ``--config `` to so this + file is the only configuration file used. This is a change from |Flake8| 2 + where pep8 would simply merge this configuration file into the configuration + generated by user and project files (where this takes precedence). + +- **New in 3.0.0** The user can specify ``--isolated`` to disable + configuration via discovered configuration files. + +To facilitate the configuration file management, we've taken a different +approach to discovery and management of files than pep8. In pep8 1.5, 1.6, and +1.7 configuration discovery and management was centralized in `66 lines of +very terse python`_ which was confusing and not very explicit. The terseness +of this function (|Flake8| 3.0.0's authors believe) caused the confusion and +problems with pep8's 1.6 series. As such, |Flake8| has separated out +discovery, management, and merging into a module to make reasoning about each +of these pieces easier and more explicit (as well as easier to test). + +Configuration file discovery and raw ini reading is managed by +:func:`~flake8.options.config.load_config`. This produces a loaded +:class:`~configparser.RawConfigParser` and a config directory (which will be +used later to normalize paths). + +Next, :func:`~flake8.options.config.parse_config` parses options using the +types in the ``OptionManager``. + +Most of this is done in :func:`~flake8.options.aggregator.aggregate_options`. + +Aggregating Configuration File and Command Line Arguments +--------------------------------------------------------- + +:func:`~flake8.options.aggregator.aggregate_options` accepts an instance of +:class:`~flake8.options.manager.OptionManager` and does the work to parse the +command-line arguments. + +After parsing the configuration file, we determine the default ignore list. We +use the defaults from the OptionManager and update those with the parsed +configuration files. Finally we parse the user-provided options one last time +using the option defaults and configuration file values as defaults. The +parser merges on the command-line specified arguments for us so we have our +final, definitive, aggregated options. + +.. _66 lines of very terse python: + https://github.com/PyCQA/pep8/blob/b8088a2b6bc5b76bece174efad877f764529bc74/pep8.py#L1981..L2047 + +API Documentation +----------------- + +.. autofunction:: flake8.options.aggregator.aggregate_options + +.. autoclass:: flake8.options.manager.Option + :members: __init__, normalize, to_argparse + +.. autoclass:: flake8.options.manager.OptionManager + :members: + :special-members: + +.. autofunction:: flake8.options.config.load_config + +.. autofunction:: flake8.options.config.parse_config diff --git a/src/flake8-main/flake8-main/docs/source/internal/plugin_handling.rst b/src/flake8-main/flake8-main/docs/source/internal/plugin_handling.rst new file mode 100644 index 0000000..f1c7b9f --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/internal/plugin_handling.rst @@ -0,0 +1,43 @@ +Plugin Handling +=============== + +Plugin Management +----------------- + +|Flake8| 3.0 added support for other plugins besides those which define +new checks. It now supports: + +- extra checks + +- alternative report formatters + +Default Plugins +--------------- + +Finally, |Flake8| has always provided its own plugin shim for Pyflakes. As +part of that we carry our own shim in-tree and now store that in +:mod:`flake8.plugins.pyflakes`. + +|Flake8| also registers plugins for pycodestyle. Each check in pycodestyle +requires different parameters and it cannot easily be shimmed together like +Pyflakes was. As such, plugins have a concept of a "group". If you look at our +:file:`setup.py` you will see that we register pycodestyle checks roughly like +so: + +.. code:: + + pycodestyle. = pycodestyle: + +We do this to identify that ``>`` is part of a group. This also +enables us to special-case how we handle reporting those checks. Instead of +reporting each check in the ``--version`` output, we only report +``pycodestyle`` once. + +API Documentation +----------------- + +.. autofunction:: flake8.plugins.finder.parse_plugin_options + +.. autofunction:: flake8.plugins.finder.find_plugins + +.. autofunction:: flake8.plugins.finder.load_plugins diff --git a/src/flake8-main/flake8-main/docs/source/internal/releases.rst b/src/flake8-main/flake8-main/docs/source/internal/releases.rst new file mode 100644 index 0000000..d71796d --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/internal/releases.rst @@ -0,0 +1,97 @@ +================== + Releasing Flake8 +================== + +There is not much that is hard to find about how |Flake8| is released. + +- We use **major** releases (e.g., 2.0.0, 3.0.0, etc.) for big, potentially + backwards incompatible, releases. + +- We use **minor** releases (e.g., 2.1.0, 2.2.0, 3.1.0, 3.2.0, etc.) for + releases that contain features and dependency version changes. + +- We use **patch** releases (e.g., 2.1.1, 2.1.2, 3.0.1, 3.0.10, etc.) for + releases that contain *only* bug fixes. + +In this sense we follow semantic versioning. But we follow it as more of a set +of guidelines. We're also not perfect, so we may make mistakes, and that's +fine. + + +Major Releases +============== + +Major releases are often associated with backwards incompatibility. |Flake8| +hopes to avoid those, but will occasionally need them. + +Historically, |Flake8| has generated major releases for: + +- Unvendoring dependencies (2.0) + +- Large scale refactoring (2.0, 3.0, 5.0, 6.0) + +- Subtly breaking CLI changes (3.0, 4.0, 5.0, 6.0, 7.0) + +- Breaking changes to its plugin interface (3.0) + +Major releases can also contain: + +- Bug fixes (which may have backwards incompatible solutions) + +- New features + +- Dependency changes + + +Minor Releases +============== + +Minor releases often have new features in them, which we define roughly as: + +- New command-line flags + +- New behaviour that does not break backwards compatibility + +- New errors detected by dependencies, e.g., by raising the upper limit on + PyFlakes we introduce F405 + +- Bug fixes + + +Patch Releases +============== + +Patch releases should only ever have bug fixes in them. + +We do not update dependency constraints in patch releases. If you do not +install |Flake8| from PyPI, there is a chance that your packager is using +different requirements. Some downstream redistributors have been known to +force a new version of PyFlakes, pep8/PyCodestyle, or McCabe into place. +Occasionally this will cause breakage when using |Flake8|. There is little +we can do to help you in those cases. + + +Process +======= + +To prepare a release, we create a file in :file:`docs/source/release-notes/` +named: ``{{ release_number }}.rst`` (e.g., ``3.0.0.rst``). We note bug fixes, +improvements, and dependency version changes as well as other items of note +for users. + +Before releasing, the following tox test environments must pass: + +- Python 3.9 (a.k.a., ``tox -e py39``) + +- Python 3.13 (a.k.a., ``tox -e py313``) + +- PyPy 3 (a.k.a., ``tox -e pypy3``) + +- Linters (a.k.a., ``tox -e linters``) + +We tag the most recent commit that passes those items and contains our release +notes. + +Finally, we run ``tox -e release`` to build source distributions (e.g., +``flake8-3.0.0.tar.gz``), universal wheels, and upload them to PyPI with +Twine. diff --git a/src/flake8-main/flake8-main/docs/source/internal/start-to-finish.rst b/src/flake8-main/flake8-main/docs/source/internal/start-to-finish.rst new file mode 100644 index 0000000..5e31083 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/internal/start-to-finish.rst @@ -0,0 +1,115 @@ +================================== + What Happens When You Run Flake8 +================================== + +Given |Flake8| 3.0's new organization and structure, it might be a bit much +for some people to understand what happens from when you call ``flake8`` on the +command-line to when it completes. This section aims to give you something of +a technical overview of what exactly happens. + + +Invocation +========== + +The exact way that we end up in our ``main`` function for Flake8 depends on +how you invoke it. If you do something like: + +.. prompt:: bash + + flake8 + +Then your shell looks up where ``flake8`` the executable lives and executes +it. In almost every case, this is a tiny python script generated by +``setuptools`` using the console script entry points that |Flake8| declares +in its :file:`setup.py`. This might look something like: + +.. code-block:: python + + #!/path/to/python + # EASY-INSTALL-ENTRY-SCRIPT: 'flake8==3.0.0','console_scripts','flake8' + __requires__ = 'flake8==3.0.0' + import sys + from pkg_resources import load_entry_point + + if __name__ == '__main__': + sys.exit( + load_entry_point('flake8==3.0.0', 'console_scripts', 'flake8')() + ) + +If instead you invoke it like: + +.. prompt:: bash + + python -m flake8 + +Then you're relying on Python to find :mod:`flake8.__main__` and run that. In +both cases, however, you end up in :func:`flake8.main.cli.main`. This is the +primary way that users will end up starting Flake8. This function creates an +instance of |Application|. + +Application Logic +================= + +When we create our |Application| instance, we record the start time and parse +our command-line arguments so we can configure the verbosity of |Flake8|'s +logging. For the most part, every path then calls +:meth:`~flake8.main.application.Application.run` which in turn calls: + +- :meth:`~flake8.main.application.Application.initialize` +- :meth:`~flake8.main.application.Application.run_checks` +- :meth:`~flake8.main.application.Application.report_errors` +- :meth:`~flake8.main.application.Application.report_benchmarks` + +Our Git hook, however, runs these individually. + +Application Initialization +-------------------------- + +:meth:`~flake8.main.application.Application.initialize` loads all of our +:term:`plugin`\ s, registers the options for those plugins, parses the +command-line arguments, makes our formatter (as selected by the user), makes +our :class:`~flake8.style_guide.StyleGuide` and finally makes our +:class:`file checker manager `. + +Running Our Checks +------------------ + +:meth:`~flake8.main.application.Application.run_checks` then creates an +instance of :class:`flake8.checker.FileChecker` for each file to be checked +after aggregating all of the files that are not excluded and match the +provided file-patterns. Then, if we're on a system that supports +:mod:`multiprocessing` **and** :option:`flake8 --jobs` is either ``auto`` or +a number greater than 1, we will begin processing the files in subprocesses. +Otherwise, we'll run the checks in parallel. + +After we start running the checks, we start aggregating the reported +:term:`violation`\ s in the main process. After the checks are done running, +we record the end time. + +Reporting Violations +-------------------- + +Next, the application takes the violations from the file checker manager, and +feeds them through the :class:`~flake8.style_guide.StyleGuide`. This +relies on a :class:`~flake8.style_guide.DecisionEngine` instance to determine +whether the particular :term:`error code` is selected or ignored and then +appropriately sends it to the formatter (or not). + +Reporting Benchmarks +-------------------- + +Finally, if the user has asked to see benchmarks (i.e., :option:`flake8 +--benchmark`) then we print the benchmarks. + + +Exiting +======= + +Once :meth:`~flake8.main.application.Application.run` has finished, we then +call :meth:`~flake8.main.application.Application.exit` which looks at how +many errors were reported and whether the user specified :option:`flake8 +--exit-zero` and exits with the appropriate exit code. + + +.. Replacements +.. |Application| replace:: :class:`~flake8.main.application.Application` diff --git a/src/flake8-main/flake8-main/docs/source/internal/utils.rst b/src/flake8-main/flake8-main/docs/source/internal/utils.rst new file mode 100644 index 0000000..dc53378 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/internal/utils.rst @@ -0,0 +1,68 @@ +=================== + Utility Functions +=================== + +|Flake8| has a few utility functions that it uses internally. + +.. warning:: + + As should be implied by where these are documented, these are all + **internal** utility functions. Their signatures and return types + may change between releases without notice. + + Bugs reported about these **internal** functions will be closed + immediately. + + If functions are needed by plugin developers, they may be requested + in the bug tracker and after careful consideration they *may* be added + to the *documented* stable API. + +.. autofunction:: flake8.utils.parse_comma_separated_list + +:func:`~flake8.utils.parse_comma_separated_list` takes either a string like + +.. code-block:: python + + "E121,W123,F904" + "E121,\nW123,\nF804" + " E121,\n\tW123,\n\tF804 " + " E121\n\tW123 \n\tF804" + +And converts it to a list that looks as follows + +.. code-block:: python + + ["E121", "W123", "F904"] + +This function helps normalize any kind of comma-separated input you or |Flake8| +might receive. This is most helpful when taking advantage of |Flake8|'s +additional parameters to :class:`~flake8.options.manager.Option`. + +.. autofunction:: flake8.utils.normalize_path + +This utility takes a string that represents a path and returns the absolute +path if the string has a ``/`` in it. It also removes trailing ``/``\ s. + +.. autofunction:: flake8.utils.normalize_paths + +This function utilizes :func:`~flake8.utils.normalize_path` to normalize a +sequence of paths. See :func:`~flake8.utils.normalize_path` for what defines a +normalized path. + +.. autofunction:: flake8.utils.stdin_get_value + +This function retrieves and caches the value provided on ``sys.stdin``. This +allows plugins to use this to retrieve ``stdin`` if necessary. + +.. autofunction:: flake8.utils.is_using_stdin + +Another helpful function that is named only to be explicit given it is a very +trivial check, this checks if the user specified ``-`` in their arguments to +|Flake8| to indicate we should read from stdin. + +.. autofunction:: flake8.utils.fnmatch + +The standard library's :func:`fnmatch.fnmatch` is excellent at deciding if a +filename matches a single pattern. In our use case, however, we typically have +a list of patterns and want to know if the filename matches any of them. This +function abstracts that logic away with a little extra logic. diff --git a/src/flake8-main/flake8-main/docs/source/internal/writing-code.rst b/src/flake8-main/flake8-main/docs/source/internal/writing-code.rst new file mode 100644 index 0000000..323e107 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/internal/writing-code.rst @@ -0,0 +1,183 @@ +.. _writing-code: + +========================= + Writing Code for Flake8 +========================= + +The maintainers of |Flake8| unsurprisingly have some opinions about the style +of code maintained in the project. + +At the time of this writing, |Flake8| enables all of PyCodeStyle's checks, all +of PyFlakes' checks, and sets a maximum complexity value (for McCabe) of 10. +On top of that, we enforce PEP-0257 style doc-strings via PyDocStyle +(disabling only D203) and Google's import order style using +flake8-import-order. + +The last two are a little unusual, so we provide examples below. + + +PEP-0257 style doc-strings +========================== + +|Flake8| attempts to document both internal interfaces as well as our API and +doc-strings provide a very convenient way to do so. Even if a function, class, +or method isn't included specifically in our documentation having a doc-string +is still preferred. Further, |Flake8| has some style preferences that are not +checked by PyDocStyle. + +For example, while most people will never read the doc-string for +:func:`flake8.main.git.hook` that doc-string still provides value to the +maintainers and future collaborators. They (very explicitly) describe the +purpose of the function, a little of what it does, and what parameters it +accepts as well as what it returns. + +.. code-block:: python + + # src/flake8/main/git.py + def hook(lazy: bool = False, strict: bool = False) -> int: + """Execute Flake8 on the files in git's index. + + Determine which files are about to be committed and run Flake8 over them + to check for violations. + + :param lazy: + Find files not added to the index prior to committing. This is useful + if you frequently use ``git commit -a`` for example. This defaults to + False since it will otherwise include files not in the index. + :param strict: + If True, return the total number of errors/violations found by Flake8. + This will cause the hook to fail. + :returns: + Total number of errors found during the run. + """ + # NOTE(sigmavirus24): Delay import of application until we need it. + from flake8.main import application + app = application.Application() + with make_temporary_directory() as tempdir: + filepaths = list(copy_indexed_files_to(tempdir, lazy)) + app.initialize(['.']) + app.options.exclude = update_excludes(app.options.exclude, tempdir) + app.run_checks(filepaths) + + app.report_errors() + if strict: + return app.result_count + return 0 + +Note that we begin the description of the parameter on a new-line and +indented 4 spaces. + +Following the above examples and guidelines should help you write doc-strings +that are stylistically correct for |Flake8|. + + +Imports +======= + +|Flake8| follows the import guidelines that Google published in their Python +Style Guide. In short this includes: + +- Only importing modules + +- Grouping imports into + + * standard library imports + + * third-party dependency imports + + * local application imports + +- Ordering imports alphabetically + +In practice this would look something like: + +.. code-block:: python + + import configparser + import logging + from os import path + + import requests + + from flake8 import exceptions + from flake8.formatting import base + +As a result, of the above, we do not: + +- Import objects into a namespace to make them accessible from that namespace + +- Import only the objects we're using + +- Add comments explaining that an import is a standard library module or + something else + + +Other Stylistic Preferences +=========================== + +Finally, |Flake8| has a few other stylistic preferences that it does not +presently enforce automatically. + +Multi-line Function/Method Calls +-------------------------------- + +When you find yourself having to split a call to a function or method up +across multiple lines, insert a new-line after the opening parenthesis, e.g., + +.. code-block:: python + + # src/flake8/main/options.py + add_option( + '-v', '--verbose', default=0, action='count', + parse_from_config=True, + help='Print more information about what is happening in flake8.' + ' This option is repeatable and will increase verbosity each ' + 'time it is repeated.', + ) + + # src/flake8/formatting/base.py + def show_statistics(self, statistics): + """Format and print the statistics.""" + for error_code in statistics.error_codes(): + stats_for_error_code = statistics.statistics_for(error_code) + statistic = next(stats_for_error_code) + count = statistic.count + count += sum(stat.count for stat in stats_for_error_code) + self._write(f'{count:<5} {error_code} {statistic.message}') + +In the first example, we put a few of the parameters all on one line, and then +added the last two on their own. In the second example, each parameter has its +own line. This particular rule is a little subjective. The general idea is +that putting one parameter per-line is preferred, but sometimes it's +reasonable and understandable to group a few together on one line. + +Comments +-------- + +If you're adding an important comment, be sure to sign it. In |Flake8| we +generally sign comments by preceding them with ``NOTE()``. For example, + +.. code-block:: python + + # NOTE(sigmavirus24): The format strings are a little confusing, even + # to me, so here's a quick explanation: + # We specify the named value first followed by a ':' to indicate we're + # formatting the value. + # Next we use '<' to indicate we want the value left aligned. + # Then '10' is the width of the area. + # For floats, finally, we only want only want at most 3 digits after + # the decimal point to be displayed. This is the precision and it + # can not be specified for integers which is why we need two separate + # format strings. + float_format = '{value:<10.3} {statistic}'.format + int_format = '{value:<10} {statistic}'.format + +Ian is well known across most websites as ``sigmavirus24`` so he signs his +comments that way. + +Verbs Belong in Function Names +------------------------------ + +|Flake8| prefers that functions have verbs in them. If you're writing a +function that returns a generator of files then ``generate_files`` will always +be preferable to ``make_files`` or ``files``. diff --git a/src/flake8-main/flake8-main/docs/source/internal/writing-documentation.rst b/src/flake8-main/flake8-main/docs/source/internal/writing-documentation.rst new file mode 100644 index 0000000..ee37517 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/internal/writing-documentation.rst @@ -0,0 +1,183 @@ +.. _docs-style: + +================================== + Writing Documentation for Flake8 +================================== + +The maintainers of |Flake8| believe strongly in benefit of style guides. +Hence, for all contributors who wish to work on our documentation, we've +put together a loose set of guidelines and best practices when adding to +our documentation. + + +View the docs locally before submitting +======================================= + +You can and should generate the docs locally before you submit a pull request +with your changes. You can build the docs by running: + +.. prompt:: bash + + tox -e docs + +From the directory containing the ``tox.ini`` file (which also contains the +``docs/`` directory that this file lives in). + +.. note:: + + If the docs don't build locally, they will not build in our continuous + integration system. We will generally not merge any pull request that + fails continuous integration. + + +Run the docs linter tests before submitting +=========================================== + +You should run the ``doc8`` linter job before you're ready to commit and fix +any errors found. + + +Capitalize Flake8 in prose +========================== + +We believe that by capitalizing |Flake8| in prose, we can help reduce +confusion between the command-line usage of ``flake8`` and the project. + +We also have defined a global replacement ``|Flake8|`` that should be used +and will replace each instance with ``:program:`Flake8```. + + +Use the prompt directive for command-line examples +================================================== + +When documenting something on the command-line, use the ``.. prompt::`` +directive to make it easier for users to copy and paste into their terminal. + +Example: + +.. code-block:: restructuredtext + + .. prompt:: bash + + flake8 --select E123,W503 dir/ + flake8 --ignore E24,W504 dir + + +Wrap lines around 79 characters +=============================== + +We use a maximum line-length in our documentation that is similar to the +default in |Flake8|. Please wrap lines at 79 characters (or less). + + +Use two new-lines before new sections +===================================== + +After the final paragraph of a section and before the next section title, +use two new-lines to separate them. This makes reading the plain-text +document a little nicer. Sphinx ignores these when rendering so they have +no semantic meaning. + +Example: + +.. code-block:: restructuredtext + + Section Header + ============== + + Paragraph. + + + Next Section Header + =================== + + Paragraph. + + +Surround document titles with equal symbols +=========================================== + +To indicate the title of a document, we place an equal number of ``=`` symbols +on the lines before and after the title. For example: + +.. code-block:: restructuredtext + + ================================== + Writing Documentation for Flake8 + ================================== + +Note also that we "center" the title by adding a leading space and having +extra ``=`` symbols at the end of those lines. + + +Use the option template for new options +======================================= + +All of |Flake8|'s command-line options are documented in the User Guide. Each +option is documented individually using the ``.. option::`` directive provided +by Sphinx. At the top of the document, in a reStructuredText comment, is a +template that should be copied and pasted into place when documening new +options. + +.. note:: + + The ordering of the options page is the order that options are printed + in the output of: + + .. prompt:: bash + + flake8 --help + + Please insert your option documentation according to that order. + + +Use anchors for easy reference linking +====================================== + +Use link anchors to allow for other areas of the documentation to use the +``:ref:`` role for intralinking documentation. Example: + +.. code-block:: restructuredtext + + .. _use-anchors: + + Use anchors for easy reference linking + ====================================== + +.. code-block:: restructuredtext + + Somewhere in this paragraph we will :ref:`reference anchors + `. + +.. note:: + + You do not need to provide custom text for the ``:ref:`` if the title of + the section has a title that is sufficient. + + +Keep your audience in mind +========================== + +|Flake8|'s documentation has three distinct (but not separate) audiences: + +#. Users + +#. Plugin Developers + +#. Flake8 Developers and Contributors + +At the moment, you're one of the third group (because you're contributing +or thinking of contributing). + +Consider that most Users aren't very interested in the internal working of +|Flake8|. When writing for Users, focus on how to do something or the +behaviour of a certain piece of configuration or invocation. + +Plugin developers will only care about the internals of |Flake8| as much as +they will have to interact with that. Keep discussions of internal to the +mininmum required. + +Finally, Flake8 Developers and Contributors need to know how everything fits +together. We don't need detail about every line of code, but cogent +explanations and design specifications will help future developers understand +the Hows and Whys of |Flake8|'s internal design. diff --git a/src/flake8-main/flake8-main/docs/source/plugin-development/.keep b/src/flake8-main/flake8-main/docs/source/plugin-development/.keep new file mode 100644 index 0000000..e69de29 diff --git a/src/flake8-main/flake8-main/docs/source/plugin-development/formatters.rst b/src/flake8-main/flake8-main/docs/source/plugin-development/formatters.rst new file mode 100644 index 0000000..8133567 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/plugin-development/formatters.rst @@ -0,0 +1,54 @@ +.. _formatting-plugins: + +=========================================== + Developing a Formatting Plugin for Flake8 +=========================================== + +|Flake8| allowed for custom formatting plugins in version +3.0.0. Let's write a plugin together: + +.. code-block:: python + + from flake8.formatting import base + + + class Example(base.BaseFormatter): + """Flake8's example formatter.""" + + pass + +We notice, as soon as we start, that we inherit from |Flake8|'s +:class:`~flake8.formatting.base.BaseFormatter` class. If we follow the +:ref:`instructions to register a plugin ` and try to use +our example formatter, e.g., ``flake8 --format=example`` then +|Flake8| will fail because we did not implement the ``format`` method. +Let's do that next. + +.. code-block:: python + + class Example(base.BaseFormatter): + """Flake8's example formatter.""" + + def format(self, error): + return 'Example formatter: {0!r}'.format(error) + +With that we're done. Obviously this isn't a very useful formatter, but it +should highlight the simplicity of creating a formatter with Flake8. If we +wanted to instead create a formatter that aggregated the results and returned +XML, JSON, or subunit we could also do that. |Flake8| interacts with the +formatter in two ways: + +#. It creates the formatter and provides it the options parsed from the + configuration files and command-line + +#. It uses the instance of the formatter and calls ``handle`` with the error. + +By default :meth:`flake8.formatting.base.BaseFormatter.handle` simply calls +the ``format`` method and then ``write``. Any extra handling you wish to do +for formatting purposes should override the ``handle`` method. + +API Documentation +================= + +.. autoclass:: flake8.formatting.base.BaseFormatter + :members: diff --git a/src/flake8-main/flake8-main/docs/source/plugin-development/index.rst b/src/flake8-main/flake8-main/docs/source/plugin-development/index.rst new file mode 100644 index 0000000..9088942 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/plugin-development/index.rst @@ -0,0 +1,67 @@ +============================ + Writing Plugins for Flake8 +============================ + +Since |Flake8| 2.0, the |Flake8| tool has allowed for extensions and custom +plugins. In |Flake8| 3.0, we're expanding that ability to customize and +extend **and** we're attempting to thoroughly document it. Some of the +documentation in this section may reference third-party documentation to +reduce duplication and to point you, the developer, towards the authoritative +documentation for those pieces. + +Getting Started +=============== + +To get started writing a |Flake8| :term:`plugin` you first need: + +- An idea for a plugin + +- An available package name on PyPI + +- One or more versions of Python installed + +- A text editor or IDE of some kind + +- An idea of what *kind* of plugin you want to build: + + * Formatter + + * Check + +Once you've gathered these things, you can get started. + +All plugins for |Flake8| must be registered via +:external+packaging:doc:`entry points`. In this +section we cover: + +- How to register your plugin so |Flake8| can find it + +- How to make |Flake8| provide your check plugin with information (via + command-line flags, function/class parameters, etc.) + +- How to make a formatter plugin + +- How to write your check plugin so that it works with |Flake8| 2.x and 3.x + + +Video Tutorial +============== + +Here's a tutorial which goes over building an ast checking plugin from scratch: + +.. raw:: html + +
+ +
+ +Detailed Plugin Development Documentation +========================================= + +.. toctree:: + :caption: Plugin Developer Documentation + :maxdepth: 2 + + registering-plugins + plugin-parameters + formatters diff --git a/src/flake8-main/flake8-main/docs/source/plugin-development/plugin-parameters.rst b/src/flake8-main/flake8-main/docs/source/plugin-development/plugin-parameters.rst new file mode 100644 index 0000000..931c186 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/plugin-development/plugin-parameters.rst @@ -0,0 +1,178 @@ +.. _plugin-parameters: + +========================================== + Receiving Information For A Check Plugin +========================================== + +Plugins to |Flake8| have a great deal of information that they can request +from a :class:`~flake8.processor.FileProcessor` instance. Historically, +|Flake8| has supported two types of plugins: + +#. classes that accept parsed abstract syntax trees (ASTs) + +#. functions that accept a range of arguments + +|Flake8| now does not distinguish between the two types of plugins. Any plugin +can accept either an AST or a range of arguments. Further, any plugin that has +certain callable attributes can also register options and receive parsed +options. + + +Indicating Desired Data +======================= + +|Flake8| inspects the plugin's signature to determine what parameters it +expects using :func:`flake8.plugins.finder._parameters_for`. +:attr:`flake8.plugins.finder.LoadedPlugin.parameters` caches the values so that +each plugin makes that fairly expensive call once per plugin. When processing +a file, a plugin can ask for any of the following: + +- :attr:`~flake8.processor.FileProcessor.blank_before` +- :attr:`~flake8.processor.FileProcessor.blank_lines` +- :attr:`~flake8.processor.FileProcessor.checker_state` +- :attr:`~flake8.processor.FileProcessor.indent_char` +- :attr:`~flake8.processor.FileProcessor.indent_level` +- :attr:`~flake8.processor.FileProcessor.line_number` +- :attr:`~flake8.processor.FileProcessor.logical_line` +- :attr:`~flake8.processor.FileProcessor.multiline` +- :attr:`~flake8.processor.FileProcessor.noqa` +- :attr:`~flake8.processor.FileProcessor.previous_indent_level` +- :attr:`~flake8.processor.FileProcessor.previous_logical` +- :attr:`~flake8.processor.FileProcessor.previous_unindented_logical_line` +- :attr:`~flake8.processor.FileProcessor.tokens` + +Some properties are set once per file for plugins which iterate itself over +the data instead of being called on each physical or logical line. + +- :attr:`~flake8.processor.FileProcessor.filename` +- :attr:`~flake8.processor.FileProcessor.file_tokens` +- :attr:`~flake8.processor.FileProcessor.lines` +- :attr:`~flake8.processor.FileProcessor.max_line_length` +- :attr:`~flake8.processor.FileProcessor.max_doc_length` +- :attr:`~flake8.processor.FileProcessor.total_lines` +- :attr:`~flake8.processor.FileProcessor.verbose` + +These parameters can also be supplied to plugins working on each line +separately. + +Plugins that depend on ``physical_line`` or ``logical_line`` are run on each +physical or logical line once. These parameters should be the first in the +list of arguments (with the exception of ``self``). Plugins that need an AST +(e.g., PyFlakes and McCabe) should depend on ``tree``. These plugins will run +once per file. The parameters listed above can be combined with +``physical_line``, ``logical_line``, and ``tree``. + + +Registering Options +=================== + +Any plugin that has callable attributes ``add_options`` and +``parse_options`` can parse option information and register new options. + +Your ``add_options`` function should expect to receive an instance of +|OptionManager|. An |OptionManager| instance behaves very similarly to +:class:`optparse.OptionParser`. It, however, uses the layer that |Flake8| has +developed on top of :mod:`argparse` to also handle configuration file parsing. +:meth:`~flake8.options.manager.OptionManager.add_option` creates an |Option| +which accepts the same parameters as :mod:`optparse` as well as three extra +boolean parameters: + +- ``parse_from_config`` + + The command-line option should also be parsed from config files discovered + by |Flake8|. + + .. note:: + + This takes the place of appending strings to a list on the + :class:`optparse.OptionParser`. + +- ``comma_separated_list`` + + The value provided to this option is a comma-separated list. After parsing + the value, it should be further broken up into a list. This also allows us + to handle values like: + + .. code:: + + E123,E124, + E125, + E126 + +- ``normalize_paths`` + + The value provided to this option is a path. It should be normalized to be + an absolute path. This can be combined with ``comma_separated_list`` to + allow a comma-separated list of paths. + +Each of these options works individually or can be combined. Let's look at a +couple examples from |Flake8|. In each example, we will have +``option_manager`` which is an instance of |OptionManager|. + +.. code-block:: python + + option_manager.add_option( + '--max-line-length', type='int', metavar='n', + default=defaults.MAX_LINE_LENGTH, parse_from_config=True, + help='Maximum allowed line length for the entirety of this run. ' + '(Default: %(default)s)', + ) + +Here we are adding the ``--max-line-length`` command-line option which is +always an integer and will be parsed from the configuration file. Since we +provide a default, we take advantage of :mod:`argparse`\ 's willingness to +display that in the help text with ``%(default)s``. + +.. code-block:: python + + option_manager.add_option( + '--select', metavar='errors', default='', + parse_from_config=True, comma_separated_list=True, + help='Comma-separated list of errors and warnings to enable.' + ' For example, ``--select=E4,E51,W234``. (Default: %(default)s)', + ) + +In adding the ``--select`` command-line option, we're also indicating to the +|OptionManager| that we want the value parsed from the config files and parsed +as a comma-separated list. + +.. code-block:: python + + option_manager.add_option( + '--exclude', metavar='patterns', default=defaults.EXCLUDE, + comma_separated_list=True, parse_from_config=True, + normalize_paths=True, + help='Comma-separated list of files or directories to exclude.' + '(Default: %(default)s)', + ) + +Finally, we show an option that uses all three extra flags. Values from +``--exclude`` will be parsed from the config, converted from a comma-separated +list, and then each item will be normalized. + +For information about other parameters to +:meth:`~flake8.options.manager.OptionManager.add_option` refer to the +documentation of :mod:`argparse`. + + +Accessing Parsed Options +======================== + +When a plugin has a callable ``parse_options`` attribute, |Flake8| will call +it and attempt to provide the |OptionManager| instance, the parsed options +which will be an instance of :class:`argparse.Namespace`, and the extra +arguments that were not parsed by the |OptionManager|. If that fails, we will +just pass the :class:`argparse.Namespace`. In other words, your +``parse_options`` callable will have one of the following signatures: + +.. code-block:: python + + def parse_options(option_manager, options, args): + pass + # or + def parse_options(options): + pass + +.. substitutions +.. |OptionManager| replace:: :class:`~flake8.options.manager.OptionManager` +.. |Option| replace:: :class:`~flake8.options.manager.Option` diff --git a/src/flake8-main/flake8-main/docs/source/plugin-development/registering-plugins.rst b/src/flake8-main/flake8-main/docs/source/plugin-development/registering-plugins.rst new file mode 100644 index 0000000..964ff99 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/plugin-development/registering-plugins.rst @@ -0,0 +1,157 @@ +.. _register-a-plugin: + +================================== + Registering a Plugin with Flake8 +================================== + +To register any kind of plugin with |Flake8|, you need: + +#. A way to install the plugin (whether it is packaged on its own or + as part of something else). In this section, we will use a ``setup.py`` + written for an example plugin. + +#. A name for your plugin that will (ideally) be unique. + +|Flake8| relies on functionality provided by build tools called +:external+packaging:doc:`entry points`. These +allow any package to register a plugin with |Flake8| via that package's +metadata. + +Let's presume that we already have our plugin written and it's in a module +called ``flake8_example``. We will also assume ``setuptools`` is used as a +:external+packaging:term:`Build Backend`, but be aware that most backends +support entry points. + +We might have a ``setup.py`` that looks something like: + +.. code-block:: python + + import setuptools + + requires = [ + "flake8 > 3.0.0", + ] + + flake8_entry_point = # ... + + setuptools.setup( + name="flake8_example", + license="MIT", + version="0.1.0", + description="our extension to flake8", + author="Me", + author_email="example@example.com", + url="https://github.com/me/flake8_example", + packages=[ + "flake8_example", + ], + install_requires=requires, + entry_points={ + flake8_entry_point: [ + 'X = flake8_example:ExamplePlugin', + ], + }, + classifiers=[ + "Framework :: Flake8", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Quality Assurance", + ], + ) + +Note specifically these lines: + +.. code-block:: python + + flake8_entry_point = # ... + + setuptools.setup( + # snip ... + entry_points={ + flake8_entry_point: [ + 'X = flake8_example:ExamplePlugin', + ], + }, + # snip ... + ) + +We tell setuptools to register our entry point ``X`` inside the specific +grouping of entry-points that flake8 should look in. + +|Flake8| presently looks at two groups: + +- ``flake8.extension`` + +- ``flake8.report`` + +If your plugin is one that adds checks to |Flake8|, you will use +``flake8.extension``. If your plugin performs extra report +handling (formatting, filtering, etc.) it will use ``flake8.report``. + +If our ``ExamplePlugin`` is something that adds checks, our code would look +like: + +.. code-block:: python + + setuptools.setup( + # snip ... + entry_points={ + 'flake8.extension': [ + 'X = flake8_example:ExamplePlugin', + ], + }, + # snip ... + ) + +The ``X`` in checking plugins define what error codes it is going to report. +So if the plugin reports only the error code ``X101`` your entry-point would +look like:: + + X101 = flake8_example:ExamplePlugin + +In the above case, the entry-point name and the error code produced by your +plugin are the same. + +If your plugin reports several error codes that all start with ``X10``, then +it would look like:: + + X10 = flake8_example:ExamplePlugin + +In this case as well as the following case, your entry-point name acts as +a prefix to the error codes produced by your plugin. + +If all of your plugin's error codes start with ``X1`` then it would look +like:: + + X1 = flake8_example:ExamplePlugin + +Finally, if all of your plugin's error codes start with just ``X`` then it +would look like the original example. + +|Flake8| requires each entry point to be unique amongst all plugins installed +in the users environment. Selecting an entry point that is already used can +cause plugins to be deactivated without warning! + +**Please Note:** Your entry point does not need to be exactly 4 characters +as of |Flake8| 3.0. Single letter entry point prefixes (such as the +'X' in the examples above) have caused issues in the past. As such, +please consider using a 2 or 3 character entry point prefix, +i.e., ``ABC`` is better than ``A`` but ``ABCD`` is invalid. +*A 3 letters entry point prefix followed by 3 numbers (i.e.* ``ABC123`` *) +is currently the longest allowed entry point name.* + +.. _off-by-default: + +If your plugin is intended to be opt-in, it can set the attribute +``off_by_default = True``. Users of your plugin will then need to utilize +:ref:`enable-extensions` with your plugin's entry +point. + +.. seealso:: + + The :external+setuptools:doc:`setuptools user guide ` + about entry points. diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/0.6.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/0.6.0.rst new file mode 100644 index 0000000..16590fa --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/0.6.0.rst @@ -0,0 +1,4 @@ +0.6 - 2010-02-15 +---------------- + +- Fix the McCabe metric on some loops diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/0.7.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/0.7.0.rst new file mode 100644 index 0000000..f842060 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/0.7.0.rst @@ -0,0 +1,6 @@ +0.7 - 2010-02-18 +---------------- + +- Fix pep8 initialization when run through Hg +- Make pep8 short options work when run through the command line +- Skip duplicates when controlling files via Hg diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/0.8.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/0.8.0.rst new file mode 100644 index 0000000..66b90b5 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/0.8.0.rst @@ -0,0 +1,5 @@ +0.8 - 2011-02-27 +---------------- + +- fixed hg hook +- discard unexisting files on hook check diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/0.9.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/0.9.0.rst new file mode 100644 index 0000000..be6c41b --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/0.9.0.rst @@ -0,0 +1,5 @@ +0.9 - 2011-11-09 +---------------- + +- update pep8 version to 0.6.1 +- mccabe check: gracefully handle compile failure diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/1.0.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/1.0.0.rst new file mode 100644 index 0000000..6882a43 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/1.0.0.rst @@ -0,0 +1,5 @@ +1.0 - 2011-11-29 +---------------- + +- Deactivates by default the complexity checker +- Introduces the complexity option in the HG hook and the command line. diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/1.1.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/1.1.0.rst new file mode 100644 index 0000000..dadbe2e --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/1.1.0.rst @@ -0,0 +1,8 @@ +1.1 - 2012-02-14 +---------------- + +- fixed the value returned by --version +- allow the flake8: header to be more generic +- fixed the "hg hook raises 'physical lines'" bug +- allow three argument form of raise +- now uses setuptools if available, for 'develop' command diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/1.2.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/1.2.0.rst new file mode 100644 index 0000000..de86237 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/1.2.0.rst @@ -0,0 +1,6 @@ +1.2 - 2012-02-12 +---------------- + +- added a git hook +- now Python 3 compatible +- mccabe and pyflakes have warning codes like pep8 now diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/1.3.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/1.3.0.rst new file mode 100644 index 0000000..0ddfe78 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/1.3.0.rst @@ -0,0 +1,4 @@ +1.3 - 2012-03-12 +---------------- + +- fixed false W402 warning on exception blocks. diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/1.3.1.rst b/src/flake8-main/flake8-main/docs/source/release-notes/1.3.1.rst new file mode 100644 index 0000000..b2e34ee --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/1.3.1.rst @@ -0,0 +1,4 @@ +1.3.1 - 2012-05-19 +------------------ + +- fixed support for Python 2.5 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/1.4.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/1.4.0.rst new file mode 100644 index 0000000..51f34cf --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/1.4.0.rst @@ -0,0 +1,5 @@ +1.4 - 2012-07-12 +---------------- + +- git_hook: Only check staged changes for compliance +- use pep8 1.2 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/1.5.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/1.5.0.rst new file mode 100644 index 0000000..cd0a88d --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/1.5.0.rst @@ -0,0 +1,9 @@ +1.5 - 2012-10-13 +---------------- + +- fixed the stdin +- make sure mccabe catches the syntax errors as warnings +- pep8 upgrade +- added max_line_length default value +- added Flake8Command and entry points if setuptools is around +- using the setuptools console wrapper when available diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/1.6.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/1.6.0.rst new file mode 100644 index 0000000..658be2f --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/1.6.0.rst @@ -0,0 +1,14 @@ +1.6 - 2012-11-16 +---------------- + +- changed the signatures of the ``check_file`` function in flake8/run.py, + ``skip_warning`` in flake8/util.py and the ``check``, ``checkPath`` + functions in flake8/pyflakes.py. +- fix ``--exclude`` and ``--ignore`` command flags (#14, #19) +- fix the git hook that wasn't catching files not already added to the index + (#29) +- pre-emptively includes the addition to pep8 to ignore certain lines. + Add ``# nopep8`` to the end of a line to ignore it. (#37) +- ``check_file`` can now be used without any special prior setup (#21) +- unpacking exceptions will no longer cause an exception (#20) +- fixed crash on non-existent file (#38) diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/1.6.1.rst b/src/flake8-main/flake8-main/docs/source/release-notes/1.6.1.rst new file mode 100644 index 0000000..194dfa5 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/1.6.1.rst @@ -0,0 +1,7 @@ +1.6.1 - 2012-11-24 +------------------ + +- fixed the mercurial hook, a change from a previous patch was not properly + applied +- fixed an assumption about warnings/error messages that caused an exception + to be thrown when McCabe is used diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/1.6.2.rst b/src/flake8-main/flake8-main/docs/source/release-notes/1.6.2.rst new file mode 100644 index 0000000..64633bd --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/1.6.2.rst @@ -0,0 +1,4 @@ +1.6.2 - 2012-11-25 +------------------ + +- fixed the NameError: global name 'message' is not defined (#46) diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/1.7.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/1.7.0.rst new file mode 100644 index 0000000..a3a4725 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/1.7.0.rst @@ -0,0 +1,9 @@ +1.7.0 - 2012-12-21 +------------------ + +- Fixes part of #35: Exception for no WITHITEM being an attribute of Checker + for Python 3.3 +- Support stdin +- Incorporate @phd's builtins pull request +- Fix the git hook +- Update pep8.py to the latest version diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/2.0.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/2.0.0.rst new file mode 100644 index 0000000..67169d3 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/2.0.0.rst @@ -0,0 +1,13 @@ +2.0.0 - 2013-02-23 +------------------ + +- Pyflakes errors are prefixed by an ``F`` instead of an ``E`` +- McCabe complexity warnings are prefixed by a ``C`` instead of a ``W`` +- Flake8 supports extensions through entry points +- Due to the above support, we **require** setuptools +- We publish the `documentation `_ +- Fixes #13: pep8, pyflakes and mccabe become external dependencies +- Split run.py into main.py, engine.py and hooks.py for better logic +- Expose our parser for our users +- New feature: Install git and hg hooks automagically +- By relying on pyflakes (0.6.1), we also fixed #45 and #35 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/2.1.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/2.1.0.rst new file mode 100644 index 0000000..c9e3c60 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/2.1.0.rst @@ -0,0 +1,12 @@ +2.1.0 - 2013-10-26 +------------------ + +- Add FLAKE8_LAZY and FLAKE8_IGNORE environment variable support to git and + mercurial hooks +- Force git and mercurial hooks to repsect configuration in setup.cfg +- Only check staged files if that is specified +- Fix hook file permissions +- Fix the git hook on python 3 +- Ignore non-python files when running the git hook +- Ignore .tox directories by default +- Flake8 now reports the column number for PyFlakes messages diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/2.2.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/2.2.0.rst new file mode 100644 index 0000000..357b6d2 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/2.2.0.rst @@ -0,0 +1,12 @@ +2.2.0 - 2014-06-22 +------------------ + +- New option ``doctests`` to run Pyflakes checks on doctests too +- New option ``jobs`` to launch multiple jobs in parallel +- Turn on using multiple jobs by default using the CPU count +- Add support for ``python -m flake8`` on Python 2.7 and Python 3 +- Fix Git and Mercurial hooks: issues #88, #133, #148 and #149 +- Fix crashes with Python 3.4 by upgrading dependencies +- Fix traceback when running tests with Python 2.6 +- Fix the setuptools command ``python setup.py flake8`` to read + the project configuration diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/2.2.1.rst b/src/flake8-main/flake8-main/docs/source/release-notes/2.2.1.rst new file mode 100644 index 0000000..5575f8f --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/2.2.1.rst @@ -0,0 +1,5 @@ +2.2.1 - 2014-06-30 +------------------ + +- Turn off multiple jobs by default. To enable automatic use of all CPUs, use + ``--jobs=auto``. Fixes #155 and #154. diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/2.2.2.rst b/src/flake8-main/flake8-main/docs/source/release-notes/2.2.2.rst new file mode 100644 index 0000000..8fcff88 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/2.2.2.rst @@ -0,0 +1,5 @@ +2.2.2 - 2014-07-04 +------------------ + +- Re-enable multiprocessing by default while fixing the issue Windows users + were seeing. diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/2.2.3.rst b/src/flake8-main/flake8-main/docs/source/release-notes/2.2.3.rst new file mode 100644 index 0000000..e7430f0 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/2.2.3.rst @@ -0,0 +1,4 @@ +2.2.3 - 2014-08-25 +------------------ + +- Actually turn multiprocessing on by default diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/2.2.4.rst b/src/flake8-main/flake8-main/docs/source/release-notes/2.2.4.rst new file mode 100644 index 0000000..2564948 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/2.2.4.rst @@ -0,0 +1,20 @@ +2.2.4 - 2014-10-09 +------------------ + +- Fix bugs triggered by turning multiprocessing on by default (again) + + Multiprocessing is forcibly disabled in the following cases: + + - Passing something in via stdin + + - Analyzing a diff + + - Using windows + +- Fix --install-hook when there are no config files present for pep8 or + flake8. + +- Fix how the setuptools command parses excludes in config files + +- Fix how the git hook determines which files to analyze (Thanks Chris + Buccella!) diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/2.2.5.rst b/src/flake8-main/flake8-main/docs/source/release-notes/2.2.5.rst new file mode 100644 index 0000000..540278f --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/2.2.5.rst @@ -0,0 +1,6 @@ +2.2.5 - 2014-10-19 +------------------ + +- Flush standard out when using multiprocessing + +- Make the check for "# flake8: noqa" more strict diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/2.3.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/2.3.0.rst new file mode 100644 index 0000000..341d06e --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/2.3.0.rst @@ -0,0 +1,8 @@ +2.3.0 - 2015-01-04 +------------------ + +- **Feature**: Add ``--output-file`` option to specify a file to write to + instead of ``stdout``. + +- **Bug** Fix interleaving of output while using multiprocessing + (:issue:`60`) diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/2.4.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/2.4.0.rst new file mode 100644 index 0000000..f8860dc --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/2.4.0.rst @@ -0,0 +1,26 @@ +2.4.0 - 2015-03-07 +------------------ + +- **Bug** Print filenames when using multiprocessing and ``-q`` option. + (:issue:`74`) + +- **Bug** Put upper cap on dependencies. The caps for 2.4.0 are: + + - ``pep8 < 1.6`` (Related to :issue:`78`) + + - ``mccabe < 0.4`` + + - ``pyflakes < 0.9`` + + See also :issue:`75` + +- **Bug** Files excluded in a config file were not being excluded when flake8 + was run from a git hook. (:issue:`2`) + +- **Improvement** Print warnings for users who are providing mutually + exclusive options to flake8. (:issue:`51`, :issue:`386`) + +- **Feature** Allow git hook configuration to live in ``.git/config``. + See the updated `VCS hooks docs`_ for more details. (:issue:`387`) + +.. _VCS hooks docs: https://flake8.readthedocs.io/en/latest/user/using-hooks.html diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/2.4.1.rst b/src/flake8-main/flake8-main/docs/source/release-notes/2.4.1.rst new file mode 100644 index 0000000..0e74929 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/2.4.1.rst @@ -0,0 +1,9 @@ +2.4.1 - 2015-05-18 +------------------ + +- **Bug** Do not raise a ``SystemError`` unless there were errors in the + setuptools command. (:issue:`82`, :issue:`390`) + +- **Bug** Do not verify dependencies of extensions loaded via entry-points. + +- **Improvement** Blacklist versions of pep8 we know are broken diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/2.5.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/2.5.0.rst new file mode 100644 index 0000000..dafd416 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/2.5.0.rst @@ -0,0 +1,18 @@ +2.5.0 - 2015-10-26 +------------------ + +- **Improvement** Raise cap on PyFlakes for Python 3.5 support + +- **Improvement** Avoid deprecation warnings when loading extensions + (:issue:`102`, :issue:`445`) + +- **Improvement** Separate logic to enable "off-by-default" extensions + (:issue:`110`) + +- **Bug** Properly parse options to setuptools Flake8 command (:issue:`408`) + +- **Bug** Fix exceptions when output on stdout is truncated before Flake8 + finishes writing the output (:issue:`112`) + +- **Bug** Fix error on OS X where Flake8 can no longer acquire or create new + semaphores (:issue:`117`) diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/2.5.1.rst b/src/flake8-main/flake8-main/docs/source/release-notes/2.5.1.rst new file mode 100644 index 0000000..cd73315 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/2.5.1.rst @@ -0,0 +1,9 @@ +2.5.1 - 2015-12-08 +------------------ + +- **Bug** Properly look for ``.flake8`` in current working directory + (:issue:`458`) + +- **Bug** Monkey-patch ``pep8.stdin_get_value`` to cache the actual value in + stdin. This helps plugins relying on the function when run with + multiprocessing. (:issue:`460`, :issue:`462`) diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/2.5.2.rst b/src/flake8-main/flake8-main/docs/source/release-notes/2.5.2.rst new file mode 100644 index 0000000..a093c9f --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/2.5.2.rst @@ -0,0 +1,7 @@ +2.5.2 - 2016-01-30 +------------------ + +- **Bug** Parse ``output_file`` and ``enable_extensions`` from config files + +- **Improvement** Raise upper bound on mccabe plugin to allow for version + 0.4.0 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/2.5.3.rst b/src/flake8-main/flake8-main/docs/source/release-notes/2.5.3.rst new file mode 100644 index 0000000..85dbf33 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/2.5.3.rst @@ -0,0 +1,5 @@ +2.5.3 - 2016-02-11 +------------------ + +- **Bug** Actually parse ``output_file`` and ``enable_extensions`` from config + files diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/2.5.4.rst b/src/flake8-main/flake8-main/docs/source/release-notes/2.5.4.rst new file mode 100644 index 0000000..5ba03ba --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/2.5.4.rst @@ -0,0 +1,4 @@ +2.5.4 - 2016-02-11 +------------------ + +- **Bug** Missed an attribute rename during the v2.5.3 release. diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/2.5.5.rst b/src/flake8-main/flake8-main/docs/source/release-notes/2.5.5.rst new file mode 100644 index 0000000..683cfb6 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/2.5.5.rst @@ -0,0 +1,7 @@ +2.5.5 - 2016-06-14 +------------------ + +- **Bug** Fix setuptools integration when parsing config files + +- **Bug** Don't pass the user's config path as the config_file when creating a + StyleGuide diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/2.6.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/2.6.0.rst new file mode 100644 index 0000000..77898e9 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/2.6.0.rst @@ -0,0 +1,15 @@ +2.6.0 - 2016-06-15 +------------------ + +- **Requirements Change** Switch to pycodestyle as all future pep8 releases + will use that package name + +- **Improvement** Allow for Windows users on *select* versions of Python to + use ``--jobs`` and multiprocessing + +- **Improvement** Update bounds on McCabe + +- **Improvement** Update bounds on PyFlakes and blacklist known broken + versions + +- **Improvement** Handle new PyFlakes warning with a new error code: F405 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/2.6.1.rst b/src/flake8-main/flake8-main/docs/source/release-notes/2.6.1.rst new file mode 100644 index 0000000..d05bd18 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/2.6.1.rst @@ -0,0 +1,6 @@ +2.6.1 - 2016-06-25 +------------------ + +- **Bug** Update the config files to search for to include ``setup.cfg`` and + ``tox.ini``. This was broken in 2.5.5 when we stopped passing + ``config_file`` to our Style Guide diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/2.6.2.rst b/src/flake8-main/flake8-main/docs/source/release-notes/2.6.2.rst new file mode 100644 index 0000000..0d0bdc1 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/2.6.2.rst @@ -0,0 +1,4 @@ +2.6.2 - 2016-06-25 +------------------ + +- **Bug** Fix packaging error during release process. diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.0.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.0.0.rst new file mode 100644 index 0000000..28c0b7f --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.0.0.rst @@ -0,0 +1,63 @@ +3.0.0 -- 2016-07-25 +------------------- + +- Rewrite our documentation from scratch! (https://flake8.pycqa.org) + +- Drop explicit support for Pythons 2.6, 3.2, and 3.3. + +- Remove dependence on pep8/pycodestyle for file processing, plugin + dispatching, and more. We now control all of this while keeping backwards + compatibility. + +- ``--select`` and ``--ignore`` can now both be specified and try to find the + most specific rule from each. For example, if you do ``--select E --ignore + E123`` then we will report everything that starts with ``E`` except for + ``E123``. Previously, you would have had to do ``--ignore E123,F,W`` which + will also still work, but the former should be far more intuitive. + +- Add support for in-line ``# noqa`` comments to specify **only** the error + codes to be ignored, e.g., ``# noqa: E123,W503`` + +- Add entry-point for formatters as well as a base class that new formatters + can inherit from. See the documentation for more details. + +- Add detailed verbose output using the standard library logging module. + +- Enhance our usage of optparse for plugin developers by adding new parameters + to the ``add_option`` that plugins use to register new options. + +- Update ``--install-hook`` to require the name of version control system hook + you wish to install a Flake8. + +- Stop checking sub-directories more than once via the setuptools command + +- When passing a file on standard-in, allow the caller to specify + ``--stdin-display-name`` so the output is properly formatted + +- The Git hook now uses ``sys.executable`` to format the shebang line. + This allows Flake8 to install a hook script from a virtualenv that points to + that virtualenv's Flake8 as opposed to a global one (without the virtualenv + being sourced). + +- Print results in a deterministic and consistent ordering when used with + multiprocessing + +- When using ``--count``, the output is no longer written to stderr. + +- AST plugins can either be functions or classes and all plugins can now + register options so long as there are callable attributes named as we + expect. + +- Stop forcibly re-adding ``.tox``, ``.eggs``, and ``*.eggs`` to + ``--exclude``. Flake8 2.x started always appending those three patterns + to any exclude list (including the default and any user supplied list). + Flake8 3 has stopped adding these in, so you may see errors when upgrading + due to these patterns no longer being forcibly excluded by default if you + have your own exclude patterns specified. + + To fix this, add the appropriate patterns to your exclude patterns list. + + .. note:: + + This item was added in November of 2016, as a result of a bug + report. diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.0.1.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.0.1.rst new file mode 100644 index 0000000..39b27be --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.0.1.rst @@ -0,0 +1,12 @@ +3.0.1 -- 2016-07-25 +------------------- + +- Fix regression in handling of ``# noqa`` for multiline strings. + (See also :issue:`1024`) + +- Fix regression in handling of ``--output-file`` when not also using + ``--verbose``. (See also :issue:`1026`) + +- Fix regression in handling of ``--quiet``. (See also :issue:`1026`) + +- Fix regression in handling of ``--statistics``. (See also :issue:`1026`) diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.0.2.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.0.2.rst new file mode 100644 index 0000000..3cf588a --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.0.2.rst @@ -0,0 +1,10 @@ +3.0.2 -- 2016-07-26 +------------------- + +- Fix local config file discovery. (See also :issue:`528`) + +- Fix indexing of column numbers. We accidentally were starting column indices + at 0 instead of 1. + +- Fix regression in handling of errors like E402 that rely on a combination of + attributes. (See also :issue:`530`) diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.0.3.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.0.3.rst new file mode 100644 index 0000000..74ee4c0 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.0.3.rst @@ -0,0 +1,32 @@ +3.0.3 -- 2016-07-30 +------------------- + +- Disable ``--jobs`` for any version of Python on Windows. + (See also `this Python bug report`_) + +- Raise exception when entry_point in plugin not callable. + This raises an informative error when a plugin fails to load because its + entry_point is not callable, which can happen with a plugin which is buggy or + not updated for the current version of flake8. This is nicer than raising a + `PicklingError` about failing to pickle a module (See also :issue:`1014`) + +- Fix ``# noqa`` comments followed by a ``:`` and explanation broken by + 3.0.0 (See also :issue:`1025`) + +- Always open our output file in append mode so we do not overwrite log + messages. (See also :issue:`535`) + +- When normalizing path values read from configuration, keep in context the + directory where the configuration was found so that relative paths work. + (See also :issue:`1036`) + +- Fix issue where users were unable to ignore plugin errors that were on + by default. (See also :issue:`1037`) + +- Fix our legacy API StyleGuide's ``init_report`` method to actually override + the previous formatter. (See also :issue:`136`) + + +.. links +.. _this Python bug report: + https://bugs.python.org/issue27649 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.0.4.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.0.4.rst new file mode 100644 index 0000000..3ad92b1 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.0.4.rst @@ -0,0 +1,11 @@ +3.0.4 -- 2016-08-08 +------------------- + +- Side-step a Pickling Error when using Flake8 with multiprocessing on Unix + systems. (See also :issue:`1014`) + +- Fix an Attribute Error raised when dealing with Invalid Syntax. (See also + :issue:`539`) + +- Fix an unhandled Syntax Error when tokenizing files. (See also + :issue:`540`) diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.1.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.1.0.rst new file mode 100644 index 0000000..c3ca92d --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.1.0.rst @@ -0,0 +1,52 @@ +3.1.0 -- 2016-11-14 +------------------- + +You can view the `3.1.0 milestone`_ on GitHub for more details. + +- Add ``--bug-report`` flag to make issue reporters' lives easier. + +- Collect configuration files from the current directory when using our Git + hook. (See also :issue:`142`, :issue:`150`, :issue:`155`) + +- Avoid unhandled exceptions when dealing with SyntaxErrors. (See also + :issue:`146`, :issue:`170`) + +- Exit early if the value for ``--diff`` is empty. (See also :issue:`158`) + +- Handle empty ``--stdin-display-name`` values. (See also :issue:`167`) + +- Properly report the column number of Syntax Errors. We were assuming that + all reports of column numbers were 0-indexed, however, SyntaxErrors report + the column number as 1-indexed. This caused us to report a column number + that was 1 past the actual position. Further, when combined with + SyntaxErrors that occur at a newline, this caused the position to be + visually off by two. (See also :issue:`169`) + +- Fix the behaviour of ``--enable-extensions``. Previously, items specified + here were still ignored due to the fact that the off-by-default extension + codes were being left in the ``ignore`` list. (See also :issue:`171`) + +- Fix problems around ``--select`` and ``--ignore`` behaviour that prevented + codes that were neither explicitly selected nor explicitly ignored from + being reported. (See also :issue:`174`) + +- Truly be quiet when the user specifies ``-q`` one or more times. Previously, + we were showing the if the user specified ``-q`` and ``--show-source``. We + have fixed this bug. (See also :issue:`177`) + +- Add new File Processor attribute, ``previous_unindented_logical_line`` to + accommodate pycodestyle 2.1.0. (See also :issue:`178`) + +- When something goes wrong, exit non-zero. (See also :issue:`180`, + :issue:`141`) + +- Add ``--tee`` as an option to allow use of ``--output-file`` and printing to + standard out. + +- Allow the git plugin to actually be lazy when collecting files. + +- Allow for pycodestyle 2.1 series and pyflakes 1.3 series. + +.. links +.. _3.1.0 milestone: + https://github.com/pycqa/flake8/milestone/12 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.1.1.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.1.1.rst new file mode 100644 index 0000000..a7f4081 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.1.1.rst @@ -0,0 +1,14 @@ +3.1.1 -- 2016-11-14 +------------------- + +You can view the `3.1.1 milestone`_ on GitHub for more details. + +- Do not attempt to install/distribute a ``man`` file with the Python package; + leave this for others to do. (See also :issue:`186`) + +- Fix packaging bug where wheel version constraints specified in setup.cfg did + not match the constraints in setup.py. (See also :issue:`187`) + +.. links +.. _3.1.1 milestone: + https://github.com/pycqa/flake8/milestone/13 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.2.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.2.0.rst new file mode 100644 index 0000000..a7599ef --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.2.0.rst @@ -0,0 +1,11 @@ +3.2.0 -- 2016-11-14 +------------------- + +You can view the `3.2.0 milestone`_ on GitHub for more details. + +- Allow for pycodestyle 2.2.0 which fixes a bug in E305 (See also + :issue:`188`) + +.. links +.. _3.2.0 milestone: + https://github.com/pycqa/flake8/milestone/14 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.2.1.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.2.1.rst new file mode 100644 index 0000000..0a4c3c7 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.2.1.rst @@ -0,0 +1,19 @@ +3.2.1 -- 2016-11-21 +------------------- + +You can view the `3.2.1 milestone`_ on GitHub for more details. + +- Fix subtle bug when deciding whether to report an on-by-default's violation + (See also :issue:`189`) + +- Fix another bug around SyntaxErrors not being reported at the right column + and row (See also :issue:`191` and :issue:`169` for a related, previously + fixed bug) + +- Fix regression from 2.x where we run checks against explicitly provided + files, even if they don't match the filename patterns. (See also + :issue:`198`) + +.. links +.. _3.2.1 milestone: + https://github.com/pycqa/flake8/milestone/15 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.3.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.3.0.rst new file mode 100644 index 0000000..431ab15 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.3.0.rst @@ -0,0 +1,37 @@ +3.3.0 -- 2017-02-06 +------------------- + +You can view the `3.3.0 milestone`_ on GitHub for more details. + +- Add support for Python 3.6 (via dependencies). **Note** Flake8 does not + guarantee that all plugins will support Python 3.6. + +- Added unique error codes for all missing PyFlakes messages. (14 new + codes, see "Error / Violation Codes") + +- Dramatically improve the performance of Flake8. (See also :issue:`829`) + +- Display the local file path instead of the temporary file path when + using the git hook. (See also :issue:`176`) + +- Add methods to Report class that will be called when Flake8 starts and + finishes processing a file. (See also :issue:`183`) + +- Fix problem where hooks should only check \*.py files. (See also + :issue:`200`) + +- Fix handling of SyntaxErrors that do not include physical line information. + (See also :issue:`542`) + +- Update upper bound on PyFlakes to allow for PyFlakes 1.5.0. (See also + :issue:`549`) + +- Update setuptools integration to less eagerly deduplicate packages. + (See also :issue:`552`) + +- Force ``flake8 --version`` to be repeatable between invocations. (See also + :issue:`554`) + +.. all links +.. _3.3.0 milestone: + https://github.com/pycqa/flake8/milestone/16 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.4.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.4.0.rst new file mode 100644 index 0000000..c4a53d0 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.4.0.rst @@ -0,0 +1,25 @@ +3.4.0 -- 2017-07-27 +------------------- + +You can view the `3.4.0 milestone`_ on GitHub for more details. + +- Refine logic around ``--select`` and ``--ignore`` when combined with the + default values for each. (See also :issue:`572`) + +- Handle spaces as an alternate separate for error codes, e.g., + ``--ignore 'E123 E234'``. (See also :issue:`580`) + +- Filter out empty select and ignore codes, e.g., ``--ignore E123,,E234``. + (See also :issue:`581`) + +- Specify dependencies appropriately in ``setup.py`` (See also :issue:`592`) + +- Fix bug in parsing ``--quiet`` and ``--verbose`` from config files. + (See also :issue:`1169`) + +- Remove unused import of ``os`` in the git hook template (See also + :issue:`1170`) + +.. all links +.. _3.4.0 milestone: + https://github.com/pycqa/flake8/milestone/17 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.4.1.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.4.1.rst new file mode 100644 index 0000000..dbbb437 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.4.1.rst @@ -0,0 +1,11 @@ +3.4.1 -- 2017-07-28 +------------------- + +You can view the `3.4.1 milestone`_ on GitHub for more details. + +- Fix minor regression when users specify only a ``--select`` list with items + in the enabled/extended select list. (See also :issue:`605`) + +.. all links +.. _3.4.1 milestone: + https://github.com/pycqa/flake8/milestone/18 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.5.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.5.0.rst new file mode 100644 index 0000000..75a5b70 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.5.0.rst @@ -0,0 +1,32 @@ +3.5.0 -- 2017-10-23 +------------------- + +You can view the `3.5.0 milestone`_ on GitHub for more details. + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Allow for PyFlakes 1.6.0 (See also :issue:`1058`) + +- Start using new PyCodestyle checks for bare excepts and ambiguous identifier + (See also :issue:`611`) + +Features +~~~~~~~~ + +- Print out information about configuring VCS hooks (See also :issue:`586`) + +- Allow users to develop plugins "local" to a repository without using + setuptools. See our documentation on local plugins for more information. + (See also :issue:`608`) + +Bugs Fixed +~~~~~~~~~~ + +- Catch and helpfully report ``UnicodeDecodeError``\ s when parsing + configuration files. (See also :issue:`609`) + + +.. all links +.. _3.5.0 milestone: + https://github.com/pycqa/flake8/milestone/19 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.6.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.6.0.rst new file mode 100644 index 0000000..140a3bf --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.6.0.rst @@ -0,0 +1,61 @@ +3.6.0 -- 2018-10-23 +------------------- + +You can view the `3.6.0 milestone`_ on GitHub for more details. + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- pycodestyle has been updated to >= 2.4.0, < 2.5.0 (See also :issue:`1068`, + :issue:`652`, :issue:`869`, :issue:`881`, :issue:`1239`) + +- Pyflakes has been updated to >= 2.0.0, < 2.1.0 (See also :issue:`655`, + :issue:`883`) + +- flake8 requires python 2.x >= 2.7 or python 3.x >= 3.4 (See also + :issue:`876`) + +Features +~~~~~~~~ + +- Add ``paths`` to allow local plugins to exist outside of ``sys.path`` (See + also :issue:`1067`, :issue:`1237`) + +- Copy ``setup.cfg`` files to the temporary git hook execution directory (See + also :issue:`1299`) + +- Only skip a file if ``# flake8: noqa`` is on a line by itself (See also + :issue:`259`, :issue:`873`) + +- Provide a better user experience for broken plugins (See also :issue:`1178`) + +- Report ``E902`` when a file passed on the command line does not exist (See + also :issue:`645`, :issue:`878`) + +- Add ``--extend-ignore`` for extending the default ``ignore`` instead of + overriding it (See also :issue:`1061`, :issue:`1180`) + +Bugs Fixed +~~~~~~~~~~ + +- Respect a formatter's newline setting when printing (See also :issue:`1238`) + +- Fix leaking of processes in the legacy api (See also :issue:`650`, + :issue:`879`) + +- Fix a ``SyntaxWarning`` for an invalid escape sequence (See also + :issue:`1186`) + +- Fix ``DeprecationWarning`` due to import of ``abc`` classes from the + ``collections`` module (See also :issue:`887`) + +- Defer ``setuptools`` import to improve flake8 startup time (See also + :issue:`1190`) + +- Fix inconsistent line endings in ``FileProcessor.lines`` when running under + python 3.x (See also :issue:`263`, :issue:`889`) + + +.. all links +.. _3.6.0 milestone: + https://github.com/pycqa/flake8/milestone/20 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.7.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.7.0.rst new file mode 100644 index 0000000..024b90c --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.7.0.rst @@ -0,0 +1,45 @@ +3.7.0 -- 2019-01-29 +------------------- + +You can view the `3.7.0 milestone`_ on GitHub for more details. + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Add dependency on ``entrypoints`` >= 0.3, < 0.4 (See also :issue:`897`, + :issue:`1197`) + +- Pyflakes has been updated to >= 2.1.0, < 2.2.0 (See also :issue:`912`, + :issue:`913`) + +- pycodestyle has been updated to >= 2.5.0, < 2.6.0 (See also :issue:`915`) + +Features +~~~~~~~~ + +- Add support for ``per-file-ignores`` (See also :issue:`892`, :issue:`511`, + :issue:`911`, :issue:`277`) + +- Enable use of ``float`` and ``complex`` option types (See also :issue:`894`, + :issue:`258`) + +- Improve startup performance by switching from ``pkg_resources`` to + ``entrypoints`` (See also :issue:`897`) + +- Add metadata for use through the `pre-commit`_ git hooks framework (See also + :issue:`901`, :issue:`1196`) + +- Allow physical line checks to return more than one result (See also + :issue:`902`) + +- Allow ``# noqa:X123`` comments without space between the colon and codes + list (See also :issue:`906`, :issue:`276`) + +- Remove broken and unused ``flake8.listen`` plugin type (See also + :issue:`907`, :issue:`663`) + +.. all links +.. _3.7.0 milestone: + https://github.com/pycqa/flake8/milestone/22 +.. _pre-commit: + https://pre-commit.com/ diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.7.1.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.7.1.rst new file mode 100644 index 0000000..218ac07 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.7.1.rst @@ -0,0 +1,14 @@ +3.7.1 -- 2019-01-30 +------------------- + +You can view the `3.7.1 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix capitalized filenames in ``per-file-ignores`` setting (See also + :issue:`917`, :issue:`287`) + +.. all links +.. _3.7.1 milestone: + https://github.com/pycqa/flake8/milestone/23 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.7.2.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.7.2.rst new file mode 100644 index 0000000..98d64fb --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.7.2.rst @@ -0,0 +1,20 @@ +3.7.2 -- 2019-01-30 +------------------- + +You can view the `3.7.2 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix broken ``flake8 --diff`` (regressed in 3.7.0) (See also :issue:`919`, + :issue:`667`) + +- Fix typo in plugin exception reporting (See also :issue:`908`, + :issue:`668`) + +- Fix ``AttributeError`` while attempting to use the legacy api (regressed in + 3.7.0) (See also :issue:`1198`, :issue:`673`) + +.. all links +.. _3.7.2 milestone: + https://github.com/pycqa/flake8/milestone/24 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.7.3.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.7.3.rst new file mode 100644 index 0000000..69f4eec --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.7.3.rst @@ -0,0 +1,23 @@ +3.7.3 -- 2019-01-30 +------------------- + +You can view the `3.7.3 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix imports of ``typing`` in python 3.5.0 / 3.5.1 (See also :issue:`1199`, + :issue:`674`) + +- Fix ``flake8 --statistics`` (See also :issue:`920`, :issue:`675`) + +- Gracefully ignore ``flake8-per-file-ignores`` plugin if installed (See also + :issue:`1201`, :issue:`671`) + +- Improve error message for malformed ``per-file-ignores`` (See also + :issue:`921`, :issue:`288`) + + +.. all links +.. _3.7.3 milestone: + https://github.com/pycqa/flake8/milestone/25 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.7.4.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.7.4.rst new file mode 100644 index 0000000..80a9024 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.7.4.rst @@ -0,0 +1,15 @@ +3.7.4 -- 2019-01-31 +------------------- + +You can view the `3.7.4 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix performance regression with lots of ``per-file-ignores`` and errors + (See also :issue:`922`, :issue:`677`) + + +.. all links +.. _3.7.4 milestone: + https://github.com/pycqa/flake8/milestone/26 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.7.5.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.7.5.rst new file mode 100644 index 0000000..769893c --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.7.5.rst @@ -0,0 +1,15 @@ +3.7.5 -- 2019-02-04 +------------------- + +You can view the `3.7.5 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix reporting of pyflakes "referenced before assignment" error (See also + :issue:`923`, :issue:`679`) + + +.. all links +.. _3.7.5 milestone: + https://github.com/pycqa/flake8/milestone/27 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.7.6.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.7.6.rst new file mode 100644 index 0000000..a18eaef --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.7.6.rst @@ -0,0 +1,17 @@ +3.7.6 -- 2019-02-18 +------------------- + +You can view the `3.7.6 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix ``--per-file-ignores`` for multi-letter error codes (See also + :issue:`1203`, :issue:`683`) + +- Improve flake8 speed when only 1 filename is passed (See also :issue:`1204`) + + +.. all links +.. _3.7.6 milestone: + https://github.com/pycqa/flake8/milestone/28 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.7.7.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.7.7.rst new file mode 100644 index 0000000..b7a221a --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.7.7.rst @@ -0,0 +1,15 @@ +3.7.7 -- 2019-02-25 +------------------- + +You can view the `3.7.7 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix crashes in plugins causing ``flake8`` to hang while unpickling errors + (See also :issue:`1206`, :issue:`681`) + + +.. all links +.. _3.7.7 milestone: + https://github.com/pycqa/flake8/milestone/29 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.7.8.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.7.8.rst new file mode 100644 index 0000000..e0af4a3 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.7.8.rst @@ -0,0 +1,23 @@ +3.7.8 -- 2019-07-08 +------------------- + +You can view the `3.7.8 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix handling of ``Application.parse_preliminary_options_and_args`` when + argv is an empty list (See also :issue:`1303`, :issue:`694`) + +- Fix crash when a file parses but fails to tokenize (See also :issue:`1210`, + :issue:`1088`) + +- Log the full traceback on plugin exceptions (See also :issue:`926`) + +- Fix ``# noqa: ...`` comments with multi-letter codes (See also :issue:`931`, + :issue:`1101`) + + +.. all links +.. _3.7.8 milestone: + https://github.com/pycqa/flake8/milestone/30 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.7.9.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.7.9.rst new file mode 100644 index 0000000..29467a0 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.7.9.rst @@ -0,0 +1,15 @@ +3.7.9 -- 2019-10-28 +------------------- + +You can view the `3.7.9 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Disable multiprocessing when the multiprocessing method is ``spawn`` (such + as on macos in python3.8) (See also :issue:`956`, :issue:`315`) + + +.. all links +.. _3.7.9 milestone: + https://github.com/pycqa/flake8/milestone/32 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.8.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.8.0.rst new file mode 100644 index 0000000..08eb6a1 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.8.0.rst @@ -0,0 +1,139 @@ +3.8.0 -- 2020-05-11 +------------------- + +You can view the `3.8.0 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix logical checks which report positions out of bounds (See also + :issue:`987`, :issue:`723`) + +- Fix ``--exclude=.*`` accidentally matching ``.`` and ``..`` (See also + :issue:`441`, :issue:`360`) + +Deprecations +~~~~~~~~~~~~ + +- Add deprecation message for vcs hooks (See also :issue:`985`, + :issue:`296`) + + +3.8.0a2 -- 2020-04-24 +--------------------- + +You can view the `3.8.0 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix ``type="str"`` optparse options (See also :issue:`984`) + + +3.8.0a1 -- 2020-04-24 +--------------------- + +You can view the `3.8.0 milestone`_ on GitHub for more details. + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Remove dependency on ``entrypoints`` and add dependency on + ``importlib-metadata`` (only for ``python<3.8``) (See also :issue:`1297`, + :issue:`297`) + +- Pyflakes has been updated to >= 2.2.0, < 2.3.0 (See also :issue:`982`) + +- pycodestyle has been updated to >= 2.6.0a1, < 2.7.0 (See also :issue:`983`) + +Features +~~~~~~~~ + +- Add ``--extend-exclude`` option to add to ``--exclude`` without overwriting + (See also :issue:`1211`, :issue:`1091`) + +- Move argument parsing from ``optparse`` to ``argparse`` (See also + :issue:`939` + +- Group plugin options in ``--help`` (See also :issue:`1219`, :issue:`294`) + +- Remove parsing of ``verbose`` from configuration files as it was not + consistently applied (See also :issue:`1245`, :issue:`245`) + +- Remove parsing of ``output_file`` from configuration files as it was not + consistently applied (See also :issue:`1246`) + +- Resolve configuration files relative to ``cwd`` instead of common prefix of + passed filenames. You may need to change ``flake8 subproject`` to + ``cd subproject && flake8 .`` (See also :issue:`952`) + +- Officially support python3.8 (See also :issue:`963`) + +- ``--disable-noqa`` now also disables ``# flake8: noqa`` (See also + :issue:`1296`, :issue:`318`) + +- Ensure that a missing file produces a ``E902`` error (See also :issue:`1262`, + :issue:`328`) + +- ``# noqa`` comments now apply to all of the lines in an explicit ``\`` + continuation or in a line continued by a multi-line string (See also + :issue:`1266`, :issue:`621`) + +Bugs Fixed +~~~~~~~~~~ + +- Fix ``--exclude=./t.py`` to only match ``t.py`` at the top level (See also + :issue:`1208`, :issue:`628`) + +- Fix ``--show-source`` when a file is indented with tabs (See also + :issue:`1218`, :issue:`719`) + +- Fix crash when ``--max-line-length`` is given a non-integer (See also + :issue:`939`, :issue:`704`) + +- Prevent flip-flopping of ``indent_char`` causing extra ``E101`` errors (See + also :issue:`949`, `pycodestyle#886`_) + +- Only enable multiprocessing when the method is ``fork`` fixing issues + on macos with python3.8+ (See also :issue:`955`, :issue:`315`) (note: this + fix also landed in 3.7.9) + +- ``noqa`` is now only handled by flake8 fixing specific-noqa. Plugins + requesting this parameter will always receive ``False`` (See also + :issue:`1214`, :issue:`1104`) + +- Fix duplicate loading of plugins when invoked via ``python -m flake8`` (See + also :issue:`1297`) + +- Fix early exit when ``--exit-zero`` and ``--diff`` are provided and the diff + is empty (See also :issue:`970`) + +- Consistently split lines when ``\f`` is present when reading from stdin (See + also :issue:`976`, :issue:`202`) + +Deprecations +~~~~~~~~~~~~ + +- ``python setup.py flake8`` (setuptools integration) is now deprecated and + will be removed in a future version (See also :issue:`935`, :issue:`1098`) + +- ``type='string'`` (optparse) types are deprecated, use + ``type=callable`` (argparse) instead. Support for ``type='string'`` will + be removed in a future version (See also :issue:`939`) + +- ``%default`` in plugin option help text is deprecated, use ``%(default)s`` + instead. Support for ``%default`` will be removed in a future version (See + also :issue:`939`) + +- optparse-style ``action='callback'`` setting for options is deprecated, use + argparse action classes instead. This will be removed in a future version + (See also :issue:`939`) + + +.. all links +.. _3.8.0 milestone: + https://github.com/pycqa/flake8/milestone/31 + +.. issue links +.. _pycodestyle#886: + https://github.com/PyCQA/pycodestyle/issues/886 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.8.1.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.8.1.rst new file mode 100644 index 0000000..21e433f --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.8.1.rst @@ -0,0 +1,15 @@ +3.8.1 -- 2020-05-11 +------------------- + +You can view the `3.8.1 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix ``--output-file`` (regression in 3.8.0) (See also :issue:`990`, + :issue:`725`) + + +.. all links +.. _3.8.1 milestone: + https://github.com/pycqa/flake8/milestone/33 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.8.2.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.8.2.rst new file mode 100644 index 0000000..2f78eaf --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.8.2.rst @@ -0,0 +1,22 @@ +3.8.2 -- 2020-05-22 +------------------- + +You can view the `3.8.2 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Improve performance by eliminating unnecessary sort (See also :issue:`991`) + +- Improve messaging of ``--jobs`` argument by utilizing ``argparse`` (See also + :issue:`1269`, :issue:`1110`) + +- Fix file configuration options to be relative to the config passed on the + command line (See also :issue:`442`, :issue:`736`) + +- Fix incorrect handling of ``--extend-exclude`` by treating its values as + files (See also :issue:`1271`, :issue:`738`) + +.. all links +.. _3.8.2 milestone: + https://github.com/pycqa/flake8/milestone/34 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.8.3.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.8.3.rst new file mode 100644 index 0000000..64fa62a --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.8.3.rst @@ -0,0 +1,17 @@ +3.8.3 -- 2020-06-08 +------------------- + +You can view the `3.8.3 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Also catch ``SyntaxError`` when tokenizing (See also :issue:`992`, + :issue:`747`) + +- Fix ``--jobs`` default display in ``flake8 --help`` (See also :issue:`1272`, + :issue:`750`) + +.. all links +.. _3.8.3 milestone: + https://github.com/pycqa/flake8/milestone/35 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.8.4.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.8.4.rst new file mode 100644 index 0000000..01260ed --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.8.4.rst @@ -0,0 +1,17 @@ +3.8.4 -- 2020-10-02 +------------------- + +You can view the `3.8.4 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix multiprocessing errors on platforms without ``sem_open`` syscall. (See + also :issue:`1282`) + +- Fix skipping of physical checks on the last line of a file which does not + end in a newline (See also :issue:`997`) + +.. all links +.. _3.8.4 milestone: + https://github.com/pycqa/flake8/milestone/36 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.9.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.9.0.rst new file mode 100644 index 0000000..e1024fe --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.9.0.rst @@ -0,0 +1,33 @@ +3.9.0 -- 2021-03-14 +------------------- + +You can view the `3.9.0 milestone`_ on GitHub for more details. + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Pyflakes has been updated to >= 2.3.0, < 2.4.0 (See also :issue:`1006`) + +- pycodestyle has been updated to >= 2.7.0, < 2.8.0 (See also :issue:`1007`) + +Deprecations +~~~~~~~~~~~~ + +- Drop support for python 3.4 (See also :issue:`1283`) + +Features +~~~~~~~~ + +- Add ``--no-show-source`` option to disable ``--show-source`` (See also + :issue:`995`) + +Bugs Fixed +~~~~~~~~~~ + +- Fix handling of ``crlf`` line endings when linting stdin (See also + :issue:`1002`) + + +.. all links +.. _3.9.0 milestone: + https://github.com/pycqa/flake8/milestone/37 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.9.1.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.9.1.rst new file mode 100644 index 0000000..55bccdc --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.9.1.rst @@ -0,0 +1,15 @@ +3.9.1 -- 2021-04-15 +------------------- + +You can view the `3.9.1 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix codes being ignored by plugins utilizing ``extend_default_ignore`` (See + also :pull:`1317`) + + +.. all links +.. _3.9.1 milestone: + https://github.com/PyCQA/flake8/milestone/38 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/3.9.2.rst b/src/flake8-main/flake8-main/docs/source/release-notes/3.9.2.rst new file mode 100644 index 0000000..ebcada5 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/3.9.2.rst @@ -0,0 +1,21 @@ +3.9.2 -- 2021-05-08 +------------------- + +You can view the `3.9.2 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix error message for ``E111`` in ``pycodestyle`` (See also :pull:`1328`, + :issue:`1327`). + +Deprecations +~~~~~~~~~~~~ + +- ``indent_size_str`` is deprecated, use ``str(indent_size)`` instead (See + also :pull:`1328`, :issue:`1327`). + + +.. all links +.. _3.9.2 milestone: + https://github.com/PyCQA/flake8/milestone/40 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/4.0.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/4.0.0.rst new file mode 100644 index 0000000..b6c9870 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/4.0.0.rst @@ -0,0 +1,43 @@ +4.0.0 -- 2021-10-10 +------------------- + +You can view the `4.0.0 milestone`_ on GitHub for more details. + +Backwards Incompatible Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Remove ``--install-hook`` vcs integration (See also :issue:`1008`). +- Remove ``setuptools`` command (See also :issue:`1009`). +- Migrate from GitLab to GitHub (See also :pull:`1305`). +- Due to constant confusion by users, user-level |Flake8| configuration files + are no longer supported. Files will not be searched for in the user's home + directory (e.g., ``~/.flake8``) nor in the XDG config directory (e.g., + ``~/.config/flake8``). (See also :pull:`1404`). + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- pycodestyle has been updated to >= 2.8.0, < 2.9.0 (See also :pull:`1406`). +- Pyflakes has been updated to >= 2.4.0, < 2.5.0 (See also :pull:`1406`). +- flake8 requires python >= 3.6 (See also :issue:`1010`). + +Features +~~~~~~~~ + +- Add ``--extend-select`` option (See also :pull:`1312` :issue:`1061`). +- Automatically create directories for output files (See also :pull:`1329`). + +Bugs Fixed +~~~~~~~~~~ + +- ``ast`` parse before tokenizing to improve ``SyntaxError`` errors (See also + :pull:`1320` :issue:`740`). +- Fix warning in ``--indent-size`` argparse help (See also :pull:`1367`). +- Fix handling ``SyntaxError`` in python 3.10+ (See also :pull:`1374` + :issue:`1372`). +- Fix writing non-cp1252-encodable when output is piped on windows (See also + :pull:`1382` :issue:`1381`). + +.. all links +.. _4.0.0 milestone: + https://github.com/PyCQA/flake8/milestone/39 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/4.0.1.rst b/src/flake8-main/flake8-main/docs/source/release-notes/4.0.1.rst new file mode 100644 index 0000000..402e4b0 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/4.0.1.rst @@ -0,0 +1,15 @@ +4.0.1 -- 2021-10-11 +------------------- + +You can view the `4.0.1 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix parallel execution collecting a ``SyntaxError`` (See also :pull:`1410` + :issue:`1408`). + + +.. all links +.. _4.0.1 milestone: + https://github.com/PyCQA/flake8/milestone/41 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/5.0.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/5.0.0.rst new file mode 100644 index 0000000..e18efc4 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/5.0.0.rst @@ -0,0 +1,76 @@ +5.0.0 -- 2022-07-30 +------------------- + +You can view the `5.0.0 milestone`_ on GitHub for more details. + +Backwards Incompatible Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Remove ``indent_size_str`` (See also :pull:`1411`). +- Remove some dead code (See also :pull:`1453`, :pull:`1540`, :pull:`1541`). +- Missing explicitly-specified configuration is now an error (See also + :issue:`1497`, :pull:`1498`). +- Always read configuration files as UTF-8 (See also :issue:`1532`, + :pull:`1533`). +- Remove manpage from docs -- use ``help2man`` or related tools instead (See + also :pull:`1557`). +- Forbid invalid plugin codes (See also :issue:`325`, :pull:`1579`). + + +Deprecations +~~~~~~~~~~~~ + +- Deprecate ``--diff`` option (See also :issue:`1389`, :pull:`1441`). + + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- pycodestyle has been updated to >= 2.9.0, < 2.10.0 (See also :pull:`1626`). +- Pyflakes has been updated to >= 2.5.0, < 2.6.0 (See also :pull:`1625`). +- mccabe has been updated to >= 0.7.0, < 0.8.0 (See also :pull:`1542`). + + +Features +~~~~~~~~ + +- Add colors to output, configurable via ``--color`` (See also :issue:`1379`, + :pull:`1440`). +- Add ``.nox`` to the default exclude list (See also :issue:`1442`, + :pull:`1443`). +- Don't consider a config file which does not contain flake8 settings (See + also :issue:`199`, :pull:`1472`). +- Duplicate ``local-plugins`` names are now allowed (See also :issue:`362`, + :pull:`1504`). +- Consider ``.`` to be a path in config files (See also :issue:`1494`, + :pull:`1508`) +- Add ``--require-plugins`` option taking distribution names (See also + :issue:`283`, :pull:`1535`). +- Improve performance by removing debug logs (See also :pull:`1537`, + :pull:`1544`). +- Include failing file path in plugin execution error (See also :issue:`265`, + :pull:`1543`). +- Improve performance by pre-generating a ``pycodestyle`` plugin (See also + :pull:`1545`). +- Properly differentiate between explicitly ignored / selected and default + ignored / selected options (See also :issue:`284`, :pull:`1576`, + :pull:`1609`). + + +Bugs Fixed +~~~~~~~~~~ + +- Fix physical line plugins not receiving all lines in the case of + triple-quoted strings (See also :issue:`1534`, :pull:`1536`). +- Fix duplicate error logging in the case of plugin issues (See also + :pull:`1538`). +- Fix inconsistent ordering of ``--ignore`` in ``--help`` (See also + :issue:`1550`, :pull:`1552`). +- Fix memory leak of style guides by avoiding ``@lru_cache`` of a method (See + also :pull:`1573`). +- Fix ignoring of configuration files exactly in the home directory (See also + :issue:`1617`, :pull:`1618`). + +.. all links +.. _5.0.0 milestone: + https://github.com/PyCQA/flake8/milestone/42 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/5.0.1.rst b/src/flake8-main/flake8-main/docs/source/release-notes/5.0.1.rst new file mode 100644 index 0000000..89b6bf6 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/5.0.1.rst @@ -0,0 +1,15 @@ +5.0.1 -- 2022-07-31 +------------------- + +You can view the `5.0.1 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix duplicate plugin discovery on misconfigured pythons (See also + :issue:`1627`, :pull:`1631`). + + +.. all links +.. _5.0.1 milestone: + https://github.com/PyCQA/flake8/milestone/43 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/5.0.2.rst b/src/flake8-main/flake8-main/docs/source/release-notes/5.0.2.rst new file mode 100644 index 0000000..322d202 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/5.0.2.rst @@ -0,0 +1,16 @@ +5.0.2 -- 2022-08-01 +------------------- + +You can view the `5.0.2 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix execution on python == 3.8.0 (See also :issue:`1637`, :pull:`1641`). +- Fix config discovery when home does not exist (See also :issue:`1640`, + :pull:`1642`). + + +.. all links +.. _5.0.2 milestone: + https://github.com/PyCQA/flake8/milestone/44 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/5.0.3.rst b/src/flake8-main/flake8-main/docs/source/release-notes/5.0.3.rst new file mode 100644 index 0000000..25f8d93 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/5.0.3.rst @@ -0,0 +1,15 @@ +5.0.3 -- 2022-08-01 +------------------- + +You can view the `5.0.3 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Work around partial reads of configuration files with syntax errors (See + also :issue:`1647`, :pull:`1648`). + + +.. all links +.. _5.0.3 milestone: + https://github.com/PyCQA/flake8/milestone/45 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/5.0.4.rst b/src/flake8-main/flake8-main/docs/source/release-notes/5.0.4.rst new file mode 100644 index 0000000..b5c4889 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/5.0.4.rst @@ -0,0 +1,15 @@ +5.0.4 -- 2022-08-03 +------------------- + +You can view the `5.0.4 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Set a lower bound on ``importlib-metadata`` to prevent ``RecursionError`` + (See also :issue:`1650`, :pull:`1653`). + + +.. all links +.. _5.0.4 milestone: + https://github.com/PyCQA/flake8/milestone/46 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/6.0.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/6.0.0.rst new file mode 100644 index 0000000..edb01df --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/6.0.0.rst @@ -0,0 +1,35 @@ +6.0.0 -- 2022-11-23 +------------------- + +You can view the `6.0.0 milestone`_ on GitHub for more details. + +Backwards Incompatible Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Remove ``--diff`` option (See also :issue:`1389`, :pull:`1720`). +- Produce an error when invalid codes are specified in configuration (See also + :issue:`1689`, :pull:`1713`). +- Produce an error if the file specified in ``--extend-config`` does not exist + (See also :issue:`1729`, :pull:`1732`). +- Remove ``optparse`` compatibility support (See also :pull:`1739`). + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- pycodestyle has been updated to >= 2.10.0, < 2.11.0 (See also :pull:`1746`). +- Pyflakes has been updated to >= 3.0.0, < 3.1.0 (See also :pull:`1748`). + +Features +~~~~~~~~ + +- Require python >= 3.8.1 (See also :pull:`1633`, :pull:`1741`). +- List available formatters in for ``--format`` option in ``--help`` (See also + :issue:`223`, :pull:`1624`). +- Improve multiprocessing performance (See also :pull:`1723`). +- Enable multiprocessing on non-fork platforms (See also :pull:`1723`). +- Ensure results are sorted when discovered from files (See also :issue:`1670`, + :pull:`1726`). + +.. all links +.. _6.0.0 milestone: + https://github.com/PyCQA/flake8/milestone/47 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/6.1.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/6.1.0.rst new file mode 100644 index 0000000..a2244a9 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/6.1.0.rst @@ -0,0 +1,22 @@ +6.1.0 -- 2023-07-29 +------------------- + +You can view the `6.1.0 milestone`_ on GitHub for more details. + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Pyflakes has been updated to >= 3.1.0, < 3.2.0 (See also :pull:`1847`). +- pycodestyle has been updated to >= 2.11.0, < 2.12.0 (See also :pull:`1848`). + +Features +~~~~~~~~ + +- Deprecate ``--include-in-doctest``, ``--exclude-from-doctest`` (See also + :issue:`1747`, :pull:`1768`). +- Add support for python 3.12 (See also :pull:`1832`, :pull:`1849`, + :pull:`1850`). + +.. all links +.. _6.1.0 milestone: + https://github.com/PyCQA/flake8/milestone/48 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/7.0.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/7.0.0.rst new file mode 100644 index 0000000..6cd852a --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/7.0.0.rst @@ -0,0 +1,19 @@ +7.0.0 -- 2024-01-04 +------------------- + +You can view the `7.0.0 milestone`_ on GitHub for more details. + +Backwards Incompatible Changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Remove ``--include-in-doctest`` and ``--exclude-from-doctest`` options. + (See also :issue:`1747`, :pull:`1854`) + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Pyflakes has been updated to >= 3.2.0, < 3.3.0 (See also :pull:`1906`). + +.. all links +.. _7.0.0 milestone: + https://github.com/PyCQA/flake8/milestone/49 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/7.1.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/7.1.0.rst new file mode 100644 index 0000000..2229baa --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/7.1.0.rst @@ -0,0 +1,13 @@ +7.1.0 -- 2024-06-15 +------------------- + +You can view the `7.1.0 milestone`_ on GitHub for more details. + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- pycodestyle has been updated to >= 2.12.0, < 2.13.0 (See also :pull:`1939`). + +.. all links +.. _7.1.0 milestone: + https://github.com/PyCQA/flake8/milestone/50 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/7.1.1.rst b/src/flake8-main/flake8-main/docs/source/release-notes/7.1.1.rst new file mode 100644 index 0000000..62f2d11 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/7.1.1.rst @@ -0,0 +1,15 @@ +7.1.1 -- 2024-08-04 +------------------- + +You can view the `7.1.1 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Properly preserve escaped `{` and `}` in fstrings in logical lines in 3.12+. + (See also :issue:`1948`, :pull:`1949`). + + +.. all links +.. _7.1.1 milestone: + https://github.com/PyCQA/flake8/milestone/51 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/7.1.2.rst b/src/flake8-main/flake8-main/docs/source/release-notes/7.1.2.rst new file mode 100644 index 0000000..010656c --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/7.1.2.rst @@ -0,0 +1,15 @@ +7.1.2 -- 2025-02-16 +------------------- + +You can view the `7.1.2 milestone`_ on GitHub for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Avoid starting unnecessary processes when "# files" < "jobs". + (See also :pull:`1966`). + + +.. all links +.. _7.1.2 milestone: + https://github.com/PyCQA/flake8/milestone/52 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/7.2.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/7.2.0.rst new file mode 100644 index 0000000..fe124d7 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/7.2.0.rst @@ -0,0 +1,19 @@ +7.2.0 -- 2025-03-29 +------------------- + +You can view the `7.2.0 milestone`_ on GitHub for more details. + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- pycodestyle has been updated to >= 2.13.0, < 2.14.0 (See also :pull:`1974`). +- pyflakes has been updated to >= 3.3.0, < 3.4.0 (See also :pull:`1974`). + +Features +~~~~~~~~ + +- Require python >= 3.9 (See also :pull:`1973`). + +.. all links +.. _7.2.0 milestone: + https://github.com/PyCQA/flake8/milestone/53 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/7.3.0.rst b/src/flake8-main/flake8-main/docs/source/release-notes/7.3.0.rst new file mode 100644 index 0000000..dedc918 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/7.3.0.rst @@ -0,0 +1,15 @@ +7.3.0 -- 2025-06-20 +------------------- + +You can view the `7.3.0 milestone`_ on GitHub for more details. + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Added support for python 3.14 (See also :pull:`1983`). +- pycodestyle has been updated to >= 2.14.0, < 2.15.0 (See also :pull:`1985`). +- Pyflakes has been updated to >= 3.4.0, < 3.5.0 (See also :pull:`1985`). + +.. all links +.. _7.3.0 milestone: + https://github.com/PyCQA/flake8/milestone/54 diff --git a/src/flake8-main/flake8-main/docs/source/release-notes/index.rst b/src/flake8-main/flake8-main/docs/source/release-notes/index.rst new file mode 100644 index 0000000..10697df --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/release-notes/index.rst @@ -0,0 +1,128 @@ +=========================== + Release Notes and History +=========================== + +All of the release notes that have been recorded for Flake8 are organized here +with the newest releases first. + +7.x Release Series +================== + +.. toctree:: + 7.3.0 + 7.2.0 + 7.1.2 + 7.1.1 + 7.1.0 + 7.0.0 + +6.x Release Series +================== + +.. toctree:: + 6.1.0 + 6.0.0 + +5.x Release Series +================== + +.. toctree:: + 5.0.4 + 5.0.3 + 5.0.2 + 5.0.1 + 5.0.0 + +4.x Release Series +================== + +.. toctree:: + 4.0.1 + 4.0.0 + +3.x Release Series +================== + +.. toctree:: + 3.9.2 + 3.9.1 + 3.9.0 + 3.8.4 + 3.8.3 + 3.8.2 + 3.8.1 + 3.8.0 + 3.7.9 + 3.7.8 + 3.7.7 + 3.7.6 + 3.7.5 + 3.7.4 + 3.7.3 + 3.7.2 + 3.7.1 + 3.7.0 + 3.6.0 + 3.5.0 + 3.4.1 + 3.4.0 + 3.3.0 + 3.2.1 + 3.2.0 + 3.1.1 + 3.1.0 + 3.0.4 + 3.0.3 + 3.0.2 + 3.0.1 + 3.0.0 + +2.x Release Series +================== + +.. toctree:: + 2.6.2 + 2.6.1 + 2.6.0 + 2.5.5 + 2.5.4 + 2.5.3 + 2.5.2 + 2.5.1 + 2.5.0 + 2.4.1 + 2.4.0 + 2.3.0 + 2.2.5 + 2.2.4 + 2.2.3 + 2.2.2 + 2.2.1 + 2.2.0 + 2.1.0 + 2.0.0 + +1.x Release Series +================== + +.. toctree:: + 1.7.0 + 1.6.2 + 1.6.1 + 1.6.0 + 1.5.0 + 1.4.0 + 1.3.1 + 1.3.0 + 1.2.0 + 1.1.0 + 1.0.0 + +0.x Release Series +================== + +.. toctree:: + 0.9.0 + 0.8.0 + 0.7.0 + 0.6.0 diff --git a/src/flake8-main/flake8-main/docs/source/requirements.txt b/src/flake8-main/flake8-main/docs/source/requirements.txt new file mode 100644 index 0000000..765fb13 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/requirements.txt @@ -0,0 +1,4 @@ +sphinx>=2.1.0,!=3.1.0 +sphinx-rtd-theme>=1.2.2 +sphinx-prompt>=1.8.0 +docutils!=0.18 diff --git a/src/flake8-main/flake8-main/docs/source/user/.keep b/src/flake8-main/flake8-main/docs/source/user/.keep new file mode 100644 index 0000000..e69de29 diff --git a/src/flake8-main/flake8-main/docs/source/user/configuration.rst b/src/flake8-main/flake8-main/docs/source/user/configuration.rst new file mode 100644 index 0000000..70fc22b --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/user/configuration.rst @@ -0,0 +1,294 @@ +.. _configuration: + +==================== + Configuring Flake8 +==================== + +Once you have learned how to :ref:`invoke ` |Flake8|, you will soon +want to learn how to configure it so you do not have to specify the same +options every time you use it. + +This section will show you how to make + +.. prompt:: bash + + flake8 + +Remember that you want to specify certain options without writing + +.. prompt:: bash + + flake8 --select E123,W456 --enable-extensions H111 + + +Configuration Locations +======================= + +|Flake8| supports storing its configuration in your project in one of +``setup.cfg``, ``tox.ini``, or ``.flake8``. + +Values set at the command line have highest priority, then those in the +project configuration file, and finally there are the defaults. However, +there are additional command line options which can alter this. + + +Project Configuration +--------------------- + +|Flake8| is written with the understanding that people organize projects into +sub-directories. Let's take for example |Flake8|'s own project structure + +.. code:: + + flake8 + ├── docs + │   ├── build + │   └── source + │   ├── _static + │   ├── _templates + │   ├── dev + │   ├── internal + │   └── user + ├── flake8 + │   ├── formatting + │   ├── main + │   ├── options + │   └── plugins + └── tests + ├── fixtures + │   └── config_files + ├── integration + └── unit + +In the top-level ``flake8`` directory (which contains ``docs``, ``flake8``, +and ``tests``) there's also ``tox.ini`` and ``setup.cfg`` files. In our case, +we keep our |Flake8| configuration in ``tox.ini``. Regardless of whether you +keep your config in ``.flake8``, ``setup.cfg``, or ``tox.ini`` we expect you +to use INI to configure |Flake8| (since each of these files already uses INI +as a format). This means that any |Flake8| configuration you wish to set needs +to be in the ``flake8`` section, which means it needs to start like so: + +.. code-block:: ini + + [flake8] + +Each command-line option that you want to specify in your config file can +be named in either of two ways: + +#. Using underscores (``_``) instead of hyphens (``-``) + +#. Simply using hyphens (without the leading hyphens) + +.. note:: + + Not every |Flake8| command-line option can be specified in the + configuration file. See :ref:`our list of options ` to + determine which options will be parsed from the configuration files. + +Let's actually look at |Flake8|'s own configuration section: + +.. code-block:: ini + + [flake8] + extend-ignore = E203 + exclude = .git,__pycache__,docs/source/conf.py,old,build,dist + max-complexity = 10 + +This is equivalent to: + +.. prompt:: bash + + flake8 --extend-ignore E203 \ + --exclude .git,__pycache__,docs/source/conf.py,old,build,dist \ + --max-complexity 10 + +In our case, if we wanted to, we could also do + +.. code-block:: ini + + [flake8] + extend-ignore = E203 + exclude = + .git, + __pycache__, + docs/source/conf.py, + old, + build, + dist + max-complexity = 10 + +This allows us to add comments for why we're excluding items, e.g. + +.. code-block:: ini + + [flake8] + extend-ignore = E203 + exclude = + # No need to traverse our git directory + .git, + # There's no value in checking cache directories + __pycache__, + # The conf file is mostly autogenerated, ignore it + docs/source/conf.py, + # The old directory contains Flake8 2.0 + old, + # This contains our built documentation + build, + # This contains builds of flake8 that we don't want to check + dist + max-complexity = 10 + +.. note:: + + Following the recommended settings for + `Python's configparser `_, + |Flake8| does not support inline comments for any of the keys. So while + this is fine: + + .. code-block:: ini + + [flake8] + per-file-ignores = + # imported but unused + __init__.py: F401 + + this is not: + + .. code-block:: ini + + [flake8] + per-file-ignores = + __init__.py: F401 # imported but unused + + +.. note:: + + If you're using Python 2, you will notice that we download the + :mod:`configparser` backport from PyPI. That backport enables us to + support this behaviour on all supported versions of Python. + + Please do **not** open issues about this dependency to |Flake8|. + +.. note:: + + You can also specify ``--max-complexity`` as ``max_complexity = 10``. + +This is also useful if you have a long list of error codes to ignore. Let's +look at a portion of a project's Flake8 configuration in their ``tox.ini``: + +.. code-block:: ini + + [flake8] + # it's not a bug that we aren't using all of hacking, ignore: + # H101: Use TODO(NAME) + # H202: assertRaises Exception too broad + # H233: Python 3.x incompatible use of print operator + # H301: one import per line + # H306: imports not in alphabetical order (time, os) + # H401: docstring should not start with a space + # H403: multi line docstrings should end on a new line + # H404: multi line docstring should start without a leading new line + # H405: multi line docstring summary not separated with an empty line + # H501: Do not use self.__dict__ for string formatting + extend-ignore = H101,H202,H233,H301,H306,H401,H403,H404,H405,H501 + +They use the comments to describe the check but they could also write this as: + +.. code-block:: ini + + [flake8] + # it's not a bug that we aren't using all of hacking + extend-ignore = + # H101: Use TODO(NAME) + H101, + # H202: assertRaises Exception too broad + H202, + # H233: Python 3.x incompatible use of print operator + H233, + # H301: one import per line + H301, + # H306: imports not in alphabetical order (time, os) + H306, + # H401: docstring should not start with a space + H401, + # H403: multi line docstrings should end on a new line + H403, + # H404: multi line docstring should start without a leading new line + H404, + # H405: multi line docstring summary not separated with an empty line + H405, + # H501: Do not use self.__dict__ for string formatting + H501 + +Or they could use each comment to describe **why** they've ignored the check. +|Flake8| knows how to parse these lists and will appropriately handle +these situations. + + +Using Local Plugins +------------------- + +.. versionadded:: 3.5.0 + +|Flake8| allows users to write plugins that live locally in a project. These +plugins do not need to use setuptools or any of the other overhead associated +with plugins distributed on PyPI. To use these plugins, users must specify +them in their configuration file (i.e., ``.flake8``, ``setup.cfg``, or +``tox.ini``). This must be configured in a separate INI section named +``flake8:local-plugins``. + +Users may configure plugins that check source code, i.e., ``extension`` +plugins, and plugins that report errors, i.e., ``report`` plugins. + +An example configuration might look like: + +.. code-block:: ini + + [flake8:local-plugins] + extension = + MC1 = project.flake8.checkers:MyChecker1 + MC2 = project.flake8.checkers:MyChecker2 + report = + MR1 = project.flake8.reporters:MyReporter1 + MR2 = project.flake8.reporters:MyReporter2 + +|Flake8| will also, however, allow for commas to separate the plugins for +example: + +.. code-block:: ini + + [flake8:local-plugins] + extension = + MC1 = project.flake8.checkers:MyChecker1, + MC2 = project.flake8.checkers:MyChecker2 + report = + MR1 = project.flake8.reporters:MyReporter1, + MR2 = project.flake8.reporters:MyReporter2 + +These configurations will allow you to select your own custom reporter plugin +that you've designed or will utilize your new check classes. + +If your package is installed in the same virtualenv that |Flake8| will run +from, and your local plugins are part of that package, you're all set; |Flake8| +will be able to import your local plugins. However, if you are working on a +project that isn't set up as an installable package, or |Flake8| doesn't run +from the same virtualenv your code runs in, you may need to tell |Flake8| where +to import your local plugins from. You can do this via the ``paths`` option in +the ``local-plugins`` section of your config: + +.. code-block:: ini + + [flake8:local-plugins] + extension = + MC1 = myflake8plugin:MyChecker1 + paths = + ./path/to + +Relative paths will be interpreted relative to the config file. Multiple paths +can be listed (comma separated just like ``exclude``) as needed. If your local +plugins have any dependencies, it's up to you to ensure they are installed in +whatever Python environment |Flake8| runs in. + +.. note:: + + These plugins otherwise follow the same guidelines as regular plugins. diff --git a/src/flake8-main/flake8-main/docs/source/user/error-codes.rst b/src/flake8-main/flake8-main/docs/source/user/error-codes.rst new file mode 100644 index 0000000..c8b46c1 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/user/error-codes.rst @@ -0,0 +1,129 @@ +.. _error_codes: + +========================= + Error / Violation Codes +========================= + +Flake8 and its plugins assign a code to each message that we refer to as an +:term:`error code` (or :term:`violation`). Most plugins will list their error +codes in their documentation or README. + +Flake8 installs ``pycodestyle``, ``pyflakes``, and ``mccabe`` by default and +generates its own :term:`error code`\ s for ``pyflakes``: + ++------+---------------------------------------------------------------------+ +| Code | Example Message | ++======+=====================================================================+ +| F401 | ``module`` imported but unused | ++------+---------------------------------------------------------------------+ +| F402 | import ``module`` from line ``N`` shadowed by loop variable | ++------+---------------------------------------------------------------------+ +| F403 | 'from ``module`` import \*' used; unable to detect undefined names | ++------+---------------------------------------------------------------------+ +| F404 | future import(s) ``name`` after other statements | ++------+---------------------------------------------------------------------+ +| F405 | ``name`` may be undefined, or defined from star imports: ``module`` | ++------+---------------------------------------------------------------------+ +| F406 | 'from ``module`` import \*' only allowed at module level | ++------+---------------------------------------------------------------------+ +| F407 | an undefined ``__future__`` feature name was imported | ++------+---------------------------------------------------------------------+ ++------+---------------------------------------------------------------------+ +| F501 | invalid ``%`` format literal | ++------+---------------------------------------------------------------------+ +| F502 | ``%`` format expected mapping but got sequence | ++------+---------------------------------------------------------------------+ +| F503 | ``%`` format expected sequence but got mapping | ++------+---------------------------------------------------------------------+ +| F504 | ``%`` format unused named arguments | ++------+---------------------------------------------------------------------+ +| F505 | ``%`` format missing named arguments | ++------+---------------------------------------------------------------------+ +| F506 | ``%`` format mixed positional and named arguments | ++------+---------------------------------------------------------------------+ +| F507 | ``%`` format mismatch of placeholder and argument count | ++------+---------------------------------------------------------------------+ +| F508 | ``%`` format with ``*`` specifier requires a sequence | ++------+---------------------------------------------------------------------+ +| F509 | ``%`` format with unsupported format character | ++------+---------------------------------------------------------------------+ +| F521 | ``.format(...)`` invalid format string | ++------+---------------------------------------------------------------------+ +| F522 | ``.format(...)`` unused named arguments | ++------+---------------------------------------------------------------------+ +| F523 | ``.format(...)`` unused positional arguments | ++------+---------------------------------------------------------------------+ +| F524 | ``.format(...)`` missing argument | ++------+---------------------------------------------------------------------+ +| F525 | ``.format(...)`` mixing automatic and manual numbering | ++------+---------------------------------------------------------------------+ +| F541 | f-string without any placeholders | ++------+---------------------------------------------------------------------+ +| F542 | t-string without any placeholders | ++------+---------------------------------------------------------------------+ ++------+---------------------------------------------------------------------+ +| F601 | dictionary key ``name`` repeated with different values | ++------+---------------------------------------------------------------------+ +| F602 | dictionary key variable ``name`` repeated with different values | ++------+---------------------------------------------------------------------+ +| F621 | too many expressions in an assignment with star-unpacking | ++------+---------------------------------------------------------------------+ +| F622 | two or more starred expressions in an assignment ``(a, *b, *c = d)``| ++------+---------------------------------------------------------------------+ +| F631 | assertion test is a tuple, which is always ``True`` | ++------+---------------------------------------------------------------------+ +| F632 | use ``==/!=`` to compare ``str``, ``bytes``, and ``int`` literals | ++------+---------------------------------------------------------------------+ +| F633 | use of ``>>`` is invalid with ``print`` function | ++------+---------------------------------------------------------------------+ +| F634 | if test is a tuple, which is always ``True`` | ++------+---------------------------------------------------------------------+ ++------+---------------------------------------------------------------------+ +| F701 | a ``break`` statement outside of a ``while`` or ``for`` loop | ++------+---------------------------------------------------------------------+ +| F702 | a ``continue`` statement outside of a ``while`` or ``for`` loop | ++------+---------------------------------------------------------------------+ +| F704 | a ``yield`` or ``yield from`` statement outside of a function | ++------+---------------------------------------------------------------------+ +| F706 | a ``return`` statement outside of a function/method | ++------+---------------------------------------------------------------------+ +| F707 | an ``except:`` block as not the last exception handler | ++------+---------------------------------------------------------------------+ +| F721 | syntax error in doctest | ++------+---------------------------------------------------------------------+ +| F722 | syntax error in forward annotation | ++------+---------------------------------------------------------------------+ +| F723 | syntax error in type comment | ++------+---------------------------------------------------------------------+ ++------+---------------------------------------------------------------------+ +| F811 | redefinition of unused ``name`` from line ``N`` | ++------+---------------------------------------------------------------------+ +| F821 | undefined name ``name`` | ++------+---------------------------------------------------------------------+ +| F822 | undefined name ``name`` in ``__all__`` | ++------+---------------------------------------------------------------------+ +| F823 | local variable ``name`` ... referenced before assignment | ++------+---------------------------------------------------------------------+ +| F824 | ``global name`` / ``nonlocal name`` is unused: name is never | +| | assigned in scope | ++------+---------------------------------------------------------------------+ +| F831 | duplicate argument ``name`` in function definition | ++------+---------------------------------------------------------------------+ +| F841 | local variable ``name`` is assigned to but never used | ++------+---------------------------------------------------------------------+ ++------+---------------------------------------------------------------------+ +| F901 | ``raise NotImplemented`` should be ``raise NotImplementedError`` | ++------+---------------------------------------------------------------------+ + +We also report one extra error: ``E999``. We report ``E999`` when we fail to +compile a file into an Abstract Syntax Tree for the plugins that require it. + +``mccabe`` only ever reports one :term:`violation` - ``C901`` based on the +complexity value provided by the user. + +Users should also reference `pycodestyle's list of error codes`_. + + +.. links +.. _pycodestyle's list of error codes: + https://pycodestyle.readthedocs.io/en/latest/intro.html#error-codes diff --git a/src/flake8-main/flake8-main/docs/source/user/index.rst b/src/flake8-main/flake8-main/docs/source/user/index.rst new file mode 100644 index 0000000..90d5b14 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/user/index.rst @@ -0,0 +1,33 @@ +============== + Using Flake8 +============== + +|Flake8| can be used in many ways. A few: + +- invoked on the command-line + +- invoked via Python + +This guide will cover all of these and the nuances for using |Flake8|. + +.. note:: + + This portion of |Flake8|'s documentation does not cover installation. See + the :ref:`installation-guide` section for how to install |Flake8|. + +.. toctree:: + :maxdepth: 2 + + invocation + configuration + options + error-codes + violations + using-plugins + using-hooks + python-api + +.. config files +.. command-line tutorial +.. VCS usage +.. installing and using plugins diff --git a/src/flake8-main/flake8-main/docs/source/user/invocation.rst b/src/flake8-main/flake8-main/docs/source/user/invocation.rst new file mode 100644 index 0000000..10895dd --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/user/invocation.rst @@ -0,0 +1,90 @@ +.. _invocation: + +================= + Invoking Flake8 +================= + +Once you have :ref:`installed ` |Flake8|, you can begin +using it. Most of the time, you will be able to generically invoke |Flake8| +like so: + +.. prompt:: bash + + flake8 ... + +Where you simply allow the shell running in your terminal to locate |Flake8|. +In some cases, though, you may have installed |Flake8| for multiple versions +of Python (e.g., Python 3.13 and Python 3.14) and you need to call a specific +version. In that case, you will have much better results using: + +.. prompt:: bash + + python3.13 -m flake8 + +Or + +.. prompt:: bash + + python3.14 -m flake8 + +Since that will tell the correct version of Python to run |Flake8|. + +.. note:: + + Installing |Flake8| once will not install it on both Python 3.13 and + Python 3.14. It will only install it for the version of Python that + is running pip. + +It is also possible to specify command-line options directly to |Flake8|: + +.. prompt:: bash + + flake8 --select E123 + +Or + +.. prompt:: bash + + python -m flake8 --select E123 + +.. note:: + + This is the last time we will show both versions of an invocation. + From now on, we'll simply use ``flake8`` and assume that the user + knows they can instead use ``python -m flake8``. + +It's also possible to narrow what |Flake8| will try to check by specifying +exactly the paths and directories you want it to check. Let's assume that +we have a directory with python files and sub-directories which have python +files (and may have more sub-directories) called ``my_project``. Then if +we only want errors from files found inside ``my_project`` we can do: + +.. prompt:: bash + + flake8 my_project + +And if we only want certain errors (e.g., ``E123``) from files in that +directory we can also do: + +.. prompt:: bash + + flake8 --select E123 my_project + +If you want to explore more options that can be passed on the command-line, +you can use the ``--help`` option: + +.. prompt:: bash + + flake8 --help + +And you should see something like: + +.. code:: + + Usage: flake8 [options] file file ... + + Options: + --version show program's version number and exit + -h, --help show this help message and exit + + ... diff --git a/src/flake8-main/flake8-main/docs/source/user/options.rst b/src/flake8-main/flake8-main/docs/source/user/options.rst new file mode 100644 index 0000000..bd80c87 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/user/options.rst @@ -0,0 +1,1091 @@ +.. _options-list: + +================================================ + Full Listing of Options and Their Descriptions +================================================ + +.. + NOTE(sigmavirus24): When adding new options here, please follow the + following _rough_ template: + + .. option:: --[=] + + :ref:`Go back to index ` + + Active description of option's purpose (note that each description + starts with an active verb) + + Command-line usage: + + .. prompt:: bash + + flake8 --[=] [positional params] + + This **can[ not]** be specified in config files. + + (If it can be, an example using .. code-block:: ini) + + Thank you for your contribution to Flake8's documentation. + +.. _top: + +Index of Options +================ + +- :option:`flake8 --version` + +- :option:`flake8 --help` + +- :option:`flake8 --verbose` + +- :option:`flake8 --quiet` + +- :option:`flake8 --color` + +- :option:`flake8 --count` + +- :option:`flake8 --exclude` + +- :option:`flake8 --extend-exclude` + +- :option:`flake8 --filename` + +- :option:`flake8 --stdin-display-name` + +- :option:`flake8 --format` + +- :option:`flake8 --hang-closing` + +- :option:`flake8 --ignore` + +- :option:`flake8 --extend-ignore` + +- :option:`flake8 --per-file-ignores` + +- :option:`flake8 --max-line-length` + +- :option:`flake8 --max-doc-length` + +- :option:`flake8 --indent-size` + +- :option:`flake8 --select` + +- :option:`flake8 --extend-select` + +- :option:`flake8 --disable-noqa` + +- :option:`flake8 --show-source` + +- :option:`flake8 --statistics` + +- :option:`flake8 --require-plugins` + +- :option:`flake8 --enable-extensions` + +- :option:`flake8 --exit-zero` + +- :option:`flake8 --jobs` + +- :option:`flake8 --output-file` + +- :option:`flake8 --tee` + +- :option:`flake8 --append-config` + +- :option:`flake8 --config` + +- :option:`flake8 --isolated` + +- :option:`flake8 --builtins` + +- :option:`flake8 --doctests` + +- :option:`flake8 --benchmark` + +- :option:`flake8 --bug-report` + +- :option:`flake8 --max-complexity` + + +Options and their Descriptions +============================== + +.. program:: flake8 + +.. option:: --version + + :ref:`Go back to index ` + + Show |Flake8|'s version as well as the versions of all plugins + installed. + + Command-line usage: + + .. prompt:: bash + + flake8 --version + + This **can not** be specified in config files. + + +.. option:: -h, --help + + :ref:`Go back to index ` + + Show a description of how to use |Flake8| and its options. + + Command-line usage: + + .. prompt:: bash + + flake8 --help + flake8 -h + + This **can not** be specified in config files. + + +.. option:: -v, --verbose + + :ref:`Go back to index ` + + Increase the verbosity of |Flake8|'s output. Each time you specify + it, it will print more and more information. + + Command-line example: + + .. prompt:: bash + + flake8 -vv + + This **can not** be specified in config files. + + +.. option:: -q, --quiet + + :ref:`Go back to index ` + + Decrease the verbosity of |Flake8|'s output. Each time you specify it, + it will print less and less information. + + Command-line example: + + .. prompt:: bash + + flake8 -q + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + quiet = 1 + +.. option:: --color + + :ref:`Go back to index ` + + Whether to use color in output. Defaults to ``auto``. + + Possible options are ``auto``, ``always``, and ``never``. + + This **can not** be specified in config files. + + When color is enabled, the following substitutions are enabled: + + - ``%(bold)s`` + - ``%(black)s`` + - ``%(red)s`` + - ``%(green)s`` + - ``%(yellow)s`` + - ``%(blue)s`` + - ``%(magenta)s`` + - ``%(cyan)s`` + - ``%(white)s`` + - ``%(reset)s`` + + +.. option:: --count + + :ref:`Go back to index ` + + Print the total number of errors. + + Command-line example: + + .. prompt:: bash + + flake8 --count dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + count = True + + +.. option:: --exclude= + + :ref:`Go back to index ` + + Provide a comma-separated list of glob patterns to exclude from checks. + + This defaults to: ``.svn,CVS,.bzr,.hg,.git,__pycache__,.tox,.nox,.eggs,*.egg`` + + Example patterns: + + - ``*.pyc`` will match any file that ends with ``.pyc`` + + - ``__pycache__`` will match any path that has ``__pycache__`` in it + + - ``lib/python`` will look expand that using :func:`os.path.abspath` and + look for matching paths + + Command-line example: + + .. prompt:: bash + + flake8 --exclude=*.pyc dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + exclude = + .tox, + __pycache__ + + +.. option:: --extend-exclude= + + :ref:`Go back to index ` + + .. versionadded:: 3.8.0 + + Provide a comma-separated list of glob patterns to add to the list of excluded ones. + Similar considerations as in :option:`--exclude` apply here with regard to the + value. + + The difference to the :option:`--exclude` option is, that this option can be + used to selectively add individual patterns without overriding the default + list entirely. + + Command-line example: + + .. prompt:: bash + + flake8 --extend-exclude=legacy/,vendor/ dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + extend-exclude = + legacy/, + vendor/ + extend-exclude = legacy/,vendor/ + + +.. option:: --filename= + + :ref:`Go back to index ` + + Provide a comma-separate list of glob patterns to include for checks. + + This defaults to: ``*.py`` + + Example patterns: + + - ``*.py`` will match any file that ends with ``.py`` + + - ``__pycache__`` will match any path that has ``__pycache__`` in it + + - ``lib/python`` will look expand that using :func:`os.path.abspath` and + look for matching paths + + Command-line example: + + .. prompt:: bash + + flake8 --filename=*.py dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + filename = + example.py, + another-example*.py + + +.. option:: --stdin-display-name= + + :ref:`Go back to index ` + + Provide the name to use to report warnings and errors from code on stdin. + + Instead of reporting an error as something like: + + .. code:: + + stdin:82:73 E501 line too long + + You can specify this option to have it report whatever value you want + instead of stdin. + + This defaults to: ``stdin`` + + Command-line example: + + .. prompt:: bash + + cat file.py | flake8 --stdin-display-name=file.py - + + This **can not** be specified in config files. + + +.. option:: --format= + + :ref:`Go back to index ` + + Select the formatter used to display errors to the user. + + This defaults to: ``default`` + + By default, there are two formatters available: + + - default + - pylint + + Other formatters can be installed. Refer to their documentation for the + name to use to select them. Further, users can specify their own format + string. The variables available are: + + - code + - col + - path + - row + - text + + The default formatter has a format string of: + + .. code-block:: python + + '%(path)s:%(row)d:%(col)d: %(code)s %(text)s' + + Command-line example: + + .. prompt:: bash + + flake8 --format=pylint dir/ + flake8 --format='%(path)s::%(row)d,%(col)d::%(code)s::%(text)s' dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + format=pylint + format=%(path)s::%(row)d,%(col)d::%(code)s::%(text)s + + +.. option:: --hang-closing + + :ref:`Go back to index ` + + Toggle whether pycodestyle should enforce matching the indentation of the + opening bracket's line. When you specify this, it will prefer that you + hang the closing bracket rather than match the indentation. + + Command-line example: + + .. prompt:: bash + + flake8 --hang-closing dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + hang_closing = True + hang-closing = True + + +.. option:: --ignore= + + :ref:`Go back to index ` + + Specify a list of codes to ignore. The list is expected to be + comma-separated, and does not need to specify an error code exactly. + Since |Flake8| 3.0, this **can** be combined with :option:`--select`. See + :option:`--select` for more information. + + For example, if you wish to only ignore ``W234``, then you can specify + that. But if you want to ignore all codes that start with ``W23`` you + need only specify ``W23`` to ignore them. This also works for ``W2`` and + ``W`` (for example). + + This defaults to: ``E121,E123,E126,E226,E24,E704,W503,W504`` + + Command-line example: + + .. prompt:: bash + + flake8 --ignore=E121,E123 dir/ + flake8 --ignore=E24,E704 dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + ignore = + E121, + E123 + ignore = E121,E123 + + +.. option:: --extend-ignore= + + :ref:`Go back to index ` + + .. versionadded:: 3.6.0 + + Specify a list of codes to add to the list of ignored ones. Similar + considerations as in :option:`--ignore` apply here with regard to the + value. + + The difference to the :option:`--ignore` option is, that this option can be + used to selectively add individual codes without overriding the default + list entirely. + + Command-line example: + + .. prompt:: bash + + flake8 --extend-ignore=E4,E51,W234 dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + extend-ignore = + E4, + E51, + W234 + extend-ignore = E4,E51,W234 + + +.. option:: --per-file-ignores=[ ] + + :ref:`Go back to index ` + + .. versionadded:: 3.7.0 + + Specify a list of mappings of files and the codes that should be ignored + for the entirety of the file. This allows for a project to have a default + list of violations that should be ignored as well as file-specific + violations for files that have not been made compliant with the project + rules. + + This option supports syntax similar to :option:`--exclude` such that glob + patterns will also work here. + + This can be combined with both :option:`--ignore` and + :option:`--extend-ignore` to achieve a full flexibility of style options. + + Command-line usage: + + .. prompt:: bash + + flake8 --per-file-ignores='project/__init__.py:F401 setup.py:E121' + flake8 --per-file-ignores='project/*/__init__.py:F401 setup.py:E121' + + This **can** be specified in config files. + + .. code-block:: ini + + per-file-ignores = + project/__init__.py:F401 + setup.py:E121 + other_project/*:W9 + +.. option:: --max-line-length= + + :ref:`Go back to index ` + + Set the maximum length that any line (with some exceptions) may be. + + Exceptions include lines that are either strings or comments which are + entirely URLs. For example: + + .. code-block:: python + + # https://some-super-long-domain-name.com/with/some/very/long/path + + url = '''\ + https://... + ''' + + This defaults to: ``79`` + + Command-line example: + + .. prompt:: bash + + flake8 --max-line-length 99 dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + max-line-length = 79 + +.. option:: --max-doc-length= + + :ref:`Go back to index ` + + Set the maximum length that a comment or docstring line may be. + + By default, there is no limit on documentation line length. + + Command-line example: + + .. prompt:: bash + + flake8 --max-doc-length 99 dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + max-doc-length = 79 + +.. option:: --indent-size= + + :ref:`Go back to index ` + + Set the number of spaces used for indentation. + + By default, 4. + + Command-line example: + + .. prompt:: bash + + flake8 --indent-size 2 dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + indent-size = 2 + +.. option:: --select= + + :ref:`Go back to index ` + + **You usually do not need to specify this option as the default includes + all installed plugin codes.** + + Specify the list of error codes you wish |Flake8| to report. Similarly to + :option:`--ignore`. You can specify a portion of an error code to get all + that start with that string. For example, you can use ``E``, ``E4``, + ``E43``, and ``E431``. + + Command-line example: + + .. prompt:: bash + + flake8 --select=E431,E5,W,F dir/ + flake8 --select=E,W dir/ + + This can also be combined with :option:`--ignore`: + + .. prompt:: bash + + flake8 --select=E --ignore=E432 dir/ + + This will report all codes that start with ``E``, but ignore ``E432`` + specifically. This is more flexibly than the |Flake8| 2.x and 1.x used + to be. + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + select = + E431, + W, + F + + +.. option:: --extend-select= + + :ref:`Go back to index ` + + .. versionadded:: 4.0.0 + + **You usually do not need to specify this option as the default includes + all installed plugin codes.** + + Specify a list of codes to add to the list of selected ones. Similar + considerations as in :option:`--select` apply here with regard to the + value. + + The difference to the :option:`--select` option is, that this option can be + used to selectively add individual codes without overriding the default + list entirely. + + Command-line example: + + .. prompt:: bash + + flake8 --extend-select=E4,E51,W234 dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + extend-select = + E4, + E51, + W234 + + +.. option:: --disable-noqa + + :ref:`Go back to index ` + + Report all errors, even if it is on the same line as a ``# NOQA`` comment. + ``# NOQA`` can be used to silence messages on specific lines. Sometimes, + users will want to see what errors are being silenced without editing the + file. This option allows you to see all the warnings, errors, etc. + reported. + + Command-line example: + + .. prompt:: bash + + flake8 --disable-noqa dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + disable_noqa = True + disable-noqa = True + + +.. option:: --show-source + + :ref:`Go back to index ` + + Print the source code generating the error/warning in question. + + Command-line example: + + .. prompt:: bash + + flake8 --show-source dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + show_source = True + show-source = True + + +.. option:: --statistics + + :ref:`Go back to index ` + + Count the number of occurrences of each error/warning code and + print a report. + + Command-line example: + + .. prompt:: bash + + flake8 --statistics + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + statistics = True + + +.. option:: --require-plugins= + + :ref:`Go back to index ` + + Require specific plugins to be installed before running. + + This option takes a list of distribution names (usually the name you would + use when running ``pip install``). + + Command-line example: + + .. prompt:: bash + + flake8 --require-plugins=flake8-2020,flake8-typing-extensions dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + require-plugins = + flake8-2020 + flake8-typing-extensions + + +.. _option-enable-extensions: + +.. option:: --enable-extensions= + + :ref:`Go back to index ` + + Enable :ref:`off-by-default` extensions. + + Plugins to |Flake8| have the option of registering themselves as + off-by-default. These plugins will not be loaded unless enabled by this + option. + + Command-line example: + + .. prompt:: bash + + flake8 --enable-extensions=H111 dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + enable-extensions = + H111, + G123 + enable_extensions = + H111, + G123 + + +.. option:: --exit-zero + + :ref:`Go back to index ` + + Force |Flake8| to use the exit status code 0 even if there are errors. + + By default |Flake8| will exit with a non-zero integer if there are errors. + + Command-line example: + + .. prompt:: bash + + flake8 --exit-zero dir/ + + This **can not** be specified in config files. + + +.. option:: --jobs= + + :ref:`Go back to index ` + + Specify the number of subprocesses that |Flake8| will use to run checks in + parallel. + + .. note:: + + This option is ignored on platforms where ``fork`` is not a + supported ``multiprocessing`` method. + + This defaults to: ``auto`` + + The default behaviour will use the number of CPUs on your machine as + reported by :func:`multiprocessing.cpu_count`. + + Command-line example: + + .. prompt:: bash + + flake8 --jobs=8 dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + jobs = 8 + + +.. option:: --output-file= + + :ref:`Go back to index ` + + Redirect all output to the specified file. + + Command-line example: + + .. prompt:: bash + + flake8 --output-file=output.txt dir/ + flake8 -vv --output-file=output.txt dir/ + + +.. option:: --tee + + :ref:`Go back to index ` + + Also print output to stdout if output-file has been configured. + + Command-line example: + + .. prompt:: bash + + flake8 --tee --output-file=output.txt dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + tee = True + + +.. option:: --append-config= + + :ref:`Go back to index ` + + .. versionadded:: 3.6.0 + + Provide extra config files to parse in after and in addition to the files + that |Flake8| found on its own. Since these files are the last ones read + into the Configuration Parser, so it has the highest precedence if it + provides an option specified in another config file. + + Command-line example: + + .. prompt:: bash + + flake8 --append-config=my-extra-config.ini dir/ + + This **can not** be specified in config files. + + +.. option:: --config= + + :ref:`Go back to index ` + + Provide a path to a config file that will be the only config file read and + used. This will cause |Flake8| to ignore all other config files that + exist. + + Command-line example: + + .. prompt:: bash + + flake8 --config=my-only-config.ini dir/ + + This **can not** be specified in config files. + + +.. option:: --isolated + + :ref:`Go back to index ` + + Ignore any config files and use |Flake8| as if there were no config files + found. + + Command-line example: + + .. prompt:: bash + + flake8 --isolated dir/ + + This **can not** be specified in config files. + + +.. option:: --builtins= + + :ref:`Go back to index ` + + Provide a custom list of builtin functions, objects, names, etc. + + This allows you to let pyflakes know about builtins that it may + not immediately recognize so it does not report warnings for using + an undefined name. + + This is registered by the default PyFlakes plugin. + + Command-line example: + + .. prompt:: bash + + flake8 --builtins=_,_LE,_LW dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + builtins = + _, + _LE, + _LW + + +.. option:: --doctests + + :ref:`Go back to index ` + + Enable PyFlakes syntax checking of doctests in docstrings. + + This is registered by the default PyFlakes plugin. + + Command-line example: + + .. prompt:: bash + + flake8 --doctests dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + doctests = True + + +.. option:: --benchmark + + :ref:`Go back to index ` + + Collect and print benchmarks for this run of |Flake8|. This aggregates the + total number of: + + - tokens + - physical lines + - logical lines + - files + + and the number of elapsed seconds. + + Command-line usage: + + .. prompt:: bash + + flake8 --benchmark dir/ + + This **can not** be specified in config files. + + +.. option:: --bug-report + + :ref:`Go back to index ` + + Generate information necessary to file a complete bug report for Flake8. + This will pretty-print a JSON blob that should be copied and pasted into a + bug report for Flake8. + + Command-line usage: + + .. prompt:: bash + + flake8 --bug-report + + The output should look vaguely like: + + .. code-block:: js + + { + "dependencies": [ + { + "dependency": "setuptools", + "version": "25.1.1" + } + ], + "platform": { + "python_implementation": "CPython", + "python_version": "2.7.12", + "system": "Darwin" + }, + "plugins": [ + { + "plugin": "mccabe", + "version": "0.5.1" + }, + { + "plugin": "pycodestyle", + "version": "2.0.0" + }, + { + "plugin": "pyflakes", + "version": "1.2.3" + } + ], + "version": "3.1.0.dev0" + } + + This **can not** be specified in config files. + + +.. option:: --max-complexity= + + :ref:`Go back to index ` + + Set the maximum allowed McCabe complexity value for a block of code. + + This option is provided by the ``mccabe`` dependency's |Flake8| plugin. + + Command-line usage: + + .. prompt:: bash + + flake8 --max-complexity 15 dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + max-complexity = 15 diff --git a/src/flake8-main/flake8-main/docs/source/user/python-api.rst b/src/flake8-main/flake8-main/docs/source/user/python-api.rst new file mode 100644 index 0000000..f51b146 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/user/python-api.rst @@ -0,0 +1,103 @@ +=================== + Public Python API +=================== + +|Flake8| 3.0.0 presently does not have a public, stable Python API. + +When it does it will be located in :mod:`flake8.api` and that will +be documented here. + + +Legacy API +========== + +When |Flake8| broke its hard dependency on the tricky internals of +pycodestyle, it lost the easy backwards compatibility as well. To help +existing users of that API we have :mod:`flake8.api.legacy`. This module +includes a couple classes (which are documented below) and a function. + +The main usage that the developers of Flake8 observed was using the +:func:`~flake8.api.legacy.get_style_guide` function and then calling +:meth:`~flake8.api.legacy.StyleGuide.check_files`. To a lesser extent, +people also seemed to use the :meth:`~flake8.api.legacy.Report.get_statistics` +method on what ``check_files`` returns. We then sought to preserve that +API in this module. + +Let's look at an example piece of code together: + +.. code-block:: python + + from flake8.api import legacy as flake8 + + + style_guide = flake8.get_style_guide(ignore=['E24', 'W503']) + report = style_guide.check_files([...]) + assert report.get_statistics('E') == [], 'Flake8 found violations' + +This represents the basic universal usage of all existing Flake8 2.x +integrations. Each example we found was obviously slightly different, +but this is kind of the gist, so let's walk through this. + +Everything that is backwards compatible for our API is in the +:mod:`flake8.api.legacy` submodule. This is to indicate, clearly, that +the old API is being used. + +We create a |StyleGuide| by calling |style_guide|. We can pass options +to |style_guide| that correspond to the command-line options one might use. +For example, we can pass ``ignore``, ``select``, ``exclude``, ``format``, etc. +Our legacy API, does not enforce legacy behaviour, so we can combine +``ignore`` and ``select`` like we might on the command-line, e.g., + +.. code-block:: python + + style_guide = flake8.get_style_guide( + ignore=['E24', 'W5'], + select=['E', 'W', 'F'], + format='pylint', + ) + +Once we have our |StyleGuide| we can use the same methods that we used before, +namely + +.. automethod:: flake8.api.legacy.StyleGuide.check_files + +.. automethod:: flake8.api.legacy.StyleGuide.excluded + +.. automethod:: flake8.api.legacy.StyleGuide.init_report + +.. automethod:: flake8.api.legacy.StyleGuide.input_file + +.. warning:: + + These are not *perfectly* backwards compatible. Not all arguments are + respected, and some of the types necessary for something to work have + changed. + +Most people, we observed, were using +:meth:`~flake8.api.legacy.StyleGuide.check_files`. You can use this to specify +a list of filenames or directories to check. In |Flake8| 3.0, however, we +return a different object that has similar methods. We return a |Report| which +has the method + +.. automethod:: flake8.api.legacy.Report.get_statistics + +Most usage of this method that we noted was as documented above. Keep in mind, +however, that it provides a list of strings and not anything more malleable. + + +Autogenerated Legacy Documentation +---------------------------------- + +.. automodule:: flake8.api.legacy + :members: + +.. autoclass:: flake8.api.legacy.StyleGuide + :members: options, paths + +.. autoclass:: flake8.api.legacy.Report + :members: total_errors + + +.. |style_guide| replace:: :func:`flake8.api.legacy.get_style_guide` +.. |StyleGuide| replace:: :class:`flake8.api.legacy.StyleGuide` +.. |Report| replace:: :class:`flake8.api.legacy.Report` diff --git a/src/flake8-main/flake8-main/docs/source/user/using-hooks.rst b/src/flake8-main/flake8-main/docs/source/user/using-hooks.rst new file mode 100644 index 0000000..5a2e2b0 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/user/using-hooks.rst @@ -0,0 +1,42 @@ +============================= + Using Version Control Hooks +============================= + +Usage with the `pre-commit`_ git hooks framework +================================================ + +|Flake8| can be included as a hook for `pre-commit`_. The easiest way to get +started is to add this configuration to your ``.pre-commit-config.yaml``: + +.. code-block:: yaml + + - repo: https://github.com/pycqa/flake8 + rev: '' # pick a git hash / tag to point to + hooks: + - id: flake8 + +See the `pre-commit docs`_ for how to customize this configuration. + +Checked-in python files will be passed as positional arguments. ``flake8`` +will always lint explicitly passed arguments (:option:`flake8 --exclude` has +no effect). Instead use ``pre-commit``'s ``exclude: ...`` regex to exclude +files. ``pre-commit`` won't ever pass untracked files to ``flake8`` so +excluding ``.git`` / ``.tox`` / etc. is unnecessary. + +.. code-block:: yaml + + - id: flake8 + exclude: ^testing/(data|examples)/ + +``pre-commit`` creates an isolated environment for hooks. To use ``flake8`` +plugins, use the ``additional_dependencies`` setting. + +.. code-block:: yaml + + - id: flake8 + additional_dependencies: [flake8-docstrings] + +.. _pre-commit: + https://pre-commit.com/ +.. _pre-commit docs: + https://pre-commit.com/#pre-commit-configyaml---hooks diff --git a/src/flake8-main/flake8-main/docs/source/user/using-plugins.rst b/src/flake8-main/flake8-main/docs/source/user/using-plugins.rst new file mode 100644 index 0000000..5d577c5 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/user/using-plugins.rst @@ -0,0 +1,64 @@ +================================== + Using Plugins For Fun and Profit +================================== + +|Flake8| is useful on its own but a lot of |Flake8|'s popularity is due to +its extensibility. Our community has developed :term:`plugin`\ s that augment +|Flake8|'s behaviour. Most of these plugins are uploaded to PyPI_. The +developers of these plugins often have some style they wish to enforce. + +For example, `flake8-docstrings`_ adds a check for :pep:`257` style +conformance. Others attempt to enforce consistency, like `flake8-quotes`_. + +.. note:: + + The accuracy or reliability of these plugins may vary wildly from plugin + to plugin and not all plugins are guaranteed to work with |Flake8| 3.0. + +To install a third-party plugin, make sure that you know which version of +Python (or pip) you used to install |Flake8|. You can then use the most +appropriate of: + +.. prompt:: bash + + pip install + pip3 install + python -m pip install + python3 -m pip install + python3.9 -m pip install + +To install the plugin, where ```` is the package name on PyPI_. +To verify installation use: + +.. prompt:: bash + + flake8 --version + python -m flake8 --version + +To see the plugin's name and version in the output. + +.. seealso:: :ref:`How to Invoke Flake8 ` + +After installation, most plugins immediately start reporting :term:`error`\ s. +Check the plugin's documentation for which error codes it returns and if it +disables any by default. + +.. note:: + + You can use both :option:`flake8 --select` and :option:`flake8 --ignore` + with plugins. + +Some plugins register new options, so be sure to check :option:`flake8 --help` +for new flags and documentation. These plugins may also allow these flags to +be specified in your configuration file. Hopefully, the plugin authors have +documented this for you. + +.. seealso:: :ref:`Configuring Flake8 ` + + +.. _PyPI: + https://pypi.org/ +.. _flake8-docstrings: + https://pypi.org/project/flake8-docstrings/ +.. _flake8-quotes: + https://pypi.org/project/flake8-quotes/ diff --git a/src/flake8-main/flake8-main/docs/source/user/violations.rst b/src/flake8-main/flake8-main/docs/source/user/violations.rst new file mode 100644 index 0000000..e7591e7 --- /dev/null +++ b/src/flake8-main/flake8-main/docs/source/user/violations.rst @@ -0,0 +1,306 @@ +=================================== + Selecting and Ignoring Violations +=================================== + +It is possible to select and ignore certain violations reported by |Flake8| +and the plugins we've installed. It's also possible as of |Flake8| 3.0 to +combine usage of :option:`flake8 --select` and :option:`flake8 --ignore`. This +chapter of the User Guide aims to educate about how Flake8 will report errors +based on different inputs. + + + +Ignoring Violations with Flake8 +=============================== + +By default, |Flake8| has a list of error codes that it ignores. The list used +by a version of |Flake8| may be different than the list used by a different +version. To see the default list, :option:`flake8 --help` will +show the output with the current default list. + +Extending the Default Ignore List +--------------------------------- + +If we want to extend the default list of ignored error codes, we can use +:option:`flake8 --extend-ignore` to specify a comma-separated list of codes +for a specific run on the command line, e.g., + +.. prompt:: bash + + flake8 --extend-ignore=E1,E23 path/to/files/ path/to/more/files + +This tells |Flake8| to ignore any error codes starting with ``E1`` and ``E23``, +in addition the default ignore list. To view the default error code ignore +list, run :option:`flake8 --help` and refer to the help text for +:option:`flake8 --ignore`. + + +.. + The section below used to be titled `Changing the Default Ignore List`, but + was renamed for clarity. + Explicitly retain the old section anchor so as to not break links: + +.. _changing-the-ignore-list: + +Overriding the Default Ignore List +---------------------------------- + +If we want to *completely* override the default list of ignored error codes, we +can use :option:`flake8 --ignore` to specify a comma-separated list of codes +for a specific run on the command-line, e.g., + +.. prompt:: bash + + flake8 --ignore=E1,E23,W503 path/to/files/ path/to/more/files/ + +This tells |Flake8| to *only* ignore error codes starting with ``E1``, ``E23``, +or ``W503`` while it is running. + +.. note:: + + The documentation for :option:`flake8 --ignore` shows examples for how + to change the ignore list in the configuration file. See also + :ref:`configuration` as well for details about how to use configuration + files. + + +In-line Ignoring Errors +----------------------- + +In some cases, we might not want to ignore an error code (or class of error +codes) for the entirety of our project. Instead, we might want to ignore the +specific error code on a specific line. Let's take for example a line like + +.. code-block:: python + + example = lambda: 'example' + +Sometimes we genuinely need something this simple. We could instead define +a function like we normally would. Note, in some contexts this distracts from +what is actually happening. In those cases, we can also do: + +.. code-block:: python + + example = lambda: 'example' # noqa: E731 + +This will only ignore the error from pycodestyle that checks for lambda +assignments and generates an ``E731``. If there are other errors on the line +then those will be reported. ``# noqa`` is case-insensitive, without the colon +the part after ``# noqa`` would be ignored. + +.. note:: + + If we ever want to disable |Flake8| respecting ``# noqa`` comments, we can + refer to :option:`flake8 --disable-noqa`. + +If we instead had more than one error that we wished to ignore, we could +list all of the errors with commas separating them: + +.. code-block:: python + + # noqa: E731,E123 + +Finally, if we have a particularly bad line of code, we can ignore every error +using simply ``# noqa`` with nothing after it. + +Contents before and after the ``# noqa: ...`` portion are ignored so multiple +comments may appear on one line. Here are several examples: + +.. code-block:: python + + # mypy requires `# type: ignore` to appear first + x = 5 # type: ignore # noqa: ABC123 + + # can use to add useful user information to a noqa comment + y = 6 # noqa: ABC456 # TODO: will fix this later + + +Ignoring Entire Files +--------------------- + +Imagine a situation where we are adding |Flake8| to a codebase. Let's further +imagine that with the exception of a few particularly bad files, we can add +|Flake8| easily and move on with our lives. There are two ways to ignore the +file: + +#. By explicitly adding it to our list of excluded paths (see: :option:`flake8 + --exclude`) + +#. By adding ``# flake8: noqa`` to the file + +The former is the **recommended** way of ignoring entire files. By using our +exclude list, we can include it in our configuration file and have one central +place to find what files aren't included in |Flake8| checks. The latter has the +benefit that when we run |Flake8| with :option:`flake8 --disable-noqa` all of +the errors in that file will show up without having to modify our +configuration. Both exist so we can choose which is better for us. + + + +Selecting Violations with Flake8 +================================ + +|Flake8| has a default list of violation classes that we use. This list is: + +- ``C90`` + + All ``C90`` class violations are reported when the user specifies + :option:`flake8 --max-complexity` + +- ``E`` + + All ``E`` class violations are "errors" reported by pycodestyle + +- ``F`` + + All ``F`` class violations are reported by pyflakes + +- ``W`` + + All ``W`` class violations are "warnings" reported by pycodestyle + +This list can be overridden by specifying :option:`flake8 --select`. Just as +specifying :option:`flake8 --ignore` will change the behaviour of |Flake8|, so +will :option:`flake8 --select`. + +Let's look through some examples using this sample code: + +.. code-block:: python + + # example.py + def foo(): + print( + "Hello" + "World" + ) + +By default, if we run ``flake8`` on this file we'll get: + +.. prompt:: bash + + flake8 example.py + +.. code:: text + + example.py:4:9: E131 continuation line unaligned for hanging indent + +Now let's select all ``E`` class violations: + +.. prompt:: bash + + flake8 --select E example.py + +.. code:: text + + example.py:3:17: E126 continuation line over-indented for hanging indent + example.py:4:9: E131 continuation line unaligned for hanging indent + example.py:5:9: E121 continuation line under-indented for hanging indent + +Suddenly we now have far more errors that are reported to us. Using +``--select`` alone will override the default ``--ignore`` list. In these cases, +the user is telling us that they want all ``E`` violations and so we ignore +our list of violations that we ignore by default. + +We can also be highly specific. For example, we can do + +.. prompt:: bash + + flake8 --select E121 example.py + +.. code:: text + + example.py:5:9: E121 continuation line under-indented for hanging indent + +We can also specify lists of items to select both on the command-line and in +our configuration files. + +.. prompt:: bash + + flake8 --select E121,E131 example.py + +.. code:: text + + example.py:4:9: E131 continuation line unaligned for hanging indent + example.py:5:9: E121 continuation line under-indented for hanging indent + + + +Selecting and Ignoring Simultaneously For Fun and Profit +======================================================== + +Prior to |Flake8| 3.0, all handling of :option:`flake8 --select` and +:option:`flake8 --ignore` was delegated to pycodestyle. Its handling of the +options significantly differs from how |Flake8| 3.0 has been designed. + +pycodestyle has always preferred ``--ignore`` over ``--select`` and will +ignore ``--select`` if the user provides both. |Flake8| 3.0 will now do its +best to intuitively combine both options provided by the user. Let's look at +some examples using: + +.. code-block:: python + + # example.py + import os + + + def foo(): + var = 1 + print( + "Hello" + "World" + ) + +If we run |Flake8| with its default settings we get: + +.. prompt:: bash + + flake8 example.py + +.. code:: text + + example.py:1:1: F401 'os' imported but unused + example.py:5:5: F841 local variable 'var' is assigned to but never used + example.py:8:9: E131 continuation line unaligned for hanging indent + +Now let's select all ``E`` and ``F`` violations including those in the default +ignore list. + +.. prompt:: bash + + flake8 --select E,F example.py + +.. code:: text + + example.py:1:1: F401 'os' imported but unused + example.py:5:5: F841 local variable 'var' is assigned to but never used + example.py:7:17: E126 continuation line over-indented for hanging indent + example.py:8:9: E131 continuation line unaligned for hanging indent + example.py:9:9: E121 continuation line under-indented for hanging indent + +Now let's selectively ignore some of these while selecting the rest: + +.. prompt:: bash + + flake8 --select E,F --ignore F401,E121 example.py + +.. code:: text + + example.py:5:5: F841 local variable 'var' is assigned to but never used + example.py:7:17: E126 continuation line over-indented for hanging indent + example.py:8:9: E131 continuation line unaligned for hanging indent + +Via this example, we can see that the *most specific* **user-specified** rule +will win. So in the above, we had very vague select rules and two very +specific ignore rules. Let's look at a different example: + +.. prompt:: bash + + flake8 --select F401,E131 --ignore E,F example.py + +.. code:: text + + example.py:1:1: F401 'os' imported but unused + example.py:8:9: E131 continuation line unaligned for hanging indent + +In this case, we see that since our selected violation codes were more +specific those were reported. diff --git a/src/flake8-main/flake8-main/example-plugin/setup.py b/src/flake8-main/flake8-main/example-plugin/setup.py new file mode 100644 index 0000000..9e7c89f --- /dev/null +++ b/src/flake8-main/flake8-main/example-plugin/setup.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import setuptools + +setuptools.setup( + name="flake8-example-plugin", + license="MIT", + version="1.0.0", + description="Example plugin to Flake8", + author="Ian Cordasco", + author_email="graffatcolmingov@gmail.com", + url="https://github.com/pycqa/flake8", + package_dir={"": "src/"}, + packages=["flake8_example_plugin"], + entry_points={ + "flake8.extension": [ + "X1 = flake8_example_plugin:ExampleOne", + "X2 = flake8_example_plugin:ExampleTwo", + ], + }, + classifiers=[ + "Framework :: Flake8", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Quality Assurance", + ], +) diff --git a/src/flake8-main/flake8-main/example-plugin/src/flake8_example_plugin/__init__.py b/src/flake8-main/flake8-main/example-plugin/src/flake8_example_plugin/__init__.py new file mode 100644 index 0000000..47851da --- /dev/null +++ b/src/flake8-main/flake8-main/example-plugin/src/flake8_example_plugin/__init__.py @@ -0,0 +1,10 @@ +"""Module for an example Flake8 plugin.""" +from __future__ import annotations + +from .off_by_default import ExampleTwo +from .on_by_default import ExampleOne + +__all__ = ( + "ExampleOne", + "ExampleTwo", +) diff --git a/src/flake8-main/flake8-main/example-plugin/src/flake8_example_plugin/off_by_default.py b/src/flake8-main/flake8-main/example-plugin/src/flake8_example_plugin/off_by_default.py new file mode 100644 index 0000000..d140ca1 --- /dev/null +++ b/src/flake8-main/flake8-main/example-plugin/src/flake8_example_plugin/off_by_default.py @@ -0,0 +1,20 @@ +"""Our first example plugin.""" +from __future__ import annotations + + +class ExampleTwo: + """Second Example Plugin.""" + + off_by_default = True + + def __init__(self, tree): + self.tree = tree + + def run(self): + """Do nothing.""" + yield ( + 1, + 0, + "X200 The off-by-default plugin was enabled", + "OffByDefaultPlugin", + ) diff --git a/src/flake8-main/flake8-main/example-plugin/src/flake8_example_plugin/on_by_default.py b/src/flake8-main/flake8-main/example-plugin/src/flake8_example_plugin/on_by_default.py new file mode 100644 index 0000000..d2da126 --- /dev/null +++ b/src/flake8-main/flake8-main/example-plugin/src/flake8_example_plugin/on_by_default.py @@ -0,0 +1,13 @@ +"""Our first example plugin.""" +from __future__ import annotations + + +class ExampleOne: + """First Example Plugin.""" + + def __init__(self, tree): + self.tree = tree + + def run(self): + """Do nothing.""" + yield from [] diff --git a/src/flake8-main/flake8-main/pytest.ini b/src/flake8-main/flake8-main/pytest.ini new file mode 100644 index 0000000..0301af3 --- /dev/null +++ b/src/flake8-main/flake8-main/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +norecursedirs = .git .* *.egg* docs dist build +addopts = -rw +filterwarnings = error diff --git a/src/flake8-main/flake8-main/setup.cfg b/src/flake8-main/flake8-main/setup.cfg new file mode 100644 index 0000000..dc967d3 --- /dev/null +++ b/src/flake8-main/flake8-main/setup.cfg @@ -0,0 +1,74 @@ +[metadata] +name = flake8 +version = attr: flake8.__version__ +description = the modular source code checker: pep8 pyflakes and co +long_description = file: README.rst +long_description_content_type = text/x-rst +url = https://github.com/pycqa/flake8 +author = Tarek Ziade +author_email = tarek@ziade.org +maintainer = Ian Stapleton Cordasco +maintainer_email = graffatcolmingov@gmail.com +license = MIT +license_files = LICENSE +classifiers = + Development Status :: 5 - Production/Stable + Environment :: Console + Framework :: Flake8 + Intended Audience :: Developers + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy + Topic :: Software Development :: Libraries :: Python Modules + Topic :: Software Development :: Quality Assurance + +[options] +packages = find: +install_requires = + mccabe>=0.7.0,<0.8.0 + pycodestyle>=2.14.0,<2.15.0 + pyflakes>=3.4.0,<3.5.0 +python_requires = >=3.9 +package_dir = + =src + +[options.packages.find] +where = src + +[options.entry_points] +console_scripts = + flake8 = flake8.main.cli:main +flake8.extension = + F = flake8.plugins.pyflakes:FlakesChecker + E = flake8.plugins.pycodestyle:pycodestyle_logical + W = flake8.plugins.pycodestyle:pycodestyle_physical +flake8.report = + default = flake8.formatting.default:Default + pylint = flake8.formatting.default:Pylint + quiet-filename = flake8.formatting.default:FilenameOnly + quiet-nothing = flake8.formatting.default:Nothing + +[bdist_wheel] +universal = 1 + +[coverage:run] +source = + flake8 + tests +plugins = covdefaults + +[coverage:report] +fail_under = 97 + +[mypy] +check_untyped_defs = true +disallow_any_generics = true +disallow_incomplete_defs = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_unused_ignores = true + +[mypy-tests.*] +disallow_untyped_defs = false diff --git a/src/flake8-main/flake8-main/setup.py b/src/flake8-main/flake8-main/setup.py new file mode 100644 index 0000000..253a22e --- /dev/null +++ b/src/flake8-main/flake8-main/setup.py @@ -0,0 +1,11 @@ +"""Packaging logic for Flake8.""" +from __future__ import annotations + +import os +import sys + +import setuptools + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src")) + +setuptools.setup() diff --git a/src/flake8-main/flake8-main/src/flake8/__init__.py b/src/flake8-main/flake8-main/src/flake8/__init__.py new file mode 100644 index 0000000..0dea638 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/__init__.py @@ -0,0 +1,70 @@ +"""Top-level module for Flake8. + +This module + +- initializes logging for the command-line tool +- tracks the version of the package +- provides a way to configure logging for the command-line tool + +.. autofunction:: flake8.configure_logging + +""" +from __future__ import annotations + +import logging +import sys + +LOG = logging.getLogger(__name__) +LOG.addHandler(logging.NullHandler()) + +__version__ = "7.3.0" +__version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit()) + +_VERBOSITY_TO_LOG_LEVEL = { + # output more than warnings but not debugging info + 1: logging.INFO, # INFO is a numerical level of 20 + # output debugging information + 2: logging.DEBUG, # DEBUG is a numerical level of 10 +} + +LOG_FORMAT = ( + "%(name)-25s %(processName)-11s %(relativeCreated)6d " + "%(levelname)-8s %(message)s" +) + + +def configure_logging( + verbosity: int, + filename: str | None = None, + logformat: str = LOG_FORMAT, +) -> None: + """Configure logging for flake8. + + :param verbosity: + How verbose to be in logging information. + :param filename: + Name of the file to append log information to. + If ``None`` this will log to ``sys.stderr``. + If the name is "stdout" or "stderr" this will log to the appropriate + stream. + """ + if verbosity <= 0: + return + + verbosity = min(verbosity, max(_VERBOSITY_TO_LOG_LEVEL)) + log_level = _VERBOSITY_TO_LOG_LEVEL[verbosity] + + if not filename or filename in ("stderr", "stdout"): + fileobj = getattr(sys, filename or "stderr") + handler_cls: type[logging.Handler] = logging.StreamHandler + else: + fileobj = filename + handler_cls = logging.FileHandler + + handler = handler_cls(fileobj) + handler.setFormatter(logging.Formatter(logformat)) + LOG.addHandler(handler) + LOG.setLevel(log_level) + LOG.debug( + "Added a %s logging handler to logger root at %s", filename, __name__, + ) diff --git a/src/flake8-main/flake8-main/src/flake8/__main__.py b/src/flake8-main/flake8-main/src/flake8/__main__.py new file mode 100644 index 0000000..8f7e7c9 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/__main__.py @@ -0,0 +1,7 @@ +"""Module allowing for ``python -m flake8 ...``.""" +from __future__ import annotations + +from flake8.main.cli import main + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/flake8-main/flake8-main/src/flake8/_compat.py b/src/flake8-main/flake8-main/src/flake8/_compat.py new file mode 100644 index 0000000..22bb84e --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/_compat.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +import sys +import tokenize + +if sys.version_info >= (3, 12): # pragma: >=3.12 cover + FSTRING_START = tokenize.FSTRING_START + FSTRING_MIDDLE = tokenize.FSTRING_MIDDLE + FSTRING_END = tokenize.FSTRING_END +else: # pragma: <3.12 cover + FSTRING_START = FSTRING_MIDDLE = FSTRING_END = -1 + +if sys.version_info >= (3, 14): # pragma: >=3.14 cover + TSTRING_START = tokenize.TSTRING_START + TSTRING_MIDDLE = tokenize.TSTRING_MIDDLE + TSTRING_END = tokenize.TSTRING_END +else: # pragma: <3.14 cover + TSTRING_START = TSTRING_MIDDLE = TSTRING_END = -1 diff --git a/src/flake8-main/flake8-main/src/flake8/api/__init__.py b/src/flake8-main/flake8-main/src/flake8/api/__init__.py new file mode 100644 index 0000000..c5f9711 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/api/__init__.py @@ -0,0 +1,6 @@ +"""Module containing all public entry-points for Flake8. + +This is the only submodule in Flake8 with a guaranteed stable API. All other +submodules are considered internal only and are subject to change. +""" +from __future__ import annotations diff --git a/src/flake8-main/flake8-main/src/flake8/api/legacy.py b/src/flake8-main/flake8-main/src/flake8/api/legacy.py new file mode 100644 index 0000000..4d5c91d --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/api/legacy.py @@ -0,0 +1,216 @@ +"""Module containing shims around Flake8 2.x behaviour. + +Previously, users would import :func:`get_style_guide` from ``flake8.engine``. +In 3.0 we no longer have an "engine" module but we maintain the API from it. +""" +from __future__ import annotations + +import argparse +import logging +import os.path +from typing import Any + +from flake8.discover_files import expand_paths +from flake8.formatting import base as formatter +from flake8.main import application as app +from flake8.options.parse_args import parse_args + +LOG = logging.getLogger(__name__) + + +__all__ = ("get_style_guide",) + + +class Report: + """Public facing object that mimic's Flake8 2.0's API. + + .. note:: + + There are important changes in how this object behaves compared to + the object provided in Flake8 2.x. + + .. warning:: + + This should not be instantiated by users. + + .. versionchanged:: 3.0.0 + """ + + def __init__(self, application: app.Application) -> None: + """Initialize the Report for the user. + + .. warning:: This should not be instantiated by users. + """ + assert application.guide is not None + self._application = application + self._style_guide = application.guide + self._stats = self._style_guide.stats + + @property + def total_errors(self) -> int: + """Return the total number of errors.""" + return self._application.result_count + + def get_statistics(self, violation: str) -> list[str]: + """Get the list of occurrences of a violation. + + :returns: + List of occurrences of a violation formatted as: + {Count} {Error Code} {Message}, e.g., + ``8 E531 Some error message about the error`` + """ + return [ + f"{s.count} {s.error_code} {s.message}" + for s in self._stats.statistics_for(violation) + ] + + +class StyleGuide: + """Public facing object that mimic's Flake8 2.0's StyleGuide. + + .. note:: + + There are important changes in how this object behaves compared to + the StyleGuide object provided in Flake8 2.x. + + .. warning:: + + This object should not be instantiated directly by users. + + .. versionchanged:: 3.0.0 + """ + + def __init__(self, application: app.Application) -> None: + """Initialize our StyleGuide.""" + self._application = application + self._file_checker_manager = application.file_checker_manager + + @property + def options(self) -> argparse.Namespace: + """Return application's options. + + An instance of :class:`argparse.Namespace` containing parsed options. + """ + assert self._application.options is not None + return self._application.options + + @property + def paths(self) -> list[str]: + """Return the extra arguments passed as paths.""" + assert self._application.options is not None + return self._application.options.filenames + + def check_files(self, paths: list[str] | None = None) -> Report: + """Run collected checks on the files provided. + + This will check the files passed in and return a :class:`Report` + instance. + + :param paths: + List of filenames (or paths) to check. + :returns: + Object that mimic's Flake8 2.0's Reporter class. + """ + assert self._application.options is not None + self._application.options.filenames = paths + self._application.run_checks() + self._application.report_errors() + return Report(self._application) + + def excluded(self, filename: str, parent: str | None = None) -> bool: + """Determine if a file is excluded. + + :param filename: + Path to the file to check if it is excluded. + :param parent: + Name of the parent directory containing the file. + :returns: + True if the filename is excluded, False otherwise. + """ + + def excluded(path: str) -> bool: + paths = tuple( + expand_paths( + paths=[path], + stdin_display_name=self.options.stdin_display_name, + filename_patterns=self.options.filename, + exclude=self.options.exclude, + ), + ) + return not paths + + return excluded(filename) or ( + parent is not None and excluded(os.path.join(parent, filename)) + ) + + def init_report( + self, + reporter: type[formatter.BaseFormatter] | None = None, + ) -> None: + """Set up a formatter for this run of Flake8.""" + if reporter is None: + return + if not issubclass(reporter, formatter.BaseFormatter): + raise ValueError( + "Report should be subclass of " + "flake8.formatter.BaseFormatter.", + ) + self._application.formatter = reporter(self.options) + self._application.guide = None + # NOTE(sigmavirus24): This isn't the intended use of + # Application#make_guide but it works pretty well. + # Stop cringing... I know it's gross. + self._application.make_guide() + self._application.file_checker_manager = None + self._application.make_file_checker_manager([]) + + def input_file( + self, + filename: str, + lines: Any | None = None, + expected: Any | None = None, + line_offset: Any | None = 0, + ) -> Report: + """Run collected checks on a single file. + + This will check the file passed in and return a :class:`Report` + instance. + + :param filename: + The path to the file to check. + :param lines: + Ignored since Flake8 3.0. + :param expected: + Ignored since Flake8 3.0. + :param line_offset: + Ignored since Flake8 3.0. + :returns: + Object that mimic's Flake8 2.0's Reporter class. + """ + return self.check_files([filename]) + + +def get_style_guide(**kwargs: Any) -> StyleGuide: + r"""Provision a StyleGuide for use. + + :param \*\*kwargs: + Keyword arguments that provide some options for the StyleGuide. + :returns: + An initialized StyleGuide + """ + application = app.Application() + application.plugins, application.options = parse_args([]) + # We basically want application.initialize to be called but with these + # options set instead before we make our formatter, notifier, internal + # style guide and file checker manager. + options = application.options + for key, value in kwargs.items(): + try: + getattr(options, key) + setattr(options, key, value) + except AttributeError: + LOG.error('Could not update option "%s"', key) + application.make_formatter() + application.make_guide() + application.make_file_checker_manager([]) + return StyleGuide(application) diff --git a/src/flake8-main/flake8-main/src/flake8/checker.py b/src/flake8-main/flake8-main/src/flake8/checker.py new file mode 100644 index 0000000..d957915 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/checker.py @@ -0,0 +1,615 @@ +"""Checker Manager and Checker classes.""" +from __future__ import annotations + +import argparse +import contextlib +import errno +import logging +import multiprocessing.pool +import operator +import signal +import tokenize +from collections.abc import Generator +from collections.abc import Sequence +from typing import Any +from typing import Optional + +from flake8 import defaults +from flake8 import exceptions +from flake8 import processor +from flake8 import utils +from flake8._compat import FSTRING_START +from flake8._compat import TSTRING_START +from flake8.discover_files import expand_paths +from flake8.options.parse_args import parse_args +from flake8.plugins.finder import Checkers +from flake8.plugins.finder import LoadedPlugin +from flake8.style_guide import StyleGuideManager + +Results = list[tuple[str, int, int, str, Optional[str]]] + +LOG = logging.getLogger(__name__) + +SERIAL_RETRY_ERRNOS = { + # ENOSPC: Added by sigmavirus24 + # > On some operating systems (OSX), multiprocessing may cause an + # > ENOSPC error while trying to create a Semaphore. + # > In those cases, we should replace the customized Queue Report + # > class with pep8's StandardReport class to ensure users don't run + # > into this problem. + # > (See also: https://github.com/pycqa/flake8/issues/117) + errno.ENOSPC, + # NOTE(sigmavirus24): When adding to this list, include the reasoning + # on the lines before the error code and always append your error + # code. Further, please always add a trailing `,` to reduce the visual + # noise in diffs. +} + +_mp: tuple[Checkers, argparse.Namespace] | None = None + + +@contextlib.contextmanager +def _mp_prefork( + plugins: Checkers, options: argparse.Namespace, +) -> Generator[None]: + # we can save significant startup work w/ `fork` multiprocessing + global _mp + _mp = plugins, options + try: + yield + finally: + _mp = None + + +def _mp_init(argv: Sequence[str]) -> None: + global _mp + + # Ensure correct signaling of ^C using multiprocessing.Pool. + signal.signal(signal.SIGINT, signal.SIG_IGN) + + # for `fork` this'll already be set + if _mp is None: + plugins, options = parse_args(argv) + _mp = plugins.checkers, options + + +def _mp_run(filename: str) -> tuple[str, Results, dict[str, int]]: + assert _mp is not None, _mp + plugins, options = _mp + return FileChecker( + filename=filename, plugins=plugins, options=options, + ).run_checks() + + +class Manager: + """Manage the parallelism and checker instances for each plugin and file. + + This class will be responsible for the following: + + - Determining the parallelism of Flake8, e.g.: + + * Do we use :mod:`multiprocessing` or is it unavailable? + + * Do we automatically decide on the number of jobs to use or did the + user provide that? + + - Falling back to a serial way of processing files if we run into an + OSError related to :mod:`multiprocessing` + + - Organizing the results of each checker so we can group the output + together and make our output deterministic. + """ + + def __init__( + self, + style_guide: StyleGuideManager, + plugins: Checkers, + argv: Sequence[str], + ) -> None: + """Initialize our Manager instance.""" + self.style_guide = style_guide + self.options = style_guide.options + self.plugins = plugins + self.jobs = self._job_count() + self.statistics = { + "files": 0, + "logical lines": 0, + "physical lines": 0, + "tokens": 0, + } + self.exclude = (*self.options.exclude, *self.options.extend_exclude) + self.argv = argv + self.results: list[tuple[str, Results, dict[str, int]]] = [] + + def _process_statistics(self) -> None: + for _, _, statistics in self.results: + for statistic in defaults.STATISTIC_NAMES: + self.statistics[statistic] += statistics[statistic] + self.statistics["files"] += len(self.filenames) + + def _job_count(self) -> int: + # First we walk through all of our error cases: + # - multiprocessing library is not present + # - the user provided stdin and that's not something we can handle + # well + # - the user provided some awful input + + if utils.is_using_stdin(self.options.filenames): + LOG.warning( + "The --jobs option is not compatible with supplying " + "input using - . Ignoring --jobs arguments.", + ) + return 0 + + jobs = self.options.jobs + + # If the value is "auto", we want to let the multiprocessing library + # decide the number based on the number of CPUs. However, if that + # function is not implemented for this particular value of Python we + # default to 1 + if jobs.is_auto: + try: + return multiprocessing.cpu_count() + except NotImplementedError: + return 0 + + # Otherwise, we know jobs should be an integer and we can just convert + # it to an integer + return jobs.n_jobs + + def _handle_results(self, filename: str, results: Results) -> int: + style_guide = self.style_guide + reported_results_count = 0 + for error_code, line_number, column, text, physical_line in results: + reported_results_count += style_guide.handle_error( + code=error_code, + filename=filename, + line_number=line_number, + column_number=column, + text=text, + physical_line=physical_line, + ) + return reported_results_count + + def report(self) -> tuple[int, int]: + """Report all of the errors found in the managed file checkers. + + This iterates over each of the checkers and reports the errors sorted + by line number. + + :returns: + A tuple of the total results found and the results reported. + """ + results_reported = results_found = 0 + self.results.sort(key=operator.itemgetter(0)) + for filename, results, _ in self.results: + results.sort(key=operator.itemgetter(1, 2)) + with self.style_guide.processing_file(filename): + results_reported += self._handle_results(filename, results) + results_found += len(results) + return (results_found, results_reported) + + def run_parallel(self) -> None: + """Run the checkers in parallel.""" + with _mp_prefork(self.plugins, self.options): + pool = _try_initialize_processpool(self.jobs, self.argv) + + if pool is None: + self.run_serial() + return + + pool_closed = False + try: + self.results = list(pool.imap_unordered(_mp_run, self.filenames)) + pool.close() + pool.join() + pool_closed = True + finally: + if not pool_closed: + pool.terminate() + pool.join() + + def run_serial(self) -> None: + """Run the checkers in serial.""" + self.results = [ + FileChecker( + filename=filename, + plugins=self.plugins, + options=self.options, + ).run_checks() + for filename in self.filenames + ] + + def run(self) -> None: + """Run all the checkers. + + This will intelligently decide whether to run the checks in parallel + or whether to run them in serial. + + If running the checks in parallel causes a problem (e.g., + :issue:`117`) this also implements fallback to serial processing. + """ + try: + if self.jobs > 1 and len(self.filenames) > 1: + self.run_parallel() + else: + self.run_serial() + except KeyboardInterrupt: + LOG.warning("Flake8 was interrupted by the user") + raise exceptions.EarlyQuit("Early quit while running checks") + + def start(self) -> None: + """Start checking files. + + :param paths: + Path names to check. This is passed directly to + :meth:`~Manager.make_checkers`. + """ + LOG.info("Making checkers") + self.filenames = tuple( + expand_paths( + paths=self.options.filenames, + stdin_display_name=self.options.stdin_display_name, + filename_patterns=self.options.filename, + exclude=self.exclude, + ), + ) + self.jobs = min(len(self.filenames), self.jobs) + + def stop(self) -> None: + """Stop checking files.""" + self._process_statistics() + + +class FileChecker: + """Manage running checks for a file and aggregate the results.""" + + def __init__( + self, + *, + filename: str, + plugins: Checkers, + options: argparse.Namespace, + ) -> None: + """Initialize our file checker.""" + self.options = options + self.filename = filename + self.plugins = plugins + self.results: Results = [] + self.statistics = { + "tokens": 0, + "logical lines": 0, + "physical lines": 0, + } + self.processor = self._make_processor() + self.display_name = filename + self.should_process = False + if self.processor is not None: + self.display_name = self.processor.filename + self.should_process = not self.processor.should_ignore_file() + self.statistics["physical lines"] = len(self.processor.lines) + + def __repr__(self) -> str: + """Provide helpful debugging representation.""" + return f"FileChecker for {self.filename}" + + def _make_processor(self) -> processor.FileProcessor | None: + try: + return processor.FileProcessor(self.filename, self.options) + except OSError as e: + # If we can not read the file due to an IOError (e.g., the file + # does not exist or we do not have the permissions to open it) + # then we need to format that exception for the user. + # NOTE(sigmavirus24): Historically, pep8 has always reported this + # as an E902. We probably *want* a better error code for this + # going forward. + self.report("E902", 0, 0, f"{type(e).__name__}: {e}") + return None + + def report( + self, + error_code: str | None, + line_number: int, + column: int, + text: str, + ) -> str: + """Report an error by storing it in the results list.""" + if error_code is None: + error_code, text = text.split(" ", 1) + + # If we're recovering from a problem in _make_processor, we will not + # have this attribute. + if hasattr(self, "processor") and self.processor is not None: + line = self.processor.noqa_line_for(line_number) + else: + line = None + + self.results.append((error_code, line_number, column, text, line)) + return error_code + + def run_check(self, plugin: LoadedPlugin, **arguments: Any) -> Any: + """Run the check in a single plugin.""" + assert self.processor is not None, self.filename + try: + params = self.processor.keyword_arguments_for( + plugin.parameters, arguments, + ) + except AttributeError as ae: + raise exceptions.PluginRequestedUnknownParameters( + plugin_name=plugin.display_name, exception=ae, + ) + try: + return plugin.obj(**arguments, **params) + except Exception as all_exc: + LOG.critical( + "Plugin %s raised an unexpected exception", + plugin.display_name, + exc_info=True, + ) + raise exceptions.PluginExecutionFailed( + filename=self.filename, + plugin_name=plugin.display_name, + exception=all_exc, + ) + + @staticmethod + def _extract_syntax_information(exception: Exception) -> tuple[int, int]: + if ( + len(exception.args) > 1 + and exception.args[1] + and len(exception.args[1]) > 2 + ): + token = exception.args[1] + row, column = token[1:3] + elif ( + isinstance(exception, tokenize.TokenError) + and len(exception.args) == 2 + and len(exception.args[1]) == 2 + ): + token = () + row, column = exception.args[1] + else: + token = () + row, column = (1, 0) + + if ( + column > 0 + and token + and isinstance(exception, SyntaxError) + and len(token) == 4 # Python 3.9 or earlier + ): + # NOTE(sigmavirus24): SyntaxErrors report 1-indexed column + # numbers. We need to decrement the column number by 1 at + # least. + column_offset = 1 + row_offset = 0 + # See also: https://github.com/pycqa/flake8/issues/169, + # https://github.com/PyCQA/flake8/issues/1372 + # On Python 3.9 and earlier, token will be a 4-item tuple with the + # last item being the string. Starting with 3.10, they added to + # the tuple so now instead of it ending with the code that failed + # to parse, it ends with the end of the section of code that + # failed to parse. Luckily the absolute position in the tuple is + # stable across versions so we can use that here + physical_line = token[3] + + # NOTE(sigmavirus24): Not all "tokens" have a string as the last + # argument. In this event, let's skip trying to find the correct + # column and row values. + if physical_line is not None: + # NOTE(sigmavirus24): SyntaxErrors also don't exactly have a + # "physical" line so much as what was accumulated by the point + # tokenizing failed. + # See also: https://github.com/pycqa/flake8/issues/169 + lines = physical_line.rstrip("\n").split("\n") + row_offset = len(lines) - 1 + logical_line = lines[0] + logical_line_length = len(logical_line) + if column > logical_line_length: + column = logical_line_length + row -= row_offset + column -= column_offset + return row, column + + def run_ast_checks(self) -> None: + """Run all checks expecting an abstract syntax tree.""" + assert self.processor is not None, self.filename + ast = self.processor.build_ast() + + for plugin in self.plugins.tree: + checker = self.run_check(plugin, tree=ast) + # If the plugin uses a class, call the run method of it, otherwise + # the call should return something iterable itself + try: + runner = checker.run() + except AttributeError: + runner = checker + for line_number, offset, text, _ in runner: + self.report( + error_code=None, + line_number=line_number, + column=offset, + text=text, + ) + + def run_logical_checks(self) -> None: + """Run all checks expecting a logical line.""" + assert self.processor is not None + comments, logical_line, mapping = self.processor.build_logical_line() + if not mapping: + return + self.processor.update_state(mapping) + + LOG.debug('Logical line: "%s"', logical_line.rstrip()) + + for plugin in self.plugins.logical_line: + self.processor.update_checker_state_for(plugin) + results = self.run_check(plugin, logical_line=logical_line) or () + for offset, text in results: + line_number, column_offset = find_offset(offset, mapping) + if line_number == column_offset == 0: + LOG.warning("position of error out of bounds: %s", plugin) + self.report( + error_code=None, + line_number=line_number, + column=column_offset, + text=text, + ) + + self.processor.next_logical_line() + + def run_physical_checks(self, physical_line: str) -> None: + """Run all checks for a given physical line. + + A single physical check may return multiple errors. + """ + assert self.processor is not None + for plugin in self.plugins.physical_line: + self.processor.update_checker_state_for(plugin) + result = self.run_check(plugin, physical_line=physical_line) + + if result is not None: + # This is a single result if first element is an int + column_offset = None + try: + column_offset = result[0] + except (IndexError, TypeError): + pass + + if isinstance(column_offset, int): + # If we only have a single result, convert to a collection + result = (result,) + + for result_single in result: + column_offset, text = result_single + self.report( + error_code=None, + line_number=self.processor.line_number, + column=column_offset, + text=text, + ) + + def process_tokens(self) -> None: + """Process tokens and trigger checks. + + Instead of using this directly, you should use + :meth:`flake8.checker.FileChecker.run_checks`. + """ + assert self.processor is not None + parens = 0 + statistics = self.statistics + file_processor = self.processor + prev_physical = "" + for token in file_processor.generate_tokens(): + statistics["tokens"] += 1 + self.check_physical_eol(token, prev_physical) + token_type, text = token[0:2] + if token_type == tokenize.OP: + parens = processor.count_parentheses(parens, text) + elif parens == 0: + if processor.token_is_newline(token): + self.handle_newline(token_type) + prev_physical = token[4] + + if file_processor.tokens: + # If any tokens are left over, process them + self.run_physical_checks(file_processor.lines[-1]) + self.run_logical_checks() + + def run_checks(self) -> tuple[str, Results, dict[str, int]]: + """Run checks against the file.""" + if self.processor is None or not self.should_process: + return self.display_name, self.results, self.statistics + + try: + self.run_ast_checks() + self.process_tokens() + except (SyntaxError, tokenize.TokenError) as e: + code = "E902" if isinstance(e, tokenize.TokenError) else "E999" + row, column = self._extract_syntax_information(e) + self.report(code, row, column, f"{type(e).__name__}: {e.args[0]}") + return self.display_name, self.results, self.statistics + + logical_lines = self.processor.statistics["logical lines"] + self.statistics["logical lines"] = logical_lines + return self.display_name, self.results, self.statistics + + def handle_newline(self, token_type: int) -> None: + """Handle the logic when encountering a newline token.""" + assert self.processor is not None + if token_type == tokenize.NEWLINE: + self.run_logical_checks() + self.processor.reset_blank_before() + elif len(self.processor.tokens) == 1: + # The physical line contains only this token. + self.processor.visited_new_blank_line() + self.processor.delete_first_token() + else: + self.run_logical_checks() + + def check_physical_eol( + self, token: tokenize.TokenInfo, prev_physical: str, + ) -> None: + """Run physical checks if and only if it is at the end of the line.""" + assert self.processor is not None + if token.type == FSTRING_START: # pragma: >=3.12 cover + self.processor.fstring_start(token.start[0]) + elif token.type == TSTRING_START: # pragma: >=3.14 cover + self.processor.tstring_start(token.start[0]) + # a newline token ends a single physical line. + elif processor.is_eol_token(token): + # if the file does not end with a newline, the NEWLINE + # token is inserted by the parser, but it does not contain + # the previous physical line in `token[4]` + if token.line == "": + self.run_physical_checks(prev_physical) + else: + self.run_physical_checks(token.line) + elif processor.is_multiline_string(token): + # Less obviously, a string that contains newlines is a + # multiline string, either triple-quoted or with internal + # newlines backslash-escaped. Check every physical line in the + # string *except* for the last one: its newline is outside of + # the multiline string, so we consider it a regular physical + # line, and will check it like any other physical line. + # + # Subtleties: + # - have to wind self.line_number back because initially it + # points to the last line of the string, and we want + # check_physical() to give accurate feedback + for line in self.processor.multiline_string(token): + self.run_physical_checks(line) + + +def _try_initialize_processpool( + job_count: int, + argv: Sequence[str], +) -> multiprocessing.pool.Pool | None: + """Return a new process pool instance if we are able to create one.""" + try: + return multiprocessing.Pool(job_count, _mp_init, initargs=(argv,)) + except OSError as err: + if err.errno not in SERIAL_RETRY_ERRNOS: + raise + except ImportError: + pass + + return None + + +def find_offset( + offset: int, mapping: processor._LogicalMapping, +) -> tuple[int, int]: + """Find the offset tuple for a single offset.""" + if isinstance(offset, tuple): + return offset + + for token in mapping: + token_offset = token[0] + if offset <= token_offset: + position = token[1] + break + else: + position = (0, 0) + offset = token_offset = 0 + return (position[0], position[1] + offset - token_offset) diff --git a/src/flake8-main/flake8-main/src/flake8/defaults.py b/src/flake8-main/flake8-main/src/flake8/defaults.py new file mode 100644 index 0000000..57abda1 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/defaults.py @@ -0,0 +1,45 @@ +"""Constants that define defaults.""" +from __future__ import annotations + +import re + +EXCLUDE = ( + ".svn", + "CVS", + ".bzr", + ".hg", + ".git", + "__pycache__", + ".tox", + ".nox", + ".eggs", + "*.egg", +) +IGNORE = ("E121", "E123", "E126", "E226", "E24", "E704", "W503", "W504") +MAX_LINE_LENGTH = 79 +INDENT_SIZE = 4 + +# Other constants +WHITESPACE = frozenset(" \t") + +STATISTIC_NAMES = ("logical lines", "physical lines", "tokens") + +NOQA_INLINE_REGEXP = re.compile( + # We're looking for items that look like this: + # ``# noqa`` + # ``# noqa: E123`` + # ``# noqa: E123,W451,F921`` + # ``# noqa:E123,W451,F921`` + # ``# NoQA: E123,W451,F921`` + # ``# NOQA: E123,W451,F921`` + # ``# NOQA:E123,W451,F921`` + # We do not want to capture the ``: `` that follows ``noqa`` + # We do not care about the casing of ``noqa`` + # We want a comma-separated list of errors + r"# noqa(?::[\s]?(?P([A-Z]+[0-9]+(?:[,\s]+)?)+))?", + re.IGNORECASE, +) + +NOQA_FILE = re.compile(r"\s*# flake8[:=]\s*noqa", re.I) + +VALID_CODE_PREFIX = re.compile("^[A-Z]{1,3}[0-9]{0,3}$", re.ASCII) diff --git a/src/flake8-main/flake8-main/src/flake8/discover_files.py b/src/flake8-main/flake8-main/src/flake8/discover_files.py new file mode 100644 index 0000000..da28ba5 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/discover_files.py @@ -0,0 +1,89 @@ +"""Functions related to discovering paths.""" +from __future__ import annotations + +import logging +import os.path +from collections.abc import Generator +from collections.abc import Sequence +from typing import Callable + +from flake8 import utils + +LOG = logging.getLogger(__name__) + + +def _filenames_from( + arg: str, + *, + predicate: Callable[[str], bool], +) -> Generator[str]: + """Generate filenames from an argument. + + :param arg: + Parameter from the command-line. + :param predicate: + Predicate to use to filter out filenames. If the predicate + returns ``True`` we will exclude the filename, otherwise we + will yield it. By default, we include every filename + generated. + :returns: + Generator of paths + """ + if predicate(arg): + return + + if os.path.isdir(arg): + for root, sub_directories, files in os.walk(arg): + # NOTE(sigmavirus24): os.walk() will skip a directory if you + # remove it from the list of sub-directories. + for directory in tuple(sub_directories): + joined = os.path.join(root, directory) + if predicate(joined): + sub_directories.remove(directory) + + for filename in files: + joined = os.path.join(root, filename) + if not predicate(joined): + yield joined + else: + yield arg + + +def expand_paths( + *, + paths: Sequence[str], + stdin_display_name: str, + filename_patterns: Sequence[str], + exclude: Sequence[str], +) -> Generator[str]: + """Expand out ``paths`` from commandline to the lintable files.""" + if not paths: + paths = ["."] + + def is_excluded(arg: str) -> bool: + if arg == "-": + # if the stdin_display_name is the default, always include it + if stdin_display_name == "stdin": + return False + arg = stdin_display_name + + return utils.matches_filename( + arg, + patterns=exclude, + log_message='"%(path)s" has %(whether)sbeen excluded', + logger=LOG, + ) + + return ( + filename + for path in paths + for filename in _filenames_from(path, predicate=is_excluded) + if ( + # always lint `-` + filename == "-" + # always lint explicitly passed (even if not matching filter) + or path == filename + # otherwise, check the file against filtered patterns + or utils.fnmatch(filename, filename_patterns) + ) + ) diff --git a/src/flake8-main/flake8-main/src/flake8/exceptions.py b/src/flake8-main/flake8-main/src/flake8/exceptions.py new file mode 100644 index 0000000..18646e7 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/exceptions.py @@ -0,0 +1,78 @@ +"""Exception classes for all of Flake8.""" +from __future__ import annotations + + +class Flake8Exception(Exception): + """Plain Flake8 exception.""" + + +class EarlyQuit(Flake8Exception): + """Except raised when encountering a KeyboardInterrupt.""" + + +class ExecutionError(Flake8Exception): + """Exception raised during execution of Flake8.""" + + +class FailedToLoadPlugin(Flake8Exception): + """Exception raised when a plugin fails to load.""" + + FORMAT = 'Flake8 failed to load plugin "%(name)s" due to %(exc)s.' + + def __init__(self, plugin_name: str, exception: Exception) -> None: + """Initialize our FailedToLoadPlugin exception.""" + self.plugin_name = plugin_name + self.original_exception = exception + super().__init__(plugin_name, exception) + + def __str__(self) -> str: + """Format our exception message.""" + return self.FORMAT % { + "name": self.plugin_name, + "exc": self.original_exception, + } + + +class PluginRequestedUnknownParameters(Flake8Exception): + """The plugin requested unknown parameters.""" + + FORMAT = '"%(name)s" requested unknown parameters causing %(exc)s' + + def __init__(self, plugin_name: str, exception: Exception) -> None: + """Pop certain keyword arguments for initialization.""" + self.plugin_name = plugin_name + self.original_exception = exception + super().__init__(plugin_name, exception) + + def __str__(self) -> str: + """Format our exception message.""" + return self.FORMAT % { + "name": self.plugin_name, + "exc": self.original_exception, + } + + +class PluginExecutionFailed(Flake8Exception): + """The plugin failed during execution.""" + + FORMAT = '{fname}: "{plugin}" failed during execution due to {exc!r}' + + def __init__( + self, + filename: str, + plugin_name: str, + exception: Exception, + ) -> None: + """Utilize keyword arguments for message generation.""" + self.filename = filename + self.plugin_name = plugin_name + self.original_exception = exception + super().__init__(filename, plugin_name, exception) + + def __str__(self) -> str: + """Format our exception message.""" + return self.FORMAT.format( + fname=self.filename, + plugin=self.plugin_name, + exc=self.original_exception, + ) diff --git a/src/flake8-main/flake8-main/src/flake8/formatting/__init__.py b/src/flake8-main/flake8-main/src/flake8/formatting/__init__.py new file mode 100644 index 0000000..732d0b6 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/formatting/__init__.py @@ -0,0 +1,2 @@ +"""Submodule containing the default formatters for Flake8.""" +from __future__ import annotations diff --git a/src/flake8-main/flake8-main/src/flake8/formatting/_windows_color.py b/src/flake8-main/flake8-main/src/flake8/formatting/_windows_color.py new file mode 100644 index 0000000..a06fdb9 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/formatting/_windows_color.py @@ -0,0 +1,61 @@ +"""ctypes hackery to enable color processing on windows. + +See: https://github.com/pre-commit/pre-commit/blob/cb40e96/pre_commit/color.py +""" +from __future__ import annotations + +import sys + +if sys.platform == "win32": # pragma: no cover (windows) + + def _enable() -> None: + from ctypes import POINTER + from ctypes import windll + from ctypes import WinError + from ctypes import WINFUNCTYPE + from ctypes.wintypes import BOOL + from ctypes.wintypes import DWORD + from ctypes.wintypes import HANDLE + + STD_ERROR_HANDLE = -12 + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 + + def bool_errcheck(result, func, args): + if not result: + raise WinError() + return args + + GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)( + ("GetStdHandle", windll.kernel32), + ((1, "nStdHandle"),), + ) + + GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))( + ("GetConsoleMode", windll.kernel32), + ((1, "hConsoleHandle"), (2, "lpMode")), + ) + GetConsoleMode.errcheck = bool_errcheck + + SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)( + ("SetConsoleMode", windll.kernel32), + ((1, "hConsoleHandle"), (1, "dwMode")), + ) + SetConsoleMode.errcheck = bool_errcheck + + # As of Windows 10, the Windows console supports (some) ANSI escape + # sequences, but it needs to be enabled using `SetConsoleMode` first. + # + # More info on the escape sequences supported: + # https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx + stderr = GetStdHandle(STD_ERROR_HANDLE) + flags = GetConsoleMode(stderr) + SetConsoleMode(stderr, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + + try: + _enable() + except OSError: + terminal_supports_color = False + else: + terminal_supports_color = True +else: # pragma: win32 no cover + terminal_supports_color = True diff --git a/src/flake8-main/flake8-main/src/flake8/formatting/base.py b/src/flake8-main/flake8-main/src/flake8/formatting/base.py new file mode 100644 index 0000000..bbbfdff --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/formatting/base.py @@ -0,0 +1,202 @@ +"""The base class and interface for all formatting plugins.""" +from __future__ import annotations + +import argparse +import os +import sys +from typing import IO + +from flake8.formatting import _windows_color +from flake8.statistics import Statistics +from flake8.violation import Violation + + +class BaseFormatter: + """Class defining the formatter interface. + + .. attribute:: options + + The options parsed from both configuration files and the command-line. + + .. attribute:: filename + + If specified by the user, the path to store the results of the run. + + .. attribute:: output_fd + + Initialized when the :meth:`start` is called. This will be a file + object opened for writing. + + .. attribute:: newline + + The string to add to the end of a line. This is only used when the + output filename has been specified. + """ + + def __init__(self, options: argparse.Namespace) -> None: + """Initialize with the options parsed from config and cli. + + This also calls a hook, :meth:`after_init`, so subclasses do not need + to call super to call this method. + + :param options: + User specified configuration parsed from both configuration files + and the command-line interface. + """ + self.options = options + self.filename = options.output_file + self.output_fd: IO[str] | None = None + self.newline = "\n" + self.color = options.color == "always" or ( + options.color == "auto" + and sys.stdout.isatty() + and _windows_color.terminal_supports_color + ) + self.after_init() + + def after_init(self) -> None: + """Initialize the formatter further.""" + + def beginning(self, filename: str) -> None: + """Notify the formatter that we're starting to process a file. + + :param filename: + The name of the file that Flake8 is beginning to report results + from. + """ + + def finished(self, filename: str) -> None: + """Notify the formatter that we've finished processing a file. + + :param filename: + The name of the file that Flake8 has finished reporting results + from. + """ + + def start(self) -> None: + """Prepare the formatter to receive input. + + This defaults to initializing :attr:`output_fd` if :attr:`filename` + """ + if self.filename: + dirname = os.path.dirname(os.path.abspath(self.filename)) + os.makedirs(dirname, exist_ok=True) + self.output_fd = open(self.filename, "a") + + def handle(self, error: Violation) -> None: + """Handle an error reported by Flake8. + + This defaults to calling :meth:`format`, :meth:`show_source`, and + then :meth:`write`. To extend how errors are handled, override this + method. + + :param error: + This will be an instance of + :class:`~flake8.violation.Violation`. + """ + line = self.format(error) + source = self.show_source(error) + self.write(line, source) + + def format(self, error: Violation) -> str | None: + """Format an error reported by Flake8. + + This method **must** be implemented by subclasses. + + :param error: + This will be an instance of + :class:`~flake8.violation.Violation`. + :returns: + The formatted error string. + """ + raise NotImplementedError( + "Subclass of BaseFormatter did not implement" " format.", + ) + + def show_statistics(self, statistics: Statistics) -> None: + """Format and print the statistics.""" + for error_code in statistics.error_codes(): + stats_for_error_code = statistics.statistics_for(error_code) + statistic = next(stats_for_error_code) + count = statistic.count + count += sum(stat.count for stat in stats_for_error_code) + self._write(f"{count:<5} {error_code} {statistic.message}") + + def show_benchmarks(self, benchmarks: list[tuple[str, float]]) -> None: + """Format and print the benchmarks.""" + # NOTE(sigmavirus24): The format strings are a little confusing, even + # to me, so here's a quick explanation: + # We specify the named value first followed by a ':' to indicate we're + # formatting the value. + # Next we use '<' to indicate we want the value left aligned. + # Then '10' is the width of the area. + # For floats, finally, we only want only want at most 3 digits after + # the decimal point to be displayed. This is the precision and it + # can not be specified for integers which is why we need two separate + # format strings. + float_format = "{value:<10.3} {statistic}".format + int_format = "{value:<10} {statistic}".format + for statistic, value in benchmarks: + if isinstance(value, int): + benchmark = int_format(statistic=statistic, value=value) + else: + benchmark = float_format(statistic=statistic, value=value) + self._write(benchmark) + + def show_source(self, error: Violation) -> str | None: + """Show the physical line generating the error. + + This also adds an indicator for the particular part of the line that + is reported as generating the problem. + + :param error: + This will be an instance of + :class:`~flake8.violation.Violation`. + :returns: + The formatted error string if the user wants to show the source. + If the user does not want to show the source, this will return + ``None``. + """ + if not self.options.show_source or error.physical_line is None: + return "" + + # Because column numbers are 1-indexed, we need to remove one to get + # the proper number of space characters. + indent = "".join( + c if c.isspace() else " " + for c in error.physical_line[: error.column_number - 1] + ) + # Physical lines have a newline at the end, no need to add an extra + # one + return f"{error.physical_line}{indent}^" + + def _write(self, output: str) -> None: + """Handle logic of whether to use an output file or print().""" + if self.output_fd is not None: + self.output_fd.write(output + self.newline) + if self.output_fd is None or self.options.tee: + sys.stdout.buffer.write(output.encode() + self.newline.encode()) + + def write(self, line: str | None, source: str | None) -> None: + """Write the line either to the output file or stdout. + + This handles deciding whether to write to a file or print to standard + out for subclasses. Override this if you want behaviour that differs + from the default. + + :param line: + The formatted string to print or write. + :param source: + The source code that has been formatted and associated with the + line of output. + """ + if line: + self._write(line) + if source: + self._write(source) + + def stop(self) -> None: + """Clean up after reporting is finished.""" + if self.output_fd is not None: + self.output_fd.close() + self.output_fd = None diff --git a/src/flake8-main/flake8-main/src/flake8/formatting/default.py b/src/flake8-main/flake8-main/src/flake8/formatting/default.py new file mode 100644 index 0000000..b5d08ff --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/formatting/default.py @@ -0,0 +1,109 @@ +"""Default formatting class for Flake8.""" +from __future__ import annotations + +from flake8.formatting import base +from flake8.violation import Violation + +COLORS = { + "bold": "\033[1m", + "black": "\033[30m", + "red": "\033[31m", + "green": "\033[32m", + "yellow": "\033[33m", + "blue": "\033[34m", + "magenta": "\033[35m", + "cyan": "\033[36m", + "white": "\033[37m", + "reset": "\033[m", +} +COLORS_OFF = {k: "" for k in COLORS} + + +class SimpleFormatter(base.BaseFormatter): + """Simple abstraction for Default and Pylint formatter commonality. + + Sub-classes of this need to define an ``error_format`` attribute in order + to succeed. The ``format`` method relies on that attribute and expects the + ``error_format`` string to use the old-style formatting strings with named + parameters: + + * code + * text + * path + * row + * col + + """ + + error_format: str + + def format(self, error: Violation) -> str | None: + """Format and write error out. + + If an output filename is specified, write formatted errors to that + file. Otherwise, print the formatted error to standard out. + """ + return self.error_format % { + "code": error.code, + "text": error.text, + "path": error.filename, + "row": error.line_number, + "col": error.column_number, + **(COLORS if self.color else COLORS_OFF), + } + + +class Default(SimpleFormatter): + """Default formatter for Flake8. + + This also handles backwards compatibility for people specifying a custom + format string. + """ + + error_format = ( + "%(bold)s%(path)s%(reset)s" + "%(cyan)s:%(reset)s%(row)d%(cyan)s:%(reset)s%(col)d%(cyan)s:%(reset)s " + "%(bold)s%(red)s%(code)s%(reset)s %(text)s" + ) + + def after_init(self) -> None: + """Check for a custom format string.""" + if self.options.format.lower() != "default": + self.error_format = self.options.format + + +class Pylint(SimpleFormatter): + """Pylint formatter for Flake8.""" + + error_format = "%(path)s:%(row)d: [%(code)s] %(text)s" + + +class FilenameOnly(SimpleFormatter): + """Only print filenames, e.g., flake8 -q.""" + + error_format = "%(path)s" + + def after_init(self) -> None: + """Initialize our set of filenames.""" + self.filenames_already_printed: set[str] = set() + + def show_source(self, error: Violation) -> str | None: + """Do not include the source code.""" + + def format(self, error: Violation) -> str | None: + """Ensure we only print each error once.""" + if error.filename not in self.filenames_already_printed: + self.filenames_already_printed.add(error.filename) + return super().format(error) + else: + return None + + +class Nothing(base.BaseFormatter): + """Print absolutely nothing.""" + + def format(self, error: Violation) -> str | None: + """Do nothing.""" + + def show_source(self, error: Violation) -> str | None: + """Do not print the source.""" diff --git a/src/flake8-main/flake8-main/src/flake8/main/__init__.py b/src/flake8-main/flake8-main/src/flake8/main/__init__.py new file mode 100644 index 0000000..85bcff4 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/main/__init__.py @@ -0,0 +1,2 @@ +"""Module containing the logic for the Flake8 entry-points.""" +from __future__ import annotations diff --git a/src/flake8-main/flake8-main/src/flake8/main/application.py b/src/flake8-main/flake8-main/src/flake8/main/application.py new file mode 100644 index 0000000..165a6ef --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/main/application.py @@ -0,0 +1,215 @@ +"""Module containing the application logic for Flake8.""" +from __future__ import annotations + +import argparse +import json +import logging +import time +from collections.abc import Sequence + +import flake8 +from flake8 import checker +from flake8 import defaults +from flake8 import exceptions +from flake8 import style_guide +from flake8.formatting.base import BaseFormatter +from flake8.main import debug +from flake8.options.parse_args import parse_args +from flake8.plugins import finder +from flake8.plugins import reporter + + +LOG = logging.getLogger(__name__) + + +class Application: + """Abstract our application into a class.""" + + def __init__(self) -> None: + """Initialize our application.""" + #: The timestamp when the Application instance was instantiated. + self.start_time = time.time() + #: The timestamp when the Application finished reported errors. + self.end_time: float | None = None + + self.plugins: finder.Plugins | None = None + #: The user-selected formatter from :attr:`formatting_plugins` + self.formatter: BaseFormatter | None = None + #: The :class:`flake8.style_guide.StyleGuideManager` built from the + #: user's options + self.guide: style_guide.StyleGuideManager | None = None + #: The :class:`flake8.checker.Manager` that will handle running all of + #: the checks selected by the user. + self.file_checker_manager: checker.Manager | None = None + + #: The user-supplied options parsed into an instance of + #: :class:`argparse.Namespace` + self.options: argparse.Namespace | None = None + #: The number of errors, warnings, and other messages after running + #: flake8 and taking into account ignored errors and lines. + self.result_count = 0 + #: The total number of errors before accounting for ignored errors and + #: lines. + self.total_result_count = 0 + #: Whether or not something catastrophic happened and we should exit + #: with a non-zero status code + self.catastrophic_failure = False + + def exit_code(self) -> int: + """Return the program exit code.""" + if self.catastrophic_failure: + return 1 + assert self.options is not None + if self.options.exit_zero: + return 0 + else: + return int(self.result_count > 0) + + def make_formatter(self) -> None: + """Initialize a formatter based on the parsed options.""" + assert self.plugins is not None + assert self.options is not None + self.formatter = reporter.make(self.plugins.reporters, self.options) + + def make_guide(self) -> None: + """Initialize our StyleGuide.""" + assert self.formatter is not None + assert self.options is not None + self.guide = style_guide.StyleGuideManager( + self.options, self.formatter, + ) + + def make_file_checker_manager(self, argv: Sequence[str]) -> None: + """Initialize our FileChecker Manager.""" + assert self.guide is not None + assert self.plugins is not None + self.file_checker_manager = checker.Manager( + style_guide=self.guide, + plugins=self.plugins.checkers, + argv=argv, + ) + + def run_checks(self) -> None: + """Run the actual checks with the FileChecker Manager. + + This method encapsulates the logic to make a + :class:`~flake8.checker.Manger` instance run the checks it is + managing. + """ + assert self.file_checker_manager is not None + + self.file_checker_manager.start() + try: + self.file_checker_manager.run() + except exceptions.PluginExecutionFailed as plugin_failed: + print(str(plugin_failed)) + print("Run flake8 with greater verbosity to see more details") + self.catastrophic_failure = True + LOG.info("Finished running") + self.file_checker_manager.stop() + self.end_time = time.time() + + def report_benchmarks(self) -> None: + """Aggregate, calculate, and report benchmarks for this run.""" + assert self.options is not None + if not self.options.benchmark: + return + + assert self.file_checker_manager is not None + assert self.end_time is not None + time_elapsed = self.end_time - self.start_time + statistics = [("seconds elapsed", time_elapsed)] + add_statistic = statistics.append + for statistic in defaults.STATISTIC_NAMES + ("files",): + value = self.file_checker_manager.statistics[statistic] + total_description = f"total {statistic} processed" + add_statistic((total_description, value)) + per_second_description = f"{statistic} processed per second" + add_statistic((per_second_description, int(value / time_elapsed))) + + assert self.formatter is not None + self.formatter.show_benchmarks(statistics) + + def report_errors(self) -> None: + """Report all the errors found by flake8 3.0. + + This also updates the :attr:`result_count` attribute with the total + number of errors, warnings, and other messages found. + """ + LOG.info("Reporting errors") + assert self.file_checker_manager is not None + results = self.file_checker_manager.report() + self.total_result_count, self.result_count = results + LOG.info( + "Found a total of %d violations and reported %d", + self.total_result_count, + self.result_count, + ) + + def report_statistics(self) -> None: + """Aggregate and report statistics from this run.""" + assert self.options is not None + if not self.options.statistics: + return + + assert self.formatter is not None + assert self.guide is not None + self.formatter.show_statistics(self.guide.stats) + + def initialize(self, argv: Sequence[str]) -> None: + """Initialize the application to be run. + + This finds the plugins, registers their options, and parses the + command-line arguments. + """ + self.plugins, self.options = parse_args(argv) + + if self.options.bug_report: + info = debug.information(flake8.__version__, self.plugins) + print(json.dumps(info, indent=2, sort_keys=True)) + raise SystemExit(0) + + self.make_formatter() + self.make_guide() + self.make_file_checker_manager(argv) + + def report(self) -> None: + """Report errors, statistics, and benchmarks.""" + assert self.formatter is not None + self.formatter.start() + self.report_errors() + self.report_statistics() + self.report_benchmarks() + self.formatter.stop() + + def _run(self, argv: Sequence[str]) -> None: + self.initialize(argv) + self.run_checks() + self.report() + + def run(self, argv: Sequence[str]) -> None: + """Run our application. + + This method will also handle KeyboardInterrupt exceptions for the + entirety of the flake8 application. If it sees a KeyboardInterrupt it + will forcibly clean up the :class:`~flake8.checker.Manager`. + """ + try: + self._run(argv) + except KeyboardInterrupt as exc: + print("... stopped") + LOG.critical("Caught keyboard interrupt from user") + LOG.exception(exc) + self.catastrophic_failure = True + except exceptions.ExecutionError as exc: + print("There was a critical error during execution of Flake8:") + print(exc) + LOG.exception(exc) + self.catastrophic_failure = True + except exceptions.EarlyQuit: + self.catastrophic_failure = True + print("... stopped while processing files") + else: + assert self.options is not None + if self.options.count: + print(self.result_count) diff --git a/src/flake8-main/flake8-main/src/flake8/main/cli.py b/src/flake8-main/flake8-main/src/flake8/main/cli.py new file mode 100644 index 0000000..1a52f36 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/main/cli.py @@ -0,0 +1,24 @@ +"""Command-line implementation of flake8.""" +from __future__ import annotations + +import sys +from collections.abc import Sequence + +from flake8.main import application + + +def main(argv: Sequence[str] | None = None) -> int: + """Execute the main bit of the application. + + This handles the creation of an instance of :class:`Application`, runs it, + and then exits the application. + + :param argv: + The arguments to be passed to the application for parsing. + """ + if argv is None: + argv = sys.argv[1:] + + app = application.Application() + app.run(argv) + return app.exit_code() diff --git a/src/flake8-main/flake8-main/src/flake8/main/debug.py b/src/flake8-main/flake8-main/src/flake8/main/debug.py new file mode 100644 index 0000000..73ca74b --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/main/debug.py @@ -0,0 +1,30 @@ +"""Module containing the logic for our debugging logic.""" +from __future__ import annotations + +import platform +from typing import Any + +from flake8.plugins.finder import Plugins + + +def information(version: str, plugins: Plugins) -> dict[str, Any]: + """Generate the information to be printed for the bug report.""" + versions = sorted( + { + (loaded.plugin.package, loaded.plugin.version) + for loaded in plugins.all_plugins() + if loaded.plugin.package not in {"flake8", "local"} + }, + ) + return { + "version": version, + "plugins": [ + {"plugin": plugin, "version": version} + for plugin, version in versions + ], + "platform": { + "python_implementation": platform.python_implementation(), + "python_version": platform.python_version(), + "system": platform.system(), + }, + } diff --git a/src/flake8-main/flake8-main/src/flake8/main/options.py b/src/flake8-main/flake8-main/src/flake8/main/options.py new file mode 100644 index 0000000..e8cbe09 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/main/options.py @@ -0,0 +1,396 @@ +"""Contains the logic for all of the default options for Flake8.""" +from __future__ import annotations + +import argparse + +from flake8 import defaults +from flake8.options.manager import OptionManager + + +def stage1_arg_parser() -> argparse.ArgumentParser: + """Register the preliminary options on our OptionManager. + + The preliminary options include: + + - ``-v``/``--verbose`` + - ``--output-file`` + - ``--append-config`` + - ``--config`` + - ``--isolated`` + - ``--enable-extensions`` + """ + parser = argparse.ArgumentParser(add_help=False) + + parser.add_argument( + "-v", + "--verbose", + default=0, + action="count", + help="Print more information about what is happening in flake8. " + "This option is repeatable and will increase verbosity each " + "time it is repeated.", + ) + + parser.add_argument( + "--output-file", default=None, help="Redirect report to a file.", + ) + + # Config file options + + parser.add_argument( + "--append-config", + action="append", + default=[], + help="Provide extra config files to parse in addition to the files " + "found by Flake8 by default. These files are the last ones read " + "and so they take the highest precedence when multiple files " + "provide the same option.", + ) + + parser.add_argument( + "--config", + default=None, + help="Path to the config file that will be the authoritative config " + "source. This will cause Flake8 to ignore all other " + "configuration files.", + ) + + parser.add_argument( + "--isolated", + default=False, + action="store_true", + help="Ignore all configuration files.", + ) + + # Plugin enablement options + + parser.add_argument( + "--enable-extensions", + help="Enable plugins and extensions that are otherwise disabled " + "by default", + ) + + parser.add_argument( + "--require-plugins", + help="Require specific plugins to be installed before running", + ) + + return parser + + +class JobsArgument: + """Type callback for the --jobs argument.""" + + def __init__(self, arg: str) -> None: + """Parse and validate the --jobs argument. + + :param arg: The argument passed by argparse for validation + """ + self.is_auto = False + self.n_jobs = -1 + if arg == "auto": + self.is_auto = True + elif arg.isdigit(): + self.n_jobs = int(arg) + else: + raise argparse.ArgumentTypeError( + f"{arg!r} must be 'auto' or an integer.", + ) + + def __repr__(self) -> str: + """Representation for debugging.""" + return f"{type(self).__name__}({str(self)!r})" + + def __str__(self) -> str: + """Format our JobsArgument class.""" + return "auto" if self.is_auto else str(self.n_jobs) + + +def register_default_options(option_manager: OptionManager) -> None: + """Register the default options on our OptionManager. + + The default options include: + + - ``-q``/``--quiet`` + - ``--color`` + - ``--count`` + - ``--exclude`` + - ``--extend-exclude`` + - ``--filename`` + - ``--format`` + - ``--hang-closing`` + - ``--ignore`` + - ``--extend-ignore`` + - ``--per-file-ignores`` + - ``--max-line-length`` + - ``--max-doc-length`` + - ``--indent-size`` + - ``--select`` + - ``--extend-select`` + - ``--disable-noqa`` + - ``--show-source`` + - ``--statistics`` + - ``--exit-zero`` + - ``-j``/``--jobs`` + - ``--tee`` + - ``--benchmark`` + - ``--bug-report`` + """ + add_option = option_manager.add_option + + add_option( + "-q", + "--quiet", + default=0, + action="count", + parse_from_config=True, + help="Report only file names, or nothing. This option is repeatable.", + ) + + add_option( + "--color", + choices=("auto", "always", "never"), + default="auto", + help="Whether to use color in output. Defaults to `%(default)s`.", + ) + + add_option( + "--count", + action="store_true", + parse_from_config=True, + help="Print total number of errors to standard output after " + "all other output.", + ) + + add_option( + "--exclude", + metavar="patterns", + default=",".join(defaults.EXCLUDE), + comma_separated_list=True, + parse_from_config=True, + normalize_paths=True, + help="Comma-separated list of files or directories to exclude. " + "(Default: %(default)s)", + ) + + add_option( + "--extend-exclude", + metavar="patterns", + default="", + parse_from_config=True, + comma_separated_list=True, + normalize_paths=True, + help="Comma-separated list of files or directories to add to the list " + "of excluded ones.", + ) + + add_option( + "--filename", + metavar="patterns", + default="*.py", + parse_from_config=True, + comma_separated_list=True, + help="Only check for filenames matching the patterns in this comma-" + "separated list. (Default: %(default)s)", + ) + + add_option( + "--stdin-display-name", + default="stdin", + help="The name used when reporting errors from code passed via stdin. " + "This is useful for editors piping the file contents to flake8. " + "(Default: %(default)s)", + ) + + # TODO(sigmavirus24): Figure out --first/--repeat + + # NOTE(sigmavirus24): We can't use choices for this option since users can + # freely provide a format string and that will break if we restrict their + # choices. + add_option( + "--format", + metavar="format", + default="default", + parse_from_config=True, + help=( + f"Format errors according to the chosen formatter " + f"({', '.join(sorted(option_manager.formatter_names))}) " + f"or a format string containing %%-style " + f"mapping keys (code, col, path, row, text). " + f"For example, " + f"``--format=pylint`` or ``--format='%%(path)s %%(code)s'``. " + f"(Default: %(default)s)" + ), + ) + + add_option( + "--hang-closing", + action="store_true", + parse_from_config=True, + help="Hang closing bracket instead of matching indentation of opening " + "bracket's line.", + ) + + add_option( + "--ignore", + metavar="errors", + parse_from_config=True, + comma_separated_list=True, + help=( + f"Comma-separated list of error codes to ignore (or skip). " + f"For example, ``--ignore=E4,E51,W234``. " + f"(Default: {','.join(defaults.IGNORE)})" + ), + ) + + add_option( + "--extend-ignore", + metavar="errors", + parse_from_config=True, + comma_separated_list=True, + help="Comma-separated list of error codes to add to the list of " + "ignored ones. For example, ``--extend-ignore=E4,E51,W234``.", + ) + + add_option( + "--per-file-ignores", + default="", + parse_from_config=True, + help="A pairing of filenames and violation codes that defines which " + "violations to ignore in a particular file. The filenames can be " + "specified in a manner similar to the ``--exclude`` option and the " + "violations work similarly to the ``--ignore`` and ``--select`` " + "options.", + ) + + add_option( + "--max-line-length", + type=int, + metavar="n", + default=defaults.MAX_LINE_LENGTH, + parse_from_config=True, + help="Maximum allowed line length for the entirety of this run. " + "(Default: %(default)s)", + ) + + add_option( + "--max-doc-length", + type=int, + metavar="n", + default=None, + parse_from_config=True, + help="Maximum allowed doc line length for the entirety of this run. " + "(Default: %(default)s)", + ) + add_option( + "--indent-size", + type=int, + metavar="n", + default=defaults.INDENT_SIZE, + parse_from_config=True, + help="Number of spaces used for indentation (Default: %(default)s)", + ) + + add_option( + "--select", + metavar="errors", + parse_from_config=True, + comma_separated_list=True, + help=( + "Limit the reported error codes to codes prefix-matched by this " + "list. " + "You usually do not need to specify this option as the default " + "includes all installed plugin codes. " + "For example, ``--select=E4,E51,W234``." + ), + ) + + add_option( + "--extend-select", + metavar="errors", + parse_from_config=True, + comma_separated_list=True, + help=( + "Add additional error codes to the default ``--select``. " + "You usually do not need to specify this option as the default " + "includes all installed plugin codes. " + "For example, ``--extend-select=E4,E51,W234``." + ), + ) + + add_option( + "--disable-noqa", + default=False, + parse_from_config=True, + action="store_true", + help='Disable the effect of "# noqa". This will report errors on ' + 'lines with "# noqa" at the end.', + ) + + # TODO(sigmavirus24): Decide what to do about --show-pep8 + + add_option( + "--show-source", + action="store_true", + parse_from_config=True, + help="Show the source generate each error or warning.", + ) + add_option( + "--no-show-source", + action="store_false", + dest="show_source", + parse_from_config=False, + help="Negate --show-source", + ) + + add_option( + "--statistics", + action="store_true", + parse_from_config=True, + help="Count errors.", + ) + + # Flake8 options + + add_option( + "--exit-zero", + action="store_true", + help='Exit with status code "0" even if there are errors.', + ) + + add_option( + "-j", + "--jobs", + default="auto", + parse_from_config=True, + type=JobsArgument, + help="Number of subprocesses to use to run checks in parallel. " + 'This is ignored on Windows. The default, "auto", will ' + "auto-detect the number of processors available to use. " + "(Default: %(default)s)", + ) + + add_option( + "--tee", + default=False, + parse_from_config=True, + action="store_true", + help="Write to stdout and output-file.", + ) + + # Benchmarking + + add_option( + "--benchmark", + default=False, + action="store_true", + help="Print benchmark information about this run of Flake8", + ) + + # Debugging + + add_option( + "--bug-report", + action="store_true", + help="Print information necessary when preparing a bug report", + ) diff --git a/src/flake8-main/flake8-main/src/flake8/options/__init__.py b/src/flake8-main/flake8-main/src/flake8/options/__init__.py new file mode 100644 index 0000000..3578223 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/options/__init__.py @@ -0,0 +1,13 @@ +"""Package containing the option manager and config management logic. + +- :mod:`flake8.options.config` contains the logic for finding, parsing, and + merging configuration files. + +- :mod:`flake8.options.manager` contains the logic for managing customized + Flake8 command-line and configuration options. + +- :mod:`flake8.options.aggregator` uses objects from both of the above modules + to aggregate configuration into one object used by plugins and Flake8. + +""" +from __future__ import annotations diff --git a/src/flake8-main/flake8-main/src/flake8/options/aggregator.py b/src/flake8-main/flake8-main/src/flake8/options/aggregator.py new file mode 100644 index 0000000..999161a --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/options/aggregator.py @@ -0,0 +1,56 @@ +"""Aggregation function for CLI specified options and config file options. + +This holds the logic that uses the collected and merged config files and +applies the user-specified command-line configuration on top of it. +""" +from __future__ import annotations + +import argparse +import configparser +import logging +from collections.abc import Sequence + +from flake8.options import config +from flake8.options.manager import OptionManager + +LOG = logging.getLogger(__name__) + + +def aggregate_options( + manager: OptionManager, + cfg: configparser.RawConfigParser, + cfg_dir: str, + argv: Sequence[str] | None, +) -> argparse.Namespace: + """Aggregate and merge CLI and config file options.""" + # Get defaults from the option parser + default_values = manager.parse_args([]) + + # Get the parsed config + parsed_config = config.parse_config(manager, cfg, cfg_dir) + + # store the plugin-set extended default ignore / select + default_values.extended_default_ignore = manager.extended_default_ignore + default_values.extended_default_select = manager.extended_default_select + + # Merge values parsed from config onto the default values returned + for config_name, value in parsed_config.items(): + dest_name = config_name + # If the config name is somehow different from the destination name, + # fetch the destination name from our Option + if not hasattr(default_values, config_name): + dest_val = manager.config_options_dict[config_name].dest + assert isinstance(dest_val, str) + dest_name = dest_val + + LOG.debug( + 'Overriding default value of (%s) for "%s" with (%s)', + getattr(default_values, dest_name, None), + dest_name, + value, + ) + # Override the default values with the config values + setattr(default_values, dest_name, value) + + # Finally parse the command-line options + return manager.parse_args(argv, default_values) diff --git a/src/flake8-main/flake8-main/src/flake8/options/config.py b/src/flake8-main/flake8-main/src/flake8/options/config.py new file mode 100644 index 0000000..fddee55 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/options/config.py @@ -0,0 +1,140 @@ +"""Config handling logic for Flake8.""" +from __future__ import annotations + +import configparser +import logging +import os.path +from typing import Any + +from flake8 import exceptions +from flake8.defaults import VALID_CODE_PREFIX +from flake8.options.manager import OptionManager + +LOG = logging.getLogger(__name__) + + +def _stat_key(s: str) -> tuple[int, int]: + # same as what's used by samefile / samestat + st = os.stat(s) + return st.st_ino, st.st_dev + + +def _find_config_file(path: str) -> str | None: + # on windows if the homedir isn't detected this returns back `~` + home = os.path.expanduser("~") + try: + home_stat = _stat_key(home) if home != "~" else None + except OSError: # FileNotFoundError / PermissionError / etc. + home_stat = None + + dir_stat = _stat_key(path) + while True: + for candidate in ("setup.cfg", "tox.ini", ".flake8"): + cfg = configparser.RawConfigParser() + cfg_path = os.path.join(path, candidate) + try: + cfg.read(cfg_path, encoding="UTF-8") + except (UnicodeDecodeError, configparser.ParsingError) as e: + LOG.warning("ignoring unparseable config %s: %s", cfg_path, e) + else: + # only consider it a config if it contains flake8 sections + if "flake8" in cfg or "flake8:local-plugins" in cfg: + return cfg_path + + new_path = os.path.dirname(path) + new_dir_stat = _stat_key(new_path) + if new_dir_stat == dir_stat or new_dir_stat == home_stat: + break + else: + path = new_path + dir_stat = new_dir_stat + + # did not find any configuration file + return None + + +def load_config( + config: str | None, + extra: list[str], + *, + isolated: bool = False, +) -> tuple[configparser.RawConfigParser, str]: + """Load the configuration given the user options. + + - in ``isolated`` mode, return an empty configuration + - if a config file is given in ``config`` use that, otherwise attempt to + discover a configuration using ``tox.ini`` / ``setup.cfg`` / ``.flake8`` + - finally, load any ``extra`` configuration files + """ + pwd = os.path.abspath(".") + + if isolated: + return configparser.RawConfigParser(), pwd + + if config is None: + config = _find_config_file(pwd) + + cfg = configparser.RawConfigParser() + if config is not None: + if not cfg.read(config, encoding="UTF-8"): + raise exceptions.ExecutionError( + f"The specified config file does not exist: {config}", + ) + cfg_dir = os.path.dirname(config) + else: + cfg_dir = pwd + + # TODO: remove this and replace it with configuration modifying plugins + # read the additional configs afterwards + for filename in extra: + if not cfg.read(filename, encoding="UTF-8"): + raise exceptions.ExecutionError( + f"The specified config file does not exist: {filename}", + ) + + return cfg, cfg_dir + + +def parse_config( + option_manager: OptionManager, + cfg: configparser.RawConfigParser, + cfg_dir: str, +) -> dict[str, Any]: + """Parse and normalize the typed configuration options.""" + if "flake8" not in cfg: + return {} + + config_dict = {} + + for option_name in cfg["flake8"]: + option = option_manager.config_options_dict.get(option_name) + if option is None: + LOG.debug('Option "%s" is not registered. Ignoring.', option_name) + continue + + # Use the appropriate method to parse the config value + value: Any + if option.type is int or option.action == "count": + value = cfg.getint("flake8", option_name) + elif option.action in {"store_true", "store_false"}: + value = cfg.getboolean("flake8", option_name) + else: + value = cfg.get("flake8", option_name) + + LOG.debug('Option "%s" returned value: %r', option_name, value) + + final_value = option.normalize(value, cfg_dir) + + if option_name in {"ignore", "extend-ignore"}: + for error_code in final_value: + if not VALID_CODE_PREFIX.match(error_code): + raise ValueError( + f"Error code {error_code!r} " + f"supplied to {option_name!r} option " + f"does not match {VALID_CODE_PREFIX.pattern!r}", + ) + + assert option.config_name is not None + config_dict[option.config_name] = final_value + + return config_dict diff --git a/src/flake8-main/flake8-main/src/flake8/options/manager.py b/src/flake8-main/flake8-main/src/flake8/options/manager.py new file mode 100644 index 0000000..addd3ec --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/options/manager.py @@ -0,0 +1,320 @@ +"""Option handling and Option management logic.""" +from __future__ import annotations + +import argparse +import enum +import functools +import logging +from collections.abc import Sequence +from typing import Any +from typing import Callable + +from flake8 import utils +from flake8.plugins.finder import Plugins + +LOG = logging.getLogger(__name__) + +# represent a singleton of "not passed arguments". +# an enum is chosen to trick mypy +_ARG = enum.Enum("_ARG", "NO") + + +def _flake8_normalize( + value: str, + *args: str, + comma_separated_list: bool = False, + normalize_paths: bool = False, +) -> str | list[str]: + ret: str | list[str] = value + if comma_separated_list and isinstance(ret, str): + ret = utils.parse_comma_separated_list(value) + + if normalize_paths: + if isinstance(ret, str): + ret = utils.normalize_path(ret, *args) + else: + ret = utils.normalize_paths(ret, *args) + + return ret + + +class Option: + """Our wrapper around an argparse argument parsers to add features.""" + + def __init__( + self, + short_option_name: str | _ARG = _ARG.NO, + long_option_name: str | _ARG = _ARG.NO, + # Options below are taken from argparse.ArgumentParser.add_argument + action: str | type[argparse.Action] | _ARG = _ARG.NO, + default: Any | _ARG = _ARG.NO, + type: Callable[..., Any] | _ARG = _ARG.NO, + dest: str | _ARG = _ARG.NO, + nargs: int | str | _ARG = _ARG.NO, + const: Any | _ARG = _ARG.NO, + choices: Sequence[Any] | _ARG = _ARG.NO, + help: str | _ARG = _ARG.NO, + metavar: str | _ARG = _ARG.NO, + required: bool | _ARG = _ARG.NO, + # Options below here are specific to Flake8 + parse_from_config: bool = False, + comma_separated_list: bool = False, + normalize_paths: bool = False, + ) -> None: + """Initialize an Option instance. + + The following are all passed directly through to argparse. + + :param short_option_name: + The short name of the option (e.g., ``-x``). This will be the + first argument passed to ``ArgumentParser.add_argument`` + :param long_option_name: + The long name of the option (e.g., ``--xtra-long-option``). This + will be the second argument passed to + ``ArgumentParser.add_argument`` + :param default: + Default value of the option. + :param dest: + Attribute name to store parsed option value as. + :param nargs: + Number of arguments to parse for this option. + :param const: + Constant value to store on a common destination. Usually used in + conjunction with ``action="store_const"``. + :param choices: + Possible values for the option. + :param help: + Help text displayed in the usage information. + :param metavar: + Name to use instead of the long option name for help text. + :param required: + Whether this option is required or not. + + The following options may be passed directly through to :mod:`argparse` + but may need some massaging. + + :param type: + A callable to normalize the type (as is the case in + :mod:`argparse`). + :param action: + Any action allowed by :mod:`argparse`. + + The following parameters are for Flake8's option handling alone. + + :param parse_from_config: + Whether or not this option should be parsed out of config files. + :param comma_separated_list: + Whether the option is a comma separated list when parsing from a + config file. + :param normalize_paths: + Whether the option is expecting a path or list of paths and should + attempt to normalize the paths to absolute paths. + """ + if ( + long_option_name is _ARG.NO + and short_option_name is not _ARG.NO + and short_option_name.startswith("--") + ): + short_option_name, long_option_name = _ARG.NO, short_option_name + + # flake8 special type normalization + if comma_separated_list or normalize_paths: + type = functools.partial( + _flake8_normalize, + comma_separated_list=comma_separated_list, + normalize_paths=normalize_paths, + ) + + self.short_option_name = short_option_name + self.long_option_name = long_option_name + self.option_args = [ + x + for x in (short_option_name, long_option_name) + if x is not _ARG.NO + ] + self.action = action + self.default = default + self.type = type + self.dest = dest + self.nargs = nargs + self.const = const + self.choices = choices + self.help = help + self.metavar = metavar + self.required = required + self.option_kwargs: dict[str, Any | _ARG] = { + "action": self.action, + "default": self.default, + "type": self.type, + "dest": self.dest, + "nargs": self.nargs, + "const": self.const, + "choices": self.choices, + "help": self.help, + "metavar": self.metavar, + "required": self.required, + } + + # Set our custom attributes + self.parse_from_config = parse_from_config + self.comma_separated_list = comma_separated_list + self.normalize_paths = normalize_paths + + self.config_name: str | None = None + if parse_from_config: + if long_option_name is _ARG.NO: + raise ValueError( + "When specifying parse_from_config=True, " + "a long_option_name must also be specified.", + ) + self.config_name = long_option_name[2:].replace("-", "_") + + self._opt = None + + @property + def filtered_option_kwargs(self) -> dict[str, Any]: + """Return any actually-specified arguments.""" + return { + k: v for k, v in self.option_kwargs.items() if v is not _ARG.NO + } + + def __repr__(self) -> str: # noqa: D105 + parts = [] + for arg in self.option_args: + parts.append(arg) + for k, v in self.filtered_option_kwargs.items(): + parts.append(f"{k}={v!r}") + return f"Option({', '.join(parts)})" + + def normalize(self, value: Any, *normalize_args: str) -> Any: + """Normalize the value based on the option configuration.""" + if self.comma_separated_list and isinstance(value, str): + value = utils.parse_comma_separated_list(value) + + if self.normalize_paths: + if isinstance(value, list): + value = utils.normalize_paths(value, *normalize_args) + else: + value = utils.normalize_path(value, *normalize_args) + + return value + + def to_argparse(self) -> tuple[list[str], dict[str, Any]]: + """Convert a Flake8 Option to argparse ``add_argument`` arguments.""" + return self.option_args, self.filtered_option_kwargs + + +class OptionManager: + """Manage Options and OptionParser while adding post-processing.""" + + def __init__( + self, + *, + version: str, + plugin_versions: str, + parents: list[argparse.ArgumentParser], + formatter_names: list[str], + ) -> None: + """Initialize an instance of an OptionManager.""" + self.formatter_names = formatter_names + self.parser = argparse.ArgumentParser( + prog="flake8", + usage="%(prog)s [options] file file ...", + parents=parents, + epilog=f"Installed plugins: {plugin_versions}", + ) + self.parser.add_argument( + "--version", + action="version", + version=( + f"{version} ({plugin_versions}) " + f"{utils.get_python_version()}" + ), + ) + self.parser.add_argument("filenames", nargs="*", metavar="filename") + + self.config_options_dict: dict[str, Option] = {} + self.options: list[Option] = [] + self.extended_default_ignore: list[str] = [] + self.extended_default_select: list[str] = [] + + self._current_group: argparse._ArgumentGroup | None = None + + # TODO: maybe make this a free function to reduce api surface area + def register_plugins(self, plugins: Plugins) -> None: + """Register the plugin options (if needed).""" + groups: dict[str, argparse._ArgumentGroup] = {} + + def _set_group(name: str) -> None: + try: + self._current_group = groups[name] + except KeyError: + group = self.parser.add_argument_group(name) + self._current_group = groups[name] = group + + for loaded in plugins.all_plugins(): + add_options = getattr(loaded.obj, "add_options", None) + if add_options: + _set_group(loaded.plugin.package) + add_options(self) + + if loaded.plugin.entry_point.group == "flake8.extension": + self.extend_default_select([loaded.entry_name]) + + # isn't strictly necessary, but seems cleaner + self._current_group = None + + def add_option(self, *args: Any, **kwargs: Any) -> None: + """Create and register a new option. + + See parameters for :class:`~flake8.options.manager.Option` for + acceptable arguments to this method. + + .. note:: + + ``short_option_name`` and ``long_option_name`` may be specified + positionally as they are with argparse normally. + """ + option = Option(*args, **kwargs) + option_args, option_kwargs = option.to_argparse() + if self._current_group is not None: + self._current_group.add_argument(*option_args, **option_kwargs) + else: + self.parser.add_argument(*option_args, **option_kwargs) + self.options.append(option) + if option.parse_from_config: + name = option.config_name + assert name is not None + self.config_options_dict[name] = option + self.config_options_dict[name.replace("_", "-")] = option + LOG.debug('Registered option "%s".', option) + + def extend_default_ignore(self, error_codes: Sequence[str]) -> None: + """Extend the default ignore list with the error codes provided. + + :param error_codes: + List of strings that are the error/warning codes with which to + extend the default ignore list. + """ + LOG.debug("Extending default ignore list with %r", error_codes) + self.extended_default_ignore.extend(error_codes) + + def extend_default_select(self, error_codes: Sequence[str]) -> None: + """Extend the default select list with the error codes provided. + + :param error_codes: + List of strings that are the error/warning codes with which + to extend the default select list. + """ + LOG.debug("Extending default select list with %r", error_codes) + self.extended_default_select.extend(error_codes) + + def parse_args( + self, + args: Sequence[str] | None = None, + values: argparse.Namespace | None = None, + ) -> argparse.Namespace: + """Proxy to calling the OptionParser's parse_args method.""" + if values: + self.parser.set_defaults(**vars(values)) + return self.parser.parse_args(args) diff --git a/src/flake8-main/flake8-main/src/flake8/options/parse_args.py b/src/flake8-main/flake8-main/src/flake8/options/parse_args.py new file mode 100644 index 0000000..ff5e08f --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/options/parse_args.py @@ -0,0 +1,70 @@ +"""Procedure for parsing args, config, loading plugins.""" +from __future__ import annotations + +import argparse +from collections.abc import Sequence + +import flake8 +from flake8.main import options +from flake8.options import aggregator +from flake8.options import config +from flake8.options import manager +from flake8.plugins import finder + + +def parse_args( + argv: Sequence[str], +) -> tuple[finder.Plugins, argparse.Namespace]: + """Procedure for parsing args, config, loading plugins.""" + prelim_parser = options.stage1_arg_parser() + + args0, rest = prelim_parser.parse_known_args(argv) + # XXX (ericvw): Special case "forwarding" the output file option so + # that it can be reparsed again for the BaseFormatter.filename. + if args0.output_file: + rest.extend(("--output-file", args0.output_file)) + + flake8.configure_logging(args0.verbose, args0.output_file) + + cfg, cfg_dir = config.load_config( + config=args0.config, + extra=args0.append_config, + isolated=args0.isolated, + ) + + plugin_opts = finder.parse_plugin_options( + cfg, + cfg_dir, + enable_extensions=args0.enable_extensions, + require_plugins=args0.require_plugins, + ) + raw_plugins = finder.find_plugins(cfg, plugin_opts) + plugins = finder.load_plugins(raw_plugins, plugin_opts) + + option_manager = manager.OptionManager( + version=flake8.__version__, + plugin_versions=plugins.versions_str(), + parents=[prelim_parser], + formatter_names=list(plugins.reporters), + ) + options.register_default_options(option_manager) + option_manager.register_plugins(plugins) + + opts = aggregator.aggregate_options(option_manager, cfg, cfg_dir, rest) + + for loaded in plugins.all_plugins(): + parse_options = getattr(loaded.obj, "parse_options", None) + if parse_options is None: + continue + + # XXX: ideally we wouldn't have two forms of parse_options + try: + parse_options( + option_manager, + opts, + opts.filenames, + ) + except TypeError: + parse_options(opts) + + return plugins, opts diff --git a/src/flake8-main/flake8-main/src/flake8/plugins/__init__.py b/src/flake8-main/flake8-main/src/flake8/plugins/__init__.py new file mode 100644 index 0000000..b540313 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/plugins/__init__.py @@ -0,0 +1,2 @@ +"""Submodule of built-in plugins and plugin managers.""" +from __future__ import annotations diff --git a/src/flake8-main/flake8-main/src/flake8/plugins/finder.py b/src/flake8-main/flake8-main/src/flake8/plugins/finder.py new file mode 100644 index 0000000..4da3402 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/plugins/finder.py @@ -0,0 +1,365 @@ +"""Functions related to finding and loading plugins.""" +from __future__ import annotations + +import configparser +import importlib.metadata +import inspect +import itertools +import logging +import sys +from collections.abc import Generator +from collections.abc import Iterable +from typing import Any +from typing import NamedTuple + +from flake8 import utils +from flake8.defaults import VALID_CODE_PREFIX +from flake8.exceptions import ExecutionError +from flake8.exceptions import FailedToLoadPlugin + +LOG = logging.getLogger(__name__) + +FLAKE8_GROUPS = frozenset(("flake8.extension", "flake8.report")) + +BANNED_PLUGINS = { + "flake8-colors": "5.0", + "flake8-per-file-ignores": "3.7", +} + + +class Plugin(NamedTuple): + """A plugin before loading.""" + + package: str + version: str + entry_point: importlib.metadata.EntryPoint + + +class LoadedPlugin(NamedTuple): + """Represents a plugin after being imported.""" + + plugin: Plugin + obj: Any + parameters: dict[str, bool] + + @property + def entry_name(self) -> str: + """Return the name given in the packaging metadata.""" + return self.plugin.entry_point.name + + @property + def display_name(self) -> str: + """Return the name for use in user-facing / error messages.""" + return f"{self.plugin.package}[{self.entry_name}]" + + +class Checkers(NamedTuple): + """Classified plugins needed for checking.""" + + tree: list[LoadedPlugin] + logical_line: list[LoadedPlugin] + physical_line: list[LoadedPlugin] + + +class Plugins(NamedTuple): + """Classified plugins.""" + + checkers: Checkers + reporters: dict[str, LoadedPlugin] + disabled: list[LoadedPlugin] + + def all_plugins(self) -> Generator[LoadedPlugin]: + """Return an iterator over all :class:`LoadedPlugin`s.""" + yield from self.checkers.tree + yield from self.checkers.logical_line + yield from self.checkers.physical_line + yield from self.reporters.values() + + def versions_str(self) -> str: + """Return a user-displayed list of plugin versions.""" + return ", ".join( + sorted( + { + f"{loaded.plugin.package}: {loaded.plugin.version}" + for loaded in self.all_plugins() + if loaded.plugin.package not in {"flake8", "local"} + }, + ), + ) + + +class PluginOptions(NamedTuple): + """Options related to plugin loading.""" + + local_plugin_paths: tuple[str, ...] + enable_extensions: frozenset[str] + require_plugins: frozenset[str] + + @classmethod + def blank(cls) -> PluginOptions: + """Make a blank PluginOptions, mostly used for tests.""" + return cls( + local_plugin_paths=(), + enable_extensions=frozenset(), + require_plugins=frozenset(), + ) + + +def _parse_option( + cfg: configparser.RawConfigParser, + cfg_opt_name: str, + opt: str | None, +) -> list[str]: + # specified on commandline: use that + if opt is not None: + return utils.parse_comma_separated_list(opt) + else: + # ideally this would reuse our config parsing framework but we need to + # parse this from preliminary options before plugins are enabled + for opt_name in (cfg_opt_name, cfg_opt_name.replace("_", "-")): + val = cfg.get("flake8", opt_name, fallback=None) + if val is not None: + return utils.parse_comma_separated_list(val) + else: + return [] + + +def parse_plugin_options( + cfg: configparser.RawConfigParser, + cfg_dir: str, + *, + enable_extensions: str | None, + require_plugins: str | None, +) -> PluginOptions: + """Parse plugin loading related options.""" + paths_s = cfg.get("flake8:local-plugins", "paths", fallback="").strip() + paths = utils.parse_comma_separated_list(paths_s) + paths = utils.normalize_paths(paths, cfg_dir) + + return PluginOptions( + local_plugin_paths=tuple(paths), + enable_extensions=frozenset( + _parse_option(cfg, "enable_extensions", enable_extensions), + ), + require_plugins=frozenset( + _parse_option(cfg, "require_plugins", require_plugins), + ), + ) + + +def _flake8_plugins( + eps: Iterable[importlib.metadata.EntryPoint], + name: str, + version: str, +) -> Generator[Plugin]: + pyflakes_meta = importlib.metadata.distribution("pyflakes").metadata + pycodestyle_meta = importlib.metadata.distribution("pycodestyle").metadata + + for ep in eps: + if ep.group not in FLAKE8_GROUPS: + continue + + if ep.name == "F": + yield Plugin(pyflakes_meta["name"], pyflakes_meta["version"], ep) + elif ep.name in "EW": + # pycodestyle provides both `E` and `W` -- but our default select + # handles those + # ideally pycodestyle's plugin entrypoints would exactly represent + # the codes they produce... + yield Plugin( + pycodestyle_meta["name"], pycodestyle_meta["version"], ep, + ) + else: + yield Plugin(name, version, ep) + + +def _find_importlib_plugins() -> Generator[Plugin]: + # some misconfigured pythons (RHEL) have things on `sys.path` twice + seen = set() + for dist in importlib.metadata.distributions(): + # assigned to prevent continual reparsing + eps = dist.entry_points + + # perf: skip parsing `.metadata` (slow) if no entry points match + if not any(ep.group in FLAKE8_GROUPS for ep in eps): + continue + + # assigned to prevent continual reparsing + meta = dist.metadata + + if meta["name"] in seen: + continue + else: + seen.add(meta["name"]) + + if meta["name"] in BANNED_PLUGINS: + LOG.warning( + "%s plugin is obsolete in flake8>=%s", + meta["name"], + BANNED_PLUGINS[meta["name"]], + ) + continue + elif meta["name"] == "flake8": + # special case flake8 which provides plugins for pyflakes / + # pycodestyle + yield from _flake8_plugins(eps, meta["name"], meta["version"]) + continue + + for ep in eps: + if ep.group in FLAKE8_GROUPS: + yield Plugin(meta["name"], meta["version"], ep) + + +def _find_local_plugins( + cfg: configparser.RawConfigParser, +) -> Generator[Plugin]: + for plugin_type in ("extension", "report"): + group = f"flake8.{plugin_type}" + for plugin_s in utils.parse_comma_separated_list( + cfg.get("flake8:local-plugins", plugin_type, fallback="").strip(), + regexp=utils.LOCAL_PLUGIN_LIST_RE, + ): + name, _, entry_str = plugin_s.partition("=") + name, entry_str = name.strip(), entry_str.strip() + ep = importlib.metadata.EntryPoint(name, entry_str, group) + yield Plugin("local", "local", ep) + + +def _check_required_plugins( + plugins: list[Plugin], + expected: frozenset[str], +) -> None: + plugin_names = { + utils.normalize_pypi_name(plugin.package) for plugin in plugins + } + expected_names = {utils.normalize_pypi_name(name) for name in expected} + missing_plugins = expected_names - plugin_names + + if missing_plugins: + raise ExecutionError( + f"required plugins were not installed!\n" + f"- installed: {', '.join(sorted(plugin_names))}\n" + f"- expected: {', '.join(sorted(expected_names))}\n" + f"- missing: {', '.join(sorted(missing_plugins))}", + ) + + +def find_plugins( + cfg: configparser.RawConfigParser, + opts: PluginOptions, +) -> list[Plugin]: + """Discovers all plugins (but does not load them).""" + ret = [*_find_importlib_plugins(), *_find_local_plugins(cfg)] + + # for determinism, sort the list + ret.sort() + + _check_required_plugins(ret, opts.require_plugins) + + return ret + + +def _parameters_for(func: Any) -> dict[str, bool]: + """Return the parameters for the plugin. + + This will inspect the plugin and return either the function parameters + if the plugin is a function or the parameters for ``__init__`` after + ``self`` if the plugin is a class. + + :returns: + A dictionary mapping the parameter name to whether or not it is + required (a.k.a., is positional only/does not have a default). + """ + is_class = not inspect.isfunction(func) + if is_class: + func = func.__init__ + + parameters = { + parameter.name: parameter.default is inspect.Parameter.empty + for parameter in inspect.signature(func).parameters.values() + if parameter.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD + } + + if is_class: + parameters.pop("self", None) + + return parameters + + +def _load_plugin(plugin: Plugin) -> LoadedPlugin: + try: + obj = plugin.entry_point.load() + except Exception as e: + raise FailedToLoadPlugin(plugin.package, e) + + if not callable(obj): + err = TypeError("expected loaded plugin to be callable") + raise FailedToLoadPlugin(plugin.package, err) + + return LoadedPlugin(plugin, obj, _parameters_for(obj)) + + +def _import_plugins( + plugins: list[Plugin], + opts: PluginOptions, +) -> list[LoadedPlugin]: + sys.path.extend(opts.local_plugin_paths) + return [_load_plugin(p) for p in plugins] + + +def _classify_plugins( + plugins: list[LoadedPlugin], + opts: PluginOptions, +) -> Plugins: + tree = [] + logical_line = [] + physical_line = [] + reporters = {} + disabled = [] + + for loaded in plugins: + if ( + getattr(loaded.obj, "off_by_default", False) + and loaded.plugin.entry_point.name not in opts.enable_extensions + ): + disabled.append(loaded) + elif loaded.plugin.entry_point.group == "flake8.report": + reporters[loaded.entry_name] = loaded + elif "tree" in loaded.parameters: + tree.append(loaded) + elif "logical_line" in loaded.parameters: + logical_line.append(loaded) + elif "physical_line" in loaded.parameters: + physical_line.append(loaded) + else: + raise NotImplementedError(f"what plugin type? {loaded}") + + for loaded in itertools.chain(tree, logical_line, physical_line): + if not VALID_CODE_PREFIX.match(loaded.entry_name): + raise ExecutionError( + f"plugin code for `{loaded.display_name}` does not match " + f"{VALID_CODE_PREFIX.pattern}", + ) + + return Plugins( + checkers=Checkers( + tree=tree, + logical_line=logical_line, + physical_line=physical_line, + ), + reporters=reporters, + disabled=disabled, + ) + + +def load_plugins( + plugins: list[Plugin], + opts: PluginOptions, +) -> Plugins: + """Load and classify all flake8 plugins. + + - first: extends ``sys.path`` with ``paths`` (to import local plugins) + - next: converts the ``Plugin``s to ``LoadedPlugins`` + - finally: classifies plugins into their specific types + """ + return _classify_plugins(_import_plugins(plugins, opts), opts) diff --git a/src/flake8-main/flake8-main/src/flake8/plugins/pycodestyle.py b/src/flake8-main/flake8-main/src/flake8/plugins/pycodestyle.py new file mode 100644 index 0000000..cd760dc --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/plugins/pycodestyle.py @@ -0,0 +1,112 @@ +"""Generated using ./bin/gen-pycodestyle-plugin.""" +# fmt: off +from __future__ import annotations + +from collections.abc import Generator +from typing import Any + +from pycodestyle import ambiguous_identifier as _ambiguous_identifier +from pycodestyle import bare_except as _bare_except +from pycodestyle import blank_lines as _blank_lines +from pycodestyle import break_after_binary_operator as _break_after_binary_operator # noqa: E501 +from pycodestyle import break_before_binary_operator as _break_before_binary_operator # noqa: E501 +from pycodestyle import comparison_negative as _comparison_negative +from pycodestyle import comparison_to_singleton as _comparison_to_singleton +from pycodestyle import comparison_type as _comparison_type +from pycodestyle import compound_statements as _compound_statements +from pycodestyle import continued_indentation as _continued_indentation +from pycodestyle import explicit_line_join as _explicit_line_join +from pycodestyle import extraneous_whitespace as _extraneous_whitespace +from pycodestyle import imports_on_separate_lines as _imports_on_separate_lines +from pycodestyle import indentation as _indentation +from pycodestyle import maximum_doc_length as _maximum_doc_length +from pycodestyle import maximum_line_length as _maximum_line_length +from pycodestyle import missing_whitespace as _missing_whitespace +from pycodestyle import missing_whitespace_after_keyword as _missing_whitespace_after_keyword # noqa: E501 +from pycodestyle import module_imports_on_top_of_file as _module_imports_on_top_of_file # noqa: E501 +from pycodestyle import python_3000_invalid_escape_sequence as _python_3000_invalid_escape_sequence # noqa: E501 +from pycodestyle import tabs_obsolete as _tabs_obsolete +from pycodestyle import tabs_or_spaces as _tabs_or_spaces +from pycodestyle import trailing_blank_lines as _trailing_blank_lines +from pycodestyle import trailing_whitespace as _trailing_whitespace +from pycodestyle import whitespace_around_comma as _whitespace_around_comma +from pycodestyle import whitespace_around_keywords as _whitespace_around_keywords # noqa: E501 +from pycodestyle import whitespace_around_named_parameter_equals as _whitespace_around_named_parameter_equals # noqa: E501 +from pycodestyle import whitespace_around_operator as _whitespace_around_operator # noqa: E501 +from pycodestyle import whitespace_before_comment as _whitespace_before_comment +from pycodestyle import whitespace_before_parameters as _whitespace_before_parameters # noqa: E501 + + +def pycodestyle_logical( + blank_before: Any, + blank_lines: Any, + checker_state: Any, + hang_closing: Any, + indent_char: Any, + indent_level: Any, + indent_size: Any, + line_number: Any, + lines: Any, + logical_line: Any, + max_doc_length: Any, + noqa: Any, + previous_indent_level: Any, + previous_logical: Any, + previous_unindented_logical_line: Any, + tokens: Any, + verbose: Any, +) -> Generator[tuple[int, str]]: + """Run pycodestyle logical checks.""" + yield from _ambiguous_identifier(logical_line, tokens) + yield from _bare_except(logical_line, noqa) + yield from _blank_lines(logical_line, blank_lines, indent_level, line_number, blank_before, previous_logical, previous_unindented_logical_line, previous_indent_level, lines) # noqa: E501 + yield from _break_after_binary_operator(logical_line, tokens) + yield from _break_before_binary_operator(logical_line, tokens) + yield from _comparison_negative(logical_line) + yield from _comparison_to_singleton(logical_line, noqa) + yield from _comparison_type(logical_line, noqa) + yield from _compound_statements(logical_line) + yield from _continued_indentation(logical_line, tokens, indent_level, hang_closing, indent_char, indent_size, noqa, verbose) # noqa: E501 + yield from _explicit_line_join(logical_line, tokens) + yield from _extraneous_whitespace(logical_line) + yield from _imports_on_separate_lines(logical_line) + yield from _indentation(logical_line, previous_logical, indent_char, indent_level, previous_indent_level, indent_size) # noqa: E501 + yield from _maximum_doc_length(logical_line, max_doc_length, noqa, tokens) + yield from _missing_whitespace(logical_line, tokens) + yield from _missing_whitespace_after_keyword(logical_line, tokens) + yield from _module_imports_on_top_of_file(logical_line, indent_level, checker_state, noqa) # noqa: E501 + yield from _python_3000_invalid_escape_sequence(logical_line, tokens, noqa) + yield from _whitespace_around_comma(logical_line) + yield from _whitespace_around_keywords(logical_line) + yield from _whitespace_around_named_parameter_equals(logical_line, tokens) + yield from _whitespace_around_operator(logical_line) + yield from _whitespace_before_comment(logical_line, tokens) + yield from _whitespace_before_parameters(logical_line, tokens) + + +def pycodestyle_physical( + indent_char: Any, + line_number: Any, + lines: Any, + max_line_length: Any, + multiline: Any, + noqa: Any, + physical_line: Any, + total_lines: Any, +) -> Generator[tuple[int, str]]: + """Run pycodestyle physical checks.""" + ret = _maximum_line_length(physical_line, max_line_length, multiline, line_number, noqa) # noqa: E501 + if ret is not None: + yield ret + ret = _tabs_obsolete(physical_line) + if ret is not None: + yield ret + ret = _tabs_or_spaces(physical_line, indent_char) + if ret is not None: + yield ret + ret = _trailing_blank_lines(physical_line, lines, line_number, total_lines) + if ret is not None: + yield ret + ret = _trailing_whitespace(physical_line) + if ret is not None: + yield ret diff --git a/src/flake8-main/flake8-main/src/flake8/plugins/pyflakes.py b/src/flake8-main/flake8-main/src/flake8/plugins/pyflakes.py new file mode 100644 index 0000000..9844025 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/plugins/pyflakes.py @@ -0,0 +1,114 @@ +"""Plugin built-in to Flake8 to treat pyflakes as a plugin.""" +from __future__ import annotations + +import argparse +import ast +import logging +from collections.abc import Generator +from typing import Any + +import pyflakes.checker + +from flake8.options.manager import OptionManager + +LOG = logging.getLogger(__name__) + +FLAKE8_PYFLAKES_CODES = { + "UnusedImport": "F401", + "ImportShadowedByLoopVar": "F402", + "ImportStarUsed": "F403", + "LateFutureImport": "F404", + "ImportStarUsage": "F405", + "ImportStarNotPermitted": "F406", + "FutureFeatureNotDefined": "F407", + "PercentFormatInvalidFormat": "F501", + "PercentFormatExpectedMapping": "F502", + "PercentFormatExpectedSequence": "F503", + "PercentFormatExtraNamedArguments": "F504", + "PercentFormatMissingArgument": "F505", + "PercentFormatMixedPositionalAndNamed": "F506", + "PercentFormatPositionalCountMismatch": "F507", + "PercentFormatStarRequiresSequence": "F508", + "PercentFormatUnsupportedFormatCharacter": "F509", + "StringDotFormatInvalidFormat": "F521", + "StringDotFormatExtraNamedArguments": "F522", + "StringDotFormatExtraPositionalArguments": "F523", + "StringDotFormatMissingArgument": "F524", + "StringDotFormatMixingAutomatic": "F525", + "FStringMissingPlaceholders": "F541", + "TStringMissingPlaceholders": "F542", + "MultiValueRepeatedKeyLiteral": "F601", + "MultiValueRepeatedKeyVariable": "F602", + "TooManyExpressionsInStarredAssignment": "F621", + "TwoStarredExpressions": "F622", + "AssertTuple": "F631", + "IsLiteral": "F632", + "InvalidPrintSyntax": "F633", + "IfTuple": "F634", + "BreakOutsideLoop": "F701", + "ContinueOutsideLoop": "F702", + "YieldOutsideFunction": "F704", + "ReturnOutsideFunction": "F706", + "DefaultExceptNotLast": "F707", + "DoctestSyntaxError": "F721", + "ForwardAnnotationSyntaxError": "F722", + "RedefinedWhileUnused": "F811", + "UndefinedName": "F821", + "UndefinedExport": "F822", + "UndefinedLocal": "F823", + "UnusedIndirectAssignment": "F824", + "DuplicateArgument": "F831", + "UnusedVariable": "F841", + "UnusedAnnotation": "F842", + "RaiseNotImplemented": "F901", +} + + +class FlakesChecker(pyflakes.checker.Checker): + """Subclass the Pyflakes checker to conform with the flake8 API.""" + + with_doctest = False + + def __init__(self, tree: ast.AST, filename: str) -> None: + """Initialize the PyFlakes plugin with an AST tree and filename.""" + super().__init__( + tree, filename=filename, withDoctest=self.with_doctest, + ) + + @classmethod + def add_options(cls, parser: OptionManager) -> None: + """Register options for PyFlakes on the Flake8 OptionManager.""" + parser.add_option( + "--builtins", + parse_from_config=True, + comma_separated_list=True, + help="define more built-ins, comma separated", + ) + parser.add_option( + "--doctests", + default=False, + action="store_true", + parse_from_config=True, + help="also check syntax of the doctests", + ) + + @classmethod + def parse_options(cls, options: argparse.Namespace) -> None: + """Parse option values from Flake8's OptionManager.""" + if options.builtins: + cls.builtIns = cls.builtIns.union(options.builtins) + cls.with_doctest = options.doctests + + def run(self) -> Generator[tuple[int, int, str, type[Any]]]: + """Run the plugin.""" + for message in self.messages: + col = getattr(message, "col", 0) + yield ( + message.lineno, + col, + "{} {}".format( + FLAKE8_PYFLAKES_CODES.get(type(message).__name__, "F999"), + message.message % message.message_args, + ), + message.__class__, + ) diff --git a/src/flake8-main/flake8-main/src/flake8/plugins/reporter.py b/src/flake8-main/flake8-main/src/flake8/plugins/reporter.py new file mode 100644 index 0000000..a5749c0 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/plugins/reporter.py @@ -0,0 +1,42 @@ +"""Functions for constructing the requested report plugin.""" +from __future__ import annotations + +import argparse +import logging + +from flake8.formatting.base import BaseFormatter +from flake8.plugins.finder import LoadedPlugin + +LOG = logging.getLogger(__name__) + + +def make( + reporters: dict[str, LoadedPlugin], + options: argparse.Namespace, +) -> BaseFormatter: + """Make the formatter from the requested user options. + + - if :option:`flake8 --quiet` is specified, return the ``quiet-filename`` + formatter. + - if :option:`flake8 --quiet` is specified at least twice, return the + ``quiet-nothing`` formatter. + - otherwise attempt to return the formatter by name. + - failing that, assume it is a format string and return the ``default`` + formatter. + """ + format_name = options.format + if options.quiet == 1: + format_name = "quiet-filename" + elif options.quiet >= 2: + format_name = "quiet-nothing" + + try: + format_plugin = reporters[format_name] + except KeyError: + LOG.warning( + "%r is an unknown formatter. Falling back to default.", + format_name, + ) + format_plugin = reporters["default"] + + return format_plugin.obj(options) diff --git a/src/flake8-main/flake8-main/src/flake8/processor.py b/src/flake8-main/flake8-main/src/flake8/processor.py new file mode 100644 index 0000000..b1742ca --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/processor.py @@ -0,0 +1,454 @@ +"""Module containing our file processor that tokenizes a file for checks.""" +from __future__ import annotations + +import argparse +import ast +import functools +import logging +import tokenize +from collections.abc import Generator +from typing import Any + +from flake8 import defaults +from flake8 import utils +from flake8._compat import FSTRING_END +from flake8._compat import FSTRING_MIDDLE +from flake8._compat import TSTRING_END +from flake8._compat import TSTRING_MIDDLE +from flake8.plugins.finder import LoadedPlugin + +LOG = logging.getLogger(__name__) +NEWLINE = frozenset([tokenize.NL, tokenize.NEWLINE]) + +SKIP_TOKENS = frozenset( + [tokenize.NL, tokenize.NEWLINE, tokenize.INDENT, tokenize.DEDENT], +) + +_LogicalMapping = list[tuple[int, tuple[int, int]]] +_Logical = tuple[list[str], list[str], _LogicalMapping] + + +class FileProcessor: + """Processes a file and holds state. + + This processes a file by generating tokens, logical and physical lines, + and AST trees. This also provides a way of passing state about the file + to checks expecting that state. Any public attribute on this object can + be requested by a plugin. The known public attributes are: + + - :attr:`blank_before` + - :attr:`blank_lines` + - :attr:`checker_state` + - :attr:`indent_char` + - :attr:`indent_level` + - :attr:`line_number` + - :attr:`logical_line` + - :attr:`max_line_length` + - :attr:`max_doc_length` + - :attr:`multiline` + - :attr:`noqa` + - :attr:`previous_indent_level` + - :attr:`previous_logical` + - :attr:`previous_unindented_logical_line` + - :attr:`tokens` + - :attr:`file_tokens` + - :attr:`total_lines` + - :attr:`verbose` + """ + + #: always ``False``, included for compatibility + noqa = False + + def __init__( + self, + filename: str, + options: argparse.Namespace, + lines: list[str] | None = None, + ) -> None: + """Initialize our file processor. + + :param filename: Name of the file to process + """ + self.options = options + self.filename = filename + self.lines = lines if lines is not None else self.read_lines() + self.strip_utf_bom() + + # Defaults for public attributes + #: Number of preceding blank lines + self.blank_before = 0 + #: Number of blank lines + self.blank_lines = 0 + #: Checker states for each plugin? + self._checker_states: dict[str, dict[Any, Any]] = {} + #: Current checker state + self.checker_state: dict[Any, Any] = {} + #: User provided option for hang closing + self.hang_closing = options.hang_closing + #: Character used for indentation + self.indent_char: str | None = None + #: Current level of indentation + self.indent_level = 0 + #: Number of spaces used for indentation + self.indent_size = options.indent_size + #: Line number in the file + self.line_number = 0 + #: Current logical line + self.logical_line = "" + #: Maximum line length as configured by the user + self.max_line_length = options.max_line_length + #: Maximum docstring / comment line length as configured by the user + self.max_doc_length = options.max_doc_length + #: Whether the current physical line is multiline + self.multiline = False + #: Previous level of indentation + self.previous_indent_level = 0 + #: Previous logical line + self.previous_logical = "" + #: Previous unindented (i.e. top-level) logical line + self.previous_unindented_logical_line = "" + #: Current set of tokens + self.tokens: list[tokenize.TokenInfo] = [] + #: Total number of lines in the file + self.total_lines = len(self.lines) + #: Verbosity level of Flake8 + self.verbose = options.verbose + #: Statistics dictionary + self.statistics = {"logical lines": 0} + self._fstring_start = self._tstring_start = -1 + + @functools.cached_property + def file_tokens(self) -> list[tokenize.TokenInfo]: + """Return the complete set of tokens for a file.""" + line_iter = iter(self.lines) + return list(tokenize.generate_tokens(lambda: next(line_iter))) + + def fstring_start(self, lineno: int) -> None: # pragma: >=3.12 cover + """Signal the beginning of an fstring.""" + self._fstring_start = lineno + + def tstring_start(self, lineno: int) -> None: # pragma: >=3.14 cover + """Signal the beginning of an tstring.""" + self._tstring_start = lineno + + def multiline_string(self, token: tokenize.TokenInfo) -> Generator[str]: + """Iterate through the lines of a multiline string.""" + if token.type == FSTRING_END: # pragma: >=3.12 cover + start = self._fstring_start + elif token.type == TSTRING_END: # pragma: >=3.14 cover + start = self._tstring_start + else: + start = token.start[0] + + self.multiline = True + self.line_number = start + # intentionally don't include the last line, that line will be + # terminated later by a future end-of-line + for _ in range(start, token.end[0]): + yield self.lines[self.line_number - 1] + self.line_number += 1 + self.multiline = False + + def reset_blank_before(self) -> None: + """Reset the blank_before attribute to zero.""" + self.blank_before = 0 + + def delete_first_token(self) -> None: + """Delete the first token in the list of tokens.""" + del self.tokens[0] + + def visited_new_blank_line(self) -> None: + """Note that we visited a new blank line.""" + self.blank_lines += 1 + + def update_state(self, mapping: _LogicalMapping) -> None: + """Update the indent level based on the logical line mapping.""" + (start_row, start_col) = mapping[0][1] + start_line = self.lines[start_row - 1] + self.indent_level = expand_indent(start_line[:start_col]) + if self.blank_before < self.blank_lines: + self.blank_before = self.blank_lines + + def update_checker_state_for(self, plugin: LoadedPlugin) -> None: + """Update the checker_state attribute for the plugin.""" + if "checker_state" in plugin.parameters: + self.checker_state = self._checker_states.setdefault( + plugin.entry_name, {}, + ) + + def next_logical_line(self) -> None: + """Record the previous logical line. + + This also resets the tokens list and the blank_lines count. + """ + if self.logical_line: + self.previous_indent_level = self.indent_level + self.previous_logical = self.logical_line + if not self.indent_level: + self.previous_unindented_logical_line = self.logical_line + self.blank_lines = 0 + self.tokens = [] + + def build_logical_line_tokens(self) -> _Logical: # noqa: C901 + """Build the mapping, comments, and logical line lists.""" + logical = [] + comments = [] + mapping: _LogicalMapping = [] + length = 0 + previous_row = previous_column = None + for token_type, text, start, end, line in self.tokens: + if token_type in SKIP_TOKENS: + continue + if not mapping: + mapping = [(0, start)] + if token_type == tokenize.COMMENT: + comments.append(text) + continue + if token_type == tokenize.STRING: + text = mutate_string(text) + elif token_type in { + FSTRING_MIDDLE, + TSTRING_MIDDLE, + }: # pragma: >=3.12 cover # noqa: E501 + # A curly brace in an FSTRING_MIDDLE token must be an escaped + # curly brace. Both 'text' and 'end' will account for the + # escaped version of the token (i.e. a single brace) rather + # than the raw double brace version, so we must counteract this + brace_offset = text.count("{") + text.count("}") + text = "x" * (len(text) + brace_offset) + end = (end[0], end[1] + brace_offset) + if previous_row is not None and previous_column is not None: + (start_row, start_column) = start + if previous_row != start_row: + row_index = previous_row - 1 + column_index = previous_column - 1 + previous_text = self.lines[row_index][column_index] + if previous_text == "," or ( + previous_text not in "{[(" and text not in "}])" + ): + text = f" {text}" + elif previous_column != start_column: + text = line[previous_column:start_column] + text + logical.append(text) + length += len(text) + mapping.append((length, end)) + (previous_row, previous_column) = end + return comments, logical, mapping + + def build_ast(self) -> ast.AST: + """Build an abstract syntax tree from the list of lines.""" + return ast.parse("".join(self.lines)) + + def build_logical_line(self) -> tuple[str, str, _LogicalMapping]: + """Build a logical line from the current tokens list.""" + comments, logical, mapping_list = self.build_logical_line_tokens() + joined_comments = "".join(comments) + self.logical_line = "".join(logical) + self.statistics["logical lines"] += 1 + return joined_comments, self.logical_line, mapping_list + + def keyword_arguments_for( + self, + parameters: dict[str, bool], + arguments: dict[str, Any], + ) -> dict[str, Any]: + """Generate the keyword arguments for a list of parameters.""" + ret = {} + for param, required in parameters.items(): + if param in arguments: + continue + try: + ret[param] = getattr(self, param) + except AttributeError: + if required: + raise + else: + LOG.warning( + 'Plugin requested optional parameter "%s" ' + "but this is not an available parameter.", + param, + ) + return ret + + def generate_tokens(self) -> Generator[tokenize.TokenInfo]: + """Tokenize the file and yield the tokens.""" + for token in tokenize.generate_tokens(self.next_line): + if token[2][0] > self.total_lines: + break + self.tokens.append(token) + yield token + + def _noqa_line_range(self, min_line: int, max_line: int) -> dict[int, str]: + line_range = range(min_line, max_line + 1) + joined = "".join(self.lines[min_line - 1: max_line]) + return dict.fromkeys(line_range, joined) + + @functools.cached_property + def _noqa_line_mapping(self) -> dict[int, str]: + """Map from line number to the line we'll search for `noqa` in.""" + try: + file_tokens = self.file_tokens + except (tokenize.TokenError, SyntaxError): + # if we failed to parse the file tokens, we'll always fail in + # the future, so set this so the code does not try again + return {} + else: + ret = {} + + min_line = len(self.lines) + 2 + max_line = -1 + for tp, _, (s_line, _), (e_line, _), _ in file_tokens: + if tp == tokenize.ENDMARKER or tp == tokenize.DEDENT: + continue + + min_line = min(min_line, s_line) + max_line = max(max_line, e_line) + + if tp in (tokenize.NL, tokenize.NEWLINE): + ret.update(self._noqa_line_range(min_line, max_line)) + + min_line = len(self.lines) + 2 + max_line = -1 + + return ret + + def noqa_line_for(self, line_number: int) -> str | None: + """Retrieve the line which will be used to determine noqa.""" + # NOTE(sigmavirus24): Some plugins choose to report errors for empty + # files on Line 1. In those cases, we shouldn't bother trying to + # retrieve a physical line (since none exist). + return self._noqa_line_mapping.get(line_number) + + def next_line(self) -> str: + """Get the next line from the list.""" + if self.line_number >= self.total_lines: + return "" + line = self.lines[self.line_number] + self.line_number += 1 + if self.indent_char is None and line[:1] in defaults.WHITESPACE: + self.indent_char = line[0] + return line + + def read_lines(self) -> list[str]: + """Read the lines for this file checker.""" + if self.filename == "-": + self.filename = self.options.stdin_display_name or "stdin" + lines = self.read_lines_from_stdin() + else: + lines = self.read_lines_from_filename() + return lines + + def read_lines_from_filename(self) -> list[str]: + """Read the lines for a file.""" + try: + with tokenize.open(self.filename) as fd: + return fd.readlines() + except (SyntaxError, UnicodeError): + # If we can't detect the codec with tokenize.detect_encoding, or + # the detected encoding is incorrect, just fallback to latin-1. + with open(self.filename, encoding="latin-1") as fd: + return fd.readlines() + + def read_lines_from_stdin(self) -> list[str]: + """Read the lines from standard in.""" + return utils.stdin_get_lines() + + def should_ignore_file(self) -> bool: + """Check if ``flake8: noqa`` is in the file to be ignored. + + :returns: + True if a line matches :attr:`defaults.NOQA_FILE`, + otherwise False + """ + if not self.options.disable_noqa and any( + defaults.NOQA_FILE.match(line) for line in self.lines + ): + return True + elif any(defaults.NOQA_FILE.search(line) for line in self.lines): + LOG.warning( + "Detected `flake8: noqa` on line with code. To ignore an " + "error on a line use `noqa` instead.", + ) + return False + else: + return False + + def strip_utf_bom(self) -> None: + """Strip the UTF bom from the lines of the file.""" + if not self.lines: + # If we have nothing to analyze quit early + return + + # If the first byte of the file is a UTF-8 BOM, strip it + if self.lines[0][:1] == "\uFEFF": + self.lines[0] = self.lines[0][1:] + elif self.lines[0][:3] == "\xEF\xBB\xBF": + self.lines[0] = self.lines[0][3:] + + +def is_eol_token(token: tokenize.TokenInfo) -> bool: + """Check if the token is an end-of-line token.""" + return token[0] in NEWLINE or token[4][token[3][1]:].lstrip() == "\\\n" + + +def is_multiline_string(token: tokenize.TokenInfo) -> bool: + """Check if this is a multiline string.""" + return token.type in {FSTRING_END, TSTRING_END} or ( + token.type == tokenize.STRING and "\n" in token.string + ) + + +def token_is_newline(token: tokenize.TokenInfo) -> bool: + """Check if the token type is a newline token type.""" + return token[0] in NEWLINE + + +def count_parentheses(current_parentheses_count: int, token_text: str) -> int: + """Count the number of parentheses.""" + if token_text in "([{": # nosec + return current_parentheses_count + 1 + elif token_text in "}])": # nosec + return current_parentheses_count - 1 + return current_parentheses_count + + +def expand_indent(line: str) -> int: + r"""Return the amount of indentation. + + Tabs are expanded to the next multiple of 8. + + >>> expand_indent(' ') + 4 + >>> expand_indent('\t') + 8 + >>> expand_indent(' \t') + 8 + >>> expand_indent(' \t') + 16 + """ + return len(line.expandtabs(8)) + + +# NOTE(sigmavirus24): This was taken wholesale from +# https://github.com/PyCQA/pycodestyle. The in-line comments were edited to be +# more descriptive. +def mutate_string(text: str) -> str: + """Replace contents with 'xxx' to prevent syntax matching. + + >>> mutate_string('"abc"') + '"xxx"' + >>> mutate_string("'''abc'''") + "'''xxx'''" + >>> mutate_string("r'abc'") + "r'xxx'" + """ + # NOTE(sigmavirus24): If there are string modifiers (e.g., b, u, r) + # use the last "character" to determine if we're using single or double + # quotes and then find the first instance of it + start = text.index(text[-1]) + 1 + end = len(text) - 1 + # Check for triple-quoted strings + if text[-3:] in ('"""', "'''"): + start += 2 + end -= 2 + return text[:start] + "x" * (end - start) + text[end:] diff --git a/src/flake8-main/flake8-main/src/flake8/statistics.py b/src/flake8-main/flake8-main/src/flake8/statistics.py new file mode 100644 index 0000000..b30e4c7 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/statistics.py @@ -0,0 +1,131 @@ +"""Statistic collection logic for Flake8.""" +from __future__ import annotations + +from collections.abc import Generator +from typing import NamedTuple + +from flake8.violation import Violation + + +class Statistics: + """Manager of aggregated statistics for a run of Flake8.""" + + def __init__(self) -> None: + """Initialize the underlying dictionary for our statistics.""" + self._store: dict[Key, Statistic] = {} + + def error_codes(self) -> list[str]: + """Return all unique error codes stored. + + :returns: + Sorted list of error codes. + """ + return sorted({key.code for key in self._store}) + + def record(self, error: Violation) -> None: + """Add the fact that the error was seen in the file. + + :param error: + The Violation instance containing the information about the + violation. + """ + key = Key.create_from(error) + if key not in self._store: + self._store[key] = Statistic.create_from(error) + self._store[key].increment() + + def statistics_for( + self, prefix: str, filename: str | None = None, + ) -> Generator[Statistic]: + """Generate statistics for the prefix and filename. + + If you have a :class:`Statistics` object that has recorded errors, + you can generate the statistics for a prefix (e.g., ``E``, ``E1``, + ``W50``, ``W503``) with the optional filter of a filename as well. + + .. code-block:: python + + >>> stats = Statistics() + >>> stats.statistics_for('E12', + filename='src/flake8/statistics.py') + + >>> stats.statistics_for('W') + + + :param prefix: + The error class or specific error code to find statistics for. + :param filename: + (Optional) The filename to further filter results by. + :returns: + Generator of instances of :class:`Statistic` + """ + matching_errors = sorted( + key for key in self._store if key.matches(prefix, filename) + ) + for error_code in matching_errors: + yield self._store[error_code] + + +class Key(NamedTuple): + """Simple key structure for the Statistics dictionary. + + To make things clearer, easier to read, and more understandable, we use a + namedtuple here for all Keys in the underlying dictionary for the + Statistics object. + """ + + filename: str + code: str + + @classmethod + def create_from(cls, error: Violation) -> Key: + """Create a Key from :class:`flake8.violation.Violation`.""" + return cls(filename=error.filename, code=error.code) + + def matches(self, prefix: str, filename: str | None) -> bool: + """Determine if this key matches some constraints. + + :param prefix: + The error code prefix that this key's error code should start with. + :param filename: + The filename that we potentially want to match on. This can be + None to only match on error prefix. + :returns: + True if the Key's code starts with the prefix and either filename + is None, or the Key's filename matches the value passed in. + """ + return self.code.startswith(prefix) and ( + filename is None or self.filename == filename + ) + + +class Statistic: + """Simple wrapper around the logic of each statistic. + + Instead of maintaining a simple but potentially hard to reason about + tuple, we create a class which has attributes and a couple + convenience methods on it. + """ + + def __init__( + self, error_code: str, filename: str, message: str, count: int, + ) -> None: + """Initialize our Statistic.""" + self.error_code = error_code + self.filename = filename + self.message = message + self.count = count + + @classmethod + def create_from(cls, error: Violation) -> Statistic: + """Create a Statistic from a :class:`flake8.violation.Violation`.""" + return cls( + error_code=error.code, + filename=error.filename, + message=error.text, + count=0, + ) + + def increment(self) -> None: + """Increment the number of times we've seen this error in this file.""" + self.count += 1 diff --git a/src/flake8-main/flake8-main/src/flake8/style_guide.py b/src/flake8-main/flake8-main/src/flake8/style_guide.py new file mode 100644 index 0000000..d675df7 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/style_guide.py @@ -0,0 +1,425 @@ +"""Implementation of the StyleGuide used by Flake8.""" +from __future__ import annotations + +import argparse +import contextlib +import copy +import enum +import functools +import logging +from collections.abc import Generator +from collections.abc import Sequence + +from flake8 import defaults +from flake8 import statistics +from flake8 import utils +from flake8.formatting import base as base_formatter +from flake8.violation import Violation + +__all__ = ("StyleGuide",) + +LOG = logging.getLogger(__name__) + + +class Selected(enum.Enum): + """Enum representing an explicitly or implicitly selected code.""" + + Explicitly = "explicitly selected" + Implicitly = "implicitly selected" + + +class Ignored(enum.Enum): + """Enum representing an explicitly or implicitly ignored code.""" + + Explicitly = "explicitly ignored" + Implicitly = "implicitly ignored" + + +class Decision(enum.Enum): + """Enum representing whether a code should be ignored or selected.""" + + Ignored = "ignored error" + Selected = "selected error" + + +def _explicitly_chosen( + *, + option: list[str] | None, + extend: list[str] | None, +) -> tuple[str, ...]: + ret = [*(option or []), *(extend or [])] + return tuple(sorted(ret, reverse=True)) + + +def _select_ignore( + *, + option: list[str] | None, + default: tuple[str, ...], + extended_default: list[str], + extend: list[str] | None, +) -> tuple[str, ...]: + # option was explicitly set, ignore the default and extended default + if option is not None: + ret = [*option, *(extend or [])] + else: + ret = [*default, *extended_default, *(extend or [])] + return tuple(sorted(ret, reverse=True)) + + +class DecisionEngine: + """A class for managing the decision process around violations. + + This contains the logic for whether a violation should be reported or + ignored. + """ + + def __init__(self, options: argparse.Namespace) -> None: + """Initialize the engine.""" + self.cache: dict[str, Decision] = {} + + self.selected_explicitly = _explicitly_chosen( + option=options.select, + extend=options.extend_select, + ) + self.ignored_explicitly = _explicitly_chosen( + option=options.ignore, + extend=options.extend_ignore, + ) + + self.selected = _select_ignore( + option=options.select, + default=(), + extended_default=options.extended_default_select, + extend=options.extend_select, + ) + self.ignored = _select_ignore( + option=options.ignore, + default=defaults.IGNORE, + extended_default=options.extended_default_ignore, + extend=options.extend_ignore, + ) + + def was_selected(self, code: str) -> Selected | Ignored: + """Determine if the code has been selected by the user. + + :param code: The code for the check that has been run. + :returns: + Selected.Implicitly if the selected list is empty, + Selected.Explicitly if the selected list is not empty and a match + was found, + Ignored.Implicitly if the selected list is not empty but no match + was found. + """ + if code.startswith(self.selected_explicitly): + return Selected.Explicitly + elif code.startswith(self.selected): + return Selected.Implicitly + else: + return Ignored.Implicitly + + def was_ignored(self, code: str) -> Selected | Ignored: + """Determine if the code has been ignored by the user. + + :param code: + The code for the check that has been run. + :returns: + Selected.Implicitly if the ignored list is empty, + Ignored.Explicitly if the ignored list is not empty and a match was + found, + Selected.Implicitly if the ignored list is not empty but no match + was found. + """ + if code.startswith(self.ignored_explicitly): + return Ignored.Explicitly + elif code.startswith(self.ignored): + return Ignored.Implicitly + else: + return Selected.Implicitly + + def make_decision(self, code: str) -> Decision: + """Decide if code should be ignored or selected.""" + selected = self.was_selected(code) + ignored = self.was_ignored(code) + LOG.debug( + "The user configured %r to be %r, %r", + code, + selected, + ignored, + ) + + if isinstance(selected, Selected) and isinstance(ignored, Selected): + return Decision.Selected + elif isinstance(selected, Ignored) and isinstance(ignored, Ignored): + return Decision.Ignored + elif ( + selected is Selected.Explicitly + and ignored is not Ignored.Explicitly + ): + return Decision.Selected + elif ( + selected is not Selected.Explicitly + and ignored is Ignored.Explicitly + ): + return Decision.Ignored + elif selected is Ignored.Implicitly and ignored is Selected.Implicitly: + return Decision.Ignored + elif ( + selected is Selected.Explicitly and ignored is Ignored.Explicitly + ) or ( + selected is Selected.Implicitly and ignored is Ignored.Implicitly + ): + # we only get here if it was in both lists: longest prefix wins + select = next(s for s in self.selected if code.startswith(s)) + ignore = next(s for s in self.ignored if code.startswith(s)) + if len(select) > len(ignore): + return Decision.Selected + else: + return Decision.Ignored + else: + raise AssertionError(f"unreachable {code} {selected} {ignored}") + + def decision_for(self, code: str) -> Decision: + """Return the decision for a specific code. + + This method caches the decisions for codes to avoid retracing the same + logic over and over again. We only care about the select and ignore + rules as specified by the user in their configuration files and + command-line flags. + + This method does not look at whether the specific line is being + ignored in the file itself. + + :param code: The code for the check that has been run. + """ + decision = self.cache.get(code) + if decision is None: + decision = self.make_decision(code) + self.cache[code] = decision + LOG.debug('"%s" will be "%s"', code, decision) + return decision + + +class StyleGuideManager: + """Manage multiple style guides for a single run.""" + + def __init__( + self, + options: argparse.Namespace, + formatter: base_formatter.BaseFormatter, + decider: DecisionEngine | None = None, + ) -> None: + """Initialize our StyleGuide. + + .. todo:: Add parameter documentation. + """ + self.options = options + self.formatter = formatter + self.stats = statistics.Statistics() + self.decider = decider or DecisionEngine(options) + self.style_guides: list[StyleGuide] = [] + self.default_style_guide = StyleGuide( + options, formatter, self.stats, decider=decider, + ) + self.style_guides = [ + self.default_style_guide, + *self.populate_style_guides_with(options), + ] + + self.style_guide_for = functools.cache(self._style_guide_for) + + def populate_style_guides_with( + self, options: argparse.Namespace, + ) -> Generator[StyleGuide]: + """Generate style guides from the per-file-ignores option. + + :param options: + The original options parsed from the CLI and config file. + :returns: + A copy of the default style guide with overridden values. + """ + per_file = utils.parse_files_to_codes_mapping(options.per_file_ignores) + for filename, violations in per_file: + yield self.default_style_guide.copy( + filename=filename, extend_ignore_with=violations, + ) + + def _style_guide_for(self, filename: str) -> StyleGuide: + """Find the StyleGuide for the filename in particular.""" + return max( + (g for g in self.style_guides if g.applies_to(filename)), + key=lambda g: len(g.filename or ""), + ) + + @contextlib.contextmanager + def processing_file(self, filename: str) -> Generator[StyleGuide]: + """Record the fact that we're processing the file's results.""" + guide = self.style_guide_for(filename) + with guide.processing_file(filename): + yield guide + + def handle_error( + self, + code: str, + filename: str, + line_number: int, + column_number: int, + text: str, + physical_line: str | None = None, + ) -> int: + """Handle an error reported by a check. + + :param code: + The error code found, e.g., E123. + :param filename: + The file in which the error was found. + :param line_number: + The line number (where counting starts at 1) at which the error + occurs. + :param column_number: + The column number (where counting starts at 1) at which the error + occurs. + :param text: + The text of the error message. + :param physical_line: + The actual physical line causing the error. + :returns: + 1 if the error was reported. 0 if it was ignored. This is to allow + for counting of the number of errors found that were not ignored. + """ + guide = self.style_guide_for(filename) + return guide.handle_error( + code, filename, line_number, column_number, text, physical_line, + ) + + +class StyleGuide: + """Manage a Flake8 user's style guide.""" + + def __init__( + self, + options: argparse.Namespace, + formatter: base_formatter.BaseFormatter, + stats: statistics.Statistics, + filename: str | None = None, + decider: DecisionEngine | None = None, + ): + """Initialize our StyleGuide. + + .. todo:: Add parameter documentation. + """ + self.options = options + self.formatter = formatter + self.stats = stats + self.decider = decider or DecisionEngine(options) + self.filename = filename + if self.filename: + self.filename = utils.normalize_path(self.filename) + + def __repr__(self) -> str: + """Make it easier to debug which StyleGuide we're using.""" + return f"" + + def copy( + self, + filename: str | None = None, + extend_ignore_with: Sequence[str] | None = None, + ) -> StyleGuide: + """Create a copy of this style guide with different values.""" + filename = filename or self.filename + options = copy.deepcopy(self.options) + options.extend_ignore = options.extend_ignore or [] + options.extend_ignore.extend(extend_ignore_with or []) + return StyleGuide( + options, self.formatter, self.stats, filename=filename, + ) + + @contextlib.contextmanager + def processing_file(self, filename: str) -> Generator[StyleGuide]: + """Record the fact that we're processing the file's results.""" + self.formatter.beginning(filename) + yield self + self.formatter.finished(filename) + + def applies_to(self, filename: str) -> bool: + """Check if this StyleGuide applies to the file. + + :param filename: + The name of the file with violations that we're potentially + applying this StyleGuide to. + :returns: + True if this applies, False otherwise + """ + if self.filename is None: + return True + return utils.matches_filename( + filename, + patterns=[self.filename], + log_message=f'{self!r} does %(whether)smatch "%(path)s"', + logger=LOG, + ) + + def should_report_error(self, code: str) -> Decision: + """Determine if the error code should be reported or ignored. + + This method only cares about the select and ignore rules as specified + by the user in their configuration files and command-line flags. + + This method does not look at whether the specific line is being + ignored in the file itself. + + :param code: + The code for the check that has been run. + """ + return self.decider.decision_for(code) + + def handle_error( + self, + code: str, + filename: str, + line_number: int, + column_number: int, + text: str, + physical_line: str | None = None, + ) -> int: + """Handle an error reported by a check. + + :param code: + The error code found, e.g., E123. + :param filename: + The file in which the error was found. + :param line_number: + The line number (where counting starts at 1) at which the error + occurs. + :param column_number: + The column number (where counting starts at 1) at which the error + occurs. + :param text: + The text of the error message. + :param physical_line: + The actual physical line causing the error. + :returns: + 1 if the error was reported. 0 if it was ignored. This is to allow + for counting of the number of errors found that were not ignored. + """ + disable_noqa = self.options.disable_noqa + # NOTE(sigmavirus24): Apparently we're provided with 0-indexed column + # numbers so we have to offset that here. + if not column_number: + column_number = 0 + error = Violation( + code, + filename, + line_number, + column_number + 1, + text, + physical_line, + ) + error_is_selected = ( + self.should_report_error(error.code) is Decision.Selected + ) + is_not_inline_ignored = error.is_inline_ignored(disable_noqa) is False + if error_is_selected and is_not_inline_ignored: + self.formatter.handle(error) + self.stats.record(error) + return 1 + return 0 diff --git a/src/flake8-main/flake8-main/src/flake8/utils.py b/src/flake8-main/flake8-main/src/flake8/utils.py new file mode 100644 index 0000000..e5c086e --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/utils.py @@ -0,0 +1,280 @@ +"""Utility methods for flake8.""" +from __future__ import annotations + +import fnmatch as _fnmatch +import functools +import io +import logging +import os +import platform +import re +import sys +import textwrap +import tokenize +from collections.abc import Sequence +from re import Pattern +from typing import NamedTuple + +from flake8 import exceptions + +COMMA_SEPARATED_LIST_RE = re.compile(r"[,\s]") +LOCAL_PLUGIN_LIST_RE = re.compile(r"[,\t\n\r\f\v]") +NORMALIZE_PACKAGE_NAME_RE = re.compile(r"[-_.]+") + + +def parse_comma_separated_list( + value: str, regexp: Pattern[str] = COMMA_SEPARATED_LIST_RE, +) -> list[str]: + """Parse a comma-separated list. + + :param value: + String to be parsed and normalized. + :param regexp: + Compiled regular expression used to split the value when it is a + string. + :returns: + List of values with whitespace stripped. + """ + assert isinstance(value, str), value + + separated = regexp.split(value) + item_gen = (item.strip() for item in separated) + return [item for item in item_gen if item] + + +class _Token(NamedTuple): + tp: str + src: str + + +_CODE, _FILE, _COLON, _COMMA, _WS = "code", "file", "colon", "comma", "ws" +_EOF = "eof" +_FILE_LIST_TOKEN_TYPES = [ + (re.compile(r"[A-Z]+[0-9]*(?=$|\s|,)"), _CODE), + (re.compile(r"[^\s:,]+"), _FILE), + (re.compile(r"\s*:\s*"), _COLON), + (re.compile(r"\s*,\s*"), _COMMA), + (re.compile(r"\s+"), _WS), +] + + +def _tokenize_files_to_codes_mapping(value: str) -> list[_Token]: + tokens = [] + i = 0 + while i < len(value): + for token_re, token_name in _FILE_LIST_TOKEN_TYPES: + match = token_re.match(value, i) + if match: + tokens.append(_Token(token_name, match.group().strip())) + i = match.end() + break + else: + raise AssertionError("unreachable", value, i) + tokens.append(_Token(_EOF, "")) + + return tokens + + +def parse_files_to_codes_mapping( # noqa: C901 + value_: Sequence[str] | str, +) -> list[tuple[str, list[str]]]: + """Parse a files-to-codes mapping. + + A files-to-codes mapping a sequence of values specified as + `filenames list:codes list ...`. Each of the lists may be separated by + either comma or whitespace tokens. + + :param value: String to be parsed and normalized. + """ + if not isinstance(value_, str): + value = "\n".join(value_) + else: + value = value_ + + ret: list[tuple[str, list[str]]] = [] + if not value.strip(): + return ret + + class State: + seen_sep = True + seen_colon = False + filenames: list[str] = [] + codes: list[str] = [] + + def _reset() -> None: + if State.codes: + for filename in State.filenames: + ret.append((filename, State.codes)) + State.seen_sep = True + State.seen_colon = False + State.filenames = [] + State.codes = [] + + def _unexpected_token() -> exceptions.ExecutionError: + return exceptions.ExecutionError( + f"Expected `per-file-ignores` to be a mapping from file exclude " + f"patterns to ignore codes.\n\n" + f"Configured `per-file-ignores` setting:\n\n" + f"{textwrap.indent(value.strip(), ' ')}", + ) + + for token in _tokenize_files_to_codes_mapping(value): + # legal in any state: separator sets the sep bit + if token.tp in {_COMMA, _WS}: + State.seen_sep = True + # looking for filenames + elif not State.seen_colon: + if token.tp == _COLON: + State.seen_colon = True + State.seen_sep = True + elif State.seen_sep and token.tp == _FILE: + State.filenames.append(token.src) + State.seen_sep = False + else: + raise _unexpected_token() + # looking for codes + else: + if token.tp == _EOF: + _reset() + elif State.seen_sep and token.tp == _CODE: + State.codes.append(token.src) + State.seen_sep = False + elif State.seen_sep and token.tp == _FILE: + _reset() + State.filenames.append(token.src) + State.seen_sep = False + else: + raise _unexpected_token() + + return ret + + +def normalize_paths( + paths: Sequence[str], parent: str = os.curdir, +) -> list[str]: + """Normalize a list of paths relative to a parent directory. + + :returns: + The normalized paths. + """ + assert isinstance(paths, list), paths + return [normalize_path(p, parent) for p in paths] + + +def normalize_path(path: str, parent: str = os.curdir) -> str: + """Normalize a single-path. + + :returns: + The normalized path. + """ + # NOTE(sigmavirus24): Using os.path.sep and os.path.altsep allow for + # Windows compatibility with both Windows-style paths (c:\foo\bar) and + # Unix style paths (/foo/bar). + separator = os.path.sep + # NOTE(sigmavirus24): os.path.altsep may be None + alternate_separator = os.path.altsep or "" + if ( + path == "." + or separator in path + or (alternate_separator and alternate_separator in path) + ): + path = os.path.abspath(os.path.join(parent, path)) + return path.rstrip(separator + alternate_separator) + + +@functools.lru_cache(maxsize=1) +def stdin_get_value() -> str: + """Get and cache it so plugins can use it.""" + stdin_value = sys.stdin.buffer.read() + fd = io.BytesIO(stdin_value) + try: + coding, _ = tokenize.detect_encoding(fd.readline) + fd.seek(0) + return io.TextIOWrapper(fd, coding).read() + except (LookupError, SyntaxError, UnicodeError): + return stdin_value.decode("utf-8") + + +def stdin_get_lines() -> list[str]: + """Return lines of stdin split according to file splitting.""" + return list(io.StringIO(stdin_get_value())) + + +def is_using_stdin(paths: list[str]) -> bool: + """Determine if we're going to read from stdin. + + :param paths: + The paths that we're going to check. + :returns: + True if stdin (-) is in the path, otherwise False + """ + return "-" in paths + + +def fnmatch(filename: str, patterns: Sequence[str]) -> bool: + """Wrap :func:`fnmatch.fnmatch` to add some functionality. + + :param filename: + Name of the file we're trying to match. + :param patterns: + Patterns we're using to try to match the filename. + :param default: + The default value if patterns is empty + :returns: + True if a pattern matches the filename, False if it doesn't. + ``True`` if patterns is empty. + """ + if not patterns: + return True + return any(_fnmatch.fnmatch(filename, pattern) for pattern in patterns) + + +def matches_filename( + path: str, + patterns: Sequence[str], + log_message: str, + logger: logging.Logger, +) -> bool: + """Use fnmatch to discern if a path exists in patterns. + + :param path: + The path to the file under question + :param patterns: + The patterns to match the path against. + :param log_message: + The message used for logging purposes. + :returns: + True if path matches patterns, False otherwise + """ + if not patterns: + return False + basename = os.path.basename(path) + if basename not in {".", ".."} and fnmatch(basename, patterns): + logger.debug(log_message, {"path": basename, "whether": ""}) + return True + + absolute_path = os.path.abspath(path) + match = fnmatch(absolute_path, patterns) + logger.debug( + log_message, + {"path": absolute_path, "whether": "" if match else "not "}, + ) + return match + + +def get_python_version() -> str: + """Find and format the python implementation and version. + + :returns: + Implementation name, version, and platform as a string. + """ + return "{} {} on {}".format( + platform.python_implementation(), + platform.python_version(), + platform.system(), + ) + + +def normalize_pypi_name(s: str) -> str: + """Normalize a distribution name according to PEP 503.""" + return NORMALIZE_PACKAGE_NAME_RE.sub("-", s).lower() diff --git a/src/flake8-main/flake8-main/src/flake8/violation.py b/src/flake8-main/flake8-main/src/flake8/violation.py new file mode 100644 index 0000000..8535178 --- /dev/null +++ b/src/flake8-main/flake8-main/src/flake8/violation.py @@ -0,0 +1,69 @@ +"""Contains the Violation error class used internally.""" +from __future__ import annotations + +import functools +import linecache +import logging +from re import Match +from typing import NamedTuple + +from flake8 import defaults +from flake8 import utils + + +LOG = logging.getLogger(__name__) + + +@functools.lru_cache(maxsize=512) +def _find_noqa(physical_line: str) -> Match[str] | None: + return defaults.NOQA_INLINE_REGEXP.search(physical_line) + + +class Violation(NamedTuple): + """Class representing a violation reported by Flake8.""" + + code: str + filename: str + line_number: int + column_number: int + text: str + physical_line: str | None + + def is_inline_ignored(self, disable_noqa: bool) -> bool: + """Determine if a comment has been added to ignore this line. + + :param disable_noqa: + Whether or not users have provided ``--disable-noqa``. + :returns: + True if error is ignored in-line, False otherwise. + """ + physical_line = self.physical_line + # TODO(sigmavirus24): Determine how to handle stdin with linecache + if disable_noqa: + return False + + if physical_line is None: + physical_line = linecache.getline(self.filename, self.line_number) + noqa_match = _find_noqa(physical_line) + if noqa_match is None: + LOG.debug("%r is not inline ignored", self) + return False + + codes_str = noqa_match.groupdict()["codes"] + if codes_str is None: + LOG.debug("%r is ignored by a blanket ``# noqa``", self) + return True + + codes = set(utils.parse_comma_separated_list(codes_str)) + if self.code in codes or self.code.startswith(tuple(codes)): + LOG.debug( + "%r is ignored specifically inline with ``# noqa: %s``", + self, + codes_str, + ) + return True + + LOG.debug( + "%r is not ignored inline with ``# noqa: %s``", self, codes_str, + ) + return False diff --git a/src/flake8-main/flake8-main/tests/__init__.py b/src/flake8-main/flake8-main/tests/__init__.py new file mode 100644 index 0000000..ee1f2a0 --- /dev/null +++ b/src/flake8-main/flake8-main/tests/__init__.py @@ -0,0 +1,2 @@ +"""This is here because mypy doesn't understand PEP 420.""" +from __future__ import annotations diff --git a/src/flake8-main/flake8-main/tests/conftest.py b/src/flake8-main/flake8-main/tests/conftest.py new file mode 100644 index 0000000..ac413fb --- /dev/null +++ b/src/flake8-main/flake8-main/tests/conftest.py @@ -0,0 +1,8 @@ +"""Test configuration for py.test.""" +from __future__ import annotations + +import sys + +import flake8 + +flake8.configure_logging(2, "test-logs-%s.%s.log" % sys.version_info[0:2]) diff --git a/src/flake8-main/flake8-main/tests/integration/__init__.py b/src/flake8-main/flake8-main/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/flake8-main/flake8-main/tests/integration/subdir/__init__.py b/src/flake8-main/flake8-main/tests/integration/subdir/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/flake8-main/flake8-main/tests/integration/subdir/aplugin.py b/src/flake8-main/flake8-main/tests/integration/subdir/aplugin.py new file mode 100644 index 0000000..97b06a9 --- /dev/null +++ b/src/flake8-main/flake8-main/tests/integration/subdir/aplugin.py @@ -0,0 +1,12 @@ +"""Module that is off sys.path by default, for testing local-plugin-paths.""" +from __future__ import annotations + + +class ExtensionTestPlugin2: + """Extension test plugin in its own directory.""" + + def __init__(self, tree): + """Construct an instance of test plugin.""" + + def run(self): + """Do nothing.""" diff --git a/src/flake8-main/flake8-main/tests/integration/test_aggregator.py b/src/flake8-main/flake8-main/tests/integration/test_aggregator.py new file mode 100644 index 0000000..006ac5f --- /dev/null +++ b/src/flake8-main/flake8-main/tests/integration/test_aggregator.py @@ -0,0 +1,83 @@ +"""Test aggregation of config files and command-line options.""" +from __future__ import annotations + +import os + +import pytest + +from flake8.main import options +from flake8.options import aggregator +from flake8.options import config +from flake8.options import manager + + +@pytest.fixture +def optmanager(): + """Create a new OptionManager.""" + option_manager = manager.OptionManager( + version="3.0.0", + plugin_versions="", + parents=[], + formatter_names=[], + ) + options.register_default_options(option_manager) + return option_manager + + +@pytest.fixture +def flake8_config(tmp_path): + cfg_s = """\ +[flake8] +ignore = + E123, + W234, + E111 +exclude = + foo/, + bar/, + bogus/ +quiet = 1 +""" + cfg = tmp_path.joinpath("tox.ini") + cfg.write_text(cfg_s) + return str(cfg) + + +def test_aggregate_options_with_config(optmanager, flake8_config): + """Verify we aggregate options and config values appropriately.""" + arguments = [ + "flake8", + "--select", + "E11,E34,E402,W,F", + "--exclude", + "tests/*", + ] + cfg, cfg_dir = config.load_config(flake8_config, []) + options = aggregator.aggregate_options( + optmanager, + cfg, + cfg_dir, + arguments, + ) + + assert options.select == ["E11", "E34", "E402", "W", "F"] + assert options.ignore == ["E123", "W234", "E111"] + assert options.exclude == [os.path.abspath("tests/*")] + + +def test_aggregate_options_when_isolated(optmanager, flake8_config): + """Verify we aggregate options and config values appropriately.""" + arguments = [ + "flake8", + "--select", + "E11,E34,E402,W,F", + "--exclude", + "tests/*", + ] + cfg, cfg_dir = config.load_config(flake8_config, [], isolated=True) + optmanager.extend_default_ignore(["E8"]) + options = aggregator.aggregate_options(optmanager, cfg, cfg_dir, arguments) + + assert options.select == ["E11", "E34", "E402", "W", "F"] + assert options.ignore is None + assert options.exclude == [os.path.abspath("tests/*")] diff --git a/src/flake8-main/flake8-main/tests/integration/test_api_legacy.py b/src/flake8-main/flake8-main/tests/integration/test_api_legacy.py new file mode 100644 index 0000000..b386bd5 --- /dev/null +++ b/src/flake8-main/flake8-main/tests/integration/test_api_legacy.py @@ -0,0 +1,15 @@ +"""Integration tests for the legacy api.""" +from __future__ import annotations + +from flake8.api import legacy + + +def test_legacy_api(tmpdir): + """A basic end-to-end test for the legacy api reporting errors.""" + with tmpdir.as_cwd(): + t_py = tmpdir.join("t.py") + t_py.write("import os # unused import\n") + + style_guide = legacy.get_style_guide() + report = style_guide.check_files([t_py.strpath]) + assert report.total_errors == 1 diff --git a/src/flake8-main/flake8-main/tests/integration/test_checker.py b/src/flake8-main/flake8-main/tests/integration/test_checker.py new file mode 100644 index 0000000..60d0132 --- /dev/null +++ b/src/flake8-main/flake8-main/tests/integration/test_checker.py @@ -0,0 +1,342 @@ +"""Integration tests for the checker submodule.""" +from __future__ import annotations + +import importlib.metadata +import sys +from unittest import mock + +import pytest + +from flake8 import checker +from flake8.plugins import finder +from flake8.processor import FileProcessor + +PHYSICAL_LINE = "# Physical line content" + +EXPECTED_REPORT = (1, 1, "T000 Expected Message") +EXPECTED_REPORT_PHYSICAL_LINE = (1, "T000 Expected Message") +EXPECTED_RESULT_PHYSICAL_LINE = ("T000", 0, 1, "Expected Message", None) + + +class PluginClass: + """Simple file plugin class yielding the expected report.""" + + def __init__(self, tree): + """Construct a dummy object to provide mandatory parameter.""" + pass + + def run(self): + """Run class yielding one element containing the expected report.""" + yield EXPECTED_REPORT + (type(self),) + + +def plugin_func_gen(tree): + """Yield the expected report.""" + yield EXPECTED_REPORT + (type(plugin_func_gen),) + + +def plugin_func_list(tree): + """Return a list of expected reports.""" + return [EXPECTED_REPORT + (type(plugin_func_list),)] + + +def plugin_func_physical_ret(physical_line): + """Expect report from a physical_line. Single return.""" + return EXPECTED_REPORT_PHYSICAL_LINE + + +def plugin_func_physical_none(physical_line): + """Expect report from a physical_line. No results.""" + return None + + +def plugin_func_physical_list_single(physical_line): + """Expect report from a physical_line. List of single result.""" + return [EXPECTED_REPORT_PHYSICAL_LINE] + + +def plugin_func_physical_list_multiple(physical_line): + """Expect report from a physical_line. List of multiple results.""" + return [EXPECTED_REPORT_PHYSICAL_LINE] * 2 + + +def plugin_func_physical_gen_single(physical_line): + """Expect report from a physical_line. Generator of single result.""" + yield EXPECTED_REPORT_PHYSICAL_LINE + + +def plugin_func_physical_gen_multiple(physical_line): + """Expect report from a physical_line. Generator of multiple results.""" + for _ in range(3): + yield EXPECTED_REPORT_PHYSICAL_LINE + + +def plugin_func_out_of_bounds(logical_line): + """This produces an error out of bounds.""" + yield 10000, "L100 test" + + +def mock_file_checker_with_plugin(plugin_target): + """Get a mock FileChecker class with plugin_target registered. + + Useful as a starting point for mocking reports/results. + """ + to_load = [ + finder.Plugin( + "flake-package", + "9001", + importlib.metadata.EntryPoint( + "Q", + f"{plugin_target.__module__}:{plugin_target.__name__}", + "flake8.extension", + ), + ), + ] + opts = finder.PluginOptions.blank() + plugins = finder.load_plugins(to_load, opts) + + # Prevent it from reading lines from stdin or somewhere else + with mock.patch( + "flake8.processor.FileProcessor.read_lines", return_value=["Line 1"], + ): + file_checker = checker.FileChecker( + filename="-", + plugins=plugins.checkers, + options=mock.MagicMock(), + ) + return file_checker + + +@pytest.mark.parametrize( + "plugin_target", + [ + PluginClass, + plugin_func_gen, + plugin_func_list, + ], +) +def test_handle_file_plugins(plugin_target): + """Test the FileChecker class handling different file plugin types.""" + file_checker = mock_file_checker_with_plugin(plugin_target) + + # Do not actually build an AST + file_checker.processor.build_ast = lambda: True + + # Forward reports to this mock + report = mock.Mock() + file_checker.report = report + file_checker.run_ast_checks() + report.assert_called_once_with( + error_code=None, + line_number=EXPECTED_REPORT[0], + column=EXPECTED_REPORT[1], + text=EXPECTED_REPORT[2], + ) + + +@pytest.mark.parametrize( + "plugin_target,len_results", + [ + (plugin_func_physical_ret, 1), + (plugin_func_physical_none, 0), + (plugin_func_physical_list_single, 1), + (plugin_func_physical_list_multiple, 2), + (plugin_func_physical_gen_single, 1), + (plugin_func_physical_gen_multiple, 3), + ], +) +def test_line_check_results(plugin_target, len_results): + """Test the FileChecker class handling results from line checks.""" + file_checker = mock_file_checker_with_plugin(plugin_target) + + # Results will be stored in an internal array + file_checker.run_physical_checks(PHYSICAL_LINE) + expected = [EXPECTED_RESULT_PHYSICAL_LINE] * len_results + assert file_checker.results == expected + + +def test_logical_line_offset_out_of_bounds(): + """Ensure that logical line offsets that are out of bounds do not crash.""" + + file_checker = mock_file_checker_with_plugin(plugin_func_out_of_bounds) + + logical_ret = ( + "", + 'print("xxxxxxxxxxx")', + [(0, (1, 0)), (5, (1, 5)), (6, (1, 6)), (19, (1, 19)), (20, (1, 20))], + ) + with mock.patch.object( + FileProcessor, + "build_logical_line", + return_value=logical_ret, + ): + file_checker.run_logical_checks() + assert file_checker.results == [("L100", 0, 0, "test", None)] + + +PLACEHOLDER_CODE = 'some_line = "of" * code' + + +@pytest.mark.parametrize( + "results, expected_order", + [ + # No entries should be added + ([], []), + # Results are correctly ordered + ( + [ + ("A101", 1, 1, "placeholder error", PLACEHOLDER_CODE), + ("A101", 2, 1, "placeholder error", PLACEHOLDER_CODE), + ], + [0, 1], + ), + # Reversed order of lines + ( + [ + ("A101", 2, 1, "placeholder error", PLACEHOLDER_CODE), + ("A101", 1, 1, "placeholder error", PLACEHOLDER_CODE), + ], + [1, 0], + ), + # Columns are not ordered correctly + # (when reports are ordered correctly) + ( + [ + ("A101", 1, 2, "placeholder error", PLACEHOLDER_CODE), + ("A101", 1, 1, "placeholder error", PLACEHOLDER_CODE), + ("A101", 2, 1, "placeholder error", PLACEHOLDER_CODE), + ], + [1, 0, 2], + ), + ( + [ + ("A101", 2, 1, "placeholder error", PLACEHOLDER_CODE), + ("A101", 1, 1, "placeholder error", PLACEHOLDER_CODE), + ("A101", 1, 2, "placeholder error", PLACEHOLDER_CODE), + ], + [1, 2, 0], + ), + ( + [ + ("A101", 1, 2, "placeholder error", PLACEHOLDER_CODE), + ("A101", 2, 2, "placeholder error", PLACEHOLDER_CODE), + ("A101", 2, 1, "placeholder error", PLACEHOLDER_CODE), + ], + [0, 2, 1], + ), + ( + [ + ("A101", 1, 3, "placeholder error", PLACEHOLDER_CODE), + ("A101", 2, 2, "placeholder error", PLACEHOLDER_CODE), + ("A101", 3, 1, "placeholder error", PLACEHOLDER_CODE), + ], + [0, 1, 2], + ), + ( + [ + ("A101", 1, 1, "placeholder error", PLACEHOLDER_CODE), + ("A101", 1, 3, "placeholder error", PLACEHOLDER_CODE), + ("A101", 2, 2, "placeholder error", PLACEHOLDER_CODE), + ], + [0, 1, 2], + ), + # Previously sort column and message (so reversed) (see bug 196) + ( + [ + ("A101", 1, 1, "placeholder error", PLACEHOLDER_CODE), + ("A101", 2, 1, "charlie error", PLACEHOLDER_CODE), + ], + [0, 1], + ), + ], +) +def test_report_order(results, expected_order): + """ + Test in which order the results will be reported. + + It gets a list of reports from the file checkers and verifies that the + result will be ordered independent from the original report. + """ + + def count_side_effect(name, sorted_results): + """Side effect for the result handler to tell all are reported.""" + return len(sorted_results) + + # To simplify the parameters (and prevent copy & pasting) reuse report + # tuples to create the expected result lists from the indexes + expected_results = [results[index] for index in expected_order] + + style_guide = mock.MagicMock(spec=["options", "processing_file"]) + + # Create a placeholder manager without arguments or plugins + # Just add one custom file checker which just provides the results + manager = checker.Manager(style_guide, finder.Checkers([], [], []), []) + manager.results = [("placeholder", results, {})] + # _handle_results is the first place which gets the sorted result + # Should something non-private be mocked instead? + handler = mock.Mock(side_effect=count_side_effect) + with mock.patch.object(manager, "_handle_results", handler): + assert manager.report() == (len(results), len(results)) + handler.assert_called_once_with("placeholder", expected_results) + + +def test_acquire_when_multiprocessing_pool_can_initialize(): + """Verify successful importing of hardware semaphore support. + + Mock the behaviour of a platform that has a hardware sem_open + implementation, and then attempt to initialize a multiprocessing + Pool object. + + This simulates the behaviour on most common platforms. + """ + with mock.patch("multiprocessing.Pool") as pool: + result = checker._try_initialize_processpool(2, []) + + pool.assert_called_once_with(2, checker._mp_init, initargs=([],)) + assert result is pool.return_value + + +def test_acquire_when_multiprocessing_pool_can_not_initialize(): + """Verify unsuccessful importing of hardware semaphore support. + + Mock the behaviour of a platform that has not got a hardware sem_open + implementation, and then attempt to initialize a multiprocessing + Pool object. + + This scenario will occur on platforms such as Termux and on some + more exotic devices. + + https://github.com/python/cpython/blob/4e02981de0952f54bf87967f8e10d169d6946b40/Lib/multiprocessing/synchronize.py#L30-L33 + """ + with mock.patch("multiprocessing.Pool", side_effect=ImportError) as pool: + result = checker._try_initialize_processpool(2, []) + + pool.assert_called_once_with(2, checker._mp_init, initargs=([],)) + assert result is None + + +def test_handling_syntaxerrors_across_pythons(): + """Verify we properly handle exception argument tuples. + + Python 3.10 added more information to the SyntaxError parse token tuple. + We need to handle that correctly to avoid crashing. + https://github.com/PyCQA/flake8/issues/1372 + """ + if sys.version_info < (3, 10): # pragma: no cover (<3.10) + # Python 3.9 or older + err = SyntaxError( + "invalid syntax", ("", 2, 5, "bad python:\n"), + ) + expected = (2, 4) + else: # pragma: no cover (3.10+) + err = SyntaxError( + "invalid syntax", ("", 2, 1, "bad python:\n", 2, 11), + ) + expected = (2, 1) + file_checker = checker.FileChecker( + filename="-", + plugins=finder.Checkers([], [], []), + options=mock.MagicMock(), + ) + actual = file_checker._extract_syntax_information(err) + assert actual == expected diff --git a/src/flake8-main/flake8-main/tests/integration/test_main.py b/src/flake8-main/flake8-main/tests/integration/test_main.py new file mode 100644 index 0000000..5091573 --- /dev/null +++ b/src/flake8-main/flake8-main/tests/integration/test_main.py @@ -0,0 +1,400 @@ +"""Integration tests for the main entrypoint of flake8.""" +from __future__ import annotations + +import json +import os +import sys +from unittest import mock + +import pytest + +from flake8 import utils +from flake8.main import cli +from flake8.options import config + + +def test_form_feed_line_split(tmpdir, capsys): + """Test that form feed is treated the same for stdin.""" + src = "x=1\n\f\ny=1\n" + expected_out = """\ +t.py:1:2: E225 missing whitespace around operator +t.py:3:2: E225 missing whitespace around operator +""" + + with tmpdir.as_cwd(): + tmpdir.join("t.py").write(src) + + with mock.patch.object(utils, "stdin_get_value", return_value=src): + assert cli.main(["-", "--stdin-display-name=t.py"]) == 1 + out, err = capsys.readouterr() + assert out == expected_out + assert err == "" + + assert cli.main(["t.py"]) == 1 + out, err = capsys.readouterr() + assert out == expected_out + assert err == "" + + +def test_e101_indent_char_does_not_reset(tmpdir, capsys): + """Ensure that E101 with an existing indent_char does not reset it.""" + t_py_contents = """\ +if True: + print('space indented') + +s = '''\ +\ttab indented +''' # noqa: E101 + +if True: + print('space indented') +""" + + with tmpdir.as_cwd(): + tmpdir.join("t.py").write(t_py_contents) + assert cli.main(["t.py"]) == 0 + + +def test_statistics_option(tmpdir, capsys): + """Ensure that `flake8 --statistics` works.""" + with tmpdir.as_cwd(): + tmpdir.join("t.py").write("import os\nimport sys\n") + assert cli.main(["--statistics", "t.py"]) == 1 + + expected = """\ +t.py:1:1: F401 'os' imported but unused +t.py:2:1: F401 'sys' imported but unused +2 F401 'os' imported but unused +""" + out, err = capsys.readouterr() + assert out == expected + assert err == "" + + +def test_show_source_option(tmpdir, capsys): + """Ensure that --show-source and --no-show-source work.""" + with tmpdir.as_cwd(): + tmpdir.join("tox.ini").write("[flake8]\nshow_source = true\n") + tmpdir.join("t.py").write("import os\n") + assert cli.main(["t.py"]) == 1 + + expected = """\ +t.py:1:1: F401 'os' imported but unused +import os +^ +""" + out, err = capsys.readouterr() + assert out == expected + assert err == "" + + with tmpdir.as_cwd(): + assert cli.main(["t.py", "--no-show-source"]) == 1 + + expected = """\ +t.py:1:1: F401 'os' imported but unused +""" + out, err = capsys.readouterr() + assert out == expected + assert err == "" + + +def test_errors_sorted(tmpdir, capsys): + with tmpdir.as_cwd(): + for c in "abcde": + tmpdir.join(f"{c}.py").write("import os\n") + assert cli.main(["./"]) == 1 + + # file traversal was done in inode-order before + # this uses a significant number of files such that it's unlikely to pass + expected = """\ +./a.py:1:1: F401 'os' imported but unused +./b.py:1:1: F401 'os' imported but unused +./c.py:1:1: F401 'os' imported but unused +./d.py:1:1: F401 'os' imported but unused +./e.py:1:1: F401 'os' imported but unused +""" + out, err = capsys.readouterr() + assert out == expected + assert err == "" + + +def test_extend_exclude(tmpdir, capsys): + """Ensure that `flake8 --extend-exclude` works.""" + for d in ["project", "vendor", "legacy", ".git", ".tox", ".hg"]: + tmpdir.mkdir(d).join("t.py").write("import os\nimport sys\n") + + with tmpdir.as_cwd(): + assert cli.main(["--extend-exclude=vendor,legacy/"]) == 1 + + out, err = capsys.readouterr() + expected_out = """\ +./project/t.py:1:1: F401 'os' imported but unused +./project/t.py:2:1: F401 'sys' imported but unused +""" + assert out == expected_out.replace("/", os.sep) + assert err == "" + + +def test_malformed_per_file_ignores_error(tmpdir, capsys): + """Test the error message for malformed `per-file-ignores`.""" + setup_cfg = """\ +[flake8] +per-file-ignores = + incorrect/* + values/* +""" + expected = """\ +There was a critical error during execution of Flake8: +Expected `per-file-ignores` to be a mapping from file exclude patterns to ignore codes. + +Configured `per-file-ignores` setting: + + incorrect/* + values/* +""" # noqa: E501 + + with tmpdir.as_cwd(): + tmpdir.join("setup.cfg").write(setup_cfg) + assert cli.main(["."]) == 1 + + out, err = capsys.readouterr() + assert out == expected + + +def test_tokenization_error_but_not_syntax_error(tmpdir, capsys): + """Test that flake8 does not crash on tokenization errors.""" + with tmpdir.as_cwd(): + # this is a crash in the tokenizer, but not in the ast + tmpdir.join("t.py").write("b'foo' \\\n") + assert cli.main(["t.py"]) == 1 + + if hasattr(sys, "pypy_version_info"): # pragma: no cover (pypy) + expected = "t.py:2:1: E999 SyntaxError: end of file (EOF) in multi-line statement\n" # noqa: E501 + elif sys.version_info < (3, 10): # pragma: no cover (cp38+) + expected = "t.py:1:8: E999 SyntaxError: unexpected EOF while parsing\n" + else: # pragma: no cover (cp310+) + expected = "t.py:1:10: E999 SyntaxError: unexpected EOF while parsing\n" # noqa: E501 + + out, err = capsys.readouterr() + assert out == expected + assert err == "" + + +def test_tokenization_error_is_a_syntax_error(tmpdir, capsys): + """Test when tokenize raises a SyntaxError.""" + with tmpdir.as_cwd(): + tmpdir.join("t.py").write("if True:\n pass\n pass\n") + assert cli.main(["t.py"]) == 1 + + if hasattr(sys, "pypy_version_info"): # pragma: no cover (pypy) + expected = "t.py:3:2: E999 IndentationError: unindent does not match any outer indentation level\n" # noqa: E501 + elif sys.version_info < (3, 10): # pragma: no cover (= (3, 12): # pragma: >=3.12 cover + expected = """\ +t.py:1:1: T001 "f'xxx{hello}xxxx{world}xxx'" +""" + else: # pragma: <3.12 cover + expected = """\ +t.py:1:1: T001 "f'xxxxxxxxxxxxxxxxxxxxxxxx'" +""" + out, err = capsys.readouterr() + assert out == expected + + +@pytest.mark.xfail(sys.version_info < (3, 14), reason="3.14+") +def test_tstring_logical_line(tmpdir, capsys): # pragma: >=3.14 cover + cfg_s = f"""\ +[flake8] +extend-ignore = F +[flake8:local-plugins] +extension = + T = {yields_logical_line.__module__}:{yields_logical_line.__name__} +""" + + cfg = tmpdir.join("tox.ini") + cfg.write(cfg_s) + + src = """\ +t''' +hello {world} +''' +t'{{"{hello}": "{world}"}}' +""" + t_py = tmpdir.join("t.py") + t_py.write_binary(src.encode()) + + with tmpdir.as_cwd(): + assert main(("t.py", "--config", str(cfg))) == 1 + + expected = """\ +t.py:1:1: T001 "t'''xxxxxxx{world}x'''" +t.py:4:1: T001 "t'xxx{hello}xxxx{world}xxx'" +""" + out, err = capsys.readouterr() + assert out == expected diff --git a/src/flake8-main/flake8-main/tests/unit/__init__.py b/src/flake8-main/flake8-main/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/flake8-main/flake8-main/tests/unit/conftest.py b/src/flake8-main/flake8-main/tests/unit/conftest.py new file mode 100644 index 0000000..0f8386a --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/conftest.py @@ -0,0 +1,24 @@ +"""Shared fixtures between unit tests.""" +from __future__ import annotations + +import argparse + +import pytest + + +def options_from(**kwargs): + """Generate a Values instances with our kwargs.""" + kwargs.setdefault("hang_closing", True) + kwargs.setdefault("max_line_length", 79) + kwargs.setdefault("max_doc_length", None) + kwargs.setdefault("indent_size", 4) + kwargs.setdefault("verbose", 0) + kwargs.setdefault("stdin_display_name", "stdin") + kwargs.setdefault("disable_noqa", False) + return argparse.Namespace(**kwargs) + + +@pytest.fixture +def default_options(): + """Fixture returning the default options of flake8.""" + return options_from() diff --git a/src/flake8-main/flake8-main/tests/unit/plugins/__init__.py b/src/flake8-main/flake8-main/tests/unit/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/flake8-main/flake8-main/tests/unit/plugins/finder_test.py b/src/flake8-main/flake8-main/tests/unit/plugins/finder_test.py new file mode 100644 index 0000000..a155ef1 --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/plugins/finder_test.py @@ -0,0 +1,845 @@ +from __future__ import annotations + +import configparser +import importlib.metadata +import sys +from unittest import mock + +import pytest + +from flake8.exceptions import ExecutionError +from flake8.exceptions import FailedToLoadPlugin +from flake8.plugins import finder +from flake8.plugins.pyflakes import FlakesChecker + + +def _ep(name="X", value="dne:dne", group="flake8.extension"): + return importlib.metadata.EntryPoint(name, value, group) + + +def _plugin(package="local", version="local", ep=None): + if ep is None: + ep = _ep() + return finder.Plugin(package, version, ep) + + +def _loaded(plugin=None, obj=None, parameters=None): + if plugin is None: + plugin = _plugin() + if parameters is None: + parameters = {"tree": True} + return finder.LoadedPlugin(plugin, obj, parameters) + + +def test_loaded_plugin_entry_name_vs_display_name(): + loaded = _loaded(_plugin(package="package-name", ep=_ep(name="Q"))) + assert loaded.entry_name == "Q" + assert loaded.display_name == "package-name[Q]" + + +def test_plugins_all_plugins(): + tree_plugin = _loaded(parameters={"tree": True}) + logical_line_plugin = _loaded(parameters={"logical_line": True}) + physical_line_plugin = _loaded(parameters={"physical_line": True}) + report_plugin = _loaded( + plugin=_plugin(ep=_ep(name="R", group="flake8.report")), + ) + + plugins = finder.Plugins( + checkers=finder.Checkers( + tree=[tree_plugin], + logical_line=[logical_line_plugin], + physical_line=[physical_line_plugin], + ), + reporters={"R": report_plugin}, + disabled=[], + ) + + assert tuple(plugins.all_plugins()) == ( + tree_plugin, + logical_line_plugin, + physical_line_plugin, + report_plugin, + ) + + +def test_plugins_versions_str(): + plugins = finder.Plugins( + checkers=finder.Checkers( + tree=[_loaded(_plugin(package="pkg1", version="1"))], + logical_line=[_loaded(_plugin(package="pkg2", version="2"))], + physical_line=[_loaded(_plugin(package="pkg1", version="1"))], + ), + reporters={ + # ignore flake8 builtin plugins + "default": _loaded(_plugin(package="flake8")), + # ignore local plugins + "custom": _loaded(_plugin(package="local")), + }, + disabled=[], + ) + assert plugins.versions_str() == "pkg1: 1, pkg2: 2" + + +@pytest.fixture +def pyflakes_dist(tmp_path): + metadata = """\ +Metadata-Version: 2.1 +Name: pyflakes +Version: 9000.1.0 +""" + d = tmp_path.joinpath("pyflakes.dist-info") + d.mkdir() + d.joinpath("METADATA").write_text(metadata) + return importlib.metadata.PathDistribution(d) + + +@pytest.fixture +def pycodestyle_dist(tmp_path): + metadata = """\ +Metadata-Version: 2.1 +Name: pycodestyle +Version: 9000.2.0 +""" + d = tmp_path.joinpath("pycodestyle.dist-info") + d.mkdir() + d.joinpath("METADATA").write_text(metadata) + return importlib.metadata.PathDistribution(d) + + +@pytest.fixture +def flake8_dist(tmp_path): + metadata = """\ +Metadata-Version: 2.1 +Name: flake8 +Version: 9001 +""" + entry_points = """\ +[console_scripts] +flake8 = flake8.main.cli:main + +[flake8.extension] +F = flake8.plugins.pyflakes:FlakesChecker +E = flake8.plugins.pycodestyle:pycodestyle_logical +W = flake8.plugins.pycodestyle:pycodestyle_physical + +[flake8.report] +default = flake8.formatting.default:Default +pylint = flake8.formatting.default:Pylint +""" + d = tmp_path.joinpath("flake8.dist-info") + d.mkdir() + d.joinpath("METADATA").write_text(metadata) + d.joinpath("entry_points.txt").write_text(entry_points) + return importlib.metadata.PathDistribution(d) + + +@pytest.fixture +def flake8_foo_dist(tmp_path): + metadata = """\ +Metadata-Version: 2.1 +Name: flake8-foo +Version: 1.2.3 +""" + eps = """\ +[console_scripts] +foo = flake8_foo:main +[flake8.extension] +Q = flake8_foo:Plugin +[flake8.report] +foo = flake8_foo:Formatter +""" + d = tmp_path.joinpath("flake8_foo.dist-info") + d.mkdir() + d.joinpath("METADATA").write_text(metadata) + d.joinpath("entry_points.txt").write_text(eps) + return importlib.metadata.PathDistribution(d) + + +@pytest.fixture +def mock_distribution(pyflakes_dist, pycodestyle_dist): + dists = {"pyflakes": pyflakes_dist, "pycodestyle": pycodestyle_dist} + with mock.patch.object(importlib.metadata, "distribution", dists.get): + yield + + +def test_flake8_plugins(flake8_dist, mock_distribution): + """Ensure entrypoints for flake8 are parsed specially.""" + + eps = flake8_dist.entry_points + ret = set(finder._flake8_plugins(eps, "flake8", "9001")) + assert ret == { + finder.Plugin( + "pyflakes", + "9000.1.0", + importlib.metadata.EntryPoint( + "F", + "flake8.plugins.pyflakes:FlakesChecker", + "flake8.extension", + ), + ), + finder.Plugin( + "pycodestyle", + "9000.2.0", + importlib.metadata.EntryPoint( + "E", + "flake8.plugins.pycodestyle:pycodestyle_logical", + "flake8.extension", + ), + ), + finder.Plugin( + "pycodestyle", + "9000.2.0", + importlib.metadata.EntryPoint( + "W", + "flake8.plugins.pycodestyle:pycodestyle_physical", + "flake8.extension", + ), + ), + finder.Plugin( + "flake8", + "9001", + importlib.metadata.EntryPoint( + "default", + "flake8.formatting.default:Default", + "flake8.report", + ), + ), + finder.Plugin( + "flake8", + "9001", + importlib.metadata.EntryPoint( + "pylint", "flake8.formatting.default:Pylint", "flake8.report", + ), + ), + } + + +def test_importlib_plugins( + tmp_path, + flake8_dist, + flake8_foo_dist, + mock_distribution, + caplog, +): + """Ensure we can load plugins from importlib.metadata.""" + + # make sure flake8-colors is skipped + flake8_colors_metadata = """\ +Metadata-Version: 2.1 +Name: flake8-colors +Version: 1.2.3 +""" + flake8_colors_eps = """\ +[flake8.extension] +flake8-colors = flake8_colors:ColorFormatter +""" + flake8_colors_d = tmp_path.joinpath("flake8_colors.dist-info") + flake8_colors_d.mkdir() + flake8_colors_d.joinpath("METADATA").write_text(flake8_colors_metadata) + flake8_colors_d.joinpath("entry_points.txt").write_text(flake8_colors_eps) + flake8_colors_dist = importlib.metadata.PathDistribution(flake8_colors_d) + + unrelated_metadata = """\ +Metadata-Version: 2.1 +Name: unrelated +Version: 4.5.6 +""" + unrelated_eps = """\ +[console_scripts] +unrelated = unrelated:main +""" + unrelated_d = tmp_path.joinpath("unrelated.dist-info") + unrelated_d.mkdir() + unrelated_d.joinpath("METADATA").write_text(unrelated_metadata) + unrelated_d.joinpath("entry_points.txt").write_text(unrelated_eps) + unrelated_dist = importlib.metadata.PathDistribution(unrelated_d) + + with mock.patch.object( + importlib.metadata, + "distributions", + return_value=[ + flake8_dist, + flake8_colors_dist, + flake8_foo_dist, + unrelated_dist, + ], + ): + ret = set(finder._find_importlib_plugins()) + + assert ret == { + finder.Plugin( + "flake8-foo", + "1.2.3", + importlib.metadata.EntryPoint( + "Q", "flake8_foo:Plugin", "flake8.extension", + ), + ), + finder.Plugin( + "pycodestyle", + "9000.2.0", + importlib.metadata.EntryPoint( + "E", + "flake8.plugins.pycodestyle:pycodestyle_logical", + "flake8.extension", + ), + ), + finder.Plugin( + "pycodestyle", + "9000.2.0", + importlib.metadata.EntryPoint( + "W", + "flake8.plugins.pycodestyle:pycodestyle_physical", + "flake8.extension", + ), + ), + finder.Plugin( + "pyflakes", + "9000.1.0", + importlib.metadata.EntryPoint( + "F", + "flake8.plugins.pyflakes:FlakesChecker", + "flake8.extension", + ), + ), + finder.Plugin( + "flake8", + "9001", + importlib.metadata.EntryPoint( + "default", + "flake8.formatting.default:Default", + "flake8.report", + ), + ), + finder.Plugin( + "flake8", + "9001", + importlib.metadata.EntryPoint( + "pylint", "flake8.formatting.default:Pylint", "flake8.report", + ), + ), + finder.Plugin( + "flake8-foo", + "1.2.3", + importlib.metadata.EntryPoint( + "foo", "flake8_foo:Formatter", "flake8.report", + ), + ), + } + + assert caplog.record_tuples == [ + ( + "flake8.plugins.finder", + 30, + "flake8-colors plugin is obsolete in flake8>=5.0", + ), + ] + + +def test_duplicate_dists(flake8_dist): + # some poorly packaged pythons put lib and lib64 on sys.path resulting in + # duplicates from `importlib.metadata.distributions` + with mock.patch.object( + importlib.metadata, + "distributions", + return_value=[ + flake8_dist, + flake8_dist, + ], + ): + ret = list(finder._find_importlib_plugins()) + + # we should not have duplicates + assert len(ret) == len(set(ret)) + + +def test_find_local_plugins_nothing(): + cfg = configparser.RawConfigParser() + assert set(finder._find_local_plugins(cfg)) == set() + + +@pytest.fixture +def local_plugin_cfg(): + cfg = configparser.RawConfigParser() + cfg.add_section("flake8:local-plugins") + cfg.set("flake8:local-plugins", "extension", "Y=mod2:attr, X = mod:attr") + cfg.set("flake8:local-plugins", "report", "Z=mod3:attr") + return cfg + + +def test_find_local_plugins(local_plugin_cfg): + ret = set(finder._find_local_plugins(local_plugin_cfg)) + assert ret == { + finder.Plugin( + "local", + "local", + importlib.metadata.EntryPoint( + "X", + "mod:attr", + "flake8.extension", + ), + ), + finder.Plugin( + "local", + "local", + importlib.metadata.EntryPoint( + "Y", + "mod2:attr", + "flake8.extension", + ), + ), + finder.Plugin( + "local", + "local", + importlib.metadata.EntryPoint( + "Z", + "mod3:attr", + "flake8.report", + ), + ), + } + + +def test_parse_plugin_options_not_specified(tmp_path): + cfg = configparser.RawConfigParser() + opts = finder.parse_plugin_options( + cfg, + str(tmp_path), + enable_extensions=None, + require_plugins=None, + ) + expected = finder.PluginOptions( + local_plugin_paths=(), + enable_extensions=frozenset(), + require_plugins=frozenset(), + ) + assert opts == expected + + +def test_parse_enabled_from_commandline(tmp_path): + cfg = configparser.RawConfigParser() + cfg.add_section("flake8") + cfg.set("flake8", "enable_extensions", "A,B,C") + opts = finder.parse_plugin_options( + cfg, + str(tmp_path), + enable_extensions="D,E,F", + require_plugins=None, + ) + assert opts.enable_extensions == frozenset(("D", "E", "F")) + + +@pytest.mark.parametrize("opt", ("enable_extensions", "enable-extensions")) +def test_parse_enabled_from_config(opt, tmp_path): + cfg = configparser.RawConfigParser() + cfg.add_section("flake8") + cfg.set("flake8", opt, "A,B,C") + opts = finder.parse_plugin_options( + cfg, + str(tmp_path), + enable_extensions=None, + require_plugins=None, + ) + assert opts.enable_extensions == frozenset(("A", "B", "C")) + + +def test_parse_plugin_options_local_plugin_paths_missing(tmp_path): + cfg = configparser.RawConfigParser() + opts = finder.parse_plugin_options( + cfg, + str(tmp_path), + enable_extensions=None, + require_plugins=None, + ) + assert opts.local_plugin_paths == () + + +def test_parse_plugin_options_local_plugin_paths(tmp_path): + cfg = configparser.RawConfigParser() + cfg.add_section("flake8:local-plugins") + cfg.set("flake8:local-plugins", "paths", "./a, ./b") + opts = finder.parse_plugin_options( + cfg, + str(tmp_path), + enable_extensions=None, + require_plugins=None, + ) + + expected = (str(tmp_path.joinpath("a")), str(tmp_path.joinpath("b"))) + assert opts.local_plugin_paths == expected + + +def test_find_plugins( + tmp_path, + flake8_dist, + flake8_foo_dist, + mock_distribution, + local_plugin_cfg, +): + opts = finder.PluginOptions.blank() + with mock.patch.object( + importlib.metadata, + "distributions", + return_value=[flake8_dist, flake8_foo_dist], + ): + ret = finder.find_plugins(local_plugin_cfg, opts) + + assert ret == [ + finder.Plugin( + "flake8", + "9001", + importlib.metadata.EntryPoint( + "default", + "flake8.formatting.default:Default", + "flake8.report", + ), + ), + finder.Plugin( + "flake8", + "9001", + importlib.metadata.EntryPoint( + "pylint", "flake8.formatting.default:Pylint", "flake8.report", + ), + ), + finder.Plugin( + "flake8-foo", + "1.2.3", + importlib.metadata.EntryPoint( + "Q", "flake8_foo:Plugin", "flake8.extension", + ), + ), + finder.Plugin( + "flake8-foo", + "1.2.3", + importlib.metadata.EntryPoint( + "foo", "flake8_foo:Formatter", "flake8.report", + ), + ), + finder.Plugin( + "local", + "local", + importlib.metadata.EntryPoint("X", "mod:attr", "flake8.extension"), + ), + finder.Plugin( + "local", + "local", + importlib.metadata.EntryPoint( + "Y", "mod2:attr", "flake8.extension", + ), + ), + finder.Plugin( + "local", + "local", + importlib.metadata.EntryPoint("Z", "mod3:attr", "flake8.report"), + ), + finder.Plugin( + "pycodestyle", + "9000.2.0", + importlib.metadata.EntryPoint( + "E", + "flake8.plugins.pycodestyle:pycodestyle_logical", + "flake8.extension", + ), + ), + finder.Plugin( + "pycodestyle", + "9000.2.0", + importlib.metadata.EntryPoint( + "W", + "flake8.plugins.pycodestyle:pycodestyle_physical", + "flake8.extension", + ), + ), + finder.Plugin( + "pyflakes", + "9000.1.0", + importlib.metadata.EntryPoint( + "F", + "flake8.plugins.pyflakes:FlakesChecker", + "flake8.extension", + ), + ), + ] + + +def test_find_plugins_plugin_is_present(flake8_foo_dist): + cfg = configparser.RawConfigParser() + options_flake8_foo_required = finder.PluginOptions( + local_plugin_paths=(), + enable_extensions=frozenset(), + require_plugins=frozenset(("flake8-foo",)), + ) + options_not_required = finder.PluginOptions( + local_plugin_paths=(), + enable_extensions=frozenset(), + require_plugins=frozenset(), + ) + + with mock.patch.object( + importlib.metadata, + "distributions", + return_value=[flake8_foo_dist], + ): + # neither of these raise, `flake8-foo` is satisfied + finder.find_plugins(cfg, options_flake8_foo_required) + finder.find_plugins(cfg, options_not_required) + + +def test_find_plugins_plugin_is_missing(flake8_dist, flake8_foo_dist): + cfg = configparser.RawConfigParser() + options_flake8_foo_required = finder.PluginOptions( + local_plugin_paths=(), + enable_extensions=frozenset(), + require_plugins=frozenset(("flake8-foo",)), + ) + options_not_required = finder.PluginOptions( + local_plugin_paths=(), + enable_extensions=frozenset(), + require_plugins=frozenset(), + ) + + with mock.patch.object( + importlib.metadata, + "distributions", + return_value=[flake8_dist], + ): + # this is ok, no special requirements + finder.find_plugins(cfg, options_not_required) + + # but we get a nice error for missing plugins here! + with pytest.raises(ExecutionError) as excinfo: + finder.find_plugins(cfg, options_flake8_foo_required) + + (msg,) = excinfo.value.args + assert msg == ( + "required plugins were not installed!\n" + "- installed: flake8, pycodestyle, pyflakes\n" + "- expected: flake8-foo\n" + "- missing: flake8-foo" + ) + + +def test_find_plugins_name_normalization(flake8_foo_dist): + cfg = configparser.RawConfigParser() + opts = finder.PluginOptions( + local_plugin_paths=(), + enable_extensions=frozenset(), + # this name will be normalized before checking + require_plugins=frozenset(("Flake8_Foo",)), + ) + + with mock.patch.object( + importlib.metadata, + "distributions", + return_value=[flake8_foo_dist], + ): + finder.find_plugins(cfg, opts) + + +def test_parameters_for_class_plugin(): + """Verify that we can retrieve the parameters for a class plugin.""" + + class FakeCheck: + def __init__(self, tree): + raise NotImplementedError + + assert finder._parameters_for(FakeCheck) == {"tree": True} + + +def test_parameters_for_function_plugin(): + """Verify that we retrieve the parameters for a function plugin.""" + + def fake_plugin(physical_line, self, tree, optional=None): + raise NotImplementedError + + assert finder._parameters_for(fake_plugin) == { + "physical_line": True, + "self": True, + "tree": True, + "optional": False, + } + + +def test_load_plugin_import_error(): + plugin = _plugin(ep=_ep(value="dne:dne")) + + with pytest.raises(FailedToLoadPlugin) as excinfo: + finder._load_plugin(plugin) + + pkg, e = excinfo.value.args + assert pkg == "local" + assert isinstance(e, ModuleNotFoundError) + + +def test_load_plugin_not_callable(): + plugin = _plugin(ep=_ep(value="os:curdir")) + + with pytest.raises(FailedToLoadPlugin) as excinfo: + finder._load_plugin(plugin) + + pkg, e = excinfo.value.args + assert pkg == "local" + assert isinstance(e, TypeError) + assert e.args == ("expected loaded plugin to be callable",) + + +def test_load_plugin_ok(): + plugin = _plugin(ep=_ep(value="flake8.plugins.pyflakes:FlakesChecker")) + + loaded = finder._load_plugin(plugin) + + assert loaded == finder.LoadedPlugin( + plugin, + FlakesChecker, + {"tree": True, "filename": True}, + ) + + +@pytest.fixture +def reset_sys(): + orig_path = sys.path[:] + orig_modules = sys.modules.copy() + yield + sys.path[:] = orig_path + sys.modules.clear() + sys.modules.update(orig_modules) + + +@pytest.mark.usefixtures("reset_sys") +def test_import_plugins_extends_sys_path(): + plugin = _plugin(ep=_ep(value="aplugin:ExtensionTestPlugin2")) + + opts = finder.PluginOptions( + local_plugin_paths=("tests/integration/subdir",), + enable_extensions=frozenset(), + require_plugins=frozenset(), + ) + ret = finder._import_plugins([plugin], opts) + + import aplugin + + assert ret == [ + finder.LoadedPlugin( + plugin, + aplugin.ExtensionTestPlugin2, + {"tree": True}, + ), + ] + + +def test_classify_plugins(): + report_plugin = _loaded( + plugin=_plugin(ep=_ep(name="R", group="flake8.report")), + ) + tree_plugin = _loaded(parameters={"tree": True}) + logical_line_plugin = _loaded(parameters={"logical_line": True}) + physical_line_plugin = _loaded(parameters={"physical_line": True}) + + classified = finder._classify_plugins( + [ + report_plugin, + tree_plugin, + logical_line_plugin, + physical_line_plugin, + ], + finder.PluginOptions.blank(), + ) + + assert classified == finder.Plugins( + checkers=finder.Checkers( + tree=[tree_plugin], + logical_line=[logical_line_plugin], + physical_line=[physical_line_plugin], + ), + reporters={"R": report_plugin}, + disabled=[], + ) + + +def test_classify_plugins_enable_a_disabled_plugin(): + obj = mock.Mock(off_by_default=True) + plugin = _plugin(ep=_ep(name="ABC")) + loaded = _loaded(plugin=plugin, parameters={"tree": True}, obj=obj) + + normal_opts = finder.PluginOptions( + local_plugin_paths=(), + enable_extensions=frozenset(), + require_plugins=frozenset(), + ) + classified_normal = finder._classify_plugins([loaded], normal_opts) + enabled_opts = finder.PluginOptions( + local_plugin_paths=(), + enable_extensions=frozenset(("ABC",)), + require_plugins=frozenset(), + ) + classified_enabled = finder._classify_plugins([loaded], enabled_opts) + + assert classified_normal == finder.Plugins( + checkers=finder.Checkers([], [], []), + reporters={}, + disabled=[loaded], + ) + assert classified_enabled == finder.Plugins( + checkers=finder.Checkers([loaded], [], []), + reporters={}, + disabled=[], + ) + + +def test_classify_plugins_does_not_error_on_reporter_prefix(): + # these are ok, don't check their name + plugin = _plugin(ep=_ep(name="report-er", group="flake8.report")) + loaded = _loaded(plugin=plugin) + + opts = finder.PluginOptions.blank() + classified = finder._classify_plugins([loaded], opts) + + assert classified == finder.Plugins( + checkers=finder.Checkers([], [], []), + reporters={"report-er": loaded}, + disabled=[], + ) + + +def test_classify_plugins_errors_on_incorrect_checker_name(): + plugin = _plugin(ep=_ep(name="INVALID", group="flake8.extension")) + loaded = _loaded(plugin=plugin, parameters={"tree": True}) + + with pytest.raises(ExecutionError) as excinfo: + finder._classify_plugins([loaded], finder.PluginOptions.blank()) + + (msg,) = excinfo.value.args + assert msg == ( + "plugin code for `local[INVALID]` " + "does not match ^[A-Z]{1,3}[0-9]{0,3}$" + ) + + +@pytest.mark.usefixtures("reset_sys") +def test_load_plugins(): + plugin = _plugin(ep=_ep(value="aplugin:ExtensionTestPlugin2")) + + opts = finder.PluginOptions( + local_plugin_paths=("tests/integration/subdir",), + enable_extensions=frozenset(), + require_plugins=frozenset(), + ) + ret = finder.load_plugins([plugin], opts) + + import aplugin + + assert ret == finder.Plugins( + checkers=finder.Checkers( + tree=[ + finder.LoadedPlugin( + plugin, + aplugin.ExtensionTestPlugin2, + {"tree": True}, + ), + ], + logical_line=[], + physical_line=[], + ), + reporters={}, + disabled=[], + ) diff --git a/src/flake8-main/flake8-main/tests/unit/plugins/pycodestyle_test.py b/src/flake8-main/flake8-main/tests/unit/plugins/pycodestyle_test.py new file mode 100644 index 0000000..1b00d9d --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/plugins/pycodestyle_test.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import importlib.machinery +import importlib.util +import os.path + +import flake8.plugins.pycodestyle + +HERE = os.path.dirname(os.path.abspath(__file__)) + + +def test_up_to_date(): + """Validate that the generated pycodestyle plugin is up to date. + + We generate two "meta" plugins for pycodestyle to avoid calling overhead. + + To regenerate run: + + ./bin/gen-pycodestyle-plugin > src/flake8/plugins/pycodestyle.py + """ + + path = os.path.join(HERE, "../../../bin/gen-pycodestyle-plugin") + name = os.path.basename(path) + loader = importlib.machinery.SourceFileLoader(name, path) + spec = importlib.util.spec_from_loader(loader.name, loader) + assert spec is not None + mod = importlib.util.module_from_spec(spec) + loader.exec_module(mod) + + expected = "".join(f"{line}\n" for line in mod.lines()) + + with open(flake8.plugins.pycodestyle.__file__) as f: + contents = f.read() + + assert contents == expected diff --git a/src/flake8-main/flake8-main/tests/unit/plugins/reporter_test.py b/src/flake8-main/flake8-main/tests/unit/plugins/reporter_test.py new file mode 100644 index 0000000..48b2873 --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/plugins/reporter_test.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +import argparse +import importlib.metadata + +import pytest + +from flake8.formatting import default +from flake8.plugins import finder +from flake8.plugins import reporter + + +def _opts(**kwargs): + kwargs.setdefault("quiet", 0) + kwargs.setdefault("color", "never") + kwargs.setdefault("output_file", None) + return argparse.Namespace(**kwargs) + + +@pytest.fixture +def reporters(): + def _plugin(name, cls): + return finder.LoadedPlugin( + finder.Plugin( + "flake8", + "123", + importlib.metadata.EntryPoint( + name, f"{cls.__module__}:{cls.__name__}", "flake8.report", + ), + ), + cls, + {"options": True}, + ) + + return { + "default": _plugin("default", default.Default), + "pylint": _plugin("pylint", default.Pylint), + "quiet-filename": _plugin("quiet-filename", default.FilenameOnly), + "quiet-nothing": _plugin("quiet-nothing", default.Nothing), + } + + +def test_make_formatter_default(reporters): + ret = reporter.make(reporters, _opts(format="default")) + assert isinstance(ret, default.Default) + assert ret.error_format == default.Default.error_format + + +def test_make_formatter_quiet_filename(reporters): + ret = reporter.make(reporters, _opts(format="default", quiet=1)) + assert isinstance(ret, default.FilenameOnly) + + +@pytest.mark.parametrize("quiet", (2, 3)) +def test_make_formatter_very_quiet(reporters, quiet): + ret = reporter.make(reporters, _opts(format="default", quiet=quiet)) + assert isinstance(ret, default.Nothing) + + +def test_make_formatter_custom(reporters): + ret = reporter.make(reporters, _opts(format="pylint")) + assert isinstance(ret, default.Pylint) + + +def test_make_formatter_format_string(reporters, caplog): + ret = reporter.make(reporters, _opts(format="hi %(code)s")) + assert isinstance(ret, default.Default) + assert ret.error_format == "hi %(code)s" + + assert caplog.record_tuples == [ + ( + "flake8.plugins.reporter", + 30, + "'hi %(code)s' is an unknown formatter. Falling back to default.", + ), + ] diff --git a/src/flake8-main/flake8-main/tests/unit/test_application.py b/src/flake8-main/flake8-main/tests/unit/test_application.py new file mode 100644 index 0000000..3c93085 --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_application.py @@ -0,0 +1,46 @@ +"""Tests for the Application class.""" +from __future__ import annotations + +import argparse + +import pytest + +from flake8.main import application as app + + +def options(**kwargs): + """Generate argparse.Namespace for our Application.""" + kwargs.setdefault("verbose", 0) + kwargs.setdefault("output_file", None) + kwargs.setdefault("count", False) + kwargs.setdefault("exit_zero", False) + return argparse.Namespace(**kwargs) + + +@pytest.fixture +def application(): + """Create an application.""" + return app.Application() + + +@pytest.mark.parametrize( + "result_count, catastrophic, exit_zero, value", + [ + (0, False, False, 0), + (0, True, False, 1), + (2, False, False, 1), + (2, True, False, 1), + (0, True, True, 1), + (2, False, True, 0), + (2, True, True, 1), + ], +) +def test_application_exit_code( + result_count, catastrophic, exit_zero, value, application, +): + """Verify Application.exit_code returns the correct value.""" + application.result_count = result_count + application.catastrophic_failure = catastrophic + application.options = options(exit_zero=exit_zero) + + assert application.exit_code() == value diff --git a/src/flake8-main/flake8-main/tests/unit/test_base_formatter.py b/src/flake8-main/flake8-main/tests/unit/test_base_formatter.py new file mode 100644 index 0000000..0d81c81 --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_base_formatter.py @@ -0,0 +1,222 @@ +"""Tests for the BaseFormatter object.""" +from __future__ import annotations + +import argparse +import sys +from unittest import mock + +import pytest + +from flake8.formatting import _windows_color +from flake8.formatting import base +from flake8.violation import Violation + + +def options(**kwargs): + """Create an argparse.Namespace instance.""" + kwargs.setdefault("color", "auto") + kwargs.setdefault("output_file", None) + kwargs.setdefault("tee", False) + return argparse.Namespace(**kwargs) + + +@pytest.mark.parametrize("filename", [None, "out.txt"]) +def test_start(filename): + """Verify we open a new file in the start method.""" + mock_open = mock.mock_open() + formatter = base.BaseFormatter(options(output_file=filename)) + with mock.patch("flake8.formatting.base.open", mock_open): + formatter.start() + + if filename is None: + assert mock_open.called is False + else: + mock_open.assert_called_once_with(filename, "a") + + +def test_stop(): + """Verify we close open file objects.""" + filemock = mock.Mock() + formatter = base.BaseFormatter(options()) + formatter.output_fd = filemock + formatter.stop() + + filemock.close.assert_called_once_with() + assert formatter.output_fd is None + + +def test_format_needs_to_be_implemented(): + """Ensure BaseFormatter#format raises a NotImplementedError.""" + formatter = base.BaseFormatter(options()) + with pytest.raises(NotImplementedError): + formatter.format( + Violation("A000", "file.py", 1, 1, "error text", None), + ) + + +def test_show_source_returns_nothing_when_not_showing_source(): + """Ensure we return nothing when users want nothing.""" + formatter = base.BaseFormatter(options(show_source=False)) + assert ( + formatter.show_source( + Violation("A000", "file.py", 1, 1, "error text", "line"), + ) + == "" + ) + + +def test_show_source_returns_nothing_when_there_is_source(): + """Ensure we return nothing when there is no line.""" + formatter = base.BaseFormatter(options(show_source=True)) + assert ( + formatter.show_source( + Violation("A000", "file.py", 1, 1, "error text", None), + ) + == "" + ) + + +@pytest.mark.parametrize( + ("line1", "line2", "column"), + [ + ( + "x=1\n", + " ^", + 2, + ), + ( + " x=(1\n +2)\n", + " ^", + 5, + ), + ( + "\tx\t=\ty\n", + "\t \t \t^", + 6, + ), + ], +) +def test_show_source_updates_physical_line_appropriately(line1, line2, column): + """Ensure the error column is appropriately indicated.""" + formatter = base.BaseFormatter(options(show_source=True)) + error = Violation("A000", "file.py", 1, column, "error", line1) + output = formatter.show_source(error) + assert output == line1 + line2 + + +@pytest.mark.parametrize("tee", [False, True]) +def test_write_uses_an_output_file(tee, capsys): + """Verify that we use the output file when it's present.""" + line = "Something to write" + source = "source" + filemock = mock.Mock() + + formatter = base.BaseFormatter(options(tee=tee)) + formatter.output_fd = filemock + + formatter.write(line, source) + if tee: + assert capsys.readouterr().out == f"{line}\n{source}\n" + else: + assert capsys.readouterr().out == "" + + assert filemock.write.called is True + assert filemock.write.call_count == 2 + assert filemock.write.mock_calls == [ + mock.call(line + formatter.newline), + mock.call(source + formatter.newline), + ] + + +def test_write_produces_stdout(capsys): + """Verify that we write to stdout without an output file.""" + line = "Something to write" + source = "source" + + formatter = base.BaseFormatter(options()) + formatter.write(line, source) + + assert capsys.readouterr().out == f"{line}\n{source}\n" + + +def test_color_always_is_true(): + """Verify that color='always' sets it to True.""" + formatter = base.BaseFormatter(options(color="always")) + assert formatter.color is True + + +def _mock_isatty(val): + attrs = {"isatty.return_value": val} + return mock.patch.object(sys, "stdout", **attrs) + + +def _mock_windows_color(val): + return mock.patch.object(_windows_color, "terminal_supports_color", val) + + +def test_color_auto_is_true_for_tty(): + """Verify that color='auto' sets it to True for a tty.""" + with _mock_isatty(True), _mock_windows_color(True): + formatter = base.BaseFormatter(options(color="auto")) + assert formatter.color is True + + +def test_color_auto_is_false_without_tty(): + """Verify that color='auto' sets it to False without a tty.""" + with _mock_isatty(False), _mock_windows_color(True): + formatter = base.BaseFormatter(options(color="auto")) + assert formatter.color is False + + +def test_color_auto_is_false_if_not_supported_on_windows(): + """Verify that color='auto' is False if not supported on windows.""" + with _mock_isatty(True), _mock_windows_color(False): + formatter = base.BaseFormatter(options(color="auto")) + assert formatter.color is False + + +def test_color_never_is_false(): + """Verify that color='never' sets it to False despite a tty.""" + with _mock_isatty(True), _mock_windows_color(True): + formatter = base.BaseFormatter(options(color="never")) + assert formatter.color is False + + +class AfterInitFormatter(base.BaseFormatter): + """Subclass for testing after_init.""" + + def after_init(self): + """Define method to verify operation.""" + self.post_initialized = True + + +def test_after_init_is_always_called(): + """Verify after_init is called.""" + formatter = AfterInitFormatter(options()) + assert formatter.post_initialized is True + + +class FormatFormatter(base.BaseFormatter): + """Subclass for testing format.""" + + def format(self, error): + """Define method to verify operation.""" + return repr(error) + + +def test_handle_formats_the_error(): + """Verify that a formatter will call format from handle.""" + formatter = FormatFormatter(options(show_source=False)) + filemock = formatter.output_fd = mock.Mock() + error = Violation( + code="A001", + filename="example.py", + line_number=1, + column_number=1, + text="Fake error", + physical_line="a = 1", + ) + + formatter.handle(error) + + filemock.write.assert_called_once_with(repr(error) + "\n") diff --git a/src/flake8-main/flake8-main/tests/unit/test_checker_manager.py b/src/flake8-main/flake8-main/tests/unit/test_checker_manager.py new file mode 100644 index 0000000..eecba3b --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_checker_manager.py @@ -0,0 +1,82 @@ +"""Tests for the Manager object for FileCheckers.""" +from __future__ import annotations + +import errno +import multiprocessing +from unittest import mock + +import pytest + +from flake8 import checker +from flake8.main.options import JobsArgument +from flake8.plugins import finder + + +def style_guide_mock(): + """Create a mock StyleGuide object.""" + return mock.MagicMock(**{"options.jobs": JobsArgument("4")}) + + +def _parallel_checker_manager(): + """Call Manager.run() and return the number of calls to `run_serial`.""" + style_guide = style_guide_mock() + manager = checker.Manager(style_guide, finder.Checkers([], [], []), []) + # multiple files is needed for parallel mode + manager.filenames = ("file1", "file2") + return manager + + +def test_oserrors_cause_serial_fall_back(): + """Verify that OSErrors will cause the Manager to fallback to serial.""" + err = OSError(errno.ENOSPC, "Ominous message about spaceeeeee") + with mock.patch("_multiprocessing.SemLock", side_effect=err): + manager = _parallel_checker_manager() + with mock.patch.object(manager, "run_serial") as serial: + manager.run() + assert serial.call_count == 1 + + +def test_oserrors_are_reraised(): + """Verify that unexpected OSErrors will cause the Manager to reraise.""" + err = OSError(errno.EAGAIN, "Ominous message") + with mock.patch("_multiprocessing.SemLock", side_effect=err): + manager = _parallel_checker_manager() + with ( + mock.patch.object(manager, "run_serial") as serial, + pytest.raises(OSError), + ): + manager.run() + assert serial.call_count == 0 + + +def test_multiprocessing_cpu_count_not_implemented(): + """Verify that jobs is 0 if cpu_count is unavailable.""" + style_guide = style_guide_mock() + style_guide.options.jobs = JobsArgument("auto") + + with mock.patch.object( + multiprocessing, + "cpu_count", + side_effect=NotImplementedError, + ): + manager = checker.Manager(style_guide, finder.Checkers([], [], []), []) + assert manager.jobs == 0 + + +def test_jobs_count_limited_to_file_count(): + style_guide = style_guide_mock() + style_guide.options.jobs = JobsArgument("4") + style_guide.options.filenames = ["file1", "file2"] + manager = checker.Manager(style_guide, finder.Checkers([], [], []), []) + assert manager.jobs == 4 + manager.start() + assert manager.jobs == 2 + + +def test_make_checkers(): + """Verify that we create a list of FileChecker instances.""" + style_guide = style_guide_mock() + style_guide.options.filenames = ["file1", "file2"] + manager = checker.Manager(style_guide, finder.Checkers([], [], []), []) + manager.start() + assert manager.filenames == ("file1", "file2") diff --git a/src/flake8-main/flake8-main/tests/unit/test_debug.py b/src/flake8-main/flake8-main/tests/unit/test_debug.py new file mode 100644 index 0000000..298b598 --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_debug.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +import importlib.metadata +from unittest import mock + +from flake8.main import debug +from flake8.plugins import finder + + +def test_debug_information(): + def _plugin(pkg, version, ep_name): + return finder.LoadedPlugin( + finder.Plugin( + pkg, + version, + importlib.metadata.EntryPoint( + ep_name, "dne:dne", "flake8.extension", + ), + ), + None, + {}, + ) + + plugins = finder.Plugins( + checkers=finder.Checkers( + tree=[ + _plugin("pkg1", "1.2.3", "X1"), + _plugin("pkg1", "1.2.3", "X2"), + _plugin("pkg2", "4.5.6", "X3"), + ], + logical_line=[], + physical_line=[], + ), + reporters={}, + disabled=[], + ) + + info = debug.information("9001", plugins) + assert info == { + "version": "9001", + "plugins": [ + {"plugin": "pkg1", "version": "1.2.3"}, + {"plugin": "pkg2", "version": "4.5.6"}, + ], + "platform": { + "python_implementation": mock.ANY, + "python_version": mock.ANY, + "system": mock.ANY, + }, + } diff --git a/src/flake8-main/flake8-main/tests/unit/test_decision_engine.py b/src/flake8-main/flake8-main/tests/unit/test_decision_engine.py new file mode 100644 index 0000000..cd8f80d --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_decision_engine.py @@ -0,0 +1,245 @@ +"""Tests for the flake8.style_guide.DecisionEngine class.""" +from __future__ import annotations + +import argparse + +import pytest + +from flake8 import style_guide + + +def create_options(**kwargs): + """Create and return an instance of argparse.Namespace.""" + kwargs.setdefault("select", None) + kwargs.setdefault("ignore", None) + kwargs.setdefault("extend_select", None) + kwargs.setdefault("extend_ignore", None) + kwargs.setdefault("extended_default_select", ["C90", "F", "E", "W"]) + kwargs.setdefault("extended_default_ignore", []) + kwargs.setdefault("disable_noqa", False) + return argparse.Namespace(**kwargs) + + +@pytest.mark.parametrize( + "ignore_list,extend_ignore,error_code", + [ + (["E111", "E121"], [], "E111"), + (["E111", "E121"], [], "E121"), + (["E111"], ["E121"], "E121"), + (["E11", "E12"], [], "E121"), + (["E2", "E12"], [], "E121"), + (["E2", "E12"], [], "E211"), + (["E2", "E3"], ["E12"], "E211"), + ], +) +def test_was_ignored_ignores_errors(ignore_list, extend_ignore, error_code): + """Verify we detect users explicitly ignoring an error.""" + decider = style_guide.DecisionEngine( + create_options(ignore=ignore_list, extend_ignore=extend_ignore), + ) + + assert decider.was_ignored(error_code) is style_guide.Ignored.Explicitly + + +@pytest.mark.parametrize( + "ignore_list,extend_ignore,error_code", + [ + (["E111", "E121"], [], "E112"), + (["E111", "E121"], [], "E122"), + (["E11", "E12"], ["E121"], "W121"), + (["E2", "E12"], [], "E112"), + (["E2", "E12"], [], "E111"), + (["E2", "E12"], ["W11", "E3"], "E111"), + ], +) +def test_was_ignored_implicitly_selects_errors( + ignore_list, extend_ignore, error_code, +): + """Verify we detect users does not explicitly ignore an error.""" + decider = style_guide.DecisionEngine( + create_options(ignore=ignore_list, extend_ignore=extend_ignore), + ) + + assert decider.was_ignored(error_code) is style_guide.Selected.Implicitly + + +@pytest.mark.parametrize( + ("select_list", "extend_select", "error_code"), + ( + (["E111", "E121"], [], "E111"), + (["E111", "E121"], [], "E121"), + (["E11", "E12"], [], "E121"), + (["E2", "E12"], [], "E121"), + (["E2", "E12"], [], "E211"), + (["E1"], ["E2"], "E211"), + ([], ["E2"], "E211"), + (["E1"], ["E2"], "E211"), + (["E111"], ["E121"], "E121"), + ), +) +def test_was_selected_selects_errors(select_list, extend_select, error_code): + """Verify we detect users explicitly selecting an error.""" + decider = style_guide.DecisionEngine( + options=create_options( + select=select_list, + extend_select=extend_select, + ), + ) + + assert decider.was_selected(error_code) is style_guide.Selected.Explicitly + + +def test_was_selected_implicitly_selects_errors(): + """Verify we detect users implicitly selecting an error.""" + error_code = "E121" + decider = style_guide.DecisionEngine( + create_options( + select=None, + extended_default_select=["E"], + ), + ) + + assert decider.was_selected(error_code) is style_guide.Selected.Implicitly + + +@pytest.mark.parametrize( + "select_list,error_code", + [ + (["E111", "E121"], "E112"), + (["E111", "E121"], "E122"), + (["E11", "E12"], "E132"), + (["E2", "E12"], "E321"), + (["E2", "E12"], "E410"), + ], +) +def test_was_selected_excludes_errors(select_list, error_code): + """Verify we detect users implicitly excludes an error.""" + decider = style_guide.DecisionEngine(create_options(select=select_list)) + + assert decider.was_selected(error_code) is style_guide.Ignored.Implicitly + + +@pytest.mark.parametrize( + "select_list,ignore_list,extend_ignore,error_code,expected", + [ + (["E111", "E121"], [], None, "E111", style_guide.Decision.Selected), + (["E111", "E121"], [], None, "E112", style_guide.Decision.Ignored), + (["E111", "E121"], [], None, "E121", style_guide.Decision.Selected), + (["E111", "E121"], [], None, "E122", style_guide.Decision.Ignored), + (["E11", "E12"], [], None, "E132", style_guide.Decision.Ignored), + (["E2", "E12"], [], None, "E321", style_guide.Decision.Ignored), + (["E2", "E12"], [], None, "E410", style_guide.Decision.Ignored), + (["E11", "E121"], ["E1"], [], "E112", style_guide.Decision.Selected), + (["E11", "E121"], [], ["E1"], "E112", style_guide.Decision.Selected), + ( + ["E111", "E121"], + ["E2"], + ["E3"], + "E122", + style_guide.Decision.Ignored, + ), + (["E11", "E12"], ["E13"], None, "E132", style_guide.Decision.Ignored), + (["E1", "E3"], ["E32"], None, "E321", style_guide.Decision.Ignored), + ([], ["E2", "E12"], None, "E410", style_guide.Decision.Ignored), + ( + ["E4"], + ["E2", "E12", "E41"], + None, + "E410", + style_guide.Decision.Ignored, + ), + ( + ["E41"], + ["E2", "E12", "E4"], + None, + "E410", + style_guide.Decision.Selected, + ), + (["E"], ["F"], None, "E410", style_guide.Decision.Selected), + (["F"], [], None, "E410", style_guide.Decision.Ignored), + (["E"], None, None, "E126", style_guide.Decision.Selected), + (["W"], None, None, "E126", style_guide.Decision.Ignored), + (["E"], None, None, "W391", style_guide.Decision.Ignored), + (["E", "W"], ["E13"], None, "E131", style_guide.Decision.Ignored), + (None, ["E13"], None, "E131", style_guide.Decision.Ignored), + ( + None, + None, + ["W391"], + "E126", + style_guide.Decision.Ignored, + ), + ( + None, + None, + None, + "W391", + style_guide.Decision.Selected, + ), + ], +) +def test_decision_for( + select_list, ignore_list, extend_ignore, error_code, expected, +): + """Verify we decide when to report an error.""" + decider = style_guide.DecisionEngine( + create_options( + select=select_list, + ignore=ignore_list, + extend_ignore=extend_ignore, + ), + ) + + assert decider.decision_for(error_code) is expected + + +def test_implicitly_selected_and_implicitly_ignored_defers_to_length(): + decider = style_guide.DecisionEngine( + create_options( + # no options selected by user + select=None, + ignore=None, + extend_select=None, + extend_ignore=None, + # a plugin is installed and extends default ignore + extended_default_select=["P"], + extended_default_ignore=["P002"], + ), + ) + + assert decider.decision_for("P001") is style_guide.Decision.Selected + assert decider.decision_for("P002") is style_guide.Decision.Ignored + + +def test_user_can_extend_select_to_enable_plugin_default_ignored(): + decider = style_guide.DecisionEngine( + create_options( + # user options --extend-select=P002 + select=None, + ignore=None, + extend_select=["P002"], + extend_ignore=None, + # a plugin is installed and extends default ignore + extended_default_select=["P"], + extended_default_ignore=["P002"], + ), + ) + + assert decider.decision_for("P002") is style_guide.Decision.Selected + + +def test_plugin_extends_default_ignore_but_extend_selected(): + decider = style_guide.DecisionEngine( + create_options( + # user options --extend-select P002 --extend-ignore E501 + select=None, + ignore=None, + extend_select=["P002"], + extend_ignore=["E501"], + # a plugin is installed and extends default ignore + extended_default_select=["P"], + extended_default_ignore=["P002"], + ), + ) + + assert decider.decision_for("P002") is style_guide.Decision.Selected diff --git a/src/flake8-main/flake8-main/tests/unit/test_defaults.py b/src/flake8-main/flake8-main/tests/unit/test_defaults.py new file mode 100644 index 0000000..822b8f0 --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_defaults.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import pytest + +from flake8.defaults import VALID_CODE_PREFIX + + +@pytest.mark.parametrize( + "s", + ( + "E", + "E1", + "E123", + "ABC", + "ABC1", + "ABC123", + ), +) +def test_valid_plugin_prefixes(s): + assert VALID_CODE_PREFIX.match(s) + + +@pytest.mark.parametrize( + "s", + ( + "", + "A1234", + "ABCD", + "abc", + "a-b", + "☃", + "A𝟗", + ), +) +def test_invalid_plugin_prefixes(s): + assert VALID_CODE_PREFIX.match(s) is None diff --git a/src/flake8-main/flake8-main/tests/unit/test_discover_files.py b/src/flake8-main/flake8-main/tests/unit/test_discover_files.py new file mode 100644 index 0000000..ea55ccc --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_discover_files.py @@ -0,0 +1,166 @@ +from __future__ import annotations + +import os.path + +import pytest + +from flake8 import utils +from flake8.discover_files import _filenames_from +from flake8.discover_files import expand_paths + + +@pytest.fixture +def files_dir(tmpdir): + """Create test dir for testing filenames_from.""" + with tmpdir.as_cwd(): + tmpdir.join("a/b/c.py").ensure() + tmpdir.join("a/b/d.py").ensure() + tmpdir.join("a/b/e/f.py").ensure() + yield tmpdir + + +def _noop(path): + return False + + +def _normpath(s): + return s.replace("/", os.sep) + + +def _normpaths(pths): + return {_normpath(pth) for pth in pths} + + +@pytest.mark.usefixtures("files_dir") +def test_filenames_from_a_directory(): + """Verify that filenames_from walks a directory.""" + filenames = set(_filenames_from(_normpath("a/b/"), predicate=_noop)) + # should include all files + expected = _normpaths(("a/b/c.py", "a/b/d.py", "a/b/e/f.py")) + assert filenames == expected + + +@pytest.mark.usefixtures("files_dir") +def test_filenames_from_a_directory_with_a_predicate(): + """Verify that predicates filter filenames_from.""" + filenames = set( + _filenames_from( + arg=_normpath("a/b/"), + predicate=lambda path: path.endswith(_normpath("b/c.py")), + ), + ) + # should not include c.py + expected = _normpaths(("a/b/d.py", "a/b/e/f.py")) + assert filenames == expected + + +@pytest.mark.usefixtures("files_dir") +def test_filenames_from_a_directory_with_a_predicate_from_the_current_dir(): + """Verify that predicates filter filenames_from.""" + filenames = set( + _filenames_from( + arg=_normpath("./a/b"), + predicate=lambda path: path == "c.py", + ), + ) + # none should have matched the predicate so all returned + expected = _normpaths(("./a/b/c.py", "./a/b/d.py", "./a/b/e/f.py")) + assert filenames == expected + + +@pytest.mark.usefixtures("files_dir") +def test_filenames_from_a_single_file(): + """Verify that we simply yield that filename.""" + filenames = set(_filenames_from(_normpath("a/b/c.py"), predicate=_noop)) + assert filenames == {_normpath("a/b/c.py")} + + +def test_filenames_from_a_single_file_does_not_exist(): + """Verify that a passed filename which does not exist is returned back.""" + filenames = set(_filenames_from(_normpath("d/n/e.py"), predicate=_noop)) + assert filenames == {_normpath("d/n/e.py")} + + +def test_filenames_from_exclude_doesnt_exclude_directory_names(tmpdir): + """Verify that we don't greedily exclude subdirs.""" + tmpdir.join("1/dont_return_me.py").ensure() + tmpdir.join("2/1/return_me.py").ensure() + exclude = [tmpdir.join("1").strpath] + + def predicate(pth): + return utils.fnmatch(os.path.abspath(pth), exclude) + + with tmpdir.as_cwd(): + filenames = list(_filenames_from(".", predicate=predicate)) + assert filenames == [os.path.join(".", "2", "1", "return_me.py")] + + +def test_filenames_from_predicate_applies_to_initial_arg(tmp_path): + """Test that the predicate is also applied to the passed argument.""" + fname = str(tmp_path.joinpath("f.py")) + ret = tuple(_filenames_from(fname, predicate=lambda _: True)) + assert ret == () + + +def test_filenames_from_predicate_applies_to_dirname(tmp_path): + """Test that the predicate can filter whole directories.""" + a_dir = tmp_path.joinpath("a") + a_dir.mkdir() + a_dir.joinpath("b.py").touch() + + b_py = tmp_path.joinpath("b.py") + b_py.touch() + + def predicate(p): + # filter out the /a directory + return p.endswith("a") + + ret = tuple(_filenames_from(str(tmp_path), predicate=predicate)) + assert ret == (str(b_py),) + + +def _expand_paths( + *, + paths=(".",), + stdin_display_name="stdin", + filename_patterns=("*.py",), + exclude=(), +): + return set( + expand_paths( + paths=paths, + stdin_display_name=stdin_display_name, + filename_patterns=filename_patterns, + exclude=exclude, + ), + ) + + +@pytest.mark.usefixtures("files_dir") +def test_expand_paths_honors_exclude(): + expected = _normpaths(("./a/b/c.py", "./a/b/e/f.py")) + assert _expand_paths(exclude=["d.py"]) == expected + + +@pytest.mark.usefixtures("files_dir") +def test_expand_paths_defaults_to_dot(): + expected = _normpaths(("./a/b/c.py", "./a/b/d.py", "./a/b/e/f.py")) + assert _expand_paths(paths=()) == expected + + +def test_default_stdin_name_is_not_filtered(): + assert _expand_paths(paths=("-",)) == {"-"} + + +def test_alternate_stdin_name_is_filtered(): + ret = _expand_paths( + paths=("-",), + stdin_display_name="wat", + exclude=("wat",), + ) + assert ret == set() + + +def test_filename_included_even_if_not_matching_include(tmp_path): + some_file = str(tmp_path.joinpath("some/file")) + assert _expand_paths(paths=(some_file,)) == {some_file} diff --git a/src/flake8-main/flake8-main/tests/unit/test_exceptions.py b/src/flake8-main/flake8-main/tests/unit/test_exceptions.py new file mode 100644 index 0000000..99b298b --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_exceptions.py @@ -0,0 +1,36 @@ +"""Tests for the flake8.exceptions module.""" +from __future__ import annotations + +import pickle + +import pytest + +from flake8 import exceptions + + +@pytest.mark.parametrize( + "err", + ( + exceptions.FailedToLoadPlugin( + plugin_name="plugin_name", + exception=ValueError("boom!"), + ), + exceptions.PluginRequestedUnknownParameters( + plugin_name="plugin_name", + exception=ValueError("boom!"), + ), + exceptions.PluginExecutionFailed( + filename="filename.py", + plugin_name="plugin_name", + exception=ValueError("boom!"), + ), + ), +) +def test_pickleable(err): + """Ensure that our exceptions can cross pickle boundaries.""" + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + new_err = pickle.loads(pickle.dumps(err, protocol=proto)) + assert str(err) == str(new_err) + orig_e = err.original_exception + new_e = new_err.original_exception + assert (type(orig_e), orig_e.args) == (type(new_e), new_e.args) diff --git a/src/flake8-main/flake8-main/tests/unit/test_file_checker.py b/src/flake8-main/flake8-main/tests/unit/test_file_checker.py new file mode 100644 index 0000000..627a936 --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_file_checker.py @@ -0,0 +1,65 @@ +"""Unit tests for the FileChecker class.""" +from __future__ import annotations + +import argparse +import importlib.metadata +from unittest import mock + +import pytest + +import flake8 +from flake8 import checker +from flake8.plugins import finder + + +@mock.patch("flake8.checker.FileChecker._make_processor", return_value=None) +def test_repr(*args): + """Verify we generate a correct repr.""" + file_checker = checker.FileChecker( + filename="example.py", + plugins=finder.Checkers([], [], []), + options=argparse.Namespace(), + ) + assert repr(file_checker) == "FileChecker for example.py" + + +def test_nonexistent_file(): + """Verify that checking non-existent file results in an error.""" + c = checker.FileChecker( + filename="example.py", + plugins=finder.Checkers([], [], []), + options=argparse.Namespace(), + ) + + assert c.processor is None + assert not c.should_process + assert len(c.results) == 1 + error = c.results[0] + assert error[0] == "E902" + + +def test_raises_exception_on_failed_plugin(tmp_path, default_options): + """Checks that a failing plugin results in PluginExecutionFailed.""" + fname = tmp_path.joinpath("t.py") + fname.touch() + plugin = finder.LoadedPlugin( + finder.Plugin( + "plugin-name", + "1.2.3", + importlib.metadata.EntryPoint("X", "dne:dne", "flake8.extension"), + ), + mock.Mock(side_effect=ValueError), + {}, + ) + fchecker = checker.FileChecker( + filename=str(fname), + plugins=finder.Checkers([], [], []), + options=default_options, + ) + with pytest.raises(flake8.exceptions.PluginExecutionFailed) as excinfo: + fchecker.run_check(plugin) + expected = ( + f'{fname}: "plugin-name[X]" failed during execution ' + f"due to ValueError()" + ) + assert str(excinfo.value) == expected diff --git a/src/flake8-main/flake8-main/tests/unit/test_file_processor.py b/src/flake8-main/flake8-main/tests/unit/test_file_processor.py new file mode 100644 index 0000000..22c5bcf --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_file_processor.py @@ -0,0 +1,392 @@ +"""Tests for the FileProcessor class.""" +from __future__ import annotations + +import ast +import tokenize +from unittest import mock + +import pytest + +from flake8 import processor + + +def test_read_lines_splits_lines(default_options): + """Verify that read_lines splits the lines of the file.""" + file_processor = processor.FileProcessor(__file__, default_options) + lines = file_processor.lines + assert len(lines) > 5 + assert lines[0].strip() == '"""Tests for the FileProcessor class."""' + + +def _lines_from_file(tmpdir, contents, options): + f = tmpdir.join("f.py") + # be careful to write the bytes exactly to avoid newline munging + f.write_binary(contents) + return processor.FileProcessor(f.strpath, options).lines + + +def test_read_lines_universal_newlines(tmpdir, default_options): + r"""Verify that line endings are translated to \n.""" + lines = _lines_from_file( + tmpdir, b"# coding: utf-8\r\nx = 1\r\n", default_options, + ) + assert lines == ["# coding: utf-8\n", "x = 1\n"] + + +def test_read_lines_incorrect_utf_16(tmpdir, default_options): + """Verify that an incorrectly encoded file is read as latin-1.""" + lines = _lines_from_file( + tmpdir, b"# coding: utf16\nx = 1\n", default_options, + ) + assert lines == ["# coding: utf16\n", "x = 1\n"] + + +def test_read_lines_unknown_encoding(tmpdir, default_options): + """Verify that an unknown encoding is still read as latin-1.""" + lines = _lines_from_file( + tmpdir, b"# coding: fake-encoding\nx = 1\n", default_options, + ) + assert lines == ["# coding: fake-encoding\n", "x = 1\n"] + + +@pytest.mark.parametrize( + "first_line", + [ + '\xEF\xBB\xBF"""Module docstring."""\n', + '\uFEFF"""Module docstring."""\n', + ], +) +def test_strip_utf_bom(first_line, default_options): + r"""Verify that we strip '\xEF\xBB\xBF' from the first line.""" + lines = [first_line] + file_processor = processor.FileProcessor("-", default_options, lines[:]) + assert file_processor.lines != lines + assert file_processor.lines[0] == '"""Module docstring."""\n' + + +@pytest.mark.parametrize( + "lines, expected", + [ + (['\xEF\xBB\xBF"""Module docstring."""\n'], False), + (['\uFEFF"""Module docstring."""\n'], False), + (["#!/usr/bin/python", "# flake8 is great", "a = 1"], False), + (["#!/usr/bin/python", "# flake8: noqa", "a = 1"], True), + (["#!/usr/bin/python", "# flake8:noqa", "a = 1"], True), + (["# flake8: noqa", "#!/usr/bin/python", "a = 1"], True), + (["# flake8:noqa", "#!/usr/bin/python", "a = 1"], True), + (["#!/usr/bin/python", "a = 1", "# flake8: noqa"], True), + (["#!/usr/bin/python", "a = 1", "# flake8:noqa"], True), + (["#!/usr/bin/python", "a = 1 # flake8: noqa"], False), + (["#!/usr/bin/python", "a = 1 # flake8:noqa"], False), + ], +) +def test_should_ignore_file(lines, expected, default_options): + """Verify that we ignore a file if told to.""" + file_processor = processor.FileProcessor("-", default_options, lines) + assert file_processor.should_ignore_file() is expected + + +def test_should_ignore_file_to_handle_disable_noqa(default_options): + """Verify that we ignore a file if told to.""" + lines = ["# flake8: noqa"] + file_processor = processor.FileProcessor("-", default_options, lines) + assert file_processor.should_ignore_file() is True + default_options.disable_noqa = True + file_processor = processor.FileProcessor("-", default_options, lines) + assert file_processor.should_ignore_file() is False + + +@mock.patch("flake8.utils.stdin_get_value") +def test_read_lines_from_stdin(stdin_get_value, default_options): + """Verify that we use our own utility function to retrieve stdin.""" + stdin_get_value.return_value = "" + processor.FileProcessor("-", default_options) + stdin_get_value.assert_called_once_with() + + +@mock.patch("flake8.utils.stdin_get_value") +def test_stdin_filename_attribute(stdin_get_value, default_options): + """Verify that we update the filename attribute.""" + stdin_get_value.return_value = "" + file_processor = processor.FileProcessor("-", default_options) + assert file_processor.filename == "stdin" + + +@mock.patch("flake8.utils.stdin_get_value") +def test_read_lines_uses_display_name(stdin_get_value, default_options): + """Verify that when processing stdin we use a display name if present.""" + default_options.stdin_display_name = "display_name.py" + stdin_get_value.return_value = "" + file_processor = processor.FileProcessor("-", default_options) + assert file_processor.filename == "display_name.py" + + +@mock.patch("flake8.utils.stdin_get_value") +def test_read_lines_ignores_empty_display_name( + stdin_get_value, + default_options, +): + """Verify that when processing stdin we use a display name if present.""" + stdin_get_value.return_value = "" + default_options.stdin_display_name = "" + file_processor = processor.FileProcessor("-", default_options) + assert file_processor.filename == "stdin" + + +def test_noqa_line_for(default_options): + """Verify we grab the correct line from the cached lines.""" + file_processor = processor.FileProcessor( + "-", + default_options, + lines=[ + "Line 1\n", + "Line 2\n", + "Line 3\n", + ], + ) + + for i in range(1, 4): + assert file_processor.noqa_line_for(i) == f"Line {i}\n" + + +def test_noqa_line_for_continuation(default_options): + """Verify that the correct "line" is retrieved for continuation.""" + src = '''\ +from foo \\ + import bar # 2 + +x = """ +hello +world +""" # 7 +''' + lines = src.splitlines(True) + file_processor = processor.FileProcessor("-", default_options, lines=lines) + + assert file_processor.noqa_line_for(0) is None + + l_1_2 = "from foo \\\n import bar # 2\n" + assert file_processor.noqa_line_for(1) == l_1_2 + assert file_processor.noqa_line_for(2) == l_1_2 + + assert file_processor.noqa_line_for(3) == "\n" + + l_4_7 = 'x = """\nhello\nworld\n""" # 7\n' + for i in (4, 5, 6, 7): + assert file_processor.noqa_line_for(i) == l_4_7 + + assert file_processor.noqa_line_for(8) is None + + +def test_noqa_line_for_no_eol_at_end_of_file(default_options): + """Verify that we properly handle noqa line at the end of the file.""" + src = "from foo \\\nimport bar" # no end of file newline + lines = src.splitlines(True) + file_processor = processor.FileProcessor("-", default_options, lines=lines) + + l_1_2 = "from foo \\\nimport bar" + assert file_processor.noqa_line_for(1) == l_1_2 + assert file_processor.noqa_line_for(2) == l_1_2 + + +def test_next_line(default_options): + """Verify we update the file_processor state for each new line.""" + file_processor = processor.FileProcessor( + "-", + default_options, + lines=[ + "Line 1", + "Line 2", + "Line 3", + ], + ) + + for i in range(1, 4): + assert file_processor.next_line() == f"Line {i}" + assert file_processor.line_number == i + + +@pytest.mark.parametrize( + "params, args, expected_kwargs", + [ + ( + {"blank_before": True, "blank_lines": True}, + {}, + {"blank_before": 0, "blank_lines": 0}, + ), + ( + {"noqa": True, "fake": True}, + {"fake": "foo"}, + {"noqa": False}, + ), + ( + {"blank_before": True, "blank_lines": True, "noqa": True}, + {"blank_before": 10, "blank_lines": 5, "noqa": True}, + {}, + ), + ({}, {"fake": "foo"}, {}), + ({"non-existent": False}, {"fake": "foo"}, {}), + ], +) +def test_keyword_arguments_for(params, args, expected_kwargs, default_options): + """Verify the keyword args are generated properly.""" + file_processor = processor.FileProcessor( + "-", + default_options, + lines=[ + "Line 1", + ], + ) + ret = file_processor.keyword_arguments_for(params, args) + + assert ret == expected_kwargs + + +def test_keyword_arguments_for_does_not_handle_attribute_errors( + default_options, +): + """Verify we re-raise AttributeErrors.""" + file_processor = processor.FileProcessor( + "-", + default_options, + lines=[ + "Line 1", + ], + ) + + with pytest.raises(AttributeError): + file_processor.keyword_arguments_for({"fake": True}, {}) + + +def test_processor_split_line(default_options): + file_processor = processor.FileProcessor( + "-", + default_options, + lines=[ + 'x = """\n', + "contents\n", + '"""\n', + ], + ) + token = tokenize.TokenInfo( + 3, + '"""\ncontents\n"""', + (1, 4), + (3, 3), + 'x = """\ncontents\n"""\n', + ) + expected = [('x = """\n', 1, True), ("contents\n", 2, True)] + assert file_processor.multiline is False + actual = [ + (line, file_processor.line_number, file_processor.multiline) + for line in file_processor.multiline_string(token) + ] + assert file_processor.multiline is False + assert expected == actual + assert file_processor.line_number == 3 + + +def test_build_ast(default_options): + """Verify the logic for how we build an AST for plugins.""" + file_processor = processor.FileProcessor( + "-", default_options, lines=["a = 1\n"], + ) + + module = file_processor.build_ast() + assert isinstance(module, ast.Module) + + +def test_next_logical_line_updates_the_previous_logical_line(default_options): + """Verify that we update our tracking of the previous logical line.""" + file_processor = processor.FileProcessor( + "-", default_options, lines=["a = 1\n"], + ) + + file_processor.indent_level = 1 + file_processor.logical_line = "a = 1" + assert file_processor.previous_logical == "" + assert file_processor.previous_indent_level == 0 + + file_processor.next_logical_line() + assert file_processor.previous_logical == "a = 1" + assert file_processor.previous_indent_level == 1 + + +def test_visited_new_blank_line(default_options): + """Verify we update the number of blank lines seen.""" + file_processor = processor.FileProcessor( + "-", default_options, lines=["a = 1\n"], + ) + + assert file_processor.blank_lines == 0 + file_processor.visited_new_blank_line() + assert file_processor.blank_lines == 1 + + +@pytest.mark.parametrize( + "string, expected", + [ + ('""', '""'), + ("''", "''"), + ('"a"', '"x"'), + ("'a'", "'x'"), + ('"x"', '"x"'), + ("'x'", "'x'"), + ('"abcdef"', '"xxxxxx"'), + ("'abcdef'", "'xxxxxx'"), + ('""""""', '""""""'), + ("''''''", "''''''"), + ('"""a"""', '"""x"""'), + ("'''a'''", "'''x'''"), + ('"""x"""', '"""x"""'), + ("'''x'''", "'''x'''"), + ('"""abcdef"""', '"""xxxxxx"""'), + ("'''abcdef'''", "'''xxxxxx'''"), + ('"""xxxxxx"""', '"""xxxxxx"""'), + ("'''xxxxxx'''", "'''xxxxxx'''"), + ], +) +def test_mutate_string(string, expected, default_options): + """Verify we appropriately mutate the string to sanitize it.""" + actual = processor.mutate_string(string) + assert expected == actual + + +@pytest.mark.parametrize( + "string, expected", + [ + (" ", 4), + (" ", 6), + ("\t", 8), + ("\t\t", 16), + (" \t", 8), + (" \t", 16), + ], +) +def test_expand_indent(string, expected): + """Verify we correctly measure the amount of indentation.""" + actual = processor.expand_indent(string) + assert expected == actual + + +@pytest.mark.parametrize( + "current_count, token_text, expected", + [ + (0, "(", 1), + (0, "[", 1), + (0, "{", 1), + (1, ")", 0), + (1, "]", 0), + (1, "}", 0), + (10, "+", 10), + ], +) +def test_count_parentheses(current_count, token_text, expected): + """Verify our arithmetic is correct.""" + assert processor.count_parentheses(current_count, token_text) == expected + + +def test_nonexistent_file(default_options): + """Verify that FileProcessor raises IOError when a file does not exist.""" + with pytest.raises(IOError): + processor.FileProcessor("foobar.py", default_options) diff --git a/src/flake8-main/flake8-main/tests/unit/test_filenameonly_formatter.py b/src/flake8-main/flake8-main/tests/unit/test_filenameonly_formatter.py new file mode 100644 index 0000000..77f75b9 --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_filenameonly_formatter.py @@ -0,0 +1,41 @@ +"""Tests for the FilenameOnly formatter object.""" +from __future__ import annotations + +import argparse + +from flake8.formatting import default +from flake8.violation import Violation + + +def options(**kwargs): + """Create an argparse.Namespace instance.""" + kwargs.setdefault("color", "auto") + kwargs.setdefault("output_file", None) + kwargs.setdefault("tee", False) + return argparse.Namespace(**kwargs) + + +def test_caches_filenames_already_printed(): + """Verify we cache filenames when we format them.""" + formatter = default.FilenameOnly(options()) + assert formatter.filenames_already_printed == set() + + formatter.format(Violation("code", "file.py", 1, 1, "text", "l")) + assert formatter.filenames_already_printed == {"file.py"} + + +def test_only_returns_a_string_once_from_format(): + """Verify format ignores the second error with the same filename.""" + formatter = default.FilenameOnly(options()) + error = Violation("code", "file.py", 1, 1, "text", "1") + + assert formatter.format(error) == "file.py" + assert formatter.format(error) is None + + +def test_show_source_returns_nothing(): + """Verify show_source returns nothing.""" + formatter = default.FilenameOnly(options()) + error = Violation("code", "file.py", 1, 1, "text", "1") + + assert formatter.show_source(error) is None diff --git a/src/flake8-main/flake8-main/tests/unit/test_legacy_api.py b/src/flake8-main/flake8-main/tests/unit/test_legacy_api.py new file mode 100644 index 0000000..c6af630 --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_legacy_api.py @@ -0,0 +1,112 @@ +"""Tests for Flake8's legacy API.""" +from __future__ import annotations + +from unittest import mock + +import pytest + +from flake8.api import legacy as api +from flake8.formatting import base as formatter + + +def test_styleguide_options(): + """Show that we proxy the StyleGuide.options attribute.""" + app = mock.Mock() + app.options = "options" + style_guide = api.StyleGuide(app) + assert style_guide.options == "options" + + +def test_styleguide_paths(): + """Show that we proxy the StyleGuide.paths attribute.""" + app = mock.Mock() + app.options.filenames = ["paths"] + style_guide = api.StyleGuide(app) + assert style_guide.paths == ["paths"] + + +def test_styleguide_check_files(): + """Verify we call the right application methods.""" + paths = ["foo", "bar"] + app = mock.Mock() + style_guide = api.StyleGuide(app) + report = style_guide.check_files(paths) + + assert app.options.filenames == paths + app.run_checks.assert_called_once_with() + app.report_errors.assert_called_once_with() + assert isinstance(report, api.Report) + + +def test_styleguide_excluded(): + """Verify we delegate to our file checker manager. + + When we add the parent argument, we don't check that is_path_excluded was + called only once. + """ + style_guide = api.get_style_guide(exclude=["file*", "*/parent/*"]) + assert not style_guide.excluded("unrelated.py") + assert style_guide.excluded("file.py") + assert style_guide.excluded("test.py", "parent") + + +def test_styleguide_init_report_does_nothing(): + """Verify if we use None that we don't call anything.""" + app = mock.Mock() + style_guide = api.StyleGuide(app) + style_guide.init_report() + assert app.make_formatter.called is False + assert app.make_guide.called is False + + +def test_styleguide_init_report_with_non_subclass(): + """Verify we raise a ValueError with non BaseFormatter subclasses.""" + app = mock.Mock() + style_guide = api.StyleGuide(app) + with pytest.raises(ValueError): + style_guide.init_report(object) # type: ignore + assert app.make_formatter.called is False + assert app.make_guide.called is False + + +def test_styleguide_init_report(): + """Verify we do the right incantation for the Application.""" + app = mock.Mock(guide="fake") + style_guide = api.StyleGuide(app) + + class FakeFormatter(formatter.BaseFormatter): + def format(self, *args): + raise NotImplementedError + + style_guide.init_report(FakeFormatter) + assert isinstance(app.formatter, FakeFormatter) + assert app.guide is None + app.make_guide.assert_called_once_with() + + +def test_styleguide_input_file(): + """Verify we call StyleGuide.check_files with the filename.""" + app = mock.Mock() + style_guide = api.StyleGuide(app) + with mock.patch.object(style_guide, "check_files") as check_files: + style_guide.input_file("file.py") + check_files.assert_called_once_with(["file.py"]) + + +def test_report_total_errors(): + """Verify total errors is just a proxy attribute.""" + app = mock.Mock(result_count="Fake count") + report = api.Report(app) + assert report.total_errors == "Fake count" + + +def test_report_get_statistics(): + """Verify that we use the statistics object.""" + stats = mock.Mock() + stats.statistics_for.return_value = [] + style_guide = mock.Mock(stats=stats) + app = mock.Mock(guide=style_guide) + + report = api.Report(app) + assert report.get_statistics("E") == [] + stats.statistics_for.assert_called_once_with("E") diff --git a/src/flake8-main/flake8-main/tests/unit/test_main_options.py b/src/flake8-main/flake8-main/tests/unit/test_main_options.py new file mode 100644 index 0000000..0b1fb69 --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_main_options.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from flake8.main import options + + +def test_stage1_arg_parser(): + stage1_parser = options.stage1_arg_parser() + opts, args = stage1_parser.parse_known_args( + ["--foo", "--verbose", "src", "setup.py", "--statistics", "--version"], + ) + + assert opts.verbose + assert args == ["--foo", "src", "setup.py", "--statistics", "--version"] + + +def test_stage1_arg_parser_ignores_help(): + stage1_parser = options.stage1_arg_parser() + _, args = stage1_parser.parse_known_args(["--help", "-h"]) + assert args == ["--help", "-h"] diff --git a/src/flake8-main/flake8-main/tests/unit/test_nothing_formatter.py b/src/flake8-main/flake8-main/tests/unit/test_nothing_formatter.py new file mode 100644 index 0000000..76929fd --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_nothing_formatter.py @@ -0,0 +1,31 @@ +"""Tests for the Nothing formatter obbject.""" +from __future__ import annotations + +import argparse + +from flake8.formatting import default +from flake8.violation import Violation + + +def options(**kwargs): + """Create an argparse.Namespace instance.""" + kwargs.setdefault("color", "auto") + kwargs.setdefault("output_file", None) + kwargs.setdefault("tee", False) + return argparse.Namespace(**kwargs) + + +def test_format_returns_nothing(): + """Verify Nothing.format returns None.""" + formatter = default.Nothing(options()) + error = Violation("code", "file.py", 1, 1, "text", "1") + + assert formatter.format(error) is None + + +def test_show_source_returns_nothing(): + """Verify Nothing.show_source returns None.""" + formatter = default.Nothing(options()) + error = Violation("code", "file.py", 1, 1, "text", "1") + + assert formatter.show_source(error) is None diff --git a/src/flake8-main/flake8-main/tests/unit/test_option.py b/src/flake8-main/flake8-main/tests/unit/test_option.py new file mode 100644 index 0000000..4b3070d --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_option.py @@ -0,0 +1,57 @@ +"""Unit tests for flake8.options.manager.Option.""" +from __future__ import annotations + +import functools +from unittest import mock + +import pytest + +from flake8.options import manager + + +def test_to_argparse(): + """Test conversion to an argparse arguments.""" + opt = manager.Option( + short_option_name="-t", + long_option_name="--test", + action="count", + parse_from_config=True, + normalize_paths=True, + ) + assert opt.normalize_paths is True + assert opt.parse_from_config is True + + args, kwargs = opt.to_argparse() + assert args == ["-t", "--test"] + assert kwargs == {"action": "count", "type": mock.ANY} + assert isinstance(kwargs["type"], functools.partial) + + +def test_to_argparse_creates_an_option_as_we_expect(): + """Show that we pass all keyword args to argparse.""" + opt = manager.Option("-t", "--test", action="count") + args, kwargs = opt.to_argparse() + assert args == ["-t", "--test"] + assert kwargs == {"action": "count"} + + +def test_config_name_generation(): + """Show that we generate the config name deterministically.""" + opt = manager.Option( + long_option_name="--some-very-long-option-name", + parse_from_config=True, + ) + + assert opt.config_name == "some_very_long_option_name" + + +def test_config_name_needs_long_option_name(): + """Show that we error out if the Option should be parsed from config.""" + with pytest.raises(ValueError): + manager.Option("-s", parse_from_config=True) + + +def test_dest_is_not_overridden(): + """Show that we do not override custom destinations.""" + opt = manager.Option("-s", "--short", dest="something_not_short") + assert opt.dest == "something_not_short" diff --git a/src/flake8-main/flake8-main/tests/unit/test_option_manager.py b/src/flake8-main/flake8-main/tests/unit/test_option_manager.py new file mode 100644 index 0000000..9904a2e --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_option_manager.py @@ -0,0 +1,210 @@ +"""Unit tests for flake.options.manager.OptionManager.""" +from __future__ import annotations + +import argparse +import os + +import pytest + +from flake8.main.options import JobsArgument +from flake8.options import manager + +TEST_VERSION = "3.0.0b1" + + +@pytest.fixture +def optmanager(): + """Generate a simple OptionManager with default test arguments.""" + return manager.OptionManager( + version=TEST_VERSION, + plugin_versions="", + parents=[], + formatter_names=[], + ) + + +def test_option_manager_creates_option_parser(optmanager): + """Verify that a new manager creates a new parser.""" + assert isinstance(optmanager.parser, argparse.ArgumentParser) + + +def test_option_manager_including_parent_options(): + """Verify parent options are included in the parsed options.""" + # GIVEN + parent_parser = argparse.ArgumentParser(add_help=False) + parent_parser.add_argument("--parent") + + # WHEN + optmanager = manager.OptionManager( + version=TEST_VERSION, + plugin_versions="", + parents=[parent_parser], + formatter_names=[], + ) + options = optmanager.parse_args(["--parent", "foo"]) + + # THEN + assert options.parent == "foo" + + +def test_parse_args_forwarding_default_values(optmanager): + """Verify default provided values are present in the final result.""" + namespace = argparse.Namespace(foo="bar") + options = optmanager.parse_args([], namespace) + assert options.foo == "bar" + + +def test_parse_args_forwarding_type_coercion(optmanager): + """Verify default provided values are type converted from add_option.""" + optmanager.add_option("--foo", type=int) + namespace = argparse.Namespace(foo="5") + options = optmanager.parse_args([], namespace) + assert options.foo == 5 + + +def test_add_option_short_option_only(optmanager): + """Verify the behaviour of adding a short-option only.""" + assert optmanager.options == [] + assert optmanager.config_options_dict == {} + + optmanager.add_option("-s", help="Test short opt") + assert optmanager.options[0].short_option_name == "-s" + + +def test_add_option_long_option_only(optmanager): + """Verify the behaviour of adding a long-option only.""" + assert optmanager.options == [] + assert optmanager.config_options_dict == {} + + optmanager.add_option("--long", help="Test long opt") + assert optmanager.options[0].short_option_name is manager._ARG.NO + assert optmanager.options[0].long_option_name == "--long" + + +def test_add_short_and_long_option_names(optmanager): + """Verify the behaviour of using both short and long option names.""" + assert optmanager.options == [] + assert optmanager.config_options_dict == {} + + optmanager.add_option("-b", "--both", help="Test both opts") + assert optmanager.options[0].short_option_name == "-b" + assert optmanager.options[0].long_option_name == "--both" + + +def test_add_option_with_custom_args(optmanager): + """Verify that add_option handles custom Flake8 parameters.""" + assert optmanager.options == [] + assert optmanager.config_options_dict == {} + + optmanager.add_option("--parse", parse_from_config=True) + optmanager.add_option("--commas", comma_separated_list=True) + optmanager.add_option("--files", normalize_paths=True) + + attrs = ["parse_from_config", "comma_separated_list", "normalize_paths"] + for option, attr in zip(optmanager.options, attrs): + assert getattr(option, attr) is True + + +def test_parse_args_normalize_path(optmanager): + """Show that parse_args handles path normalization.""" + assert optmanager.options == [] + assert optmanager.config_options_dict == {} + + optmanager.add_option("--config", normalize_paths=True) + + options = optmanager.parse_args(["--config", "../config.ini"]) + assert options.config == os.path.abspath("../config.ini") + + +def test_parse_args_handles_comma_separated_defaults(optmanager): + """Show that parse_args handles defaults that are comma-separated.""" + assert optmanager.options == [] + assert optmanager.config_options_dict == {} + + optmanager.add_option( + "--exclude", default="E123,W234", comma_separated_list=True, + ) + + options = optmanager.parse_args([]) + assert options.exclude == ["E123", "W234"] + + +def test_parse_args_handles_comma_separated_lists(optmanager): + """Show that parse_args handles user-specified comma-separated lists.""" + assert optmanager.options == [] + assert optmanager.config_options_dict == {} + + optmanager.add_option( + "--exclude", default="E123,W234", comma_separated_list=True, + ) + + options = optmanager.parse_args(["--exclude", "E201,W111,F280"]) + assert options.exclude == ["E201", "W111", "F280"] + + +def test_parse_args_normalize_paths(optmanager): + """Verify parse_args normalizes a comma-separated list of paths.""" + assert optmanager.options == [] + assert optmanager.config_options_dict == {} + + optmanager.add_option( + "--extra-config", normalize_paths=True, comma_separated_list=True, + ) + + options = optmanager.parse_args( + ["--extra-config", "../config.ini,tox.ini,flake8/some-other.cfg"], + ) + assert options.extra_config == [ + os.path.abspath("../config.ini"), + "tox.ini", + os.path.abspath("flake8/some-other.cfg"), + ] + + +def test_extend_default_ignore(optmanager): + """Verify that we update the extended default ignore list.""" + assert optmanager.extended_default_ignore == [] + + optmanager.extend_default_ignore(["T100", "T101", "T102"]) + assert optmanager.extended_default_ignore == ["T100", "T101", "T102"] + + +@pytest.mark.parametrize( + ("s", "is_auto", "n_jobs"), + ( + ("auto", True, -1), + ("4", False, 4), + ), +) +def test_parse_valid_jobs_argument(s, is_auto, n_jobs): + """Test that --jobs properly parses valid arguments.""" + jobs_opt = JobsArgument(s) + assert is_auto == jobs_opt.is_auto + assert n_jobs == jobs_opt.n_jobs + + +def test_parse_invalid_jobs_argument(optmanager, capsys): + """Test that --jobs properly rejects invalid arguments.""" + namespace = argparse.Namespace() + optmanager.add_option("--jobs", type=JobsArgument) + with pytest.raises(SystemExit): + optmanager.parse_args(["--jobs=foo"], namespace) + out, err = capsys.readouterr() + output = out + err + expected = ( + "\nflake8: error: argument --jobs: " + "'foo' must be 'auto' or an integer.\n" + ) + assert expected in output + + +def test_jobs_argument_str(): + """Test that JobsArgument has a correct __str__.""" + assert str(JobsArgument("auto")) == "auto" + assert str(JobsArgument("123")) == "123" + + +def test_jobs_argument_repr(): + """Test that JobsArgument has a correct __repr__.""" + assert repr(JobsArgument("auto")) == "JobsArgument('auto')" + assert repr(JobsArgument("123")) == "JobsArgument('123')" diff --git a/src/flake8-main/flake8-main/tests/unit/test_options_config.py b/src/flake8-main/flake8-main/tests/unit/test_options_config.py new file mode 100644 index 0000000..d73f471 --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_options_config.py @@ -0,0 +1,259 @@ +from __future__ import annotations + +import configparser +import os.path +from unittest import mock + +import pytest + +from flake8 import exceptions +from flake8.main.options import register_default_options +from flake8.options import config +from flake8.options.manager import OptionManager + + +def test_config_not_found_returns_none(tmp_path): + assert config._find_config_file(str(tmp_path)) is None + + +def test_config_file_without_section_is_not_considered(tmp_path): + tmp_path.joinpath("setup.cfg").touch() + + assert config._find_config_file(str(tmp_path)) is None + + +def test_config_file_with_parse_error_is_not_considered(tmp_path, caplog): + # the syntax error here is deliberately to trigger a partial parse + # https://github.com/python/cpython/issues/95546 + tmp_path.joinpath("setup.cfg").write_text("[flake8]\nx = 1\n...") + + assert config._find_config_file(str(tmp_path)) is None + + assert len(caplog.record_tuples) == 1 + ((mod, level, msg),) = caplog.record_tuples + assert (mod, level) == ("flake8.options.config", 30) + assert msg.startswith("ignoring unparseable config ") + + +def test_config_file_with_encoding_error_is_not_considered(tmp_path, caplog): + tmp_path.joinpath("setup.cfg").write_bytes(b"\xa0\xef\xfe\x12") + + assert config._find_config_file(str(tmp_path)) is None + + assert len(caplog.record_tuples) == 1 + ((mod, level, msg),) = caplog.record_tuples + assert (mod, level) == ("flake8.options.config", 30) + assert msg.startswith("ignoring unparseable config ") + + +@pytest.mark.parametrize("cfg_name", ("setup.cfg", "tox.ini", ".flake8")) +def test_find_config_file_exists_at_path(tmp_path, cfg_name): + expected = tmp_path.joinpath(cfg_name) + expected.write_text("[flake8]") + + assert config._find_config_file(str(tmp_path)) == str(expected) + + +@pytest.mark.parametrize("section", ("flake8", "flake8:local-plugins")) +def test_find_config_either_section(tmp_path, section): + expected = tmp_path.joinpath("setup.cfg") + expected.write_text(f"[{section}]") + + assert config._find_config_file(str(tmp_path)) == str(expected) + + +def test_find_config_searches_upwards(tmp_path): + subdir = tmp_path.joinpath("d") + subdir.mkdir() + + expected = tmp_path.joinpath("setup.cfg") + expected.write_text("[flake8]") + + assert config._find_config_file(str(subdir)) == str(expected) + + +def test_find_config_ignores_homedir(tmp_path): + subdir = tmp_path.joinpath("d") + subdir.mkdir() + + tmp_path.joinpath(".flake8").write_text("[flake8]") + + with mock.patch.object(os.path, "expanduser", return_value=str(tmp_path)): + assert config._find_config_file(str(subdir)) is None + + +def test_find_config_ignores_unknown_homedir(tmp_path): + subdir = tmp_path.joinpath("d") + + with mock.patch.object(os.path, "expanduser", return_value=str(subdir)): + assert config._find_config_file(str(tmp_path)) is None + + +def test_load_config_config_specified_skips_discovery(tmpdir): + tmpdir.join("setup.cfg").write("[flake8]\nindent-size=2\n") + custom_cfg = tmpdir.join("custom.cfg") + custom_cfg.write("[flake8]\nindent-size=8\n") + + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config(str(custom_cfg), [], isolated=False) + + assert cfg.get("flake8", "indent-size") == "8" + assert cfg_dir == str(tmpdir) + + +def test_load_config_no_config_file_does_discovery(tmpdir): + tmpdir.join("setup.cfg").write("[flake8]\nindent-size=2\n") + + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config(None, [], isolated=False) + + assert cfg.get("flake8", "indent-size") == "2" + assert cfg_dir == str(tmpdir) + + +def test_load_config_no_config_found_sets_cfg_dir_to_pwd(tmpdir): + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config(None, [], isolated=False) + + assert cfg.sections() == [] + assert cfg_dir == str(tmpdir) + + +def test_load_config_isolated_ignores_configuration(tmpdir): + tmpdir.join("setup.cfg").write("[flake8]\nindent-size=2\n") + + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config(None, [], isolated=True) + + assert cfg.sections() == [] + assert cfg_dir == str(tmpdir) + + +def test_load_config_append_config(tmpdir): + tmpdir.join("setup.cfg").write("[flake8]\nindent-size=2\n") + other = tmpdir.join("other.cfg") + other.write("[flake8]\nindent-size=8\n") + + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config(None, [str(other)], isolated=False) + + assert cfg.get("flake8", "indent-size") == "8" + assert cfg_dir == str(tmpdir) + + +NON_ASCII_CONFIG = "# ☃\n[flake8]\nindent-size=8\n" + + +def test_load_auto_config_utf8(tmpdir): + tmpdir.join("setup.cfg").write_text(NON_ASCII_CONFIG, encoding="UTF-8") + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config(None, [], isolated=False) + assert cfg["flake8"]["indent-size"] == "8" + + +def test_load_explicit_config_utf8(tmpdir): + tmpdir.join("t.cfg").write_text(NON_ASCII_CONFIG, encoding="UTF-8") + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config("t.cfg", [], isolated=False) + assert cfg["flake8"]["indent-size"] == "8" + + +def test_load_extra_config_utf8(tmpdir): + tmpdir.join("setup.cfg").write("[flake8]\nindent-size=2\n") + tmpdir.join("t.cfg").write_text(NON_ASCII_CONFIG, encoding="UTF-8") + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config(None, ["t.cfg"], isolated=False) + assert cfg["flake8"]["indent-size"] == "8" + + +@pytest.fixture +def opt_manager(): + ret = OptionManager( + version="123", plugin_versions="", parents=[], formatter_names=[], + ) + register_default_options(ret) + return ret + + +def test_parse_config_no_values(tmp_path, opt_manager): + cfg = configparser.RawConfigParser() + ret = config.parse_config(opt_manager, cfg, tmp_path) + assert ret == {} + + +def test_parse_config_typed_values(tmp_path, opt_manager): + cfg = configparser.RawConfigParser() + cfg.add_section("flake8") + cfg.set("flake8", "indent_size", "2") + cfg.set("flake8", "hang_closing", "true") + # test normalizing dashed-options + cfg.set("flake8", "extend-exclude", "d/1,d/2") + + ret = config.parse_config(opt_manager, cfg, str(tmp_path)) + assert ret == { + "indent_size": 2, + "hang_closing": True, + "extend_exclude": [ + str(tmp_path.joinpath("d/1")), + str(tmp_path.joinpath("d/2")), + ], + } + + +def test_parse_config_ignores_unknowns(tmp_path, opt_manager, caplog): + cfg = configparser.RawConfigParser() + cfg.add_section("flake8") + cfg.set("flake8", "wat", "wat") + + ret = config.parse_config(opt_manager, cfg, str(tmp_path)) + assert ret == {} + + assert caplog.record_tuples == [ + ( + "flake8.options.config", + 10, + 'Option "wat" is not registered. Ignoring.', + ), + ] + + +def test_load_config_missing_file_raises_exception(capsys): + with pytest.raises(exceptions.ExecutionError): + config.load_config("foo.cfg", []) + + +def test_load_config_missing_append_config_raise_exception(): + with pytest.raises(exceptions.ExecutionError): + config.load_config(None, ["dont_exist_config.cfg"], isolated=False) + + +def test_invalid_ignore_codes_raise_error(tmpdir, opt_manager): + tmpdir.join("setup.cfg").write("[flake8]\nignore = E203, //comment") + with tmpdir.as_cwd(): + cfg, _ = config.load_config("setup.cfg", [], isolated=False) + + with pytest.raises(ValueError) as excinfo: + config.parse_config(opt_manager, cfg, tmpdir) + + expected = ( + "Error code '//comment' supplied to 'ignore' option " + "does not match '^[A-Z]{1,3}[0-9]{0,3}$'" + ) + (msg,) = excinfo.value.args + assert msg == expected + + +def test_invalid_extend_ignore_codes_raise_error(tmpdir, opt_manager): + tmpdir.join("setup.cfg").write("[flake8]\nextend-ignore = E203, //comment") + with tmpdir.as_cwd(): + cfg, _ = config.load_config("setup.cfg", [], isolated=False) + + with pytest.raises(ValueError) as excinfo: + config.parse_config(opt_manager, cfg, tmpdir) + + expected = ( + "Error code '//comment' supplied to 'extend-ignore' option " + "does not match '^[A-Z]{1,3}[0-9]{0,3}$'" + ) + (msg,) = excinfo.value.args + assert msg == expected diff --git a/src/flake8-main/flake8-main/tests/unit/test_pyflakes_codes.py b/src/flake8-main/flake8-main/tests/unit/test_pyflakes_codes.py new file mode 100644 index 0000000..444008a --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_pyflakes_codes.py @@ -0,0 +1,35 @@ +"""Tests of pyflakes monkey patches.""" +from __future__ import annotations + +import ast + +import pyflakes + +from flake8.plugins import pyflakes as pyflakes_shim + + +def test_all_pyflakes_messages_have_flake8_codes_assigned(): + """Verify all PyFlakes messages have error codes assigned.""" + messages = { + name + for name, obj in vars(pyflakes.messages).items() + if name[0].isupper() and obj.message + } + assert messages == set(pyflakes_shim.FLAKE8_PYFLAKES_CODES) + + +def test_undefined_local_code(): + """In pyflakes 2.1.0 this code's string formatting was changed.""" + src = """\ +import sys + +def f(): + sys = sys +""" + tree = ast.parse(src) + checker = pyflakes_shim.FlakesChecker(tree, "t.py") + message_texts = [s for _, _, s, _ in checker.run()] + assert message_texts == [ + "F823 local variable 'sys' defined in enclosing scope on line 1 referenced before assignment", # noqa: E501 + "F841 local variable 'sys' is assigned to but never used", + ] diff --git a/src/flake8-main/flake8-main/tests/unit/test_statistics.py b/src/flake8-main/flake8-main/tests/unit/test_statistics.py new file mode 100644 index 0000000..261f360 --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_statistics.py @@ -0,0 +1,124 @@ +"""Tests for the statistics module in Flake8.""" +from __future__ import annotations + +import pytest + +from flake8 import statistics as stats +from flake8.violation import Violation + +DEFAULT_ERROR_CODE = "E100" +DEFAULT_FILENAME = "file.py" +DEFAULT_TEXT = "Default text" + + +def make_error(**kwargs): + """Create errors with a bunch of default values.""" + kwargs.setdefault("code", DEFAULT_ERROR_CODE) + kwargs.setdefault("filename", DEFAULT_FILENAME) + kwargs.setdefault("line_number", 1) + kwargs.setdefault("column_number", 1) + kwargs.setdefault("text", DEFAULT_TEXT) + return Violation(**kwargs, physical_line=None) + + +def test_key_creation(): + """Verify how we create Keys from Errors.""" + key = stats.Key.create_from(make_error()) + assert key == (DEFAULT_FILENAME, DEFAULT_ERROR_CODE) + assert key.filename == DEFAULT_FILENAME + assert key.code == DEFAULT_ERROR_CODE + + +@pytest.mark.parametrize( + "code, filename, args, expected_result", + [ + # Error prefix matches + ("E123", "file000.py", ("E", None), True), + ("E123", "file000.py", ("E1", None), True), + ("E123", "file000.py", ("E12", None), True), + ("E123", "file000.py", ("E123", None), True), + # Error prefix and filename match + ("E123", "file000.py", ("E", "file000.py"), True), + ("E123", "file000.py", ("E1", "file000.py"), True), + ("E123", "file000.py", ("E12", "file000.py"), True), + ("E123", "file000.py", ("E123", "file000.py"), True), + # Error prefix does not match + ("E123", "file000.py", ("W", None), False), + # Error prefix matches but filename does not + ("E123", "file000.py", ("E", "file001.py"), False), + # Error prefix does not match but filename does + ("E123", "file000.py", ("W", "file000.py"), False), + # Neither error prefix match nor filename + ("E123", "file000.py", ("W", "file001.py"), False), + ], +) +def test_key_matching(code, filename, args, expected_result): + """Verify Key#matches behaves as we expect with fthe above input.""" + key = stats.Key.create_from(make_error(code=code, filename=filename)) + assert key.matches(*args) is expected_result + + +def test_statistic_creation(): + """Verify how we create Statistic objects from Errors.""" + stat = stats.Statistic.create_from(make_error()) + assert stat.error_code == DEFAULT_ERROR_CODE + assert stat.message == DEFAULT_TEXT + assert stat.filename == DEFAULT_FILENAME + assert stat.count == 0 + + +def test_statistic_increment(): + """Verify we update the count.""" + stat = stats.Statistic.create_from(make_error()) + assert stat.count == 0 + stat.increment() + assert stat.count == 1 + + +def test_recording_statistics(): + """Verify that we appropriately create a new Statistic and store it.""" + aggregator = stats.Statistics() + assert list(aggregator.statistics_for("E")) == [] + aggregator.record(make_error()) + storage = aggregator._store + for key, value in storage.items(): + assert isinstance(key, stats.Key) + assert isinstance(value, stats.Statistic) + + assert storage[stats.Key(DEFAULT_FILENAME, DEFAULT_ERROR_CODE)].count == 1 + + +def test_statistics_for_single_record(): + """Show we can retrieve the only statistic recorded.""" + aggregator = stats.Statistics() + assert list(aggregator.statistics_for("E")) == [] + aggregator.record(make_error()) + statistics = list(aggregator.statistics_for("E")) + assert len(statistics) == 1 + assert isinstance(statistics[0], stats.Statistic) + + +def test_statistics_for_filters_by_filename(): + """Show we can retrieve the only statistic recorded.""" + aggregator = stats.Statistics() + assert list(aggregator.statistics_for("E")) == [] + aggregator.record(make_error()) + aggregator.record(make_error(filename="example.py")) + + statistics = list(aggregator.statistics_for("E", DEFAULT_FILENAME)) + assert len(statistics) == 1 + assert isinstance(statistics[0], stats.Statistic) + + +def test_statistic_for_retrieves_more_than_one_value(): + """Show this works for more than a couple statistic values.""" + aggregator = stats.Statistics() + for i in range(50): + aggregator.record(make_error(code=f"E1{i:02d}")) + aggregator.record(make_error(code=f"W2{i:02d}")) + + statistics = list(aggregator.statistics_for("E")) + assert len(statistics) == 50 + + statistics = list(aggregator.statistics_for("W22")) + assert len(statistics) == 10 diff --git a/src/flake8-main/flake8-main/tests/unit/test_style_guide.py b/src/flake8-main/flake8-main/tests/unit/test_style_guide.py new file mode 100644 index 0000000..c66cfd2 --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_style_guide.py @@ -0,0 +1,147 @@ +"""Tests for the flake8.style_guide.StyleGuide class.""" +from __future__ import annotations + +import argparse +from unittest import mock + +import pytest + +from flake8 import statistics +from flake8 import style_guide +from flake8 import utils +from flake8.formatting import base + + +def create_options(**kwargs): + """Create and return an instance of argparse.Namespace.""" + kwargs.setdefault("select", []) + kwargs.setdefault("extended_default_select", []) + kwargs.setdefault("extended_default_ignore", []) + kwargs.setdefault("extend_select", []) + kwargs.setdefault("ignore", []) + kwargs.setdefault("extend_ignore", []) + kwargs.setdefault("disable_noqa", False) + kwargs.setdefault("enable_extensions", []) + kwargs.setdefault("per_file_ignores", []) + return argparse.Namespace(**kwargs) + + +def test_handle_error_does_not_raise_type_errors(): + """Verify that we handle our inputs better.""" + formatter = mock.create_autospec(base.BaseFormatter, instance=True) + guide = style_guide.StyleGuide( + create_options(select=["T111"], ignore=[]), + formatter=formatter, + stats=statistics.Statistics(), + ) + + assert 1 == guide.handle_error( + "T111", "file.py", 1, 1, "error found", "a = 1", + ) + + +def test_style_guide_manager(): + """Verify how the StyleGuideManager creates a default style guide.""" + formatter = mock.create_autospec(base.BaseFormatter, instance=True) + options = create_options() + guide = style_guide.StyleGuideManager(options, formatter=formatter) + assert guide.default_style_guide.options is options + assert len(guide.style_guides) == 1 + + +PER_FILE_IGNORES_UNPARSED = [ + "first_file.py:W9", + "second_file.py:F4,F9", + "third_file.py:E3", + "sub_dir/*:F4", +] + + +@pytest.mark.parametrize( + "style_guide_file,filename,expected", + [ + ("first_file.py", "first_file.py", True), + ("first_file.py", "second_file.py", False), + ("sub_dir/*.py", "first_file.py", False), + ("sub_dir/*.py", "sub_dir/file.py", True), + ("sub_dir/*.py", "other_dir/file.py", False), + ], +) +def test_style_guide_applies_to(style_guide_file, filename, expected): + """Verify that we match a file to its style guide.""" + formatter = mock.create_autospec(base.BaseFormatter, instance=True) + options = create_options() + guide = style_guide.StyleGuide( + options, + formatter=formatter, + stats=statistics.Statistics(), + filename=style_guide_file, + ) + assert guide.applies_to(filename) is expected + + +def test_style_guide_manager_pre_file_ignores_parsing(): + """Verify how the StyleGuideManager creates a default style guide.""" + formatter = mock.create_autospec(base.BaseFormatter, instance=True) + options = create_options(per_file_ignores=PER_FILE_IGNORES_UNPARSED) + guide = style_guide.StyleGuideManager(options, formatter=formatter) + assert len(guide.style_guides) == 5 + expected = [ + utils.normalize_path(p) + for p in [ + "first_file.py", + "second_file.py", + "third_file.py", + "sub_dir/*", + ] + ] + assert expected == [g.filename for g in guide.style_guides[1:]] + + +@pytest.mark.parametrize( + "ignores,violation,filename,handle_error_return", + [ + (["E1", "E2"], "F401", "first_file.py", 1), + (["E1", "E2"], "E121", "first_file.py", 0), + (["E1", "E2"], "F401", "second_file.py", 0), + (["E1", "E2"], "F401", "third_file.py", 1), + (["E1", "E2"], "E311", "third_file.py", 0), + (["E1", "E2"], "F401", "sub_dir/file.py", 0), + ], +) +def test_style_guide_manager_pre_file_ignores( + ignores, violation, filename, handle_error_return, +): + """Verify how the StyleGuideManager creates a default style guide.""" + formatter = mock.create_autospec(base.BaseFormatter, instance=True) + options = create_options( + ignore=ignores, + select=["E", "F", "W"], + per_file_ignores=PER_FILE_IGNORES_UNPARSED, + ) + guide = style_guide.StyleGuideManager(options, formatter=formatter) + assert ( + guide.handle_error(violation, filename, 1, 1, "Fake text") + == handle_error_return + ) + + +@pytest.mark.parametrize( + "filename,expected", + [ + ("first_file.py", utils.normalize_path("first_file.py")), + ("second_file.py", utils.normalize_path("second_file.py")), + ("third_file.py", utils.normalize_path("third_file.py")), + ("fourth_file.py", None), + ("sub_dir/__init__.py", utils.normalize_path("sub_dir/*")), + ("other_dir/__init__.py", None), + ], +) +def test_style_guide_manager_style_guide_for(filename, expected): + """Verify the style guide selection function.""" + formatter = mock.create_autospec(base.BaseFormatter, instance=True) + options = create_options(per_file_ignores=PER_FILE_IGNORES_UNPARSED) + guide = style_guide.StyleGuideManager(options, formatter=formatter) + + file_guide = guide.style_guide_for(filename) + assert file_guide.filename == expected diff --git a/src/flake8-main/flake8-main/tests/unit/test_utils.py b/src/flake8-main/flake8-main/tests/unit/test_utils.py new file mode 100644 index 0000000..82eef63 --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_utils.py @@ -0,0 +1,213 @@ +"""Tests for flake8's utils module.""" +from __future__ import annotations + +import io +import logging +import os +import sys +from unittest import mock + +import pytest + +from flake8 import exceptions +from flake8 import utils + +RELATIVE_PATHS = ["flake8", "pep8", "pyflakes", "mccabe"] + + +@pytest.mark.parametrize( + "value,expected", + [ + ("E123,\n\tW234,\n E206", ["E123", "W234", "E206"]), + ("E123,W234,E206", ["E123", "W234", "E206"]), + ("E123 W234 E206", ["E123", "W234", "E206"]), + ("E123\nW234 E206", ["E123", "W234", "E206"]), + ("E123\nW234\nE206", ["E123", "W234", "E206"]), + ("E123,W234,E206,", ["E123", "W234", "E206"]), + ("E123,W234,E206, ,\n", ["E123", "W234", "E206"]), + ("E123,W234,,E206,,", ["E123", "W234", "E206"]), + ("E123, W234,, E206,,", ["E123", "W234", "E206"]), + ("E123,,W234,,E206,,", ["E123", "W234", "E206"]), + ("", []), + ], +) +def test_parse_comma_separated_list(value, expected): + """Verify that similar inputs produce identical outputs.""" + assert utils.parse_comma_separated_list(value) == expected + + +@pytest.mark.parametrize( + ("value", "expected"), + ( + # empty option configures nothing + ("", []), + (" ", []), + ("\n\n\n", []), + # basic case + ( + "f.py:E123", + [("f.py", ["E123"])], + ), + # multiple filenames, multiple codes + ( + "f.py,g.py:E,F", + [("f.py", ["E", "F"]), ("g.py", ["E", "F"])], + ), + # demonstrate that whitespace is not important around tokens + ( + " f.py , g.py : E , F ", + [("f.py", ["E", "F"]), ("g.py", ["E", "F"])], + ), + # whitespace can separate groups of configuration + ( + "f.py:E g.py:F", + [("f.py", ["E"]), ("g.py", ["F"])], + ), + # newlines can separate groups of configuration + ( + "f.py: E\ng.py: F\n", + [("f.py", ["E"]), ("g.py", ["F"])], + ), + # whitespace can be used in place of commas + ( + "f.py g.py: E F", + [("f.py", ["E", "F"]), ("g.py", ["E", "F"])], + ), + # go ahead, indent your codes + ( + "f.py:\n E,F\ng.py:\n G,H", + [("f.py", ["E", "F"]), ("g.py", ["G", "H"])], + ), + # capitalized filenames are ok too + ( + "F.py,G.py: F,G", + [("F.py", ["F", "G"]), ("G.py", ["F", "G"])], + ), + # it's easier to allow zero filenames or zero codes than forbid it + (":E", []), + ("f.py:", []), + (":E f.py:F", [("f.py", ["F"])]), + ("f.py: g.py:F", [("g.py", ["F"])]), + ("f.py:E:", []), + ("f.py:E.py:", []), + ("f.py:Eg.py:F", [("Eg.py", ["F"])]), + # sequences are also valid (?) + ( + ["f.py:E,F", "g.py:G,H"], + [("f.py", ["E", "F"]), ("g.py", ["G", "H"])], + ), + # six-digits codes are allowed + ( + "f.py: ABC123", + [("f.py", ["ABC123"])], + ), + ), +) +def test_parse_files_to_codes_mapping(value, expected): + """Test parsing of valid files-to-codes mappings.""" + assert utils.parse_files_to_codes_mapping(value) == expected + + +@pytest.mark.parametrize( + "value", + ( + # code while looking for filenames + "E123", + "f.py,E123", + "f.py E123", + # eof while looking for filenames + "f.py", + "f.py:E,g.py" + # colon while looking for codes + "f.py::", + # no separator between + "f.py:E1F1", + ), +) +def test_invalid_file_list(value): + """Test parsing of invalid files-to-codes mappings.""" + with pytest.raises(exceptions.ExecutionError): + utils.parse_files_to_codes_mapping(value) + + +@pytest.mark.parametrize( + "value,expected", + [ + ("flake8", "flake8"), + (".", os.path.abspath(".")), + ("../flake8", os.path.abspath("../flake8")), + ("flake8/", os.path.abspath("flake8")), + ], +) +def test_normalize_path(value, expected): + """Verify that we normalize paths provided to the tool.""" + assert utils.normalize_path(value) == expected + + +@pytest.mark.parametrize( + "value,expected", + [ + ( + ["flake8", "pep8", "pyflakes", "mccabe"], + ["flake8", "pep8", "pyflakes", "mccabe"], + ), + ( + ["../flake8", "../pep8", "../pyflakes", "../mccabe"], + [os.path.abspath(f"../{p}") for p in RELATIVE_PATHS], + ), + ], +) +def test_normalize_paths(value, expected): + """Verify we normalizes a sequence of paths provided to the tool.""" + assert utils.normalize_paths(value) == expected + + +def test_matches_filename_for_excluding_dotfiles(): + """Verify that `.` and `..` are not matched by `.*`.""" + logger = logging.Logger(__name__) + assert not utils.matches_filename(".", (".*",), "", logger) + assert not utils.matches_filename("..", (".*",), "", logger) + + +@pytest.mark.parametrize( + "filename,patterns,expected", + [ + ("foo.py", [], True), + ("foo.py", ["*.pyc"], False), + ("foo.pyc", ["*.pyc"], True), + ("foo.pyc", ["*.swp", "*.pyc", "*.py"], True), + ], +) +def test_fnmatch(filename, patterns, expected): + """Verify that our fnmatch wrapper works as expected.""" + assert utils.fnmatch(filename, patterns) is expected + + +def test_stdin_get_value_crlf(): + """Ensure that stdin is normalized from crlf to lf.""" + stdin = io.TextIOWrapper(io.BytesIO(b"1\r\n2\r\n"), "UTF-8") + with mock.patch.object(sys, "stdin", stdin): + assert utils.stdin_get_value.__wrapped__() == "1\n2\n" + + +def test_stdin_unknown_coding_token(): + """Ensure we produce source even for unknown encodings.""" + stdin = io.TextIOWrapper(io.BytesIO(b"# coding: unknown\n"), "UTF-8") + with mock.patch.object(sys, "stdin", stdin): + assert utils.stdin_get_value.__wrapped__() == "# coding: unknown\n" + + +@pytest.mark.parametrize( + ("s", "expected"), + ( + ("", ""), + ("my-plugin", "my-plugin"), + ("MyPlugin", "myplugin"), + ("my_plugin", "my-plugin"), + ("my.plugin", "my-plugin"), + ("my--plugin", "my-plugin"), + ("my__plugin", "my-plugin"), + ), +) +def test_normalize_pypi_name(s, expected): + assert utils.normalize_pypi_name(s) == expected diff --git a/src/flake8-main/flake8-main/tests/unit/test_violation.py b/src/flake8-main/flake8-main/tests/unit/test_violation.py new file mode 100644 index 0000000..1b4852b --- /dev/null +++ b/src/flake8-main/flake8-main/tests/unit/test_violation.py @@ -0,0 +1,53 @@ +"""Tests for the flake8.violation.Violation class.""" +from __future__ import annotations + +from unittest import mock + +import pytest + +from flake8.violation import Violation + + +@pytest.mark.parametrize( + "error_code,physical_line,expected_result", + [ + ("E111", "a = 1", False), + ("E121", "a = 1 # noqa: E111", False), + ("E121", "a = 1 # noqa: E111,W123,F821", False), + ("E111", "a = 1 # noqa: E111,W123,F821", True), + ("W123", "a = 1 # noqa: E111,W123,F821", True), + ("W123", "a = 1 # noqa: E111, W123,F821", True), + ("E111", "a = 1 # noqa: E11,W123,F821", True), + ("E121", "a = 1 # noqa:E111,W123,F821", False), + ("E111", "a = 1 # noqa:E111,W123,F821", True), + ("W123", "a = 1 # noqa:E111,W123,F821", True), + ("W123", "a = 1 # noqa:E111, W123,F821", True), + ("E111", "a = 1 # noqa:E11,W123,F821", True), + ("E111", "a = 1 # noqa, analysis:ignore", True), + ("E111", "a = 1 # noqa analysis:ignore", True), + ("E111", "a = 1 # noqa - We do not care", True), + ("E111", "a = 1 # noqa: We do not care", True), + ("E111", "a = 1 # noqa:We do not care", True), + ("ABC123", "a = 1 # noqa: ABC123", True), + ("E111", "a = 1 # noqa: ABC123", False), + ("ABC123", "a = 1 # noqa: ABC124", False), + ], +) +def test_is_inline_ignored(error_code, physical_line, expected_result): + """Verify that we detect inline usage of ``# noqa``.""" + error = Violation(error_code, "filename.py", 1, 1, "error text", None) + # We want `None` to be passed as the physical line so we actually use our + # monkey-patched linecache.getline value. + + with mock.patch("linecache.getline", return_value=physical_line): + assert error.is_inline_ignored(False) is expected_result + + +def test_disable_is_inline_ignored(): + """Verify that is_inline_ignored exits immediately if disabling NoQA.""" + error = Violation("E121", "filename.py", 1, 1, "error text", "line") + + with mock.patch("linecache.getline") as getline: + assert error.is_inline_ignored(True) is False + + assert getline.called is False diff --git a/src/flake8-main/flake8-main/tox.ini b/src/flake8-main/flake8-main/tox.ini new file mode 100644 index 0000000..539b5c4 --- /dev/null +++ b/src/flake8-main/flake8-main/tox.ini @@ -0,0 +1,126 @@ +[tox] +minversion=2.3.1 +envlist = py,flake8,linters,docs + +[testenv] +deps = + pytest!=3.0.5,!=5.2.3 + coverage>=6 + covdefaults +commands = + coverage run -m pytest {posargs} + coverage report + # ensure 100% coverage of tests + coverage report --fail-under 100 --include tests/* + +# Dogfood our current main version +[testenv:dogfood] +skip_install = true +deps = + wheel +commands = + python setup.py -qq bdist_wheel + pip install --force-reinstall -U --pre --find-links ./dist/ flake8 + flake8 --version + flake8 src/flake8/ tests/ setup.py + +# Linters +[testenv:flake8] +skip_install = true +deps = + flake8 + flake8-bugbear + flake8-docstrings>=1.3.1 + flake8-typing-imports>=1.1 + pep8-naming +commands = + flake8 src/flake8/ tests/ setup.py + +[testenv:pylint] +skip_install = true +deps = + pyflakes + pylint!=2.5.0 +commands = + pylint src/flake8 + +[testenv:doc8] +skip_install = true +deps = + sphinx + doc8 +commands = + doc8 docs/source/ + +[testenv:pre-commit] +skip_install = true +deps = pre-commit +commands = + pre-commit run --all-files --show-diff-on-failure + +[testenv:bandit] +skip_install = true +deps = + bandit +commands = + bandit -r src/flake8/ -c .bandit.yml + +[testenv:linters] +skip_install = true +deps = + {[testenv:flake8]deps} + {[testenv:pylint]deps} + {[testenv:doc8]deps} + {[testenv:readme]deps} + {[testenv:bandit]deps} +commands = + {[testenv:flake8]commands} + {[testenv:pylint]commands} + {[testenv:doc8]commands} + {[testenv:readme]commands} + {[testenv:bandit]commands} + +# Documentation +[testenv:docs] +deps = + -rdocs/source/requirements.txt +commands = + sphinx-build -E -W -c docs/source/ -b html docs/source/ docs/build/html + +[testenv:serve-docs] +skip_install = true +changedir = docs/build/html +deps = +commands = + python -m http.server {posargs} + +[testenv:readme] +deps = + readme_renderer +commands = + python setup.py check -r -s + +# Release tooling +[testenv:build] +skip_install = true +deps = + wheel + setuptools +commands = + python setup.py -q sdist bdist_wheel + +[testenv:release] +skip_install = true +deps = + {[testenv:build]deps} + twine >= 1.5.0 +commands = + {[testenv:build]commands} + twine upload --skip-existing dist/* + +[flake8] +extend-ignore = E203 +per-file-ignores = + src/flake8/formatting/_windows_color.py: N806 + tests/*: D +max-complexity = 10 diff --git a/src/node_modules/.bin/mkdirp b/src/node_modules/.bin/mkdirp new file mode 100644 index 0000000..6ba5765 --- /dev/null +++ b/src/node_modules/.bin/mkdirp @@ -0,0 +1,12 @@ +#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../mkdirp/bin/cmd.js" "$@" +else + exec node "$basedir/../mkdirp/bin/cmd.js" "$@" +fi diff --git a/src/node_modules/.bin/mkdirp.cmd b/src/node_modules/.bin/mkdirp.cmd new file mode 100644 index 0000000..a865dd9 --- /dev/null +++ b/src/node_modules/.bin/mkdirp.cmd @@ -0,0 +1,17 @@ +@ECHO off +GOTO start +:find_dp0 +SET dp0=%~dp0 +EXIT /b +:start +SETLOCAL +CALL :find_dp0 + +IF EXIST "%dp0%\node.exe" ( + SET "_prog=%dp0%\node.exe" +) ELSE ( + SET "_prog=node" + SET PATHEXT=%PATHEXT:;.JS;=;% +) + +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\mkdirp\bin\cmd.js" %* diff --git a/src/node_modules/.bin/mkdirp.ps1 b/src/node_modules/.bin/mkdirp.ps1 new file mode 100644 index 0000000..911e854 --- /dev/null +++ b/src/node_modules/.bin/mkdirp.ps1 @@ -0,0 +1,28 @@ +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../mkdirp/bin/cmd.js" $args + } else { + & "$basedir/node$exe" "$basedir/../mkdirp/bin/cmd.js" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node$exe" "$basedir/../mkdirp/bin/cmd.js" $args + } else { + & "node$exe" "$basedir/../mkdirp/bin/cmd.js" $args + } + $ret=$LASTEXITCODE +} +exit $ret diff --git a/src/node_modules/.package-lock.json b/src/node_modules/.package-lock.json new file mode 100644 index 0000000..a9e12a6 --- /dev/null +++ b/src/node_modules/.package-lock.json @@ -0,0 +1,943 @@ +{ + "name": "src", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmmirror.com/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/src/node_modules/accepts/HISTORY.md b/src/node_modules/accepts/HISTORY.md new file mode 100644 index 0000000..627a81d --- /dev/null +++ b/src/node_modules/accepts/HISTORY.md @@ -0,0 +1,250 @@ +2.0.0 / 2024-08-31 +================== + + * Drop node <18 support + * deps: mime-types@^3.0.0 + * deps: negotiator@^1.0.0 + +1.3.8 / 2022-02-02 +================== + + * deps: mime-types@~2.1.34 + - deps: mime-db@~1.51.0 + * deps: negotiator@0.6.3 + +1.3.7 / 2019-04-29 +================== + + * deps: negotiator@0.6.2 + - Fix sorting charset, encoding, and language with extra parameters + +1.3.6 / 2019-04-28 +================== + + * deps: mime-types@~2.1.24 + - deps: mime-db@~1.40.0 + +1.3.5 / 2018-02-28 +================== + + * deps: mime-types@~2.1.18 + - deps: mime-db@~1.33.0 + +1.3.4 / 2017-08-22 +================== + + * deps: mime-types@~2.1.16 + - deps: mime-db@~1.29.0 + +1.3.3 / 2016-05-02 +================== + + * deps: mime-types@~2.1.11 + - deps: mime-db@~1.23.0 + * deps: negotiator@0.6.1 + - perf: improve `Accept` parsing speed + - perf: improve `Accept-Charset` parsing speed + - perf: improve `Accept-Encoding` parsing speed + - perf: improve `Accept-Language` parsing speed + +1.3.2 / 2016-03-08 +================== + + * deps: mime-types@~2.1.10 + - Fix extension of `application/dash+xml` + - Update primary extension for `audio/mp4` + - deps: mime-db@~1.22.0 + +1.3.1 / 2016-01-19 +================== + + * deps: mime-types@~2.1.9 + - deps: mime-db@~1.21.0 + +1.3.0 / 2015-09-29 +================== + + * deps: mime-types@~2.1.7 + - deps: mime-db@~1.19.0 + * deps: negotiator@0.6.0 + - Fix including type extensions in parameters in `Accept` parsing + - Fix parsing `Accept` parameters with quoted equals + - Fix parsing `Accept` parameters with quoted semicolons + - Lazy-load modules from main entry point + - perf: delay type concatenation until needed + - perf: enable strict mode + - perf: hoist regular expressions + - perf: remove closures getting spec properties + - perf: remove a closure from media type parsing + - perf: remove property delete from media type parsing + +1.2.13 / 2015-09-06 +=================== + + * deps: mime-types@~2.1.6 + - deps: mime-db@~1.18.0 + +1.2.12 / 2015-07-30 +=================== + + * deps: mime-types@~2.1.4 + - deps: mime-db@~1.16.0 + +1.2.11 / 2015-07-16 +=================== + + * deps: mime-types@~2.1.3 + - deps: mime-db@~1.15.0 + +1.2.10 / 2015-07-01 +=================== + + * deps: mime-types@~2.1.2 + - deps: mime-db@~1.14.0 + +1.2.9 / 2015-06-08 +================== + + * deps: mime-types@~2.1.1 + - perf: fix deopt during mapping + +1.2.8 / 2015-06-07 +================== + + * deps: mime-types@~2.1.0 + - deps: mime-db@~1.13.0 + * perf: avoid argument reassignment & argument slice + * perf: avoid negotiator recursive construction + * perf: enable strict mode + * perf: remove unnecessary bitwise operator + +1.2.7 / 2015-05-10 +================== + + * deps: negotiator@0.5.3 + - Fix media type parameter matching to be case-insensitive + +1.2.6 / 2015-05-07 +================== + + * deps: mime-types@~2.0.11 + - deps: mime-db@~1.9.1 + * deps: negotiator@0.5.2 + - Fix comparing media types with quoted values + - Fix splitting media types with quoted commas + +1.2.5 / 2015-03-13 +================== + + * deps: mime-types@~2.0.10 + - deps: mime-db@~1.8.0 + +1.2.4 / 2015-02-14 +================== + + * Support Node.js 0.6 + * deps: mime-types@~2.0.9 + - deps: mime-db@~1.7.0 + * deps: negotiator@0.5.1 + - Fix preference sorting to be stable for long acceptable lists + +1.2.3 / 2015-01-31 +================== + + * deps: mime-types@~2.0.8 + - deps: mime-db@~1.6.0 + +1.2.2 / 2014-12-30 +================== + + * deps: mime-types@~2.0.7 + - deps: mime-db@~1.5.0 + +1.2.1 / 2014-12-30 +================== + + * deps: mime-types@~2.0.5 + - deps: mime-db@~1.3.1 + +1.2.0 / 2014-12-19 +================== + + * deps: negotiator@0.5.0 + - Fix list return order when large accepted list + - Fix missing identity encoding when q=0 exists + - Remove dynamic building of Negotiator class + +1.1.4 / 2014-12-10 +================== + + * deps: mime-types@~2.0.4 + - deps: mime-db@~1.3.0 + +1.1.3 / 2014-11-09 +================== + + * deps: mime-types@~2.0.3 + - deps: mime-db@~1.2.0 + +1.1.2 / 2014-10-14 +================== + + * deps: negotiator@0.4.9 + - Fix error when media type has invalid parameter + +1.1.1 / 2014-09-28 +================== + + * deps: mime-types@~2.0.2 + - deps: mime-db@~1.1.0 + * deps: negotiator@0.4.8 + - Fix all negotiations to be case-insensitive + - Stable sort preferences of same quality according to client order + +1.1.0 / 2014-09-02 +================== + + * update `mime-types` + +1.0.7 / 2014-07-04 +================== + + * Fix wrong type returned from `type` when match after unknown extension + +1.0.6 / 2014-06-24 +================== + + * deps: negotiator@0.4.7 + +1.0.5 / 2014-06-20 +================== + + * fix crash when unknown extension given + +1.0.4 / 2014-06-19 +================== + + * use `mime-types` + +1.0.3 / 2014-06-11 +================== + + * deps: negotiator@0.4.6 + - Order by specificity when quality is the same + +1.0.2 / 2014-05-29 +================== + + * Fix interpretation when header not in request + * deps: pin negotiator@0.4.5 + +1.0.1 / 2014-01-18 +================== + + * Identity encoding isn't always acceptable + * deps: negotiator@~0.4.0 + +1.0.0 / 2013-12-27 +================== + + * Genesis diff --git a/src/node_modules/accepts/LICENSE b/src/node_modules/accepts/LICENSE new file mode 100644 index 0000000..0616607 --- /dev/null +++ b/src/node_modules/accepts/LICENSE @@ -0,0 +1,23 @@ +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/node_modules/accepts/README.md b/src/node_modules/accepts/README.md new file mode 100644 index 0000000..f3f10c4 --- /dev/null +++ b/src/node_modules/accepts/README.md @@ -0,0 +1,140 @@ +# accepts + +[![NPM Version][npm-version-image]][npm-url] +[![NPM Downloads][npm-downloads-image]][npm-url] +[![Node.js Version][node-version-image]][node-version-url] +[![Build Status][github-actions-ci-image]][github-actions-ci-url] +[![Test Coverage][coveralls-image]][coveralls-url] + +Higher level content negotiation based on [negotiator](https://www.npmjs.com/package/negotiator). +Extracted from [koa](https://www.npmjs.com/package/koa) for general use. + +In addition to negotiator, it allows: + +- Allows types as an array or arguments list, ie `(['text/html', 'application/json'])` + as well as `('text/html', 'application/json')`. +- Allows type shorthands such as `json`. +- Returns `false` when no types match +- Treats non-existent headers as `*` + +## Installation + +This is a [Node.js](https://nodejs.org/en/) module available through the +[npm registry](https://www.npmjs.com/). Installation is done using the +[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): + +```sh +$ npm install accepts +``` + +## API + +```js +var accepts = require('accepts') +``` + +### accepts(req) + +Create a new `Accepts` object for the given `req`. + +#### .charset(charsets) + +Return the first accepted charset. If nothing in `charsets` is accepted, +then `false` is returned. + +#### .charsets() + +Return the charsets that the request accepts, in the order of the client's +preference (most preferred first). + +#### .encoding(encodings) + +Return the first accepted encoding. If nothing in `encodings` is accepted, +then `false` is returned. + +#### .encodings() + +Return the encodings that the request accepts, in the order of the client's +preference (most preferred first). + +#### .language(languages) + +Return the first accepted language. If nothing in `languages` is accepted, +then `false` is returned. + +#### .languages() + +Return the languages that the request accepts, in the order of the client's +preference (most preferred first). + +#### .type(types) + +Return the first accepted type (and it is returned as the same text as what +appears in the `types` array). If nothing in `types` is accepted, then `false` +is returned. + +The `types` array can contain full MIME types or file extensions. Any value +that is not a full MIME type is passed to `require('mime-types').lookup`. + +#### .types() + +Return the types that the request accepts, in the order of the client's +preference (most preferred first). + +## Examples + +### Simple type negotiation + +This simple example shows how to use `accepts` to return a different typed +respond body based on what the client wants to accept. The server lists it's +preferences in order and will get back the best match between the client and +server. + +```js +var accepts = require('accepts') +var http = require('http') + +function app (req, res) { + var accept = accepts(req) + + // the order of this list is significant; should be server preferred order + switch (accept.type(['json', 'html'])) { + case 'json': + res.setHeader('Content-Type', 'application/json') + res.write('{"hello":"world!"}') + break + case 'html': + res.setHeader('Content-Type', 'text/html') + res.write('hello, world!') + break + default: + // the fallback is text/plain, so no need to specify it above + res.setHeader('Content-Type', 'text/plain') + res.write('hello, world!') + break + } + + res.end() +} + +http.createServer(app).listen(3000) +``` + +You can test this out with the cURL program: +```sh +curl -I -H'Accept: text/html' http://localhost:3000/ +``` + +## License + +[MIT](LICENSE) + +[coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/accepts/master +[coveralls-url]: https://coveralls.io/r/jshttp/accepts?branch=master +[github-actions-ci-image]: https://badgen.net/github/checks/jshttp/accepts/master?label=ci +[github-actions-ci-url]: https://github.com/jshttp/accepts/actions/workflows/ci.yml +[node-version-image]: https://badgen.net/npm/node/accepts +[node-version-url]: https://nodejs.org/en/download +[npm-downloads-image]: https://badgen.net/npm/dm/accepts +[npm-url]: https://npmjs.org/package/accepts +[npm-version-image]: https://badgen.net/npm/v/accepts diff --git a/src/node_modules/accepts/index.js b/src/node_modules/accepts/index.js new file mode 100644 index 0000000..4f2840c --- /dev/null +++ b/src/node_modules/accepts/index.js @@ -0,0 +1,238 @@ +/*! + * accepts + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module dependencies. + * @private + */ + +var Negotiator = require('negotiator') +var mime = require('mime-types') + +/** + * Module exports. + * @public + */ + +module.exports = Accepts + +/** + * Create a new Accepts object for the given req. + * + * @param {object} req + * @public + */ + +function Accepts (req) { + if (!(this instanceof Accepts)) { + return new Accepts(req) + } + + this.headers = req.headers + this.negotiator = new Negotiator(req) +} + +/** + * Check if the given `type(s)` is acceptable, returning + * the best match when true, otherwise `undefined`, in which + * case you should respond with 406 "Not Acceptable". + * + * The `type` value may be a single mime type string + * such as "application/json", the extension name + * such as "json" or an array `["json", "html", "text/plain"]`. When a list + * or array is given the _best_ match, if any is returned. + * + * Examples: + * + * // Accept: text/html + * this.types('html'); + * // => "html" + * + * // Accept: text/*, application/json + * this.types('html'); + * // => "html" + * this.types('text/html'); + * // => "text/html" + * this.types('json', 'text'); + * // => "json" + * this.types('application/json'); + * // => "application/json" + * + * // Accept: text/*, application/json + * this.types('image/png'); + * this.types('png'); + * // => undefined + * + * // Accept: text/*;q=.5, application/json + * this.types(['html', 'json']); + * this.types('html', 'json'); + * // => "json" + * + * @param {String|Array} types... + * @return {String|Array|Boolean} + * @public + */ + +Accepts.prototype.type = +Accepts.prototype.types = function (types_) { + var types = types_ + + // support flattened arguments + if (types && !Array.isArray(types)) { + types = new Array(arguments.length) + for (var i = 0; i < types.length; i++) { + types[i] = arguments[i] + } + } + + // no types, return all requested types + if (!types || types.length === 0) { + return this.negotiator.mediaTypes() + } + + // no accept header, return first given type + if (!this.headers.accept) { + return types[0] + } + + var mimes = types.map(extToMime) + var accepts = this.negotiator.mediaTypes(mimes.filter(validMime)) + var first = accepts[0] + + return first + ? types[mimes.indexOf(first)] + : false +} + +/** + * Return accepted encodings or best fit based on `encodings`. + * + * Given `Accept-Encoding: gzip, deflate` + * an array sorted by quality is returned: + * + * ['gzip', 'deflate'] + * + * @param {String|Array} encodings... + * @return {String|Array} + * @public + */ + +Accepts.prototype.encoding = +Accepts.prototype.encodings = function (encodings_) { + var encodings = encodings_ + + // support flattened arguments + if (encodings && !Array.isArray(encodings)) { + encodings = new Array(arguments.length) + for (var i = 0; i < encodings.length; i++) { + encodings[i] = arguments[i] + } + } + + // no encodings, return all requested encodings + if (!encodings || encodings.length === 0) { + return this.negotiator.encodings() + } + + return this.negotiator.encodings(encodings)[0] || false +} + +/** + * Return accepted charsets or best fit based on `charsets`. + * + * Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5` + * an array sorted by quality is returned: + * + * ['utf-8', 'utf-7', 'iso-8859-1'] + * + * @param {String|Array} charsets... + * @return {String|Array} + * @public + */ + +Accepts.prototype.charset = +Accepts.prototype.charsets = function (charsets_) { + var charsets = charsets_ + + // support flattened arguments + if (charsets && !Array.isArray(charsets)) { + charsets = new Array(arguments.length) + for (var i = 0; i < charsets.length; i++) { + charsets[i] = arguments[i] + } + } + + // no charsets, return all requested charsets + if (!charsets || charsets.length === 0) { + return this.negotiator.charsets() + } + + return this.negotiator.charsets(charsets)[0] || false +} + +/** + * Return accepted languages or best fit based on `langs`. + * + * Given `Accept-Language: en;q=0.8, es, pt` + * an array sorted by quality is returned: + * + * ['es', 'pt', 'en'] + * + * @param {String|Array} langs... + * @return {Array|String} + * @public + */ + +Accepts.prototype.lang = +Accepts.prototype.langs = +Accepts.prototype.language = +Accepts.prototype.languages = function (languages_) { + var languages = languages_ + + // support flattened arguments + if (languages && !Array.isArray(languages)) { + languages = new Array(arguments.length) + for (var i = 0; i < languages.length; i++) { + languages[i] = arguments[i] + } + } + + // no languages, return all requested languages + if (!languages || languages.length === 0) { + return this.negotiator.languages() + } + + return this.negotiator.languages(languages)[0] || false +} + +/** + * Convert extnames to mime. + * + * @param {String} type + * @return {String} + * @private + */ + +function extToMime (type) { + return type.indexOf('/') === -1 + ? mime.lookup(type) + : type +} + +/** + * Check if mime is valid. + * + * @param {String} type + * @return {Boolean} + * @private + */ + +function validMime (type) { + return typeof type === 'string' +} diff --git a/src/node_modules/accepts/package.json b/src/node_modules/accepts/package.json new file mode 100644 index 0000000..b35b262 --- /dev/null +++ b/src/node_modules/accepts/package.json @@ -0,0 +1,47 @@ +{ + "name": "accepts", + "description": "Higher-level content negotiation", + "version": "2.0.0", + "contributors": [ + "Douglas Christopher Wilson ", + "Jonathan Ong (http://jongleberry.com)" + ], + "license": "MIT", + "repository": "jshttp/accepts", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "devDependencies": { + "deep-equal": "1.0.1", + "eslint": "7.32.0", + "eslint-config-standard": "14.1.1", + "eslint-plugin-import": "2.25.4", + "eslint-plugin-markdown": "2.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-promise": "4.3.1", + "eslint-plugin-standard": "4.1.0", + "mocha": "9.2.0", + "nyc": "15.1.0" + }, + "files": [ + "LICENSE", + "HISTORY.md", + "index.js" + ], + "engines": { + "node": ">= 0.6" + }, + "scripts": { + "lint": "eslint .", + "test": "mocha --reporter spec --check-leaks --bail test/", + "test-ci": "nyc --reporter=lcov --reporter=text npm test", + "test-cov": "nyc --reporter=html --reporter=text npm test" + }, + "keywords": [ + "content", + "negotiation", + "accept", + "accepts" + ] +} diff --git a/src/node_modules/append-field/.npmignore b/src/node_modules/append-field/.npmignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/src/node_modules/append-field/.npmignore @@ -0,0 +1 @@ +node_modules/ diff --git a/src/node_modules/append-field/LICENSE b/src/node_modules/append-field/LICENSE new file mode 100644 index 0000000..14b1f89 --- /dev/null +++ b/src/node_modules/append-field/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Linus Unnebäck + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/node_modules/append-field/README.md b/src/node_modules/append-field/README.md new file mode 100644 index 0000000..62b901b --- /dev/null +++ b/src/node_modules/append-field/README.md @@ -0,0 +1,44 @@ +# `append-field` + +A [W3C HTML JSON forms spec](http://www.w3.org/TR/html-json-forms/) compliant +field appender (for lack of a better name). Useful for people implementing +`application/x-www-form-urlencoded` and `multipart/form-data` parsers. + +It works best on objects created with `Object.create(null)`. Otherwise it might +conflict with variables from the prototype (e.g. `hasOwnProperty`). + +## Installation + +```sh +npm install --save append-field +``` + +## Usage + +```javascript +var appendField = require('append-field') +var obj = Object.create(null) + +appendField(obj, 'pets[0][species]', 'Dahut') +appendField(obj, 'pets[0][name]', 'Hypatia') +appendField(obj, 'pets[1][species]', 'Felis Stultus') +appendField(obj, 'pets[1][name]', 'Billie') + +console.log(obj) +``` + +```text +{ pets: + [ { species: 'Dahut', name: 'Hypatia' }, + { species: 'Felis Stultus', name: 'Billie' } ] } +``` + +## API + +### `appendField(store, key, value)` + +Adds the field named `key` with the value `value` to the object `store`. + +## License + +MIT diff --git a/src/node_modules/append-field/index.js b/src/node_modules/append-field/index.js new file mode 100644 index 0000000..fc5acc8 --- /dev/null +++ b/src/node_modules/append-field/index.js @@ -0,0 +1,12 @@ +var parsePath = require('./lib/parse-path') +var setValue = require('./lib/set-value') + +function appendField (store, key, value) { + var steps = parsePath(key) + + steps.reduce(function (context, step) { + return setValue(context, step, context[step.key], value) + }, store) +} + +module.exports = appendField diff --git a/src/node_modules/append-field/lib/parse-path.js b/src/node_modules/append-field/lib/parse-path.js new file mode 100644 index 0000000..31d6179 --- /dev/null +++ b/src/node_modules/append-field/lib/parse-path.js @@ -0,0 +1,53 @@ +var reFirstKey = /^[^\[]*/ +var reDigitPath = /^\[(\d+)\]/ +var reNormalPath = /^\[([^\]]+)\]/ + +function parsePath (key) { + function failure () { + return [{ type: 'object', key: key, last: true }] + } + + var firstKey = reFirstKey.exec(key)[0] + if (!firstKey) return failure() + + var len = key.length + var pos = firstKey.length + var tail = { type: 'object', key: firstKey } + var steps = [tail] + + while (pos < len) { + var m + + if (key[pos] === '[' && key[pos + 1] === ']') { + pos += 2 + tail.append = true + if (pos !== len) return failure() + continue + } + + m = reDigitPath.exec(key.substring(pos)) + if (m !== null) { + pos += m[0].length + tail.nextType = 'array' + tail = { type: 'array', key: parseInt(m[1], 10) } + steps.push(tail) + continue + } + + m = reNormalPath.exec(key.substring(pos)) + if (m !== null) { + pos += m[0].length + tail.nextType = 'object' + tail = { type: 'object', key: m[1] } + steps.push(tail) + continue + } + + return failure() + } + + tail.last = true + return steps +} + +module.exports = parsePath diff --git a/src/node_modules/append-field/lib/set-value.js b/src/node_modules/append-field/lib/set-value.js new file mode 100644 index 0000000..c15e873 --- /dev/null +++ b/src/node_modules/append-field/lib/set-value.js @@ -0,0 +1,64 @@ +function valueType (value) { + if (value === undefined) return 'undefined' + if (Array.isArray(value)) return 'array' + if (typeof value === 'object') return 'object' + return 'scalar' +} + +function setLastValue (context, step, currentValue, entryValue) { + switch (valueType(currentValue)) { + case 'undefined': + if (step.append) { + context[step.key] = [entryValue] + } else { + context[step.key] = entryValue + } + break + case 'array': + context[step.key].push(entryValue) + break + case 'object': + return setLastValue(currentValue, { type: 'object', key: '', last: true }, currentValue[''], entryValue) + case 'scalar': + context[step.key] = [context[step.key], entryValue] + break + } + + return context +} + +function setValue (context, step, currentValue, entryValue) { + if (step.last) return setLastValue(context, step, currentValue, entryValue) + + var obj + switch (valueType(currentValue)) { + case 'undefined': + if (step.nextType === 'array') { + context[step.key] = [] + } else { + context[step.key] = Object.create(null) + } + return context[step.key] + case 'object': + return context[step.key] + case 'array': + if (step.nextType === 'array') { + return currentValue + } + + obj = Object.create(null) + context[step.key] = obj + currentValue.forEach(function (item, i) { + if (item !== undefined) obj['' + i] = item + }) + + return obj + case 'scalar': + obj = Object.create(null) + obj[''] = currentValue + context[step.key] = obj + return obj + } +} + +module.exports = setValue diff --git a/src/node_modules/append-field/package.json b/src/node_modules/append-field/package.json new file mode 100644 index 0000000..8d6e716 --- /dev/null +++ b/src/node_modules/append-field/package.json @@ -0,0 +1,19 @@ +{ + "name": "append-field", + "version": "1.0.0", + "license": "MIT", + "author": "Linus Unnebäck ", + "main": "index.js", + "devDependencies": { + "mocha": "^2.2.4", + "standard": "^6.0.5", + "testdata-w3c-json-form": "^0.2.0" + }, + "scripts": { + "test": "standard && mocha" + }, + "repository": { + "type": "git", + "url": "http://github.com/LinusU/node-append-field.git" + } +} diff --git a/src/node_modules/append-field/test/forms.js b/src/node_modules/append-field/test/forms.js new file mode 100644 index 0000000..dd6fbc9 --- /dev/null +++ b/src/node_modules/append-field/test/forms.js @@ -0,0 +1,19 @@ +/* eslint-env mocha */ + +var assert = require('assert') +var appendField = require('../') +var testData = require('testdata-w3c-json-form') + +describe('Append Field', function () { + for (var test of testData) { + it('handles ' + test.name, function () { + var store = Object.create(null) + + for (var field of test.fields) { + appendField(store, field.key, field.value) + } + + assert.deepEqual(store, test.expected) + }) + } +}) diff --git a/src/node_modules/body-parser/HISTORY.md b/src/node_modules/body-parser/HISTORY.md new file mode 100644 index 0000000..17dd110 --- /dev/null +++ b/src/node_modules/body-parser/HISTORY.md @@ -0,0 +1,731 @@ +2.2.0 / 2025-03-27 +========================= + +* refactor: normalize common options for all parsers +* deps: + * iconv-lite@^0.6.3 + +2.1.0 / 2025-02-10 +========================= + +* deps: + * type-is@^2.0.0 + * debug@^4.4.0 + * Removed destroy +* refactor: prefix built-in node module imports +* use the node require cache instead of custom caching + +2.0.2 / 2024-10-31 +========================= + +* remove `unpipe` package and use native `unpipe()` method + +2.0.1 / 2024-09-10 +========================= + +* Restore expected behavior `extended` to `false` + +2.0.0 / 2024-09-10 +========================= +* Propagate changes from 1.20.3 +* add brotli support #406 +* Breaking Change: Node.js 18 is the minimum supported version + +2.0.0-beta.2 / 2023-02-23 +========================= + +This incorporates all changes after 1.19.1 up to 1.20.2. + + * Remove deprecated `bodyParser()` combination middleware + * deps: debug@3.1.0 + - Add `DEBUG_HIDE_DATE` environment variable + - Change timer to per-namespace instead of global + - Change non-TTY date format + - Remove `DEBUG_FD` environment variable support + - Support 256 namespace colors + * deps: iconv-lite@0.5.2 + - Add encoding cp720 + - Add encoding UTF-32 + * deps: raw-body@3.0.0-beta.1 + +2.0.0-beta.1 / 2021-12-17 +========================= + + * Drop support for Node.js 0.8 + * `req.body` is no longer always initialized to `{}` + - it is left `undefined` unless a body is parsed + * `urlencoded` parser now defaults `extended` to `false` + * Use `on-finished` to determine when body read + +1.20.3 / 2024-09-10 +=================== + + * deps: qs@6.13.0 + * add `depth` option to customize the depth level in the parser + * IMPORTANT: The default `depth` level for parsing URL-encoded data is now `32` (previously was `Infinity`) + +1.20.2 / 2023-02-21 +=================== + + * Fix strict json error message on Node.js 19+ + * deps: content-type@~1.0.5 + - perf: skip value escaping when unnecessary + * deps: raw-body@2.5.2 + +1.20.1 / 2022-10-06 +=================== + + * deps: qs@6.11.0 + * perf: remove unnecessary object clone + +1.20.0 / 2022-04-02 +=================== + + * Fix error message for json parse whitespace in `strict` + * Fix internal error when inflated body exceeds limit + * Prevent loss of async hooks context + * Prevent hanging when request already read + * deps: depd@2.0.0 + - Replace internal `eval` usage with `Function` constructor + - Use instance methods on `process` to check for listeners + * deps: http-errors@2.0.0 + - deps: depd@2.0.0 + - deps: statuses@2.0.1 + * deps: on-finished@2.4.1 + * deps: qs@6.10.3 + * deps: raw-body@2.5.1 + - deps: http-errors@2.0.0 + +1.19.2 / 2022-02-15 +=================== + + * deps: bytes@3.1.2 + * deps: qs@6.9.7 + * Fix handling of `__proto__` keys + * deps: raw-body@2.4.3 + - deps: bytes@3.1.2 + +1.19.1 / 2021-12-10 +=================== + + * deps: bytes@3.1.1 + * deps: http-errors@1.8.1 + - deps: inherits@2.0.4 + - deps: toidentifier@1.0.1 + - deps: setprototypeof@1.2.0 + * deps: qs@6.9.6 + * deps: raw-body@2.4.2 + - deps: bytes@3.1.1 + - deps: http-errors@1.8.1 + * deps: safe-buffer@5.2.1 + * deps: type-is@~1.6.18 + +1.19.0 / 2019-04-25 +=================== + + * deps: bytes@3.1.0 + - Add petabyte (`pb`) support + * deps: http-errors@1.7.2 + - Set constructor name when possible + - deps: setprototypeof@1.1.1 + - deps: statuses@'>= 1.5.0 < 2' + * deps: iconv-lite@0.4.24 + - Added encoding MIK + * deps: qs@6.7.0 + - Fix parsing array brackets after index + * deps: raw-body@2.4.0 + - deps: bytes@3.1.0 + - deps: http-errors@1.7.2 + - deps: iconv-lite@0.4.24 + * deps: type-is@~1.6.17 + - deps: mime-types@~2.1.24 + - perf: prevent internal `throw` on invalid type + +1.18.3 / 2018-05-14 +=================== + + * Fix stack trace for strict json parse error + * deps: depd@~1.1.2 + - perf: remove argument reassignment + * deps: http-errors@~1.6.3 + - deps: depd@~1.1.2 + - deps: setprototypeof@1.1.0 + - deps: statuses@'>= 1.3.1 < 2' + * deps: iconv-lite@0.4.23 + - Fix loading encoding with year appended + - Fix deprecation warnings on Node.js 10+ + * deps: qs@6.5.2 + * deps: raw-body@2.3.3 + - deps: http-errors@1.6.3 + - deps: iconv-lite@0.4.23 + * deps: type-is@~1.6.16 + - deps: mime-types@~2.1.18 + +1.18.2 / 2017-09-22 +=================== + + * deps: debug@2.6.9 + * perf: remove argument reassignment + +1.18.1 / 2017-09-12 +=================== + + * deps: content-type@~1.0.4 + - perf: remove argument reassignment + - perf: skip parameter parsing when no parameters + * deps: iconv-lite@0.4.19 + - Fix ISO-8859-1 regression + - Update Windows-1255 + * deps: qs@6.5.1 + - Fix parsing & compacting very deep objects + * deps: raw-body@2.3.2 + - deps: iconv-lite@0.4.19 + +1.18.0 / 2017-09-08 +=================== + + * Fix JSON strict violation error to match native parse error + * Include the `body` property on verify errors + * Include the `type` property on all generated errors + * Use `http-errors` to set status code on errors + * deps: bytes@3.0.0 + * deps: debug@2.6.8 + * deps: depd@~1.1.1 + - Remove unnecessary `Buffer` loading + * deps: http-errors@~1.6.2 + - deps: depd@1.1.1 + * deps: iconv-lite@0.4.18 + - Add support for React Native + - Add a warning if not loaded as utf-8 + - Fix CESU-8 decoding in Node.js 8 + - Improve speed of ISO-8859-1 encoding + * deps: qs@6.5.0 + * deps: raw-body@2.3.1 + - Use `http-errors` for standard emitted errors + - deps: bytes@3.0.0 + - deps: iconv-lite@0.4.18 + - perf: skip buffer decoding on overage chunk + * perf: prevent internal `throw` when missing charset + +1.17.2 / 2017-05-17 +=================== + + * deps: debug@2.6.7 + - Fix `DEBUG_MAX_ARRAY_LENGTH` + - deps: ms@2.0.0 + * deps: type-is@~1.6.15 + - deps: mime-types@~2.1.15 + +1.17.1 / 2017-03-06 +=================== + + * deps: qs@6.4.0 + - Fix regression parsing keys starting with `[` + +1.17.0 / 2017-03-01 +=================== + + * deps: http-errors@~1.6.1 + - Make `message` property enumerable for `HttpError`s + - deps: setprototypeof@1.0.3 + * deps: qs@6.3.1 + - Fix compacting nested arrays + +1.16.1 / 2017-02-10 +=================== + + * deps: debug@2.6.1 + - Fix deprecation messages in WebStorm and other editors + - Undeprecate `DEBUG_FD` set to `1` or `2` + +1.16.0 / 2017-01-17 +=================== + + * deps: debug@2.6.0 + - Allow colors in workers + - Deprecated `DEBUG_FD` environment variable + - Fix error when running under React Native + - Use same color for same namespace + - deps: ms@0.7.2 + * deps: http-errors@~1.5.1 + - deps: inherits@2.0.3 + - deps: setprototypeof@1.0.2 + - deps: statuses@'>= 1.3.1 < 2' + * deps: iconv-lite@0.4.15 + - Added encoding MS-31J + - Added encoding MS-932 + - Added encoding MS-936 + - Added encoding MS-949 + - Added encoding MS-950 + - Fix GBK/GB18030 handling of Euro character + * deps: qs@6.2.1 + - Fix array parsing from skipping empty values + * deps: raw-body@~2.2.0 + - deps: iconv-lite@0.4.15 + * deps: type-is@~1.6.14 + - deps: mime-types@~2.1.13 + +1.15.2 / 2016-06-19 +=================== + + * deps: bytes@2.4.0 + * deps: content-type@~1.0.2 + - perf: enable strict mode + * deps: http-errors@~1.5.0 + - Use `setprototypeof` module to replace `__proto__` setting + - deps: statuses@'>= 1.3.0 < 2' + - perf: enable strict mode + * deps: qs@6.2.0 + * deps: raw-body@~2.1.7 + - deps: bytes@2.4.0 + - perf: remove double-cleanup on happy path + * deps: type-is@~1.6.13 + - deps: mime-types@~2.1.11 + +1.15.1 / 2016-05-05 +=================== + + * deps: bytes@2.3.0 + - Drop partial bytes on all parsed units + - Fix parsing byte string that looks like hex + * deps: raw-body@~2.1.6 + - deps: bytes@2.3.0 + * deps: type-is@~1.6.12 + - deps: mime-types@~2.1.10 + +1.15.0 / 2016-02-10 +=================== + + * deps: http-errors@~1.4.0 + - Add `HttpError` export, for `err instanceof createError.HttpError` + - deps: inherits@2.0.1 + - deps: statuses@'>= 1.2.1 < 2' + * deps: qs@6.1.0 + * deps: type-is@~1.6.11 + - deps: mime-types@~2.1.9 + +1.14.2 / 2015-12-16 +=================== + + * deps: bytes@2.2.0 + * deps: iconv-lite@0.4.13 + * deps: qs@5.2.0 + * deps: raw-body@~2.1.5 + - deps: bytes@2.2.0 + - deps: iconv-lite@0.4.13 + * deps: type-is@~1.6.10 + - deps: mime-types@~2.1.8 + +1.14.1 / 2015-09-27 +=================== + + * Fix issue where invalid charset results in 400 when `verify` used + * deps: iconv-lite@0.4.12 + - Fix CESU-8 decoding in Node.js 4.x + * deps: raw-body@~2.1.4 + - Fix masking critical errors from `iconv-lite` + - deps: iconv-lite@0.4.12 + * deps: type-is@~1.6.9 + - deps: mime-types@~2.1.7 + +1.14.0 / 2015-09-16 +=================== + + * Fix JSON strict parse error to match syntax errors + * Provide static `require` analysis in `urlencoded` parser + * deps: depd@~1.1.0 + - Support web browser loading + * deps: qs@5.1.0 + * deps: raw-body@~2.1.3 + - Fix sync callback when attaching data listener causes sync read + * deps: type-is@~1.6.8 + - Fix type error when given invalid type to match against + - deps: mime-types@~2.1.6 + +1.13.3 / 2015-07-31 +=================== + + * deps: type-is@~1.6.6 + - deps: mime-types@~2.1.4 + +1.13.2 / 2015-07-05 +=================== + + * deps: iconv-lite@0.4.11 + * deps: qs@4.0.0 + - Fix dropping parameters like `hasOwnProperty` + - Fix user-visible incompatibilities from 3.1.0 + - Fix various parsing edge cases + * deps: raw-body@~2.1.2 + - Fix error stack traces to skip `makeError` + - deps: iconv-lite@0.4.11 + * deps: type-is@~1.6.4 + - deps: mime-types@~2.1.2 + - perf: enable strict mode + - perf: remove argument reassignment + +1.13.1 / 2015-06-16 +=================== + + * deps: qs@2.4.2 + - Downgraded from 3.1.0 because of user-visible incompatibilities + +1.13.0 / 2015-06-14 +=================== + + * Add `statusCode` property on `Error`s, in addition to `status` + * Change `type` default to `application/json` for JSON parser + * Change `type` default to `application/x-www-form-urlencoded` for urlencoded parser + * Provide static `require` analysis + * Use the `http-errors` module to generate errors + * deps: bytes@2.1.0 + - Slight optimizations + * deps: iconv-lite@0.4.10 + - The encoding UTF-16 without BOM now defaults to UTF-16LE when detection fails + - Leading BOM is now removed when decoding + * deps: on-finished@~2.3.0 + - Add defined behavior for HTTP `CONNECT` requests + - Add defined behavior for HTTP `Upgrade` requests + - deps: ee-first@1.1.1 + * deps: qs@3.1.0 + - Fix dropping parameters like `hasOwnProperty` + - Fix various parsing edge cases + - Parsed object now has `null` prototype + * deps: raw-body@~2.1.1 + - Use `unpipe` module for unpiping requests + - deps: iconv-lite@0.4.10 + * deps: type-is@~1.6.3 + - deps: mime-types@~2.1.1 + - perf: reduce try block size + - perf: remove bitwise operations + * perf: enable strict mode + * perf: remove argument reassignment + * perf: remove delete call + +1.12.4 / 2015-05-10 +=================== + + * deps: debug@~2.2.0 + * deps: qs@2.4.2 + - Fix allowing parameters like `constructor` + * deps: on-finished@~2.2.1 + * deps: raw-body@~2.0.1 + - Fix a false-positive when unpiping in Node.js 0.8 + - deps: bytes@2.0.1 + * deps: type-is@~1.6.2 + - deps: mime-types@~2.0.11 + +1.12.3 / 2015-04-15 +=================== + + * Slight efficiency improvement when not debugging + * deps: depd@~1.0.1 + * deps: iconv-lite@0.4.8 + - Add encoding alias UNICODE-1-1-UTF-7 + * deps: raw-body@1.3.4 + - Fix hanging callback if request aborts during read + - deps: iconv-lite@0.4.8 + +1.12.2 / 2015-03-16 +=================== + + * deps: qs@2.4.1 + - Fix error when parameter `hasOwnProperty` is present + +1.12.1 / 2015-03-15 +=================== + + * deps: debug@~2.1.3 + - Fix high intensity foreground color for bold + - deps: ms@0.7.0 + * deps: type-is@~1.6.1 + - deps: mime-types@~2.0.10 + +1.12.0 / 2015-02-13 +=================== + + * add `debug` messages + * accept a function for the `type` option + * use `content-type` to parse `Content-Type` headers + * deps: iconv-lite@0.4.7 + - Gracefully support enumerables on `Object.prototype` + * deps: raw-body@1.3.3 + - deps: iconv-lite@0.4.7 + * deps: type-is@~1.6.0 + - fix argument reassignment + - fix false-positives in `hasBody` `Transfer-Encoding` check + - support wildcard for both type and subtype (`*/*`) + - deps: mime-types@~2.0.9 + +1.11.0 / 2015-01-30 +=================== + + * make internal `extended: true` depth limit infinity + * deps: type-is@~1.5.6 + - deps: mime-types@~2.0.8 + +1.10.2 / 2015-01-20 +=================== + + * deps: iconv-lite@0.4.6 + - Fix rare aliases of single-byte encodings + * deps: raw-body@1.3.2 + - deps: iconv-lite@0.4.6 + +1.10.1 / 2015-01-01 +=================== + + * deps: on-finished@~2.2.0 + * deps: type-is@~1.5.5 + - deps: mime-types@~2.0.7 + +1.10.0 / 2014-12-02 +=================== + + * make internal `extended: true` array limit dynamic + +1.9.3 / 2014-11-21 +================== + + * deps: iconv-lite@0.4.5 + - Fix Windows-31J and X-SJIS encoding support + * deps: qs@2.3.3 + - Fix `arrayLimit` behavior + * deps: raw-body@1.3.1 + - deps: iconv-lite@0.4.5 + * deps: type-is@~1.5.3 + - deps: mime-types@~2.0.3 + +1.9.2 / 2014-10-27 +================== + + * deps: qs@2.3.2 + - Fix parsing of mixed objects and values + +1.9.1 / 2014-10-22 +================== + + * deps: on-finished@~2.1.1 + - Fix handling of pipelined requests + * deps: qs@2.3.0 + - Fix parsing of mixed implicit and explicit arrays + * deps: type-is@~1.5.2 + - deps: mime-types@~2.0.2 + +1.9.0 / 2014-09-24 +================== + + * include the charset in "unsupported charset" error message + * include the encoding in "unsupported content encoding" error message + * deps: depd@~1.0.0 + +1.8.4 / 2014-09-23 +================== + + * fix content encoding to be case-insensitive + +1.8.3 / 2014-09-19 +================== + + * deps: qs@2.2.4 + - Fix issue with object keys starting with numbers truncated + +1.8.2 / 2014-09-15 +================== + + * deps: depd@0.4.5 + +1.8.1 / 2014-09-07 +================== + + * deps: media-typer@0.3.0 + * deps: type-is@~1.5.1 + +1.8.0 / 2014-09-05 +================== + + * make empty-body-handling consistent between chunked requests + - empty `json` produces `{}` + - empty `raw` produces `new Buffer(0)` + - empty `text` produces `''` + - empty `urlencoded` produces `{}` + * deps: qs@2.2.3 + - Fix issue where first empty value in array is discarded + * deps: type-is@~1.5.0 + - fix `hasbody` to be true for `content-length: 0` + +1.7.0 / 2014-09-01 +================== + + * add `parameterLimit` option to `urlencoded` parser + * change `urlencoded` extended array limit to 100 + * respond with 413 when over `parameterLimit` in `urlencoded` + +1.6.7 / 2014-08-29 +================== + + * deps: qs@2.2.2 + - Remove unnecessary cloning + +1.6.6 / 2014-08-27 +================== + + * deps: qs@2.2.0 + - Array parsing fix + - Performance improvements + +1.6.5 / 2014-08-16 +================== + + * deps: on-finished@2.1.0 + +1.6.4 / 2014-08-14 +================== + + * deps: qs@1.2.2 + +1.6.3 / 2014-08-10 +================== + + * deps: qs@1.2.1 + +1.6.2 / 2014-08-07 +================== + + * deps: qs@1.2.0 + - Fix parsing array of objects + +1.6.1 / 2014-08-06 +================== + + * deps: qs@1.1.0 + - Accept urlencoded square brackets + - Accept empty values in implicit array notation + +1.6.0 / 2014-08-05 +================== + + * deps: qs@1.0.2 + - Complete rewrite + - Limits array length to 20 + - Limits object depth to 5 + - Limits parameters to 1,000 + +1.5.2 / 2014-07-27 +================== + + * deps: depd@0.4.4 + - Work-around v8 generating empty stack traces + +1.5.1 / 2014-07-26 +================== + + * deps: depd@0.4.3 + - Fix exception when global `Error.stackTraceLimit` is too low + +1.5.0 / 2014-07-20 +================== + + * deps: depd@0.4.2 + - Add `TRACE_DEPRECATION` environment variable + - Remove non-standard grey color from color output + - Support `--no-deprecation` argument + - Support `--trace-deprecation` argument + * deps: iconv-lite@0.4.4 + - Added encoding UTF-7 + * deps: raw-body@1.3.0 + - deps: iconv-lite@0.4.4 + - Added encoding UTF-7 + - Fix `Cannot switch to old mode now` error on Node.js 0.10+ + * deps: type-is@~1.3.2 + +1.4.3 / 2014-06-19 +================== + + * deps: type-is@1.3.1 + - fix global variable leak + +1.4.2 / 2014-06-19 +================== + + * deps: type-is@1.3.0 + - improve type parsing + +1.4.1 / 2014-06-19 +================== + + * fix urlencoded extended deprecation message + +1.4.0 / 2014-06-19 +================== + + * add `text` parser + * add `raw` parser + * check accepted charset in content-type (accepts utf-8) + * check accepted encoding in content-encoding (accepts identity) + * deprecate `bodyParser()` middleware; use `.json()` and `.urlencoded()` as needed + * deprecate `urlencoded()` without provided `extended` option + * lazy-load urlencoded parsers + * parsers split into files for reduced mem usage + * support gzip and deflate bodies + - set `inflate: false` to turn off + * deps: raw-body@1.2.2 + - Support all encodings from `iconv-lite` + +1.3.1 / 2014-06-11 +================== + + * deps: type-is@1.2.1 + - Switch dependency from mime to mime-types@1.0.0 + +1.3.0 / 2014-05-31 +================== + + * add `extended` option to urlencoded parser + +1.2.2 / 2014-05-27 +================== + + * deps: raw-body@1.1.6 + - assert stream encoding on node.js 0.8 + - assert stream encoding on node.js < 0.10.6 + - deps: bytes@1 + +1.2.1 / 2014-05-26 +================== + + * invoke `next(err)` after request fully read + - prevents hung responses and socket hang ups + +1.2.0 / 2014-05-11 +================== + + * add `verify` option + * deps: type-is@1.2.0 + - support suffix matching + +1.1.2 / 2014-05-11 +================== + + * improve json parser speed + +1.1.1 / 2014-05-11 +================== + + * fix repeated limit parsing with every request + +1.1.0 / 2014-05-10 +================== + + * add `type` option + * deps: pin for safety and consistency + +1.0.2 / 2014-04-14 +================== + + * use `type-is` module + +1.0.1 / 2014-03-20 +================== + + * lower default limits to 100kb diff --git a/src/node_modules/body-parser/LICENSE b/src/node_modules/body-parser/LICENSE new file mode 100644 index 0000000..386b7b6 --- /dev/null +++ b/src/node_modules/body-parser/LICENSE @@ -0,0 +1,23 @@ +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/node_modules/body-parser/README.md b/src/node_modules/body-parser/README.md new file mode 100644 index 0000000..9fcd4c6 --- /dev/null +++ b/src/node_modules/body-parser/README.md @@ -0,0 +1,491 @@ +# body-parser + +[![NPM Version][npm-version-image]][npm-url] +[![NPM Downloads][npm-downloads-image]][npm-url] +[![Build Status][ci-image]][ci-url] +[![Test Coverage][coveralls-image]][coveralls-url] +[![OpenSSF Scorecard Badge][ossf-scorecard-badge]][ossf-scorecard-visualizer] + +Node.js body parsing middleware. + +Parse incoming request bodies in a middleware before your handlers, available +under the `req.body` property. + +**Note** As `req.body`'s shape is based on user-controlled input, all +properties and values in this object are untrusted and should be validated +before trusting. For example, `req.body.foo.toString()` may fail in multiple +ways, for example the `foo` property may not be there or may not be a string, +and `toString` may not be a function and instead a string or other user input. + +[Learn about the anatomy of an HTTP transaction in Node.js](https://nodejs.org/en/docs/guides/anatomy-of-an-http-transaction/). + +_This does not handle multipart bodies_, due to their complex and typically +large nature. For multipart bodies, you may be interested in the following +modules: + + * [busboy](https://www.npmjs.org/package/busboy#readme) and + [connect-busboy](https://www.npmjs.org/package/connect-busboy#readme) + * [multiparty](https://www.npmjs.org/package/multiparty#readme) and + [connect-multiparty](https://www.npmjs.org/package/connect-multiparty#readme) + * [formidable](https://www.npmjs.org/package/formidable#readme) + * [multer](https://www.npmjs.org/package/multer#readme) + +This module provides the following parsers: + + * [JSON body parser](#bodyparserjsonoptions) + * [Raw body parser](#bodyparserrawoptions) + * [Text body parser](#bodyparsertextoptions) + * [URL-encoded form body parser](#bodyparserurlencodedoptions) + +Other body parsers you might be interested in: + +- [body](https://www.npmjs.org/package/body#readme) +- [co-body](https://www.npmjs.org/package/co-body#readme) + +## Installation + +```sh +$ npm install body-parser +``` + +## API + +```js +const bodyParser = require('body-parser') +``` + +The `bodyParser` object exposes various factories to create middlewares. All +middlewares will populate the `req.body` property with the parsed body when +the `Content-Type` request header matches the `type` option. + +The various errors returned by this module are described in the +[errors section](#errors). + +### bodyParser.json([options]) + +Returns middleware that only parses `json` and only looks at requests where +the `Content-Type` header matches the `type` option. This parser accepts any +Unicode encoding of the body and supports automatic inflation of `gzip`, +`br` (brotli) and `deflate` encodings. + +A new `body` object containing the parsed data is populated on the `request` +object after the middleware (i.e. `req.body`). + +#### Options + +The `json` function takes an optional `options` object that may contain any of +the following keys: + +##### inflate + +When set to `true`, then deflated (compressed) bodies will be inflated; when +`false`, deflated bodies are rejected. Defaults to `true`. + +##### limit + +Controls the maximum request body size. If this is a number, then the value +specifies the number of bytes; if it is a string, the value is passed to the +[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults +to `'100kb'`. + +##### reviver + +The `reviver` option is passed directly to `JSON.parse` as the second +argument. You can find more information on this argument +[in the MDN documentation about JSON.parse](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#Example.3A_Using_the_reviver_parameter). + +##### strict + +When set to `true`, will only accept arrays and objects; when `false` will +accept anything `JSON.parse` accepts. Defaults to `true`. + +##### type + +The `type` option is used to determine what media type the middleware will +parse. This option can be a string, array of strings, or a function. If not a +function, `type` option is passed directly to the +[type-is](https://www.npmjs.org/package/type-is#readme) library and this can +be an extension name (like `json`), a mime type (like `application/json`), or +a mime type with a wildcard (like `*/*` or `*/json`). If a function, the `type` +option is called as `fn(req)` and the request is parsed if it returns a truthy +value. Defaults to `application/json`. + +##### verify + +The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`, +where `buf` is a `Buffer` of the raw request body and `encoding` is the +encoding of the request. The parsing can be aborted by throwing an error. + +### bodyParser.raw([options]) + +Returns middleware that parses all bodies as a `Buffer` and only looks at +requests where the `Content-Type` header matches the `type` option. This +parser supports automatic inflation of `gzip`, `br` (brotli) and `deflate` +encodings. + +A new `body` object containing the parsed data is populated on the `request` +object after the middleware (i.e. `req.body`). This will be a `Buffer` object +of the body. + +#### Options + +The `raw` function takes an optional `options` object that may contain any of +the following keys: + +##### inflate + +When set to `true`, then deflated (compressed) bodies will be inflated; when +`false`, deflated bodies are rejected. Defaults to `true`. + +##### limit + +Controls the maximum request body size. If this is a number, then the value +specifies the number of bytes; if it is a string, the value is passed to the +[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults +to `'100kb'`. + +##### type + +The `type` option is used to determine what media type the middleware will +parse. This option can be a string, array of strings, or a function. +If not a function, `type` option is passed directly to the +[type-is](https://www.npmjs.org/package/type-is#readme) library and this +can be an extension name (like `bin`), a mime type (like +`application/octet-stream`), or a mime type with a wildcard (like `*/*` or +`application/*`). If a function, the `type` option is called as `fn(req)` +and the request is parsed if it returns a truthy value. Defaults to +`application/octet-stream`. + +##### verify + +The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`, +where `buf` is a `Buffer` of the raw request body and `encoding` is the +encoding of the request. The parsing can be aborted by throwing an error. + +### bodyParser.text([options]) + +Returns middleware that parses all bodies as a string and only looks at +requests where the `Content-Type` header matches the `type` option. This +parser supports automatic inflation of `gzip`, `br` (brotli) and `deflate` +encodings. + +A new `body` string containing the parsed data is populated on the `request` +object after the middleware (i.e. `req.body`). This will be a string of the +body. + +#### Options + +The `text` function takes an optional `options` object that may contain any of +the following keys: + +##### defaultCharset + +Specify the default character set for the text content if the charset is not +specified in the `Content-Type` header of the request. Defaults to `utf-8`. + +##### inflate + +When set to `true`, then deflated (compressed) bodies will be inflated; when +`false`, deflated bodies are rejected. Defaults to `true`. + +##### limit + +Controls the maximum request body size. If this is a number, then the value +specifies the number of bytes; if it is a string, the value is passed to the +[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults +to `'100kb'`. + +##### type + +The `type` option is used to determine what media type the middleware will +parse. This option can be a string, array of strings, or a function. If not +a function, `type` option is passed directly to the +[type-is](https://www.npmjs.org/package/type-is#readme) library and this can +be an extension name (like `txt`), a mime type (like `text/plain`), or a mime +type with a wildcard (like `*/*` or `text/*`). If a function, the `type` +option is called as `fn(req)` and the request is parsed if it returns a +truthy value. Defaults to `text/plain`. + +##### verify + +The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`, +where `buf` is a `Buffer` of the raw request body and `encoding` is the +encoding of the request. The parsing can be aborted by throwing an error. + +### bodyParser.urlencoded([options]) + +Returns middleware that only parses `urlencoded` bodies and only looks at +requests where the `Content-Type` header matches the `type` option. This +parser accepts only UTF-8 encoding of the body and supports automatic +inflation of `gzip`, `br` (brotli) and `deflate` encodings. + +A new `body` object containing the parsed data is populated on the `request` +object after the middleware (i.e. `req.body`). This object will contain +key-value pairs, where the value can be a string or array (when `extended` is +`false`), or any type (when `extended` is `true`). + +#### Options + +The `urlencoded` function takes an optional `options` object that may contain +any of the following keys: + +##### extended + +The "extended" syntax allows for rich objects and arrays to be encoded into the +URL-encoded format, allowing for a JSON-like experience with URL-encoded. For +more information, please [see the qs +library](https://www.npmjs.org/package/qs#readme). + +Defaults to `false`. + +##### inflate + +When set to `true`, then deflated (compressed) bodies will be inflated; when +`false`, deflated bodies are rejected. Defaults to `true`. + +##### limit + +Controls the maximum request body size. If this is a number, then the value +specifies the number of bytes; if it is a string, the value is passed to the +[bytes](https://www.npmjs.com/package/bytes) library for parsing. Defaults +to `'100kb'`. + +##### parameterLimit + +The `parameterLimit` option controls the maximum number of parameters that +are allowed in the URL-encoded data. If a request contains more parameters +than this value, a 413 will be returned to the client. Defaults to `1000`. + +##### type + +The `type` option is used to determine what media type the middleware will +parse. This option can be a string, array of strings, or a function. If not +a function, `type` option is passed directly to the +[type-is](https://www.npmjs.org/package/type-is#readme) library and this can +be an extension name (like `urlencoded`), a mime type (like +`application/x-www-form-urlencoded`), or a mime type with a wildcard (like +`*/x-www-form-urlencoded`). If a function, the `type` option is called as +`fn(req)` and the request is parsed if it returns a truthy value. Defaults +to `application/x-www-form-urlencoded`. + +##### verify + +The `verify` option, if supplied, is called as `verify(req, res, buf, encoding)`, +where `buf` is a `Buffer` of the raw request body and `encoding` is the +encoding of the request. The parsing can be aborted by throwing an error. + +##### defaultCharset + +The default charset to parse as, if not specified in content-type. Must be +either `utf-8` or `iso-8859-1`. Defaults to `utf-8`. + +##### charsetSentinel + +Whether to let the value of the `utf8` parameter take precedence as the charset +selector. It requires the form to contain a parameter named `utf8` with a value +of `✓`. Defaults to `false`. + +##### interpretNumericEntities + +Whether to decode numeric entities such as `☺` when parsing an iso-8859-1 +form. Defaults to `false`. + + +#### depth + +The `depth` option is used to configure the maximum depth of the `qs` library when `extended` is `true`. This allows you to limit the amount of keys that are parsed and can be useful to prevent certain types of abuse. Defaults to `32`. It is recommended to keep this value as low as possible. + +## Errors + +The middlewares provided by this module create errors using the +[`http-errors` module](https://www.npmjs.com/package/http-errors). The errors +will typically have a `status`/`statusCode` property that contains the suggested +HTTP response code, an `expose` property to determine if the `message` property +should be displayed to the client, a `type` property to determine the type of +error without matching against the `message`, and a `body` property containing +the read body, if available. + +The following are the common errors created, though any error can come through +for various reasons. + +### content encoding unsupported + +This error will occur when the request had a `Content-Encoding` header that +contained an encoding but the "inflation" option was set to `false`. The +`status` property is set to `415`, the `type` property is set to +`'encoding.unsupported'`, and the `charset` property will be set to the +encoding that is unsupported. + +### entity parse failed + +This error will occur when the request contained an entity that could not be +parsed by the middleware. The `status` property is set to `400`, the `type` +property is set to `'entity.parse.failed'`, and the `body` property is set to +the entity value that failed parsing. + +### entity verify failed + +This error will occur when the request contained an entity that could not be +failed verification by the defined `verify` option. The `status` property is +set to `403`, the `type` property is set to `'entity.verify.failed'`, and the +`body` property is set to the entity value that failed verification. + +### request aborted + +This error will occur when the request is aborted by the client before reading +the body has finished. The `received` property will be set to the number of +bytes received before the request was aborted and the `expected` property is +set to the number of expected bytes. The `status` property is set to `400` +and `type` property is set to `'request.aborted'`. + +### request entity too large + +This error will occur when the request body's size is larger than the "limit" +option. The `limit` property will be set to the byte limit and the `length` +property will be set to the request body's length. The `status` property is +set to `413` and the `type` property is set to `'entity.too.large'`. + +### request size did not match content length + +This error will occur when the request's length did not match the length from +the `Content-Length` header. This typically occurs when the request is malformed, +typically when the `Content-Length` header was calculated based on characters +instead of bytes. The `status` property is set to `400` and the `type` property +is set to `'request.size.invalid'`. + +### stream encoding should not be set + +This error will occur when something called the `req.setEncoding` method prior +to this middleware. This module operates directly on bytes only and you cannot +call `req.setEncoding` when using this module. The `status` property is set to +`500` and the `type` property is set to `'stream.encoding.set'`. + +### stream is not readable + +This error will occur when the request is no longer readable when this middleware +attempts to read it. This typically means something other than a middleware from +this module read the request body already and the middleware was also configured to +read the same request. The `status` property is set to `500` and the `type` +property is set to `'stream.not.readable'`. + +### too many parameters + +This error will occur when the content of the request exceeds the configured +`parameterLimit` for the `urlencoded` parser. The `status` property is set to +`413` and the `type` property is set to `'parameters.too.many'`. + +### unsupported charset "BOGUS" + +This error will occur when the request had a charset parameter in the +`Content-Type` header, but the `iconv-lite` module does not support it OR the +parser does not support it. The charset is contained in the message as well +as in the `charset` property. The `status` property is set to `415`, the +`type` property is set to `'charset.unsupported'`, and the `charset` property +is set to the charset that is unsupported. + +### unsupported content encoding "bogus" + +This error will occur when the request had a `Content-Encoding` header that +contained an unsupported encoding. The encoding is contained in the message +as well as in the `encoding` property. The `status` property is set to `415`, +the `type` property is set to `'encoding.unsupported'`, and the `encoding` +property is set to the encoding that is unsupported. + +### The input exceeded the depth + +This error occurs when using `bodyParser.urlencoded` with the `extended` property set to `true` and the input exceeds the configured `depth` option. The `status` property is set to `400`. It is recommended to review the `depth` option and evaluate if it requires a higher value. When the `depth` option is set to `32` (default value), the error will not be thrown. + +## Examples + +### Express/Connect top-level generic + +This example demonstrates adding a generic JSON and URL-encoded parser as a +top-level middleware, which will parse the bodies of all incoming requests. +This is the simplest setup. + +```js +const express = require('express') +const bodyParser = require('body-parser') + +const app = express() + +// parse application/x-www-form-urlencoded +app.use(bodyParser.urlencoded()) + +// parse application/json +app.use(bodyParser.json()) + +app.use(function (req, res) { + res.setHeader('Content-Type', 'text/plain') + res.write('you posted:\n') + res.end(String(JSON.stringify(req.body, null, 2))) +}) +``` + +### Express route-specific + +This example demonstrates adding body parsers specifically to the routes that +need them. In general, this is the most recommended way to use body-parser with +Express. + +```js +const express = require('express') +const bodyParser = require('body-parser') + +const app = express() + +// create application/json parser +const jsonParser = bodyParser.json() + +// create application/x-www-form-urlencoded parser +const urlencodedParser = bodyParser.urlencoded() + +// POST /login gets urlencoded bodies +app.post('/login', urlencodedParser, function (req, res) { + if (!req.body || !req.body.username) res.sendStatus(400) + res.send('welcome, ' + req.body.username) +}) + +// POST /api/users gets JSON bodies +app.post('/api/users', jsonParser, function (req, res) { + if (!req.body) res.sendStatus(400) + // create user in req.body +}) +``` + +### Change accepted type for parsers + +All the parsers accept a `type` option which allows you to change the +`Content-Type` that the middleware will parse. + +```js +const express = require('express') +const bodyParser = require('body-parser') + +const app = express() + +// parse various different custom JSON types as JSON +app.use(bodyParser.json({ type: 'application/*+json' })) + +// parse some custom thing into a Buffer +app.use(bodyParser.raw({ type: 'application/vnd.custom-type' })) + +// parse an HTML body into a string +app.use(bodyParser.text({ type: 'text/html' })) +``` + +## License + +[MIT](LICENSE) + +[ci-image]: https://badgen.net/github/checks/expressjs/body-parser/master?label=ci +[ci-url]: https://github.com/expressjs/body-parser/actions/workflows/ci.yml +[coveralls-image]: https://badgen.net/coveralls/c/github/expressjs/body-parser/master +[coveralls-url]: https://coveralls.io/r/expressjs/body-parser?branch=master +[node-version-image]: https://badgen.net/npm/node/body-parser +[node-version-url]: https://nodejs.org/en/download +[npm-downloads-image]: https://badgen.net/npm/dm/body-parser +[npm-url]: https://npmjs.org/package/body-parser +[npm-version-image]: https://badgen.net/npm/v/body-parser +[ossf-scorecard-badge]: https://api.scorecard.dev/projects/github.com/expressjs/body-parser/badge +[ossf-scorecard-visualizer]: https://ossf.github.io/scorecard-visualizer/#/projects/github.com/expressjs/body-parser \ No newline at end of file diff --git a/src/node_modules/body-parser/index.js b/src/node_modules/body-parser/index.js new file mode 100644 index 0000000..d722d0b --- /dev/null +++ b/src/node_modules/body-parser/index.js @@ -0,0 +1,80 @@ +/*! + * body-parser + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * @typedef Parsers + * @type {function} + * @property {function} json + * @property {function} raw + * @property {function} text + * @property {function} urlencoded + */ + +/** + * Module exports. + * @type {Parsers} + */ + +exports = module.exports = bodyParser + +/** + * JSON parser. + * @public + */ + +Object.defineProperty(exports, 'json', { + configurable: true, + enumerable: true, + get: () => require('./lib/types/json') +}) + +/** + * Raw parser. + * @public + */ + +Object.defineProperty(exports, 'raw', { + configurable: true, + enumerable: true, + get: () => require('./lib/types/raw') +}) + +/** + * Text parser. + * @public + */ + +Object.defineProperty(exports, 'text', { + configurable: true, + enumerable: true, + get: () => require('./lib/types/text') +}) + +/** + * URL-encoded parser. + * @public + */ + +Object.defineProperty(exports, 'urlencoded', { + configurable: true, + enumerable: true, + get: () => require('./lib/types/urlencoded') +}) + +/** + * Create a middleware to parse json and urlencoded bodies. + * + * @param {object} [options] + * @return {function} + * @deprecated + * @public + */ + +function bodyParser () { + throw new Error('The bodyParser() generic has been split into individual middleware to use instead.') +} diff --git a/src/node_modules/body-parser/lib/read.js b/src/node_modules/body-parser/lib/read.js new file mode 100644 index 0000000..eee8b11 --- /dev/null +++ b/src/node_modules/body-parser/lib/read.js @@ -0,0 +1,210 @@ +/*! + * body-parser + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module dependencies. + * @private + */ + +var createError = require('http-errors') +var getBody = require('raw-body') +var iconv = require('iconv-lite') +var onFinished = require('on-finished') +var zlib = require('node:zlib') + +/** + * Module exports. + */ + +module.exports = read + +/** + * Read a request into a buffer and parse. + * + * @param {object} req + * @param {object} res + * @param {function} next + * @param {function} parse + * @param {function} debug + * @param {object} options + * @private + */ + +function read (req, res, next, parse, debug, options) { + var length + var opts = options + var stream + + // read options + var encoding = opts.encoding !== null + ? opts.encoding + : null + var verify = opts.verify + + try { + // get the content stream + stream = contentstream(req, debug, opts.inflate) + length = stream.length + stream.length = undefined + } catch (err) { + return next(err) + } + + // set raw-body options + opts.length = length + opts.encoding = verify + ? null + : encoding + + // assert charset is supported + if (opts.encoding === null && encoding !== null && !iconv.encodingExists(encoding)) { + return next(createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', { + charset: encoding.toLowerCase(), + type: 'charset.unsupported' + })) + } + + // read body + debug('read body') + getBody(stream, opts, function (error, body) { + if (error) { + var _error + + if (error.type === 'encoding.unsupported') { + // echo back charset + _error = createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', { + charset: encoding.toLowerCase(), + type: 'charset.unsupported' + }) + } else { + // set status code on error + _error = createError(400, error) + } + + // unpipe from stream and destroy + if (stream !== req) { + req.unpipe() + stream.destroy() + } + + // read off entire request + dump(req, function onfinished () { + next(createError(400, _error)) + }) + return + } + + // verify + if (verify) { + try { + debug('verify body') + verify(req, res, body, encoding) + } catch (err) { + next(createError(403, err, { + body: body, + type: err.type || 'entity.verify.failed' + })) + return + } + } + + // parse + var str = body + try { + debug('parse body') + str = typeof body !== 'string' && encoding !== null + ? iconv.decode(body, encoding) + : body + req.body = parse(str, encoding) + } catch (err) { + next(createError(400, err, { + body: str, + type: err.type || 'entity.parse.failed' + })) + return + } + + next() + }) +} + +/** + * Get the content stream of the request. + * + * @param {object} req + * @param {function} debug + * @param {boolean} [inflate=true] + * @return {object} + * @api private + */ + +function contentstream (req, debug, inflate) { + var encoding = (req.headers['content-encoding'] || 'identity').toLowerCase() + var length = req.headers['content-length'] + + debug('content-encoding "%s"', encoding) + + if (inflate === false && encoding !== 'identity') { + throw createError(415, 'content encoding unsupported', { + encoding: encoding, + type: 'encoding.unsupported' + }) + } + + if (encoding === 'identity') { + req.length = length + return req + } + + var stream = createDecompressionStream(encoding, debug) + req.pipe(stream) + return stream +} + +/** + * Create a decompression stream for the given encoding. + * @param {string} encoding + * @param {function} debug + * @return {object} + * @api private + */ +function createDecompressionStream (encoding, debug) { + switch (encoding) { + case 'deflate': + debug('inflate body') + return zlib.createInflate() + case 'gzip': + debug('gunzip body') + return zlib.createGunzip() + case 'br': + debug('brotli decompress body') + return zlib.createBrotliDecompress() + default: + throw createError(415, 'unsupported content encoding "' + encoding + '"', { + encoding: encoding, + type: 'encoding.unsupported' + }) + } +} + +/** + * Dump the contents of a request. + * + * @param {object} req + * @param {function} callback + * @api private + */ + +function dump (req, callback) { + if (onFinished.isFinished(req)) { + callback(null) + } else { + onFinished(req, callback) + req.resume() + } +} diff --git a/src/node_modules/body-parser/lib/types/json.js b/src/node_modules/body-parser/lib/types/json.js new file mode 100644 index 0000000..078ce71 --- /dev/null +++ b/src/node_modules/body-parser/lib/types/json.js @@ -0,0 +1,206 @@ +/*! + * body-parser + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module dependencies. + * @private + */ + +var createError = require('http-errors') +var debug = require('debug')('body-parser:json') +var isFinished = require('on-finished').isFinished +var read = require('../read') +var typeis = require('type-is') +var { getCharset, normalizeOptions } = require('../utils') + +/** + * Module exports. + */ + +module.exports = json + +/** + * RegExp to match the first non-space in a string. + * + * Allowed whitespace is defined in RFC 7159: + * + * ws = *( + * %x20 / ; Space + * %x09 / ; Horizontal tab + * %x0A / ; Line feed or New line + * %x0D ) ; Carriage return + */ + +var FIRST_CHAR_REGEXP = /^[\x20\x09\x0a\x0d]*([^\x20\x09\x0a\x0d])/ // eslint-disable-line no-control-regex + +var JSON_SYNTAX_CHAR = '#' +var JSON_SYNTAX_REGEXP = /#+/g + +/** + * Create a middleware to parse JSON bodies. + * + * @param {object} [options] + * @return {function} + * @public + */ + +function json (options) { + var { inflate, limit, verify, shouldParse } = normalizeOptions(options, 'application/json') + + var reviver = options?.reviver + var strict = options?.strict !== false + + function parse (body) { + if (body.length === 0) { + // special-case empty json body, as it's a common client-side mistake + // TODO: maybe make this configurable or part of "strict" option + return {} + } + + if (strict) { + var first = firstchar(body) + + if (first !== '{' && first !== '[') { + debug('strict violation') + throw createStrictSyntaxError(body, first) + } + } + + try { + debug('parse json') + return JSON.parse(body, reviver) + } catch (e) { + throw normalizeJsonSyntaxError(e, { + message: e.message, + stack: e.stack + }) + } + } + + return function jsonParser (req, res, next) { + if (isFinished(req)) { + debug('body already parsed') + next() + return + } + + if (!('body' in req)) { + req.body = undefined + } + + // skip requests without bodies + if (!typeis.hasBody(req)) { + debug('skip empty body') + next() + return + } + + debug('content-type %j', req.headers['content-type']) + + // determine if request should be parsed + if (!shouldParse(req)) { + debug('skip parsing') + next() + return + } + + // assert charset per RFC 7159 sec 8.1 + var charset = getCharset(req) || 'utf-8' + if (charset.slice(0, 4) !== 'utf-') { + debug('invalid charset') + next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', { + charset: charset, + type: 'charset.unsupported' + })) + return + } + + // read + read(req, res, next, parse, debug, { + encoding: charset, + inflate, + limit, + verify + }) + } +} + +/** + * Create strict violation syntax error matching native error. + * + * @param {string} str + * @param {string} char + * @return {Error} + * @private + */ + +function createStrictSyntaxError (str, char) { + var index = str.indexOf(char) + var partial = '' + + if (index !== -1) { + partial = str.substring(0, index) + JSON_SYNTAX_CHAR + + for (var i = index + 1; i < str.length; i++) { + partial += JSON_SYNTAX_CHAR + } + } + + try { + JSON.parse(partial); /* istanbul ignore next */ throw new SyntaxError('strict violation') + } catch (e) { + return normalizeJsonSyntaxError(e, { + message: e.message.replace(JSON_SYNTAX_REGEXP, function (placeholder) { + return str.substring(index, index + placeholder.length) + }), + stack: e.stack + }) + } +} + +/** + * Get the first non-whitespace character in a string. + * + * @param {string} str + * @return {function} + * @private + */ + +function firstchar (str) { + var match = FIRST_CHAR_REGEXP.exec(str) + + return match + ? match[1] + : undefined +} + +/** + * Normalize a SyntaxError for JSON.parse. + * + * @param {SyntaxError} error + * @param {object} obj + * @return {SyntaxError} + */ + +function normalizeJsonSyntaxError (error, obj) { + var keys = Object.getOwnPropertyNames(error) + + for (var i = 0; i < keys.length; i++) { + var key = keys[i] + if (key !== 'stack' && key !== 'message') { + delete error[key] + } + } + + // replace stack before message for Node.js 0.10 and below + error.stack = obj.stack.replace(error.message, obj.message) + error.message = obj.message + + return error +} diff --git a/src/node_modules/body-parser/lib/types/raw.js b/src/node_modules/body-parser/lib/types/raw.js new file mode 100644 index 0000000..3788ff2 --- /dev/null +++ b/src/node_modules/body-parser/lib/types/raw.js @@ -0,0 +1,75 @@ +/*! + * body-parser + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module dependencies. + */ + +var debug = require('debug')('body-parser:raw') +var isFinished = require('on-finished').isFinished +var read = require('../read') +var typeis = require('type-is') +var { normalizeOptions } = require('../utils') + +/** + * Module exports. + */ + +module.exports = raw + +/** + * Create a middleware to parse raw bodies. + * + * @param {object} [options] + * @return {function} + * @api public + */ + +function raw (options) { + var { inflate, limit, verify, shouldParse } = normalizeOptions(options, 'application/octet-stream') + + function parse (buf) { + return buf + } + + return function rawParser (req, res, next) { + if (isFinished(req)) { + debug('body already parsed') + next() + return + } + + if (!('body' in req)) { + req.body = undefined + } + + // skip requests without bodies + if (!typeis.hasBody(req)) { + debug('skip empty body') + next() + return + } + + debug('content-type %j', req.headers['content-type']) + + // determine if request should be parsed + if (!shouldParse(req)) { + debug('skip parsing') + next() + return + } + + // read + read(req, res, next, parse, debug, { + encoding: null, + inflate, + limit, + verify + }) + } +} diff --git a/src/node_modules/body-parser/lib/types/text.js b/src/node_modules/body-parser/lib/types/text.js new file mode 100644 index 0000000..3e0ab1b --- /dev/null +++ b/src/node_modules/body-parser/lib/types/text.js @@ -0,0 +1,80 @@ +/*! + * body-parser + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module dependencies. + */ + +var debug = require('debug')('body-parser:text') +var isFinished = require('on-finished').isFinished +var read = require('../read') +var typeis = require('type-is') +var { getCharset, normalizeOptions } = require('../utils') + +/** + * Module exports. + */ + +module.exports = text + +/** + * Create a middleware to parse text bodies. + * + * @param {object} [options] + * @return {function} + * @api public + */ + +function text (options) { + var { inflate, limit, verify, shouldParse } = normalizeOptions(options, 'text/plain') + + var defaultCharset = options?.defaultCharset || 'utf-8' + + function parse (buf) { + return buf + } + + return function textParser (req, res, next) { + if (isFinished(req)) { + debug('body already parsed') + next() + return + } + + if (!('body' in req)) { + req.body = undefined + } + + // skip requests without bodies + if (!typeis.hasBody(req)) { + debug('skip empty body') + next() + return + } + + debug('content-type %j', req.headers['content-type']) + + // determine if request should be parsed + if (!shouldParse(req)) { + debug('skip parsing') + next() + return + } + + // get charset + var charset = getCharset(req) || defaultCharset + + // read + read(req, res, next, parse, debug, { + encoding: charset, + inflate, + limit, + verify + }) + } +} diff --git a/src/node_modules/body-parser/lib/types/urlencoded.js b/src/node_modules/body-parser/lib/types/urlencoded.js new file mode 100644 index 0000000..f993425 --- /dev/null +++ b/src/node_modules/body-parser/lib/types/urlencoded.js @@ -0,0 +1,177 @@ +/*! + * body-parser + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module dependencies. + * @private + */ + +var createError = require('http-errors') +var debug = require('debug')('body-parser:urlencoded') +var isFinished = require('on-finished').isFinished +var read = require('../read') +var typeis = require('type-is') +var qs = require('qs') +var { getCharset, normalizeOptions } = require('../utils') + +/** + * Module exports. + */ + +module.exports = urlencoded + +/** + * Create a middleware to parse urlencoded bodies. + * + * @param {object} [options] + * @return {function} + * @public + */ + +function urlencoded (options) { + var { inflate, limit, verify, shouldParse } = normalizeOptions(options, 'application/x-www-form-urlencoded') + + var defaultCharset = options?.defaultCharset || 'utf-8' + if (defaultCharset !== 'utf-8' && defaultCharset !== 'iso-8859-1') { + throw new TypeError('option defaultCharset must be either utf-8 or iso-8859-1') + } + + // create the appropriate query parser + var queryparse = createQueryParser(options) + + function parse (body, encoding) { + return body.length + ? queryparse(body, encoding) + : {} + } + + return function urlencodedParser (req, res, next) { + if (isFinished(req)) { + debug('body already parsed') + next() + return + } + + if (!('body' in req)) { + req.body = undefined + } + + // skip requests without bodies + if (!typeis.hasBody(req)) { + debug('skip empty body') + next() + return + } + + debug('content-type %j', req.headers['content-type']) + + // determine if request should be parsed + if (!shouldParse(req)) { + debug('skip parsing') + next() + return + } + + // assert charset + var charset = getCharset(req) || defaultCharset + if (charset !== 'utf-8' && charset !== 'iso-8859-1') { + debug('invalid charset') + next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', { + charset: charset, + type: 'charset.unsupported' + })) + return + } + + // read + read(req, res, next, parse, debug, { + encoding: charset, + inflate, + limit, + verify + }) + } +} + +/** + * Get the extended query parser. + * + * @param {object} options + */ + +function createQueryParser (options) { + var extended = Boolean(options?.extended) + var parameterLimit = options?.parameterLimit !== undefined + ? options?.parameterLimit + : 1000 + var charsetSentinel = options?.charsetSentinel + var interpretNumericEntities = options?.interpretNumericEntities + var depth = extended ? (options?.depth !== undefined ? options?.depth : 32) : 0 + + if (isNaN(parameterLimit) || parameterLimit < 1) { + throw new TypeError('option parameterLimit must be a positive number') + } + + if (isNaN(depth) || depth < 0) { + throw new TypeError('option depth must be a zero or a positive number') + } + + if (isFinite(parameterLimit)) { + parameterLimit = parameterLimit | 0 + } + + return function queryparse (body, encoding) { + var paramCount = parameterCount(body, parameterLimit) + + if (paramCount === undefined) { + debug('too many parameters') + throw createError(413, 'too many parameters', { + type: 'parameters.too.many' + }) + } + + var arrayLimit = extended ? Math.max(100, paramCount) : 0 + + debug('parse ' + (extended ? 'extended ' : '') + 'urlencoding') + try { + return qs.parse(body, { + allowPrototypes: true, + arrayLimit: arrayLimit, + depth: depth, + charsetSentinel: charsetSentinel, + interpretNumericEntities: interpretNumericEntities, + charset: encoding, + parameterLimit: parameterLimit, + strictDepth: true + }) + } catch (err) { + if (err instanceof RangeError) { + throw createError(400, 'The input exceeded the depth', { + type: 'querystring.parse.rangeError' + }) + } else { + throw err + } + } + } +} + +/** + * Count the number of parameters, stopping once limit reached + * + * @param {string} body + * @param {number} limit + * @api private + */ + +function parameterCount (body, limit) { + var len = body.split('&').length + + return len > limit ? undefined : len - 1 +} diff --git a/src/node_modules/body-parser/lib/utils.js b/src/node_modules/body-parser/lib/utils.js new file mode 100644 index 0000000..eee5d95 --- /dev/null +++ b/src/node_modules/body-parser/lib/utils.js @@ -0,0 +1,83 @@ +'use strict' + +/** + * Module dependencies. + */ + +var bytes = require('bytes') +var contentType = require('content-type') +var typeis = require('type-is') + +/** + * Module exports. + */ + +module.exports = { + getCharset, + normalizeOptions +} + +/** + * Get the charset of a request. + * + * @param {object} req + * @api private + */ + +function getCharset (req) { + try { + return (contentType.parse(req).parameters.charset || '').toLowerCase() + } catch { + return undefined + } +} + +/** + * Get the simple type checker. + * + * @param {string | string[]} type + * @return {function} + */ + +function typeChecker (type) { + return function checkType (req) { + return Boolean(typeis(req, type)) + } +} + +/** + * Normalizes the common options for all parsers. + * + * @param {object} options options to normalize + * @param {string | string[] | function} defaultType default content type(s) or a function to determine it + * @returns {object} + */ +function normalizeOptions (options, defaultType) { + if (!defaultType) { + // Parsers must define a default content type + throw new TypeError('defaultType must be provided') + } + + var inflate = options?.inflate !== false + var limit = typeof options?.limit !== 'number' + ? bytes.parse(options?.limit || '100kb') + : options?.limit + var type = options?.type || defaultType + var verify = options?.verify || false + + if (verify !== false && typeof verify !== 'function') { + throw new TypeError('option verify must be function') + } + + // create the appropriate type checking function + var shouldParse = typeof type !== 'function' + ? typeChecker(type) + : type + + return { + inflate, + limit, + verify, + shouldParse + } +} diff --git a/src/node_modules/body-parser/package.json b/src/node_modules/body-parser/package.json new file mode 100644 index 0000000..e7f763b --- /dev/null +++ b/src/node_modules/body-parser/package.json @@ -0,0 +1,49 @@ +{ + "name": "body-parser", + "description": "Node.js body parsing middleware", + "version": "2.2.0", + "contributors": [ + "Douglas Christopher Wilson ", + "Jonathan Ong (http://jongleberry.com)" + ], + "license": "MIT", + "repository": "expressjs/body-parser", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "devDependencies": { + "eslint": "8.34.0", + "eslint-config-standard": "14.1.1", + "eslint-plugin-import": "2.27.5", + "eslint-plugin-markdown": "3.0.0", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-promise": "6.1.1", + "eslint-plugin-standard": "4.1.0", + "mocha": "^11.1.0", + "nyc": "^17.1.0", + "supertest": "^7.0.0" + }, + "files": [ + "lib/", + "LICENSE", + "HISTORY.md", + "index.js" + ], + "engines": { + "node": ">=18" + }, + "scripts": { + "lint": "eslint .", + "test": "mocha --reporter spec --check-leaks test/", + "test-ci": "nyc --reporter=lcovonly --reporter=text npm test", + "test-cov": "nyc --reporter=html --reporter=text npm test" + } +} diff --git a/src/node_modules/buffer-from/LICENSE b/src/node_modules/buffer-from/LICENSE new file mode 100644 index 0000000..e4bf1d6 --- /dev/null +++ b/src/node_modules/buffer-from/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016, 2018 Linus Unnebäck + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/node_modules/buffer-from/index.js b/src/node_modules/buffer-from/index.js new file mode 100644 index 0000000..e1a58b5 --- /dev/null +++ b/src/node_modules/buffer-from/index.js @@ -0,0 +1,72 @@ +/* eslint-disable node/no-deprecated-api */ + +var toString = Object.prototype.toString + +var isModern = ( + typeof Buffer !== 'undefined' && + typeof Buffer.alloc === 'function' && + typeof Buffer.allocUnsafe === 'function' && + typeof Buffer.from === 'function' +) + +function isArrayBuffer (input) { + return toString.call(input).slice(8, -1) === 'ArrayBuffer' +} + +function fromArrayBuffer (obj, byteOffset, length) { + byteOffset >>>= 0 + + var maxLength = obj.byteLength - byteOffset + + if (maxLength < 0) { + throw new RangeError("'offset' is out of bounds") + } + + if (length === undefined) { + length = maxLength + } else { + length >>>= 0 + + if (length > maxLength) { + throw new RangeError("'length' is out of bounds") + } + } + + return isModern + ? Buffer.from(obj.slice(byteOffset, byteOffset + length)) + : new Buffer(new Uint8Array(obj.slice(byteOffset, byteOffset + length))) +} + +function fromString (string, encoding) { + if (typeof encoding !== 'string' || encoding === '') { + encoding = 'utf8' + } + + if (!Buffer.isEncoding(encoding)) { + throw new TypeError('"encoding" must be a valid string encoding') + } + + return isModern + ? Buffer.from(string, encoding) + : new Buffer(string, encoding) +} + +function bufferFrom (value, encodingOrOffset, length) { + if (typeof value === 'number') { + throw new TypeError('"value" argument must not be a number') + } + + if (isArrayBuffer(value)) { + return fromArrayBuffer(value, encodingOrOffset, length) + } + + if (typeof value === 'string') { + return fromString(value, encodingOrOffset) + } + + return isModern + ? Buffer.from(value) + : new Buffer(value) +} + +module.exports = bufferFrom diff --git a/src/node_modules/buffer-from/package.json b/src/node_modules/buffer-from/package.json new file mode 100644 index 0000000..6ac5327 --- /dev/null +++ b/src/node_modules/buffer-from/package.json @@ -0,0 +1,19 @@ +{ + "name": "buffer-from", + "version": "1.1.2", + "license": "MIT", + "repository": "LinusU/buffer-from", + "files": [ + "index.js" + ], + "scripts": { + "test": "standard && node test" + }, + "devDependencies": { + "standard": "^12.0.1" + }, + "keywords": [ + "buffer", + "buffer from" + ] +} diff --git a/src/node_modules/buffer-from/readme.md b/src/node_modules/buffer-from/readme.md new file mode 100644 index 0000000..9880a55 --- /dev/null +++ b/src/node_modules/buffer-from/readme.md @@ -0,0 +1,69 @@ +# Buffer From + +A [ponyfill](https://ponyfill.com) for `Buffer.from`, uses native implementation if available. + +## Installation + +```sh +npm install --save buffer-from +``` + +## Usage + +```js +const bufferFrom = require('buffer-from') + +console.log(bufferFrom([1, 2, 3, 4])) +//=> + +const arr = new Uint8Array([1, 2, 3, 4]) +console.log(bufferFrom(arr.buffer, 1, 2)) +//=> + +console.log(bufferFrom('test', 'utf8')) +//=> + +const buf = bufferFrom('test') +console.log(bufferFrom(buf)) +//=> +``` + +## API + +### bufferFrom(array) + +- `array` <Array> + +Allocates a new `Buffer` using an `array` of octets. + +### bufferFrom(arrayBuffer[, byteOffset[, length]]) + +- `arrayBuffer` <ArrayBuffer> The `.buffer` property of a TypedArray or ArrayBuffer +- `byteOffset` <Integer> Where to start copying from `arrayBuffer`. **Default:** `0` +- `length` <Integer> How many bytes to copy from `arrayBuffer`. **Default:** `arrayBuffer.length - byteOffset` + +When passed a reference to the `.buffer` property of a TypedArray instance, the +newly created `Buffer` will share the same allocated memory as the TypedArray. + +The optional `byteOffset` and `length` arguments specify a memory range within +the `arrayBuffer` that will be shared by the `Buffer`. + +### bufferFrom(buffer) + +- `buffer` <Buffer> An existing `Buffer` to copy data from + +Copies the passed `buffer` data onto a new `Buffer` instance. + +### bufferFrom(string[, encoding]) + +- `string` <String> A string to encode. +- `encoding` <String> The encoding of `string`. **Default:** `'utf8'` + +Creates a new `Buffer` containing the given JavaScript string `string`. If +provided, the `encoding` parameter identifies the character encoding of +`string`. + +## See also + +- [buffer-alloc](https://github.com/LinusU/buffer-alloc) A ponyfill for `Buffer.alloc` +- [buffer-alloc-unsafe](https://github.com/LinusU/buffer-alloc-unsafe) A ponyfill for `Buffer.allocUnsafe` diff --git a/src/node_modules/busboy/.eslintrc.js b/src/node_modules/busboy/.eslintrc.js new file mode 100644 index 0000000..be9311d --- /dev/null +++ b/src/node_modules/busboy/.eslintrc.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + extends: '@mscdex/eslint-config', +}; diff --git a/src/node_modules/busboy/.github/workflows/ci.yml b/src/node_modules/busboy/.github/workflows/ci.yml new file mode 100644 index 0000000..799bae0 --- /dev/null +++ b/src/node_modules/busboy/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + pull_request: + push: + branches: [ master ] + +jobs: + tests-linux: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [10.16.0, 10.x, 12.x, 14.x, 16.x] + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - name: Install module + run: npm install + - name: Run tests + run: npm test diff --git a/src/node_modules/busboy/.github/workflows/lint.yml b/src/node_modules/busboy/.github/workflows/lint.yml new file mode 100644 index 0000000..9f9e1f5 --- /dev/null +++ b/src/node_modules/busboy/.github/workflows/lint.yml @@ -0,0 +1,23 @@ +name: lint + +on: + pull_request: + push: + branches: [ master ] + +env: + NODE_VERSION: 16.x + +jobs: + lint-js: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v1 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Install ESLint + ESLint configs/plugins + run: npm install --only=dev + - name: Lint files + run: npm run lint diff --git a/src/node_modules/busboy/LICENSE b/src/node_modules/busboy/LICENSE new file mode 100644 index 0000000..290762e --- /dev/null +++ b/src/node_modules/busboy/LICENSE @@ -0,0 +1,19 @@ +Copyright Brian White. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. \ No newline at end of file diff --git a/src/node_modules/busboy/README.md b/src/node_modules/busboy/README.md new file mode 100644 index 0000000..654af30 --- /dev/null +++ b/src/node_modules/busboy/README.md @@ -0,0 +1,191 @@ +# Description + +A node.js module for parsing incoming HTML form data. + +Changes (breaking or otherwise) in v1.0.0 can be found [here](https://github.com/mscdex/busboy/issues/266). + +# Requirements + +* [node.js](http://nodejs.org/) -- v10.16.0 or newer + + +# Install + + npm install busboy + + +# Examples + +* Parsing (multipart) with default options: + +```js +const http = require('http'); + +const busboy = require('busboy'); + +http.createServer((req, res) => { + if (req.method === 'POST') { + console.log('POST request'); + const bb = busboy({ headers: req.headers }); + bb.on('file', (name, file, info) => { + const { filename, encoding, mimeType } = info; + console.log( + `File [${name}]: filename: %j, encoding: %j, mimeType: %j`, + filename, + encoding, + mimeType + ); + file.on('data', (data) => { + console.log(`File [${name}] got ${data.length} bytes`); + }).on('close', () => { + console.log(`File [${name}] done`); + }); + }); + bb.on('field', (name, val, info) => { + console.log(`Field [${name}]: value: %j`, val); + }); + bb.on('close', () => { + console.log('Done parsing form!'); + res.writeHead(303, { Connection: 'close', Location: '/' }); + res.end(); + }); + req.pipe(bb); + } else if (req.method === 'GET') { + res.writeHead(200, { Connection: 'close' }); + res.end(` + + + +
+
+
+ +
+ + + `); + } +}).listen(8000, () => { + console.log('Listening for requests'); +}); + +// Example output: +// +// Listening for requests +// < ... form submitted ... > +// POST request +// File [filefield]: filename: "logo.jpg", encoding: "binary", mime: "image/jpeg" +// File [filefield] got 11912 bytes +// Field [textfield]: value: "testing! :-)" +// File [filefield] done +// Done parsing form! +``` + +* Save all incoming files to disk: + +```js +const { randomFillSync } = require('crypto'); +const fs = require('fs'); +const http = require('http'); +const os = require('os'); +const path = require('path'); + +const busboy = require('busboy'); + +const random = (() => { + const buf = Buffer.alloc(16); + return () => randomFillSync(buf).toString('hex'); +})(); + +http.createServer((req, res) => { + if (req.method === 'POST') { + const bb = busboy({ headers: req.headers }); + bb.on('file', (name, file, info) => { + const saveTo = path.join(os.tmpdir(), `busboy-upload-${random()}`); + file.pipe(fs.createWriteStream(saveTo)); + }); + bb.on('close', () => { + res.writeHead(200, { 'Connection': 'close' }); + res.end(`That's all folks!`); + }); + req.pipe(bb); + return; + } + res.writeHead(404); + res.end(); +}).listen(8000, () => { + console.log('Listening for requests'); +}); +``` + + +# API + +## Exports + +`busboy` exports a single function: + +**( _function_ )**(< _object_ >config) - Creates and returns a new _Writable_ form parser stream. + +* Valid `config` properties: + + * **headers** - _object_ - These are the HTTP headers of the incoming request, which are used by individual parsers. + + * **highWaterMark** - _integer_ - highWaterMark to use for the parser stream. **Default:** node's _stream.Writable_ default. + + * **fileHwm** - _integer_ - highWaterMark to use for individual file streams. **Default:** node's _stream.Readable_ default. + + * **defCharset** - _string_ - Default character set to use when one isn't defined. **Default:** `'utf8'`. + + * **defParamCharset** - _string_ - For multipart forms, the default character set to use for values of part header parameters (e.g. filename) that are not extended parameters (that contain an explicit charset). **Default:** `'latin1'`. + + * **preservePath** - _boolean_ - If paths in filenames from file parts in a `'multipart/form-data'` request shall be preserved. **Default:** `false`. + + * **limits** - _object_ - Various limits on incoming data. Valid properties are: + + * **fieldNameSize** - _integer_ - Max field name size (in bytes). **Default:** `100`. + + * **fieldSize** - _integer_ - Max field value size (in bytes). **Default:** `1048576` (1MB). + + * **fields** - _integer_ - Max number of non-file fields. **Default:** `Infinity`. + + * **fileSize** - _integer_ - For multipart forms, the max file size (in bytes). **Default:** `Infinity`. + + * **files** - _integer_ - For multipart forms, the max number of file fields. **Default:** `Infinity`. + + * **parts** - _integer_ - For multipart forms, the max number of parts (fields + files). **Default:** `Infinity`. + + * **headerPairs** - _integer_ - For multipart forms, the max number of header key-value pairs to parse. **Default:** `2000` (same as node's http module). + +This function can throw exceptions if there is something wrong with the values in `config`. For example, if the Content-Type in `headers` is missing entirely, is not a supported type, or is missing the boundary for `'multipart/form-data'` requests. + +## (Special) Parser stream events + +* **file**(< _string_ >name, < _Readable_ >stream, < _object_ >info) - Emitted for each new file found. `name` contains the form field name. `stream` is a _Readable_ stream containing the file's data. No transformations/conversions (e.g. base64 to raw binary) are done on the file's data. `info` contains the following properties: + + * `filename` - _string_ - If supplied, this contains the file's filename. **WARNING:** You should almost _never_ use this value as-is (especially if you are using `preservePath: true` in your `config`) as it could contain malicious input. You are better off generating your own (safe) filenames, or at the very least using a hash of the filename. + + * `encoding` - _string_ - The file's `'Content-Transfer-Encoding'` value. + + * `mimeType` - _string_ - The file's `'Content-Type'` value. + + **Note:** If you listen for this event, you should always consume the `stream` whether you care about its contents or not (you can simply do `stream.resume();` if you want to discard/skip the contents), otherwise the `'finish'`/`'close'` event will never fire on the busboy parser stream. + However, if you aren't accepting files, you can either simply not listen for the `'file'` event at all or set `limits.files` to `0`, and any/all files will be automatically skipped (these skipped files will still count towards any configured `limits.files` and `limits.parts` limits though). + + **Note:** If a configured `limits.fileSize` limit was reached for a file, `stream` will both have a boolean property `truncated` set to `true` (best checked at the end of the stream) and emit a `'limit'` event to notify you when this happens. + +* **field**(< _string_ >name, < _string_ >value, < _object_ >info) - Emitted for each new non-file field found. `name` contains the form field name. `value` contains the string value of the field. `info` contains the following properties: + + * `nameTruncated` - _boolean_ - Whether `name` was truncated or not (due to a configured `limits.fieldNameSize` limit) + + * `valueTruncated` - _boolean_ - Whether `value` was truncated or not (due to a configured `limits.fieldSize` limit) + + * `encoding` - _string_ - The field's `'Content-Transfer-Encoding'` value. + + * `mimeType` - _string_ - The field's `'Content-Type'` value. + +* **partsLimit**() - Emitted when the configured `limits.parts` limit has been reached. No more `'file'` or `'field'` events will be emitted. + +* **filesLimit**() - Emitted when the configured `limits.files` limit has been reached. No more `'file'` events will be emitted. + +* **fieldsLimit**() - Emitted when the configured `limits.fields` limit has been reached. No more `'field'` events will be emitted. diff --git a/src/node_modules/busboy/bench/bench-multipart-fields-100mb-big.js b/src/node_modules/busboy/bench/bench-multipart-fields-100mb-big.js new file mode 100644 index 0000000..ef15729 --- /dev/null +++ b/src/node_modules/busboy/bench/bench-multipart-fields-100mb-big.js @@ -0,0 +1,149 @@ +'use strict'; + +function createMultipartBuffers(boundary, sizes) { + const bufs = []; + for (let i = 0; i < sizes.length; ++i) { + const mb = sizes[i] * 1024 * 1024; + bufs.push(Buffer.from([ + `--${boundary}`, + `content-disposition: form-data; name="field${i + 1}"`, + '', + '0'.repeat(mb), + '', + ].join('\r\n'))); + } + bufs.push(Buffer.from([ + `--${boundary}--`, + '', + ].join('\r\n'))); + return bufs; +} + +const boundary = '-----------------------------168072824752491622650073'; +const buffers = createMultipartBuffers(boundary, [ + 10, + 10, + 10, + 20, + 50, +]); +const calls = { + partBegin: 0, + headerField: 0, + headerValue: 0, + headerEnd: 0, + headersEnd: 0, + partData: 0, + partEnd: 0, + end: 0, +}; + +const moduleName = process.argv[2]; +switch (moduleName) { + case 'busboy': { + const busboy = require('busboy'); + + const parser = busboy({ + limits: { + fieldSizeLimit: Infinity, + }, + headers: { + 'content-type': `multipart/form-data; boundary=${boundary}`, + }, + }); + parser.on('field', (name, val, info) => { + ++calls.partBegin; + ++calls.partData; + ++calls.partEnd; + }).on('close', () => { + ++calls.end; + console.timeEnd(moduleName); + }); + + console.time(moduleName); + for (const buf of buffers) + parser.write(buf); + break; + } + + case 'formidable': { + const { MultipartParser } = require('formidable'); + + const parser = new MultipartParser(); + parser.initWithBoundary(boundary); + parser.on('data', ({ name }) => { + ++calls[name]; + if (name === 'end') + console.timeEnd(moduleName); + }); + + console.time(moduleName); + for (const buf of buffers) + parser.write(buf); + + break; + } + + case 'multiparty': { + const { Readable } = require('stream'); + + const { Form } = require('multiparty'); + + const form = new Form({ + maxFieldsSize: Infinity, + maxFields: Infinity, + maxFilesSize: Infinity, + autoFields: false, + autoFiles: false, + }); + + const req = new Readable({ read: () => {} }); + req.headers = { + 'content-type': `multipart/form-data; boundary=${boundary}`, + }; + + function hijack(name, fn) { + const oldFn = form[name]; + form[name] = function() { + fn(); + return oldFn.apply(this, arguments); + }; + } + + hijack('onParseHeaderField', () => { + ++calls.headerField; + }); + hijack('onParseHeaderValue', () => { + ++calls.headerValue; + }); + hijack('onParsePartBegin', () => { + ++calls.partBegin; + }); + hijack('onParsePartData', () => { + ++calls.partData; + }); + hijack('onParsePartEnd', () => { + ++calls.partEnd; + }); + + form.on('close', () => { + ++calls.end; + console.timeEnd(moduleName); + }).on('part', (p) => p.resume()); + + console.time(moduleName); + form.parse(req); + for (const buf of buffers) + req.push(buf); + req.push(null); + + break; + } + + default: + if (moduleName === undefined) + console.error('Missing parser module name'); + else + console.error(`Invalid parser module name: ${moduleName}`); + process.exit(1); +} diff --git a/src/node_modules/busboy/bench/bench-multipart-fields-100mb-small.js b/src/node_modules/busboy/bench/bench-multipart-fields-100mb-small.js new file mode 100644 index 0000000..f32d421 --- /dev/null +++ b/src/node_modules/busboy/bench/bench-multipart-fields-100mb-small.js @@ -0,0 +1,143 @@ +'use strict'; + +function createMultipartBuffers(boundary, sizes) { + const bufs = []; + for (let i = 0; i < sizes.length; ++i) { + const mb = sizes[i] * 1024 * 1024; + bufs.push(Buffer.from([ + `--${boundary}`, + `content-disposition: form-data; name="field${i + 1}"`, + '', + '0'.repeat(mb), + '', + ].join('\r\n'))); + } + bufs.push(Buffer.from([ + `--${boundary}--`, + '', + ].join('\r\n'))); + return bufs; +} + +const boundary = '-----------------------------168072824752491622650073'; +const buffers = createMultipartBuffers(boundary, (new Array(100)).fill(1)); +const calls = { + partBegin: 0, + headerField: 0, + headerValue: 0, + headerEnd: 0, + headersEnd: 0, + partData: 0, + partEnd: 0, + end: 0, +}; + +const moduleName = process.argv[2]; +switch (moduleName) { + case 'busboy': { + const busboy = require('busboy'); + + const parser = busboy({ + limits: { + fieldSizeLimit: Infinity, + }, + headers: { + 'content-type': `multipart/form-data; boundary=${boundary}`, + }, + }); + parser.on('field', (name, val, info) => { + ++calls.partBegin; + ++calls.partData; + ++calls.partEnd; + }).on('close', () => { + ++calls.end; + console.timeEnd(moduleName); + }); + + console.time(moduleName); + for (const buf of buffers) + parser.write(buf); + break; + } + + case 'formidable': { + const { MultipartParser } = require('formidable'); + + const parser = new MultipartParser(); + parser.initWithBoundary(boundary); + parser.on('data', ({ name }) => { + ++calls[name]; + if (name === 'end') + console.timeEnd(moduleName); + }); + + console.time(moduleName); + for (const buf of buffers) + parser.write(buf); + + break; + } + + case 'multiparty': { + const { Readable } = require('stream'); + + const { Form } = require('multiparty'); + + const form = new Form({ + maxFieldsSize: Infinity, + maxFields: Infinity, + maxFilesSize: Infinity, + autoFields: false, + autoFiles: false, + }); + + const req = new Readable({ read: () => {} }); + req.headers = { + 'content-type': `multipart/form-data; boundary=${boundary}`, + }; + + function hijack(name, fn) { + const oldFn = form[name]; + form[name] = function() { + fn(); + return oldFn.apply(this, arguments); + }; + } + + hijack('onParseHeaderField', () => { + ++calls.headerField; + }); + hijack('onParseHeaderValue', () => { + ++calls.headerValue; + }); + hijack('onParsePartBegin', () => { + ++calls.partBegin; + }); + hijack('onParsePartData', () => { + ++calls.partData; + }); + hijack('onParsePartEnd', () => { + ++calls.partEnd; + }); + + form.on('close', () => { + ++calls.end; + console.timeEnd(moduleName); + }).on('part', (p) => p.resume()); + + console.time(moduleName); + form.parse(req); + for (const buf of buffers) + req.push(buf); + req.push(null); + + break; + } + + default: + if (moduleName === undefined) + console.error('Missing parser module name'); + else + console.error(`Invalid parser module name: ${moduleName}`); + process.exit(1); +} diff --git a/src/node_modules/busboy/bench/bench-multipart-files-100mb-big.js b/src/node_modules/busboy/bench/bench-multipart-files-100mb-big.js new file mode 100644 index 0000000..b46bdee --- /dev/null +++ b/src/node_modules/busboy/bench/bench-multipart-files-100mb-big.js @@ -0,0 +1,154 @@ +'use strict'; + +function createMultipartBuffers(boundary, sizes) { + const bufs = []; + for (let i = 0; i < sizes.length; ++i) { + const mb = sizes[i] * 1024 * 1024; + bufs.push(Buffer.from([ + `--${boundary}`, + `content-disposition: form-data; name="file${i + 1}"; ` + + `filename="random${i + 1}.bin"`, + 'content-type: application/octet-stream', + '', + '0'.repeat(mb), + '', + ].join('\r\n'))); + } + bufs.push(Buffer.from([ + `--${boundary}--`, + '', + ].join('\r\n'))); + return bufs; +} + +const boundary = '-----------------------------168072824752491622650073'; +const buffers = createMultipartBuffers(boundary, [ + 10, + 10, + 10, + 20, + 50, +]); +const calls = { + partBegin: 0, + headerField: 0, + headerValue: 0, + headerEnd: 0, + headersEnd: 0, + partData: 0, + partEnd: 0, + end: 0, +}; + +const moduleName = process.argv[2]; +switch (moduleName) { + case 'busboy': { + const busboy = require('busboy'); + + const parser = busboy({ + limits: { + fieldSizeLimit: Infinity, + }, + headers: { + 'content-type': `multipart/form-data; boundary=${boundary}`, + }, + }); + parser.on('file', (name, stream, info) => { + ++calls.partBegin; + stream.on('data', (chunk) => { + ++calls.partData; + }).on('end', () => { + ++calls.partEnd; + }); + }).on('close', () => { + ++calls.end; + console.timeEnd(moduleName); + }); + + console.time(moduleName); + for (const buf of buffers) + parser.write(buf); + break; + } + + case 'formidable': { + const { MultipartParser } = require('formidable'); + + const parser = new MultipartParser(); + parser.initWithBoundary(boundary); + parser.on('data', ({ name }) => { + ++calls[name]; + if (name === 'end') + console.timeEnd(moduleName); + }); + + console.time(moduleName); + for (const buf of buffers) + parser.write(buf); + + break; + } + + case 'multiparty': { + const { Readable } = require('stream'); + + const { Form } = require('multiparty'); + + const form = new Form({ + maxFieldsSize: Infinity, + maxFields: Infinity, + maxFilesSize: Infinity, + autoFields: false, + autoFiles: false, + }); + + const req = new Readable({ read: () => {} }); + req.headers = { + 'content-type': `multipart/form-data; boundary=${boundary}`, + }; + + function hijack(name, fn) { + const oldFn = form[name]; + form[name] = function() { + fn(); + return oldFn.apply(this, arguments); + }; + } + + hijack('onParseHeaderField', () => { + ++calls.headerField; + }); + hijack('onParseHeaderValue', () => { + ++calls.headerValue; + }); + hijack('onParsePartBegin', () => { + ++calls.partBegin; + }); + hijack('onParsePartData', () => { + ++calls.partData; + }); + hijack('onParsePartEnd', () => { + ++calls.partEnd; + }); + + form.on('close', () => { + ++calls.end; + console.timeEnd(moduleName); + }).on('part', (p) => p.resume()); + + console.time(moduleName); + form.parse(req); + for (const buf of buffers) + req.push(buf); + req.push(null); + + break; + } + + default: + if (moduleName === undefined) + console.error('Missing parser module name'); + else + console.error(`Invalid parser module name: ${moduleName}`); + process.exit(1); +} diff --git a/src/node_modules/busboy/bench/bench-multipart-files-100mb-small.js b/src/node_modules/busboy/bench/bench-multipart-files-100mb-small.js new file mode 100644 index 0000000..46b5dff --- /dev/null +++ b/src/node_modules/busboy/bench/bench-multipart-files-100mb-small.js @@ -0,0 +1,148 @@ +'use strict'; + +function createMultipartBuffers(boundary, sizes) { + const bufs = []; + for (let i = 0; i < sizes.length; ++i) { + const mb = sizes[i] * 1024 * 1024; + bufs.push(Buffer.from([ + `--${boundary}`, + `content-disposition: form-data; name="file${i + 1}"; ` + + `filename="random${i + 1}.bin"`, + 'content-type: application/octet-stream', + '', + '0'.repeat(mb), + '', + ].join('\r\n'))); + } + bufs.push(Buffer.from([ + `--${boundary}--`, + '', + ].join('\r\n'))); + return bufs; +} + +const boundary = '-----------------------------168072824752491622650073'; +const buffers = createMultipartBuffers(boundary, (new Array(100)).fill(1)); +const calls = { + partBegin: 0, + headerField: 0, + headerValue: 0, + headerEnd: 0, + headersEnd: 0, + partData: 0, + partEnd: 0, + end: 0, +}; + +const moduleName = process.argv[2]; +switch (moduleName) { + case 'busboy': { + const busboy = require('busboy'); + + const parser = busboy({ + limits: { + fieldSizeLimit: Infinity, + }, + headers: { + 'content-type': `multipart/form-data; boundary=${boundary}`, + }, + }); + parser.on('file', (name, stream, info) => { + ++calls.partBegin; + stream.on('data', (chunk) => { + ++calls.partData; + }).on('end', () => { + ++calls.partEnd; + }); + }).on('close', () => { + ++calls.end; + console.timeEnd(moduleName); + }); + + console.time(moduleName); + for (const buf of buffers) + parser.write(buf); + break; + } + + case 'formidable': { + const { MultipartParser } = require('formidable'); + + const parser = new MultipartParser(); + parser.initWithBoundary(boundary); + parser.on('data', ({ name }) => { + ++calls[name]; + if (name === 'end') + console.timeEnd(moduleName); + }); + + console.time(moduleName); + for (const buf of buffers) + parser.write(buf); + + break; + } + + case 'multiparty': { + const { Readable } = require('stream'); + + const { Form } = require('multiparty'); + + const form = new Form({ + maxFieldsSize: Infinity, + maxFields: Infinity, + maxFilesSize: Infinity, + autoFields: false, + autoFiles: false, + }); + + const req = new Readable({ read: () => {} }); + req.headers = { + 'content-type': `multipart/form-data; boundary=${boundary}`, + }; + + function hijack(name, fn) { + const oldFn = form[name]; + form[name] = function() { + fn(); + return oldFn.apply(this, arguments); + }; + } + + hijack('onParseHeaderField', () => { + ++calls.headerField; + }); + hijack('onParseHeaderValue', () => { + ++calls.headerValue; + }); + hijack('onParsePartBegin', () => { + ++calls.partBegin; + }); + hijack('onParsePartData', () => { + ++calls.partData; + }); + hijack('onParsePartEnd', () => { + ++calls.partEnd; + }); + + form.on('close', () => { + ++calls.end; + console.timeEnd(moduleName); + }).on('part', (p) => p.resume()); + + console.time(moduleName); + form.parse(req); + for (const buf of buffers) + req.push(buf); + req.push(null); + + break; + } + + default: + if (moduleName === undefined) + console.error('Missing parser module name'); + else + console.error(`Invalid parser module name: ${moduleName}`); + process.exit(1); +} diff --git a/src/node_modules/busboy/bench/bench-urlencoded-fields-100pairs-small.js b/src/node_modules/busboy/bench/bench-urlencoded-fields-100pairs-small.js new file mode 100644 index 0000000..5c337df --- /dev/null +++ b/src/node_modules/busboy/bench/bench-urlencoded-fields-100pairs-small.js @@ -0,0 +1,101 @@ +'use strict'; + +const buffers = [ + Buffer.from( + (new Array(100)).fill('').map((_, i) => `key${i}=value${i}`).join('&') + ), +]; +const calls = { + field: 0, + end: 0, +}; + +let n = 3e3; + +const moduleName = process.argv[2]; +switch (moduleName) { + case 'busboy': { + const busboy = require('busboy'); + + console.time(moduleName); + (function next() { + const parser = busboy({ + limits: { + fieldSizeLimit: Infinity, + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded; charset=utf-8', + }, + }); + parser.on('field', (name, val, info) => { + ++calls.field; + }).on('close', () => { + ++calls.end; + if (--n === 0) + console.timeEnd(moduleName); + else + process.nextTick(next); + }); + + for (const buf of buffers) + parser.write(buf); + parser.end(); + })(); + break; + } + + case 'formidable': { + const QuerystringParser = + require('formidable/src/parsers/Querystring.js'); + + console.time(moduleName); + (function next() { + const parser = new QuerystringParser(); + parser.on('data', (obj) => { + ++calls.field; + }).on('end', () => { + ++calls.end; + if (--n === 0) + console.timeEnd(moduleName); + else + process.nextTick(next); + }); + + for (const buf of buffers) + parser.write(buf); + parser.end(); + })(); + break; + } + + case 'formidable-streaming': { + const QuerystringParser = + require('formidable/src/parsers/StreamingQuerystring.js'); + + console.time(moduleName); + (function next() { + const parser = new QuerystringParser(); + parser.on('data', (obj) => { + ++calls.field; + }).on('end', () => { + ++calls.end; + if (--n === 0) + console.timeEnd(moduleName); + else + process.nextTick(next); + }); + + for (const buf of buffers) + parser.write(buf); + parser.end(); + })(); + break; + } + + default: + if (moduleName === undefined) + console.error('Missing parser module name'); + else + console.error(`Invalid parser module name: ${moduleName}`); + process.exit(1); +} diff --git a/src/node_modules/busboy/bench/bench-urlencoded-fields-900pairs-small-alt.js b/src/node_modules/busboy/bench/bench-urlencoded-fields-900pairs-small-alt.js new file mode 100644 index 0000000..1f5645c --- /dev/null +++ b/src/node_modules/busboy/bench/bench-urlencoded-fields-900pairs-small-alt.js @@ -0,0 +1,84 @@ +'use strict'; + +const buffers = [ + Buffer.from( + (new Array(900)).fill('').map((_, i) => `key${i}=value${i}`).join('&') + ), +]; +const calls = { + field: 0, + end: 0, +}; + +const moduleName = process.argv[2]; +switch (moduleName) { + case 'busboy': { + const busboy = require('busboy'); + + console.time(moduleName); + const parser = busboy({ + limits: { + fieldSizeLimit: Infinity, + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded; charset=utf-8', + }, + }); + parser.on('field', (name, val, info) => { + ++calls.field; + }).on('close', () => { + ++calls.end; + console.timeEnd(moduleName); + }); + + for (const buf of buffers) + parser.write(buf); + parser.end(); + break; + } + + case 'formidable': { + const QuerystringParser = + require('formidable/src/parsers/Querystring.js'); + + console.time(moduleName); + const parser = new QuerystringParser(); + parser.on('data', (obj) => { + ++calls.field; + }).on('end', () => { + ++calls.end; + console.timeEnd(moduleName); + }); + + for (const buf of buffers) + parser.write(buf); + parser.end(); + break; + } + + case 'formidable-streaming': { + const QuerystringParser = + require('formidable/src/parsers/StreamingQuerystring.js'); + + console.time(moduleName); + const parser = new QuerystringParser(); + parser.on('data', (obj) => { + ++calls.field; + }).on('end', () => { + ++calls.end; + console.timeEnd(moduleName); + }); + + for (const buf of buffers) + parser.write(buf); + parser.end(); + break; + } + + default: + if (moduleName === undefined) + console.error('Missing parser module name'); + else + console.error(`Invalid parser module name: ${moduleName}`); + process.exit(1); +} diff --git a/src/node_modules/busboy/lib/index.js b/src/node_modules/busboy/lib/index.js new file mode 100644 index 0000000..873272d --- /dev/null +++ b/src/node_modules/busboy/lib/index.js @@ -0,0 +1,57 @@ +'use strict'; + +const { parseContentType } = require('./utils.js'); + +function getInstance(cfg) { + const headers = cfg.headers; + const conType = parseContentType(headers['content-type']); + if (!conType) + throw new Error('Malformed content type'); + + for (const type of TYPES) { + const matched = type.detect(conType); + if (!matched) + continue; + + const instanceCfg = { + limits: cfg.limits, + headers, + conType, + highWaterMark: undefined, + fileHwm: undefined, + defCharset: undefined, + defParamCharset: undefined, + preservePath: false, + }; + if (cfg.highWaterMark) + instanceCfg.highWaterMark = cfg.highWaterMark; + if (cfg.fileHwm) + instanceCfg.fileHwm = cfg.fileHwm; + instanceCfg.defCharset = cfg.defCharset; + instanceCfg.defParamCharset = cfg.defParamCharset; + instanceCfg.preservePath = cfg.preservePath; + return new type(instanceCfg); + } + + throw new Error(`Unsupported content type: ${headers['content-type']}`); +} + +// Note: types are explicitly listed here for easier bundling +// See: https://github.com/mscdex/busboy/issues/121 +const TYPES = [ + require('./types/multipart'), + require('./types/urlencoded'), +].filter(function(typemod) { return typeof typemod.detect === 'function'; }); + +module.exports = (cfg) => { + if (typeof cfg !== 'object' || cfg === null) + cfg = {}; + + if (typeof cfg.headers !== 'object' + || cfg.headers === null + || typeof cfg.headers['content-type'] !== 'string') { + throw new Error('Missing Content-Type'); + } + + return getInstance(cfg); +}; diff --git a/src/node_modules/busboy/lib/types/multipart.js b/src/node_modules/busboy/lib/types/multipart.js new file mode 100644 index 0000000..cc0d7bb --- /dev/null +++ b/src/node_modules/busboy/lib/types/multipart.js @@ -0,0 +1,653 @@ +'use strict'; + +const { Readable, Writable } = require('stream'); + +const StreamSearch = require('streamsearch'); + +const { + basename, + convertToUTF8, + getDecoder, + parseContentType, + parseDisposition, +} = require('../utils.js'); + +const BUF_CRLF = Buffer.from('\r\n'); +const BUF_CR = Buffer.from('\r'); +const BUF_DASH = Buffer.from('-'); + +function noop() {} + +const MAX_HEADER_PAIRS = 2000; // From node +const MAX_HEADER_SIZE = 16 * 1024; // From node (its default value) + +const HPARSER_NAME = 0; +const HPARSER_PRE_OWS = 1; +const HPARSER_VALUE = 2; +class HeaderParser { + constructor(cb) { + this.header = Object.create(null); + this.pairCount = 0; + this.byteCount = 0; + this.state = HPARSER_NAME; + this.name = ''; + this.value = ''; + this.crlf = 0; + this.cb = cb; + } + + reset() { + this.header = Object.create(null); + this.pairCount = 0; + this.byteCount = 0; + this.state = HPARSER_NAME; + this.name = ''; + this.value = ''; + this.crlf = 0; + } + + push(chunk, pos, end) { + let start = pos; + while (pos < end) { + switch (this.state) { + case HPARSER_NAME: { + let done = false; + for (; pos < end; ++pos) { + if (this.byteCount === MAX_HEADER_SIZE) + return -1; + ++this.byteCount; + const code = chunk[pos]; + if (TOKEN[code] !== 1) { + if (code !== 58/* ':' */) + return -1; + this.name += chunk.latin1Slice(start, pos); + if (this.name.length === 0) + return -1; + ++pos; + done = true; + this.state = HPARSER_PRE_OWS; + break; + } + } + if (!done) { + this.name += chunk.latin1Slice(start, pos); + break; + } + // FALLTHROUGH + } + case HPARSER_PRE_OWS: { + // Skip optional whitespace + let done = false; + for (; pos < end; ++pos) { + if (this.byteCount === MAX_HEADER_SIZE) + return -1; + ++this.byteCount; + const code = chunk[pos]; + if (code !== 32/* ' ' */ && code !== 9/* '\t' */) { + start = pos; + done = true; + this.state = HPARSER_VALUE; + break; + } + } + if (!done) + break; + // FALLTHROUGH + } + case HPARSER_VALUE: + switch (this.crlf) { + case 0: // Nothing yet + for (; pos < end; ++pos) { + if (this.byteCount === MAX_HEADER_SIZE) + return -1; + ++this.byteCount; + const code = chunk[pos]; + if (FIELD_VCHAR[code] !== 1) { + if (code !== 13/* '\r' */) + return -1; + ++this.crlf; + break; + } + } + this.value += chunk.latin1Slice(start, pos++); + break; + case 1: // Received CR + if (this.byteCount === MAX_HEADER_SIZE) + return -1; + ++this.byteCount; + if (chunk[pos++] !== 10/* '\n' */) + return -1; + ++this.crlf; + break; + case 2: { // Received CR LF + if (this.byteCount === MAX_HEADER_SIZE) + return -1; + ++this.byteCount; + const code = chunk[pos]; + if (code === 32/* ' ' */ || code === 9/* '\t' */) { + // Folded value + start = pos; + this.crlf = 0; + } else { + if (++this.pairCount < MAX_HEADER_PAIRS) { + this.name = this.name.toLowerCase(); + if (this.header[this.name] === undefined) + this.header[this.name] = [this.value]; + else + this.header[this.name].push(this.value); + } + if (code === 13/* '\r' */) { + ++this.crlf; + ++pos; + } else { + // Assume start of next header field name + start = pos; + this.crlf = 0; + this.state = HPARSER_NAME; + this.name = ''; + this.value = ''; + } + } + break; + } + case 3: { // Received CR LF CR + if (this.byteCount === MAX_HEADER_SIZE) + return -1; + ++this.byteCount; + if (chunk[pos++] !== 10/* '\n' */) + return -1; + // End of header + const header = this.header; + this.reset(); + this.cb(header); + return pos; + } + } + break; + } + } + + return pos; + } +} + +class FileStream extends Readable { + constructor(opts, owner) { + super(opts); + this.truncated = false; + this._readcb = null; + this.once('end', () => { + // We need to make sure that we call any outstanding _writecb() that is + // associated with this file so that processing of the rest of the form + // can continue. This may not happen if the file stream ends right after + // backpressure kicks in, so we force it here. + this._read(); + if (--owner._fileEndsLeft === 0 && owner._finalcb) { + const cb = owner._finalcb; + owner._finalcb = null; + // Make sure other 'end' event handlers get a chance to be executed + // before busboy's 'finish' event is emitted + process.nextTick(cb); + } + }); + } + _read(n) { + const cb = this._readcb; + if (cb) { + this._readcb = null; + cb(); + } + } +} + +const ignoreData = { + push: (chunk, pos) => {}, + destroy: () => {}, +}; + +function callAndUnsetCb(self, err) { + const cb = self._writecb; + self._writecb = null; + if (err) + self.destroy(err); + else if (cb) + cb(); +} + +function nullDecoder(val, hint) { + return val; +} + +class Multipart extends Writable { + constructor(cfg) { + const streamOpts = { + autoDestroy: true, + emitClose: true, + highWaterMark: (typeof cfg.highWaterMark === 'number' + ? cfg.highWaterMark + : undefined), + }; + super(streamOpts); + + if (!cfg.conType.params || typeof cfg.conType.params.boundary !== 'string') + throw new Error('Multipart: Boundary not found'); + + const boundary = cfg.conType.params.boundary; + const paramDecoder = (typeof cfg.defParamCharset === 'string' + && cfg.defParamCharset + ? getDecoder(cfg.defParamCharset) + : nullDecoder); + const defCharset = (cfg.defCharset || 'utf8'); + const preservePath = cfg.preservePath; + const fileOpts = { + autoDestroy: true, + emitClose: true, + highWaterMark: (typeof cfg.fileHwm === 'number' + ? cfg.fileHwm + : undefined), + }; + + const limits = cfg.limits; + const fieldSizeLimit = (limits && typeof limits.fieldSize === 'number' + ? limits.fieldSize + : 1 * 1024 * 1024); + const fileSizeLimit = (limits && typeof limits.fileSize === 'number' + ? limits.fileSize + : Infinity); + const filesLimit = (limits && typeof limits.files === 'number' + ? limits.files + : Infinity); + const fieldsLimit = (limits && typeof limits.fields === 'number' + ? limits.fields + : Infinity); + const partsLimit = (limits && typeof limits.parts === 'number' + ? limits.parts + : Infinity); + + let parts = -1; // Account for initial boundary + let fields = 0; + let files = 0; + let skipPart = false; + + this._fileEndsLeft = 0; + this._fileStream = undefined; + this._complete = false; + let fileSize = 0; + + let field; + let fieldSize = 0; + let partCharset; + let partEncoding; + let partType; + let partName; + let partTruncated = false; + + let hitFilesLimit = false; + let hitFieldsLimit = false; + + this._hparser = null; + const hparser = new HeaderParser((header) => { + this._hparser = null; + skipPart = false; + + partType = 'text/plain'; + partCharset = defCharset; + partEncoding = '7bit'; + partName = undefined; + partTruncated = false; + + let filename; + if (!header['content-disposition']) { + skipPart = true; + return; + } + + const disp = parseDisposition(header['content-disposition'][0], + paramDecoder); + if (!disp || disp.type !== 'form-data') { + skipPart = true; + return; + } + + if (disp.params) { + if (disp.params.name) + partName = disp.params.name; + + if (disp.params['filename*']) + filename = disp.params['filename*']; + else if (disp.params.filename) + filename = disp.params.filename; + + if (filename !== undefined && !preservePath) + filename = basename(filename); + } + + if (header['content-type']) { + const conType = parseContentType(header['content-type'][0]); + if (conType) { + partType = `${conType.type}/${conType.subtype}`; + if (conType.params && typeof conType.params.charset === 'string') + partCharset = conType.params.charset.toLowerCase(); + } + } + + if (header['content-transfer-encoding']) + partEncoding = header['content-transfer-encoding'][0].toLowerCase(); + + if (partType === 'application/octet-stream' || filename !== undefined) { + // File + + if (files === filesLimit) { + if (!hitFilesLimit) { + hitFilesLimit = true; + this.emit('filesLimit'); + } + skipPart = true; + return; + } + ++files; + + if (this.listenerCount('file') === 0) { + skipPart = true; + return; + } + + fileSize = 0; + this._fileStream = new FileStream(fileOpts, this); + ++this._fileEndsLeft; + this.emit( + 'file', + partName, + this._fileStream, + { filename, + encoding: partEncoding, + mimeType: partType } + ); + } else { + // Non-file + + if (fields === fieldsLimit) { + if (!hitFieldsLimit) { + hitFieldsLimit = true; + this.emit('fieldsLimit'); + } + skipPart = true; + return; + } + ++fields; + + if (this.listenerCount('field') === 0) { + skipPart = true; + return; + } + + field = []; + fieldSize = 0; + } + }); + + let matchPostBoundary = 0; + const ssCb = (isMatch, data, start, end, isDataSafe) => { +retrydata: + while (data) { + if (this._hparser !== null) { + const ret = this._hparser.push(data, start, end); + if (ret === -1) { + this._hparser = null; + hparser.reset(); + this.emit('error', new Error('Malformed part header')); + break; + } + start = ret; + } + + if (start === end) + break; + + if (matchPostBoundary !== 0) { + if (matchPostBoundary === 1) { + switch (data[start]) { + case 45: // '-' + // Try matching '--' after boundary + matchPostBoundary = 2; + ++start; + break; + case 13: // '\r' + // Try matching CR LF before header + matchPostBoundary = 3; + ++start; + break; + default: + matchPostBoundary = 0; + } + if (start === end) + return; + } + + if (matchPostBoundary === 2) { + matchPostBoundary = 0; + if (data[start] === 45/* '-' */) { + // End of multipart data + this._complete = true; + this._bparser = ignoreData; + return; + } + // We saw something other than '-', so put the dash we consumed + // "back" + const writecb = this._writecb; + this._writecb = noop; + ssCb(false, BUF_DASH, 0, 1, false); + this._writecb = writecb; + } else if (matchPostBoundary === 3) { + matchPostBoundary = 0; + if (data[start] === 10/* '\n' */) { + ++start; + if (parts >= partsLimit) + break; + // Prepare the header parser + this._hparser = hparser; + if (start === end) + break; + // Process the remaining data as a header + continue retrydata; + } else { + // We saw something other than LF, so put the CR we consumed + // "back" + const writecb = this._writecb; + this._writecb = noop; + ssCb(false, BUF_CR, 0, 1, false); + this._writecb = writecb; + } + } + } + + if (!skipPart) { + if (this._fileStream) { + let chunk; + const actualLen = Math.min(end - start, fileSizeLimit - fileSize); + if (!isDataSafe) { + chunk = Buffer.allocUnsafe(actualLen); + data.copy(chunk, 0, start, start + actualLen); + } else { + chunk = data.slice(start, start + actualLen); + } + + fileSize += chunk.length; + if (fileSize === fileSizeLimit) { + if (chunk.length > 0) + this._fileStream.push(chunk); + this._fileStream.emit('limit'); + this._fileStream.truncated = true; + skipPart = true; + } else if (!this._fileStream.push(chunk)) { + if (this._writecb) + this._fileStream._readcb = this._writecb; + this._writecb = null; + } + } else if (field !== undefined) { + let chunk; + const actualLen = Math.min( + end - start, + fieldSizeLimit - fieldSize + ); + if (!isDataSafe) { + chunk = Buffer.allocUnsafe(actualLen); + data.copy(chunk, 0, start, start + actualLen); + } else { + chunk = data.slice(start, start + actualLen); + } + + fieldSize += actualLen; + field.push(chunk); + if (fieldSize === fieldSizeLimit) { + skipPart = true; + partTruncated = true; + } + } + } + + break; + } + + if (isMatch) { + matchPostBoundary = 1; + + if (this._fileStream) { + // End the active file stream if the previous part was a file + this._fileStream.push(null); + this._fileStream = null; + } else if (field !== undefined) { + let data; + switch (field.length) { + case 0: + data = ''; + break; + case 1: + data = convertToUTF8(field[0], partCharset, 0); + break; + default: + data = convertToUTF8( + Buffer.concat(field, fieldSize), + partCharset, + 0 + ); + } + field = undefined; + fieldSize = 0; + this.emit( + 'field', + partName, + data, + { nameTruncated: false, + valueTruncated: partTruncated, + encoding: partEncoding, + mimeType: partType } + ); + } + + if (++parts === partsLimit) + this.emit('partsLimit'); + } + }; + this._bparser = new StreamSearch(`\r\n--${boundary}`, ssCb); + + this._writecb = null; + this._finalcb = null; + + // Just in case there is no preamble + this.write(BUF_CRLF); + } + + static detect(conType) { + return (conType.type === 'multipart' && conType.subtype === 'form-data'); + } + + _write(chunk, enc, cb) { + this._writecb = cb; + this._bparser.push(chunk, 0); + if (this._writecb) + callAndUnsetCb(this); + } + + _destroy(err, cb) { + this._hparser = null; + this._bparser = ignoreData; + if (!err) + err = checkEndState(this); + const fileStream = this._fileStream; + if (fileStream) { + this._fileStream = null; + fileStream.destroy(err); + } + cb(err); + } + + _final(cb) { + this._bparser.destroy(); + if (!this._complete) + return cb(new Error('Unexpected end of form')); + if (this._fileEndsLeft) + this._finalcb = finalcb.bind(null, this, cb); + else + finalcb(this, cb); + } +} + +function finalcb(self, cb, err) { + if (err) + return cb(err); + err = checkEndState(self); + cb(err); +} + +function checkEndState(self) { + if (self._hparser) + return new Error('Malformed part header'); + const fileStream = self._fileStream; + if (fileStream) { + self._fileStream = null; + fileStream.destroy(new Error('Unexpected end of file')); + } + if (!self._complete) + return new Error('Unexpected end of form'); +} + +const TOKEN = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]; + +const FIELD_VCHAR = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +]; + +module.exports = Multipart; diff --git a/src/node_modules/busboy/lib/types/urlencoded.js b/src/node_modules/busboy/lib/types/urlencoded.js new file mode 100644 index 0000000..5c463a2 --- /dev/null +++ b/src/node_modules/busboy/lib/types/urlencoded.js @@ -0,0 +1,350 @@ +'use strict'; + +const { Writable } = require('stream'); + +const { getDecoder } = require('../utils.js'); + +class URLEncoded extends Writable { + constructor(cfg) { + const streamOpts = { + autoDestroy: true, + emitClose: true, + highWaterMark: (typeof cfg.highWaterMark === 'number' + ? cfg.highWaterMark + : undefined), + }; + super(streamOpts); + + let charset = (cfg.defCharset || 'utf8'); + if (cfg.conType.params && typeof cfg.conType.params.charset === 'string') + charset = cfg.conType.params.charset; + + this.charset = charset; + + const limits = cfg.limits; + this.fieldSizeLimit = (limits && typeof limits.fieldSize === 'number' + ? limits.fieldSize + : 1 * 1024 * 1024); + this.fieldsLimit = (limits && typeof limits.fields === 'number' + ? limits.fields + : Infinity); + this.fieldNameSizeLimit = ( + limits && typeof limits.fieldNameSize === 'number' + ? limits.fieldNameSize + : 100 + ); + + this._inKey = true; + this._keyTrunc = false; + this._valTrunc = false; + this._bytesKey = 0; + this._bytesVal = 0; + this._fields = 0; + this._key = ''; + this._val = ''; + this._byte = -2; + this._lastPos = 0; + this._encode = 0; + this._decoder = getDecoder(charset); + } + + static detect(conType) { + return (conType.type === 'application' + && conType.subtype === 'x-www-form-urlencoded'); + } + + _write(chunk, enc, cb) { + if (this._fields >= this.fieldsLimit) + return cb(); + + let i = 0; + const len = chunk.length; + this._lastPos = 0; + + // Check if we last ended mid-percent-encoded byte + if (this._byte !== -2) { + i = readPctEnc(this, chunk, i, len); + if (i === -1) + return cb(new Error('Malformed urlencoded form')); + if (i >= len) + return cb(); + if (this._inKey) + ++this._bytesKey; + else + ++this._bytesVal; + } + +main: + while (i < len) { + if (this._inKey) { + // Parsing key + + i = skipKeyBytes(this, chunk, i, len); + + while (i < len) { + switch (chunk[i]) { + case 61: // '=' + if (this._lastPos < i) + this._key += chunk.latin1Slice(this._lastPos, i); + this._lastPos = ++i; + this._key = this._decoder(this._key, this._encode); + this._encode = 0; + this._inKey = false; + continue main; + case 38: // '&' + if (this._lastPos < i) + this._key += chunk.latin1Slice(this._lastPos, i); + this._lastPos = ++i; + this._key = this._decoder(this._key, this._encode); + this._encode = 0; + if (this._bytesKey > 0) { + this.emit( + 'field', + this._key, + '', + { nameTruncated: this._keyTrunc, + valueTruncated: false, + encoding: this.charset, + mimeType: 'text/plain' } + ); + } + this._key = ''; + this._val = ''; + this._keyTrunc = false; + this._valTrunc = false; + this._bytesKey = 0; + this._bytesVal = 0; + if (++this._fields >= this.fieldsLimit) { + this.emit('fieldsLimit'); + return cb(); + } + continue; + case 43: // '+' + if (this._lastPos < i) + this._key += chunk.latin1Slice(this._lastPos, i); + this._key += ' '; + this._lastPos = i + 1; + break; + case 37: // '%' + if (this._encode === 0) + this._encode = 1; + if (this._lastPos < i) + this._key += chunk.latin1Slice(this._lastPos, i); + this._lastPos = i + 1; + this._byte = -1; + i = readPctEnc(this, chunk, i + 1, len); + if (i === -1) + return cb(new Error('Malformed urlencoded form')); + if (i >= len) + return cb(); + ++this._bytesKey; + i = skipKeyBytes(this, chunk, i, len); + continue; + } + ++i; + ++this._bytesKey; + i = skipKeyBytes(this, chunk, i, len); + } + if (this._lastPos < i) + this._key += chunk.latin1Slice(this._lastPos, i); + } else { + // Parsing value + + i = skipValBytes(this, chunk, i, len); + + while (i < len) { + switch (chunk[i]) { + case 38: // '&' + if (this._lastPos < i) + this._val += chunk.latin1Slice(this._lastPos, i); + this._lastPos = ++i; + this._inKey = true; + this._val = this._decoder(this._val, this._encode); + this._encode = 0; + if (this._bytesKey > 0 || this._bytesVal > 0) { + this.emit( + 'field', + this._key, + this._val, + { nameTruncated: this._keyTrunc, + valueTruncated: this._valTrunc, + encoding: this.charset, + mimeType: 'text/plain' } + ); + } + this._key = ''; + this._val = ''; + this._keyTrunc = false; + this._valTrunc = false; + this._bytesKey = 0; + this._bytesVal = 0; + if (++this._fields >= this.fieldsLimit) { + this.emit('fieldsLimit'); + return cb(); + } + continue main; + case 43: // '+' + if (this._lastPos < i) + this._val += chunk.latin1Slice(this._lastPos, i); + this._val += ' '; + this._lastPos = i + 1; + break; + case 37: // '%' + if (this._encode === 0) + this._encode = 1; + if (this._lastPos < i) + this._val += chunk.latin1Slice(this._lastPos, i); + this._lastPos = i + 1; + this._byte = -1; + i = readPctEnc(this, chunk, i + 1, len); + if (i === -1) + return cb(new Error('Malformed urlencoded form')); + if (i >= len) + return cb(); + ++this._bytesVal; + i = skipValBytes(this, chunk, i, len); + continue; + } + ++i; + ++this._bytesVal; + i = skipValBytes(this, chunk, i, len); + } + if (this._lastPos < i) + this._val += chunk.latin1Slice(this._lastPos, i); + } + } + + cb(); + } + + _final(cb) { + if (this._byte !== -2) + return cb(new Error('Malformed urlencoded form')); + if (!this._inKey || this._bytesKey > 0 || this._bytesVal > 0) { + if (this._inKey) + this._key = this._decoder(this._key, this._encode); + else + this._val = this._decoder(this._val, this._encode); + this.emit( + 'field', + this._key, + this._val, + { nameTruncated: this._keyTrunc, + valueTruncated: this._valTrunc, + encoding: this.charset, + mimeType: 'text/plain' } + ); + } + cb(); + } +} + +function readPctEnc(self, chunk, pos, len) { + if (pos >= len) + return len; + + if (self._byte === -1) { + // We saw a '%' but no hex characters yet + const hexUpper = HEX_VALUES[chunk[pos++]]; + if (hexUpper === -1) + return -1; + + if (hexUpper >= 8) + self._encode = 2; // Indicate high bits detected + + if (pos < len) { + // Both hex characters are in this chunk + const hexLower = HEX_VALUES[chunk[pos++]]; + if (hexLower === -1) + return -1; + + if (self._inKey) + self._key += String.fromCharCode((hexUpper << 4) + hexLower); + else + self._val += String.fromCharCode((hexUpper << 4) + hexLower); + + self._byte = -2; + self._lastPos = pos; + } else { + // Only one hex character was available in this chunk + self._byte = hexUpper; + } + } else { + // We saw only one hex character so far + const hexLower = HEX_VALUES[chunk[pos++]]; + if (hexLower === -1) + return -1; + + if (self._inKey) + self._key += String.fromCharCode((self._byte << 4) + hexLower); + else + self._val += String.fromCharCode((self._byte << 4) + hexLower); + + self._byte = -2; + self._lastPos = pos; + } + + return pos; +} + +function skipKeyBytes(self, chunk, pos, len) { + // Skip bytes if we've truncated + if (self._bytesKey > self.fieldNameSizeLimit) { + if (!self._keyTrunc) { + if (self._lastPos < pos) + self._key += chunk.latin1Slice(self._lastPos, pos - 1); + } + self._keyTrunc = true; + for (; pos < len; ++pos) { + const code = chunk[pos]; + if (code === 61/* '=' */ || code === 38/* '&' */) + break; + ++self._bytesKey; + } + self._lastPos = pos; + } + + return pos; +} + +function skipValBytes(self, chunk, pos, len) { + // Skip bytes if we've truncated + if (self._bytesVal > self.fieldSizeLimit) { + if (!self._valTrunc) { + if (self._lastPos < pos) + self._val += chunk.latin1Slice(self._lastPos, pos - 1); + } + self._valTrunc = true; + for (; pos < len; ++pos) { + if (chunk[pos] === 38/* '&' */) + break; + ++self._bytesVal; + } + self._lastPos = pos; + } + + return pos; +} + +/* eslint-disable no-multi-spaces */ +const HEX_VALUES = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, +]; +/* eslint-enable no-multi-spaces */ + +module.exports = URLEncoded; diff --git a/src/node_modules/busboy/lib/utils.js b/src/node_modules/busboy/lib/utils.js new file mode 100644 index 0000000..8274f6c --- /dev/null +++ b/src/node_modules/busboy/lib/utils.js @@ -0,0 +1,596 @@ +'use strict'; + +function parseContentType(str) { + if (str.length === 0) + return; + + const params = Object.create(null); + let i = 0; + + // Parse type + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + if (code !== 47/* '/' */ || i === 0) + return; + break; + } + } + // Check for type without subtype + if (i === str.length) + return; + + const type = str.slice(0, i).toLowerCase(); + + // Parse subtype + const subtypeStart = ++i; + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + // Make sure we have a subtype + if (i === subtypeStart) + return; + + if (parseContentTypeParams(str, i, params) === undefined) + return; + break; + } + } + // Make sure we have a subtype + if (i === subtypeStart) + return; + + const subtype = str.slice(subtypeStart, i).toLowerCase(); + + return { type, subtype, params }; +} + +function parseContentTypeParams(str, i, params) { + while (i < str.length) { + // Consume whitespace + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code !== 32/* ' ' */ && code !== 9/* '\t' */) + break; + } + + // Ended on whitespace + if (i === str.length) + break; + + // Check for malformed parameter + if (str.charCodeAt(i++) !== 59/* ';' */) + return; + + // Consume whitespace + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code !== 32/* ' ' */ && code !== 9/* '\t' */) + break; + } + + // Ended on whitespace (malformed) + if (i === str.length) + return; + + let name; + const nameStart = i; + // Parse parameter name + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + if (code !== 61/* '=' */) + return; + break; + } + } + + // No value (malformed) + if (i === str.length) + return; + + name = str.slice(nameStart, i); + ++i; // Skip over '=' + + // No value (malformed) + if (i === str.length) + return; + + let value = ''; + let valueStart; + if (str.charCodeAt(i) === 34/* '"' */) { + valueStart = ++i; + let escaping = false; + // Parse quoted value + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code === 92/* '\\' */) { + if (escaping) { + valueStart = i; + escaping = false; + } else { + value += str.slice(valueStart, i); + escaping = true; + } + continue; + } + if (code === 34/* '"' */) { + if (escaping) { + valueStart = i; + escaping = false; + continue; + } + value += str.slice(valueStart, i); + break; + } + if (escaping) { + valueStart = i - 1; + escaping = false; + } + // Invalid unescaped quoted character (malformed) + if (QDTEXT[code] !== 1) + return; + } + + // No end quote (malformed) + if (i === str.length) + return; + + ++i; // Skip over double quote + } else { + valueStart = i; + // Parse unquoted value + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + // No value (malformed) + if (i === valueStart) + return; + break; + } + } + value = str.slice(valueStart, i); + } + + name = name.toLowerCase(); + if (params[name] === undefined) + params[name] = value; + } + + return params; +} + +function parseDisposition(str, defDecoder) { + if (str.length === 0) + return; + + const params = Object.create(null); + let i = 0; + + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + if (parseDispositionParams(str, i, params, defDecoder) === undefined) + return; + break; + } + } + + const type = str.slice(0, i).toLowerCase(); + + return { type, params }; +} + +function parseDispositionParams(str, i, params, defDecoder) { + while (i < str.length) { + // Consume whitespace + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code !== 32/* ' ' */ && code !== 9/* '\t' */) + break; + } + + // Ended on whitespace + if (i === str.length) + break; + + // Check for malformed parameter + if (str.charCodeAt(i++) !== 59/* ';' */) + return; + + // Consume whitespace + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code !== 32/* ' ' */ && code !== 9/* '\t' */) + break; + } + + // Ended on whitespace (malformed) + if (i === str.length) + return; + + let name; + const nameStart = i; + // Parse parameter name + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + if (code === 61/* '=' */) + break; + return; + } + } + + // No value (malformed) + if (i === str.length) + return; + + let value = ''; + let valueStart; + let charset; + //~ let lang; + name = str.slice(nameStart, i); + if (name.charCodeAt(name.length - 1) === 42/* '*' */) { + // Extended value + + const charsetStart = ++i; + // Parse charset name + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (CHARSET[code] !== 1) { + if (code !== 39/* '\'' */) + return; + break; + } + } + + // Incomplete charset (malformed) + if (i === str.length) + return; + + charset = str.slice(charsetStart, i); + ++i; // Skip over the '\'' + + //~ const langStart = ++i; + // Parse language name + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code === 39/* '\'' */) + break; + } + + // Incomplete language (malformed) + if (i === str.length) + return; + + //~ lang = str.slice(langStart, i); + ++i; // Skip over the '\'' + + // No value (malformed) + if (i === str.length) + return; + + valueStart = i; + + let encode = 0; + // Parse value + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (EXTENDED_VALUE[code] !== 1) { + if (code === 37/* '%' */) { + let hexUpper; + let hexLower; + if (i + 2 < str.length + && (hexUpper = HEX_VALUES[str.charCodeAt(i + 1)]) !== -1 + && (hexLower = HEX_VALUES[str.charCodeAt(i + 2)]) !== -1) { + const byteVal = (hexUpper << 4) + hexLower; + value += str.slice(valueStart, i); + value += String.fromCharCode(byteVal); + i += 2; + valueStart = i + 1; + if (byteVal >= 128) + encode = 2; + else if (encode === 0) + encode = 1; + continue; + } + // '%' disallowed in non-percent encoded contexts (malformed) + return; + } + break; + } + } + + value += str.slice(valueStart, i); + value = convertToUTF8(value, charset, encode); + if (value === undefined) + return; + } else { + // Non-extended value + + ++i; // Skip over '=' + + // No value (malformed) + if (i === str.length) + return; + + if (str.charCodeAt(i) === 34/* '"' */) { + valueStart = ++i; + let escaping = false; + // Parse quoted value + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (code === 92/* '\\' */) { + if (escaping) { + valueStart = i; + escaping = false; + } else { + value += str.slice(valueStart, i); + escaping = true; + } + continue; + } + if (code === 34/* '"' */) { + if (escaping) { + valueStart = i; + escaping = false; + continue; + } + value += str.slice(valueStart, i); + break; + } + if (escaping) { + valueStart = i - 1; + escaping = false; + } + // Invalid unescaped quoted character (malformed) + if (QDTEXT[code] !== 1) + return; + } + + // No end quote (malformed) + if (i === str.length) + return; + + ++i; // Skip over double quote + } else { + valueStart = i; + // Parse unquoted value + for (; i < str.length; ++i) { + const code = str.charCodeAt(i); + if (TOKEN[code] !== 1) { + // No value (malformed) + if (i === valueStart) + return; + break; + } + } + value = str.slice(valueStart, i); + } + + value = defDecoder(value, 2); + if (value === undefined) + return; + } + + name = name.toLowerCase(); + if (params[name] === undefined) + params[name] = value; + } + + return params; +} + +function getDecoder(charset) { + let lc; + while (true) { + switch (charset) { + case 'utf-8': + case 'utf8': + return decoders.utf8; + case 'latin1': + case 'ascii': // TODO: Make these a separate, strict decoder? + case 'us-ascii': + case 'iso-8859-1': + case 'iso8859-1': + case 'iso88591': + case 'iso_8859-1': + case 'windows-1252': + case 'iso_8859-1:1987': + case 'cp1252': + case 'x-cp1252': + return decoders.latin1; + case 'utf16le': + case 'utf-16le': + case 'ucs2': + case 'ucs-2': + return decoders.utf16le; + case 'base64': + return decoders.base64; + default: + if (lc === undefined) { + lc = true; + charset = charset.toLowerCase(); + continue; + } + return decoders.other.bind(charset); + } + } +} + +const decoders = { + utf8: (data, hint) => { + if (data.length === 0) + return ''; + if (typeof data === 'string') { + // If `data` never had any percent-encoded bytes or never had any that + // were outside of the ASCII range, then we can safely just return the + // input since UTF-8 is ASCII compatible + if (hint < 2) + return data; + + data = Buffer.from(data, 'latin1'); + } + return data.utf8Slice(0, data.length); + }, + + latin1: (data, hint) => { + if (data.length === 0) + return ''; + if (typeof data === 'string') + return data; + return data.latin1Slice(0, data.length); + }, + + utf16le: (data, hint) => { + if (data.length === 0) + return ''; + if (typeof data === 'string') + data = Buffer.from(data, 'latin1'); + return data.ucs2Slice(0, data.length); + }, + + base64: (data, hint) => { + if (data.length === 0) + return ''; + if (typeof data === 'string') + data = Buffer.from(data, 'latin1'); + return data.base64Slice(0, data.length); + }, + + other: (data, hint) => { + if (data.length === 0) + return ''; + if (typeof data === 'string') + data = Buffer.from(data, 'latin1'); + try { + const decoder = new TextDecoder(this); + return decoder.decode(data); + } catch {} + }, +}; + +function convertToUTF8(data, charset, hint) { + const decode = getDecoder(charset); + if (decode) + return decode(data, hint); +} + +function basename(path) { + if (typeof path !== 'string') + return ''; + for (let i = path.length - 1; i >= 0; --i) { + switch (path.charCodeAt(i)) { + case 0x2F: // '/' + case 0x5C: // '\' + path = path.slice(i + 1); + return (path === '..' || path === '.' ? '' : path); + } + } + return (path === '..' || path === '.' ? '' : path); +} + +const TOKEN = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]; + +const QDTEXT = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +]; + +const CHARSET = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]; + +const EXTENDED_VALUE = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]; + +/* eslint-disable no-multi-spaces */ +const HEX_VALUES = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, +]; +/* eslint-enable no-multi-spaces */ + +module.exports = { + basename, + convertToUTF8, + getDecoder, + parseContentType, + parseDisposition, +}; diff --git a/src/node_modules/busboy/package.json b/src/node_modules/busboy/package.json new file mode 100644 index 0000000..ac2577f --- /dev/null +++ b/src/node_modules/busboy/package.json @@ -0,0 +1,22 @@ +{ "name": "busboy", + "version": "1.6.0", + "author": "Brian White ", + "description": "A streaming parser for HTML form data for node.js", + "main": "./lib/index.js", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "devDependencies": { + "@mscdex/eslint-config": "^1.1.0", + "eslint": "^7.32.0" + }, + "scripts": { + "test": "node test/test.js", + "lint": "eslint --cache --report-unused-disable-directives --ext=.js .eslintrc.js lib test bench", + "lint:fix": "npm run lint -- --fix" + }, + "engines": { "node": ">=10.16.0" }, + "keywords": [ "uploads", "forms", "multipart", "form-data" ], + "licenses": [ { "type": "MIT", "url": "http://github.com/mscdex/busboy/raw/master/LICENSE" } ], + "repository": { "type": "git", "url": "http://github.com/mscdex/busboy.git" } +} diff --git a/src/node_modules/busboy/test/common.js b/src/node_modules/busboy/test/common.js new file mode 100644 index 0000000..fb82ad8 --- /dev/null +++ b/src/node_modules/busboy/test/common.js @@ -0,0 +1,109 @@ +'use strict'; + +const assert = require('assert'); +const { inspect } = require('util'); + +const mustCallChecks = []; + +function noop() {} + +function runCallChecks(exitCode) { + if (exitCode !== 0) return; + + const failed = mustCallChecks.filter((context) => { + if ('minimum' in context) { + context.messageSegment = `at least ${context.minimum}`; + return context.actual < context.minimum; + } + context.messageSegment = `exactly ${context.exact}`; + return context.actual !== context.exact; + }); + + failed.forEach((context) => { + console.error('Mismatched %s function calls. Expected %s, actual %d.', + context.name, + context.messageSegment, + context.actual); + console.error(context.stack.split('\n').slice(2).join('\n')); + }); + + if (failed.length) + process.exit(1); +} + +function mustCall(fn, exact) { + return _mustCallInner(fn, exact, 'exact'); +} + +function mustCallAtLeast(fn, minimum) { + return _mustCallInner(fn, minimum, 'minimum'); +} + +function _mustCallInner(fn, criteria = 1, field) { + if (process._exiting) + throw new Error('Cannot use common.mustCall*() in process exit handler'); + + if (typeof fn === 'number') { + criteria = fn; + fn = noop; + } else if (fn === undefined) { + fn = noop; + } + + if (typeof criteria !== 'number') + throw new TypeError(`Invalid ${field} value: ${criteria}`); + + const context = { + [field]: criteria, + actual: 0, + stack: inspect(new Error()), + name: fn.name || '' + }; + + // Add the exit listener only once to avoid listener leak warnings + if (mustCallChecks.length === 0) + process.on('exit', runCallChecks); + + mustCallChecks.push(context); + + function wrapped(...args) { + ++context.actual; + return fn.call(this, ...args); + } + // TODO: remove origFn? + wrapped.origFn = fn; + + return wrapped; +} + +function getCallSite(top) { + const originalStackFormatter = Error.prepareStackTrace; + Error.prepareStackTrace = (err, stack) => + `${stack[0].getFileName()}:${stack[0].getLineNumber()}`; + const err = new Error(); + Error.captureStackTrace(err, top); + // With the V8 Error API, the stack is not formatted until it is accessed + // eslint-disable-next-line no-unused-expressions + err.stack; + Error.prepareStackTrace = originalStackFormatter; + return err.stack; +} + +function mustNotCall(msg) { + const callSite = getCallSite(mustNotCall); + return function mustNotCall(...args) { + args = args.map(inspect).join(', '); + const argsInfo = (args.length > 0 + ? `\ncalled with arguments: ${args}` + : ''); + assert.fail( + `${msg || 'function should not have been called'} at ${callSite}` + + argsInfo); + }; +} + +module.exports = { + mustCall, + mustCallAtLeast, + mustNotCall, +}; diff --git a/src/node_modules/busboy/test/test-types-multipart-charsets.js b/src/node_modules/busboy/test/test-types-multipart-charsets.js new file mode 100644 index 0000000..ed9c38a --- /dev/null +++ b/src/node_modules/busboy/test/test-types-multipart-charsets.js @@ -0,0 +1,94 @@ +'use strict'; + +const assert = require('assert'); +const { inspect } = require('util'); + +const { mustCall } = require(`${__dirname}/common.js`); + +const busboy = require('..'); + +const input = Buffer.from([ + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="テスト.dat"', + 'Content-Type: application/octet-stream', + '', + 'A'.repeat(1023), + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' +].join('\r\n')); +const boundary = '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k'; +const expected = [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('A'.repeat(1023)), + info: { + filename: 'テスト.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + }, +]; +const bb = busboy({ + defParamCharset: 'utf8', + headers: { + 'content-type': `multipart/form-data; boundary=${boundary}`, + } +}); +const results = []; + +bb.on('field', (name, val, info) => { + results.push({ type: 'field', name, val, info }); +}); + +bb.on('file', (name, stream, info) => { + const data = []; + let nb = 0; + const file = { + type: 'file', + name, + data: null, + info, + limited: false, + }; + results.push(file); + stream.on('data', (d) => { + data.push(d); + nb += d.length; + }).on('limit', () => { + file.limited = true; + }).on('close', () => { + file.data = Buffer.concat(data, nb); + assert.strictEqual(stream.truncated, file.limited); + }).once('error', (err) => { + file.err = err.message; + }); +}); + +bb.on('error', (err) => { + results.push({ error: err.message }); +}); + +bb.on('partsLimit', () => { + results.push('partsLimit'); +}); + +bb.on('filesLimit', () => { + results.push('filesLimit'); +}); + +bb.on('fieldsLimit', () => { + results.push('fieldsLimit'); +}); + +bb.on('close', mustCall(() => { + assert.deepStrictEqual( + results, + expected, + 'Results mismatch.\n' + + `Parsed: ${inspect(results)}\n` + + `Expected: ${inspect(expected)}` + ); +})); + +bb.end(input); diff --git a/src/node_modules/busboy/test/test-types-multipart-stream-pause.js b/src/node_modules/busboy/test/test-types-multipart-stream-pause.js new file mode 100644 index 0000000..df7268a --- /dev/null +++ b/src/node_modules/busboy/test/test-types-multipart-stream-pause.js @@ -0,0 +1,102 @@ +'use strict'; + +const assert = require('assert'); +const { randomFillSync } = require('crypto'); +const { inspect } = require('util'); + +const busboy = require('..'); + +const { mustCall } = require('./common.js'); + +const BOUNDARY = 'u2KxIV5yF1y+xUspOQCCZopaVgeV6Jxihv35XQJmuTx8X3sh'; + +function formDataSection(key, value) { + return Buffer.from( + `\r\n--${BOUNDARY}` + + `\r\nContent-Disposition: form-data; name="${key}"` + + `\r\n\r\n${value}` + ); +} + +function formDataFile(key, filename, contentType) { + const buf = Buffer.allocUnsafe(100000); + return Buffer.concat([ + Buffer.from(`\r\n--${BOUNDARY}\r\n`), + Buffer.from(`Content-Disposition: form-data; name="${key}"` + + `; filename="${filename}"\r\n`), + Buffer.from(`Content-Type: ${contentType}\r\n\r\n`), + randomFillSync(buf) + ]); +} + +const reqChunks = [ + Buffer.concat([ + formDataFile('file', 'file.bin', 'application/octet-stream'), + formDataSection('foo', 'foo value'), + ]), + formDataSection('bar', 'bar value'), + Buffer.from(`\r\n--${BOUNDARY}--\r\n`) +]; +const bb = busboy({ + headers: { + 'content-type': `multipart/form-data; boundary=${BOUNDARY}` + } +}); +const expected = [ + { type: 'file', + name: 'file', + info: { + filename: 'file.bin', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + }, + { type: 'field', + name: 'foo', + val: 'foo value', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + { type: 'field', + name: 'bar', + val: 'bar value', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, +]; +const results = []; + +bb.on('field', (name, val, info) => { + results.push({ type: 'field', name, val, info }); +}); + +bb.on('file', (name, stream, info) => { + results.push({ type: 'file', name, info }); + // Simulate a pipe where the destination is pausing (perhaps due to waiting + // for file system write to finish) + setTimeout(() => { + stream.resume(); + }, 10); +}); + +bb.on('close', mustCall(() => { + assert.deepStrictEqual( + results, + expected, + 'Results mismatch.\n' + + `Parsed: ${inspect(results)}\n` + + `Expected: ${inspect(expected)}` + ); +})); + +for (const chunk of reqChunks) + bb.write(chunk); +bb.end(); diff --git a/src/node_modules/busboy/test/test-types-multipart.js b/src/node_modules/busboy/test/test-types-multipart.js new file mode 100644 index 0000000..9755642 --- /dev/null +++ b/src/node_modules/busboy/test/test-types-multipart.js @@ -0,0 +1,1053 @@ +'use strict'; + +const assert = require('assert'); +const { inspect } = require('util'); + +const busboy = require('..'); + +const active = new Map(); + +const tests = [ + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'super alpha file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_1"', + '', + 'super beta file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'A'.repeat(1023), + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_1"; filename="1k_b.dat"', + 'Content-Type: application/octet-stream', + '', + 'B'.repeat(1023), + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + { type: 'field', + name: 'file_name_0', + val: 'super alpha file', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + { type: 'field', + name: 'file_name_1', + val: 'super beta file', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('A'.repeat(1023)), + info: { + filename: '1k_a.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + }, + { type: 'file', + name: 'upload_file_1', + data: Buffer.from('B'.repeat(1023)), + info: { + filename: '1k_b.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + }, + ], + what: 'Fields and files' + }, + { source: [ + ['------WebKitFormBoundaryTB2MiQ36fnSJlrhY', + 'Content-Disposition: form-data; name="cont"', + '', + 'some random content', + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY', + 'Content-Disposition: form-data; name="pass"', + '', + 'some random pass', + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY', + 'Content-Disposition: form-data; name=bit', + '', + '2', + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--' + ].join('\r\n') + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY', + expected: [ + { type: 'field', + name: 'cont', + val: 'some random content', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + { type: 'field', + name: 'pass', + val: 'some random pass', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + { type: 'field', + name: 'bit', + val: '2', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + ], + what: 'Fields only' + }, + { source: [ + '' + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY', + expected: [ + { error: 'Unexpected end of form' }, + ], + what: 'No fields and no files' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'super alpha file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + fileSize: 13, + fieldSize: 5 + }, + expected: [ + { type: 'field', + name: 'file_name_0', + val: 'super', + info: { + nameTruncated: false, + valueTruncated: true, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('ABCDEFGHIJKLM'), + info: { + filename: '1k_a.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: true, + }, + ], + what: 'Fields and files (limits)' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'super alpha file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + files: 0 + }, + expected: [ + { type: 'field', + name: 'file_name_0', + val: 'super alpha file', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + 'filesLimit', + ], + what: 'Fields and files (limits: 0 files)' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'super alpha file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_1"', + '', + 'super beta file', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'A'.repeat(1023), + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_1"; filename="1k_b.dat"', + 'Content-Type: application/octet-stream', + '', + 'B'.repeat(1023), + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + { type: 'field', + name: 'file_name_0', + val: 'super alpha file', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + { type: 'field', + name: 'file_name_1', + val: 'super beta file', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + ], + events: ['field'], + what: 'Fields and (ignored) files' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="/tmp/1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_1"; filename="C:\\files\\1k_b.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_2"; filename="relative/1k_c.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), + info: { + filename: '1k_a.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + }, + { type: 'file', + name: 'upload_file_1', + data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), + info: { + filename: '1k_b.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + }, + { type: 'file', + name: 'upload_file_2', + data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), + info: { + filename: '1k_c.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + }, + ], + what: 'Files with filenames containing paths' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="/absolute/1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_1"; filename="C:\\absolute\\1k_b.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_2"; filename="relative/1k_c.dat"', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + preservePath: true, + expected: [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), + info: { + filename: '/absolute/1k_a.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + }, + { type: 'file', + name: 'upload_file_1', + data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), + info: { + filename: 'C:\\absolute\\1k_b.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + }, + { type: 'file', + name: 'upload_file_2', + data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), + info: { + filename: 'relative/1k_c.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + }, + ], + what: 'Paths to be preserved through the preservePath option' + }, + { source: [ + ['------WebKitFormBoundaryTB2MiQ36fnSJlrhY', + 'Content-Disposition: form-data; name="cont"', + 'Content-Type: ', + '', + 'some random content', + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY', + 'Content-Disposition: ', + '', + 'some random pass', + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--' + ].join('\r\n') + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY', + expected: [ + { type: 'field', + name: 'cont', + val: 'some random content', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + ], + what: 'Empty content-type and empty content-disposition' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="file"; filename*=utf-8\'\'n%C3%A4me.txt', + 'Content-Type: application/octet-stream', + '', + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--' + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + { type: 'file', + name: 'file', + data: Buffer.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), + info: { + filename: 'näme.txt', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + }, + ], + what: 'Unicode filenames' + }, + { source: [ + ['--asdasdasdasd\r\n', + 'Content-Type: text/plain\r\n', + 'Content-Disposition: form-data; name="foo"\r\n', + '\r\n', + 'asd\r\n', + '--asdasdasdasd--' + ].join(':)') + ], + boundary: 'asdasdasdasd', + expected: [ + { error: 'Malformed part header' }, + { error: 'Unexpected end of form' }, + ], + what: 'Stopped mid-header' + }, + { source: [ + ['------WebKitFormBoundaryTB2MiQ36fnSJlrhY', + 'Content-Disposition: form-data; name="cont"', + 'Content-Type: application/json', + '', + '{}', + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--', + ].join('\r\n') + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY', + expected: [ + { type: 'field', + name: 'cont', + val: '{}', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'application/json', + }, + }, + ], + what: 'content-type for fields' + }, + { source: [ + '------WebKitFormBoundaryTB2MiQ36fnSJlrhY--', + ], + boundary: '----WebKitFormBoundaryTB2MiQ36fnSJlrhY', + expected: [], + what: 'empty form' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name=upload_file_0; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + 'Content-Transfer-Encoding: binary', + '', + '', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.alloc(0), + info: { + filename: '1k_a.dat', + encoding: 'binary', + mimeType: 'application/octet-stream', + }, + limited: false, + err: 'Unexpected end of form', + }, + { error: 'Unexpected end of form' }, + ], + what: 'Stopped mid-file #1' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name=upload_file_0; filename="1k_a.dat"', + 'Content-Type: application/octet-stream', + '', + 'a', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('a'), + info: { + filename: '1k_a.dat', + encoding: '7bit', + mimeType: 'application/octet-stream', + }, + limited: false, + err: 'Unexpected end of form', + }, + { error: 'Unexpected end of form' }, + ], + what: 'Stopped mid-file #2' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="notes.txt"', + 'Content-Type: text/plain; charset=utf8', + '', + 'a', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('a'), + info: { + filename: 'notes.txt', + encoding: '7bit', + mimeType: 'text/plain', + }, + limited: false, + }, + ], + what: 'Text file with charset' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="notes.txt"', + 'Content-Type: ', + ' text/plain; charset=utf8', + '', + 'a', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('a'), + info: { + filename: 'notes.txt', + encoding: '7bit', + mimeType: 'text/plain', + }, + limited: false, + }, + ], + what: 'Folded header value' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Type: text/plain; charset=utf8', + '', + 'a', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [], + what: 'No Content-Disposition' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'a'.repeat(64 * 1024), + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="notes.txt"', + 'Content-Type: ', + ' text/plain; charset=utf8', + '', + 'bc', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + fieldSize: Infinity, + }, + expected: [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('bc'), + info: { + filename: 'notes.txt', + encoding: '7bit', + mimeType: 'text/plain', + }, + limited: false, + }, + ], + events: [ 'file' ], + what: 'Skip field parts if no listener' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'a', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="notes.txt"', + 'Content-Type: ', + ' text/plain; charset=utf8', + '', + 'bc', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + parts: 1, + }, + expected: [ + { type: 'field', + name: 'file_name_0', + val: 'a', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + 'partsLimit', + ], + what: 'Parts limit' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_0"', + '', + 'a', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; name="file_name_1"', + '', + 'b', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + fields: 1, + }, + expected: [ + { type: 'field', + name: 'file_name_0', + val: 'a', + info: { + nameTruncated: false, + valueTruncated: false, + encoding: '7bit', + mimeType: 'text/plain', + }, + }, + 'fieldsLimit', + ], + what: 'Fields limit' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="notes.txt"', + 'Content-Type: text/plain; charset=utf8', + '', + 'ab', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_1"; filename="notes2.txt"', + 'Content-Type: text/plain; charset=utf8', + '', + 'cd', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + limits: { + files: 1, + }, + expected: [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('ab'), + info: { + filename: 'notes.txt', + encoding: '7bit', + mimeType: 'text/plain', + }, + limited: false, + }, + 'filesLimit', + ], + what: 'Files limit' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + `name="upload_file_0"; filename="${'a'.repeat(64 * 1024)}.txt"`, + 'Content-Type: text/plain; charset=utf8', + '', + 'ab', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_1"; filename="notes2.txt"', + 'Content-Type: text/plain; charset=utf8', + '', + 'cd', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + { error: 'Malformed part header' }, + { type: 'file', + name: 'upload_file_1', + data: Buffer.from('cd'), + info: { + filename: 'notes2.txt', + encoding: '7bit', + mimeType: 'text/plain', + }, + limited: false, + }, + ], + what: 'Oversized part header' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + 'name="upload_file_0"; filename="notes.txt"', + 'Content-Type: text/plain; charset=utf8', + '', + 'a'.repeat(31) + '\r', + ].join('\r\n'), + 'b'.repeat(40), + '\r\n-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--', + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + fileHwm: 32, + expected: [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('a'.repeat(31) + '\r' + 'b'.repeat(40)), + info: { + filename: 'notes.txt', + encoding: '7bit', + mimeType: 'text/plain', + }, + limited: false, + }, + ], + what: 'Lookbehind data should not stall file streams' + }, + { source: [ + ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + `name="upload_file_0"; filename="${'a'.repeat(8 * 1024)}.txt"`, + 'Content-Type: text/plain; charset=utf8', + '', + 'ab', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + `name="upload_file_1"; filename="${'b'.repeat(8 * 1024)}.txt"`, + 'Content-Type: text/plain; charset=utf8', + '', + 'cd', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + 'Content-Disposition: form-data; ' + + `name="upload_file_2"; filename="${'c'.repeat(8 * 1024)}.txt"`, + 'Content-Type: text/plain; charset=utf8', + '', + 'ef', + '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--', + ].join('\r\n') + ], + boundary: '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k', + expected: [ + { type: 'file', + name: 'upload_file_0', + data: Buffer.from('ab'), + info: { + filename: `${'a'.repeat(8 * 1024)}.txt`, + encoding: '7bit', + mimeType: 'text/plain', + }, + limited: false, + }, + { type: 'file', + name: 'upload_file_1', + data: Buffer.from('cd'), + info: { + filename: `${'b'.repeat(8 * 1024)}.txt`, + encoding: '7bit', + mimeType: 'text/plain', + }, + limited: false, + }, + { type: 'file', + name: 'upload_file_2', + data: Buffer.from('ef'), + info: { + filename: `${'c'.repeat(8 * 1024)}.txt`, + encoding: '7bit', + mimeType: 'text/plain', + }, + limited: false, + }, + ], + what: 'Header size limit should be per part' + }, + { source: [ + '\r\n--d1bf46b3-aa33-4061-b28d-6c5ced8b08ee\r\n', + 'Content-Type: application/gzip\r\n' + + 'Content-Encoding: gzip\r\n' + + 'Content-Disposition: form-data; name=batch-1; filename=batch-1' + + '\r\n\r\n', + '\r\n--d1bf46b3-aa33-4061-b28d-6c5ced8b08ee--', + ], + boundary: 'd1bf46b3-aa33-4061-b28d-6c5ced8b08ee', + expected: [ + { type: 'file', + name: 'batch-1', + data: Buffer.alloc(0), + info: { + filename: 'batch-1', + encoding: '7bit', + mimeType: 'application/gzip', + }, + limited: false, + }, + ], + what: 'Empty part' + }, +]; + +for (const test of tests) { + active.set(test, 1); + + const { what, boundary, events, limits, preservePath, fileHwm } = test; + const bb = busboy({ + fileHwm, + limits, + preservePath, + headers: { + 'content-type': `multipart/form-data; boundary=${boundary}`, + } + }); + const results = []; + + if (events === undefined || events.includes('field')) { + bb.on('field', (name, val, info) => { + results.push({ type: 'field', name, val, info }); + }); + } + + if (events === undefined || events.includes('file')) { + bb.on('file', (name, stream, info) => { + const data = []; + let nb = 0; + const file = { + type: 'file', + name, + data: null, + info, + limited: false, + }; + results.push(file); + stream.on('data', (d) => { + data.push(d); + nb += d.length; + }).on('limit', () => { + file.limited = true; + }).on('close', () => { + file.data = Buffer.concat(data, nb); + assert.strictEqual(stream.truncated, file.limited); + }).once('error', (err) => { + file.err = err.message; + }); + }); + } + + bb.on('error', (err) => { + results.push({ error: err.message }); + }); + + bb.on('partsLimit', () => { + results.push('partsLimit'); + }); + + bb.on('filesLimit', () => { + results.push('filesLimit'); + }); + + bb.on('fieldsLimit', () => { + results.push('fieldsLimit'); + }); + + bb.on('close', () => { + active.delete(test); + + assert.deepStrictEqual( + results, + test.expected, + `[${what}] Results mismatch.\n` + + `Parsed: ${inspect(results)}\n` + + `Expected: ${inspect(test.expected)}` + ); + }); + + for (const src of test.source) { + const buf = (typeof src === 'string' ? Buffer.from(src, 'utf8') : src); + bb.write(buf); + } + bb.end(); +} + +// Byte-by-byte versions +for (let test of tests) { + test = { ...test }; + test.what += ' (byte-by-byte)'; + active.set(test, 1); + + const { what, boundary, events, limits, preservePath, fileHwm } = test; + const bb = busboy({ + fileHwm, + limits, + preservePath, + headers: { + 'content-type': `multipart/form-data; boundary=${boundary}`, + } + }); + const results = []; + + if (events === undefined || events.includes('field')) { + bb.on('field', (name, val, info) => { + results.push({ type: 'field', name, val, info }); + }); + } + + if (events === undefined || events.includes('file')) { + bb.on('file', (name, stream, info) => { + const data = []; + let nb = 0; + const file = { + type: 'file', + name, + data: null, + info, + limited: false, + }; + results.push(file); + stream.on('data', (d) => { + data.push(d); + nb += d.length; + }).on('limit', () => { + file.limited = true; + }).on('close', () => { + file.data = Buffer.concat(data, nb); + assert.strictEqual(stream.truncated, file.limited); + }).once('error', (err) => { + file.err = err.message; + }); + }); + } + + bb.on('error', (err) => { + results.push({ error: err.message }); + }); + + bb.on('partsLimit', () => { + results.push('partsLimit'); + }); + + bb.on('filesLimit', () => { + results.push('filesLimit'); + }); + + bb.on('fieldsLimit', () => { + results.push('fieldsLimit'); + }); + + bb.on('close', () => { + active.delete(test); + + assert.deepStrictEqual( + results, + test.expected, + `[${what}] Results mismatch.\n` + + `Parsed: ${inspect(results)}\n` + + `Expected: ${inspect(test.expected)}` + ); + }); + + for (const src of test.source) { + const buf = (typeof src === 'string' ? Buffer.from(src, 'utf8') : src); + for (let i = 0; i < buf.length; ++i) + bb.write(buf.slice(i, i + 1)); + } + bb.end(); +} + +{ + let exception = false; + process.once('uncaughtException', (ex) => { + exception = true; + throw ex; + }); + process.on('exit', () => { + if (exception || active.size === 0) + return; + process.exitCode = 1; + console.error('=========================='); + console.error(`${active.size} test(s) did not finish:`); + console.error('=========================='); + console.error(Array.from(active.keys()).map((v) => v.what).join('\n')); + }); +} diff --git a/src/node_modules/busboy/test/test-types-urlencoded.js b/src/node_modules/busboy/test/test-types-urlencoded.js new file mode 100644 index 0000000..c35962b --- /dev/null +++ b/src/node_modules/busboy/test/test-types-urlencoded.js @@ -0,0 +1,488 @@ +'use strict'; + +const assert = require('assert'); +const { transcode } = require('buffer'); +const { inspect } = require('util'); + +const busboy = require('..'); + +const active = new Map(); + +const tests = [ + { source: ['foo'], + expected: [ + ['foo', + '', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Unassigned value' + }, + { source: ['foo=bar'], + expected: [ + ['foo', + 'bar', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Assigned value' + }, + { source: ['foo&bar=baz'], + expected: [ + ['foo', + '', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['bar', + 'baz', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Unassigned and assigned value' + }, + { source: ['foo=bar&baz'], + expected: [ + ['foo', + 'bar', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['baz', + '', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Assigned and unassigned value' + }, + { source: ['foo=bar&baz=bla'], + expected: [ + ['foo', + 'bar', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['baz', + 'bla', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Two assigned values' + }, + { source: ['foo&bar'], + expected: [ + ['foo', + '', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['bar', + '', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Two unassigned values' + }, + { source: ['foo&bar&'], + expected: [ + ['foo', + '', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['bar', + '', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Two unassigned values and ampersand' + }, + { source: ['foo+1=bar+baz%2Bquux'], + expected: [ + ['foo 1', + 'bar baz+quux', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Assigned key and value with (plus) space' + }, + { source: ['foo=bar%20baz%21'], + expected: [ + ['foo', + 'bar baz!', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Assigned value with encoded bytes' + }, + { source: ['foo%20bar=baz%20bla%21'], + expected: [ + ['foo bar', + 'baz bla!', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Assigned value with encoded bytes #2' + }, + { source: ['foo=bar%20baz%21&num=1000'], + expected: [ + ['foo', + 'bar baz!', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['num', + '1000', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Two assigned values, one with encoded bytes' + }, + { source: [ + Array.from(transcode(Buffer.from('foo'), 'utf8', 'utf16le')).map( + (n) => `%${n.toString(16).padStart(2, '0')}` + ).join(''), + '=', + Array.from(transcode(Buffer.from('😀!'), 'utf8', 'utf16le')).map( + (n) => `%${n.toString(16).padStart(2, '0')}` + ).join(''), + ], + expected: [ + ['foo', + '😀!', + { nameTruncated: false, + valueTruncated: false, + encoding: 'UTF-16LE', + mimeType: 'text/plain' }, + ], + ], + charset: 'UTF-16LE', + what: 'Encoded value with multi-byte charset' + }, + { source: [ + 'foo=<', + Array.from(transcode(Buffer.from('©:^þ'), 'utf8', 'latin1')).map( + (n) => `%${n.toString(16).padStart(2, '0')}` + ).join(''), + ], + expected: [ + ['foo', + '<©:^þ', + { nameTruncated: false, + valueTruncated: false, + encoding: 'ISO-8859-1', + mimeType: 'text/plain' }, + ], + ], + charset: 'ISO-8859-1', + what: 'Encoded value with single-byte, ASCII-compatible, non-UTF8 charset' + }, + { source: ['foo=bar&baz=bla'], + expected: [], + what: 'Limits: zero fields', + limits: { fields: 0 } + }, + { source: ['foo=bar&baz=bla'], + expected: [ + ['foo', + 'bar', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Limits: one field', + limits: { fields: 1 } + }, + { source: ['foo=bar&baz=bla'], + expected: [ + ['foo', + 'bar', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['baz', + 'bla', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Limits: field part lengths match limits', + limits: { fieldNameSize: 3, fieldSize: 3 } + }, + { source: ['foo=bar&baz=bla'], + expected: [ + ['fo', + 'bar', + { nameTruncated: true, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['ba', + 'bla', + { nameTruncated: true, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Limits: truncated field name', + limits: { fieldNameSize: 2 } + }, + { source: ['foo=bar&baz=bla'], + expected: [ + ['foo', + 'ba', + { nameTruncated: false, + valueTruncated: true, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['baz', + 'bl', + { nameTruncated: false, + valueTruncated: true, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Limits: truncated field value', + limits: { fieldSize: 2 } + }, + { source: ['foo=bar&baz=bla'], + expected: [ + ['fo', + 'ba', + { nameTruncated: true, + valueTruncated: true, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['ba', + 'bl', + { nameTruncated: true, + valueTruncated: true, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Limits: truncated field name and value', + limits: { fieldNameSize: 2, fieldSize: 2 } + }, + { source: ['foo=bar&baz=bla'], + expected: [ + ['fo', + '', + { nameTruncated: true, + valueTruncated: true, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['ba', + '', + { nameTruncated: true, + valueTruncated: true, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Limits: truncated field name and zero value limit', + limits: { fieldNameSize: 2, fieldSize: 0 } + }, + { source: ['foo=bar&baz=bla'], + expected: [ + ['', + '', + { nameTruncated: true, + valueTruncated: true, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ['', + '', + { nameTruncated: true, + valueTruncated: true, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Limits: truncated zero field name and zero value limit', + limits: { fieldNameSize: 0, fieldSize: 0 } + }, + { source: ['&'], + expected: [], + what: 'Ampersand' + }, + { source: ['&&&&&'], + expected: [], + what: 'Many ampersands' + }, + { source: ['='], + expected: [ + ['', + '', + { nameTruncated: false, + valueTruncated: false, + encoding: 'utf-8', + mimeType: 'text/plain' }, + ], + ], + what: 'Assigned value, empty name and value' + }, + { source: [''], + expected: [], + what: 'Nothing' + }, +]; + +for (const test of tests) { + active.set(test, 1); + + const { what } = test; + const charset = test.charset || 'utf-8'; + const bb = busboy({ + limits: test.limits, + headers: { + 'content-type': `application/x-www-form-urlencoded; charset=${charset}`, + }, + }); + const results = []; + + bb.on('field', (key, val, info) => { + results.push([key, val, info]); + }); + + bb.on('file', () => { + throw new Error(`[${what}] Unexpected file`); + }); + + bb.on('close', () => { + active.delete(test); + + assert.deepStrictEqual( + results, + test.expected, + `[${what}] Results mismatch.\n` + + `Parsed: ${inspect(results)}\n` + + `Expected: ${inspect(test.expected)}` + ); + }); + + for (const src of test.source) { + const buf = (typeof src === 'string' ? Buffer.from(src, 'utf8') : src); + bb.write(buf); + } + bb.end(); +} + +// Byte-by-byte versions +for (let test of tests) { + test = { ...test }; + test.what += ' (byte-by-byte)'; + active.set(test, 1); + + const { what } = test; + const charset = test.charset || 'utf-8'; + const bb = busboy({ + limits: test.limits, + headers: { + 'content-type': `application/x-www-form-urlencoded; charset="${charset}"`, + }, + }); + const results = []; + + bb.on('field', (key, val, info) => { + results.push([key, val, info]); + }); + + bb.on('file', () => { + throw new Error(`[${what}] Unexpected file`); + }); + + bb.on('close', () => { + active.delete(test); + + assert.deepStrictEqual( + results, + test.expected, + `[${what}] Results mismatch.\n` + + `Parsed: ${inspect(results)}\n` + + `Expected: ${inspect(test.expected)}` + ); + }); + + for (const src of test.source) { + const buf = (typeof src === 'string' ? Buffer.from(src, 'utf8') : src); + for (let i = 0; i < buf.length; ++i) + bb.write(buf.slice(i, i + 1)); + } + bb.end(); +} + +{ + let exception = false; + process.once('uncaughtException', (ex) => { + exception = true; + throw ex; + }); + process.on('exit', () => { + if (exception || active.size === 0) + return; + process.exitCode = 1; + console.error('=========================='); + console.error(`${active.size} test(s) did not finish:`); + console.error('=========================='); + console.error(Array.from(active.keys()).map((v) => v.what).join('\n')); + }); +} diff --git a/src/node_modules/busboy/test/test.js b/src/node_modules/busboy/test/test.js new file mode 100644 index 0000000..d0380f2 --- /dev/null +++ b/src/node_modules/busboy/test/test.js @@ -0,0 +1,20 @@ +'use strict'; + +const { spawnSync } = require('child_process'); +const { readdirSync } = require('fs'); +const { join } = require('path'); + +const files = readdirSync(__dirname).sort(); +for (const filename of files) { + if (filename.startsWith('test-')) { + const path = join(__dirname, filename); + console.log(`> Running ${filename} ...`); + const result = spawnSync(`${process.argv0} ${path}`, { + shell: true, + stdio: 'inherit', + windowsHide: true + }); + if (result.status !== 0) + process.exitCode = 1; + } +} diff --git a/src/node_modules/bytes/History.md b/src/node_modules/bytes/History.md new file mode 100644 index 0000000..d60ce0e --- /dev/null +++ b/src/node_modules/bytes/History.md @@ -0,0 +1,97 @@ +3.1.2 / 2022-01-27 +================== + + * Fix return value for un-parsable strings + +3.1.1 / 2021-11-15 +================== + + * Fix "thousandsSeparator" incorrecting formatting fractional part + +3.1.0 / 2019-01-22 +================== + + * Add petabyte (`pb`) support + +3.0.0 / 2017-08-31 +================== + + * Change "kB" to "KB" in format output + * Remove support for Node.js 0.6 + * Remove support for ComponentJS + +2.5.0 / 2017-03-24 +================== + + * Add option "unit" + +2.4.0 / 2016-06-01 +================== + + * Add option "unitSeparator" + +2.3.0 / 2016-02-15 +================== + + * Drop partial bytes on all parsed units + * Fix non-finite numbers to `.format` to return `null` + * Fix parsing byte string that looks like hex + * perf: hoist regular expressions + +2.2.0 / 2015-11-13 +================== + + * add option "decimalPlaces" + * add option "fixedDecimals" + +2.1.0 / 2015-05-21 +================== + + * add `.format` export + * add `.parse` export + +2.0.2 / 2015-05-20 +================== + + * remove map recreation + * remove unnecessary object construction + +2.0.1 / 2015-05-07 +================== + + * fix browserify require + * remove node.extend dependency + +2.0.0 / 2015-04-12 +================== + + * add option "case" + * add option "thousandsSeparator" + * return "null" on invalid parse input + * support proper round-trip: bytes(bytes(num)) === num + * units no longer case sensitive when parsing + +1.0.0 / 2014-05-05 +================== + + * add negative support. fixes #6 + +0.3.0 / 2014-03-19 +================== + + * added terabyte support + +0.2.1 / 2013-04-01 +================== + + * add .component + +0.2.0 / 2012-10-28 +================== + + * bytes(200).should.eql('200b') + +0.1.0 / 2012-07-04 +================== + + * add bytes to string conversion [yields] diff --git a/src/node_modules/bytes/LICENSE b/src/node_modules/bytes/LICENSE new file mode 100644 index 0000000..63e95a9 --- /dev/null +++ b/src/node_modules/bytes/LICENSE @@ -0,0 +1,23 @@ +(The MIT License) + +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015 Jed Watson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/node_modules/bytes/Readme.md b/src/node_modules/bytes/Readme.md new file mode 100644 index 0000000..5790e23 --- /dev/null +++ b/src/node_modules/bytes/Readme.md @@ -0,0 +1,152 @@ +# Bytes utility + +[![NPM Version][npm-image]][npm-url] +[![NPM Downloads][downloads-image]][downloads-url] +[![Build Status][ci-image]][ci-url] +[![Test Coverage][coveralls-image]][coveralls-url] + +Utility to parse a string bytes (ex: `1TB`) to bytes (`1099511627776`) and vice-versa. + +## Installation + +This is a [Node.js](https://nodejs.org/en/) module available through the +[npm registry](https://www.npmjs.com/). Installation is done using the +[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): + +```bash +$ npm install bytes +``` + +## Usage + +```js +var bytes = require('bytes'); +``` + +#### bytes(number|string value, [options]): number|string|null + +Default export function. Delegates to either `bytes.format` or `bytes.parse` based on the type of `value`. + +**Arguments** + +| Name | Type | Description | +|---------|----------|--------------------| +| value | `number`|`string` | Number value to format or string value to parse | +| options | `Object` | Conversion options for `format` | + +**Returns** + +| Name | Type | Description | +|---------|------------------|-------------------------------------------------| +| results | `string`|`number`|`null` | Return null upon error. Numeric value in bytes, or string value otherwise. | + +**Example** + +```js +bytes(1024); +// output: '1KB' + +bytes('1KB'); +// output: 1024 +``` + +#### bytes.format(number value, [options]): string|null + +Format the given value in bytes into a string. If the value is negative, it is kept as such. If it is a float, it is + rounded. + +**Arguments** + +| Name | Type | Description | +|---------|----------|--------------------| +| value | `number` | Value in bytes | +| options | `Object` | Conversion options | + +**Options** + +| Property | Type | Description | +|-------------------|--------|-----------------------------------------------------------------------------------------| +| decimalPlaces | `number`|`null` | Maximum number of decimal places to include in output. Default value to `2`. | +| fixedDecimals | `boolean`|`null` | Whether to always display the maximum number of decimal places. Default value to `false` | +| thousandsSeparator | `string`|`null` | Example of values: `' '`, `','` and `'.'`... Default value to `''`. | +| unit | `string`|`null` | The unit in which the result will be returned (B/KB/MB/GB/TB). Default value to `''` (which means auto detect). | +| unitSeparator | `string`|`null` | Separator to use between number and unit. Default value to `''`. | + +**Returns** + +| Name | Type | Description | +|---------|------------------|-------------------------------------------------| +| results | `string`|`null` | Return null upon error. String value otherwise. | + +**Example** + +```js +bytes.format(1024); +// output: '1KB' + +bytes.format(1000); +// output: '1000B' + +bytes.format(1000, {thousandsSeparator: ' '}); +// output: '1 000B' + +bytes.format(1024 * 1.7, {decimalPlaces: 0}); +// output: '2KB' + +bytes.format(1024, {unitSeparator: ' '}); +// output: '1 KB' +``` + +#### bytes.parse(string|number value): number|null + +Parse the string value into an integer in bytes. If no unit is given, or `value` +is a number, it is assumed the value is in bytes. + +Supported units and abbreviations are as follows and are case-insensitive: + + * `b` for bytes + * `kb` for kilobytes + * `mb` for megabytes + * `gb` for gigabytes + * `tb` for terabytes + * `pb` for petabytes + +The units are in powers of two, not ten. This means 1kb = 1024b according to this parser. + +**Arguments** + +| Name | Type | Description | +|---------------|--------|--------------------| +| value | `string`|`number` | String to parse, or number in bytes. | + +**Returns** + +| Name | Type | Description | +|---------|-------------|-------------------------| +| results | `number`|`null` | Return null upon error. Value in bytes otherwise. | + +**Example** + +```js +bytes.parse('1KB'); +// output: 1024 + +bytes.parse('1024'); +// output: 1024 + +bytes.parse(1024); +// output: 1024 +``` + +## License + +[MIT](LICENSE) + +[ci-image]: https://badgen.net/github/checks/visionmedia/bytes.js/master?label=ci +[ci-url]: https://github.com/visionmedia/bytes.js/actions?query=workflow%3Aci +[coveralls-image]: https://badgen.net/coveralls/c/github/visionmedia/bytes.js/master +[coveralls-url]: https://coveralls.io/r/visionmedia/bytes.js?branch=master +[downloads-image]: https://badgen.net/npm/dm/bytes +[downloads-url]: https://npmjs.org/package/bytes +[npm-image]: https://badgen.net/npm/v/bytes +[npm-url]: https://npmjs.org/package/bytes diff --git a/src/node_modules/bytes/index.js b/src/node_modules/bytes/index.js new file mode 100644 index 0000000..6f2d0f8 --- /dev/null +++ b/src/node_modules/bytes/index.js @@ -0,0 +1,170 @@ +/*! + * bytes + * Copyright(c) 2012-2014 TJ Holowaychuk + * Copyright(c) 2015 Jed Watson + * MIT Licensed + */ + +'use strict'; + +/** + * Module exports. + * @public + */ + +module.exports = bytes; +module.exports.format = format; +module.exports.parse = parse; + +/** + * Module variables. + * @private + */ + +var formatThousandsRegExp = /\B(?=(\d{3})+(?!\d))/g; + +var formatDecimalsRegExp = /(?:\.0*|(\.[^0]+)0+)$/; + +var map = { + b: 1, + kb: 1 << 10, + mb: 1 << 20, + gb: 1 << 30, + tb: Math.pow(1024, 4), + pb: Math.pow(1024, 5), +}; + +var parseRegExp = /^((-|\+)?(\d+(?:\.\d+)?)) *(kb|mb|gb|tb|pb)$/i; + +/** + * Convert the given value in bytes into a string or parse to string to an integer in bytes. + * + * @param {string|number} value + * @param {{ + * case: [string], + * decimalPlaces: [number] + * fixedDecimals: [boolean] + * thousandsSeparator: [string] + * unitSeparator: [string] + * }} [options] bytes options. + * + * @returns {string|number|null} + */ + +function bytes(value, options) { + if (typeof value === 'string') { + return parse(value); + } + + if (typeof value === 'number') { + return format(value, options); + } + + return null; +} + +/** + * Format the given value in bytes into a string. + * + * If the value is negative, it is kept as such. If it is a float, + * it is rounded. + * + * @param {number} value + * @param {object} [options] + * @param {number} [options.decimalPlaces=2] + * @param {number} [options.fixedDecimals=false] + * @param {string} [options.thousandsSeparator=] + * @param {string} [options.unit=] + * @param {string} [options.unitSeparator=] + * + * @returns {string|null} + * @public + */ + +function format(value, options) { + if (!Number.isFinite(value)) { + return null; + } + + var mag = Math.abs(value); + var thousandsSeparator = (options && options.thousandsSeparator) || ''; + var unitSeparator = (options && options.unitSeparator) || ''; + var decimalPlaces = (options && options.decimalPlaces !== undefined) ? options.decimalPlaces : 2; + var fixedDecimals = Boolean(options && options.fixedDecimals); + var unit = (options && options.unit) || ''; + + if (!unit || !map[unit.toLowerCase()]) { + if (mag >= map.pb) { + unit = 'PB'; + } else if (mag >= map.tb) { + unit = 'TB'; + } else if (mag >= map.gb) { + unit = 'GB'; + } else if (mag >= map.mb) { + unit = 'MB'; + } else if (mag >= map.kb) { + unit = 'KB'; + } else { + unit = 'B'; + } + } + + var val = value / map[unit.toLowerCase()]; + var str = val.toFixed(decimalPlaces); + + if (!fixedDecimals) { + str = str.replace(formatDecimalsRegExp, '$1'); + } + + if (thousandsSeparator) { + str = str.split('.').map(function (s, i) { + return i === 0 + ? s.replace(formatThousandsRegExp, thousandsSeparator) + : s + }).join('.'); + } + + return str + unitSeparator + unit; +} + +/** + * Parse the string value into an integer in bytes. + * + * If no unit is given, it is assumed the value is in bytes. + * + * @param {number|string} val + * + * @returns {number|null} + * @public + */ + +function parse(val) { + if (typeof val === 'number' && !isNaN(val)) { + return val; + } + + if (typeof val !== 'string') { + return null; + } + + // Test if the string passed is valid + var results = parseRegExp.exec(val); + var floatValue; + var unit = 'b'; + + if (!results) { + // Nothing could be extracted from the given string + floatValue = parseInt(val, 10); + unit = 'b' + } else { + // Retrieve the value and the unit + floatValue = parseFloat(results[1]); + unit = results[4].toLowerCase(); + } + + if (isNaN(floatValue)) { + return null; + } + + return Math.floor(map[unit] * floatValue); +} diff --git a/src/node_modules/bytes/package.json b/src/node_modules/bytes/package.json new file mode 100644 index 0000000..f2b6a8b --- /dev/null +++ b/src/node_modules/bytes/package.json @@ -0,0 +1,42 @@ +{ + "name": "bytes", + "description": "Utility to parse a string bytes to bytes and vice-versa", + "version": "3.1.2", + "author": "TJ Holowaychuk (http://tjholowaychuk.com)", + "contributors": [ + "Jed Watson ", + "Théo FIDRY " + ], + "license": "MIT", + "keywords": [ + "byte", + "bytes", + "utility", + "parse", + "parser", + "convert", + "converter" + ], + "repository": "visionmedia/bytes.js", + "devDependencies": { + "eslint": "7.32.0", + "eslint-plugin-markdown": "2.2.1", + "mocha": "9.2.0", + "nyc": "15.1.0" + }, + "files": [ + "History.md", + "LICENSE", + "Readme.md", + "index.js" + ], + "engines": { + "node": ">= 0.8" + }, + "scripts": { + "lint": "eslint .", + "test": "mocha --check-leaks --reporter spec", + "test-ci": "nyc --reporter=lcov --reporter=text npm test", + "test-cov": "nyc --reporter=html --reporter=text npm test" + } +} diff --git a/src/node_modules/call-bind-apply-helpers/.eslintrc b/src/node_modules/call-bind-apply-helpers/.eslintrc new file mode 100644 index 0000000..201e859 --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/.eslintrc @@ -0,0 +1,17 @@ +{ + "root": true, + + "extends": "@ljharb", + + "rules": { + "func-name-matching": 0, + "id-length": 0, + "new-cap": [2, { + "capIsNewExceptions": [ + "GetIntrinsic", + ], + }], + "no-extra-parens": 0, + "no-magic-numbers": 0, + }, +} diff --git a/src/node_modules/call-bind-apply-helpers/.github/FUNDING.yml b/src/node_modules/call-bind-apply-helpers/.github/FUNDING.yml new file mode 100644 index 0000000..0011e9d --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [ljharb] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: npm/call-bind-apply-helpers +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/src/node_modules/call-bind-apply-helpers/.nycrc b/src/node_modules/call-bind-apply-helpers/.nycrc new file mode 100644 index 0000000..bdd626c --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/.nycrc @@ -0,0 +1,9 @@ +{ + "all": true, + "check-coverage": false, + "reporter": ["text-summary", "text", "html", "json"], + "exclude": [ + "coverage", + "test" + ] +} diff --git a/src/node_modules/call-bind-apply-helpers/CHANGELOG.md b/src/node_modules/call-bind-apply-helpers/CHANGELOG.md new file mode 100644 index 0000000..2484942 --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [v1.0.2](https://github.com/ljharb/call-bind-apply-helpers/compare/v1.0.1...v1.0.2) - 2025-02-12 + +### Commits + +- [types] improve inferred types [`e6f9586`](https://github.com/ljharb/call-bind-apply-helpers/commit/e6f95860a3c72879cb861a858cdfb8138fbedec1) +- [Dev Deps] update `@arethetypeswrong/cli`, `@ljharb/tsconfig`, `@types/tape`, `es-value-fixtures`, `for-each`, `has-strict-mode`, `object-inspect` [`e43d540`](https://github.com/ljharb/call-bind-apply-helpers/commit/e43d5409f97543bfbb11f345d47d8ce4e066d8c1) + +## [v1.0.1](https://github.com/ljharb/call-bind-apply-helpers/compare/v1.0.0...v1.0.1) - 2024-12-08 + +### Commits + +- [types] `reflectApply`: fix types [`4efc396`](https://github.com/ljharb/call-bind-apply-helpers/commit/4efc3965351a4f02cc55e836fa391d3d11ef2ef8) +- [Fix] `reflectApply`: oops, Reflect is not a function [`83cc739`](https://github.com/ljharb/call-bind-apply-helpers/commit/83cc7395de6b79b7730bdf092f1436f0b1263c75) +- [Dev Deps] update `@arethetypeswrong/cli` [`80bd5d3`](https://github.com/ljharb/call-bind-apply-helpers/commit/80bd5d3ae58b4f6b6995ce439dd5a1bcb178a940) + +## v1.0.0 - 2024-12-05 + +### Commits + +- Initial implementation, tests, readme [`7879629`](https://github.com/ljharb/call-bind-apply-helpers/commit/78796290f9b7430c9934d6f33d94ae9bc89fce04) +- Initial commit [`3f1dc16`](https://github.com/ljharb/call-bind-apply-helpers/commit/3f1dc164afc43285631b114a5f9dd9137b2b952f) +- npm init [`081df04`](https://github.com/ljharb/call-bind-apply-helpers/commit/081df048c312fcee400922026f6e97281200a603) +- Only apps should have lockfiles [`5b9ca0f`](https://github.com/ljharb/call-bind-apply-helpers/commit/5b9ca0fe8101ebfaf309c549caac4e0a017ed930) diff --git a/src/node_modules/call-bind-apply-helpers/LICENSE b/src/node_modules/call-bind-apply-helpers/LICENSE new file mode 100644 index 0000000..f82f389 --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/node_modules/call-bind-apply-helpers/README.md b/src/node_modules/call-bind-apply-helpers/README.md new file mode 100644 index 0000000..8fc0dae --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/README.md @@ -0,0 +1,62 @@ +# call-bind-apply-helpers [![Version Badge][npm-version-svg]][package-url] + +[![github actions][actions-image]][actions-url] +[![coverage][codecov-image]][codecov-url] +[![dependency status][deps-svg]][deps-url] +[![dev dependency status][dev-deps-svg]][dev-deps-url] +[![License][license-image]][license-url] +[![Downloads][downloads-image]][downloads-url] + +[![npm badge][npm-badge-png]][package-url] + +Helper functions around Function call/apply/bind, for use in `call-bind`. + +The only packages that should likely ever use this package directly are `call-bind` and `get-intrinsic`. +Please use `call-bind` unless you have a very good reason not to. + +## Getting started + +```sh +npm install --save call-bind-apply-helpers +``` + +## Usage/Examples + +```js +const assert = require('assert'); +const callBindBasic = require('call-bind-apply-helpers'); + +function f(a, b) { + assert.equal(this, 1); + assert.equal(a, 2); + assert.equal(b, 3); + assert.equal(arguments.length, 2); +} + +const fBound = callBindBasic([f, 1]); + +delete Function.prototype.call; +delete Function.prototype.bind; + +fBound(2, 3); +``` + +## Tests + +Clone the repo, `npm install`, and run `npm test` + +[package-url]: https://npmjs.org/package/call-bind-apply-helpers +[npm-version-svg]: https://versionbadg.es/ljharb/call-bind-apply-helpers.svg +[deps-svg]: https://david-dm.org/ljharb/call-bind-apply-helpers.svg +[deps-url]: https://david-dm.org/ljharb/call-bind-apply-helpers +[dev-deps-svg]: https://david-dm.org/ljharb/call-bind-apply-helpers/dev-status.svg +[dev-deps-url]: https://david-dm.org/ljharb/call-bind-apply-helpers#info=devDependencies +[npm-badge-png]: https://nodei.co/npm/call-bind-apply-helpers.png?downloads=true&stars=true +[license-image]: https://img.shields.io/npm/l/call-bind-apply-helpers.svg +[license-url]: LICENSE +[downloads-image]: https://img.shields.io/npm/dm/call-bind-apply-helpers.svg +[downloads-url]: https://npm-stat.com/charts.html?package=call-bind-apply-helpers +[codecov-image]: https://codecov.io/gh/ljharb/call-bind-apply-helpers/branch/main/graphs/badge.svg +[codecov-url]: https://app.codecov.io/gh/ljharb/call-bind-apply-helpers/ +[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/ljharb/call-bind-apply-helpers +[actions-url]: https://github.com/ljharb/call-bind-apply-helpers/actions diff --git a/src/node_modules/call-bind-apply-helpers/actualApply.d.ts b/src/node_modules/call-bind-apply-helpers/actualApply.d.ts new file mode 100644 index 0000000..b87286a --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/actualApply.d.ts @@ -0,0 +1 @@ +export = Reflect.apply; \ No newline at end of file diff --git a/src/node_modules/call-bind-apply-helpers/actualApply.js b/src/node_modules/call-bind-apply-helpers/actualApply.js new file mode 100644 index 0000000..ffa5135 --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/actualApply.js @@ -0,0 +1,10 @@ +'use strict'; + +var bind = require('function-bind'); + +var $apply = require('./functionApply'); +var $call = require('./functionCall'); +var $reflectApply = require('./reflectApply'); + +/** @type {import('./actualApply')} */ +module.exports = $reflectApply || bind.call($call, $apply); diff --git a/src/node_modules/call-bind-apply-helpers/applyBind.d.ts b/src/node_modules/call-bind-apply-helpers/applyBind.d.ts new file mode 100644 index 0000000..d176c1a --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/applyBind.d.ts @@ -0,0 +1,19 @@ +import actualApply from './actualApply'; + +type TupleSplitHead = T['length'] extends N + ? T + : T extends [...infer R, any] + ? TupleSplitHead + : never + +type TupleSplitTail = O['length'] extends N + ? T + : T extends [infer F, ...infer R] + ? TupleSplitTail<[...R], N, [...O, F]> + : never + +type TupleSplit = [TupleSplitHead, TupleSplitTail] + +declare function applyBind(...args: TupleSplit, 2>[1]): ReturnType; + +export = applyBind; \ No newline at end of file diff --git a/src/node_modules/call-bind-apply-helpers/applyBind.js b/src/node_modules/call-bind-apply-helpers/applyBind.js new file mode 100644 index 0000000..d2b7723 --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/applyBind.js @@ -0,0 +1,10 @@ +'use strict'; + +var bind = require('function-bind'); +var $apply = require('./functionApply'); +var actualApply = require('./actualApply'); + +/** @type {import('./applyBind')} */ +module.exports = function applyBind() { + return actualApply(bind, $apply, arguments); +}; diff --git a/src/node_modules/call-bind-apply-helpers/functionApply.d.ts b/src/node_modules/call-bind-apply-helpers/functionApply.d.ts new file mode 100644 index 0000000..1f6e11b --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/functionApply.d.ts @@ -0,0 +1 @@ +export = Function.prototype.apply; \ No newline at end of file diff --git a/src/node_modules/call-bind-apply-helpers/functionApply.js b/src/node_modules/call-bind-apply-helpers/functionApply.js new file mode 100644 index 0000000..c71df9c --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/functionApply.js @@ -0,0 +1,4 @@ +'use strict'; + +/** @type {import('./functionApply')} */ +module.exports = Function.prototype.apply; diff --git a/src/node_modules/call-bind-apply-helpers/functionCall.d.ts b/src/node_modules/call-bind-apply-helpers/functionCall.d.ts new file mode 100644 index 0000000..15e93df --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/functionCall.d.ts @@ -0,0 +1 @@ +export = Function.prototype.call; \ No newline at end of file diff --git a/src/node_modules/call-bind-apply-helpers/functionCall.js b/src/node_modules/call-bind-apply-helpers/functionCall.js new file mode 100644 index 0000000..7a8d873 --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/functionCall.js @@ -0,0 +1,4 @@ +'use strict'; + +/** @type {import('./functionCall')} */ +module.exports = Function.prototype.call; diff --git a/src/node_modules/call-bind-apply-helpers/index.d.ts b/src/node_modules/call-bind-apply-helpers/index.d.ts new file mode 100644 index 0000000..541516b --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/index.d.ts @@ -0,0 +1,64 @@ +type RemoveFromTuple< + Tuple extends readonly unknown[], + RemoveCount extends number, + Index extends 1[] = [] +> = Index["length"] extends RemoveCount + ? Tuple + : Tuple extends [infer First, ...infer Rest] + ? RemoveFromTuple + : Tuple; + +type ConcatTuples< + Prefix extends readonly unknown[], + Suffix extends readonly unknown[] +> = [...Prefix, ...Suffix]; + +type ExtractFunctionParams = T extends (this: infer TThis, ...args: infer P extends readonly unknown[]) => infer R + ? { thisArg: TThis; params: P; returnType: R } + : never; + +type BindFunction< + T extends (this: any, ...args: any[]) => any, + TThis, + TBoundArgs extends readonly unknown[], + ReceiverBound extends boolean +> = ExtractFunctionParams extends { + thisArg: infer OrigThis; + params: infer P extends readonly unknown[]; + returnType: infer R; +} + ? ReceiverBound extends true + ? (...args: RemoveFromTuple>) => R extends [OrigThis, ...infer Rest] + ? [TThis, ...Rest] // Replace `this` with `thisArg` + : R + : >>( + thisArg: U, + ...args: RemainingArgs + ) => R extends [OrigThis, ...infer Rest] + ? [U, ...ConcatTuples] // Preserve bound args in return type + : R + : never; + +declare function callBind< + const T extends (this: any, ...args: any[]) => any, + Extracted extends ExtractFunctionParams, + const TBoundArgs extends Partial & readonly unknown[], + const TThis extends Extracted["thisArg"] +>( + args: [fn: T, thisArg: TThis, ...boundArgs: TBoundArgs] +): BindFunction; + +declare function callBind< + const T extends (this: any, ...args: any[]) => any, + Extracted extends ExtractFunctionParams, + const TBoundArgs extends Partial & readonly unknown[] +>( + args: [fn: T, ...boundArgs: TBoundArgs] +): BindFunction; + +declare function callBind( + args: [fn: Exclude, ...rest: TArgs] +): never; + +// export as namespace callBind; +export = callBind; diff --git a/src/node_modules/call-bind-apply-helpers/index.js b/src/node_modules/call-bind-apply-helpers/index.js new file mode 100644 index 0000000..2f6dab4 --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/index.js @@ -0,0 +1,15 @@ +'use strict'; + +var bind = require('function-bind'); +var $TypeError = require('es-errors/type'); + +var $call = require('./functionCall'); +var $actualApply = require('./actualApply'); + +/** @type {(args: [Function, thisArg?: unknown, ...args: unknown[]]) => Function} TODO FIXME, find a way to use import('.') */ +module.exports = function callBindBasic(args) { + if (args.length < 1 || typeof args[0] !== 'function') { + throw new $TypeError('a function is required'); + } + return $actualApply(bind, $call, args); +}; diff --git a/src/node_modules/call-bind-apply-helpers/package.json b/src/node_modules/call-bind-apply-helpers/package.json new file mode 100644 index 0000000..923b8be --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/package.json @@ -0,0 +1,85 @@ +{ + "name": "call-bind-apply-helpers", + "version": "1.0.2", + "description": "Helper functions around Function call/apply/bind, for use in `call-bind`", + "main": "index.js", + "exports": { + ".": "./index.js", + "./actualApply": "./actualApply.js", + "./applyBind": "./applyBind.js", + "./functionApply": "./functionApply.js", + "./functionCall": "./functionCall.js", + "./reflectApply": "./reflectApply.js", + "./package.json": "./package.json" + }, + "scripts": { + "prepack": "npmignore --auto --commentLines=auto", + "prepublish": "not-in-publish || npm run prepublishOnly", + "prepublishOnly": "safe-publish-latest", + "prelint": "evalmd README.md", + "lint": "eslint --ext=.js,.mjs .", + "postlint": "tsc -p . && attw -P", + "pretest": "npm run lint", + "tests-only": "nyc tape 'test/**/*.js'", + "test": "npm run tests-only", + "posttest": "npx npm@'>=10.2' audit --production", + "version": "auto-changelog && git add CHANGELOG.md", + "postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\"" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ljharb/call-bind-apply-helpers.git" + }, + "author": "Jordan Harband ", + "license": "MIT", + "bugs": { + "url": "https://github.com/ljharb/call-bind-apply-helpers/issues" + }, + "homepage": "https://github.com/ljharb/call-bind-apply-helpers#readme", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "devDependencies": { + "@arethetypeswrong/cli": "^0.17.3", + "@ljharb/eslint-config": "^21.1.1", + "@ljharb/tsconfig": "^0.2.3", + "@types/for-each": "^0.3.3", + "@types/function-bind": "^1.1.10", + "@types/object-inspect": "^1.13.0", + "@types/tape": "^5.8.1", + "auto-changelog": "^2.5.0", + "encoding": "^0.1.13", + "es-value-fixtures": "^1.7.1", + "eslint": "=8.8.0", + "evalmd": "^0.0.19", + "for-each": "^0.3.5", + "has-strict-mode": "^1.1.0", + "in-publish": "^2.0.1", + "npmignore": "^0.3.1", + "nyc": "^10.3.2", + "object-inspect": "^1.13.4", + "safe-publish-latest": "^2.0.0", + "tape": "^5.9.0", + "typescript": "next" + }, + "testling": { + "files": "test/index.js" + }, + "auto-changelog": { + "output": "CHANGELOG.md", + "template": "keepachangelog", + "unreleased": false, + "commitLimit": false, + "backfillLimit": false, + "hideCredit": true + }, + "publishConfig": { + "ignore": [ + ".github/workflows" + ] + }, + "engines": { + "node": ">= 0.4" + } +} diff --git a/src/node_modules/call-bind-apply-helpers/reflectApply.d.ts b/src/node_modules/call-bind-apply-helpers/reflectApply.d.ts new file mode 100644 index 0000000..6b2ae76 --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/reflectApply.d.ts @@ -0,0 +1,3 @@ +declare const reflectApply: false | typeof Reflect.apply; + +export = reflectApply; diff --git a/src/node_modules/call-bind-apply-helpers/reflectApply.js b/src/node_modules/call-bind-apply-helpers/reflectApply.js new file mode 100644 index 0000000..3d03caa --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/reflectApply.js @@ -0,0 +1,4 @@ +'use strict'; + +/** @type {import('./reflectApply')} */ +module.exports = typeof Reflect !== 'undefined' && Reflect && Reflect.apply; diff --git a/src/node_modules/call-bind-apply-helpers/test/index.js b/src/node_modules/call-bind-apply-helpers/test/index.js new file mode 100644 index 0000000..1cdc89e --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/test/index.js @@ -0,0 +1,63 @@ +'use strict'; + +var callBind = require('../'); +var hasStrictMode = require('has-strict-mode')(); +var forEach = require('for-each'); +var inspect = require('object-inspect'); +var v = require('es-value-fixtures'); + +var test = require('tape'); + +test('callBindBasic', function (t) { + forEach(v.nonFunctions, function (nonFunction) { + t['throws']( + // @ts-expect-error + function () { callBind([nonFunction]); }, + TypeError, + inspect(nonFunction) + ' is not a function' + ); + }); + + var sentinel = { sentinel: true }; + /** @type {(this: T, a: A, b: B) => [T | undefined, A, B]} */ + var func = function (a, b) { + // eslint-disable-next-line no-invalid-this + return [!hasStrictMode && this === global ? undefined : this, a, b]; + }; + t.equal(func.length, 2, 'original function length is 2'); + + /** type {(thisArg: unknown, a: number, b: number) => [unknown, number, number]} */ + var bound = callBind([func]); + /** type {((a: number, b: number) => [typeof sentinel, typeof a, typeof b])} */ + var boundR = callBind([func, sentinel]); + /** type {((b: number) => [typeof sentinel, number, typeof b])} */ + var boundArg = callBind([func, sentinel, /** @type {const} */ (1)]); + + // @ts-expect-error + t.deepEqual(bound(), [undefined, undefined, undefined], 'bound func with no args'); + + // @ts-expect-error + t.deepEqual(func(), [undefined, undefined, undefined], 'unbound func with too few args'); + // @ts-expect-error + t.deepEqual(bound(1, 2), [hasStrictMode ? 1 : Object(1), 2, undefined], 'bound func too few args'); + // @ts-expect-error + t.deepEqual(boundR(), [sentinel, undefined, undefined], 'bound func with receiver, with too few args'); + // @ts-expect-error + t.deepEqual(boundArg(), [sentinel, 1, undefined], 'bound func with receiver and arg, with too few args'); + + t.deepEqual(func(1, 2), [undefined, 1, 2], 'unbound func with right args'); + t.deepEqual(bound(1, 2, 3), [hasStrictMode ? 1 : Object(1), 2, 3], 'bound func with right args'); + t.deepEqual(boundR(1, 2), [sentinel, 1, 2], 'bound func with receiver, with right args'); + t.deepEqual(boundArg(2), [sentinel, 1, 2], 'bound func with receiver and arg, with right arg'); + + // @ts-expect-error + t.deepEqual(func(1, 2, 3), [undefined, 1, 2], 'unbound func with too many args'); + // @ts-expect-error + t.deepEqual(bound(1, 2, 3, 4), [hasStrictMode ? 1 : Object(1), 2, 3], 'bound func with too many args'); + // @ts-expect-error + t.deepEqual(boundR(1, 2, 3), [sentinel, 1, 2], 'bound func with receiver, with too many args'); + // @ts-expect-error + t.deepEqual(boundArg(2, 3), [sentinel, 1, 2], 'bound func with receiver and arg, with too many args'); + + t.end(); +}); diff --git a/src/node_modules/call-bind-apply-helpers/tsconfig.json b/src/node_modules/call-bind-apply-helpers/tsconfig.json new file mode 100644 index 0000000..aef9993 --- /dev/null +++ b/src/node_modules/call-bind-apply-helpers/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@ljharb/tsconfig", + "compilerOptions": { + "target": "es2021", + }, + "exclude": [ + "coverage", + ], +} \ No newline at end of file diff --git a/src/node_modules/call-bound/.eslintrc b/src/node_modules/call-bound/.eslintrc new file mode 100644 index 0000000..2612ed8 --- /dev/null +++ b/src/node_modules/call-bound/.eslintrc @@ -0,0 +1,13 @@ +{ + "root": true, + + "extends": "@ljharb", + + "rules": { + "new-cap": [2, { + "capIsNewExceptions": [ + "GetIntrinsic", + ], + }], + }, +} diff --git a/src/node_modules/call-bound/.github/FUNDING.yml b/src/node_modules/call-bound/.github/FUNDING.yml new file mode 100644 index 0000000..2a2a135 --- /dev/null +++ b/src/node_modules/call-bound/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [ljharb] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: npm/call-bound +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/src/node_modules/call-bound/.nycrc b/src/node_modules/call-bound/.nycrc new file mode 100644 index 0000000..bdd626c --- /dev/null +++ b/src/node_modules/call-bound/.nycrc @@ -0,0 +1,9 @@ +{ + "all": true, + "check-coverage": false, + "reporter": ["text-summary", "text", "html", "json"], + "exclude": [ + "coverage", + "test" + ] +} diff --git a/src/node_modules/call-bound/CHANGELOG.md b/src/node_modules/call-bound/CHANGELOG.md new file mode 100644 index 0000000..8bde4e9 --- /dev/null +++ b/src/node_modules/call-bound/CHANGELOG.md @@ -0,0 +1,42 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [v1.0.4](https://github.com/ljharb/call-bound/compare/v1.0.3...v1.0.4) - 2025-03-03 + +### Commits + +- [types] improve types [`e648922`](https://github.com/ljharb/call-bound/commit/e6489222a9e54f350fbf952ceabe51fd8b6027ff) +- [Dev Deps] update `@arethetypeswrong/cli`, `@ljharb/tsconfig`, `@types/tape`, `es-value-fixtures`, `for-each`, `has-strict-mode`, `object-inspect` [`a42a5eb`](https://github.com/ljharb/call-bound/commit/a42a5ebe6c1b54fcdc7997c7dc64fdca9e936719) +- [Deps] update `call-bind-apply-helpers`, `get-intrinsic` [`f529eac`](https://github.com/ljharb/call-bound/commit/f529eac132404c17156bbc23ab2297a25d0f20b8) + +## [v1.0.3](https://github.com/ljharb/call-bound/compare/v1.0.2...v1.0.3) - 2024-12-15 + +### Commits + +- [Refactor] use `call-bind-apply-helpers` instead of `call-bind` [`5e0b134`](https://github.com/ljharb/call-bound/commit/5e0b13496df14fb7d05dae9412f088da8d3f75be) +- [Deps] update `get-intrinsic` [`41fc967`](https://github.com/ljharb/call-bound/commit/41fc96732a22c7b7e8f381f93ccc54bb6293be2e) +- [readme] fix example [`79a0137`](https://github.com/ljharb/call-bound/commit/79a0137723f7c6d09c9c05452bbf8d5efb5d6e49) +- [meta] add `sideEffects` flag [`08b07be`](https://github.com/ljharb/call-bound/commit/08b07be7f1c03f67dc6f3cdaf0906259771859f7) + +## [v1.0.2](https://github.com/ljharb/call-bound/compare/v1.0.1...v1.0.2) - 2024-12-10 + +### Commits + +- [Dev Deps] update `@arethetypeswrong/cli`, `@ljharb/tsconfig`, `gopd` [`e6a5ffe`](https://github.com/ljharb/call-bound/commit/e6a5ffe849368fe4f74dfd6cdeca1b9baa39e8d5) +- [Deps] update `call-bind`, `get-intrinsic` [`2aeb5b5`](https://github.com/ljharb/call-bound/commit/2aeb5b521dc2b2683d1345c753ea1161de2d1c14) +- [types] improve return type [`1a0c9fe`](https://github.com/ljharb/call-bound/commit/1a0c9fe3114471e7ca1f57d104e2efe713bb4871) + +## v1.0.1 - 2024-12-05 + +### Commits + +- Initial implementation, tests, readme, types [`6d94121`](https://github.com/ljharb/call-bound/commit/6d94121a9243602e506334069f7a03189fe3363d) +- Initial commit [`0eae867`](https://github.com/ljharb/call-bound/commit/0eae867334ea025c33e6e91cdecfc9df96680cf9) +- npm init [`71b2479`](https://github.com/ljharb/call-bound/commit/71b2479c6723e0b7d91a6b663613067e98b7b275) +- Only apps should have lockfiles [`c3754a9`](https://github.com/ljharb/call-bound/commit/c3754a949b7f9132b47e2d18c1729889736741eb) +- [actions] skip `npm ls` in node < 10 [`74275a5`](https://github.com/ljharb/call-bound/commit/74275a5186b8caf6309b6b97472bdcb0df4683a8) +- [Dev Deps] add missing peer dep [`1354de8`](https://github.com/ljharb/call-bound/commit/1354de8679413e4ae9c523d85f76fa7a5e032d97) diff --git a/src/node_modules/call-bound/LICENSE b/src/node_modules/call-bound/LICENSE new file mode 100644 index 0000000..f82f389 --- /dev/null +++ b/src/node_modules/call-bound/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/node_modules/call-bound/README.md b/src/node_modules/call-bound/README.md new file mode 100644 index 0000000..a44e43e --- /dev/null +++ b/src/node_modules/call-bound/README.md @@ -0,0 +1,53 @@ +# call-bound [![Version Badge][npm-version-svg]][package-url] + +[![github actions][actions-image]][actions-url] +[![coverage][codecov-image]][codecov-url] +[![dependency status][deps-svg]][deps-url] +[![dev dependency status][dev-deps-svg]][dev-deps-url] +[![License][license-image]][license-url] +[![Downloads][downloads-image]][downloads-url] + +[![npm badge][npm-badge-png]][package-url] + +Robust call-bound JavaScript intrinsics, using `call-bind` and `get-intrinsic`. + +## Getting started + +```sh +npm install --save call-bound +``` + +## Usage/Examples + +```js +const assert = require('assert'); +const callBound = require('call-bound'); + +const slice = callBound('Array.prototype.slice'); + +delete Function.prototype.call; +delete Function.prototype.bind; +delete Array.prototype.slice; + +assert.deepEqual(slice([1, 2, 3, 4], 1, -1), [2, 3]); +``` + +## Tests + +Clone the repo, `npm install`, and run `npm test` + +[package-url]: https://npmjs.org/package/call-bound +[npm-version-svg]: https://versionbadg.es/ljharb/call-bound.svg +[deps-svg]: https://david-dm.org/ljharb/call-bound.svg +[deps-url]: https://david-dm.org/ljharb/call-bound +[dev-deps-svg]: https://david-dm.org/ljharb/call-bound/dev-status.svg +[dev-deps-url]: https://david-dm.org/ljharb/call-bound#info=devDependencies +[npm-badge-png]: https://nodei.co/npm/call-bound.png?downloads=true&stars=true +[license-image]: https://img.shields.io/npm/l/call-bound.svg +[license-url]: LICENSE +[downloads-image]: https://img.shields.io/npm/dm/call-bound.svg +[downloads-url]: https://npm-stat.com/charts.html?package=call-bound +[codecov-image]: https://codecov.io/gh/ljharb/call-bound/branch/main/graphs/badge.svg +[codecov-url]: https://app.codecov.io/gh/ljharb/call-bound/ +[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/ljharb/call-bound +[actions-url]: https://github.com/ljharb/call-bound/actions diff --git a/src/node_modules/call-bound/index.d.ts b/src/node_modules/call-bound/index.d.ts new file mode 100644 index 0000000..5562f00 --- /dev/null +++ b/src/node_modules/call-bound/index.d.ts @@ -0,0 +1,94 @@ +type Intrinsic = typeof globalThis; + +type IntrinsicName = keyof Intrinsic | `%${keyof Intrinsic}%`; + +type IntrinsicPath = IntrinsicName | `${StripPercents}.${string}` | `%${StripPercents}.${string}%`; + +type AllowMissing = boolean; + +type StripPercents = T extends `%${infer U}%` ? U : T; + +type BindMethodPrecise = + F extends (this: infer This, ...args: infer Args) => infer R + ? (obj: This, ...args: Args) => R + : F extends { + (this: infer This1, ...args: infer Args1): infer R1; + (this: infer This2, ...args: infer Args2): infer R2 + } + ? { + (obj: This1, ...args: Args1): R1; + (obj: This2, ...args: Args2): R2 + } + : never + +// Extract method type from a prototype +type GetPrototypeMethod = + (typeof globalThis)[T] extends { prototype: any } + ? M extends keyof (typeof globalThis)[T]['prototype'] + ? (typeof globalThis)[T]['prototype'][M] + : never + : never + +// Get static property/method +type GetStaticMember = + P extends keyof (typeof globalThis)[T] ? (typeof globalThis)[T][P] : never + +// Type that maps string path to actual bound function or value with better precision +type BoundIntrinsic = + S extends `${infer Obj}.prototype.${infer Method}` + ? Obj extends keyof typeof globalThis + ? BindMethodPrecise> + : unknown + : S extends `${infer Obj}.${infer Prop}` + ? Obj extends keyof typeof globalThis + ? GetStaticMember + : unknown + : unknown + +declare function arraySlice(array: readonly T[], start?: number, end?: number): T[]; +declare function arraySlice(array: ArrayLike, start?: number, end?: number): T[]; +declare function arraySlice(array: IArguments, start?: number, end?: number): T[]; + +// Special cases for methods that need explicit typing +interface SpecialCases { + '%Object.prototype.isPrototypeOf%': (thisArg: {}, obj: unknown) => boolean; + '%String.prototype.replace%': { + (str: string, searchValue: string | RegExp, replaceValue: string): string; + (str: string, searchValue: string | RegExp, replacer: (substring: string, ...args: any[]) => string): string + }; + '%Object.prototype.toString%': (obj: {}) => string; + '%Object.prototype.hasOwnProperty%': (obj: {}, v: PropertyKey) => boolean; + '%Array.prototype.slice%': typeof arraySlice; + '%Array.prototype.map%': (array: readonly T[], callbackfn: (value: T, index: number, array: readonly T[]) => U, thisArg?: any) => U[]; + '%Array.prototype.filter%': (array: readonly T[], predicate: (value: T, index: number, array: readonly T[]) => unknown, thisArg?: any) => T[]; + '%Array.prototype.indexOf%': (array: readonly T[], searchElement: T, fromIndex?: number) => number; + '%Function.prototype.apply%': (fn: (...args: A) => R, thisArg: any, args: A) => R; + '%Function.prototype.call%': (fn: (...args: A) => R, thisArg: any, ...args: A) => R; + '%Function.prototype.bind%': (fn: (...args: A) => R, thisArg: any, ...args: A) => (...remainingArgs: A) => R; + '%Promise.prototype.then%': { + (promise: Promise, onfulfilled: (value: T) => R | PromiseLike): Promise; + (promise: Promise, onfulfilled: ((value: T) => R | PromiseLike) | undefined | null, onrejected: (reason: any) => R | PromiseLike): Promise; + }; + '%RegExp.prototype.test%': (regexp: RegExp, str: string) => boolean; + '%RegExp.prototype.exec%': (regexp: RegExp, str: string) => RegExpExecArray | null; + '%Error.prototype.toString%': (error: Error) => string; + '%TypeError.prototype.toString%': (error: TypeError) => string; + '%String.prototype.split%': ( + obj: unknown, + splitter: string | RegExp | { + [Symbol.split](string: string, limit?: number): string[]; + }, + limit?: number | undefined + ) => string[]; +} + +/** + * Returns a bound function for a prototype method, or a value for a static property. + * + * @param name - The name of the intrinsic (e.g. 'Array.prototype.slice') + * @param {AllowMissing} [allowMissing] - Whether to allow missing intrinsics (default: false) + */ +declare function callBound, S extends IntrinsicPath>(name: K, allowMissing?: AllowMissing): SpecialCases[`%${StripPercents}%`]; +declare function callBound, S extends IntrinsicPath>(name: S, allowMissing?: AllowMissing): BoundIntrinsic; + +export = callBound; diff --git a/src/node_modules/call-bound/index.js b/src/node_modules/call-bound/index.js new file mode 100644 index 0000000..e9ade74 --- /dev/null +++ b/src/node_modules/call-bound/index.js @@ -0,0 +1,19 @@ +'use strict'; + +var GetIntrinsic = require('get-intrinsic'); + +var callBindBasic = require('call-bind-apply-helpers'); + +/** @type {(thisArg: string, searchString: string, position?: number) => number} */ +var $indexOf = callBindBasic([GetIntrinsic('%String.prototype.indexOf%')]); + +/** @type {import('.')} */ +module.exports = function callBoundIntrinsic(name, allowMissing) { + /* eslint no-extra-parens: 0 */ + + var intrinsic = /** @type {(this: unknown, ...args: unknown[]) => unknown} */ (GetIntrinsic(name, !!allowMissing)); + if (typeof intrinsic === 'function' && $indexOf(name, '.prototype.') > -1) { + return callBindBasic(/** @type {const} */ ([intrinsic])); + } + return intrinsic; +}; diff --git a/src/node_modules/call-bound/package.json b/src/node_modules/call-bound/package.json new file mode 100644 index 0000000..d542db4 --- /dev/null +++ b/src/node_modules/call-bound/package.json @@ -0,0 +1,99 @@ +{ + "name": "call-bound", + "version": "1.0.4", + "description": "Robust call-bound JavaScript intrinsics, using `call-bind` and `get-intrinsic`.", + "main": "index.js", + "exports": { + ".": "./index.js", + "./package.json": "./package.json" + }, + "sideEffects": false, + "scripts": { + "prepack": "npmignore --auto --commentLines=auto", + "prepublish": "not-in-publish || npm run prepublishOnly", + "prepublishOnly": "safe-publish-latest", + "prelint": "evalmd README.md", + "lint": "eslint --ext=.js,.mjs .", + "postlint": "tsc -p . && attw -P", + "pretest": "npm run lint", + "tests-only": "nyc tape 'test/**/*.js'", + "test": "npm run tests-only", + "posttest": "npx npm@'>=10.2' audit --production", + "version": "auto-changelog && git add CHANGELOG.md", + "postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\"" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ljharb/call-bound.git" + }, + "keywords": [ + "javascript", + "ecmascript", + "es", + "js", + "callbind", + "callbound", + "call", + "bind", + "bound", + "call-bind", + "call-bound", + "function", + "es-abstract" + ], + "author": "Jordan Harband ", + "funding": { + "url": "https://github.com/sponsors/ljharb" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/ljharb/call-bound/issues" + }, + "homepage": "https://github.com/ljharb/call-bound#readme", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "devDependencies": { + "@arethetypeswrong/cli": "^0.17.4", + "@ljharb/eslint-config": "^21.1.1", + "@ljharb/tsconfig": "^0.3.0", + "@types/call-bind": "^1.0.5", + "@types/get-intrinsic": "^1.2.3", + "@types/tape": "^5.8.1", + "auto-changelog": "^2.5.0", + "encoding": "^0.1.13", + "es-value-fixtures": "^1.7.1", + "eslint": "=8.8.0", + "evalmd": "^0.0.19", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "has-strict-mode": "^1.1.0", + "in-publish": "^2.0.1", + "npmignore": "^0.3.1", + "nyc": "^10.3.2", + "object-inspect": "^1.13.4", + "safe-publish-latest": "^2.0.0", + "tape": "^5.9.0", + "typescript": "next" + }, + "testling": { + "files": "test/index.js" + }, + "auto-changelog": { + "output": "CHANGELOG.md", + "template": "keepachangelog", + "unreleased": false, + "commitLimit": false, + "backfillLimit": false, + "hideCredit": true + }, + "publishConfig": { + "ignore": [ + ".github/workflows" + ] + }, + "engines": { + "node": ">= 0.4" + } +} diff --git a/src/node_modules/call-bound/test/index.js b/src/node_modules/call-bound/test/index.js new file mode 100644 index 0000000..a2fc9f0 --- /dev/null +++ b/src/node_modules/call-bound/test/index.js @@ -0,0 +1,61 @@ +'use strict'; + +var test = require('tape'); + +var callBound = require('../'); + +/** @template {true} T @template U @typedef {T extends U ? T : never} AssertType */ + +test('callBound', function (t) { + // static primitive + t.equal(callBound('Array.length'), Array.length, 'Array.length yields itself'); + t.equal(callBound('%Array.length%'), Array.length, '%Array.length% yields itself'); + + // static non-function object + t.equal(callBound('Array.prototype'), Array.prototype, 'Array.prototype yields itself'); + t.equal(callBound('%Array.prototype%'), Array.prototype, '%Array.prototype% yields itself'); + t.equal(callBound('Array.constructor'), Array.constructor, 'Array.constructor yields itself'); + t.equal(callBound('%Array.constructor%'), Array.constructor, '%Array.constructor% yields itself'); + + // static function + t.equal(callBound('Date.parse'), Date.parse, 'Date.parse yields itself'); + t.equal(callBound('%Date.parse%'), Date.parse, '%Date.parse% yields itself'); + + // prototype primitive + t.equal(callBound('Error.prototype.message'), Error.prototype.message, 'Error.prototype.message yields itself'); + t.equal(callBound('%Error.prototype.message%'), Error.prototype.message, '%Error.prototype.message% yields itself'); + + var x = callBound('Object.prototype.toString'); + var y = callBound('%Object.prototype.toString%'); + + // prototype function + t.notEqual(x, Object.prototype.toString, 'Object.prototype.toString does not yield itself'); + t.notEqual(y, Object.prototype.toString, '%Object.prototype.toString% does not yield itself'); + t.equal(x(true), Object.prototype.toString.call(true), 'call-bound Object.prototype.toString calls into the original'); + t.equal(y(true), Object.prototype.toString.call(true), 'call-bound %Object.prototype.toString% calls into the original'); + + t['throws']( + // @ts-expect-error + function () { callBound('does not exist'); }, + SyntaxError, + 'nonexistent intrinsic throws' + ); + t['throws']( + // @ts-expect-error + function () { callBound('does not exist', true); }, + SyntaxError, + 'allowMissing arg still throws for unknown intrinsic' + ); + + t.test('real but absent intrinsic', { skip: typeof WeakRef !== 'undefined' }, function (st) { + st['throws']( + function () { callBound('WeakRef'); }, + TypeError, + 'real but absent intrinsic throws' + ); + st.equal(callBound('WeakRef', true), undefined, 'allowMissing arg avoids exception'); + st.end(); + }); + + t.end(); +}); diff --git a/src/node_modules/call-bound/tsconfig.json b/src/node_modules/call-bound/tsconfig.json new file mode 100644 index 0000000..8976d98 --- /dev/null +++ b/src/node_modules/call-bound/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@ljharb/tsconfig", + "compilerOptions": { + "target": "ESNext", + "lib": ["es2024"], + }, + "exclude": [ + "coverage", + ], +} diff --git a/src/node_modules/concat-stream/LICENSE b/src/node_modules/concat-stream/LICENSE new file mode 100644 index 0000000..99c130e --- /dev/null +++ b/src/node_modules/concat-stream/LICENSE @@ -0,0 +1,24 @@ +The MIT License + +Copyright (c) 2013 Max Ogden + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/src/node_modules/concat-stream/index.js b/src/node_modules/concat-stream/index.js new file mode 100644 index 0000000..dd672a7 --- /dev/null +++ b/src/node_modules/concat-stream/index.js @@ -0,0 +1,144 @@ +var Writable = require('readable-stream').Writable +var inherits = require('inherits') +var bufferFrom = require('buffer-from') + +if (typeof Uint8Array === 'undefined') { + var U8 = require('typedarray').Uint8Array +} else { + var U8 = Uint8Array +} + +function ConcatStream(opts, cb) { + if (!(this instanceof ConcatStream)) return new ConcatStream(opts, cb) + + if (typeof opts === 'function') { + cb = opts + opts = {} + } + if (!opts) opts = {} + + var encoding = opts.encoding + var shouldInferEncoding = false + + if (!encoding) { + shouldInferEncoding = true + } else { + encoding = String(encoding).toLowerCase() + if (encoding === 'u8' || encoding === 'uint8') { + encoding = 'uint8array' + } + } + + Writable.call(this, { objectMode: true }) + + this.encoding = encoding + this.shouldInferEncoding = shouldInferEncoding + + if (cb) this.on('finish', function () { cb(this.getBody()) }) + this.body = [] +} + +module.exports = ConcatStream +inherits(ConcatStream, Writable) + +ConcatStream.prototype._write = function(chunk, enc, next) { + this.body.push(chunk) + next() +} + +ConcatStream.prototype.inferEncoding = function (buff) { + var firstBuffer = buff === undefined ? this.body[0] : buff; + if (Buffer.isBuffer(firstBuffer)) return 'buffer' + if (typeof Uint8Array !== 'undefined' && firstBuffer instanceof Uint8Array) return 'uint8array' + if (Array.isArray(firstBuffer)) return 'array' + if (typeof firstBuffer === 'string') return 'string' + if (Object.prototype.toString.call(firstBuffer) === "[object Object]") return 'object' + return 'buffer' +} + +ConcatStream.prototype.getBody = function () { + if (!this.encoding && this.body.length === 0) return [] + if (this.shouldInferEncoding) this.encoding = this.inferEncoding() + if (this.encoding === 'array') return arrayConcat(this.body) + if (this.encoding === 'string') return stringConcat(this.body) + if (this.encoding === 'buffer') return bufferConcat(this.body) + if (this.encoding === 'uint8array') return u8Concat(this.body) + return this.body +} + +var isArray = Array.isArray || function (arr) { + return Object.prototype.toString.call(arr) == '[object Array]' +} + +function isArrayish (arr) { + return /Array\]$/.test(Object.prototype.toString.call(arr)) +} + +function isBufferish (p) { + return typeof p === 'string' || isArrayish(p) || (p && typeof p.subarray === 'function') +} + +function stringConcat (parts) { + var strings = [] + var needsToString = false + for (var i = 0; i < parts.length; i++) { + var p = parts[i] + if (typeof p === 'string') { + strings.push(p) + } else if (Buffer.isBuffer(p)) { + strings.push(p) + } else if (isBufferish(p)) { + strings.push(bufferFrom(p)) + } else { + strings.push(bufferFrom(String(p))) + } + } + if (Buffer.isBuffer(parts[0])) { + strings = Buffer.concat(strings) + strings = strings.toString('utf8') + } else { + strings = strings.join('') + } + return strings +} + +function bufferConcat (parts) { + var bufs = [] + for (var i = 0; i < parts.length; i++) { + var p = parts[i] + if (Buffer.isBuffer(p)) { + bufs.push(p) + } else if (isBufferish(p)) { + bufs.push(bufferFrom(p)) + } else { + bufs.push(bufferFrom(String(p))) + } + } + return Buffer.concat(bufs) +} + +function arrayConcat (parts) { + var res = [] + for (var i = 0; i < parts.length; i++) { + res.push.apply(res, parts[i]) + } + return res +} + +function u8Concat (parts) { + var len = 0 + for (var i = 0; i < parts.length; i++) { + if (typeof parts[i] === 'string') { + parts[i] = bufferFrom(parts[i]) + } + len += parts[i].length + } + var u8 = new U8(len) + for (var i = 0, offset = 0; i < parts.length; i++) { + var part = parts[i] + for (var j = 0; j < part.length; j++) { + u8[offset++] = part[j] + } + } + return u8 +} diff --git a/src/node_modules/concat-stream/package.json b/src/node_modules/concat-stream/package.json new file mode 100644 index 0000000..3797828 --- /dev/null +++ b/src/node_modules/concat-stream/package.json @@ -0,0 +1,55 @@ +{ + "name": "concat-stream", + "version": "2.0.0", + "description": "writable stream that concatenates strings or binary data and calls a callback with the result", + "tags": [ + "stream", + "simple", + "util", + "utility" + ], + "author": "Max Ogden ", + "repository": { + "type": "git", + "url": "http://github.com/maxogden/concat-stream.git" + }, + "bugs": { + "url": "http://github.com/maxogden/concat-stream/issues" + }, + "engines": [ + "node >= 6.0" + ], + "main": "index.js", + "files": [ + "index.js" + ], + "scripts": { + "test": "tape test/*.js test/server/*.js" + }, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + }, + "devDependencies": { + "tape": "^4.6.3" + }, + "testling": { + "files": "test/*.js", + "browsers": [ + "ie/8..latest", + "firefox/17..latest", + "firefox/nightly", + "chrome/22..latest", + "chrome/canary", + "opera/12..latest", + "opera/next", + "safari/5.1..latest", + "ipad/6.0..latest", + "iphone/6.0..latest", + "android-browser/4.2..latest" + ] + } +} diff --git a/src/node_modules/concat-stream/readme.md b/src/node_modules/concat-stream/readme.md new file mode 100644 index 0000000..7aa19c4 --- /dev/null +++ b/src/node_modules/concat-stream/readme.md @@ -0,0 +1,102 @@ +# concat-stream + +Writable stream that concatenates all the data from a stream and calls a callback with the result. Use this when you want to collect all the data from a stream into a single buffer. + +[![Build Status](https://travis-ci.org/maxogden/concat-stream.svg?branch=master)](https://travis-ci.org/maxogden/concat-stream) + +[![NPM](https://nodei.co/npm/concat-stream.png)](https://nodei.co/npm/concat-stream/) + +### description + +Streams emit many buffers. If you want to collect all of the buffers, and when the stream ends concatenate all of the buffers together and receive a single buffer then this is the module for you. + +Only use this if you know you can fit all of the output of your stream into a single Buffer (e.g. in RAM). + +There are also `objectMode` streams that emit things other than Buffers, and you can concatenate these too. See below for details. + +## Related + +`concat-stream` is part of the [mississippi stream utility collection](https://github.com/maxogden/mississippi) which includes more useful stream modules similar to this one. + +### examples + +#### Buffers + +```js +var fs = require('fs') +var concat = require('concat-stream') + +var readStream = fs.createReadStream('cat.png') +var concatStream = concat(gotPicture) + +readStream.on('error', handleError) +readStream.pipe(concatStream) + +function gotPicture(imageBuffer) { + // imageBuffer is all of `cat.png` as a node.js Buffer +} + +function handleError(err) { + // handle your error appropriately here, e.g.: + console.error(err) // print the error to STDERR + process.exit(1) // exit program with non-zero exit code +} + +``` + +#### Arrays + +```js +var write = concat(function(data) {}) +write.write([1,2,3]) +write.write([4,5,6]) +write.end() +// data will be [1,2,3,4,5,6] in the above callback +``` + +#### Uint8Arrays + +```js +var write = concat(function(data) {}) +var a = new Uint8Array(3) +a[0] = 97; a[1] = 98; a[2] = 99 +write.write(a) +write.write('!') +write.end(Buffer.from('!!1')) +``` + +See `test/` for more examples + +# methods + +```js +var concat = require('concat-stream') +``` + +## var writable = concat(opts={}, cb) + +Return a `writable` stream that will fire `cb(data)` with all of the data that +was written to the stream. Data can be written to `writable` as strings, +Buffers, arrays of byte integers, and Uint8Arrays. + +By default `concat-stream` will give you back the same data type as the type of the first buffer written to the stream. Use `opts.encoding` to set what format `data` should be returned as, e.g. if you if you don't want to rely on the built-in type checking or for some other reason. + +* `string` - get a string +* `buffer` - get back a Buffer +* `array` - get an array of byte integers +* `uint8array`, `u8`, `uint8` - get back a Uint8Array +* `object`, get back an array of Objects + +If you don't specify an encoding, and the types can't be inferred (e.g. you write things that aren't in the list above), it will try to convert concat them into a `Buffer`. + +If nothing is written to `writable` then `data` will be an empty array `[]`. + +# error handling + +`concat-stream` does not handle errors for you, so you must handle errors on whatever streams you pipe into `concat-stream`. This is a general rule when programming with node.js streams: always handle errors on each and every stream. Since `concat-stream` is not itself a stream it does not emit errors. + +We recommend using [`end-of-stream`](https://npmjs.org/end-of-stream) or [`pump`](https://npmjs.org/pump) for writing error tolerant stream code. + +# license + +MIT LICENSE diff --git a/src/node_modules/content-disposition/HISTORY.md b/src/node_modules/content-disposition/HISTORY.md new file mode 100644 index 0000000..ff0b68b --- /dev/null +++ b/src/node_modules/content-disposition/HISTORY.md @@ -0,0 +1,66 @@ +1.0.0 / 2024-08-31 +================== + + * drop node <18 + * allow utf8 as alias for utf-8 + +0.5.4 / 2021-12-10 +================== + + * deps: safe-buffer@5.2.1 + +0.5.3 / 2018-12-17 +================== + + * Use `safe-buffer` for improved Buffer API + +0.5.2 / 2016-12-08 +================== + + * Fix `parse` to accept any linear whitespace character + +0.5.1 / 2016-01-17 +================== + + * perf: enable strict mode + +0.5.0 / 2014-10-11 +================== + + * Add `parse` function + +0.4.0 / 2014-09-21 +================== + + * Expand non-Unicode `filename` to the full ISO-8859-1 charset + +0.3.0 / 2014-09-20 +================== + + * Add `fallback` option + * Add `type` option + +0.2.0 / 2014-09-19 +================== + + * Reduce ambiguity of file names with hex escape in buggy browsers + +0.1.2 / 2014-09-19 +================== + + * Fix periodic invalid Unicode filename header + +0.1.1 / 2014-09-19 +================== + + * Fix invalid characters appearing in `filename*` parameter + +0.1.0 / 2014-09-18 +================== + + * Make the `filename` argument optional + +0.0.0 / 2014-09-18 +================== + + * Initial release diff --git a/src/node_modules/content-disposition/LICENSE b/src/node_modules/content-disposition/LICENSE new file mode 100644 index 0000000..84441fb --- /dev/null +++ b/src/node_modules/content-disposition/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/node_modules/content-disposition/README.md b/src/node_modules/content-disposition/README.md new file mode 100644 index 0000000..3a0bb05 --- /dev/null +++ b/src/node_modules/content-disposition/README.md @@ -0,0 +1,142 @@ +# content-disposition + +[![NPM Version][npm-image]][npm-url] +[![NPM Downloads][downloads-image]][downloads-url] +[![Node.js Version][node-version-image]][node-version-url] +[![Build Status][github-actions-ci-image]][github-actions-ci-url] +[![Test Coverage][coveralls-image]][coveralls-url] + +Create and parse HTTP `Content-Disposition` header + +## Installation + +```sh +$ npm install content-disposition +``` + +## API + +```js +var contentDisposition = require('content-disposition') +``` + +### contentDisposition(filename, options) + +Create an attachment `Content-Disposition` header value using the given file name, +if supplied. The `filename` is optional and if no file name is desired, but you +want to specify `options`, set `filename` to `undefined`. + +```js +res.setHeader('Content-Disposition', contentDisposition('∫ maths.pdf')) +``` + +**note** HTTP headers are of the ISO-8859-1 character set. If you are writing this +header through a means different from `setHeader` in Node.js, you'll want to specify +the `'binary'` encoding in Node.js. + +#### Options + +`contentDisposition` accepts these properties in the options object. + +##### fallback + +If the `filename` option is outside ISO-8859-1, then the file name is actually +stored in a supplemental field for clients that support Unicode file names and +a ISO-8859-1 version of the file name is automatically generated. + +This specifies the ISO-8859-1 file name to override the automatic generation or +disables the generation all together, defaults to `true`. + + - A string will specify the ISO-8859-1 file name to use in place of automatic + generation. + - `false` will disable including a ISO-8859-1 file name and only include the + Unicode version (unless the file name is already ISO-8859-1). + - `true` will enable automatic generation if the file name is outside ISO-8859-1. + +If the `filename` option is ISO-8859-1 and this option is specified and has a +different value, then the `filename` option is encoded in the extended field +and this set as the fallback field, even though they are both ISO-8859-1. + +##### type + +Specifies the disposition type, defaults to `"attachment"`. This can also be +`"inline"`, or any other value (all values except inline are treated like +`attachment`, but can convey additional information if both parties agree to +it). The type is normalized to lower-case. + +### contentDisposition.parse(string) + +```js +var disposition = contentDisposition.parse('attachment; filename="EURO rates.txt"; filename*=UTF-8\'\'%e2%82%ac%20rates.txt') +``` + +Parse a `Content-Disposition` header string. This automatically handles extended +("Unicode") parameters by decoding them and providing them under the standard +parameter name. This will return an object with the following properties (examples +are shown for the string `'attachment; filename="EURO rates.txt"; filename*=UTF-8\'\'%e2%82%ac%20rates.txt'`): + + - `type`: The disposition type (always lower case). Example: `'attachment'` + + - `parameters`: An object of the parameters in the disposition (name of parameter + always lower case and extended versions replace non-extended versions). Example: + `{filename: "€ rates.txt"}` + +## Examples + +### Send a file for download + +```js +var contentDisposition = require('content-disposition') +var destroy = require('destroy') +var fs = require('fs') +var http = require('http') +var onFinished = require('on-finished') + +var filePath = '/path/to/public/plans.pdf' + +http.createServer(function onRequest (req, res) { + // set headers + res.setHeader('Content-Type', 'application/pdf') + res.setHeader('Content-Disposition', contentDisposition(filePath)) + + // send file + var stream = fs.createReadStream(filePath) + stream.pipe(res) + onFinished(res, function () { + destroy(stream) + }) +}) +``` + +## Testing + +```sh +$ npm test +``` + +## References + +- [RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1][rfc-2616] +- [RFC 5987: Character Set and Language Encoding for Hypertext Transfer Protocol (HTTP) Header Field Parameters][rfc-5987] +- [RFC 6266: Use of the Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)][rfc-6266] +- [Test Cases for HTTP Content-Disposition header field (RFC 6266) and the Encodings defined in RFCs 2047, 2231 and 5987][tc-2231] + +[rfc-2616]: https://tools.ietf.org/html/rfc2616 +[rfc-5987]: https://tools.ietf.org/html/rfc5987 +[rfc-6266]: https://tools.ietf.org/html/rfc6266 +[tc-2231]: http://greenbytes.de/tech/tc2231/ + +## License + +[MIT](LICENSE) + +[npm-image]: https://img.shields.io/npm/v/content-disposition.svg +[npm-url]: https://npmjs.org/package/content-disposition +[node-version-image]: https://img.shields.io/node/v/content-disposition.svg +[node-version-url]: https://nodejs.org/en/download +[coveralls-image]: https://img.shields.io/coveralls/jshttp/content-disposition.svg +[coveralls-url]: https://coveralls.io/r/jshttp/content-disposition?branch=master +[downloads-image]: https://img.shields.io/npm/dm/content-disposition.svg +[downloads-url]: https://npmjs.org/package/content-disposition +[github-actions-ci-image]: https://img.shields.io/github/workflow/status/jshttp/content-disposition/ci/master?label=ci +[github-actions-ci-url]: https://github.com/jshttp/content-disposition?query=workflow%3Aci diff --git a/src/node_modules/content-disposition/index.js b/src/node_modules/content-disposition/index.js new file mode 100644 index 0000000..44f1d51 --- /dev/null +++ b/src/node_modules/content-disposition/index.js @@ -0,0 +1,459 @@ +/*! + * content-disposition + * Copyright(c) 2014-2017 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module exports. + * @public + */ + +module.exports = contentDisposition +module.exports.parse = parse + +/** + * Module dependencies. + * @private + */ + +var basename = require('path').basename +var Buffer = require('safe-buffer').Buffer + +/** + * RegExp to match non attr-char, *after* encodeURIComponent (i.e. not including "%") + * @private + */ + +var ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g // eslint-disable-line no-control-regex + +/** + * RegExp to match percent encoding escape. + * @private + */ + +var HEX_ESCAPE_REGEXP = /%[0-9A-Fa-f]{2}/ +var HEX_ESCAPE_REPLACE_REGEXP = /%([0-9A-Fa-f]{2})/g + +/** + * RegExp to match non-latin1 characters. + * @private + */ + +var NON_LATIN1_REGEXP = /[^\x20-\x7e\xa0-\xff]/g + +/** + * RegExp to match quoted-pair in RFC 2616 + * + * quoted-pair = "\" CHAR + * CHAR = + * @private + */ + +var QESC_REGEXP = /\\([\u0000-\u007f])/g // eslint-disable-line no-control-regex + +/** + * RegExp to match chars that must be quoted-pair in RFC 2616 + * @private + */ + +var QUOTE_REGEXP = /([\\"])/g + +/** + * RegExp for various RFC 2616 grammar + * + * parameter = token "=" ( token | quoted-string ) + * token = 1* + * separators = "(" | ")" | "<" | ">" | "@" + * | "," | ";" | ":" | "\" | <"> + * | "/" | "[" | "]" | "?" | "=" + * | "{" | "}" | SP | HT + * quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) + * qdtext = > + * quoted-pair = "\" CHAR + * CHAR = + * TEXT = + * LWS = [CRLF] 1*( SP | HT ) + * CRLF = CR LF + * CR = + * LF = + * SP = + * HT = + * CTL = + * OCTET = + * @private + */ + +var PARAM_REGEXP = /;[\x09\x20]*([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*=[\x09\x20]*("(?:[\x20!\x23-\x5b\x5d-\x7e\x80-\xff]|\\[\x20-\x7e])*"|[!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*/g // eslint-disable-line no-control-regex +var TEXT_REGEXP = /^[\x20-\x7e\x80-\xff]+$/ +var TOKEN_REGEXP = /^[!#$%&'*+.0-9A-Z^_`a-z|~-]+$/ + +/** + * RegExp for various RFC 5987 grammar + * + * ext-value = charset "'" [ language ] "'" value-chars + * charset = "UTF-8" / "ISO-8859-1" / mime-charset + * mime-charset = 1*mime-charsetc + * mime-charsetc = ALPHA / DIGIT + * / "!" / "#" / "$" / "%" / "&" + * / "+" / "-" / "^" / "_" / "`" + * / "{" / "}" / "~" + * language = ( 2*3ALPHA [ extlang ] ) + * / 4ALPHA + * / 5*8ALPHA + * extlang = *3( "-" 3ALPHA ) + * value-chars = *( pct-encoded / attr-char ) + * pct-encoded = "%" HEXDIG HEXDIG + * attr-char = ALPHA / DIGIT + * / "!" / "#" / "$" / "&" / "+" / "-" / "." + * / "^" / "_" / "`" / "|" / "~" + * @private + */ + +var EXT_VALUE_REGEXP = /^([A-Za-z0-9!#$%&+\-^_`{}~]+)'(?:[A-Za-z]{2,3}(?:-[A-Za-z]{3}){0,3}|[A-Za-z]{4,8}|)'((?:%[0-9A-Fa-f]{2}|[A-Za-z0-9!#$&+.^_`|~-])+)$/ + +/** + * RegExp for various RFC 6266 grammar + * + * disposition-type = "inline" | "attachment" | disp-ext-type + * disp-ext-type = token + * disposition-parm = filename-parm | disp-ext-parm + * filename-parm = "filename" "=" value + * | "filename*" "=" ext-value + * disp-ext-parm = token "=" value + * | ext-token "=" ext-value + * ext-token = + * @private + */ + +var DISPOSITION_TYPE_REGEXP = /^([!#$%&'*+.0-9A-Z^_`a-z|~-]+)[\x09\x20]*(?:$|;)/ // eslint-disable-line no-control-regex + +/** + * Create an attachment Content-Disposition header. + * + * @param {string} [filename] + * @param {object} [options] + * @param {string} [options.type=attachment] + * @param {string|boolean} [options.fallback=true] + * @return {string} + * @public + */ + +function contentDisposition (filename, options) { + var opts = options || {} + + // get type + var type = opts.type || 'attachment' + + // get parameters + var params = createparams(filename, opts.fallback) + + // format into string + return format(new ContentDisposition(type, params)) +} + +/** + * Create parameters object from filename and fallback. + * + * @param {string} [filename] + * @param {string|boolean} [fallback=true] + * @return {object} + * @private + */ + +function createparams (filename, fallback) { + if (filename === undefined) { + return + } + + var params = {} + + if (typeof filename !== 'string') { + throw new TypeError('filename must be a string') + } + + // fallback defaults to true + if (fallback === undefined) { + fallback = true + } + + if (typeof fallback !== 'string' && typeof fallback !== 'boolean') { + throw new TypeError('fallback must be a string or boolean') + } + + if (typeof fallback === 'string' && NON_LATIN1_REGEXP.test(fallback)) { + throw new TypeError('fallback must be ISO-8859-1 string') + } + + // restrict to file base name + var name = basename(filename) + + // determine if name is suitable for quoted string + var isQuotedString = TEXT_REGEXP.test(name) + + // generate fallback name + var fallbackName = typeof fallback !== 'string' + ? fallback && getlatin1(name) + : basename(fallback) + var hasFallback = typeof fallbackName === 'string' && fallbackName !== name + + // set extended filename parameter + if (hasFallback || !isQuotedString || HEX_ESCAPE_REGEXP.test(name)) { + params['filename*'] = name + } + + // set filename parameter + if (isQuotedString || hasFallback) { + params.filename = hasFallback + ? fallbackName + : name + } + + return params +} + +/** + * Format object to Content-Disposition header. + * + * @param {object} obj + * @param {string} obj.type + * @param {object} [obj.parameters] + * @return {string} + * @private + */ + +function format (obj) { + var parameters = obj.parameters + var type = obj.type + + if (!type || typeof type !== 'string' || !TOKEN_REGEXP.test(type)) { + throw new TypeError('invalid type') + } + + // start with normalized type + var string = String(type).toLowerCase() + + // append parameters + if (parameters && typeof parameters === 'object') { + var param + var params = Object.keys(parameters).sort() + + for (var i = 0; i < params.length; i++) { + param = params[i] + + var val = param.slice(-1) === '*' + ? ustring(parameters[param]) + : qstring(parameters[param]) + + string += '; ' + param + '=' + val + } + } + + return string +} + +/** + * Decode a RFC 5987 field value (gracefully). + * + * @param {string} str + * @return {string} + * @private + */ + +function decodefield (str) { + var match = EXT_VALUE_REGEXP.exec(str) + + if (!match) { + throw new TypeError('invalid extended field value') + } + + var charset = match[1].toLowerCase() + var encoded = match[2] + var value + + // to binary string + var binary = encoded.replace(HEX_ESCAPE_REPLACE_REGEXP, pdecode) + + switch (charset) { + case 'iso-8859-1': + value = getlatin1(binary) + break + case 'utf-8': + case 'utf8': + value = Buffer.from(binary, 'binary').toString('utf8') + break + default: + throw new TypeError('unsupported charset in extended field') + } + + return value +} + +/** + * Get ISO-8859-1 version of string. + * + * @param {string} val + * @return {string} + * @private + */ + +function getlatin1 (val) { + // simple Unicode -> ISO-8859-1 transformation + return String(val).replace(NON_LATIN1_REGEXP, '?') +} + +/** + * Parse Content-Disposition header string. + * + * @param {string} string + * @return {object} + * @public + */ + +function parse (string) { + if (!string || typeof string !== 'string') { + throw new TypeError('argument string is required') + } + + var match = DISPOSITION_TYPE_REGEXP.exec(string) + + if (!match) { + throw new TypeError('invalid type format') + } + + // normalize type + var index = match[0].length + var type = match[1].toLowerCase() + + var key + var names = [] + var params = {} + var value + + // calculate index to start at + index = PARAM_REGEXP.lastIndex = match[0].slice(-1) === ';' + ? index - 1 + : index + + // match parameters + while ((match = PARAM_REGEXP.exec(string))) { + if (match.index !== index) { + throw new TypeError('invalid parameter format') + } + + index += match[0].length + key = match[1].toLowerCase() + value = match[2] + + if (names.indexOf(key) !== -1) { + throw new TypeError('invalid duplicate parameter') + } + + names.push(key) + + if (key.indexOf('*') + 1 === key.length) { + // decode extended value + key = key.slice(0, -1) + value = decodefield(value) + + // overwrite existing value + params[key] = value + continue + } + + if (typeof params[key] === 'string') { + continue + } + + if (value[0] === '"') { + // remove quotes and escapes + value = value + .slice(1, -1) + .replace(QESC_REGEXP, '$1') + } + + params[key] = value + } + + if (index !== -1 && index !== string.length) { + throw new TypeError('invalid parameter format') + } + + return new ContentDisposition(type, params) +} + +/** + * Percent decode a single character. + * + * @param {string} str + * @param {string} hex + * @return {string} + * @private + */ + +function pdecode (str, hex) { + return String.fromCharCode(parseInt(hex, 16)) +} + +/** + * Percent encode a single character. + * + * @param {string} char + * @return {string} + * @private + */ + +function pencode (char) { + return '%' + String(char) + .charCodeAt(0) + .toString(16) + .toUpperCase() +} + +/** + * Quote a string for HTTP. + * + * @param {string} val + * @return {string} + * @private + */ + +function qstring (val) { + var str = String(val) + + return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"' +} + +/** + * Encode a Unicode string for HTTP (RFC 5987). + * + * @param {string} val + * @return {string} + * @private + */ + +function ustring (val) { + var str = String(val) + + // percent encode as UTF-8 + var encoded = encodeURIComponent(str) + .replace(ENCODE_URL_ATTR_CHAR_REGEXP, pencode) + + return 'UTF-8\'\'' + encoded +} + +/** + * Class for parsed Content-Disposition header for v8 optimization + * + * @public + * @param {string} type + * @param {object} parameters + * @constructor + */ + +function ContentDisposition (type, parameters) { + this.type = type + this.parameters = parameters +} diff --git a/src/node_modules/content-disposition/package.json b/src/node_modules/content-disposition/package.json new file mode 100644 index 0000000..5cea50b --- /dev/null +++ b/src/node_modules/content-disposition/package.json @@ -0,0 +1,44 @@ +{ + "name": "content-disposition", + "description": "Create and parse Content-Disposition header", + "version": "1.0.0", + "author": "Douglas Christopher Wilson ", + "license": "MIT", + "keywords": [ + "content-disposition", + "http", + "rfc6266", + "res" + ], + "repository": "jshttp/content-disposition", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "devDependencies": { + "deep-equal": "1.0.1", + "eslint": "7.32.0", + "eslint-config-standard": "13.0.1", + "eslint-plugin-import": "2.25.3", + "eslint-plugin-markdown": "2.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-promise": "5.2.0", + "eslint-plugin-standard": "4.1.0", + "mocha": "^9.2.2", + "nyc": "15.1.0" + }, + "files": [ + "LICENSE", + "HISTORY.md", + "README.md", + "index.js" + ], + "engines": { + "node": ">= 0.6" + }, + "scripts": { + "lint": "eslint .", + "test": "mocha --reporter spec --bail --check-leaks test/", + "test-ci": "nyc --reporter=lcovonly --reporter=text npm test", + "test-cov": "nyc --reporter=html --reporter=text npm test" + } +} diff --git a/src/node_modules/content-type/HISTORY.md b/src/node_modules/content-type/HISTORY.md new file mode 100644 index 0000000..4583671 --- /dev/null +++ b/src/node_modules/content-type/HISTORY.md @@ -0,0 +1,29 @@ +1.0.5 / 2023-01-29 +================== + + * perf: skip value escaping when unnecessary + +1.0.4 / 2017-09-11 +================== + + * perf: skip parameter parsing when no parameters + +1.0.3 / 2017-09-10 +================== + + * perf: remove argument reassignment + +1.0.2 / 2016-05-09 +================== + + * perf: enable strict mode + +1.0.1 / 2015-02-13 +================== + + * Improve missing `Content-Type` header error message + +1.0.0 / 2015-02-01 +================== + + * Initial implementation, derived from `media-typer@0.3.0` diff --git a/src/node_modules/content-type/LICENSE b/src/node_modules/content-type/LICENSE new file mode 100644 index 0000000..34b1a2d --- /dev/null +++ b/src/node_modules/content-type/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/node_modules/content-type/README.md b/src/node_modules/content-type/README.md new file mode 100644 index 0000000..c1a922a --- /dev/null +++ b/src/node_modules/content-type/README.md @@ -0,0 +1,94 @@ +# content-type + +[![NPM Version][npm-version-image]][npm-url] +[![NPM Downloads][npm-downloads-image]][npm-url] +[![Node.js Version][node-image]][node-url] +[![Build Status][ci-image]][ci-url] +[![Coverage Status][coveralls-image]][coveralls-url] + +Create and parse HTTP Content-Type header according to RFC 7231 + +## Installation + +```sh +$ npm install content-type +``` + +## API + +```js +var contentType = require('content-type') +``` + +### contentType.parse(string) + +```js +var obj = contentType.parse('image/svg+xml; charset=utf-8') +``` + +Parse a `Content-Type` header. This will return an object with the following +properties (examples are shown for the string `'image/svg+xml; charset=utf-8'`): + + - `type`: The media type (the type and subtype, always lower case). + Example: `'image/svg+xml'` + + - `parameters`: An object of the parameters in the media type (name of parameter + always lower case). Example: `{charset: 'utf-8'}` + +Throws a `TypeError` if the string is missing or invalid. + +### contentType.parse(req) + +```js +var obj = contentType.parse(req) +``` + +Parse the `Content-Type` header from the given `req`. Short-cut for +`contentType.parse(req.headers['content-type'])`. + +Throws a `TypeError` if the `Content-Type` header is missing or invalid. + +### contentType.parse(res) + +```js +var obj = contentType.parse(res) +``` + +Parse the `Content-Type` header set on the given `res`. Short-cut for +`contentType.parse(res.getHeader('content-type'))`. + +Throws a `TypeError` if the `Content-Type` header is missing or invalid. + +### contentType.format(obj) + +```js +var str = contentType.format({ + type: 'image/svg+xml', + parameters: { charset: 'utf-8' } +}) +``` + +Format an object into a `Content-Type` header. This will return a string of the +content type for the given object with the following properties (examples are +shown that produce the string `'image/svg+xml; charset=utf-8'`): + + - `type`: The media type (will be lower-cased). Example: `'image/svg+xml'` + + - `parameters`: An object of the parameters in the media type (name of the + parameter will be lower-cased). Example: `{charset: 'utf-8'}` + +Throws a `TypeError` if the object contains an invalid type or parameter names. + +## License + +[MIT](LICENSE) + +[ci-image]: https://badgen.net/github/checks/jshttp/content-type/master?label=ci +[ci-url]: https://github.com/jshttp/content-type/actions/workflows/ci.yml +[coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/content-type/master +[coveralls-url]: https://coveralls.io/r/jshttp/content-type?branch=master +[node-image]: https://badgen.net/npm/node/content-type +[node-url]: https://nodejs.org/en/download +[npm-downloads-image]: https://badgen.net/npm/dm/content-type +[npm-url]: https://npmjs.org/package/content-type +[npm-version-image]: https://badgen.net/npm/v/content-type diff --git a/src/node_modules/content-type/index.js b/src/node_modules/content-type/index.js new file mode 100644 index 0000000..41840e7 --- /dev/null +++ b/src/node_modules/content-type/index.js @@ -0,0 +1,225 @@ +/*! + * content-type + * Copyright(c) 2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * RegExp to match *( ";" parameter ) in RFC 7231 sec 3.1.1.1 + * + * parameter = token "=" ( token / quoted-string ) + * token = 1*tchar + * tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + * / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + * / DIGIT / ALPHA + * ; any VCHAR, except delimiters + * quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE + * qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text + * obs-text = %x80-FF + * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) + */ +var PARAM_REGEXP = /; *([!#$%&'*+.^_`|~0-9A-Za-z-]+) *= *("(?:[\u000b\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u000b\u0020-\u00ff])*"|[!#$%&'*+.^_`|~0-9A-Za-z-]+) */g // eslint-disable-line no-control-regex +var TEXT_REGEXP = /^[\u000b\u0020-\u007e\u0080-\u00ff]+$/ // eslint-disable-line no-control-regex +var TOKEN_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/ + +/** + * RegExp to match quoted-pair in RFC 7230 sec 3.2.6 + * + * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) + * obs-text = %x80-FF + */ +var QESC_REGEXP = /\\([\u000b\u0020-\u00ff])/g // eslint-disable-line no-control-regex + +/** + * RegExp to match chars that must be quoted-pair in RFC 7230 sec 3.2.6 + */ +var QUOTE_REGEXP = /([\\"])/g + +/** + * RegExp to match type in RFC 7231 sec 3.1.1.1 + * + * media-type = type "/" subtype + * type = token + * subtype = token + */ +var TYPE_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/ + +/** + * Module exports. + * @public + */ + +exports.format = format +exports.parse = parse + +/** + * Format object to media type. + * + * @param {object} obj + * @return {string} + * @public + */ + +function format (obj) { + if (!obj || typeof obj !== 'object') { + throw new TypeError('argument obj is required') + } + + var parameters = obj.parameters + var type = obj.type + + if (!type || !TYPE_REGEXP.test(type)) { + throw new TypeError('invalid type') + } + + var string = type + + // append parameters + if (parameters && typeof parameters === 'object') { + var param + var params = Object.keys(parameters).sort() + + for (var i = 0; i < params.length; i++) { + param = params[i] + + if (!TOKEN_REGEXP.test(param)) { + throw new TypeError('invalid parameter name') + } + + string += '; ' + param + '=' + qstring(parameters[param]) + } + } + + return string +} + +/** + * Parse media type to object. + * + * @param {string|object} string + * @return {Object} + * @public + */ + +function parse (string) { + if (!string) { + throw new TypeError('argument string is required') + } + + // support req/res-like objects as argument + var header = typeof string === 'object' + ? getcontenttype(string) + : string + + if (typeof header !== 'string') { + throw new TypeError('argument string is required to be a string') + } + + var index = header.indexOf(';') + var type = index !== -1 + ? header.slice(0, index).trim() + : header.trim() + + if (!TYPE_REGEXP.test(type)) { + throw new TypeError('invalid media type') + } + + var obj = new ContentType(type.toLowerCase()) + + // parse parameters + if (index !== -1) { + var key + var match + var value + + PARAM_REGEXP.lastIndex = index + + while ((match = PARAM_REGEXP.exec(header))) { + if (match.index !== index) { + throw new TypeError('invalid parameter format') + } + + index += match[0].length + key = match[1].toLowerCase() + value = match[2] + + if (value.charCodeAt(0) === 0x22 /* " */) { + // remove quotes + value = value.slice(1, -1) + + // remove escapes + if (value.indexOf('\\') !== -1) { + value = value.replace(QESC_REGEXP, '$1') + } + } + + obj.parameters[key] = value + } + + if (index !== header.length) { + throw new TypeError('invalid parameter format') + } + } + + return obj +} + +/** + * Get content-type from req/res objects. + * + * @param {object} + * @return {Object} + * @private + */ + +function getcontenttype (obj) { + var header + + if (typeof obj.getHeader === 'function') { + // res-like + header = obj.getHeader('content-type') + } else if (typeof obj.headers === 'object') { + // req-like + header = obj.headers && obj.headers['content-type'] + } + + if (typeof header !== 'string') { + throw new TypeError('content-type header is missing from object') + } + + return header +} + +/** + * Quote a string if necessary. + * + * @param {string} val + * @return {string} + * @private + */ + +function qstring (val) { + var str = String(val) + + // no need to quote tokens + if (TOKEN_REGEXP.test(str)) { + return str + } + + if (str.length > 0 && !TEXT_REGEXP.test(str)) { + throw new TypeError('invalid parameter value') + } + + return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"' +} + +/** + * Class to represent a content type. + * @private + */ +function ContentType (type) { + this.parameters = Object.create(null) + this.type = type +} diff --git a/src/node_modules/content-type/package.json b/src/node_modules/content-type/package.json new file mode 100644 index 0000000..9db19f6 --- /dev/null +++ b/src/node_modules/content-type/package.json @@ -0,0 +1,42 @@ +{ + "name": "content-type", + "description": "Create and parse HTTP Content-Type header", + "version": "1.0.5", + "author": "Douglas Christopher Wilson ", + "license": "MIT", + "keywords": [ + "content-type", + "http", + "req", + "res", + "rfc7231" + ], + "repository": "jshttp/content-type", + "devDependencies": { + "deep-equal": "1.0.1", + "eslint": "8.32.0", + "eslint-config-standard": "15.0.1", + "eslint-plugin-import": "2.27.5", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-promise": "6.1.1", + "eslint-plugin-standard": "4.1.0", + "mocha": "10.2.0", + "nyc": "15.1.0" + }, + "files": [ + "LICENSE", + "HISTORY.md", + "README.md", + "index.js" + ], + "engines": { + "node": ">= 0.6" + }, + "scripts": { + "lint": "eslint .", + "test": "mocha --reporter spec --check-leaks --bail test/", + "test-ci": "nyc --reporter=lcovonly --reporter=text npm test", + "test-cov": "nyc --reporter=html --reporter=text npm test", + "version": "node scripts/version-history.js && git add HISTORY.md" + } +} diff --git a/src/node_modules/cookie-signature/History.md b/src/node_modules/cookie-signature/History.md new file mode 100644 index 0000000..479211a --- /dev/null +++ b/src/node_modules/cookie-signature/History.md @@ -0,0 +1,70 @@ +1.2.2 / 2024-10-29 +================== + +* various metadata/documentation tweaks (incl. #51) + + +1.2.1 / 2023-02-27 +================== + +* update annotations for allowed secret key types (#44, thanks @jyasskin!) + + +1.2.0 / 2022-02-17 +================== + +* allow buffer and other node-supported types as key (#33) +* be pickier about extra content after signed portion (#40) +* some internal code clarity/cleanup improvements (#26) + + +1.1.0 / 2018-01-18 +================== + +* switch to built-in `crypto.timingSafeEqual` for validation instead of previous double-hash method (thank you @jodevsa!) + + +1.0.7 / 2023-04-12 +================== + +Later release for older node.js versions. See the [v1.0.x branch notes](https://github.com/tj/node-cookie-signature/blob/v1.0.x/History.md#107--2023-04-12). + + +1.0.6 / 2015-02-03 +================== + +* use `npm test` instead of `make test` to run tests +* clearer assertion messages when checking input + + +1.0.5 / 2014-09-05 +================== + +* add license to package.json + +1.0.4 / 2014-06-25 +================== + + * corrected avoidance of timing attacks (thanks @tenbits!) + +1.0.3 / 2014-01-28 +================== + + * [incorrect] fix for timing attacks + +1.0.2 / 2014-01-28 +================== + + * fix missing repository warning + * fix typo in test + +1.0.1 / 2013-04-15 +================== + + * Revert "Changed underlying HMAC algo. to sha512." + * Revert "Fix for timing attacks on MAC verification." + +0.0.1 / 2010-01-03 +================== + + * Initial release diff --git a/src/node_modules/cookie-signature/LICENSE b/src/node_modules/cookie-signature/LICENSE new file mode 100644 index 0000000..a2671bf --- /dev/null +++ b/src/node_modules/cookie-signature/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2012–2024 LearnBoost and other contributors; + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/node_modules/cookie-signature/Readme.md b/src/node_modules/cookie-signature/Readme.md new file mode 100644 index 0000000..369af15 --- /dev/null +++ b/src/node_modules/cookie-signature/Readme.md @@ -0,0 +1,23 @@ + +# cookie-signature + + Sign and unsign cookies. + +## Example + +```js +var cookie = require('cookie-signature'); + +var val = cookie.sign('hello', 'tobiiscool'); +val.should.equal('hello.DGDUkGlIkCzPz+C0B064FNgHdEjox7ch8tOBGslZ5QI'); + +var val = cookie.sign('hello', 'tobiiscool'); +cookie.unsign(val, 'tobiiscool').should.equal('hello'); +cookie.unsign(val, 'luna').should.be.false; +``` + +## License + +MIT. + +See LICENSE file for details. diff --git a/src/node_modules/cookie-signature/index.js b/src/node_modules/cookie-signature/index.js new file mode 100644 index 0000000..3fbbddb --- /dev/null +++ b/src/node_modules/cookie-signature/index.js @@ -0,0 +1,47 @@ +/** + * Module dependencies. + */ + +var crypto = require('crypto'); + +/** + * Sign the given `val` with `secret`. + * + * @param {String} val + * @param {String|NodeJS.ArrayBufferView|crypto.KeyObject} secret + * @return {String} + * @api private + */ + +exports.sign = function(val, secret){ + if ('string' != typeof val) throw new TypeError("Cookie value must be provided as a string."); + if (null == secret) throw new TypeError("Secret key must be provided."); + return val + '.' + crypto + .createHmac('sha256', secret) + .update(val) + .digest('base64') + .replace(/\=+$/, ''); +}; + +/** + * Unsign and decode the given `input` with `secret`, + * returning `false` if the signature is invalid. + * + * @param {String} input + * @param {String|NodeJS.ArrayBufferView|crypto.KeyObject} secret + * @return {String|Boolean} + * @api private + */ + +exports.unsign = function(input, secret){ + if ('string' != typeof input) throw new TypeError("Signed cookie string must be provided."); + if (null == secret) throw new TypeError("Secret key must be provided."); + var tentativeValue = input.slice(0, input.lastIndexOf('.')), + expectedInput = exports.sign(tentativeValue, secret), + expectedBuffer = Buffer.from(expectedInput), + inputBuffer = Buffer.from(input); + return ( + expectedBuffer.length === inputBuffer.length && + crypto.timingSafeEqual(expectedBuffer, inputBuffer) + ) ? tentativeValue : false; +}; diff --git a/src/node_modules/cookie-signature/package.json b/src/node_modules/cookie-signature/package.json new file mode 100644 index 0000000..a160040 --- /dev/null +++ b/src/node_modules/cookie-signature/package.json @@ -0,0 +1,24 @@ +{ + "name": "cookie-signature", + "version": "1.2.2", + "main": "index.js", + "description": "Sign and unsign cookies", + "keywords": ["cookie", "sign", "unsign"], + "author": "TJ Holowaychuk ", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/visionmedia/node-cookie-signature.git" + }, + "dependencies": {}, + "engines": { + "node": ">=6.6.0" + }, + "devDependencies": { + "mocha": "*", + "should": "*" + }, + "scripts": { + "test": "mocha --require should --reporter spec" + } +} diff --git a/src/node_modules/cookie/LICENSE b/src/node_modules/cookie/LICENSE new file mode 100644 index 0000000..058b6b4 --- /dev/null +++ b/src/node_modules/cookie/LICENSE @@ -0,0 +1,24 @@ +(The MIT License) + +Copyright (c) 2012-2014 Roman Shtylman +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/src/node_modules/cookie/README.md b/src/node_modules/cookie/README.md new file mode 100644 index 0000000..71fdac1 --- /dev/null +++ b/src/node_modules/cookie/README.md @@ -0,0 +1,317 @@ +# cookie + +[![NPM Version][npm-version-image]][npm-url] +[![NPM Downloads][npm-downloads-image]][npm-url] +[![Node.js Version][node-image]][node-url] +[![Build Status][ci-image]][ci-url] +[![Coverage Status][coveralls-image]][coveralls-url] + +Basic HTTP cookie parser and serializer for HTTP servers. + +## Installation + +This is a [Node.js](https://nodejs.org/en/) module available through the +[npm registry](https://www.npmjs.com/). Installation is done using the +[`npm install` command](https://docs.npmjs.com/getting-started/installing-npm-packages-locally): + +```sh +$ npm install cookie +``` + +## API + +```js +var cookie = require('cookie'); +``` + +### cookie.parse(str, options) + +Parse an HTTP `Cookie` header string and returning an object of all cookie name-value pairs. +The `str` argument is the string representing a `Cookie` header value and `options` is an +optional object containing additional parsing options. + +```js +var cookies = cookie.parse('foo=bar; equation=E%3Dmc%5E2'); +// { foo: 'bar', equation: 'E=mc^2' } +``` + +#### Options + +`cookie.parse` accepts these properties in the options object. + +##### decode + +Specifies a function that will be used to decode a cookie's value. Since the value of a cookie +has a limited character set (and must be a simple string), this function can be used to decode +a previously-encoded cookie value into a JavaScript string or other object. + +The default function is the global `decodeURIComponent`, which will decode any URL-encoded +sequences into their byte representations. + +**note** if an error is thrown from this function, the original, non-decoded cookie value will +be returned as the cookie's value. + +### cookie.serialize(name, value, options) + +Serialize a cookie name-value pair into a `Set-Cookie` header string. The `name` argument is the +name for the cookie, the `value` argument is the value to set the cookie to, and the `options` +argument is an optional object containing additional serialization options. + +```js +var setCookie = cookie.serialize('foo', 'bar'); +// foo=bar +``` + +#### Options + +`cookie.serialize` accepts these properties in the options object. + +##### domain + +Specifies the value for the [`Domain` `Set-Cookie` attribute][rfc-6265-5.2.3]. By default, no +domain is set, and most clients will consider the cookie to apply to only the current domain. + +##### encode + +Specifies a function that will be used to encode a cookie's value. Since value of a cookie +has a limited character set (and must be a simple string), this function can be used to encode +a value into a string suited for a cookie's value. + +The default function is the global `encodeURIComponent`, which will encode a JavaScript string +into UTF-8 byte sequences and then URL-encode any that fall outside of the cookie range. + +##### expires + +Specifies the `Date` object to be the value for the [`Expires` `Set-Cookie` attribute][rfc-6265-5.2.1]. +By default, no expiration is set, and most clients will consider this a "non-persistent cookie" and +will delete it on a condition like exiting a web browser application. + +**note** the [cookie storage model specification][rfc-6265-5.3] states that if both `expires` and +`maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this, +so if both are set, they should point to the same date and time. + +##### httpOnly + +Specifies the `boolean` value for the [`HttpOnly` `Set-Cookie` attribute][rfc-6265-5.2.6]. When truthy, +the `HttpOnly` attribute is set, otherwise it is not. By default, the `HttpOnly` attribute is not set. + +**note** be careful when setting this to `true`, as compliant clients will not allow client-side +JavaScript to see the cookie in `document.cookie`. + +##### maxAge + +Specifies the `number` (in seconds) to be the value for the [`Max-Age` `Set-Cookie` attribute][rfc-6265-5.2.2]. +The given number will be converted to an integer by rounding down. By default, no maximum age is set. + +**note** the [cookie storage model specification][rfc-6265-5.3] states that if both `expires` and +`maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this, +so if both are set, they should point to the same date and time. + +##### partitioned + +Specifies the `boolean` value for the [`Partitioned` `Set-Cookie`](rfc-cutler-httpbis-partitioned-cookies) +attribute. When truthy, the `Partitioned` attribute is set, otherwise it is not. By default, the +`Partitioned` attribute is not set. + +**note** This is an attribute that has not yet been fully standardized, and may change in the future. +This also means many clients may ignore this attribute until they understand it. + +More information about can be found in [the proposal](https://github.com/privacycg/CHIPS). + +##### path + +Specifies the value for the [`Path` `Set-Cookie` attribute][rfc-6265-5.2.4]. By default, the path +is considered the ["default path"][rfc-6265-5.1.4]. + +##### priority + +Specifies the `string` to be the value for the [`Priority` `Set-Cookie` attribute][rfc-west-cookie-priority-00-4.1]. + + - `'low'` will set the `Priority` attribute to `Low`. + - `'medium'` will set the `Priority` attribute to `Medium`, the default priority when not set. + - `'high'` will set the `Priority` attribute to `High`. + +More information about the different priority levels can be found in +[the specification][rfc-west-cookie-priority-00-4.1]. + +**note** This is an attribute that has not yet been fully standardized, and may change in the future. +This also means many clients may ignore this attribute until they understand it. + +##### sameSite + +Specifies the `boolean` or `string` to be the value for the [`SameSite` `Set-Cookie` attribute][rfc-6265bis-09-5.4.7]. + + - `true` will set the `SameSite` attribute to `Strict` for strict same site enforcement. + - `false` will not set the `SameSite` attribute. + - `'lax'` will set the `SameSite` attribute to `Lax` for lax same site enforcement. + - `'none'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie. + - `'strict'` will set the `SameSite` attribute to `Strict` for strict same site enforcement. + +More information about the different enforcement levels can be found in +[the specification][rfc-6265bis-09-5.4.7]. + +**note** This is an attribute that has not yet been fully standardized, and may change in the future. +This also means many clients may ignore this attribute until they understand it. + +##### secure + +Specifies the `boolean` value for the [`Secure` `Set-Cookie` attribute][rfc-6265-5.2.5]. When truthy, +the `Secure` attribute is set, otherwise it is not. By default, the `Secure` attribute is not set. + +**note** be careful when setting this to `true`, as compliant clients will not send the cookie back to +the server in the future if the browser does not have an HTTPS connection. + +## Example + +The following example uses this module in conjunction with the Node.js core HTTP server +to prompt a user for their name and display it back on future visits. + +```js +var cookie = require('cookie'); +var escapeHtml = require('escape-html'); +var http = require('http'); +var url = require('url'); + +function onRequest(req, res) { + // Parse the query string + var query = url.parse(req.url, true, true).query; + + if (query && query.name) { + // Set a new cookie with the name + res.setHeader('Set-Cookie', cookie.serialize('name', String(query.name), { + httpOnly: true, + maxAge: 60 * 60 * 24 * 7 // 1 week + })); + + // Redirect back after setting cookie + res.statusCode = 302; + res.setHeader('Location', req.headers.referer || '/'); + res.end(); + return; + } + + // Parse the cookies on the request + var cookies = cookie.parse(req.headers.cookie || ''); + + // Get the visitor name set in the cookie + var name = cookies.name; + + res.setHeader('Content-Type', 'text/html; charset=UTF-8'); + + if (name) { + res.write('

Welcome back, ' + escapeHtml(name) + '!

'); + } else { + res.write('

Hello, new visitor!

'); + } + + res.write('
'); + res.write(' '); + res.end('
'); +} + +http.createServer(onRequest).listen(3000); +``` + +## Testing + +```sh +$ npm test +``` + +## Benchmark + +``` +$ npm run bench + +> cookie@0.5.0 bench +> node benchmark/index.js + + node@18.18.2 + acorn@8.10.0 + ada@2.6.0 + ares@1.19.1 + brotli@1.0.9 + cldr@43.1 + icu@73.2 + llhttp@6.0.11 + modules@108 + napi@9 + nghttp2@1.57.0 + nghttp3@0.7.0 + ngtcp2@0.8.1 + openssl@3.0.10+quic + simdutf@3.2.14 + tz@2023c + undici@5.26.3 + unicode@15.0 + uv@1.44.2 + uvwasi@0.0.18 + v8@10.2.154.26-node.26 + zlib@1.2.13.1-motley + +> node benchmark/parse-top.js + + cookie.parse - top sites + + 14 tests completed. + + parse accounts.google.com x 2,588,913 ops/sec ±0.74% (186 runs sampled) + parse apple.com x 2,370,002 ops/sec ±0.69% (186 runs sampled) + parse cloudflare.com x 2,213,102 ops/sec ±0.88% (188 runs sampled) + parse docs.google.com x 2,194,157 ops/sec ±1.03% (184 runs sampled) + parse drive.google.com x 2,265,084 ops/sec ±0.79% (187 runs sampled) + parse en.wikipedia.org x 457,099 ops/sec ±0.81% (186 runs sampled) + parse linkedin.com x 504,407 ops/sec ±0.89% (186 runs sampled) + parse maps.google.com x 1,230,959 ops/sec ±0.98% (186 runs sampled) + parse microsoft.com x 926,294 ops/sec ±0.88% (184 runs sampled) + parse play.google.com x 2,311,338 ops/sec ±0.83% (185 runs sampled) + parse support.google.com x 1,508,850 ops/sec ±0.86% (186 runs sampled) + parse www.google.com x 1,022,582 ops/sec ±1.32% (182 runs sampled) + parse youtu.be x 332,136 ops/sec ±1.02% (185 runs sampled) + parse youtube.com x 323,833 ops/sec ±0.77% (183 runs sampled) + +> node benchmark/parse.js + + cookie.parse - generic + + 6 tests completed. + + simple x 3,214,032 ops/sec ±1.61% (183 runs sampled) + decode x 587,237 ops/sec ±1.16% (187 runs sampled) + unquote x 2,954,618 ops/sec ±1.35% (183 runs sampled) + duplicates x 857,008 ops/sec ±0.89% (187 runs sampled) + 10 cookies x 292,133 ops/sec ±0.89% (187 runs sampled) + 100 cookies x 22,610 ops/sec ±0.68% (187 runs sampled) +``` + +## References + +- [RFC 6265: HTTP State Management Mechanism][rfc-6265] +- [Same-site Cookies][rfc-6265bis-09-5.4.7] + +[rfc-cutler-httpbis-partitioned-cookies]: https://tools.ietf.org/html/draft-cutler-httpbis-partitioned-cookies/ +[rfc-west-cookie-priority-00-4.1]: https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1 +[rfc-6265bis-09-5.4.7]: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7 +[rfc-6265]: https://tools.ietf.org/html/rfc6265 +[rfc-6265-5.1.4]: https://tools.ietf.org/html/rfc6265#section-5.1.4 +[rfc-6265-5.2.1]: https://tools.ietf.org/html/rfc6265#section-5.2.1 +[rfc-6265-5.2.2]: https://tools.ietf.org/html/rfc6265#section-5.2.2 +[rfc-6265-5.2.3]: https://tools.ietf.org/html/rfc6265#section-5.2.3 +[rfc-6265-5.2.4]: https://tools.ietf.org/html/rfc6265#section-5.2.4 +[rfc-6265-5.2.5]: https://tools.ietf.org/html/rfc6265#section-5.2.5 +[rfc-6265-5.2.6]: https://tools.ietf.org/html/rfc6265#section-5.2.6 +[rfc-6265-5.3]: https://tools.ietf.org/html/rfc6265#section-5.3 + +## License + +[MIT](LICENSE) + +[ci-image]: https://badgen.net/github/checks/jshttp/cookie/master?label=ci +[ci-url]: https://github.com/jshttp/cookie/actions/workflows/ci.yml +[coveralls-image]: https://badgen.net/coveralls/c/github/jshttp/cookie/master +[coveralls-url]: https://coveralls.io/r/jshttp/cookie?branch=master +[node-image]: https://badgen.net/npm/node/cookie +[node-url]: https://nodejs.org/en/download +[npm-downloads-image]: https://badgen.net/npm/dm/cookie +[npm-url]: https://npmjs.org/package/cookie +[npm-version-image]: https://badgen.net/npm/v/cookie diff --git a/src/node_modules/cookie/SECURITY.md b/src/node_modules/cookie/SECURITY.md new file mode 100644 index 0000000..fd4a6c5 --- /dev/null +++ b/src/node_modules/cookie/SECURITY.md @@ -0,0 +1,25 @@ +# Security Policies and Procedures + +## Reporting a Bug + +The `cookie` team and community take all security bugs seriously. Thank +you for improving the security of the project. We appreciate your efforts and +responsible disclosure and will make every effort to acknowledge your +contributions. + +Report security bugs by emailing the current owner(s) of `cookie`. This +information can be found in the npm registry using the command +`npm owner ls cookie`. +If unsure or unable to get the information from the above, open an issue +in the [project issue tracker](https://github.com/jshttp/cookie/issues) +asking for the current contact information. + +To ensure the timely response to your report, please ensure that the entirety +of the report is contained within the email body and not solely behind a web +link or an attachment. + +At least one owner will acknowledge your email within 48 hours, and will send a +more detailed response within 48 hours indicating the next steps in handling +your report. After the initial reply to your report, the owners will +endeavor to keep you informed of the progress towards a fix and full +announcement, and may ask for additional information or guidance. diff --git a/src/node_modules/cookie/index.js b/src/node_modules/cookie/index.js new file mode 100644 index 0000000..acd5acd --- /dev/null +++ b/src/node_modules/cookie/index.js @@ -0,0 +1,335 @@ +/*! + * cookie + * Copyright(c) 2012-2014 Roman Shtylman + * Copyright(c) 2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict'; + +/** + * Module exports. + * @public + */ + +exports.parse = parse; +exports.serialize = serialize; + +/** + * Module variables. + * @private + */ + +var __toString = Object.prototype.toString +var __hasOwnProperty = Object.prototype.hasOwnProperty + +/** + * RegExp to match cookie-name in RFC 6265 sec 4.1.1 + * This refers out to the obsoleted definition of token in RFC 2616 sec 2.2 + * which has been replaced by the token definition in RFC 7230 appendix B. + * + * cookie-name = token + * token = 1*tchar + * tchar = "!" / "#" / "$" / "%" / "&" / "'" / + * "*" / "+" / "-" / "." / "^" / "_" / + * "`" / "|" / "~" / DIGIT / ALPHA + */ + +var cookieNameRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; + +/** + * RegExp to match cookie-value in RFC 6265 sec 4.1.1 + * + * cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) + * cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E + * ; US-ASCII characters excluding CTLs, + * ; whitespace DQUOTE, comma, semicolon, + * ; and backslash + */ + +var cookieValueRegExp = /^("?)[\u0021\u0023-\u002B\u002D-\u003A\u003C-\u005B\u005D-\u007E]*\1$/; + +/** + * RegExp to match domain-value in RFC 6265 sec 4.1.1 + * + * domain-value = + * ; defined in [RFC1034], Section 3.5, as + * ; enhanced by [RFC1123], Section 2.1 + * =