forked from pu8crm6xf/analysiscode
parent
0cc3e08576
commit
8422330cbf
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,8 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/src.iml" filepath="$PROJECT_DIR$/.idea/src.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.11" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="format" value="PLAIN" />
|
||||
<option name="myDocStringFormat" value="Plain" />
|
||||
</component>
|
||||
<component name="TestRunnerService">
|
||||
<option name="PROJECT_TEST_RUNNER" value="py.test" />
|
||||
</component>
|
||||
</module>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/bandit" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/flake8" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/pylint" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1 @@
|
||||
* @ericwb @lukehinds @sigmavirus24
|
||||
@ -0,0 +1,3 @@
|
||||
custom: ["https://psfmember.org/civicrm/contribute/transact/?reset=1&id=42"]
|
||||
github: [ericwb]
|
||||
tidelift: pypi/bandit
|
||||
@ -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 👍.
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -0,0 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@ -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}
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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/
|
||||
@ -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] }}
|
||||
@ -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
|
||||
@ -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/)
|
||||
@ -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
|
||||
@ -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
|
||||
@ -0,0 +1,4 @@
|
||||
[DEFAULT]
|
||||
test_path=./tests
|
||||
top_dir=./
|
||||
parallel_class=True
|
||||
@ -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 <graffatcolmingov@gmail.com>, Ian Lee <IanLee1521@gmail.com>
|
||||
or Florian Bruhin <me@the-compiler.org>. 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.
|
||||
@ -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/<your username>/bandit.git
|
||||
```
|
||||
|
||||
Create you own branch to start writing code:
|
||||
```shell script
|
||||
git switch -c mybranch
|
||||
<create local changes>
|
||||
git add <changed files>
|
||||
git commit -S
|
||||
<create a good commit message>
|
||||
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: <version> 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 `<version>`
|
||||
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 `<version>`
|
||||
5. Click on `Run workflow`
|
||||
@ -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.
|
||||
@ -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::
|
||||
|
||||
[](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=<architecture> 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/<version> \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com
|
||||
|
||||
Where `<version>` 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.
|
||||
@ -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.
|
||||
|
After Width: | Height: | Size: 1.4 MiB |
@ -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")
|
||||
@ -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()
|
||||
@ -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}
|
||||
@ -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}
|
||||
@ -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,
|
||||
}
|
||||
@ -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()
|
||||
@ -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())
|
||||
@ -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()
|
||||
@ -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
|
||||
@ -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)
|
||||
@ -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,
|
||||
)
|
||||
@ -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",
|
||||
)
|
||||
@ -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:
|
||||
<Context {'node': <_ast.Call object at 0x110252510>, '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"<Context {self._context}>"
|
||||
|
||||
@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")
|
||||
@ -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
|
||||
@ -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()
|
||||
@ -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 == "<stdin>":
|
||||
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 == "<stdin>":
|
||||
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
|
||||
@ -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<tests>[^#]+)?#?")
|
||||
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 <N>
|
||||
(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 = [
|
||||
"<stdin>" if x == "-" else x for x in new_files_list
|
||||
]
|
||||
self._parse_file("<stdin>", 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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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])
|
||||
@ -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 []
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -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)
|
||||
@ -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 <N>
|
||||
(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)
|
||||
@ -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
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<title>
|
||||
Bandit Report
|
||||
</title>
|
||||
|
||||
<style>
|
||||
|
||||
html * {
|
||||
font-family: "Arial", sans-serif;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: "Monaco", monospace;
|
||||
}
|
||||
|
||||
.bordered-box {
|
||||
border: 1px solid black;
|
||||
padding-top:.5em;
|
||||
padding-bottom:.5em;
|
||||
padding-left:1em;
|
||||
}
|
||||
|
||||
.metrics-box {
|
||||
font-size: 1.1em;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
.metrics-title {
|
||||
font-size: 1.5em;
|
||||
font-weight: 500;
|
||||
margin-bottom: .25em;
|
||||
}
|
||||
|
||||
.issue-description {
|
||||
font-size: 1.3em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.candidate-issues {
|
||||
margin-left: 2em;
|
||||
border-left: solid 1px; LightGray;
|
||||
padding-left: 5%;
|
||||
margin-top: .2em;
|
||||
margin-bottom: .2em;
|
||||
}
|
||||
|
||||
.issue-block {
|
||||
border: 1px solid LightGray;
|
||||
padding-left: .5em;
|
||||
padding-top: .5em;
|
||||
padding-bottom: .5em;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
.issue-sev-high {
|
||||
background-color: Pink;
|
||||
}
|
||||
|
||||
.issue-sev-medium {
|
||||
background-color: NavajoWhite;
|
||||
}
|
||||
|
||||
.issue-sev-low {
|
||||
background-color: LightCyan;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="metrics">
|
||||
<div class="metrics-box bordered-box">
|
||||
<div class="metrics-title">
|
||||
Metrics:<br>
|
||||
</div>
|
||||
Total lines of code: <span id="loc">9</span><br>
|
||||
Total lines skipped (#nosec): <span id="nosec">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<br>
|
||||
<div id="results">
|
||||
|
||||
<div id="issue-0">
|
||||
<div class="issue-block issue-sev-medium">
|
||||
<b>yaml_load: </b> Use of unsafe yaml load. Allows
|
||||
instantiation of arbitrary objects. Consider yaml.safe_load().<br>
|
||||
<b>Test ID:</b> B506<br>
|
||||
<b>Severity: </b>MEDIUM<br>
|
||||
<b>Confidence: </b>HIGH<br>
|
||||
<b>CWE: </b>CWE-20 (https://cwe.mitre.org/data/definitions/20.html)<br>
|
||||
<b>File: </b><a href="examples/yaml_load.py"
|
||||
target="_blank">examples/yaml_load.py</a> <br>
|
||||
<b>More info: </b><a href="https://bandit.readthedocs.io/en/latest/
|
||||
plugins/yaml_load.html" target="_blank">
|
||||
https://bandit.readthedocs.io/en/latest/plugins/yaml_load.html</a>
|
||||
<br>
|
||||
|
||||
<div class="code">
|
||||
<pre>
|
||||
5 ystr = yaml.dump({'a' : 1, 'b' : 2, 'c' : 3})
|
||||
6 y = yaml.load(ystr)
|
||||
7 yaml.dump(y)
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
.. 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 = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<title>
|
||||
Bandit Report
|
||||
</title>
|
||||
|
||||
<style>
|
||||
|
||||
html * {
|
||||
font-family: "Arial", sans-serif;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: "Monaco", monospace;
|
||||
}
|
||||
|
||||
.bordered-box {
|
||||
border: 1px solid black;
|
||||
padding-top:.5em;
|
||||
padding-bottom:.5em;
|
||||
padding-left:1em;
|
||||
}
|
||||
|
||||
.metrics-box {
|
||||
font-size: 1.1em;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
.metrics-title {
|
||||
font-size: 1.5em;
|
||||
font-weight: 500;
|
||||
margin-bottom: .25em;
|
||||
}
|
||||
|
||||
.issue-description {
|
||||
font-size: 1.3em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.candidate-issues {
|
||||
margin-left: 2em;
|
||||
border-left: solid 1px; LightGray;
|
||||
padding-left: 5%;
|
||||
margin-top: .2em;
|
||||
margin-bottom: .2em;
|
||||
}
|
||||
|
||||
.issue-block {
|
||||
border: 1px solid LightGray;
|
||||
padding-left: .5em;
|
||||
padding-top: .5em;
|
||||
padding-bottom: .5em;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
.issue-sev-high {
|
||||
background-color: Pink;
|
||||
}
|
||||
|
||||
.issue-sev-medium {
|
||||
background-color: NavajoWhite;
|
||||
}
|
||||
|
||||
.issue-sev-low {
|
||||
background-color: LightCyan;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
"""
|
||||
|
||||
report_block = """
|
||||
<body>
|
||||
{metrics}
|
||||
{skipped}
|
||||
|
||||
<br>
|
||||
<div id="results">
|
||||
{results}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
issue_block = """
|
||||
<div id="issue-{issue_no}">
|
||||
<div class="issue-block {issue_class}">
|
||||
<b>{test_name}: </b> {test_text}<br>
|
||||
<b>Test ID:</b> {test_id}<br>
|
||||
<b>Severity: </b>{severity}<br>
|
||||
<b>Confidence: </b>{confidence}<br>
|
||||
<b>CWE: </b><a href="{cwe_link}" target="_blank">CWE-{cwe.id}</a><br>
|
||||
<b>File: </b><a href="{path}" target="_blank">{path}</a><br>
|
||||
<b>Line number: </b>{line_number}<br>
|
||||
<b>More info: </b><a href="{url}" target="_blank">{url}</a><br>
|
||||
{code}
|
||||
{candidates}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
code_block = """
|
||||
<div class="code">
|
||||
<pre>
|
||||
{code}
|
||||
</pre>
|
||||
</div>
|
||||
"""
|
||||
|
||||
candidate_block = """
|
||||
<div class="candidates">
|
||||
<br>
|
||||
<b>Candidates: </b>
|
||||
{candidate_list}
|
||||
</div>
|
||||
"""
|
||||
|
||||
candidate_issue = """
|
||||
<div class="candidate">
|
||||
<div class="candidate-issues">
|
||||
<pre>{code}</pre>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
skipped_block = """
|
||||
<br>
|
||||
<div id="skipped">
|
||||
<div class="bordered-box">
|
||||
<b>Skipped files:</b><br><br>
|
||||
{files_list}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
metrics_block = """
|
||||
<div id="metrics">
|
||||
<div class="metrics-box bordered-box">
|
||||
<div class="metrics-title">
|
||||
Metrics:<br>
|
||||
</div>
|
||||
Total lines of code: <span id="loc">{loc}</span><br>
|
||||
Total lines skipped (#nosec): <span id="nosec">{nosec}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
"""
|
||||
|
||||
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} <b>reason:</b> {reason}<br>"
|
||||
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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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()
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -0,0 +1,97 @@
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
r"""
|
||||
=============
|
||||
XML Formatter
|
||||
=============
|
||||
|
||||
This formatter outputs the issues as XML.
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<testsuite name="bandit" tests="1"><testcase
|
||||
classname="examples/yaml_load.py" name="blacklist_calls"><error
|
||||
message="Use of unsafe yaml load. Allows instantiation of arbitrary
|
||||
objects. Consider yaml.safe_load(). " type="MEDIUM"
|
||||
more_info="https://bandit.readthedocs.io/en/latest/">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</error></testcase></testsuite>
|
||||
|
||||
.. 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)
|
||||
@ -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)
|
||||
@ -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"),
|
||||
)
|
||||
@ -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."
|
||||
),
|
||||
)
|
||||
@ -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"),
|
||||
)
|
||||
@ -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,
|
||||
)
|
||||
@ -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)
|
||||
@ -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()
|
||||
@ -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),
|
||||
)
|
||||
@ -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.",
|
||||
)
|
||||
@ -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)
|
||||
@ -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.",
|
||||
)
|
||||
@ -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)
|
||||
@ -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),
|
||||
)
|
||||
@ -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,
|
||||
)
|
||||
@ -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",
|
||||
)
|
||||
@ -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.",
|
||||
)
|
||||
@ -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"),
|
||||
)
|
||||
@ -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"),
|
||||
)
|
||||
@ -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://www.owasp.org/index.php/Cross-site_Scripting_(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.",
|
||||
)
|
||||
@ -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.",
|
||||
)
|
||||
@ -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://owasp.org/www-community/attacks/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 }.",
|
||||
)
|
||||
@ -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 = "<script>alert('Hello, world!')</script>"
|
||||
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.",
|
||||
)
|
||||
@ -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"),
|
||||
)
|
||||
@ -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",
|
||||
)
|
||||
@ -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"),
|
||||
)
|
||||
@ -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"
|
||||
),
|
||||
)
|
||||
@ -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)
|
||||
@ -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."),
|
||||
)
|
||||
@ -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."),
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue