feat(project-d): implement real video generation pipeline with FFmpeg
This commit is contained in:
Binary file not shown.
@@ -0,0 +1 @@
|
||||
version = "26.5.1"
|
||||
@@ -0,0 +1 @@
|
||||
version: str
|
||||
@@ -0,0 +1,33 @@
|
||||
# This is a stub package designed to roughly emulate the _yaml
|
||||
# extension module, which previously existed as a standalone module
|
||||
# and has been moved into the `yaml` package namespace.
|
||||
# It does not perfectly mimic its old counterpart, but should get
|
||||
# close enough for anyone who's relying on it even when they shouldn't.
|
||||
import yaml
|
||||
|
||||
# in some circumstances, the yaml module we imoprted may be from a different version, so we need
|
||||
# to tread carefully when poking at it here (it may not have the attributes we expect)
|
||||
if not getattr(yaml, '__with_libyaml__', False):
|
||||
from sys import version_info
|
||||
|
||||
exc = ModuleNotFoundError if version_info >= (3, 6) else ImportError
|
||||
raise exc("No module named '_yaml'")
|
||||
else:
|
||||
from yaml._yaml import *
|
||||
import warnings
|
||||
warnings.warn(
|
||||
'The _yaml extension module is now located at yaml._yaml'
|
||||
' and its location is subject to change. To use the'
|
||||
' LibYAML-based parser and emitter, import from `yaml`:'
|
||||
' `from yaml import CLoader as Loader, CDumper as Dumper`.',
|
||||
DeprecationWarning
|
||||
)
|
||||
del warnings
|
||||
# Don't `del yaml` here because yaml is actually an existing
|
||||
# namespace member of _yaml.
|
||||
|
||||
__name__ = '_yaml'
|
||||
# If the module is top-level (i.e. not a part of any specific package)
|
||||
# then the attribute should be set to ''.
|
||||
# https://docs.python.org/3.8/library/types.html
|
||||
__package__ = ''
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,32 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: ast_serialize
|
||||
Version: 0.5.0
|
||||
Classifier: Development Status :: 3 - Alpha
|
||||
Classifier: Environment :: Console
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Programming Language :: Python :: 3.13
|
||||
Classifier: Programming Language :: Python :: 3.14
|
||||
Classifier: Programming Language :: Rust
|
||||
Classifier: Topic :: Software Development
|
||||
Classifier: Typing :: Typed
|
||||
License-File: LICENSE
|
||||
Summary: Python bindings for mypy AST serialization
|
||||
Author-email: Jukka Lehtosalo <jukka.lehtosalo@iki.fi>
|
||||
License-Expression: MIT
|
||||
Requires-Python: >=3.7
|
||||
Description-Content-Type: text/x-rst
|
||||
Project-URL: Homepage, https://github.com/mypyc/ast_serialize
|
||||
Project-URL: Issues, https://github.com/mypyc/ast_serialize/issues
|
||||
|
||||
ast_serialize - Python Parser and AST Serializer
|
||||
================================================
|
||||
|
||||
This is a fast Python extension for parsing Python files and serializing the AST using
|
||||
the native binary format used by mypy. This will eventually replace the current mypy parser,
|
||||
which uses the Python stdlib ``ast`` module for parsing.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
ast_serialize-0.5.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
ast_serialize-0.5.0.dist-info/METADATA,sha256=vCIudy2LOd78i2MWSTIrHu5aj3uXFgiZbewirjps7iA,1330
|
||||
ast_serialize-0.5.0.dist-info/RECORD,,
|
||||
ast_serialize-0.5.0.dist-info/WHEEL,sha256=dB50znGoqD95tHNeV5UBZvlfLNwgEh0iQcswp5YahQ4,95
|
||||
ast_serialize-0.5.0.dist-info/licenses/LICENSE,sha256=6U831a7a7DlRSKe_D8DriXWBrLZ4cvvBzmOxOxSXfoE,1215
|
||||
ast_serialize-0.5.0.dist-info/sboms/mypy_parser.cyclonedx.json,sha256=NNB38MV88xY0TuKR_-WwtZ7LMiqA79MBlIWxkC5w5ik,97579
|
||||
ast_serialize/__init__.py,sha256=mXw0A2FKxBSTePe9oon42XOGM80x6EzbnNJWoTn_Cac,135
|
||||
ast_serialize/__init__.pyi,sha256=sWFxwtsspKAFBp1EUeJB_caFlTEvPFsrbYKiCjwUJd8,939
|
||||
ast_serialize/__pycache__/__init__.cpython-312.pyc,,
|
||||
ast_serialize/ast_serialize.pyd,sha256=F1S0YFYW3axVSlmdKOaRqvEXwYkK-CSqlT3x9DEv4xQ,2462208
|
||||
ast_serialize/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
@@ -0,0 +1,4 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: maturin (1.13.3)
|
||||
Root-Is-Purelib: false
|
||||
Tag: cp39-abi3-win_amd64
|
||||
@@ -0,0 +1,25 @@
|
||||
ast_serialize is licensed under the terms of the MIT license, reproduced below.
|
||||
|
||||
= = = = =
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2025-2026 Jukka Lehtosalo and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
+3099
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
from .ast_serialize import *
|
||||
|
||||
__doc__ = ast_serialize.__doc__
|
||||
if hasattr(ast_serialize, "__all__"):
|
||||
__all__ = ast_serialize.__all__
|
||||
@@ -0,0 +1,34 @@
|
||||
from typing import TypedDict, type_check_only
|
||||
from typing_extensions import NotRequired, TypeAlias
|
||||
|
||||
__all__ = ["parse"]
|
||||
|
||||
_TypeIgnores: TypeAlias = list[tuple[int, list[str]]]
|
||||
|
||||
@type_check_only
|
||||
class ParseError(TypedDict):
|
||||
line: int
|
||||
column: int
|
||||
message: str
|
||||
blocker: NotRequired[bool]
|
||||
code: NotRequired[str]
|
||||
|
||||
@type_check_only
|
||||
class _ASTData(TypedDict):
|
||||
is_partial_package: bool
|
||||
uses_template_strings: bool
|
||||
mypy_ignores: _TypeIgnores
|
||||
source_hash: str
|
||||
mypy_comments: list[tuple[int, str]]
|
||||
|
||||
def parse(
|
||||
fnam: str,
|
||||
source: str | bytes | None = None,
|
||||
skip_function_bodies: bool = False,
|
||||
python_version: tuple[int, int] | None = None,
|
||||
platform: str | None = None,
|
||||
always_true: list[str] | None = None,
|
||||
always_false: list[str] | None = None,
|
||||
cache_version: int = 0,
|
||||
) -> tuple[bytes, list[ParseError], _TypeIgnores, bytes, _ASTData]:
|
||||
...
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,212 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: bandit
|
||||
Version: 1.9.4
|
||||
Summary: Security oriented static analyser for python code.
|
||||
Home-page: https://bandit.readthedocs.io/
|
||||
Author: PyCQA
|
||||
Author-email: code-quality@python.org
|
||||
License: Apache-2.0
|
||||
Project-URL: Documentation, https://bandit.readthedocs.io/
|
||||
Project-URL: Release Notes, https://github.com/PyCQA/bandit/releases
|
||||
Project-URL: Source Code, https://github.com/PyCQA/bandit
|
||||
Project-URL: Issue Tracker, https://github.com/PyCQA/bandit/issues
|
||||
Project-URL: Discord, https://discord.gg/qYxpadCgkx
|
||||
Project-URL: Sponsor, https://psfmember.org/civicrm/contribute/transact/?reset=1&id=42
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Console
|
||||
Classifier: Intended Audience :: Information Technology
|
||||
Classifier: Intended Audience :: System Administrators
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Operating System :: POSIX :: Linux
|
||||
Classifier: Operating System :: MacOS :: MacOS X
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Programming Language :: Python :: 3.13
|
||||
Classifier: Programming Language :: Python :: 3.14
|
||||
Classifier: Programming Language :: Python :: 3 :: Only
|
||||
Classifier: Topic :: Security
|
||||
Requires-Python: >=3.10
|
||||
License-File: LICENSE
|
||||
Requires-Dist: PyYAML>=5.3.1
|
||||
Requires-Dist: stevedore>=1.20.0
|
||||
Requires-Dist: rich
|
||||
Requires-Dist: colorama>=0.3.9; platform_system == "Windows"
|
||||
Provides-Extra: yaml
|
||||
Requires-Dist: PyYAML; extra == "yaml"
|
||||
Provides-Extra: toml
|
||||
Requires-Dist: tomli>=1.1.0; python_version < "3.11" and extra == "toml"
|
||||
Provides-Extra: baseline
|
||||
Requires-Dist: GitPython>=3.1.30; extra == "baseline"
|
||||
Provides-Extra: sarif
|
||||
Requires-Dist: sarif-om>=1.0.4; extra == "sarif"
|
||||
Requires-Dist: jschema-to-python>=1.2.3; extra == "sarif"
|
||||
Provides-Extra: test
|
||||
Requires-Dist: coverage>=4.5.4; extra == "test"
|
||||
Requires-Dist: fixtures>=3.0.0; extra == "test"
|
||||
Requires-Dist: flake8>=4.0.0; extra == "test"
|
||||
Requires-Dist: stestr>=2.5.0; extra == "test"
|
||||
Requires-Dist: testscenarios>=0.5.0; extra == "test"
|
||||
Requires-Dist: testtools>=2.3.0; extra == "test"
|
||||
Requires-Dist: beautifulsoup4>=4.8.0; extra == "test"
|
||||
Requires-Dist: pylint==1.9.4; extra == "test"
|
||||
Dynamic: author
|
||||
Dynamic: author-email
|
||||
Dynamic: description
|
||||
Dynamic: home-page
|
||||
Dynamic: license
|
||||
Dynamic: license-file
|
||||
Dynamic: project-url
|
||||
Dynamic: provides-extra
|
||||
Dynamic: requires-dist
|
||||
Dynamic: requires-python
|
||||
Dynamic: summary
|
||||
|
||||
.. 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,151 @@
|
||||
../../Scripts/bandit-baseline.exe,sha256=WHDNzNmLxBu34xNzqO4CKLuYQZ2Fvxul9w4A3bx9NB8,108403
|
||||
../../Scripts/bandit-config-generator.exe,sha256=3QUonqqBubRh-4zCOiTNydjWx9g-IziA4jm963W2bxo,108411
|
||||
../../Scripts/bandit.exe,sha256=couCBJBoNs_W1EMjkNa1xfj0I1pl8ZwB8b-7hBKEn28,108399
|
||||
../../share/man/man1/bandit.1,sha256=k1copRknkrnXzOyOAvdvC6iwl-QAYUG1HOWRsAtkdWI,6545
|
||||
bandit-1.9.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
bandit-1.9.4.dist-info/METADATA,sha256=pc_7769UxRQY4Zz1dl3tSa8gsnoluOtEDyHZ21DJzpo,7101
|
||||
bandit-1.9.4.dist-info/RECORD,,
|
||||
bandit-1.9.4.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
bandit-1.9.4.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
|
||||
bandit-1.9.4.dist-info/entry_points.txt,sha256=WoSLidZc14iE9GTbJR_2SkYGg03fNqaIhmgf2kHSXN8,4156
|
||||
bandit-1.9.4.dist-info/licenses/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142
|
||||
bandit-1.9.4.dist-info/pbr.json,sha256=Rs6XgS2MJcc1NWYzMCjeEr8KKqIZVIHxUm8tOIiltJU,47
|
||||
bandit-1.9.4.dist-info/top_level.txt,sha256=SVJ-U-In_cpe2PQq5ZOlxjEnlAV5MfjvfFuGzg8wgdg,7
|
||||
bandit/__init__.py,sha256=yjou8RxyHpx6zHjYcBa4_CUffNYIdERGCPx6PirAo-8,683
|
||||
bandit/__main__.py,sha256=PtnKPE5k9V79ArPscEozE9ruwUIMuHlYv3yiCMJ5UBs,571
|
||||
bandit/__pycache__/__init__.cpython-312.pyc,,
|
||||
bandit/__pycache__/__main__.cpython-312.pyc,,
|
||||
bandit/blacklists/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
bandit/blacklists/__pycache__/__init__.cpython-312.pyc,,
|
||||
bandit/blacklists/__pycache__/calls.cpython-312.pyc,,
|
||||
bandit/blacklists/__pycache__/imports.cpython-312.pyc,,
|
||||
bandit/blacklists/__pycache__/utils.cpython-312.pyc,,
|
||||
bandit/blacklists/calls.py,sha256=QCVOeBCZMrxLMo4ELUbuhCt2QorTcUe9vzqFDR0T1mU,29363
|
||||
bandit/blacklists/imports.py,sha256=3lCND02DoDE9EFHPeFhEegzP3YTZb4dk9RCUA-96Tek,17269
|
||||
bandit/blacklists/utils.py,sha256=OBm8dmmQsgp5_dJcm2-eAi69u5eXujeOYDg6zhMNeTM,420
|
||||
bandit/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
bandit/cli/__pycache__/__init__.cpython-312.pyc,,
|
||||
bandit/cli/__pycache__/baseline.cpython-312.pyc,,
|
||||
bandit/cli/__pycache__/config_generator.cpython-312.pyc,,
|
||||
bandit/cli/__pycache__/main.cpython-312.pyc,,
|
||||
bandit/cli/baseline.py,sha256=Z0VsMRmKyTxTyWLELtuKret9eiRkIqH804phSzzn_wU,7841
|
||||
bandit/cli/config_generator.py,sha256=umRDttgCxuyPG_7bOXCHgsEUi_xwaQbwgSA3tdGoUfk,6281
|
||||
bandit/cli/main.py,sha256=X2FBIrc4plB_6uetRUr6mN5qZ27lyeFfsvFZnec1874,20876
|
||||
bandit/core/__init__.py,sha256=NwxNqwUmUIJBQwnsOG58nvi6owEldiyGmkkig0a-4nw,558
|
||||
bandit/core/__pycache__/__init__.cpython-312.pyc,,
|
||||
bandit/core/__pycache__/blacklisting.cpython-312.pyc,,
|
||||
bandit/core/__pycache__/config.cpython-312.pyc,,
|
||||
bandit/core/__pycache__/constants.cpython-312.pyc,,
|
||||
bandit/core/__pycache__/context.cpython-312.pyc,,
|
||||
bandit/core/__pycache__/docs_utils.cpython-312.pyc,,
|
||||
bandit/core/__pycache__/extension_loader.cpython-312.pyc,,
|
||||
bandit/core/__pycache__/issue.cpython-312.pyc,,
|
||||
bandit/core/__pycache__/manager.cpython-312.pyc,,
|
||||
bandit/core/__pycache__/meta_ast.cpython-312.pyc,,
|
||||
bandit/core/__pycache__/metrics.cpython-312.pyc,,
|
||||
bandit/core/__pycache__/node_visitor.cpython-312.pyc,,
|
||||
bandit/core/__pycache__/test_properties.cpython-312.pyc,,
|
||||
bandit/core/__pycache__/test_set.cpython-312.pyc,,
|
||||
bandit/core/__pycache__/tester.cpython-312.pyc,,
|
||||
bandit/core/__pycache__/utils.cpython-312.pyc,,
|
||||
bandit/core/blacklisting.py,sha256=AcV2Xe_gFmqRFTezvtOxDUv2z6r4v3mcXhg63MHQ85c,2780
|
||||
bandit/core/config.py,sha256=6VCkWN3PFGIG9x4FFrNjBvhTffxRZ_KEnipNmlgzav8,9840
|
||||
bandit/core/constants.py,sha256=yaB2ks72eOzrnfN7xOr3zFWxsc8eCMnppnIBj-_Jmn0,1220
|
||||
bandit/core/context.py,sha256=27qPogcEZcHdTV2ByGnBpJso7Kr8bwObk5rnunGqrOs,10667
|
||||
bandit/core/docs_utils.py,sha256=iDWwx4XTnIcAyQhLp6DSyP9C1M2pkgA2Ktb686cyf_I,1779
|
||||
bandit/core/extension_loader.py,sha256=6w8qE64A8vYU6wP3ryVZfn7Yxy5SFpw_zEnB5ttWeyU,4039
|
||||
bandit/core/issue.py,sha256=BituIds2j2gbSMaMf9iM7N_yzcGo0-qQq38Pp-Ae7ko,7069
|
||||
bandit/core/manager.py,sha256=WAxO-4moDtbgm_AuNR0TqRFd_cheYe-x46CTjV5WIZQ,17284
|
||||
bandit/core/meta_ast.py,sha256=rAUdLwsm4eTPN0oXvzyIOfVXsuKV93MLMJsUC86hTWc,1136
|
||||
bandit/core/metrics.py,sha256=wDjPmrujRszaqY0zI1W7tVTVYhnC-kHo8wCaf5vYKBA,3454
|
||||
bandit/core/node_visitor.py,sha256=HsSSE3KnKxLfS_57hK_VDgfCud6LvjA_xraZ58rMmdg,10830
|
||||
bandit/core/test_properties.py,sha256=_letTk7y9Sp5SyRaq2clLeNRjKCWnOxucglGtUMLE5Q,2106
|
||||
bandit/core/test_set.py,sha256=jweZ7eK1IGhodabF6DHO_DhBMMrHxFU03R5_z4sSrJc,4054
|
||||
bandit/core/tester.py,sha256=0im7I_Ha0iOfMqUnE6rh23E9u3X4gEww7CpxF6UCaW4,6602
|
||||
bandit/core/utils.py,sha256=hkg92rjpuYlxSlYJiNNEXQ2_Ot07-xkcoVhiNxdmbeg,12297
|
||||
bandit/formatters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
bandit/formatters/__pycache__/__init__.cpython-312.pyc,,
|
||||
bandit/formatters/__pycache__/csv.cpython-312.pyc,,
|
||||
bandit/formatters/__pycache__/custom.cpython-312.pyc,,
|
||||
bandit/formatters/__pycache__/html.cpython-312.pyc,,
|
||||
bandit/formatters/__pycache__/json.cpython-312.pyc,,
|
||||
bandit/formatters/__pycache__/sarif.cpython-312.pyc,,
|
||||
bandit/formatters/__pycache__/screen.cpython-312.pyc,,
|
||||
bandit/formatters/__pycache__/text.cpython-312.pyc,,
|
||||
bandit/formatters/__pycache__/utils.cpython-312.pyc,,
|
||||
bandit/formatters/__pycache__/xml.cpython-312.pyc,,
|
||||
bandit/formatters/__pycache__/yaml.cpython-312.pyc,,
|
||||
bandit/formatters/csv.py,sha256=IiTLncVx3hnn7A7pJpJ5Y9vxibhxHIvZnGhezhYYKSg,2313
|
||||
bandit/formatters/custom.py,sha256=21GgrLiaStknoVD9GU-sWku4nK7hJI4O7-pgyHQacbw,5363
|
||||
bandit/formatters/html.py,sha256=VNHmmKAsZWV_S-ROd4DEXJd_Uy1ipOvbD50BzihubKU,8489
|
||||
bandit/formatters/json.py,sha256=8fA-v5lsLTdoB5UwCVqxhpgeYAZDY1tK7wsZjUAFLqg,4330
|
||||
bandit/formatters/sarif.py,sha256=jP5kd9Eut0BiPIJ7e8J38vp7BLLtJFaRfEGSwGQZ41I,10791
|
||||
bandit/formatters/screen.py,sha256=71cPOEqoDznO1aHbVt-zAjWLeeTMUwYGj5kt86UGdrM,6850
|
||||
bandit/formatters/text.py,sha256=Vhh_AUATxiQpLcm0xtZ91GSJ5QeES_rXMVy1kS1H_U4,5978
|
||||
bandit/formatters/utils.py,sha256=MXmcXC1fBeRbURQKqUtqhPMtAEMO6I6-MIwcdrI_UFA,390
|
||||
bandit/formatters/xml.py,sha256=pbsa66tYlGfybq6_N5gOhTgKnSQnvJFs39z8zFCwac4,2753
|
||||
bandit/formatters/yaml.py,sha256=lmJDFXQmxp7vdC7koqRWMb9IRSMXyXEFhH2zoNu8oHc,3463
|
||||
bandit/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
bandit/plugins/__pycache__/__init__.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/app_debug.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/asserts.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/crypto_request_no_cert_validation.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/django_sql_injection.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/django_xss.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/exec.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/general_bad_file_permissions.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/general_bind_all_interfaces.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/general_hardcoded_password.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/general_hardcoded_tmp.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/hashlib_insecure_functions.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/huggingface_unsafe_download.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/injection_paramiko.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/injection_shell.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/injection_sql.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/injection_wildcard.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/insecure_ssl_tls.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/jinja2_templates.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/logging_config_insecure_listen.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/mako_templates.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/markupsafe_markup_xss.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/pytorch_load.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/request_without_timeout.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/snmp_security_check.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/ssh_no_host_key_verification.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/tarfile_unsafe_members.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/trojansource.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/try_except_continue.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/try_except_pass.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/weak_cryptographic_key.cpython-312.pyc,,
|
||||
bandit/plugins/__pycache__/yaml_load.cpython-312.pyc,,
|
||||
bandit/plugins/app_debug.py,sha256=0Zp-DTiLnuvF-jlZKhCEK-9YzRMcUc7JS6mxWV01hFc,2257
|
||||
bandit/plugins/asserts.py,sha256=iOP5WRjdpFc8j62pIkQ_cx-LYDW1aSv8qpfdU78AoXU,2305
|
||||
bandit/plugins/crypto_request_no_cert_validation.py,sha256=AyESgBZ7JtzieeJTnRXu0kknf7og1B5GI-6uA3kLbls,2660
|
||||
bandit/plugins/django_sql_injection.py,sha256=iYNAWU-j0DRgj5rDN7sboWvxcm2czm0i8849N6tzIdw,5203
|
||||
bandit/plugins/django_xss.py,sha256=HOUAk6w2lMF6RNsZsyP7i_1qa4iJGXg2Ek_Ni28joQ4,10302
|
||||
bandit/plugins/exec.py,sha256=5kosSmgI8Y2XM4Z_5hwIq7WRTmdpfDM5E7uXYTaGxgo,1357
|
||||
bandit/plugins/general_bad_file_permissions.py,sha256=8T59CP-aluBtXkQdyyQJljFiLvK4yVIy3fDSggw53Eg,3340
|
||||
bandit/plugins/general_bind_all_interfaces.py,sha256=Mn8YBkfF5Qwhx1QRMHB-5HNnzhR4neP0lI_6LyQr4Gg,1522
|
||||
bandit/plugins/general_hardcoded_password.py,sha256=YFk4-13a9duCNt5rZ0Vuj_4a1XOOFXYG7c_DgPIKpUg,8770
|
||||
bandit/plugins/general_hardcoded_tmp.py,sha256=OjDZgboZF186RK133GQksRqAkneBP14LxnBn88KSjjs,2301
|
||||
bandit/plugins/hashlib_insecure_functions.py,sha256=-cYJKULazbzqwOcq-uAGk5EvDMNTHc9BCubr3l4UHJY,4330
|
||||
bandit/plugins/huggingface_unsafe_download.py,sha256=ok-T2exvBg9AnGasmJESrJst0uFlvDarTVI2ap6FU-Y,5899
|
||||
bandit/plugins/injection_paramiko.py,sha256=bAbqH-4CHQY1ghQpjlck-Pl8DKq4G6jJoAQCY3PSzYw,2049
|
||||
bandit/plugins/injection_shell.py,sha256=ZsScLQAbyk0SQzw0-Pk7kzCxaEYtC8D_4VRzTc7cJ8k,26851
|
||||
bandit/plugins/injection_sql.py,sha256=Ub3E1in980NiAqHXj_XvD6ZZsd-rltZKqYhjDjDqtlU,4878
|
||||
bandit/plugins/injection_wildcard.py,sha256=GeHJchoDxULuaLeCxMyYuJrxVTC1vx8k6JSsXm5BDFM,5016
|
||||
bandit/plugins/insecure_ssl_tls.py,sha256=VrR9qyOyY7o1UTBw-Fw06GbE87SO4wD_j127erVfDLQ,10454
|
||||
bandit/plugins/jinja2_templates.py,sha256=5-0hPcJqm-THcZn44CSReq9_oy8Ym9QG_YN-vzv3hhg,5806
|
||||
bandit/plugins/logging_config_insecure_listen.py,sha256=UzDtLTiIwRnqpPjPIZbdtYb32BT5E5h2hhC2-m9kxGU,1944
|
||||
bandit/plugins/mako_templates.py,sha256=HBhxtofo1gGd8dKPxahJ1ELlv60NYrn0rcX4B-MYtpM,2549
|
||||
bandit/plugins/markupsafe_markup_xss.py,sha256=QTFwXe99MK26MtEEmn6tlNdy9ojQY0BMZNvKMZ3cAWg,3704
|
||||
bandit/plugins/pytorch_load.py,sha256=yDzFcn17OnkQSGn8fQ8ZzoQNL_tgUFn97dVFA-y1FeM,2672
|
||||
bandit/plugins/request_without_timeout.py,sha256=IJadPCwQVEAXZ3h3YscgvgDIzdrHM0_jozYiRN30kyE,3087
|
||||
bandit/plugins/snmp_security_check.py,sha256=tTdonRdKMKs5Rq4o4OWznW4_rjna2UhnStNLZTKG58I,3716
|
||||
bandit/plugins/ssh_no_host_key_verification.py,sha256=1Fqx5k5gtLvnWk4Gz7bQXwqx4TOxIzUGa-ouYBQGNsI,2732
|
||||
bandit/plugins/tarfile_unsafe_members.py,sha256=-VKsrS06IdH4NfbXTphi6d4AUtkjELJAuZIHfQyTKw8,3929
|
||||
bandit/plugins/trojansource.py,sha256=W-t2LRSYhoDEk9rlSnYHdPhgSpN1TExNnruMGMh22TY,2428
|
||||
bandit/plugins/try_except_continue.py,sha256=K-VrQS_YnifFwz5GC1LAUzGHTbbh9m-LHuDaJwgAS5o,3078
|
||||
bandit/plugins/try_except_pass.py,sha256=DwPiiziccoWtgE86aEmU9maKW1W8JuJxqOlnume1nis,2910
|
||||
bandit/plugins/weak_cryptographic_key.py,sha256=SGH3YM3LiBrcmuO0GjnQuZCVm42d2C68l1dGKtnwNb8,5544
|
||||
bandit/plugins/yaml_load.py,sha256=bOfCZBOcSXB3AAINJbuvcHkHebo-qyMyA4155Lgnx2g,2404
|
||||
@@ -0,0 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (79.0.1)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
[bandit.blacklists]
|
||||
calls = bandit.blacklists.calls:gen_blacklist
|
||||
imports = bandit.blacklists.imports:gen_blacklist
|
||||
|
||||
[bandit.formatters]
|
||||
csv = bandit.formatters.csv:report
|
||||
custom = bandit.formatters.custom:report
|
||||
html = bandit.formatters.html:report
|
||||
json = bandit.formatters.json:report
|
||||
sarif = bandit.formatters.sarif:report
|
||||
screen = bandit.formatters.screen:report
|
||||
txt = bandit.formatters.text:report
|
||||
xml = bandit.formatters.xml:report
|
||||
yaml = bandit.formatters.yaml:report
|
||||
|
||||
[bandit.plugins]
|
||||
any_other_function_with_shell_equals_true = bandit.plugins.injection_shell:any_other_function_with_shell_equals_true
|
||||
assert_used = bandit.plugins.asserts:assert_used
|
||||
django_extra_used = bandit.plugins.django_sql_injection:django_extra_used
|
||||
django_mark_safe = bandit.plugins.django_xss:django_mark_safe
|
||||
django_rawsql_used = bandit.plugins.django_sql_injection:django_rawsql_used
|
||||
exec_used = bandit.plugins.exec:exec_used
|
||||
flask_debug_true = bandit.plugins.app_debug:flask_debug_true
|
||||
hardcoded_bind_all_interfaces = bandit.plugins.general_bind_all_interfaces:hardcoded_bind_all_interfaces
|
||||
hardcoded_password_default = bandit.plugins.general_hardcoded_password:hardcoded_password_default
|
||||
hardcoded_password_funcarg = bandit.plugins.general_hardcoded_password:hardcoded_password_funcarg
|
||||
hardcoded_password_string = bandit.plugins.general_hardcoded_password:hardcoded_password_string
|
||||
hardcoded_sql_expressions = bandit.plugins.injection_sql:hardcoded_sql_expressions
|
||||
hardcoded_tmp_directory = bandit.plugins.general_hardcoded_tmp:hardcoded_tmp_directory
|
||||
hashlib_insecure_functions = bandit.plugins.hashlib_insecure_functions:hashlib
|
||||
huggingface_unsafe_download = bandit.plugins.huggingface_unsafe_download:huggingface_unsafe_download
|
||||
jinja2_autoescape_false = bandit.plugins.jinja2_templates:jinja2_autoescape_false
|
||||
linux_commands_wildcard_injection = bandit.plugins.injection_wildcard:linux_commands_wildcard_injection
|
||||
logging_config_insecure_listen = bandit.plugins.logging_config_insecure_listen:logging_config_insecure_listen
|
||||
markupsafe_markup_xss = bandit.plugins.markupsafe_markup_xss:markupsafe_markup_xss
|
||||
paramiko_calls = bandit.plugins.injection_paramiko:paramiko_calls
|
||||
pytorch_load = bandit.plugins.pytorch_load:pytorch_load
|
||||
request_with_no_cert_validation = bandit.plugins.crypto_request_no_cert_validation:request_with_no_cert_validation
|
||||
request_without_timeout = bandit.plugins.request_without_timeout:request_without_timeout
|
||||
set_bad_file_permissions = bandit.plugins.general_bad_file_permissions:set_bad_file_permissions
|
||||
snmp_insecure_version = bandit.plugins.snmp_security_check:snmp_insecure_version_check
|
||||
snmp_weak_cryptography = bandit.plugins.snmp_security_check:snmp_crypto_check
|
||||
ssh_no_host_key_verification = bandit.plugins.ssh_no_host_key_verification:ssh_no_host_key_verification
|
||||
ssl_with_bad_defaults = bandit.plugins.insecure_ssl_tls:ssl_with_bad_defaults
|
||||
ssl_with_bad_version = bandit.plugins.insecure_ssl_tls:ssl_with_bad_version
|
||||
ssl_with_no_version = bandit.plugins.insecure_ssl_tls:ssl_with_no_version
|
||||
start_process_with_a_shell = bandit.plugins.injection_shell:start_process_with_a_shell
|
||||
start_process_with_no_shell = bandit.plugins.injection_shell:start_process_with_no_shell
|
||||
start_process_with_partial_path = bandit.plugins.injection_shell:start_process_with_partial_path
|
||||
subprocess_popen_with_shell_equals_true = bandit.plugins.injection_shell:subprocess_popen_with_shell_equals_true
|
||||
subprocess_without_shell_equals_true = bandit.plugins.injection_shell:subprocess_without_shell_equals_true
|
||||
tarfile_unsafe_members = bandit.plugins.tarfile_unsafe_members:tarfile_unsafe_members
|
||||
trojansource = bandit.plugins.trojansource:trojansource
|
||||
try_except_continue = bandit.plugins.try_except_continue:try_except_continue
|
||||
try_except_pass = bandit.plugins.try_except_pass:try_except_pass
|
||||
use_of_mako_templates = bandit.plugins.mako_templates:use_of_mako_templates
|
||||
weak_cryptographic_key = bandit.plugins.weak_cryptographic_key:weak_cryptographic_key
|
||||
yaml_load = bandit.plugins.yaml_load:yaml_load
|
||||
|
||||
[console_scripts]
|
||||
bandit = bandit.cli.main:main
|
||||
bandit-baseline = bandit.cli.baseline:main
|
||||
bandit-config-generator = bandit.cli.config_generator:main
|
||||
@@ -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 @@
|
||||
{"git_version": "92ae8b8", "is_release": false}
|
||||
@@ -0,0 +1 @@
|
||||
bandit
|
||||
@@ -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,252 @@
|
||||
#
|
||||
# Copyright 2015 Hewlett-Packard Enterprise
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# #############################################################################
|
||||
# Bandit Baseline is a tool that runs Bandit against a Git commit, and compares
|
||||
# the current commit findings to the parent commit findings.
|
||||
# To do this it checks out the parent commit, runs Bandit (with any provided
|
||||
# filters or profiles), checks out the current commit, runs Bandit, and then
|
||||
# reports on any new findings.
|
||||
# #############################################################################
|
||||
"""Bandit is a tool designed to find common security issues in Python code."""
|
||||
import argparse
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess # nosec: B404
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
try:
|
||||
import git
|
||||
except ImportError:
|
||||
git = None
|
||||
|
||||
bandit_args = sys.argv[1:]
|
||||
baseline_tmp_file = "_bandit_baseline_run.json_"
|
||||
current_commit = None
|
||||
default_output_format = "terminal"
|
||||
LOG = logging.getLogger(__name__)
|
||||
repo = None
|
||||
report_basename = "bandit_baseline_result"
|
||||
valid_baseline_formats = ["txt", "html", "json"]
|
||||
|
||||
"""baseline.py"""
|
||||
|
||||
|
||||
def main():
|
||||
"""Execute Bandit."""
|
||||
# our cleanup function needs this and can't be passed arguments
|
||||
global current_commit
|
||||
global repo
|
||||
|
||||
parent_commit = None
|
||||
output_format = None
|
||||
repo = None
|
||||
report_fname = None
|
||||
|
||||
init_logger()
|
||||
|
||||
output_format, repo, report_fname = initialize()
|
||||
|
||||
if not repo:
|
||||
sys.exit(2)
|
||||
|
||||
# #################### Find current and parent commits ####################
|
||||
try:
|
||||
commit = repo.commit()
|
||||
current_commit = commit.hexsha
|
||||
LOG.info("Got current commit: [%s]", commit.name_rev)
|
||||
|
||||
commit = commit.parents[0]
|
||||
parent_commit = commit.hexsha
|
||||
LOG.info("Got parent commit: [%s]", commit.name_rev)
|
||||
|
||||
except git.GitCommandError:
|
||||
LOG.error("Unable to get current or parent commit")
|
||||
sys.exit(2)
|
||||
except IndexError:
|
||||
LOG.error("Parent commit not available")
|
||||
sys.exit(2)
|
||||
|
||||
# #################### Run Bandit against both commits ####################
|
||||
output_type = (
|
||||
["-f", "txt"]
|
||||
if output_format == default_output_format
|
||||
else ["-o", report_fname]
|
||||
)
|
||||
|
||||
with baseline_setup() as t:
|
||||
bandit_tmpfile = f"{t}/{baseline_tmp_file}"
|
||||
|
||||
steps = [
|
||||
{
|
||||
"message": "Getting Bandit baseline results",
|
||||
"commit": parent_commit,
|
||||
"args": bandit_args + ["-f", "json", "-o", bandit_tmpfile],
|
||||
},
|
||||
{
|
||||
"message": "Comparing Bandit results to baseline",
|
||||
"commit": current_commit,
|
||||
"args": bandit_args + ["-b", bandit_tmpfile] + output_type,
|
||||
},
|
||||
]
|
||||
|
||||
return_code = None
|
||||
|
||||
for step in steps:
|
||||
repo.head.reset(commit=step["commit"], working_tree=True)
|
||||
|
||||
LOG.info(step["message"])
|
||||
|
||||
bandit_command = ["bandit"] + step["args"]
|
||||
|
||||
try:
|
||||
output = subprocess.check_output(bandit_command) # nosec: B603
|
||||
except subprocess.CalledProcessError as e:
|
||||
output = e.output
|
||||
return_code = e.returncode
|
||||
else:
|
||||
return_code = 0
|
||||
output = output.decode("utf-8") # subprocess returns bytes
|
||||
|
||||
if return_code not in [0, 1]:
|
||||
LOG.error(
|
||||
"Error running command: %s\nOutput: %s\n",
|
||||
bandit_args,
|
||||
output,
|
||||
)
|
||||
|
||||
# #################### Output and exit ####################################
|
||||
# print output or display message about written report
|
||||
if output_format == default_output_format:
|
||||
print(output)
|
||||
else:
|
||||
LOG.info("Successfully wrote %s", report_fname)
|
||||
|
||||
# exit with the code the last Bandit run returned
|
||||
sys.exit(return_code)
|
||||
|
||||
|
||||
# #################### Clean up before exit ###################################
|
||||
@contextlib.contextmanager
|
||||
def baseline_setup():
|
||||
"""Baseline setup by creating temp folder and resetting repo."""
|
||||
d = tempfile.mkdtemp()
|
||||
yield d
|
||||
shutil.rmtree(d, True)
|
||||
|
||||
if repo:
|
||||
repo.head.reset(commit=current_commit, working_tree=True)
|
||||
|
||||
|
||||
# #################### Setup logging ##########################################
|
||||
def init_logger():
|
||||
"""Init logger."""
|
||||
LOG.handlers = []
|
||||
log_level = logging.INFO
|
||||
log_format_string = "[%(levelname)7s ] %(message)s"
|
||||
logging.captureWarnings(True)
|
||||
LOG.setLevel(log_level)
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setFormatter(logging.Formatter(log_format_string))
|
||||
LOG.addHandler(handler)
|
||||
|
||||
|
||||
# #################### Perform initialization and validate assumptions ########
|
||||
def initialize():
|
||||
"""Initialize arguments and output formats."""
|
||||
valid = True
|
||||
|
||||
# #################### Parse Args #########################################
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Bandit Baseline - Generates Bandit results compared to "
|
||||
"a baseline",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="Additional Bandit arguments such as severity filtering (-ll) "
|
||||
"can be added and will be passed to Bandit.",
|
||||
)
|
||||
if sys.version_info >= (3, 14):
|
||||
parser.suggest_on_error = True
|
||||
parser.color = False
|
||||
|
||||
parser.add_argument(
|
||||
"targets",
|
||||
metavar="targets",
|
||||
type=str,
|
||||
nargs="+",
|
||||
help="source file(s) or directory(s) to be tested",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
dest="output_format",
|
||||
action="store",
|
||||
default="terminal",
|
||||
help="specify output format",
|
||||
choices=valid_baseline_formats,
|
||||
)
|
||||
|
||||
args, _ = parser.parse_known_args()
|
||||
|
||||
# #################### Setup Output #######################################
|
||||
# set the output format, or use a default if not provided
|
||||
output_format = (
|
||||
args.output_format if args.output_format else default_output_format
|
||||
)
|
||||
|
||||
if output_format == default_output_format:
|
||||
LOG.info("No output format specified, using %s", default_output_format)
|
||||
|
||||
# set the report name based on the output format
|
||||
report_fname = f"{report_basename}.{output_format}"
|
||||
|
||||
# #################### Check Requirements #################################
|
||||
if git is None:
|
||||
LOG.error("Git not available, reinstall with baseline extra")
|
||||
valid = False
|
||||
return (None, None, None)
|
||||
|
||||
try:
|
||||
repo = git.Repo(os.getcwd())
|
||||
|
||||
except git.exc.InvalidGitRepositoryError:
|
||||
LOG.error("Bandit baseline must be called from a git project root")
|
||||
valid = False
|
||||
|
||||
except git.exc.GitCommandNotFound:
|
||||
LOG.error("Git command not found")
|
||||
valid = False
|
||||
|
||||
else:
|
||||
if repo.is_dirty():
|
||||
LOG.error(
|
||||
"Current working directory is dirty and must be " "resolved"
|
||||
)
|
||||
valid = False
|
||||
|
||||
# if output format is specified, we need to be able to write the report
|
||||
if output_format != default_output_format and os.path.exists(report_fname):
|
||||
LOG.error("File %s already exists, aborting", report_fname)
|
||||
valid = False
|
||||
|
||||
# Bandit needs to be able to create this temp file
|
||||
if os.path.exists(baseline_tmp_file):
|
||||
LOG.error(
|
||||
"Temporary file %s needs to be removed prior to running",
|
||||
baseline_tmp_file,
|
||||
)
|
||||
valid = False
|
||||
|
||||
# we must validate -o is not provided, as it will mess up Bandit baseline
|
||||
if "-o" in bandit_args:
|
||||
LOG.error("Bandit baseline must not be called with the -o option")
|
||||
valid = False
|
||||
|
||||
return (output_format, repo, report_fname) if valid else (None, None, None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,207 @@
|
||||
# Copyright 2015 Red Hat Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Bandit is a tool designed to find common security issues in Python code."""
|
||||
import argparse
|
||||
import importlib
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
|
||||
from bandit.core import extension_loader
|
||||
|
||||
PROG_NAME = "bandit_conf_generator"
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
template = """
|
||||
### Bandit config file generated from:
|
||||
# '{cli}'
|
||||
|
||||
### This config may optionally select a subset of tests to run or skip by
|
||||
### filling out the 'tests' and 'skips' lists given below. If no tests are
|
||||
### specified for inclusion then it is assumed all tests are desired. The skips
|
||||
### set will remove specific tests from the include set. This can be controlled
|
||||
### using the -t/-s CLI options. Note that the same test ID should not appear
|
||||
### in both 'tests' and 'skips', this would be nonsensical and is detected by
|
||||
### Bandit at runtime.
|
||||
|
||||
# Available tests:
|
||||
{test_list}
|
||||
|
||||
# (optional) list included test IDs here, eg '[B101, B406]':
|
||||
{test}
|
||||
|
||||
# (optional) list skipped test IDs here, eg '[B101, B406]':
|
||||
{skip}
|
||||
|
||||
### (optional) plugin settings - some test plugins require configuration data
|
||||
### that may be given here, per-plugin. All bandit test plugins have a built in
|
||||
### set of sensible defaults and these will be used if no configuration is
|
||||
### provided. It is not necessary to provide settings for every (or any) plugin
|
||||
### if the defaults are acceptable.
|
||||
|
||||
{settings}
|
||||
"""
|
||||
|
||||
|
||||
def init_logger():
|
||||
"""Init logger."""
|
||||
LOG.handlers = []
|
||||
log_level = logging.INFO
|
||||
log_format_string = "[%(levelname)5s]: %(message)s"
|
||||
logging.captureWarnings(True)
|
||||
LOG.setLevel(log_level)
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setFormatter(logging.Formatter(log_format_string))
|
||||
LOG.addHandler(handler)
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Parse arguments."""
|
||||
help_description = """Bandit Config Generator
|
||||
|
||||
This tool is used to generate an optional profile. The profile may be used
|
||||
to include or skip tests and override values for plugins.
|
||||
|
||||
When used to store an output profile, this tool will output a template that
|
||||
includes all plugins and their default settings. Any settings which aren't
|
||||
being overridden can be safely removed from the profile and default values
|
||||
will be used. Bandit will prefer settings from the profile over the built
|
||||
in values."""
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=help_description,
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
if sys.version_info >= (3, 14):
|
||||
parser.suggest_on_error = True
|
||||
parser.color = False
|
||||
|
||||
parser.add_argument(
|
||||
"--show-defaults",
|
||||
dest="show_defaults",
|
||||
action="store_true",
|
||||
help="show the default settings values for each "
|
||||
"plugin but do not output a profile",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--out",
|
||||
dest="output_file",
|
||||
action="store",
|
||||
help="output file to save profile",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--tests",
|
||||
dest="tests",
|
||||
action="store",
|
||||
default=None,
|
||||
type=str,
|
||||
help="list of test names to run",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--skip",
|
||||
dest="skips",
|
||||
action="store",
|
||||
default=None,
|
||||
type=str,
|
||||
help="list of test names to skip",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.output_file and not args.show_defaults:
|
||||
parser.print_help()
|
||||
parser.exit(1)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def get_config_settings():
|
||||
"""Get configuration settings."""
|
||||
config = {}
|
||||
for plugin in extension_loader.MANAGER.plugins:
|
||||
fn_name = plugin.name
|
||||
function = plugin.plugin
|
||||
|
||||
# if a function takes config...
|
||||
if hasattr(function, "_takes_config"):
|
||||
fn_module = importlib.import_module(function.__module__)
|
||||
|
||||
# call the config generator if it exists
|
||||
if hasattr(fn_module, "gen_config"):
|
||||
config[fn_name] = fn_module.gen_config(function._takes_config)
|
||||
|
||||
return yaml.safe_dump(config, default_flow_style=False)
|
||||
|
||||
|
||||
def main():
|
||||
"""Config generator to write configuration file."""
|
||||
init_logger()
|
||||
args = parse_args()
|
||||
|
||||
yaml_settings = get_config_settings()
|
||||
|
||||
if args.show_defaults:
|
||||
print(yaml_settings)
|
||||
|
||||
if args.output_file:
|
||||
if os.path.exists(os.path.abspath(args.output_file)):
|
||||
LOG.error("File %s already exists, exiting", args.output_file)
|
||||
sys.exit(2)
|
||||
|
||||
try:
|
||||
with open(args.output_file, "w") as f:
|
||||
skips = args.skips.split(",") if args.skips else []
|
||||
tests = args.tests.split(",") if args.tests else []
|
||||
|
||||
for skip in skips:
|
||||
if not extension_loader.MANAGER.check_id(skip):
|
||||
raise RuntimeError(f"unknown ID in skips: {skip}")
|
||||
|
||||
for test in tests:
|
||||
if not extension_loader.MANAGER.check_id(test):
|
||||
raise RuntimeError(f"unknown ID in tests: {test}")
|
||||
|
||||
tpl = "# {0} : {1}"
|
||||
test_list = [
|
||||
tpl.format(t.plugin._test_id, t.name)
|
||||
for t in extension_loader.MANAGER.plugins
|
||||
]
|
||||
|
||||
others = [
|
||||
tpl.format(k, v["name"])
|
||||
for k, v in (
|
||||
extension_loader.MANAGER.blacklist_by_id.items()
|
||||
)
|
||||
]
|
||||
test_list.extend(others)
|
||||
test_list.sort()
|
||||
|
||||
contents = template.format(
|
||||
cli=" ".join(sys.argv),
|
||||
settings=yaml_settings,
|
||||
test_list="\n".join(test_list),
|
||||
skip="skips: " + str(skips) if skips else "skips:",
|
||||
test="tests: " + str(tests) if tests else "tests:",
|
||||
)
|
||||
f.write(contents)
|
||||
|
||||
except OSError:
|
||||
LOG.error("Unable to open %s for writing", args.output_file)
|
||||
|
||||
except Exception as e:
|
||||
LOG.error("Error: %s", e)
|
||||
|
||||
else:
|
||||
LOG.info("Successfully wrote profile: %s", args.output_file)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,701 @@
|
||||
#
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Bandit is a tool designed to find common security issues in Python code."""
|
||||
import argparse
|
||||
import fnmatch
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
import bandit
|
||||
from bandit.core import config as b_config
|
||||
from bandit.core import constants
|
||||
from bandit.core import manager as b_manager
|
||||
from bandit.core import utils
|
||||
|
||||
BASE_CONFIG = "bandit.yaml"
|
||||
LOG = logging.getLogger()
|
||||
|
||||
|
||||
def _init_logger(log_level=logging.INFO, log_format=None):
|
||||
"""Initialize the logger.
|
||||
|
||||
:param debug: Whether to enable debug mode
|
||||
:return: An instantiated logging instance
|
||||
"""
|
||||
LOG.handlers = []
|
||||
|
||||
if not log_format:
|
||||
# default log format
|
||||
log_format_string = constants.log_format_string
|
||||
else:
|
||||
log_format_string = log_format
|
||||
|
||||
logging.captureWarnings(True)
|
||||
|
||||
LOG.setLevel(log_level)
|
||||
handler = logging.StreamHandler(sys.stderr)
|
||||
handler.setFormatter(logging.Formatter(log_format_string))
|
||||
LOG.addHandler(handler)
|
||||
LOG.debug("logging initialized")
|
||||
|
||||
|
||||
def _get_options_from_ini(ini_path, target):
|
||||
"""Return a dictionary of config options or None if we can't load any."""
|
||||
ini_file = None
|
||||
|
||||
if ini_path:
|
||||
ini_file = ini_path
|
||||
else:
|
||||
bandit_files = []
|
||||
|
||||
for t in target:
|
||||
for root, _, filenames in os.walk(t):
|
||||
for filename in fnmatch.filter(filenames, ".bandit"):
|
||||
bandit_files.append(os.path.join(root, filename))
|
||||
|
||||
if len(bandit_files) > 1:
|
||||
LOG.error(
|
||||
"Multiple .bandit files found - scan separately or "
|
||||
"choose one with --ini\n\t%s",
|
||||
", ".join(bandit_files),
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
elif len(bandit_files) == 1:
|
||||
ini_file = bandit_files[0]
|
||||
LOG.info("Found project level .bandit file: %s", bandit_files[0])
|
||||
|
||||
if ini_file:
|
||||
return utils.parse_ini_file(ini_file)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _init_extensions():
|
||||
from bandit.core import extension_loader as ext_loader
|
||||
|
||||
return ext_loader.MANAGER
|
||||
|
||||
|
||||
def _log_option_source(default_val, arg_val, ini_val, option_name):
|
||||
"""It's useful to show the source of each option."""
|
||||
# When default value is not defined, arg_val and ini_val is deterministic
|
||||
if default_val is None:
|
||||
if arg_val:
|
||||
LOG.info("Using command line arg for %s", option_name)
|
||||
return arg_val
|
||||
elif ini_val:
|
||||
LOG.info("Using ini file for %s", option_name)
|
||||
return ini_val
|
||||
else:
|
||||
return None
|
||||
# No value passed to command line and default value is used
|
||||
elif default_val == arg_val:
|
||||
return ini_val if ini_val else arg_val
|
||||
# Certainly a value is passed to command line
|
||||
else:
|
||||
return arg_val
|
||||
|
||||
|
||||
def _running_under_virtualenv():
|
||||
if hasattr(sys, "real_prefix"):
|
||||
return True
|
||||
elif sys.prefix != getattr(sys, "base_prefix", sys.prefix):
|
||||
return True
|
||||
|
||||
|
||||
def _get_profile(config, profile_name, config_path):
|
||||
profile = {}
|
||||
if profile_name:
|
||||
profiles = config.get_option("profiles") or {}
|
||||
profile = profiles.get(profile_name)
|
||||
if profile is None:
|
||||
raise utils.ProfileNotFound(config_path, profile_name)
|
||||
LOG.debug("read in legacy profile '%s': %s", profile_name, profile)
|
||||
else:
|
||||
profile["include"] = set(config.get_option("tests") or [])
|
||||
profile["exclude"] = set(config.get_option("skips") or [])
|
||||
return profile
|
||||
|
||||
|
||||
def _log_info(args, profile):
|
||||
inc = ",".join([t for t in profile["include"]]) or "None"
|
||||
exc = ",".join([t for t in profile["exclude"]]) or "None"
|
||||
LOG.info("profile include tests: %s", inc)
|
||||
LOG.info("profile exclude tests: %s", exc)
|
||||
LOG.info("cli include tests: %s", args.tests)
|
||||
LOG.info("cli exclude tests: %s", args.skips)
|
||||
|
||||
|
||||
def main():
|
||||
"""Bandit CLI."""
|
||||
# bring our logging stuff up as early as possible
|
||||
debug = (
|
||||
logging.DEBUG
|
||||
if "-d" in sys.argv or "--debug" in sys.argv
|
||||
else logging.INFO
|
||||
)
|
||||
_init_logger(debug)
|
||||
extension_mgr = _init_extensions()
|
||||
|
||||
baseline_formatters = [
|
||||
f.name
|
||||
for f in filter(
|
||||
lambda x: hasattr(x.plugin, "_accepts_baseline"),
|
||||
extension_mgr.formatters,
|
||||
)
|
||||
]
|
||||
|
||||
# now do normal startup
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Bandit - a Python source code security analyzer",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
if sys.version_info >= (3, 14):
|
||||
parser.suggest_on_error = True
|
||||
parser.color = False
|
||||
|
||||
parser.add_argument(
|
||||
"targets",
|
||||
metavar="targets",
|
||||
type=str,
|
||||
nargs="*",
|
||||
help="source file(s) or directory(s) to be tested",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--recursive",
|
||||
dest="recursive",
|
||||
action="store_true",
|
||||
help="find and process files in subdirectories",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-a",
|
||||
"--aggregate",
|
||||
dest="agg_type",
|
||||
action="store",
|
||||
default="file",
|
||||
type=str,
|
||||
choices=["file", "vuln"],
|
||||
help="aggregate output by vulnerability (default) or by filename",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--number",
|
||||
dest="context_lines",
|
||||
action="store",
|
||||
default=3,
|
||||
type=int,
|
||||
help="maximum number of code lines to output for each issue",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--configfile",
|
||||
dest="config_file",
|
||||
action="store",
|
||||
default=None,
|
||||
type=str,
|
||||
help="optional config file to use for selecting plugins and "
|
||||
"overriding defaults",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--profile",
|
||||
dest="profile",
|
||||
action="store",
|
||||
default=None,
|
||||
type=str,
|
||||
help="profile to use (defaults to executing all tests)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--tests",
|
||||
dest="tests",
|
||||
action="store",
|
||||
default=None,
|
||||
type=str,
|
||||
help="comma-separated list of test IDs to run",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--skip",
|
||||
dest="skips",
|
||||
action="store",
|
||||
default=None,
|
||||
type=str,
|
||||
help="comma-separated list of test IDs to skip",
|
||||
)
|
||||
severity_group = parser.add_mutually_exclusive_group(required=False)
|
||||
severity_group.add_argument(
|
||||
"-l",
|
||||
"--level",
|
||||
dest="severity",
|
||||
action="count",
|
||||
default=1,
|
||||
help="report only issues of a given severity level or "
|
||||
"higher (-l for LOW, -ll for MEDIUM, -lll for HIGH)",
|
||||
)
|
||||
severity_group.add_argument(
|
||||
"--severity-level",
|
||||
dest="severity_string",
|
||||
action="store",
|
||||
help="report only issues of a given severity level or higher."
|
||||
' "all" and "low" are likely to produce the same results, but it'
|
||||
" is possible for rules to be undefined which will"
|
||||
' not be listed in "low".',
|
||||
choices=["all", "low", "medium", "high"],
|
||||
)
|
||||
confidence_group = parser.add_mutually_exclusive_group(required=False)
|
||||
confidence_group.add_argument(
|
||||
"-i",
|
||||
"--confidence",
|
||||
dest="confidence",
|
||||
action="count",
|
||||
default=1,
|
||||
help="report only issues of a given confidence level or "
|
||||
"higher (-i for LOW, -ii for MEDIUM, -iii for HIGH)",
|
||||
)
|
||||
confidence_group.add_argument(
|
||||
"--confidence-level",
|
||||
dest="confidence_string",
|
||||
action="store",
|
||||
help="report only issues of a given confidence level or higher."
|
||||
' "all" and "low" are likely to produce the same results, but it'
|
||||
" is possible for rules to be undefined which will"
|
||||
' not be listed in "low".',
|
||||
choices=["all", "low", "medium", "high"],
|
||||
)
|
||||
output_format = (
|
||||
"screen"
|
||||
if (
|
||||
sys.stdout.isatty()
|
||||
and os.getenv("NO_COLOR") is None
|
||||
and os.getenv("TERM") != "dumb"
|
||||
)
|
||||
else "txt"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--format",
|
||||
dest="output_format",
|
||||
action="store",
|
||||
default=output_format,
|
||||
help="specify output format",
|
||||
choices=sorted(extension_mgr.formatter_names),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--msg-template",
|
||||
action="store",
|
||||
default=None,
|
||||
help="specify output message template"
|
||||
" (only usable with --format custom),"
|
||||
" see CUSTOM FORMAT section"
|
||||
" for list of available values",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
dest="output_file",
|
||||
action="store",
|
||||
nargs="?",
|
||||
type=argparse.FileType("w", encoding="utf-8"),
|
||||
default=sys.stdout,
|
||||
help="write report to filename",
|
||||
)
|
||||
group = parser.add_mutually_exclusive_group(required=False)
|
||||
group.add_argument(
|
||||
"-v",
|
||||
"--verbose",
|
||||
dest="verbose",
|
||||
action="store_true",
|
||||
help="output extra information like excluded and included files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--debug",
|
||||
dest="debug",
|
||||
action="store_true",
|
||||
help="turn on debug mode",
|
||||
)
|
||||
group.add_argument(
|
||||
"-q",
|
||||
"--quiet",
|
||||
"--silent",
|
||||
dest="quiet",
|
||||
action="store_true",
|
||||
help="only show output in the case of an error",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ignore-nosec",
|
||||
dest="ignore_nosec",
|
||||
action="store_true",
|
||||
help="do not skip lines with # nosec comments",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-x",
|
||||
"--exclude",
|
||||
dest="excluded_paths",
|
||||
action="store",
|
||||
default=",".join(constants.EXCLUDE),
|
||||
help="comma-separated list of paths (glob patterns "
|
||||
"supported) to exclude from scan "
|
||||
"(note that these are in addition to the excluded "
|
||||
"paths provided in the config file) (default: "
|
||||
+ ",".join(constants.EXCLUDE)
|
||||
+ ")",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-b",
|
||||
"--baseline",
|
||||
dest="baseline",
|
||||
action="store",
|
||||
default=None,
|
||||
help="path of a baseline report to compare against "
|
||||
"(only JSON-formatted files are accepted)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ini",
|
||||
dest="ini_path",
|
||||
action="store",
|
||||
default=None,
|
||||
help="path to a .bandit file that supplies command line arguments",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--exit-zero",
|
||||
action="store_true",
|
||||
dest="exit_zero",
|
||||
default=False,
|
||||
help="exit with 0, " "even with results found",
|
||||
)
|
||||
python_ver = sys.version.replace("\n", "")
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
action="version",
|
||||
version=f"%(prog)s {bandit.__version__}\n"
|
||||
f" python version = {python_ver}",
|
||||
)
|
||||
|
||||
parser.set_defaults(debug=False)
|
||||
parser.set_defaults(verbose=False)
|
||||
parser.set_defaults(quiet=False)
|
||||
parser.set_defaults(ignore_nosec=False)
|
||||
|
||||
plugin_info = [
|
||||
f"{a[0]}\t{a[1].name}" for a in extension_mgr.plugins_by_id.items()
|
||||
]
|
||||
blacklist_info = []
|
||||
for a in extension_mgr.blacklist.items():
|
||||
for b in a[1]:
|
||||
blacklist_info.append(f"{b['id']}\t{b['name']}")
|
||||
|
||||
plugin_list = "\n\t".join(sorted(set(plugin_info + blacklist_info)))
|
||||
dedent_text = textwrap.dedent(
|
||||
"""
|
||||
CUSTOM FORMATTING
|
||||
-----------------
|
||||
|
||||
Available tags:
|
||||
|
||||
{abspath}, {relpath}, {line}, {col}, {test_id},
|
||||
{severity}, {msg}, {confidence}, {range}
|
||||
|
||||
Example usage:
|
||||
|
||||
Default template:
|
||||
bandit -r examples/ --format custom --msg-template \\
|
||||
"{abspath}:{line}: {test_id}[bandit]: {severity}: {msg}"
|
||||
|
||||
Provides same output as:
|
||||
bandit -r examples/ --format custom
|
||||
|
||||
Tags can also be formatted in python string.format() style:
|
||||
bandit -r examples/ --format custom --msg-template \\
|
||||
"{relpath:20.20s}: {line:03}: {test_id:^8}: DEFECT: {msg:>20}"
|
||||
|
||||
See python documentation for more information about formatting style:
|
||||
https://docs.python.org/3/library/string.html
|
||||
|
||||
The following tests were discovered and loaded:
|
||||
-----------------------------------------------
|
||||
"""
|
||||
)
|
||||
parser.epilog = dedent_text + f"\t{plugin_list}"
|
||||
|
||||
# setup work - parse arguments, and initialize BanditManager
|
||||
args = parser.parse_args()
|
||||
# Check if `--msg-template` is not present without custom formatter
|
||||
if args.output_format != "custom" and args.msg_template is not None:
|
||||
parser.error("--msg-template can only be used with --format=custom")
|
||||
|
||||
# Check if confidence or severity level have been specified with strings
|
||||
if args.severity_string is not None:
|
||||
if args.severity_string == "all":
|
||||
args.severity = 1
|
||||
elif args.severity_string == "low":
|
||||
args.severity = 2
|
||||
elif args.severity_string == "medium":
|
||||
args.severity = 3
|
||||
elif args.severity_string == "high":
|
||||
args.severity = 4
|
||||
# Other strings will be blocked by argparse
|
||||
|
||||
if args.confidence_string is not None:
|
||||
if args.confidence_string == "all":
|
||||
args.confidence = 1
|
||||
elif args.confidence_string == "low":
|
||||
args.confidence = 2
|
||||
elif args.confidence_string == "medium":
|
||||
args.confidence = 3
|
||||
elif args.confidence_string == "high":
|
||||
args.confidence = 4
|
||||
# Other strings will be blocked by argparse
|
||||
|
||||
# Handle .bandit files in projects to pass cmdline args from file
|
||||
ini_options = _get_options_from_ini(args.ini_path, args.targets)
|
||||
if ini_options:
|
||||
# prefer command line, then ini file
|
||||
args.config_file = _log_option_source(
|
||||
parser.get_default("configfile"),
|
||||
args.config_file,
|
||||
ini_options.get("configfile"),
|
||||
"config file",
|
||||
)
|
||||
|
||||
args.excluded_paths = _log_option_source(
|
||||
parser.get_default("excluded_paths"),
|
||||
args.excluded_paths,
|
||||
ini_options.get("exclude"),
|
||||
"excluded paths",
|
||||
)
|
||||
|
||||
args.skips = _log_option_source(
|
||||
parser.get_default("skips"),
|
||||
args.skips,
|
||||
ini_options.get("skips"),
|
||||
"skipped tests",
|
||||
)
|
||||
|
||||
args.tests = _log_option_source(
|
||||
parser.get_default("tests"),
|
||||
args.tests,
|
||||
ini_options.get("tests"),
|
||||
"selected tests",
|
||||
)
|
||||
|
||||
ini_targets = ini_options.get("targets")
|
||||
if ini_targets:
|
||||
ini_targets = ini_targets.split(",")
|
||||
|
||||
args.targets = _log_option_source(
|
||||
parser.get_default("targets"),
|
||||
args.targets,
|
||||
ini_targets,
|
||||
"selected targets",
|
||||
)
|
||||
|
||||
# TODO(tmcpeak): any other useful options to pass from .bandit?
|
||||
|
||||
args.recursive = _log_option_source(
|
||||
parser.get_default("recursive"),
|
||||
args.recursive,
|
||||
ini_options.get("recursive"),
|
||||
"recursive scan",
|
||||
)
|
||||
|
||||
args.agg_type = _log_option_source(
|
||||
parser.get_default("agg_type"),
|
||||
args.agg_type,
|
||||
ini_options.get("aggregate"),
|
||||
"aggregate output type",
|
||||
)
|
||||
|
||||
args.context_lines = _log_option_source(
|
||||
parser.get_default("context_lines"),
|
||||
args.context_lines,
|
||||
int(ini_options.get("number") or 0) or None,
|
||||
"max code lines output for issue",
|
||||
)
|
||||
|
||||
args.profile = _log_option_source(
|
||||
parser.get_default("profile"),
|
||||
args.profile,
|
||||
ini_options.get("profile"),
|
||||
"profile",
|
||||
)
|
||||
|
||||
args.severity = _log_option_source(
|
||||
parser.get_default("severity"),
|
||||
args.severity,
|
||||
ini_options.get("level"),
|
||||
"severity level",
|
||||
)
|
||||
|
||||
args.confidence = _log_option_source(
|
||||
parser.get_default("confidence"),
|
||||
args.confidence,
|
||||
ini_options.get("confidence"),
|
||||
"confidence level",
|
||||
)
|
||||
|
||||
args.output_format = _log_option_source(
|
||||
parser.get_default("output_format"),
|
||||
args.output_format,
|
||||
ini_options.get("format"),
|
||||
"output format",
|
||||
)
|
||||
|
||||
args.msg_template = _log_option_source(
|
||||
parser.get_default("msg_template"),
|
||||
args.msg_template,
|
||||
ini_options.get("msg-template"),
|
||||
"output message template",
|
||||
)
|
||||
|
||||
args.output_file = _log_option_source(
|
||||
parser.get_default("output_file"),
|
||||
args.output_file,
|
||||
ini_options.get("output"),
|
||||
"output file",
|
||||
)
|
||||
|
||||
args.verbose = _log_option_source(
|
||||
parser.get_default("verbose"),
|
||||
args.verbose,
|
||||
ini_options.get("verbose"),
|
||||
"output extra information",
|
||||
)
|
||||
|
||||
args.debug = _log_option_source(
|
||||
parser.get_default("debug"),
|
||||
args.debug,
|
||||
ini_options.get("debug"),
|
||||
"debug mode",
|
||||
)
|
||||
|
||||
args.quiet = _log_option_source(
|
||||
parser.get_default("quiet"),
|
||||
args.quiet,
|
||||
ini_options.get("quiet"),
|
||||
"silent mode",
|
||||
)
|
||||
|
||||
args.ignore_nosec = _log_option_source(
|
||||
parser.get_default("ignore_nosec"),
|
||||
args.ignore_nosec,
|
||||
ini_options.get("ignore-nosec"),
|
||||
"do not skip lines with # nosec",
|
||||
)
|
||||
|
||||
args.baseline = _log_option_source(
|
||||
parser.get_default("baseline"),
|
||||
args.baseline,
|
||||
ini_options.get("baseline"),
|
||||
"path of a baseline report",
|
||||
)
|
||||
|
||||
try:
|
||||
b_conf = b_config.BanditConfig(config_file=args.config_file)
|
||||
except utils.ConfigError as e:
|
||||
LOG.error(e)
|
||||
sys.exit(2)
|
||||
|
||||
if not args.targets:
|
||||
parser.print_usage()
|
||||
sys.exit(2)
|
||||
|
||||
# if the log format string was set in the options, reinitialize
|
||||
if b_conf.get_option("log_format"):
|
||||
log_format = b_conf.get_option("log_format")
|
||||
_init_logger(log_level=logging.DEBUG, log_format=log_format)
|
||||
|
||||
if args.quiet:
|
||||
_init_logger(log_level=logging.WARN)
|
||||
|
||||
try:
|
||||
profile = _get_profile(b_conf, args.profile, args.config_file)
|
||||
_log_info(args, profile)
|
||||
|
||||
profile["include"].update(args.tests.split(",") if args.tests else [])
|
||||
profile["exclude"].update(args.skips.split(",") if args.skips else [])
|
||||
extension_mgr.validate_profile(profile)
|
||||
|
||||
except (utils.ProfileNotFound, ValueError) as e:
|
||||
LOG.error(e)
|
||||
sys.exit(2)
|
||||
|
||||
b_mgr = b_manager.BanditManager(
|
||||
b_conf,
|
||||
args.agg_type,
|
||||
args.debug,
|
||||
profile=profile,
|
||||
verbose=args.verbose,
|
||||
quiet=args.quiet,
|
||||
ignore_nosec=args.ignore_nosec,
|
||||
)
|
||||
|
||||
if args.baseline is not None:
|
||||
try:
|
||||
with open(args.baseline) as bl:
|
||||
data = bl.read()
|
||||
b_mgr.populate_baseline(data)
|
||||
except OSError:
|
||||
LOG.warning("Could not open baseline report: %s", args.baseline)
|
||||
sys.exit(2)
|
||||
|
||||
if args.output_format not in baseline_formatters:
|
||||
LOG.warning(
|
||||
"Baseline must be used with one of the following "
|
||||
"formats: " + str(baseline_formatters)
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
if args.output_format != "json":
|
||||
if args.config_file:
|
||||
LOG.info("using config: %s", args.config_file)
|
||||
|
||||
LOG.info(
|
||||
"running on Python %d.%d.%d",
|
||||
sys.version_info.major,
|
||||
sys.version_info.minor,
|
||||
sys.version_info.micro,
|
||||
)
|
||||
|
||||
# initiate file discovery step within Bandit Manager
|
||||
b_mgr.discover_files(args.targets, args.recursive, args.excluded_paths)
|
||||
|
||||
if not b_mgr.b_ts.tests:
|
||||
LOG.error("No tests would be run, please check the profile.")
|
||||
sys.exit(2)
|
||||
|
||||
# initiate execution of tests within Bandit Manager
|
||||
b_mgr.run_tests()
|
||||
LOG.debug(b_mgr.b_ma)
|
||||
LOG.debug(b_mgr.metrics)
|
||||
|
||||
# trigger output of results by Bandit Manager
|
||||
sev_level = constants.RANKING[args.severity - 1]
|
||||
conf_level = constants.RANKING[args.confidence - 1]
|
||||
b_mgr.output_results(
|
||||
args.context_lines,
|
||||
sev_level,
|
||||
conf_level,
|
||||
args.output_file,
|
||||
args.output_format,
|
||||
args.msg_template,
|
||||
)
|
||||
|
||||
if (
|
||||
b_mgr.results_count(sev_filter=sev_level, conf_filter=conf_level) > 0
|
||||
and not args.exit_zero
|
||||
):
|
||||
sys.exit(1)
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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,72 @@
|
||||
#
|
||||
# Copyright 2016 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import ast
|
||||
|
||||
from bandit.core import issue
|
||||
|
||||
|
||||
def report_issue(check, name):
|
||||
return issue.Issue(
|
||||
severity=check.get("level", "MEDIUM"),
|
||||
confidence="HIGH",
|
||||
cwe=check.get("cwe", issue.Cwe.NOTSET),
|
||||
text=check["message"].replace("{name}", name),
|
||||
ident=name,
|
||||
test_id=check.get("id", "LEGACY"),
|
||||
)
|
||||
|
||||
|
||||
def blacklist(context, config):
|
||||
"""Generic blacklist test, B001.
|
||||
|
||||
This generic blacklist test will be called for any encountered node with
|
||||
defined blacklist data available. This data is loaded via plugins using
|
||||
the 'bandit.blacklists' entry point. Please see the documentation for more
|
||||
details. Each blacklist datum has a unique bandit ID that may be used for
|
||||
filtering purposes, or alternatively all blacklisting can be filtered using
|
||||
the id of this built in test, 'B001'.
|
||||
"""
|
||||
blacklists = config
|
||||
node_type = context.node.__class__.__name__
|
||||
|
||||
if node_type == "Call":
|
||||
func = context.node.func
|
||||
if isinstance(func, ast.Name) and func.id == "__import__":
|
||||
if len(context.node.args):
|
||||
if isinstance(
|
||||
context.node.args[0], ast.Constant
|
||||
) and isinstance(context.node.args[0].value, str):
|
||||
name = context.node.args[0].value
|
||||
else:
|
||||
# TODO(??): import through a variable, need symbol tab
|
||||
name = "UNKNOWN"
|
||||
else:
|
||||
name = "" # handle '__import__()'
|
||||
else:
|
||||
name = context.call_function_name_qual
|
||||
# In the case the Call is an importlib.import, treat the first
|
||||
# argument name as an actual import module name.
|
||||
# Will produce None if argument is not a literal or identifier
|
||||
if name in ["importlib.import_module", "importlib.__import__"]:
|
||||
if context.call_args_count > 0:
|
||||
name = context.call_args[0]
|
||||
else:
|
||||
name = context.call_keywords["name"]
|
||||
for check in blacklists[node_type]:
|
||||
for qn in check["qualnames"]:
|
||||
if name is not None and name == qn:
|
||||
return report_issue(check, name)
|
||||
|
||||
if node_type.startswith("Import"):
|
||||
prefix = ""
|
||||
if node_type == "ImportFrom":
|
||||
if context.node.module is not None:
|
||||
prefix = context.node.module + "."
|
||||
|
||||
for check in blacklists[node_type]:
|
||||
for name in context.node.names:
|
||||
for qn in check["qualnames"]:
|
||||
if (prefix + name.name).startswith(qn):
|
||||
return report_issue(check, name.name)
|
||||
@@ -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,316 @@
|
||||
#
|
||||
# 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.Constant):
|
||||
if isinstance(literal.value, bool):
|
||||
literal_value = str(literal.value)
|
||||
elif literal.value is None:
|
||||
literal_value = str(literal.value)
|
||||
else:
|
||||
literal_value = literal.value
|
||||
|
||||
elif isinstance(literal, ast.List):
|
||||
return_list = list()
|
||||
for li in literal.elts:
|
||||
return_list.append(self._get_literal_value(li))
|
||||
literal_value = return_list
|
||||
|
||||
elif isinstance(literal, ast.Tuple):
|
||||
return_tuple = tuple()
|
||||
for ti in literal.elts:
|
||||
return_tuple += (self._get_literal_value(ti),)
|
||||
literal_value = return_tuple
|
||||
|
||||
elif isinstance(literal, ast.Set):
|
||||
return_set = set()
|
||||
for si in literal.elts:
|
||||
return_set.add(self._get_literal_value(si))
|
||||
literal_value = return_set
|
||||
|
||||
elif isinstance(literal, ast.Dict):
|
||||
literal_value = dict(zip(literal.keys, literal.values))
|
||||
|
||||
elif isinstance(literal, ast.Name):
|
||||
literal_value = literal.id
|
||||
|
||||
else:
|
||||
literal_value = None
|
||||
|
||||
return literal_value
|
||||
|
||||
def get_call_arg_value(self, argument_name):
|
||||
"""Gets the value of a named argument in a function call.
|
||||
|
||||
:return: named argument value
|
||||
"""
|
||||
kwd_values = self.call_keywords
|
||||
if kwd_values is not None and argument_name in kwd_values:
|
||||
return kwd_values[argument_name]
|
||||
|
||||
def check_call_arg_value(self, argument_name, argument_values=None):
|
||||
"""Checks for a value of a named argument in a function call.
|
||||
|
||||
Returns none if the specified argument is not found.
|
||||
:param argument_name: A string - name of the argument to look for
|
||||
:param argument_values: the value, or list of values to test against
|
||||
:return: Boolean True if argument found and matched, False if
|
||||
found and not matched, None if argument not found at all
|
||||
"""
|
||||
arg_value = self.get_call_arg_value(argument_name)
|
||||
if arg_value is not None:
|
||||
if not isinstance(argument_values, list):
|
||||
# if passed a single value, or a tuple, convert to a list
|
||||
argument_values = list((argument_values,))
|
||||
for val in argument_values:
|
||||
if arg_value == val:
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
# argument name not found, return None to allow testing for this
|
||||
# eventuality
|
||||
return None
|
||||
|
||||
def get_lineno_for_call_arg(self, argument_name):
|
||||
"""Get the line number for a specific named argument
|
||||
|
||||
In case the call is split over multiple lines, get the correct one for
|
||||
the argument.
|
||||
:param argument_name: A string - name of the argument to look for
|
||||
:return: Integer - the line number of the found argument, or -1
|
||||
"""
|
||||
if hasattr(self.node, "keywords"):
|
||||
for key in self.node.keywords:
|
||||
if key.arg == argument_name:
|
||||
return key.value.lineno
|
||||
|
||||
def get_call_arg_at_position(self, position_num):
|
||||
"""Returns positional argument at the specified position (if it exists)
|
||||
|
||||
:param position_num: The index of the argument to return the value for
|
||||
:return: Value of the argument at the specified position if it exists
|
||||
"""
|
||||
max_args = self.call_args_count
|
||||
if max_args and position_num < max_args:
|
||||
arg = self._context["call"].args[position_num]
|
||||
return getattr(arg, "attr", None) or self._get_literal_value(arg)
|
||||
else:
|
||||
return None
|
||||
|
||||
def is_module_being_imported(self, module):
|
||||
"""Check for the specified module is currently being imported
|
||||
|
||||
:param module: The module name to look for
|
||||
:return: True if the module is found, False otherwise
|
||||
"""
|
||||
return self._context.get("module") == module
|
||||
|
||||
def is_module_imported_exact(self, module):
|
||||
"""Check if a specified module has been imported; only exact matches.
|
||||
|
||||
:param module: The module name to look for
|
||||
:return: True if the module is found, False otherwise
|
||||
"""
|
||||
return module in self._context.get("imports", [])
|
||||
|
||||
def is_module_imported_like(self, module):
|
||||
"""Check if a specified module has been imported
|
||||
|
||||
Check if a specified module has been imported; specified module exists
|
||||
as part of any import statement.
|
||||
:param module: The module name to look for
|
||||
:return: True if the module is found, False otherwise
|
||||
"""
|
||||
if "imports" in self._context:
|
||||
for imp in self._context["imports"]:
|
||||
if module in imp:
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def filename(self):
|
||||
return self._context.get("filename")
|
||||
|
||||
@property
|
||||
def file_data(self):
|
||||
return self._context.get("file_data")
|
||||
|
||||
@property
|
||||
def import_aliases(self):
|
||||
return self._context.get("import_aliases")
|
||||
@@ -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 maintain a list of files which are added, and ones which have
|
||||
# been explicitly excluded
|
||||
files_list = set()
|
||||
excluded_files = set()
|
||||
|
||||
excluded_path_globs = self.b_conf.get_option("exclude_dirs") or []
|
||||
included_globs = self.b_conf.get_option("include") or ["*.py"]
|
||||
|
||||
# if there are command line provided exclusions add them to the list
|
||||
if excluded_paths:
|
||||
for path in excluded_paths.split(","):
|
||||
if os.path.isdir(path):
|
||||
path = os.path.join(path, "*")
|
||||
|
||||
excluded_path_globs.append(path)
|
||||
|
||||
# build list of files we will analyze
|
||||
for fname in targets:
|
||||
# if this is a directory and recursive is set, find all files
|
||||
if os.path.isdir(fname):
|
||||
if recursive:
|
||||
new_files, newly_excluded = _get_files_from_dir(
|
||||
fname,
|
||||
included_globs=included_globs,
|
||||
excluded_path_strings=excluded_path_globs,
|
||||
)
|
||||
files_list.update(new_files)
|
||||
excluded_files.update(newly_excluded)
|
||||
else:
|
||||
LOG.warning(
|
||||
"Skipping directory (%s), use -r flag to "
|
||||
"scan contents",
|
||||
fname,
|
||||
)
|
||||
|
||||
else:
|
||||
# if the user explicitly mentions a file on command line,
|
||||
# we'll scan it, regardless of whether it's in the included
|
||||
# file types list
|
||||
if _is_file_included(
|
||||
fname,
|
||||
included_globs,
|
||||
excluded_path_globs,
|
||||
enforce_glob=False,
|
||||
):
|
||||
if fname != "-":
|
||||
fname = os.path.join(".", fname)
|
||||
files_list.add(fname)
|
||||
else:
|
||||
excluded_files.add(fname)
|
||||
|
||||
self.files_list = sorted(files_list)
|
||||
self.excluded_files = sorted(excluded_files)
|
||||
|
||||
def run_tests(self):
|
||||
"""Runs through all files in the scope
|
||||
|
||||
:return: -
|
||||
"""
|
||||
# if we have problems with a file, we'll remove it from the files_list
|
||||
# and add it to the skipped list instead
|
||||
new_files_list = list(self.files_list)
|
||||
if (
|
||||
len(self.files_list) > PROGRESS_THRESHOLD
|
||||
and LOG.getEffectiveLevel() <= logging.INFO
|
||||
):
|
||||
files = progress.track(self.files_list)
|
||||
else:
|
||||
files = self.files_list
|
||||
|
||||
for count, fname in enumerate(files):
|
||||
LOG.debug("working on file : %s", fname)
|
||||
|
||||
try:
|
||||
if fname == "-":
|
||||
open_fd = os.fdopen(sys.stdin.fileno(), "rb", 0)
|
||||
fdata = io.BytesIO(open_fd.read())
|
||||
new_files_list = [
|
||||
"<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.value
|
||||
if not isinstance(node._bandit_parent, ast.Expr): # docstring
|
||||
self.context["linerange"] = b_utils.linerange(node._bandit_parent)
|
||||
self.update_scores(self.tester.run_tests(self.context, "Str"))
|
||||
|
||||
def visit_Bytes(self, node):
|
||||
"""Visitor for AST Bytes nodes
|
||||
|
||||
add relevant information about node to
|
||||
the context for use in tests which inspect strings.
|
||||
:param node: The node that is being inspected
|
||||
:return: -
|
||||
"""
|
||||
self.context["bytes"] = node.value
|
||||
if not isinstance(node._bandit_parent, ast.Expr): # docstring
|
||||
self.context["linerange"] = b_utils.linerange(node._bandit_parent)
|
||||
self.update_scores(self.tester.run_tests(self.context, "Bytes"))
|
||||
|
||||
def pre_visit(self, node):
|
||||
self.context = {}
|
||||
self.context["imports"] = self.imports
|
||||
self.context["import_aliases"] = self.import_aliases
|
||||
|
||||
if self.debug:
|
||||
LOG.debug(ast.dump(node))
|
||||
self.metaast.add_node(node, "", self.depth)
|
||||
|
||||
if hasattr(node, "lineno"):
|
||||
self.context["lineno"] = node.lineno
|
||||
|
||||
if hasattr(node, "col_offset"):
|
||||
self.context["col_offset"] = node.col_offset
|
||||
if hasattr(node, "end_col_offset"):
|
||||
self.context["end_col_offset"] = node.end_col_offset
|
||||
|
||||
self.context["node"] = node
|
||||
self.context["linerange"] = b_utils.linerange(node)
|
||||
self.context["filename"] = self.fname
|
||||
self.context["file_data"] = self.fdata
|
||||
|
||||
LOG.debug(
|
||||
"entering: %s %s [%s]", hex(id(node)), type(node), self.depth
|
||||
)
|
||||
self.depth += 1
|
||||
LOG.debug(self.context)
|
||||
return True
|
||||
|
||||
def visit(self, node):
|
||||
name = node.__class__.__name__
|
||||
method = "visit_" + name
|
||||
visitor = getattr(self, method, None)
|
||||
if visitor is not None:
|
||||
if self.debug:
|
||||
LOG.debug("%s called (%s)", method, ast.dump(node))
|
||||
visitor(node)
|
||||
else:
|
||||
self.update_scores(self.tester.run_tests(self.context, name))
|
||||
|
||||
def post_visit(self, node):
|
||||
self.depth -= 1
|
||||
LOG.debug("%s\texiting : %s", self.depth, hex(id(node)))
|
||||
|
||||
# HACK(tkelsey): this is needed to clean up post-recursion stuff that
|
||||
# gets setup in the visit methods for these node types.
|
||||
if isinstance(node, (ast.FunctionDef, ast.ClassDef)):
|
||||
self.namespace = b_utils.namespace_path_split(self.namespace)[0]
|
||||
|
||||
def generic_visit(self, node):
|
||||
"""Drive the visitor."""
|
||||
for _, value in ast.iter_fields(node):
|
||||
if isinstance(value, list):
|
||||
max_idx = len(value) - 1
|
||||
for idx, item in enumerate(value):
|
||||
if isinstance(item, ast.AST):
|
||||
if idx < max_idx:
|
||||
item._bandit_sibling = value[idx + 1]
|
||||
else:
|
||||
item._bandit_sibling = None
|
||||
item._bandit_parent = node
|
||||
|
||||
if self.pre_visit(item):
|
||||
self.visit(item)
|
||||
self.generic_visit(item)
|
||||
self.post_visit(item)
|
||||
|
||||
elif isinstance(value, ast.AST):
|
||||
value._bandit_sibling = None
|
||||
value._bandit_parent = node
|
||||
if self.pre_visit(value):
|
||||
self.visit(value)
|
||||
self.generic_visit(value)
|
||||
self.post_visit(value)
|
||||
|
||||
def update_scores(self, scores):
|
||||
"""Score updater
|
||||
|
||||
Since we moved from a single score value to a map of scores per
|
||||
severity, this is needed to update the stored list.
|
||||
:param score: The score list to update our scores with
|
||||
"""
|
||||
# we'll end up with something like:
|
||||
# SEVERITY: {0, 0, 0, 10} where 10 is weighted by finding and level
|
||||
for score_type in self.scores:
|
||||
self.scores[score_type] = list(
|
||||
map(operator.add, self.scores[score_type], scores[score_type])
|
||||
)
|
||||
|
||||
def process(self, data):
|
||||
"""Main process loop
|
||||
|
||||
Build and process the AST
|
||||
:param lines: lines code to process
|
||||
:return score: the aggregated score for the current file
|
||||
"""
|
||||
f_ast = ast.parse(data)
|
||||
self.generic_visit(f_ast)
|
||||
# Run tests that do not require access to the AST,
|
||||
# but only to the whole file source:
|
||||
self.context = {
|
||||
"file_data": self.fdata,
|
||||
"filename": self.fname,
|
||||
"lineno": 0,
|
||||
"linerange": [0, 1],
|
||||
"col_offset": 0,
|
||||
}
|
||||
self.update_scores(self.tester.run_tests(self.context, "File"))
|
||||
return self.scores
|
||||
@@ -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,168 @@
|
||||
#
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import copy
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
from bandit.core import constants
|
||||
from bandit.core import context as b_context
|
||||
from bandit.core import utils
|
||||
|
||||
warnings.formatwarning = utils.warnings_formatter
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BanditTester:
|
||||
def __init__(self, testset, debug, nosec_lines, metrics):
|
||||
self.results = []
|
||||
self.testset = testset
|
||||
self.last_result = None
|
||||
self.debug = debug
|
||||
self.nosec_lines = nosec_lines
|
||||
self.metrics = metrics
|
||||
|
||||
def run_tests(self, raw_context, checktype):
|
||||
"""Runs all tests for a certain type of check, for example
|
||||
|
||||
Runs all tests for a certain type of check, for example 'functions'
|
||||
store results in results.
|
||||
|
||||
:param raw_context: Raw context dictionary
|
||||
:param checktype: The type of checks to run
|
||||
:return: a score based on the number and type of test results with
|
||||
extra metrics about nosec comments
|
||||
"""
|
||||
|
||||
scores = {
|
||||
"SEVERITY": [0] * len(constants.RANKING),
|
||||
"CONFIDENCE": [0] * len(constants.RANKING),
|
||||
}
|
||||
|
||||
tests = self.testset.get_tests(checktype)
|
||||
for test in tests:
|
||||
name = test.__name__
|
||||
# execute test with an instance of the context class
|
||||
temp_context = copy.copy(raw_context)
|
||||
context = b_context.Context(temp_context)
|
||||
try:
|
||||
if hasattr(test, "_config"):
|
||||
result = test(context, test._config)
|
||||
else:
|
||||
result = test(context)
|
||||
|
||||
if result is not None:
|
||||
nosec_tests_to_skip = self._get_nosecs_from_contexts(
|
||||
temp_context, test_result=result
|
||||
)
|
||||
|
||||
if isinstance(temp_context["filename"], bytes):
|
||||
result.fname = temp_context["filename"].decode("utf-8")
|
||||
else:
|
||||
result.fname = temp_context["filename"]
|
||||
result.fdata = temp_context["file_data"]
|
||||
|
||||
if result.lineno is None:
|
||||
result.lineno = temp_context["lineno"]
|
||||
if result.linerange == []:
|
||||
result.linerange = temp_context["linerange"]
|
||||
if result.col_offset == -1:
|
||||
result.col_offset = temp_context["col_offset"]
|
||||
result.end_col_offset = temp_context.get(
|
||||
"end_col_offset", 0
|
||||
)
|
||||
result.test = name
|
||||
if result.test_id == "":
|
||||
result.test_id = test._test_id
|
||||
|
||||
# don't skip the test if there was no nosec comment
|
||||
if nosec_tests_to_skip is not None:
|
||||
# If the set is empty then it means that nosec was
|
||||
# used without test number -> update nosecs counter.
|
||||
# If the test id is in the set of tests to skip,
|
||||
# log and increment the skip by test count.
|
||||
if not nosec_tests_to_skip:
|
||||
LOG.debug("skipped, nosec without test number")
|
||||
self.metrics.note_nosec()
|
||||
continue
|
||||
if result.test_id in nosec_tests_to_skip:
|
||||
LOG.debug(
|
||||
f"skipped, nosec for test {result.test_id}"
|
||||
)
|
||||
self.metrics.note_skipped_test()
|
||||
continue
|
||||
|
||||
self.results.append(result)
|
||||
|
||||
LOG.debug("Issue identified by %s: %s", name, result)
|
||||
sev = constants.RANKING.index(result.severity)
|
||||
val = constants.RANKING_VALUES[result.severity]
|
||||
scores["SEVERITY"][sev] += val
|
||||
con = constants.RANKING.index(result.confidence)
|
||||
val = constants.RANKING_VALUES[result.confidence]
|
||||
scores["CONFIDENCE"][con] += val
|
||||
else:
|
||||
nosec_tests_to_skip = self._get_nosecs_from_contexts(
|
||||
temp_context
|
||||
)
|
||||
if (
|
||||
nosec_tests_to_skip
|
||||
and test._test_id in nosec_tests_to_skip
|
||||
):
|
||||
LOG.warning(
|
||||
f"nosec encountered ({test._test_id}), but no "
|
||||
f"failed test on file "
|
||||
f"{temp_context['filename']}:"
|
||||
f"{temp_context['lineno']}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.report_error(name, context, e)
|
||||
if self.debug:
|
||||
raise
|
||||
LOG.debug("Returning scores: %s", scores)
|
||||
return scores
|
||||
|
||||
def _get_nosecs_from_contexts(self, context, test_result=None):
|
||||
"""Use context and optional test result to get set of tests to skip.
|
||||
:param context: temp context
|
||||
:param test_result: optional test result
|
||||
:return: set of tests to skip for the line based on contexts
|
||||
"""
|
||||
nosec_tests_to_skip = set()
|
||||
base_tests = (
|
||||
self.nosec_lines.get(test_result.lineno, None)
|
||||
if test_result
|
||||
else None
|
||||
)
|
||||
context_tests = utils.get_nosec(self.nosec_lines, context)
|
||||
|
||||
# if both are none there were no comments
|
||||
# this is explicitly different from being empty.
|
||||
# empty set indicates blanket nosec comment without
|
||||
# individual test names or ids
|
||||
if base_tests is None and context_tests is None:
|
||||
nosec_tests_to_skip = None
|
||||
|
||||
# combine tests from current line and context line
|
||||
if base_tests is not None:
|
||||
nosec_tests_to_skip.update(base_tests)
|
||||
if context_tests is not None:
|
||||
nosec_tests_to_skip.update(context_tests)
|
||||
|
||||
return nosec_tests_to_skip
|
||||
|
||||
@staticmethod
|
||||
def report_error(test, context, error):
|
||||
what = "Bandit internal error running: "
|
||||
what += f"{test} "
|
||||
what += "on file %s at line %i: " % (
|
||||
context._context["filename"],
|
||||
context._context["lineno"],
|
||||
)
|
||||
what += str(error)
|
||||
import traceback
|
||||
|
||||
what += traceback.format_exc()
|
||||
LOG.error(what)
|
||||
@@ -0,0 +1,398 @@
|
||||
#
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import ast
|
||||
import logging
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
try:
|
||||
import configparser
|
||||
except ImportError:
|
||||
import ConfigParser as configparser
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
"""Various helper functions."""
|
||||
|
||||
|
||||
def _get_attr_qual_name(node, aliases):
|
||||
"""Get a the full name for the attribute node.
|
||||
|
||||
This will resolve a pseudo-qualified name for the attribute
|
||||
rooted at node as long as all the deeper nodes are Names or
|
||||
Attributes. This will give you how the code referenced the name but
|
||||
will not tell you what the name actually refers to. If we
|
||||
encounter a node without a static name we punt with an
|
||||
empty string. If this encounters something more complex, such as
|
||||
foo.mylist[0](a,b) we just return empty string.
|
||||
|
||||
:param node: AST Name or Attribute node
|
||||
:param aliases: Import aliases dictionary
|
||||
:returns: Qualified name referred to by the attribute or name.
|
||||
"""
|
||||
if isinstance(node, ast.Name):
|
||||
if node.id in aliases:
|
||||
return aliases[node.id]
|
||||
return node.id
|
||||
elif isinstance(node, ast.Attribute):
|
||||
name = f"{_get_attr_qual_name(node.value, aliases)}.{node.attr}"
|
||||
if name in aliases:
|
||||
return aliases[name]
|
||||
return name
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def get_call_name(node, aliases):
|
||||
if isinstance(node.func, ast.Name):
|
||||
if deepgetattr(node, "func.id") in aliases:
|
||||
return aliases[deepgetattr(node, "func.id")]
|
||||
return deepgetattr(node, "func.id")
|
||||
elif isinstance(node.func, ast.Attribute):
|
||||
return _get_attr_qual_name(node.func, aliases)
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def get_func_name(node):
|
||||
return node.name # TODO(tkelsey): get that qualname using enclosing scope
|
||||
|
||||
|
||||
def get_qual_attr(node, aliases):
|
||||
if isinstance(node, ast.Attribute):
|
||||
try:
|
||||
val = deepgetattr(node, "value.id")
|
||||
if val in aliases:
|
||||
prefix = aliases[val]
|
||||
else:
|
||||
prefix = deepgetattr(node, "value.id")
|
||||
except Exception:
|
||||
# NOTE(tkelsey): degrade gracefully when we can't get the fully
|
||||
# qualified name for an attr, just return its base name.
|
||||
prefix = ""
|
||||
|
||||
return f"{prefix}.{node.attr}"
|
||||
else:
|
||||
return "" # TODO(tkelsey): process other node types
|
||||
|
||||
|
||||
def deepgetattr(obj, attr):
|
||||
"""Recurses through an attribute chain to get the ultimate value."""
|
||||
for key in attr.split("."):
|
||||
obj = getattr(obj, key)
|
||||
return obj
|
||||
|
||||
|
||||
class InvalidModulePath(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConfigError(Exception):
|
||||
"""Raised when the config file fails validation."""
|
||||
|
||||
def __init__(self, message, config_file):
|
||||
self.config_file = config_file
|
||||
self.message = f"{config_file} : {message}"
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class ProfileNotFound(Exception):
|
||||
"""Raised when chosen profile cannot be found."""
|
||||
|
||||
def __init__(self, config_file, profile):
|
||||
self.config_file = config_file
|
||||
self.profile = profile
|
||||
message = "Unable to find profile ({}) in config file: {}".format(
|
||||
self.profile,
|
||||
self.config_file,
|
||||
)
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
def warnings_formatter(
|
||||
message, category=UserWarning, filename="", lineno=-1, line=""
|
||||
):
|
||||
"""Monkey patch for warnings.warn to suppress cruft output."""
|
||||
return f"{message}\n"
|
||||
|
||||
|
||||
def get_module_qualname_from_path(path):
|
||||
"""Get the module's qualified name by analysis of the path.
|
||||
|
||||
Resolve the absolute pathname and eliminate symlinks. This could result in
|
||||
an incorrect name if symlinks are used to restructure the python lib
|
||||
directory.
|
||||
|
||||
Starting from the right-most directory component look for __init__.py in
|
||||
the directory component. If it exists then the directory name is part of
|
||||
the module name. Move left to the subsequent directory components until a
|
||||
directory is found without __init__.py.
|
||||
|
||||
:param: Path to module file. Relative paths will be resolved relative to
|
||||
current working directory.
|
||||
:return: fully qualified module name
|
||||
"""
|
||||
|
||||
(head, tail) = os.path.split(path)
|
||||
if head == "" or tail == "":
|
||||
raise InvalidModulePath(
|
||||
f'Invalid python file path: "{path}" Missing path or file name'
|
||||
)
|
||||
|
||||
qname = [os.path.splitext(tail)[0]]
|
||||
while head not in ["/", ".", ""]:
|
||||
if os.path.isfile(os.path.join(head, "__init__.py")):
|
||||
(head, tail) = os.path.split(head)
|
||||
qname.insert(0, tail)
|
||||
else:
|
||||
break
|
||||
|
||||
qualname = ".".join(qname)
|
||||
return qualname
|
||||
|
||||
|
||||
def namespace_path_join(base, name):
|
||||
"""Extend the current namespace path with an additional name
|
||||
|
||||
Take a namespace path (i.e., package.module.class) and extends it
|
||||
with an additional name (i.e., package.module.class.subclass).
|
||||
This is similar to how os.path.join works.
|
||||
|
||||
:param base: (String) The base namespace path.
|
||||
:param name: (String) The new name to append to the base path.
|
||||
:returns: (String) A new namespace path resulting from combination of
|
||||
base and name.
|
||||
"""
|
||||
return f"{base}.{name}"
|
||||
|
||||
|
||||
def namespace_path_split(path):
|
||||
"""Split the namespace path into a pair (head, tail).
|
||||
|
||||
Tail will be the last namespace path component and head will
|
||||
be everything leading up to that in the path. This is similar to
|
||||
os.path.split.
|
||||
|
||||
:param path: (String) A namespace path.
|
||||
:returns: (String, String) A tuple where the first component is the base
|
||||
path and the second is the last path component.
|
||||
"""
|
||||
return tuple(path.rsplit(".", 1))
|
||||
|
||||
|
||||
def escaped_bytes_representation(b):
|
||||
"""PY3 bytes need escaping for comparison with other strings.
|
||||
|
||||
In practice it turns control characters into acceptable codepoints then
|
||||
encodes them into bytes again to turn unprintable bytes into printable
|
||||
escape sequences.
|
||||
|
||||
This is safe to do for the whole range 0..255 and result matches
|
||||
unicode_escape on a unicode string.
|
||||
"""
|
||||
return b.decode("unicode_escape").encode("unicode_escape")
|
||||
|
||||
|
||||
def calc_linerange(node):
|
||||
"""Calculate linerange for subtree"""
|
||||
if hasattr(node, "_bandit_linerange"):
|
||||
return node._bandit_linerange
|
||||
|
||||
lines_min = 9999999999
|
||||
lines_max = -1
|
||||
if hasattr(node, "lineno"):
|
||||
lines_min = node.lineno
|
||||
lines_max = node.lineno
|
||||
for n in ast.iter_child_nodes(node):
|
||||
lines_minmax = calc_linerange(n)
|
||||
lines_min = min(lines_min, lines_minmax[0])
|
||||
lines_max = max(lines_max, lines_minmax[1])
|
||||
|
||||
node._bandit_linerange = (lines_min, lines_max)
|
||||
|
||||
return (lines_min, lines_max)
|
||||
|
||||
|
||||
def linerange(node):
|
||||
"""Get line number range from a node."""
|
||||
if hasattr(node, "lineno"):
|
||||
return list(range(node.lineno, node.end_lineno + 1))
|
||||
else:
|
||||
if hasattr(node, "_bandit_linerange_stripped"):
|
||||
lines_minmax = node._bandit_linerange_stripped
|
||||
return list(range(lines_minmax[0], lines_minmax[1] + 1))
|
||||
|
||||
strip = {
|
||||
"body": None,
|
||||
"orelse": None,
|
||||
"handlers": None,
|
||||
"finalbody": None,
|
||||
}
|
||||
for key in strip.keys():
|
||||
if hasattr(node, key):
|
||||
strip[key] = getattr(node, key)
|
||||
setattr(node, key, [])
|
||||
|
||||
lines_min = 9999999999
|
||||
lines_max = -1
|
||||
if hasattr(node, "lineno"):
|
||||
lines_min = node.lineno
|
||||
lines_max = node.lineno
|
||||
for n in ast.iter_child_nodes(node):
|
||||
lines_minmax = calc_linerange(n)
|
||||
lines_min = min(lines_min, lines_minmax[0])
|
||||
lines_max = max(lines_max, lines_minmax[1])
|
||||
|
||||
for key in strip.keys():
|
||||
if strip[key] is not None:
|
||||
setattr(node, key, strip[key])
|
||||
|
||||
if lines_max == -1:
|
||||
lines_min = 0
|
||||
lines_max = 1
|
||||
|
||||
node._bandit_linerange_stripped = (lines_min, lines_max)
|
||||
|
||||
lines = list(range(lines_min, lines_max + 1))
|
||||
|
||||
"""Try and work around a known Python bug with multi-line strings."""
|
||||
# deal with multiline strings lineno behavior (Python issue #16806)
|
||||
if hasattr(node, "_bandit_sibling") and hasattr(
|
||||
node._bandit_sibling, "lineno"
|
||||
):
|
||||
start = min(lines)
|
||||
delta = node._bandit_sibling.lineno - start
|
||||
if delta > 1:
|
||||
return list(range(start, node._bandit_sibling.lineno))
|
||||
return lines
|
||||
|
||||
|
||||
def concat_string(node, stop=None):
|
||||
"""Builds a string from a ast.BinOp chain.
|
||||
|
||||
This will build a string from a series of ast.Constant nodes wrapped in
|
||||
ast.BinOp nodes. Something like "a" + "b" + "c" or "a %s" % val etc.
|
||||
The provided node can be any participant in the BinOp chain.
|
||||
|
||||
:param node: (ast.Constant or ast.BinOp) The node to process
|
||||
:param stop: (ast.Constant or ast.BinOp) Optional base node to stop at
|
||||
:returns: (Tuple) the root node of the expression, the string value
|
||||
"""
|
||||
|
||||
def _get(node, bits, stop=None):
|
||||
if node != stop:
|
||||
bits.append(
|
||||
_get(node.left, bits, stop)
|
||||
if isinstance(node.left, ast.BinOp)
|
||||
else node.left
|
||||
)
|
||||
bits.append(
|
||||
_get(node.right, bits, stop)
|
||||
if isinstance(node.right, ast.BinOp)
|
||||
else node.right
|
||||
)
|
||||
|
||||
bits = [node]
|
||||
while isinstance(node._bandit_parent, ast.BinOp):
|
||||
node = node._bandit_parent
|
||||
if isinstance(node, ast.BinOp):
|
||||
_get(node, bits, stop)
|
||||
return (
|
||||
node,
|
||||
" ".join(
|
||||
[
|
||||
x.value
|
||||
for x in bits
|
||||
if isinstance(x, ast.Constant) and isinstance(x.value, str)
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def get_called_name(node):
|
||||
"""Get a function name from an ast.Call node.
|
||||
|
||||
An ast.Call node representing a method call with present differently to one
|
||||
wrapping a function call: thing.call() vs call(). This helper will grab the
|
||||
unqualified call name correctly in either case.
|
||||
|
||||
:param node: (ast.Call) the call node
|
||||
:returns: (String) the function name
|
||||
"""
|
||||
func = node.func
|
||||
try:
|
||||
return func.attr if isinstance(func, ast.Attribute) else func.id
|
||||
except AttributeError:
|
||||
return ""
|
||||
|
||||
|
||||
def get_path_for_function(f):
|
||||
"""Get the path of the file where the function is defined.
|
||||
|
||||
:returns: the path, or None if one could not be found or f is not a real
|
||||
function
|
||||
"""
|
||||
|
||||
if hasattr(f, "__module__"):
|
||||
module_name = f.__module__
|
||||
elif hasattr(f, "im_func"):
|
||||
module_name = f.im_func.__module__
|
||||
else:
|
||||
LOG.warning("Cannot resolve file where %s is defined", f)
|
||||
return None
|
||||
|
||||
module = sys.modules[module_name]
|
||||
if hasattr(module, "__file__"):
|
||||
return module.__file__
|
||||
else:
|
||||
LOG.warning("Cannot resolve file path for module %s", module_name)
|
||||
return None
|
||||
|
||||
|
||||
def parse_ini_file(f_loc):
|
||||
config = configparser.ConfigParser()
|
||||
try:
|
||||
config.read(f_loc)
|
||||
return {k: v for k, v in config.items("bandit")}
|
||||
|
||||
except (configparser.Error, KeyError, TypeError):
|
||||
LOG.warning(
|
||||
"Unable to parse config file %s or missing [bandit] " "section",
|
||||
f_loc,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def check_ast_node(name):
|
||||
"Check if the given name is that of a valid AST node."
|
||||
try:
|
||||
# These ast Node types were deprecated in Python 3.12 and removed
|
||||
# in Python 3.14, but plugins may still check on them.
|
||||
if sys.version_info >= (3, 12) and name in (
|
||||
"Num",
|
||||
"Str",
|
||||
"Ellipsis",
|
||||
"NameConstant",
|
||||
"Bytes",
|
||||
):
|
||||
return name
|
||||
|
||||
node = getattr(ast, name)
|
||||
if issubclass(node, ast.AST):
|
||||
return name
|
||||
except AttributeError: # nosec(tkelsey): catching expected exception
|
||||
pass
|
||||
|
||||
raise TypeError(f"Error: {name} is not a valid node type in AST")
|
||||
|
||||
|
||||
def get_nosec(nosec_lines, context):
|
||||
for lineno in context["linerange"]:
|
||||
nosec = nosec_lines.get(lineno, None)
|
||||
if nosec is not None:
|
||||
return nosec
|
||||
return None
|
||||
@@ -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,155 @@
|
||||
#
|
||||
# Copyright (C) 2018 [Victor Torre](https://github.com/ehooo)
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import ast
|
||||
|
||||
import bandit
|
||||
from bandit.core import issue
|
||||
from bandit.core import test_properties as test
|
||||
|
||||
|
||||
def keywords2dict(keywords):
|
||||
kwargs = {}
|
||||
for node in keywords:
|
||||
if isinstance(node, ast.keyword):
|
||||
kwargs[node.arg] = node.value
|
||||
return kwargs
|
||||
|
||||
|
||||
@test.checks("Call")
|
||||
@test.test_id("B610")
|
||||
def django_extra_used(context):
|
||||
"""**B610: Potential SQL injection on extra function**
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
>> Issue: [B610:django_extra_used] Use of extra potential SQL attack vector.
|
||||
Severity: Medium Confidence: Medium
|
||||
CWE: CWE-89 (https://cwe.mitre.org/data/definitions/89.html)
|
||||
Location: examples/django_sql_injection_extra.py:29:0
|
||||
More Info: https://bandit.readthedocs.io/en/latest/plugins/b610_django_extra_used.html
|
||||
28 tables_str = 'django_content_type" WHERE "auth_user"."username"="admin'
|
||||
29 User.objects.all().extra(tables=[tables_str]).distinct()
|
||||
|
||||
.. seealso::
|
||||
|
||||
- https://docs.djangoproject.com/en/dev/topics/security/\
|
||||
#sql-injection-protection
|
||||
- https://cwe.mitre.org/data/definitions/89.html
|
||||
|
||||
.. versionadded:: 1.5.0
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
CWE information added
|
||||
|
||||
""" # noqa: E501
|
||||
description = "Use of extra potential SQL attack vector."
|
||||
if context.call_function_name == "extra":
|
||||
kwargs = keywords2dict(context.node.keywords)
|
||||
args = context.node.args
|
||||
if args:
|
||||
if len(args) >= 1:
|
||||
kwargs["select"] = args[0]
|
||||
if len(args) >= 2:
|
||||
kwargs["where"] = args[1]
|
||||
if len(args) >= 3:
|
||||
kwargs["params"] = args[2]
|
||||
if len(args) >= 4:
|
||||
kwargs["tables"] = args[3]
|
||||
if len(args) >= 5:
|
||||
kwargs["order_by"] = args[4]
|
||||
if len(args) >= 6:
|
||||
kwargs["select_params"] = args[5]
|
||||
insecure = False
|
||||
for key in ["where", "tables"]:
|
||||
if key in kwargs:
|
||||
if isinstance(kwargs[key], ast.List):
|
||||
for val in kwargs[key].elts:
|
||||
if not (
|
||||
isinstance(val, ast.Constant)
|
||||
and isinstance(val.value, str)
|
||||
):
|
||||
insecure = True
|
||||
break
|
||||
else:
|
||||
insecure = True
|
||||
break
|
||||
if not insecure and "select" in kwargs:
|
||||
if isinstance(kwargs["select"], ast.Dict):
|
||||
for k in kwargs["select"].keys:
|
||||
if not (
|
||||
isinstance(k, ast.Constant)
|
||||
and isinstance(k.value, str)
|
||||
):
|
||||
insecure = True
|
||||
break
|
||||
if not insecure:
|
||||
for v in kwargs["select"].values:
|
||||
if not (
|
||||
isinstance(v, ast.Constant)
|
||||
and isinstance(v.value, str)
|
||||
):
|
||||
insecure = True
|
||||
break
|
||||
else:
|
||||
insecure = True
|
||||
|
||||
if insecure:
|
||||
return bandit.Issue(
|
||||
severity=bandit.MEDIUM,
|
||||
confidence=bandit.MEDIUM,
|
||||
cwe=issue.Cwe.SQL_INJECTION,
|
||||
text=description,
|
||||
)
|
||||
|
||||
|
||||
@test.checks("Call")
|
||||
@test.test_id("B611")
|
||||
def django_rawsql_used(context):
|
||||
"""**B611: Potential SQL injection on RawSQL function**
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
>> Issue: [B611:django_rawsql_used] Use of RawSQL potential SQL attack vector.
|
||||
Severity: Medium Confidence: Medium
|
||||
CWE: CWE-89 (https://cwe.mitre.org/data/definitions/89.html)
|
||||
Location: examples/django_sql_injection_raw.py:11:26
|
||||
More Info: https://bandit.readthedocs.io/en/latest/plugins/b611_django_rawsql_used.html
|
||||
10 ' WHERE "username"="admin" OR 1=%s --'
|
||||
11 User.objects.annotate(val=RawSQL(raw, [0]))
|
||||
|
||||
.. seealso::
|
||||
|
||||
- https://docs.djangoproject.com/en/dev/topics/security/\
|
||||
#sql-injection-protection
|
||||
- https://cwe.mitre.org/data/definitions/89.html
|
||||
|
||||
.. versionadded:: 1.5.0
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
CWE information added
|
||||
|
||||
""" # noqa: E501
|
||||
description = "Use of RawSQL potential SQL attack vector."
|
||||
if context.is_module_imported_like("django.db.models"):
|
||||
if context.call_function_name == "RawSQL":
|
||||
if context.node.args:
|
||||
sql = context.node.args[0]
|
||||
else:
|
||||
kwargs = keywords2dict(context.node.keywords)
|
||||
sql = kwargs["sql"]
|
||||
|
||||
if not (
|
||||
isinstance(sql, ast.Constant) and isinstance(sql.value, str)
|
||||
):
|
||||
return bandit.Issue(
|
||||
severity=bandit.MEDIUM,
|
||||
confidence=bandit.MEDIUM,
|
||||
cwe=issue.Cwe.SQL_INJECTION,
|
||||
text=description,
|
||||
)
|
||||
@@ -0,0 +1,287 @@
|
||||
#
|
||||
# Copyright 2018 Victor Torre
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import ast
|
||||
|
||||
import bandit
|
||||
from bandit.core import issue
|
||||
from bandit.core import test_properties as test
|
||||
|
||||
|
||||
class DeepAssignation:
|
||||
def __init__(self, var_name, ignore_nodes=None):
|
||||
self.var_name = var_name
|
||||
self.ignore_nodes = ignore_nodes
|
||||
|
||||
def is_assigned_in(self, items):
|
||||
assigned = []
|
||||
for ast_inst in items:
|
||||
new_assigned = self.is_assigned(ast_inst)
|
||||
if new_assigned:
|
||||
if isinstance(new_assigned, (list, tuple)):
|
||||
assigned.extend(new_assigned)
|
||||
else:
|
||||
assigned.append(new_assigned)
|
||||
return assigned
|
||||
|
||||
def is_assigned(self, node):
|
||||
assigned = False
|
||||
if self.ignore_nodes:
|
||||
if isinstance(self.ignore_nodes, (list, tuple, object)):
|
||||
if isinstance(node, self.ignore_nodes):
|
||||
return assigned
|
||||
|
||||
if isinstance(node, ast.Expr):
|
||||
assigned = self.is_assigned(node.value)
|
||||
elif isinstance(node, ast.FunctionDef):
|
||||
for name in node.args.args:
|
||||
if isinstance(name, ast.Name):
|
||||
if name.id == self.var_name.id:
|
||||
# If is param the assignations are not affected
|
||||
return assigned
|
||||
assigned = self.is_assigned_in(node.body)
|
||||
elif isinstance(node, ast.With):
|
||||
for withitem in node.items:
|
||||
var_id = getattr(withitem.optional_vars, "id", None)
|
||||
if var_id == self.var_name.id:
|
||||
assigned = node
|
||||
else:
|
||||
assigned = self.is_assigned_in(node.body)
|
||||
elif isinstance(node, ast.Try):
|
||||
assigned = []
|
||||
assigned.extend(self.is_assigned_in(node.body))
|
||||
assigned.extend(self.is_assigned_in(node.handlers))
|
||||
assigned.extend(self.is_assigned_in(node.orelse))
|
||||
assigned.extend(self.is_assigned_in(node.finalbody))
|
||||
elif isinstance(node, ast.ExceptHandler):
|
||||
assigned = []
|
||||
assigned.extend(self.is_assigned_in(node.body))
|
||||
elif isinstance(node, (ast.If, ast.For, ast.While)):
|
||||
assigned = []
|
||||
assigned.extend(self.is_assigned_in(node.body))
|
||||
assigned.extend(self.is_assigned_in(node.orelse))
|
||||
elif isinstance(node, ast.AugAssign):
|
||||
if isinstance(node.target, ast.Name):
|
||||
if node.target.id == self.var_name.id:
|
||||
assigned = node.value
|
||||
elif isinstance(node, ast.Assign) and node.targets:
|
||||
target = node.targets[0]
|
||||
if isinstance(target, ast.Name):
|
||||
if target.id == self.var_name.id:
|
||||
assigned = node.value
|
||||
elif isinstance(target, ast.Tuple) and isinstance(
|
||||
node.value, ast.Tuple
|
||||
):
|
||||
pos = 0
|
||||
for name in target.elts:
|
||||
if name.id == self.var_name.id:
|
||||
assigned = node.value.elts[pos]
|
||||
break
|
||||
pos += 1
|
||||
return assigned
|
||||
|
||||
|
||||
def evaluate_var(xss_var, parent, until, ignore_nodes=None):
|
||||
secure = False
|
||||
if isinstance(xss_var, ast.Name):
|
||||
if isinstance(parent, ast.FunctionDef):
|
||||
for name in parent.args.args:
|
||||
if name.arg == xss_var.id:
|
||||
return False # Params are not secure
|
||||
|
||||
analyser = DeepAssignation(xss_var, ignore_nodes)
|
||||
for node in parent.body:
|
||||
if node.lineno >= until:
|
||||
break
|
||||
to = analyser.is_assigned(node)
|
||||
if to:
|
||||
if isinstance(to, ast.Constant) and isinstance(to.value, str):
|
||||
secure = True
|
||||
elif isinstance(to, ast.Name):
|
||||
secure = evaluate_var(to, parent, to.lineno, ignore_nodes)
|
||||
elif isinstance(to, ast.Call):
|
||||
secure = evaluate_call(to, parent, ignore_nodes)
|
||||
elif isinstance(to, (list, tuple)):
|
||||
num_secure = 0
|
||||
for some_to in to:
|
||||
if isinstance(some_to, ast.Constant) and isinstance(
|
||||
some_to.value, str
|
||||
):
|
||||
num_secure += 1
|
||||
elif isinstance(some_to, ast.Name):
|
||||
if evaluate_var(
|
||||
some_to, parent, node.lineno, ignore_nodes
|
||||
):
|
||||
num_secure += 1
|
||||
else:
|
||||
break
|
||||
else:
|
||||
break
|
||||
if num_secure == len(to):
|
||||
secure = True
|
||||
else:
|
||||
secure = False
|
||||
break
|
||||
else:
|
||||
secure = False
|
||||
break
|
||||
return secure
|
||||
|
||||
|
||||
def evaluate_call(call, parent, ignore_nodes=None):
|
||||
secure = False
|
||||
evaluate = False
|
||||
if isinstance(call, ast.Call) and isinstance(call.func, ast.Attribute):
|
||||
if (
|
||||
isinstance(call.func.value, ast.Constant)
|
||||
and call.func.attr == "format"
|
||||
):
|
||||
evaluate = True
|
||||
if call.keywords:
|
||||
evaluate = False # TODO(??) get support for this
|
||||
|
||||
if evaluate:
|
||||
args = list(call.args)
|
||||
num_secure = 0
|
||||
for arg in args:
|
||||
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
|
||||
num_secure += 1
|
||||
elif isinstance(arg, ast.Name):
|
||||
if evaluate_var(arg, parent, call.lineno, ignore_nodes):
|
||||
num_secure += 1
|
||||
else:
|
||||
break
|
||||
elif isinstance(arg, ast.Call):
|
||||
if evaluate_call(arg, parent, ignore_nodes):
|
||||
num_secure += 1
|
||||
else:
|
||||
break
|
||||
elif isinstance(arg, ast.Starred) and isinstance(
|
||||
arg.value, (ast.List, ast.Tuple)
|
||||
):
|
||||
args.extend(arg.value.elts)
|
||||
num_secure += 1
|
||||
else:
|
||||
break
|
||||
secure = num_secure == len(args)
|
||||
|
||||
return secure
|
||||
|
||||
|
||||
def transform2call(var):
|
||||
if isinstance(var, ast.BinOp):
|
||||
is_mod = isinstance(var.op, ast.Mod)
|
||||
is_left_str = isinstance(var.left, ast.Constant) and isinstance(
|
||||
var.left.value, str
|
||||
)
|
||||
if is_mod and is_left_str:
|
||||
new_call = ast.Call()
|
||||
new_call.args = []
|
||||
new_call.args = []
|
||||
new_call.keywords = None
|
||||
new_call.lineno = var.lineno
|
||||
new_call.func = ast.Attribute()
|
||||
new_call.func.value = var.left
|
||||
new_call.func.attr = "format"
|
||||
if isinstance(var.right, ast.Tuple):
|
||||
new_call.args = var.right.elts
|
||||
else:
|
||||
new_call.args = [var.right]
|
||||
return new_call
|
||||
|
||||
|
||||
def check_risk(node):
|
||||
description = "Potential XSS on mark_safe function."
|
||||
xss_var = node.args[0]
|
||||
|
||||
secure = False
|
||||
|
||||
if isinstance(xss_var, ast.Name):
|
||||
# Check if the var are secure
|
||||
parent = node._bandit_parent
|
||||
while not isinstance(parent, (ast.Module, ast.FunctionDef)):
|
||||
parent = parent._bandit_parent
|
||||
|
||||
is_param = False
|
||||
if isinstance(parent, ast.FunctionDef):
|
||||
for name in parent.args.args:
|
||||
if name.arg == xss_var.id:
|
||||
is_param = True
|
||||
break
|
||||
|
||||
if not is_param:
|
||||
secure = evaluate_var(xss_var, parent, node.lineno)
|
||||
elif isinstance(xss_var, ast.Call):
|
||||
parent = node._bandit_parent
|
||||
while not isinstance(parent, (ast.Module, ast.FunctionDef)):
|
||||
parent = parent._bandit_parent
|
||||
secure = evaluate_call(xss_var, parent)
|
||||
elif isinstance(xss_var, ast.BinOp):
|
||||
is_mod = isinstance(xss_var.op, ast.Mod)
|
||||
is_left_str = isinstance(xss_var.left, ast.Constant) and isinstance(
|
||||
xss_var.left.value, str
|
||||
)
|
||||
if is_mod and is_left_str:
|
||||
parent = node._bandit_parent
|
||||
while not isinstance(parent, (ast.Module, ast.FunctionDef)):
|
||||
parent = parent._bandit_parent
|
||||
new_call = transform2call(xss_var)
|
||||
secure = evaluate_call(new_call, parent)
|
||||
|
||||
if not secure:
|
||||
return bandit.Issue(
|
||||
severity=bandit.MEDIUM,
|
||||
confidence=bandit.HIGH,
|
||||
cwe=issue.Cwe.BASIC_XSS,
|
||||
text=description,
|
||||
)
|
||||
|
||||
|
||||
@test.checks("Call")
|
||||
@test.test_id("B703")
|
||||
def django_mark_safe(context):
|
||||
"""**B703: Potential XSS on mark_safe function**
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
>> Issue: [B703:django_mark_safe] Potential XSS on mark_safe function.
|
||||
Severity: Medium Confidence: High
|
||||
CWE: CWE-80 (https://cwe.mitre.org/data/definitions/80.html)
|
||||
Location: examples/mark_safe_insecure.py:159:4
|
||||
More Info: https://bandit.readthedocs.io/en/latest/plugins/b703_django_mark_safe.html
|
||||
158 str_arg = 'could be insecure'
|
||||
159 safestring.mark_safe(str_arg)
|
||||
|
||||
.. seealso::
|
||||
|
||||
- https://docs.djangoproject.com/en/dev/topics/security/\
|
||||
#cross-site-scripting-xss-protection
|
||||
- https://docs.djangoproject.com/en/dev/ref/utils/\
|
||||
#module-django.utils.safestring
|
||||
- https://docs.djangoproject.com/en/dev/ref/utils/\
|
||||
#django.utils.html.format_html
|
||||
- https://cwe.mitre.org/data/definitions/80.html
|
||||
|
||||
.. versionadded:: 1.5.0
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
CWE information added
|
||||
|
||||
""" # noqa: E501
|
||||
if context.is_module_imported_like("django.utils.safestring"):
|
||||
affected_functions = [
|
||||
"mark_safe",
|
||||
"SafeText",
|
||||
"SafeUnicode",
|
||||
"SafeString",
|
||||
"SafeBytes",
|
||||
]
|
||||
if context.call_function_name in affected_functions:
|
||||
xss = context.node.args[0]
|
||||
if not (
|
||||
isinstance(xss, ast.Constant) and isinstance(xss.value, str)
|
||||
):
|
||||
return check_risk(context.node)
|
||||
@@ -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,282 @@
|
||||
#
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import ast
|
||||
import re
|
||||
|
||||
import bandit
|
||||
from bandit.core import issue
|
||||
from bandit.core import test_properties as test
|
||||
|
||||
RE_WORDS = "(pas+wo?r?d|pass(phrase)?|pwd|token|secrete?)"
|
||||
RE_CANDIDATES = re.compile(
|
||||
"(^{0}$|_{0}_|^{0}_|_{0}$)".format(RE_WORDS), re.IGNORECASE
|
||||
)
|
||||
|
||||
|
||||
def _report(value, lineno=None):
|
||||
return bandit.Issue(
|
||||
severity=bandit.LOW,
|
||||
confidence=bandit.MEDIUM,
|
||||
cwe=issue.Cwe.HARD_CODED_PASSWORD,
|
||||
text=f"Possible hardcoded password: '{value}'",
|
||||
lineno=lineno,
|
||||
)
|
||||
|
||||
|
||||
@test.checks("Str")
|
||||
@test.test_id("B105")
|
||||
def hardcoded_password_string(context):
|
||||
"""**B105: Test for use of hard-coded password strings**
|
||||
|
||||
The use of hard-coded passwords increases the possibility of password
|
||||
guessing tremendously. This plugin test looks for all string literals and
|
||||
checks the following conditions:
|
||||
|
||||
- assigned to a variable that looks like a password
|
||||
- assigned to a dict key that looks like a password
|
||||
- assigned to a class attribute that looks like a password
|
||||
- used in a comparison with a variable that looks like a password
|
||||
|
||||
Variables are considered to look like a password if they have match any one
|
||||
of:
|
||||
|
||||
- "password"
|
||||
- "pass"
|
||||
- "passwd"
|
||||
- "pwd"
|
||||
- "secret"
|
||||
- "token"
|
||||
- "secrete"
|
||||
|
||||
Note: this can be noisy and may generate false positives.
|
||||
|
||||
**Config Options:**
|
||||
|
||||
None
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
>> Issue: Possible hardcoded password '(root)'
|
||||
Severity: Low Confidence: Low
|
||||
CWE: CWE-259 (https://cwe.mitre.org/data/definitions/259.html)
|
||||
Location: ./examples/hardcoded-passwords.py:5
|
||||
4 def someFunction2(password):
|
||||
5 if password == "root":
|
||||
6 print("OK, logged in")
|
||||
|
||||
.. seealso::
|
||||
|
||||
- https://www.owasp.org/index.php/Use_of_hard-coded_password
|
||||
- https://cwe.mitre.org/data/definitions/259.html
|
||||
|
||||
.. versionadded:: 0.9.0
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
CWE information added
|
||||
|
||||
"""
|
||||
node = context.node
|
||||
if isinstance(node._bandit_parent, ast.Assign):
|
||||
# looks for "candidate='some_string'"
|
||||
for targ in node._bandit_parent.targets:
|
||||
if isinstance(targ, ast.Name) and RE_CANDIDATES.search(targ.id):
|
||||
return _report(node.value)
|
||||
elif isinstance(targ, ast.Attribute) and RE_CANDIDATES.search(
|
||||
targ.attr
|
||||
):
|
||||
return _report(node.value)
|
||||
|
||||
elif (
|
||||
isinstance(node._bandit_parent, ast.Dict)
|
||||
and node in node._bandit_parent.keys
|
||||
and RE_CANDIDATES.search(node.value)
|
||||
):
|
||||
# looks for "{'candidate': 'some_string'}"
|
||||
dict_node = node._bandit_parent
|
||||
pos = dict_node.keys.index(node)
|
||||
value_node = dict_node.values[pos]
|
||||
if isinstance(value_node, ast.Constant):
|
||||
return _report(value_node.value)
|
||||
|
||||
elif isinstance(
|
||||
node._bandit_parent, ast.Subscript
|
||||
) and RE_CANDIDATES.search(node.value):
|
||||
# Py39+: looks for "dict[candidate]='some_string'"
|
||||
# subscript -> index -> string
|
||||
assign = node._bandit_parent._bandit_parent
|
||||
if (
|
||||
isinstance(assign, ast.Assign)
|
||||
and isinstance(assign.value, ast.Constant)
|
||||
and isinstance(assign.value.value, str)
|
||||
):
|
||||
return _report(assign.value.value)
|
||||
|
||||
elif isinstance(node._bandit_parent, ast.Index) and RE_CANDIDATES.search(
|
||||
node.value
|
||||
):
|
||||
# looks for "dict[candidate]='some_string'"
|
||||
# assign -> subscript -> index -> string
|
||||
assign = node._bandit_parent._bandit_parent._bandit_parent
|
||||
if (
|
||||
isinstance(assign, ast.Assign)
|
||||
and isinstance(assign.value, ast.Constant)
|
||||
and isinstance(assign.value.value, str)
|
||||
):
|
||||
return _report(assign.value.value)
|
||||
|
||||
elif isinstance(node._bandit_parent, ast.Compare):
|
||||
# looks for "candidate == 'some_string'"
|
||||
comp = node._bandit_parent
|
||||
if isinstance(comp.left, ast.Name):
|
||||
if RE_CANDIDATES.search(comp.left.id):
|
||||
if isinstance(
|
||||
comp.comparators[0], ast.Constant
|
||||
) and isinstance(comp.comparators[0].value, str):
|
||||
return _report(comp.comparators[0].value)
|
||||
elif isinstance(comp.left, ast.Attribute):
|
||||
if RE_CANDIDATES.search(comp.left.attr):
|
||||
if isinstance(
|
||||
comp.comparators[0], ast.Constant
|
||||
) and isinstance(comp.comparators[0].value, str):
|
||||
return _report(comp.comparators[0].value)
|
||||
|
||||
|
||||
@test.checks("Call")
|
||||
@test.test_id("B106")
|
||||
def hardcoded_password_funcarg(context):
|
||||
"""**B106: Test for use of hard-coded password function arguments**
|
||||
|
||||
The use of hard-coded passwords increases the possibility of password
|
||||
guessing tremendously. This plugin test looks for all function calls being
|
||||
passed a keyword argument that is a string literal. It checks that the
|
||||
assigned local variable does not look like a password.
|
||||
|
||||
Variables are considered to look like a password if they have match any one
|
||||
of:
|
||||
|
||||
- "password"
|
||||
- "pass"
|
||||
- "passwd"
|
||||
- "pwd"
|
||||
- "secret"
|
||||
- "token"
|
||||
- "secrete"
|
||||
|
||||
Note: this can be noisy and may generate false positives.
|
||||
|
||||
**Config Options:**
|
||||
|
||||
None
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
>> Issue: [B106:hardcoded_password_funcarg] Possible hardcoded
|
||||
password: 'blerg'
|
||||
Severity: Low Confidence: Medium
|
||||
CWE: CWE-259 (https://cwe.mitre.org/data/definitions/259.html)
|
||||
Location: ./examples/hardcoded-passwords.py:16
|
||||
15
|
||||
16 doLogin(password="blerg")
|
||||
|
||||
.. seealso::
|
||||
|
||||
- https://www.owasp.org/index.php/Use_of_hard-coded_password
|
||||
- https://cwe.mitre.org/data/definitions/259.html
|
||||
|
||||
.. versionadded:: 0.9.0
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
CWE information added
|
||||
|
||||
"""
|
||||
# looks for "function(candidate='some_string')"
|
||||
for kw in context.node.keywords:
|
||||
if (
|
||||
isinstance(kw.value, ast.Constant)
|
||||
and isinstance(kw.value.value, str)
|
||||
and RE_CANDIDATES.search(kw.arg)
|
||||
):
|
||||
return _report(kw.value.value, lineno=kw.value.lineno)
|
||||
|
||||
|
||||
@test.checks("FunctionDef")
|
||||
@test.test_id("B107")
|
||||
def hardcoded_password_default(context):
|
||||
"""**B107: Test for use of hard-coded password argument defaults**
|
||||
|
||||
The use of hard-coded passwords increases the possibility of password
|
||||
guessing tremendously. This plugin test looks for all function definitions
|
||||
that specify a default string literal for some argument. It checks that
|
||||
the argument does not look like a password.
|
||||
|
||||
Variables are considered to look like a password if they have match any one
|
||||
of:
|
||||
|
||||
- "password"
|
||||
- "pass"
|
||||
- "passwd"
|
||||
- "pwd"
|
||||
- "secret"
|
||||
- "token"
|
||||
- "secrete"
|
||||
|
||||
Note: this can be noisy and may generate false positives. We do not
|
||||
report on None values which can be legitimately used as a default value,
|
||||
when initializing a function or class.
|
||||
|
||||
**Config Options:**
|
||||
|
||||
None
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
>> Issue: [B107:hardcoded_password_default] Possible hardcoded
|
||||
password: 'Admin'
|
||||
Severity: Low Confidence: Medium
|
||||
CWE: CWE-259 (https://cwe.mitre.org/data/definitions/259.html)
|
||||
Location: ./examples/hardcoded-passwords.py:1
|
||||
|
||||
1 def someFunction(user, password="Admin"):
|
||||
2 print("Hi " + user)
|
||||
|
||||
.. seealso::
|
||||
|
||||
- https://www.owasp.org/index.php/Use_of_hard-coded_password
|
||||
- https://cwe.mitre.org/data/definitions/259.html
|
||||
|
||||
.. versionadded:: 0.9.0
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
CWE information added
|
||||
|
||||
"""
|
||||
# looks for "def function(candidate='some_string')"
|
||||
|
||||
# this pads the list of default values with "None" if nothing is given
|
||||
defs = [None] * (
|
||||
len(context.node.args.args) - len(context.node.args.defaults)
|
||||
)
|
||||
defs.extend(context.node.args.defaults)
|
||||
|
||||
# go through all (param, value)s and look for candidates
|
||||
for key, val in zip(context.node.args.args, defs):
|
||||
if isinstance(key, (ast.Name, ast.arg)):
|
||||
# Skip if the default value is None
|
||||
if val is None or (
|
||||
isinstance(val, ast.Constant) and val.value is None
|
||||
):
|
||||
continue
|
||||
if (
|
||||
isinstance(val, ast.Constant)
|
||||
and isinstance(val.value, str)
|
||||
and RE_CANDIDATES.search(key.arg)
|
||||
):
|
||||
return _report(val.value)
|
||||
@@ -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,121 @@
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
r"""
|
||||
======================================================================
|
||||
B324: Test use of insecure md4, md5, or sha1 hash functions in hashlib
|
||||
======================================================================
|
||||
|
||||
This plugin checks for the usage of the insecure MD4, MD5, or SHA1 hash
|
||||
functions in ``hashlib`` and ``crypt``. The ``hashlib.new`` function provides
|
||||
the ability to construct a new hashing object using the named algorithm. This
|
||||
can be used to create insecure hash functions like MD4 and MD5 if they are
|
||||
passed as algorithm names to this function.
|
||||
|
||||
This check does additional checking for usage of keyword usedforsecurity on all
|
||||
function variations of hashlib.
|
||||
|
||||
Similar to ``hashlib``, this plugin also checks for usage of one of the
|
||||
``crypt`` module's weak hashes. ``crypt`` also permits MD5 among other weak
|
||||
hash variants.
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
>> Issue: [B324:hashlib] Use of weak MD4, MD5, or SHA1 hash for
|
||||
security. Consider usedforsecurity=False
|
||||
Severity: High Confidence: High
|
||||
CWE: CWE-327 (https://cwe.mitre.org/data/definitions/327.html)
|
||||
Location: examples/hashlib_new_insecure_functions.py:3:0
|
||||
More Info: https://bandit.readthedocs.io/en/latest/plugins/b324_hashlib.html
|
||||
2
|
||||
3 hashlib.new('md5')
|
||||
4
|
||||
|
||||
.. seealso::
|
||||
|
||||
- https://cwe.mitre.org/data/definitions/327.html
|
||||
|
||||
.. versionadded:: 1.5.0
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
CWE information added
|
||||
|
||||
.. versionchanged:: 1.7.6
|
||||
Added check for the crypt module weak hashes
|
||||
|
||||
""" # noqa: E501
|
||||
import bandit
|
||||
from bandit.core import issue
|
||||
from bandit.core import test_properties as test
|
||||
|
||||
WEAK_HASHES = ("md4", "md5", "sha", "sha1")
|
||||
WEAK_CRYPT_HASHES = ("METHOD_CRYPT", "METHOD_MD5", "METHOD_BLOWFISH")
|
||||
|
||||
|
||||
def _hashlib_func(context, func):
|
||||
keywords = context.call_keywords
|
||||
|
||||
if func in WEAK_HASHES:
|
||||
if keywords.get("usedforsecurity", "True") == "True":
|
||||
return bandit.Issue(
|
||||
severity=bandit.HIGH,
|
||||
confidence=bandit.HIGH,
|
||||
cwe=issue.Cwe.BROKEN_CRYPTO,
|
||||
text=f"Use of weak {func.upper()} hash for security. "
|
||||
"Consider usedforsecurity=False",
|
||||
lineno=context.node.lineno,
|
||||
)
|
||||
elif func == "new":
|
||||
args = context.call_args
|
||||
name = args[0] if args else keywords.get("name", None)
|
||||
if isinstance(name, str) and name.lower() in WEAK_HASHES:
|
||||
if keywords.get("usedforsecurity", "True") == "True":
|
||||
return bandit.Issue(
|
||||
severity=bandit.HIGH,
|
||||
confidence=bandit.HIGH,
|
||||
cwe=issue.Cwe.BROKEN_CRYPTO,
|
||||
text=f"Use of weak {name.upper()} hash for "
|
||||
"security. Consider usedforsecurity=False",
|
||||
lineno=context.node.lineno,
|
||||
)
|
||||
|
||||
|
||||
def _crypt_crypt(context, func):
|
||||
args = context.call_args
|
||||
keywords = context.call_keywords
|
||||
|
||||
if func == "crypt":
|
||||
name = args[1] if len(args) > 1 else keywords.get("salt", None)
|
||||
if isinstance(name, str) and name in WEAK_CRYPT_HASHES:
|
||||
return bandit.Issue(
|
||||
severity=bandit.MEDIUM,
|
||||
confidence=bandit.HIGH,
|
||||
cwe=issue.Cwe.BROKEN_CRYPTO,
|
||||
text=f"Use of insecure crypt.{name.upper()} hash function.",
|
||||
lineno=context.node.lineno,
|
||||
)
|
||||
elif func == "mksalt":
|
||||
name = args[0] if args else keywords.get("method", None)
|
||||
if isinstance(name, str) and name in WEAK_CRYPT_HASHES:
|
||||
return bandit.Issue(
|
||||
severity=bandit.MEDIUM,
|
||||
confidence=bandit.HIGH,
|
||||
cwe=issue.Cwe.BROKEN_CRYPTO,
|
||||
text=f"Use of insecure crypt.{name.upper()} hash function.",
|
||||
lineno=context.node.lineno,
|
||||
)
|
||||
|
||||
|
||||
@test.test_id("B324")
|
||||
@test.checks("Call")
|
||||
def hashlib(context):
|
||||
if isinstance(context.call_function_name_qual, str):
|
||||
qualname_list = context.call_function_name_qual.split(".")
|
||||
func = qualname_list[-1]
|
||||
|
||||
if "hashlib" in qualname_list:
|
||||
return _hashlib_func(context, func)
|
||||
|
||||
elif "crypt" in qualname_list and func in ("crypt", "mksalt"):
|
||||
return _crypt_crypt(context, func)
|
||||
@@ -0,0 +1,166 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
r"""
|
||||
================================================
|
||||
B615: Test for unsafe Hugging Face Hub downloads
|
||||
================================================
|
||||
|
||||
This plugin checks for unsafe downloads from Hugging Face Hub without proper
|
||||
integrity verification. Downloading models, datasets, or files without
|
||||
specifying a revision based on an immmutable revision (commit) can
|
||||
lead to supply chain attacks where malicious actors could
|
||||
replace model files and use an existing tag or branch name
|
||||
to serve malicious content.
|
||||
|
||||
The secure approach is to:
|
||||
|
||||
1. Pin to specific revisions/commits when downloading models, files or datasets
|
||||
|
||||
Common unsafe patterns:
|
||||
- ``AutoModel.from_pretrained("org/model-name")``
|
||||
- ``AutoModel.from_pretrained("org/model-name", revision="main")``
|
||||
- ``AutoModel.from_pretrained("org/model-name", revision="v1.0.0")``
|
||||
- ``load_dataset("org/dataset-name")`` without revision
|
||||
- ``load_dataset("org/dataset-name", revision="main")``
|
||||
- ``load_dataset("org/dataset-name", revision="v1.0")``
|
||||
- ``AutoTokenizer.from_pretrained("org/model-name")``
|
||||
- ``AutoTokenizer.from_pretrained("org/model-name", revision="main")``
|
||||
- ``AutoTokenizer.from_pretrained("org/model-name", revision="v3.3.0")``
|
||||
- ``hf_hub_download(repo_id="org/model_name", filename="file_name")``
|
||||
- ``hf_hub_download(repo_id="org/model_name",
|
||||
filename="file_name",
|
||||
revision="main"
|
||||
)``
|
||||
- ``hf_hub_download(repo_id="org/model_name",
|
||||
filename="file_name",
|
||||
revision="v2.0.0"
|
||||
)``
|
||||
- ``snapshot_download(repo_id="org/model_name")``
|
||||
- ``snapshot_download(repo_id="org/model_name", revision="main")``
|
||||
- ``snapshot_download(repo_id="org/model_name", revision="refs/pr/1")``
|
||||
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
>> Issue: Unsafe Hugging Face Hub download without revision pinning
|
||||
Severity: Medium Confidence: High
|
||||
CWE: CWE-494 (https://cwe.mitre.org/data/definitions/494.html)
|
||||
Location: examples/huggingface_unsafe_download.py:8
|
||||
7 # Unsafe: no revision specified
|
||||
8 model = AutoModel.from_pretrained("org/model_name")
|
||||
9
|
||||
|
||||
.. seealso::
|
||||
|
||||
- https://cwe.mitre.org/data/definitions/494.html
|
||||
- https://huggingface.co/docs/huggingface_hub/en/guides/download
|
||||
|
||||
.. versionadded:: 1.8.6
|
||||
|
||||
"""
|
||||
import ast
|
||||
import string
|
||||
|
||||
import bandit
|
||||
from bandit.core import issue
|
||||
from bandit.core import test_properties as test
|
||||
|
||||
|
||||
@test.checks("Call")
|
||||
@test.test_id("B615")
|
||||
def huggingface_unsafe_download(context):
|
||||
"""
|
||||
This plugin checks for unsafe artifact download from Hugging Face Hub
|
||||
without immutable/reproducible revision pinning.
|
||||
"""
|
||||
# Check if any HuggingFace-related modules are imported
|
||||
hf_modules = [
|
||||
"transformers",
|
||||
"datasets",
|
||||
"huggingface_hub",
|
||||
]
|
||||
|
||||
# Check if any HF modules are imported
|
||||
hf_imported = any(
|
||||
context.is_module_imported_like(module) for module in hf_modules
|
||||
)
|
||||
|
||||
if not hf_imported:
|
||||
return
|
||||
|
||||
qualname = context.call_function_name_qual
|
||||
if not isinstance(qualname, str):
|
||||
return
|
||||
|
||||
unsafe_patterns = {
|
||||
# transformers library patterns
|
||||
"from_pretrained": ["transformers"],
|
||||
# datasets library patterns
|
||||
"load_dataset": ["datasets"],
|
||||
# huggingface_hub patterns
|
||||
"hf_hub_download": ["huggingface_hub"],
|
||||
"snapshot_download": ["huggingface_hub"],
|
||||
"repository_id": ["huggingface_hub"],
|
||||
}
|
||||
|
||||
qualname_parts = qualname.split(".")
|
||||
func_name = qualname_parts[-1]
|
||||
|
||||
if func_name not in unsafe_patterns:
|
||||
return
|
||||
|
||||
required_modules = unsafe_patterns[func_name]
|
||||
if not any(module in qualname_parts for module in required_modules):
|
||||
return
|
||||
|
||||
# Check for revision parameter (the key security control).
|
||||
# First, check the raw AST to see if a revision/commit_id keyword was
|
||||
# passed as a non-literal expression (variable, attribute, subscript,
|
||||
# function call, etc.). In those cases we cannot statically determine
|
||||
# the value, so we give the user the benefit of the doubt.
|
||||
call_node = context._context.get("call")
|
||||
if call_node is not None:
|
||||
for kw in getattr(call_node, "keywords", []):
|
||||
if kw.arg in ("revision", "commit_id") and not isinstance(
|
||||
kw.value, ast.Constant
|
||||
):
|
||||
return
|
||||
|
||||
revision_value = context.get_call_arg_value("revision")
|
||||
commit_id_value = context.get_call_arg_value("commit_id")
|
||||
|
||||
# Check if a revision or commit_id is specified
|
||||
revision_to_check = revision_value or commit_id_value
|
||||
|
||||
if revision_to_check is not None:
|
||||
# Check if it's a secure revision (looks like a commit hash)
|
||||
# Commit hashes: 40 chars (full SHA) or 7+ chars (short SHA)
|
||||
if isinstance(revision_to_check, str):
|
||||
# Remove quotes if present
|
||||
revision_str = str(revision_to_check).strip("\"'")
|
||||
|
||||
# Check if it looks like a commit hash (hexadecimal string)
|
||||
# Must be at least 7 characters and all hexadecimal
|
||||
is_hex = all(c in string.hexdigits for c in revision_str)
|
||||
if len(revision_str) >= 7 and is_hex:
|
||||
# This looks like a commit hash, which is secure
|
||||
return
|
||||
|
||||
# Edge case: check if this is a local path (starts with ./ or /)
|
||||
first_arg = context.get_call_arg_at_position(0)
|
||||
if first_arg and isinstance(first_arg, str):
|
||||
if first_arg.startswith(("./", "/", "../")):
|
||||
# Local paths are generally safer
|
||||
return
|
||||
|
||||
return bandit.Issue(
|
||||
severity=bandit.MEDIUM,
|
||||
confidence=bandit.HIGH,
|
||||
text=(
|
||||
f"Unsafe Hugging Face Hub download without revision pinning "
|
||||
f"in {func_name}()"
|
||||
),
|
||||
cwe=issue.Cwe.DOWNLOAD_OF_CODE_WITHOUT_INTEGRITY_CHECK,
|
||||
lineno=context.get_lineno_for_call_arg(func_name),
|
||||
)
|
||||
@@ -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,706 @@
|
||||
#
|
||||
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import ast
|
||||
import re
|
||||
|
||||
import bandit
|
||||
from bandit.core import issue
|
||||
from bandit.core import test_properties as test
|
||||
|
||||
# yuck, regex: starts with a windows drive letter (eg C:)
|
||||
# or one of our path delimiter characters (/, \, .)
|
||||
full_path_match = re.compile(r"^(?:[A-Za-z](?=\:)|[\\\/\.])")
|
||||
|
||||
|
||||
def _evaluate_shell_call(context):
|
||||
no_formatting = isinstance(
|
||||
context.node.args[0], ast.Constant
|
||||
) and isinstance(context.node.args[0].value, str)
|
||||
|
||||
if no_formatting:
|
||||
return bandit.LOW
|
||||
else:
|
||||
return bandit.HIGH
|
||||
|
||||
|
||||
def gen_config(name):
|
||||
if name == "shell_injection":
|
||||
return {
|
||||
# Start a process using the subprocess module, or one of its
|
||||
# wrappers.
|
||||
"subprocess": [
|
||||
"subprocess.Popen",
|
||||
"subprocess.call",
|
||||
"subprocess.check_call",
|
||||
"subprocess.check_output",
|
||||
"subprocess.run",
|
||||
],
|
||||
# Start a process with a function vulnerable to shell injection.
|
||||
"shell": [
|
||||
"os.system",
|
||||
"os.popen",
|
||||
"os.popen2",
|
||||
"os.popen3",
|
||||
"os.popen4",
|
||||
"popen2.popen2",
|
||||
"popen2.popen3",
|
||||
"popen2.popen4",
|
||||
"popen2.Popen3",
|
||||
"popen2.Popen4",
|
||||
"commands.getoutput",
|
||||
"commands.getstatusoutput",
|
||||
"subprocess.getoutput",
|
||||
"subprocess.getstatusoutput",
|
||||
],
|
||||
# Start a process with a function that is not vulnerable to shell
|
||||
# injection.
|
||||
"no_shell": [
|
||||
"os.execl",
|
||||
"os.execle",
|
||||
"os.execlp",
|
||||
"os.execlpe",
|
||||
"os.execv",
|
||||
"os.execve",
|
||||
"os.execvp",
|
||||
"os.execvpe",
|
||||
"os.spawnl",
|
||||
"os.spawnle",
|
||||
"os.spawnlp",
|
||||
"os.spawnlpe",
|
||||
"os.spawnv",
|
||||
"os.spawnve",
|
||||
"os.spawnvp",
|
||||
"os.spawnvpe",
|
||||
"os.startfile",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def has_shell(context):
|
||||
keywords = context.node.keywords
|
||||
result = False
|
||||
if "shell" in context.call_keywords:
|
||||
for key in keywords:
|
||||
if key.arg == "shell":
|
||||
val = key.value
|
||||
if isinstance(val, ast.Constant) and (
|
||||
isinstance(val.value, int)
|
||||
or isinstance(val.value, float)
|
||||
or isinstance(val.value, complex)
|
||||
):
|
||||
result = bool(val.value)
|
||||
elif isinstance(val, ast.List):
|
||||
result = bool(val.elts)
|
||||
elif isinstance(val, ast.Dict):
|
||||
result = bool(val.keys)
|
||||
elif isinstance(val, ast.Name) and val.id in ["False", "None"]:
|
||||
result = False
|
||||
elif isinstance(val, ast.Constant):
|
||||
result = val.value
|
||||
else:
|
||||
result = True
|
||||
return result
|
||||
|
||||
|
||||
@test.takes_config("shell_injection")
|
||||
@test.checks("Call")
|
||||
@test.test_id("B602")
|
||||
def subprocess_popen_with_shell_equals_true(context, config):
|
||||
"""**B602: Test for use of popen with shell equals true**
|
||||
|
||||
Python possesses many mechanisms to invoke an external executable. However,
|
||||
doing so may present a security issue if appropriate care is not taken to
|
||||
sanitize any user provided or variable input.
|
||||
|
||||
This plugin test is part of a family of tests built to check for process
|
||||
spawning and warn appropriately. Specifically, this test looks for the
|
||||
spawning of a subprocess using a command shell. This type of subprocess
|
||||
invocation is dangerous as it is vulnerable to various shell injection
|
||||
attacks. Great care should be taken to sanitize all input in order to
|
||||
mitigate this risk. Calls of this type are identified by a parameter of
|
||||
'shell=True' being given.
|
||||
|
||||
Additionally, this plugin scans the command string given and adjusts its
|
||||
reported severity based on how it is presented. If the command string is a
|
||||
simple static string containing no special shell characters, then the
|
||||
resulting issue has low severity. If the string is static, but contains
|
||||
shell formatting characters or wildcards, then the reported issue is
|
||||
medium. Finally, if the string is computed using Python's string
|
||||
manipulation or formatting operations, then the reported issue has high
|
||||
severity. These severity levels reflect the likelihood that the code is
|
||||
vulnerable to injection.
|
||||
|
||||
See also:
|
||||
|
||||
- :doc:`../plugins/linux_commands_wildcard_injection`
|
||||
- :doc:`../plugins/subprocess_without_shell_equals_true`
|
||||
- :doc:`../plugins/start_process_with_no_shell`
|
||||
- :doc:`../plugins/start_process_with_a_shell`
|
||||
- :doc:`../plugins/start_process_with_partial_path`
|
||||
|
||||
**Config Options:**
|
||||
|
||||
This plugin test shares a configuration with others in the same family,
|
||||
namely `shell_injection`. This configuration is divided up into three
|
||||
sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
|
||||
that spawn subprocesses, invoke commands within a shell, or invoke commands
|
||||
without a shell (by replacing the calling process) respectively.
|
||||
|
||||
This plugin specifically scans for methods listed in `subprocess` section
|
||||
that have shell=True specified.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
shell_injection:
|
||||
|
||||
# Start a process using the subprocess module, or one of its
|
||||
wrappers.
|
||||
subprocess:
|
||||
- subprocess.Popen
|
||||
- subprocess.call
|
||||
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
>> Issue: subprocess call with shell=True seems safe, but may be
|
||||
changed in the future, consider rewriting without shell
|
||||
Severity: Low Confidence: High
|
||||
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
|
||||
Location: ./examples/subprocess_shell.py:21
|
||||
20 subprocess.check_call(['/bin/ls', '-l'], shell=False)
|
||||
21 subprocess.check_call('/bin/ls -l', shell=True)
|
||||
22
|
||||
|
||||
>> Issue: call with shell=True contains special shell characters,
|
||||
consider moving extra logic into Python code
|
||||
Severity: Medium Confidence: High
|
||||
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
|
||||
Location: ./examples/subprocess_shell.py:26
|
||||
25
|
||||
26 subprocess.Popen('/bin/ls *', shell=True)
|
||||
27 subprocess.Popen('/bin/ls %s' % ('something',), shell=True)
|
||||
|
||||
>> Issue: subprocess call with shell=True identified, security issue.
|
||||
Severity: High Confidence: High
|
||||
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
|
||||
Location: ./examples/subprocess_shell.py:27
|
||||
26 subprocess.Popen('/bin/ls *', shell=True)
|
||||
27 subprocess.Popen('/bin/ls %s' % ('something',), shell=True)
|
||||
28 subprocess.Popen('/bin/ls {}'.format('something'), shell=True)
|
||||
|
||||
.. seealso::
|
||||
|
||||
- https://security.openstack.org
|
||||
- https://docs.python.org/3/library/subprocess.html#frequently-used-arguments
|
||||
- https://security.openstack.org/guidelines/dg_use-subprocess-securely.html
|
||||
- https://security.openstack.org/guidelines/dg_avoid-shell-true.html
|
||||
- https://cwe.mitre.org/data/definitions/78.html
|
||||
|
||||
.. versionadded:: 0.9.0
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
CWE information added
|
||||
|
||||
""" # noqa: E501
|
||||
if config and context.call_function_name_qual in config["subprocess"]:
|
||||
if has_shell(context):
|
||||
if len(context.call_args) > 0:
|
||||
sev = _evaluate_shell_call(context)
|
||||
if sev == bandit.LOW:
|
||||
return bandit.Issue(
|
||||
severity=bandit.LOW,
|
||||
confidence=bandit.HIGH,
|
||||
cwe=issue.Cwe.OS_COMMAND_INJECTION,
|
||||
text="subprocess call with shell=True seems safe, but "
|
||||
"may be changed in the future, consider "
|
||||
"rewriting without shell",
|
||||
lineno=context.get_lineno_for_call_arg("shell"),
|
||||
)
|
||||
else:
|
||||
return bandit.Issue(
|
||||
severity=bandit.HIGH,
|
||||
confidence=bandit.HIGH,
|
||||
cwe=issue.Cwe.OS_COMMAND_INJECTION,
|
||||
text="subprocess call with shell=True identified, "
|
||||
"security issue.",
|
||||
lineno=context.get_lineno_for_call_arg("shell"),
|
||||
)
|
||||
|
||||
|
||||
@test.takes_config("shell_injection")
|
||||
@test.checks("Call")
|
||||
@test.test_id("B603")
|
||||
def subprocess_without_shell_equals_true(context, config):
|
||||
"""**B603: Test for use of subprocess without shell equals true**
|
||||
|
||||
Python possesses many mechanisms to invoke an external executable. However,
|
||||
doing so may present a security issue if appropriate care is not taken to
|
||||
sanitize any user provided or variable input.
|
||||
|
||||
This plugin test is part of a family of tests built to check for process
|
||||
spawning and warn appropriately. Specifically, this test looks for the
|
||||
spawning of a subprocess without the use of a command shell. This type of
|
||||
subprocess invocation is not vulnerable to shell injection attacks, but
|
||||
care should still be taken to ensure validity of input.
|
||||
|
||||
Because this is a lesser issue than that described in
|
||||
`subprocess_popen_with_shell_equals_true` a LOW severity warning is
|
||||
reported.
|
||||
|
||||
See also:
|
||||
|
||||
- :doc:`../plugins/linux_commands_wildcard_injection`
|
||||
- :doc:`../plugins/subprocess_popen_with_shell_equals_true`
|
||||
- :doc:`../plugins/start_process_with_no_shell`
|
||||
- :doc:`../plugins/start_process_with_a_shell`
|
||||
- :doc:`../plugins/start_process_with_partial_path`
|
||||
|
||||
**Config Options:**
|
||||
|
||||
This plugin test shares a configuration with others in the same family,
|
||||
namely `shell_injection`. This configuration is divided up into three
|
||||
sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
|
||||
that spawn subprocesses, invoke commands within a shell, or invoke commands
|
||||
without a shell (by replacing the calling process) respectively.
|
||||
|
||||
This plugin specifically scans for methods listed in `subprocess` section
|
||||
that have shell=False specified.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
shell_injection:
|
||||
# Start a process using the subprocess module, or one of its
|
||||
wrappers.
|
||||
subprocess:
|
||||
- subprocess.Popen
|
||||
- subprocess.call
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
>> Issue: subprocess call - check for execution of untrusted input.
|
||||
Severity: Low Confidence: High
|
||||
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
|
||||
Location: ./examples/subprocess_shell.py:23
|
||||
22
|
||||
23 subprocess.check_output(['/bin/ls', '-l'])
|
||||
24
|
||||
|
||||
.. seealso::
|
||||
|
||||
- https://security.openstack.org
|
||||
- https://docs.python.org/3/library/subprocess.html#frequently-used-arguments
|
||||
- https://security.openstack.org/guidelines/dg_avoid-shell-true.html
|
||||
- https://security.openstack.org/guidelines/dg_use-subprocess-securely.html
|
||||
- https://cwe.mitre.org/data/definitions/78.html
|
||||
|
||||
.. versionadded:: 0.9.0
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
CWE information added
|
||||
|
||||
""" # noqa: E501
|
||||
if config and context.call_function_name_qual in config["subprocess"]:
|
||||
if not has_shell(context):
|
||||
return bandit.Issue(
|
||||
severity=bandit.LOW,
|
||||
confidence=bandit.HIGH,
|
||||
cwe=issue.Cwe.OS_COMMAND_INJECTION,
|
||||
text="subprocess call - check for execution of untrusted "
|
||||
"input.",
|
||||
lineno=context.get_lineno_for_call_arg("shell"),
|
||||
)
|
||||
|
||||
|
||||
@test.takes_config("shell_injection")
|
||||
@test.checks("Call")
|
||||
@test.test_id("B604")
|
||||
def any_other_function_with_shell_equals_true(context, config):
|
||||
"""**B604: Test for any function with shell equals true**
|
||||
|
||||
Python possesses many mechanisms to invoke an external executable. However,
|
||||
doing so may present a security issue if appropriate care is not taken to
|
||||
sanitize any user provided or variable input.
|
||||
|
||||
This plugin test is part of a family of tests built to check for process
|
||||
spawning and warn appropriately. Specifically, this plugin test
|
||||
interrogates method calls for the presence of a keyword parameter `shell`
|
||||
equalling true. It is related to detection of shell injection issues and is
|
||||
intended to catch custom wrappers to vulnerable methods that may have been
|
||||
created.
|
||||
|
||||
See also:
|
||||
|
||||
- :doc:`../plugins/linux_commands_wildcard_injection`
|
||||
- :doc:`../plugins/subprocess_popen_with_shell_equals_true`
|
||||
- :doc:`../plugins/subprocess_without_shell_equals_true`
|
||||
- :doc:`../plugins/start_process_with_no_shell`
|
||||
- :doc:`../plugins/start_process_with_a_shell`
|
||||
- :doc:`../plugins/start_process_with_partial_path`
|
||||
|
||||
**Config Options:**
|
||||
|
||||
This plugin test shares a configuration with others in the same family,
|
||||
namely `shell_injection`. This configuration is divided up into three
|
||||
sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
|
||||
that spawn subprocesses, invoke commands within a shell, or invoke commands
|
||||
without a shell (by replacing the calling process) respectively.
|
||||
|
||||
Specifically, this plugin excludes those functions listed under the
|
||||
subprocess section, these methods are tested in a separate specific test
|
||||
plugin and this exclusion prevents duplicate issue reporting.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
shell_injection:
|
||||
# Start a process using the subprocess module, or one of its
|
||||
wrappers.
|
||||
subprocess: [subprocess.Popen, subprocess.call,
|
||||
subprocess.check_call, subprocess.check_output
|
||||
execute_with_timeout]
|
||||
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
>> Issue: Function call with shell=True parameter identified, possible
|
||||
security issue.
|
||||
Severity: Medium Confidence: High
|
||||
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
|
||||
Location: ./examples/subprocess_shell.py:9
|
||||
8 pop('/bin/gcc --version', shell=True)
|
||||
9 Popen('/bin/gcc --version', shell=True)
|
||||
10
|
||||
|
||||
.. seealso::
|
||||
|
||||
- https://security.openstack.org/guidelines/dg_avoid-shell-true.html
|
||||
- https://security.openstack.org/guidelines/dg_use-subprocess-securely.html
|
||||
- https://cwe.mitre.org/data/definitions/78.html
|
||||
|
||||
.. versionadded:: 0.9.0
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
CWE information added
|
||||
|
||||
""" # noqa: E501
|
||||
if config and context.call_function_name_qual not in config["subprocess"]:
|
||||
if has_shell(context):
|
||||
return bandit.Issue(
|
||||
severity=bandit.MEDIUM,
|
||||
confidence=bandit.LOW,
|
||||
cwe=issue.Cwe.OS_COMMAND_INJECTION,
|
||||
text="Function call with shell=True parameter identified, "
|
||||
"possible security issue.",
|
||||
lineno=context.get_lineno_for_call_arg("shell"),
|
||||
)
|
||||
|
||||
|
||||
@test.takes_config("shell_injection")
|
||||
@test.checks("Call")
|
||||
@test.test_id("B605")
|
||||
def start_process_with_a_shell(context, config):
|
||||
"""**B605: Test for starting a process with a shell**
|
||||
|
||||
Python possesses many mechanisms to invoke an external executable. However,
|
||||
doing so may present a security issue if appropriate care is not taken to
|
||||
sanitize any user provided or variable input.
|
||||
|
||||
This plugin test is part of a family of tests built to check for process
|
||||
spawning and warn appropriately. Specifically, this test looks for the
|
||||
spawning of a subprocess using a command shell. This type of subprocess
|
||||
invocation is dangerous as it is vulnerable to various shell injection
|
||||
attacks. Great care should be taken to sanitize all input in order to
|
||||
mitigate this risk. Calls of this type are identified by the use of certain
|
||||
commands which are known to use shells. Bandit will report a LOW
|
||||
severity warning.
|
||||
|
||||
See also:
|
||||
|
||||
- :doc:`../plugins/linux_commands_wildcard_injection`
|
||||
- :doc:`../plugins/subprocess_without_shell_equals_true`
|
||||
- :doc:`../plugins/start_process_with_no_shell`
|
||||
- :doc:`../plugins/start_process_with_partial_path`
|
||||
- :doc:`../plugins/subprocess_popen_with_shell_equals_true`
|
||||
|
||||
**Config Options:**
|
||||
|
||||
This plugin test shares a configuration with others in the same family,
|
||||
namely `shell_injection`. This configuration is divided up into three
|
||||
sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
|
||||
that spawn subprocesses, invoke commands within a shell, or invoke commands
|
||||
without a shell (by replacing the calling process) respectively.
|
||||
|
||||
This plugin specifically scans for methods listed in `shell` section.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
shell_injection:
|
||||
shell:
|
||||
- os.system
|
||||
- os.popen
|
||||
- os.popen2
|
||||
- os.popen3
|
||||
- os.popen4
|
||||
- popen2.popen2
|
||||
- popen2.popen3
|
||||
- popen2.popen4
|
||||
- popen2.Popen3
|
||||
- popen2.Popen4
|
||||
- commands.getoutput
|
||||
- commands.getstatusoutput
|
||||
- subprocess.getoutput
|
||||
- subprocess.getstatusoutput
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
>> Issue: Starting a process with a shell: check for injection.
|
||||
Severity: Low Confidence: Medium
|
||||
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
|
||||
Location: examples/os_system.py:3
|
||||
2
|
||||
3 os.system('/bin/echo hi')
|
||||
|
||||
.. seealso::
|
||||
|
||||
- https://security.openstack.org
|
||||
- https://docs.python.org/3/library/os.html#os.system
|
||||
- https://docs.python.org/3/library/subprocess.html#frequently-used-arguments
|
||||
- https://security.openstack.org/guidelines/dg_use-subprocess-securely.html
|
||||
- https://cwe.mitre.org/data/definitions/78.html
|
||||
|
||||
.. versionadded:: 0.10.0
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
CWE information added
|
||||
|
||||
""" # noqa: E501
|
||||
if config and context.call_function_name_qual in config["shell"]:
|
||||
if len(context.call_args) > 0:
|
||||
sev = _evaluate_shell_call(context)
|
||||
if sev == bandit.LOW:
|
||||
return bandit.Issue(
|
||||
severity=bandit.LOW,
|
||||
confidence=bandit.HIGH,
|
||||
cwe=issue.Cwe.OS_COMMAND_INJECTION,
|
||||
text="Starting a process with a shell: "
|
||||
"Seems safe, but may be changed in the future, "
|
||||
"consider rewriting without shell",
|
||||
)
|
||||
else:
|
||||
return bandit.Issue(
|
||||
severity=bandit.HIGH,
|
||||
confidence=bandit.HIGH,
|
||||
cwe=issue.Cwe.OS_COMMAND_INJECTION,
|
||||
text="Starting a process with a shell, possible injection"
|
||||
" detected, security issue.",
|
||||
)
|
||||
|
||||
|
||||
@test.takes_config("shell_injection")
|
||||
@test.checks("Call")
|
||||
@test.test_id("B606")
|
||||
def start_process_with_no_shell(context, config):
|
||||
"""**B606: Test for starting a process with no shell**
|
||||
|
||||
Python possesses many mechanisms to invoke an external executable. However,
|
||||
doing so may present a security issue if appropriate care is not taken to
|
||||
sanitize any user provided or variable input.
|
||||
|
||||
This plugin test is part of a family of tests built to check for process
|
||||
spawning and warn appropriately. Specifically, this test looks for the
|
||||
spawning of a subprocess in a way that doesn't use a shell. Although this
|
||||
is generally safe, it maybe useful for penetration testing workflows to
|
||||
track where external system calls are used. As such a LOW severity message
|
||||
is generated.
|
||||
|
||||
See also:
|
||||
|
||||
- :doc:`../plugins/linux_commands_wildcard_injection`
|
||||
- :doc:`../plugins/subprocess_without_shell_equals_true`
|
||||
- :doc:`../plugins/start_process_with_a_shell`
|
||||
- :doc:`../plugins/start_process_with_partial_path`
|
||||
- :doc:`../plugins/subprocess_popen_with_shell_equals_true`
|
||||
|
||||
**Config Options:**
|
||||
|
||||
This plugin test shares a configuration with others in the same family,
|
||||
namely `shell_injection`. This configuration is divided up into three
|
||||
sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
|
||||
that spawn subprocesses, invoke commands within a shell, or invoke commands
|
||||
without a shell (by replacing the calling process) respectively.
|
||||
|
||||
This plugin specifically scans for methods listed in `no_shell` section.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
shell_injection:
|
||||
no_shell:
|
||||
- os.execl
|
||||
- os.execle
|
||||
- os.execlp
|
||||
- os.execlpe
|
||||
- os.execv
|
||||
- os.execve
|
||||
- os.execvp
|
||||
- os.execvpe
|
||||
- os.spawnl
|
||||
- os.spawnle
|
||||
- os.spawnlp
|
||||
- os.spawnlpe
|
||||
- os.spawnv
|
||||
- os.spawnve
|
||||
- os.spawnvp
|
||||
- os.spawnvpe
|
||||
- os.startfile
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
>> Issue: [start_process_with_no_shell] Starting a process without a
|
||||
shell.
|
||||
Severity: Low Confidence: Medium
|
||||
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
|
||||
Location: examples/os-spawn.py:8
|
||||
7 os.spawnv(mode, path, args)
|
||||
8 os.spawnve(mode, path, args, env)
|
||||
9 os.spawnvp(mode, file, args)
|
||||
|
||||
.. seealso::
|
||||
|
||||
- https://security.openstack.org
|
||||
- https://docs.python.org/3/library/os.html#os.system
|
||||
- https://docs.python.org/3/library/subprocess.html#frequently-used-arguments
|
||||
- https://security.openstack.org/guidelines/dg_use-subprocess-securely.html
|
||||
- https://cwe.mitre.org/data/definitions/78.html
|
||||
|
||||
.. versionadded:: 0.10.0
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
CWE information added
|
||||
|
||||
""" # noqa: E501
|
||||
|
||||
if config and context.call_function_name_qual in config["no_shell"]:
|
||||
return bandit.Issue(
|
||||
severity=bandit.LOW,
|
||||
confidence=bandit.MEDIUM,
|
||||
cwe=issue.Cwe.OS_COMMAND_INJECTION,
|
||||
text="Starting a process without a shell.",
|
||||
)
|
||||
|
||||
|
||||
@test.takes_config("shell_injection")
|
||||
@test.checks("Call")
|
||||
@test.test_id("B607")
|
||||
def start_process_with_partial_path(context, config):
|
||||
"""**B607: Test for starting a process with a partial path**
|
||||
|
||||
Python possesses many mechanisms to invoke an external executable. If the
|
||||
desired executable path is not fully qualified relative to the filesystem
|
||||
root then this may present a potential security risk.
|
||||
|
||||
In POSIX environments, the `PATH` environment variable is used to specify a
|
||||
set of standard locations that will be searched for the first matching
|
||||
named executable. While convenient, this behavior may allow a malicious
|
||||
actor to exert control over a system. If they are able to adjust the
|
||||
contents of the `PATH` variable, or manipulate the file system, then a
|
||||
bogus executable may be discovered in place of the desired one. This
|
||||
executable will be invoked with the user privileges of the Python process
|
||||
that spawned it, potentially a highly privileged user.
|
||||
|
||||
This test will scan the parameters of all configured Python methods,
|
||||
looking for paths that do not start at the filesystem root, that is, do not
|
||||
have a leading '/' character.
|
||||
|
||||
**Config Options:**
|
||||
|
||||
This plugin test shares a configuration with others in the same family,
|
||||
namely `shell_injection`. This configuration is divided up into three
|
||||
sections, `subprocess`, `shell` and `no_shell`. They each list Python calls
|
||||
that spawn subprocesses, invoke commands within a shell, or invoke commands
|
||||
without a shell (by replacing the calling process) respectively.
|
||||
|
||||
This test will scan parameters of all methods in all sections. Note that
|
||||
methods are fully qualified and de-aliased prior to checking.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
shell_injection:
|
||||
# Start a process using the subprocess module, or one of its
|
||||
wrappers.
|
||||
subprocess:
|
||||
- subprocess.Popen
|
||||
- subprocess.call
|
||||
|
||||
# Start a process with a function vulnerable to shell injection.
|
||||
shell:
|
||||
- os.system
|
||||
- os.popen
|
||||
- popen2.Popen3
|
||||
- popen2.Popen4
|
||||
- commands.getoutput
|
||||
- commands.getstatusoutput
|
||||
# Start a process with a function that is not vulnerable to shell
|
||||
injection.
|
||||
no_shell:
|
||||
- os.execl
|
||||
- os.execle
|
||||
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
>> Issue: Starting a process with a partial executable path
|
||||
Severity: Low Confidence: High
|
||||
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
|
||||
Location: ./examples/partial_path_process.py:3
|
||||
2 from subprocess import Popen as pop
|
||||
3 pop('gcc --version', shell=False)
|
||||
|
||||
.. seealso::
|
||||
|
||||
- https://security.openstack.org
|
||||
- https://docs.python.org/3/library/os.html#process-management
|
||||
- https://cwe.mitre.org/data/definitions/78.html
|
||||
|
||||
.. versionadded:: 0.13.0
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
CWE information added
|
||||
|
||||
"""
|
||||
|
||||
if config and len(context.call_args):
|
||||
if (
|
||||
context.call_function_name_qual in config["subprocess"]
|
||||
or context.call_function_name_qual in config["shell"]
|
||||
or context.call_function_name_qual in config["no_shell"]
|
||||
):
|
||||
node = context.node.args[0]
|
||||
# some calls take an arg list, check the first part
|
||||
if isinstance(node, ast.List) and node.elts:
|
||||
node = node.elts[0]
|
||||
|
||||
# make sure the param is a string literal and not a var name
|
||||
if (
|
||||
isinstance(node, ast.Constant)
|
||||
and isinstance(node.value, str)
|
||||
and not full_path_match.match(node.value)
|
||||
):
|
||||
return bandit.Issue(
|
||||
severity=bandit.LOW,
|
||||
confidence=bandit.HIGH,
|
||||
cwe=issue.Cwe.OS_COMMAND_INJECTION,
|
||||
text="Starting a process with a partial executable path",
|
||||
)
|
||||
@@ -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.value
|
||||
# Hierarchy for "".format() is Wrapper -> Call -> Attribute -> Str
|
||||
wrapper = node._bandit_parent._bandit_parent._bandit_parent
|
||||
if node._bandit_parent.attr == "replace":
|
||||
str_replace = True
|
||||
elif hasattr(ast, "JoinedStr") and isinstance(
|
||||
node._bandit_parent, ast.JoinedStr
|
||||
):
|
||||
substrings = [
|
||||
child
|
||||
for child in node._bandit_parent.values
|
||||
if isinstance(child, ast.Constant) and isinstance(child.value, str)
|
||||
]
|
||||
# JoinedStr consists of list of Constant and FormattedValue
|
||||
# instances. Let's perform one test for the whole string
|
||||
# and abandon all parts except the first one to raise one
|
||||
# failed test instead of many for the same SQL statement.
|
||||
if substrings and node == substrings[0]:
|
||||
statement = "".join([str(child.value) for child in substrings])
|
||||
wrapper = node._bandit_parent._bandit_parent
|
||||
|
||||
if isinstance(wrapper, ast.Call): # wrapped in "execute" call?
|
||||
names = ["execute", "executemany"]
|
||||
name = utils.get_called_name(wrapper)
|
||||
return (name in names, statement, str_replace)
|
||||
else:
|
||||
return (False, statement, str_replace)
|
||||
|
||||
|
||||
@test.checks("Str")
|
||||
@test.test_id("B608")
|
||||
def hardcoded_sql_expressions(context):
|
||||
execute_call, statement, str_replace = _evaluate_ast(context.node)
|
||||
if _check_string(statement):
|
||||
return bandit.Issue(
|
||||
severity=bandit.MEDIUM,
|
||||
confidence=(
|
||||
bandit.MEDIUM
|
||||
if execute_call and not str_replace
|
||||
else bandit.LOW
|
||||
),
|
||||
cwe=issue.Cwe.SQL_INJECTION,
|
||||
text="Possible SQL injection vector through string-based "
|
||||
"query construction.",
|
||||
)
|
||||
@@ -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,77 @@
|
||||
# Copyright (c) 2024 Stacklok, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
r"""
|
||||
==================================
|
||||
B614: Test for unsafe PyTorch load
|
||||
==================================
|
||||
|
||||
This plugin checks for unsafe use of `torch.load` and
|
||||
`torch.serialization.load`. Using `torch.load` or
|
||||
`torch.serialization.load` with untrusted data can lead to arbitrary
|
||||
code execution. There are two safe alternatives:
|
||||
|
||||
1. Use `torch.load` with `weights_only=True` where only tensor data is
|
||||
extracted, and no arbitrary Python objects are deserialized
|
||||
2. Use the `safetensors` library from huggingface, which provides a safe
|
||||
deserialization mechanism
|
||||
|
||||
With `weights_only=True`, PyTorch enforces a strict type check, ensuring
|
||||
that only torch.Tensor objects are loaded.
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
>> Issue: Use of unsafe PyTorch load
|
||||
Severity: Medium Confidence: High
|
||||
CWE: CWE-502 (https://cwe.mitre.org/data/definitions/502.html)
|
||||
Location: examples/pytorch_load_save.py:8
|
||||
7 loaded_model.load_state_dict(torch.load('model_weights.pth'))
|
||||
8 another_model.load_state_dict(torch.load('model_weights.pth',
|
||||
map_location='cpu'))
|
||||
9
|
||||
10 print("Model loaded successfully!")
|
||||
|
||||
.. seealso::
|
||||
|
||||
- https://cwe.mitre.org/data/definitions/502.html
|
||||
- https://pytorch.org/docs/stable/generated/torch.load.html#torch.load
|
||||
- https://github.com/huggingface/safetensors
|
||||
|
||||
.. versionadded:: 1.7.10
|
||||
|
||||
"""
|
||||
import bandit
|
||||
from bandit.core import issue
|
||||
from bandit.core import test_properties as test
|
||||
|
||||
|
||||
@test.checks("Call")
|
||||
@test.test_id("B614")
|
||||
def pytorch_load(context):
|
||||
"""
|
||||
This plugin checks for unsafe use of `torch.load` and
|
||||
`torch.serialization.load`. Using `torch.load` or
|
||||
`torch.serialization.load` with untrusted data can lead to
|
||||
arbitrary code execution. The safe alternative is to use
|
||||
`weights_only=True` or the safetensors library.
|
||||
"""
|
||||
imported = context.is_module_imported_exact("torch")
|
||||
qualname = context.call_function_name_qual
|
||||
if not imported and isinstance(qualname, str):
|
||||
return
|
||||
|
||||
if qualname in {"torch.load", "torch.serialization.load"}:
|
||||
# For torch.load, check if weights_only=True is specified
|
||||
weights_only = context.get_call_arg_value("weights_only")
|
||||
if weights_only == "True" or weights_only is True:
|
||||
return
|
||||
|
||||
return bandit.Issue(
|
||||
severity=bandit.MEDIUM,
|
||||
confidence=bandit.HIGH,
|
||||
text="Use of unsafe PyTorch load",
|
||||
cwe=issue.Cwe.DESERIALIZATION_OF_UNTRUSTED_DATA,
|
||||
lineno=context.get_lineno_for_call_arg("load"),
|
||||
)
|
||||
@@ -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.extractall(members=function(tarfile))`` - LOW
|
||||
* ``tarfile.extractall(members=?)`` - member is not a function - MEDIUM
|
||||
* ``tarfile.extractall()`` - members from the archive is trusted - HIGH
|
||||
|
||||
Use ``tarfile.extractall(members=function_name)`` and define a function
|
||||
that will inspect each member. Discard files that contain a directory
|
||||
traversal sequences such as ``../`` or ``\..`` along with all special filetypes
|
||||
unless you explicitly need them.
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
>> Issue: [B202:tarfile_unsafe_members] tarfile.extractall used without
|
||||
any validation. You should check members and discard dangerous ones
|
||||
Severity: High Confidence: High
|
||||
CWE: CWE-22 (https://cwe.mitre.org/data/definitions/22.html)
|
||||
Location: examples/tarfile_extractall.py:8
|
||||
More Info:
|
||||
https://bandit.readthedocs.io/en/latest/plugins/b202_tarfile_unsafe_members.html
|
||||
7 tar = tarfile.open(filename)
|
||||
8 tar.extractall(path=tempfile.mkdtemp())
|
||||
9 tar.close()
|
||||
|
||||
|
||||
.. seealso::
|
||||
|
||||
- https://docs.python.org/3/library/tarfile.html#tarfile.TarFile.extractall
|
||||
- https://docs.python.org/3/library/tarfile.html#tarfile.TarInfo
|
||||
|
||||
.. versionadded:: 1.7.5
|
||||
|
||||
.. versionchanged:: 1.7.8
|
||||
Added check for filter parameter
|
||||
|
||||
"""
|
||||
import ast
|
||||
|
||||
import bandit
|
||||
from bandit.core import issue
|
||||
from bandit.core import test_properties as test
|
||||
|
||||
|
||||
def exec_issue(level, members=""):
|
||||
if level == bandit.LOW:
|
||||
return bandit.Issue(
|
||||
severity=bandit.LOW,
|
||||
confidence=bandit.LOW,
|
||||
cwe=issue.Cwe.PATH_TRAVERSAL,
|
||||
text="Usage of tarfile.extractall(members=function(tarfile)). "
|
||||
"Make sure your function properly discards dangerous members "
|
||||
"{members}).".format(members=members),
|
||||
)
|
||||
elif level == bandit.MEDIUM:
|
||||
return bandit.Issue(
|
||||
severity=bandit.MEDIUM,
|
||||
confidence=bandit.MEDIUM,
|
||||
cwe=issue.Cwe.PATH_TRAVERSAL,
|
||||
text="Found tarfile.extractall(members=?) but couldn't "
|
||||
"identify the type of members. "
|
||||
"Check if the members were properly validated "
|
||||
"{members}).".format(members=members),
|
||||
)
|
||||
else:
|
||||
return bandit.Issue(
|
||||
severity=bandit.HIGH,
|
||||
confidence=bandit.HIGH,
|
||||
cwe=issue.Cwe.PATH_TRAVERSAL,
|
||||
text="tarfile.extractall used without any validation. "
|
||||
"Please check and discard dangerous members.",
|
||||
)
|
||||
|
||||
|
||||
def get_members_value(context):
|
||||
for keyword in context.node.keywords:
|
||||
if keyword.arg == "members":
|
||||
arg = keyword.value
|
||||
if isinstance(arg, ast.Call):
|
||||
return {"Function": arg.func.id}
|
||||
else:
|
||||
value = arg.id if isinstance(arg, ast.Name) else arg
|
||||
return {"Other": value}
|
||||
|
||||
|
||||
def is_filter_data(context):
|
||||
for keyword in context.node.keywords:
|
||||
if keyword.arg == "filter":
|
||||
arg = keyword.value
|
||||
return isinstance(arg, ast.Constant) and arg.value == "data"
|
||||
|
||||
|
||||
@test.test_id("B202")
|
||||
@test.checks("Call")
|
||||
def tarfile_unsafe_members(context):
|
||||
if all(
|
||||
[
|
||||
context.is_module_imported_exact("tarfile"),
|
||||
"extractall" in context.call_function_name,
|
||||
]
|
||||
):
|
||||
if "filter" in context.call_keywords and is_filter_data(context):
|
||||
return None
|
||||
if "members" in context.call_keywords:
|
||||
members = get_members_value(context)
|
||||
if "Function" in members:
|
||||
return exec_issue(bandit.LOW, members)
|
||||
else:
|
||||
return exec_issue(bandit.MEDIUM, members)
|
||||
return exec_issue(bandit.HIGH)
|
||||
@@ -0,0 +1,82 @@
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
r"""
|
||||
=====================================================
|
||||
B613: TrojanSource - Bidirectional control characters
|
||||
=====================================================
|
||||
|
||||
This plugin checks for the presence of unicode bidirectional control characters
|
||||
in Python source files. Those characters can be embedded in comments and strings
|
||||
to reorder source code characters in a way that changes its logic.
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
>> Issue: [B613:trojansource] A Python source file contains bidirectional control characters ('\u202e').
|
||||
Severity: High Confidence: Medium
|
||||
CWE: CWE-838 (https://cwe.mitre.org/data/definitions/838.html)
|
||||
More Info: https://bandit.readthedocs.io/en/1.7.5/plugins/b113_trojansource.html
|
||||
Location: examples/trojansource.py:4:25
|
||||
3 access_level = "user"
|
||||
4 if access_level != 'none': # Check if admin ' and access_level != 'user
|
||||
5 print("You are an admin.\n")
|
||||
|
||||
.. seealso::
|
||||
|
||||
- https://trojansource.codes/
|
||||
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-42574
|
||||
|
||||
.. versionadded:: 1.7.10
|
||||
|
||||
""" # noqa: E501
|
||||
from tokenize import detect_encoding
|
||||
|
||||
import bandit
|
||||
from bandit.core import issue
|
||||
from bandit.core import test_properties as test
|
||||
|
||||
|
||||
BIDI_CHARACTERS = (
|
||||
"\u202a",
|
||||
"\u202b",
|
||||
"\u202c",
|
||||
"\u202d",
|
||||
"\u202e",
|
||||
"\u2066",
|
||||
"\u2067",
|
||||
"\u2068",
|
||||
"\u2069",
|
||||
"\u200f",
|
||||
)
|
||||
|
||||
|
||||
@test.test_id("B613")
|
||||
@test.checks("File")
|
||||
def trojansource(context):
|
||||
src_data = context.file_data
|
||||
src_data.seek(0)
|
||||
encoding, _ = detect_encoding(src_data.readline)
|
||||
src_data.seek(0)
|
||||
for lineno, line in enumerate(
|
||||
src_data.read().decode(encoding).splitlines(), start=1
|
||||
):
|
||||
for char in BIDI_CHARACTERS:
|
||||
try:
|
||||
col_offset = line.index(char) + 1
|
||||
except ValueError:
|
||||
continue
|
||||
text = (
|
||||
"A Python source file contains bidirectional"
|
||||
" control characters (%r)." % char
|
||||
)
|
||||
b_issue = bandit.Issue(
|
||||
severity=bandit.HIGH,
|
||||
confidence=bandit.MEDIUM,
|
||||
cwe=issue.Cwe.INAPPROPRIATE_ENCODING_FOR_OUTPUT_CONTEXT,
|
||||
text=text,
|
||||
lineno=lineno,
|
||||
col_offset=col_offset,
|
||||
)
|
||||
b_issue.linerange = [lineno]
|
||||
return b_issue
|
||||
@@ -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."),
|
||||
)
|
||||
@@ -0,0 +1,165 @@
|
||||
# Copyright (c) 2015 VMware, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
r"""
|
||||
=========================================
|
||||
B505: Test for weak cryptographic key use
|
||||
=========================================
|
||||
|
||||
As computational power increases, so does the ability to break ciphers with
|
||||
smaller key lengths. The recommended key length size for RSA and DSA algorithms
|
||||
is 2048 and higher. 1024 bits and below are now considered breakable. EC key
|
||||
length sizes are recommended to be 224 and higher with 160 and below considered
|
||||
breakable. This plugin test checks for use of any key less than those limits
|
||||
and returns a high severity error if lower than the lower threshold and a
|
||||
medium severity error for those lower than the higher threshold.
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
>> Issue: DSA key sizes below 1024 bits are considered breakable.
|
||||
Severity: High Confidence: High
|
||||
CWE: CWE-326 (https://cwe.mitre.org/data/definitions/326.html)
|
||||
Location: examples/weak_cryptographic_key_sizes.py:36
|
||||
35 # Also incorrect: without keyword args
|
||||
36 dsa.generate_private_key(512,
|
||||
37 backends.default_backend())
|
||||
38 rsa.generate_private_key(3,
|
||||
|
||||
.. seealso::
|
||||
|
||||
- https://csrc.nist.gov/publications/detail/sp/800-131a/rev-2/final
|
||||
- https://security.openstack.org/guidelines/dg_strong-crypto.html
|
||||
- https://cwe.mitre.org/data/definitions/326.html
|
||||
|
||||
.. versionadded:: 0.14.0
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
CWE information added
|
||||
|
||||
"""
|
||||
import bandit
|
||||
from bandit.core import issue
|
||||
from bandit.core import test_properties as test
|
||||
|
||||
|
||||
def gen_config(name):
|
||||
if name == "weak_cryptographic_key":
|
||||
return {
|
||||
"weak_key_size_dsa_high": 1024,
|
||||
"weak_key_size_dsa_medium": 2048,
|
||||
"weak_key_size_rsa_high": 1024,
|
||||
"weak_key_size_rsa_medium": 2048,
|
||||
"weak_key_size_ec_high": 160,
|
||||
"weak_key_size_ec_medium": 224,
|
||||
}
|
||||
|
||||
|
||||
def _classify_key_size(config, key_type, key_size):
|
||||
if isinstance(key_size, str):
|
||||
# size provided via a variable - can't process it at the moment
|
||||
return
|
||||
|
||||
key_sizes = {
|
||||
"DSA": [
|
||||
(config["weak_key_size_dsa_high"], bandit.HIGH),
|
||||
(config["weak_key_size_dsa_medium"], bandit.MEDIUM),
|
||||
],
|
||||
"RSA": [
|
||||
(config["weak_key_size_rsa_high"], bandit.HIGH),
|
||||
(config["weak_key_size_rsa_medium"], bandit.MEDIUM),
|
||||
],
|
||||
"EC": [
|
||||
(config["weak_key_size_ec_high"], bandit.HIGH),
|
||||
(config["weak_key_size_ec_medium"], bandit.MEDIUM),
|
||||
],
|
||||
}
|
||||
|
||||
for size, level in key_sizes[key_type]:
|
||||
if key_size < size:
|
||||
return bandit.Issue(
|
||||
severity=level,
|
||||
confidence=bandit.HIGH,
|
||||
cwe=issue.Cwe.INADEQUATE_ENCRYPTION_STRENGTH,
|
||||
text="%s key sizes below %d bits are considered breakable. "
|
||||
% (key_type, size),
|
||||
)
|
||||
|
||||
|
||||
def _weak_crypto_key_size_cryptography_io(context, config):
|
||||
func_key_type = {
|
||||
"cryptography.hazmat.primitives.asymmetric.dsa."
|
||||
"generate_private_key": "DSA",
|
||||
"cryptography.hazmat.primitives.asymmetric.rsa."
|
||||
"generate_private_key": "RSA",
|
||||
"cryptography.hazmat.primitives.asymmetric.ec."
|
||||
"generate_private_key": "EC",
|
||||
}
|
||||
arg_position = {
|
||||
"DSA": 0,
|
||||
"RSA": 1,
|
||||
"EC": 0,
|
||||
}
|
||||
key_type = func_key_type.get(context.call_function_name_qual)
|
||||
if key_type in ["DSA", "RSA"]:
|
||||
key_size = (
|
||||
context.get_call_arg_value("key_size")
|
||||
or context.get_call_arg_at_position(arg_position[key_type])
|
||||
or 2048
|
||||
)
|
||||
return _classify_key_size(config, key_type, key_size)
|
||||
elif key_type == "EC":
|
||||
curve_key_sizes = {
|
||||
"SECT571K1": 571,
|
||||
"SECT571R1": 570,
|
||||
"SECP521R1": 521,
|
||||
"BrainpoolP512R1": 512,
|
||||
"SECT409K1": 409,
|
||||
"SECT409R1": 409,
|
||||
"BrainpoolP384R1": 384,
|
||||
"SECP384R1": 384,
|
||||
"SECT283K1": 283,
|
||||
"SECT283R1": 283,
|
||||
"BrainpoolP256R1": 256,
|
||||
"SECP256K1": 256,
|
||||
"SECP256R1": 256,
|
||||
"SECT233K1": 233,
|
||||
"SECT233R1": 233,
|
||||
"SECP224R1": 224,
|
||||
"SECP192R1": 192,
|
||||
"SECT163K1": 163,
|
||||
"SECT163R2": 163,
|
||||
}
|
||||
curve = context.get_call_arg_value("curve") or (
|
||||
len(context.call_args) > arg_position[key_type]
|
||||
and context.call_args[arg_position[key_type]]
|
||||
)
|
||||
key_size = curve_key_sizes[curve] if curve in curve_key_sizes else 224
|
||||
return _classify_key_size(config, key_type, key_size)
|
||||
|
||||
|
||||
def _weak_crypto_key_size_pycrypto(context, config):
|
||||
func_key_type = {
|
||||
"Crypto.PublicKey.DSA.generate": "DSA",
|
||||
"Crypto.PublicKey.RSA.generate": "RSA",
|
||||
"Cryptodome.PublicKey.DSA.generate": "DSA",
|
||||
"Cryptodome.PublicKey.RSA.generate": "RSA",
|
||||
}
|
||||
key_type = func_key_type.get(context.call_function_name_qual)
|
||||
if key_type:
|
||||
key_size = (
|
||||
context.get_call_arg_value("bits")
|
||||
or context.get_call_arg_at_position(0)
|
||||
or 2048
|
||||
)
|
||||
return _classify_key_size(config, key_type, key_size)
|
||||
|
||||
|
||||
@test.takes_config
|
||||
@test.checks("Call")
|
||||
@test.test_id("B505")
|
||||
def weak_cryptographic_key(context, config):
|
||||
return _weak_crypto_key_size_cryptography_io(
|
||||
context, config
|
||||
) or _weak_crypto_key_size_pycrypto(context, config)
|
||||
@@ -0,0 +1,76 @@
|
||||
#
|
||||
# Copyright (c) 2016 Rackspace, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
r"""
|
||||
===============================
|
||||
B506: Test for use of yaml load
|
||||
===============================
|
||||
|
||||
This plugin test checks for the unsafe usage of the ``yaml.load`` function from
|
||||
the PyYAML package. The yaml.load function provides the ability to construct
|
||||
an arbitrary Python object, which may be dangerous if you receive a YAML
|
||||
document from an untrusted source. The function yaml.safe_load limits this
|
||||
ability to simple Python objects like integers or lists.
|
||||
|
||||
Please see
|
||||
https://pyyaml.org/wiki/PyYAMLDocumentation#LoadingYAML for more information
|
||||
on ``yaml.load`` and yaml.safe_load
|
||||
|
||||
:Example:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
>> Issue: [yaml_load] Use of unsafe yaml load. Allows instantiation of
|
||||
arbitrary objects. Consider yaml.safe_load().
|
||||
Severity: Medium Confidence: High
|
||||
CWE: CWE-20 (https://cwe.mitre.org/data/definitions/20.html)
|
||||
Location: examples/yaml_load.py:5
|
||||
4 ystr = yaml.dump({'a' : 1, 'b' : 2, 'c' : 3})
|
||||
5 y = yaml.load(ystr)
|
||||
6 yaml.dump(y)
|
||||
|
||||
.. seealso::
|
||||
|
||||
- https://pyyaml.org/wiki/PyYAMLDocumentation#LoadingYAML
|
||||
- https://cwe.mitre.org/data/definitions/20.html
|
||||
|
||||
.. versionadded:: 1.0.0
|
||||
|
||||
.. versionchanged:: 1.7.3
|
||||
CWE information added
|
||||
|
||||
"""
|
||||
import bandit
|
||||
from bandit.core import issue
|
||||
from bandit.core import test_properties as test
|
||||
|
||||
|
||||
@test.test_id("B506")
|
||||
@test.checks("Call")
|
||||
def yaml_load(context):
|
||||
imported = context.is_module_imported_exact("yaml")
|
||||
qualname = context.call_function_name_qual
|
||||
if not imported and isinstance(qualname, str):
|
||||
return
|
||||
|
||||
qualname_list = qualname.split(".")
|
||||
func = qualname_list[-1]
|
||||
if all(
|
||||
[
|
||||
"yaml" in qualname_list,
|
||||
func == "load",
|
||||
not context.check_call_arg_value("Loader", "SafeLoader"),
|
||||
not context.check_call_arg_value("Loader", "CSafeLoader"),
|
||||
not context.get_call_arg_at_position(1) == "SafeLoader",
|
||||
not context.get_call_arg_at_position(1) == "CSafeLoader",
|
||||
]
|
||||
):
|
||||
return bandit.Issue(
|
||||
severity=bandit.MEDIUM,
|
||||
confidence=bandit.HIGH,
|
||||
cwe=issue.Cwe.IMPROPER_INPUT_VALIDATION,
|
||||
text="Use of unsafe yaml load. Allows instantiation of"
|
||||
" arbitrary objects. Consider yaml.safe_load().",
|
||||
lineno=context.node.lineno,
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,130 @@
|
||||
../../Scripts/black.exe,sha256=ychfrZ4aaWq4HfMUXo25RY6bYSl1_LdY3KL1Si7cOrM,108405
|
||||
../../Scripts/blackd.exe,sha256=zZw9IRryc8mlVMma373KM0ST4lfhT_P2sSgZkeEY0qA,108406
|
||||
30fcd23745efe32ce681__mypyc.cp312-win_amd64.pyd,sha256=-KUvtPAnWHKTrSZ0rbdGyZaFWCqtBVY9BaP6AeyW2bY,2929152
|
||||
__pycache__/_black_version.cpython-312.pyc,,
|
||||
_black_version.py,sha256=pkSDq4SPXqNR5Q78ORXayDwPtHplBk-EbnHUUhU9HTI,20
|
||||
_black_version.pyi,sha256=gXF2iFKaJ_mJncCEFLjOHdt3GxGdW_dheaJIpcZ2IwA,14
|
||||
black-26.5.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
black-26.5.1.dist-info/METADATA,sha256=nAW2UCTjFtujwLK6EKGb_S3_Oe6VpNImCivl56VbYu8,95112
|
||||
black-26.5.1.dist-info/RECORD,,
|
||||
black-26.5.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
black-26.5.1.dist-info/WHEEL,sha256=353KTKtFv5jzKf9xDgZj0filE6YAfpYWbzJsyOxzkvc,97
|
||||
black-26.5.1.dist-info/entry_points.txt,sha256=XTCA4X2yVA0tMiV7l96Gv9TyxhVhoCaznLN2XThqYSA,144
|
||||
black-26.5.1.dist-info/licenses/LICENSE,sha256=XQJSBb4crFXeCOvZ-WHsfXTQ-Zj2XxeFbd0ien078zM,1101
|
||||
black/__init__.cp312-win_amd64.pyd,sha256=K-RAMdnbj77xfo6B-tTY1eZJfjRkZ5KcRY_wQxIvQxY,10752
|
||||
black/__init__.py,sha256=GzFq-xWITQDt8ORYq1Ncb70KQdgXNEOElncc7I56y98,57315
|
||||
black/__main__.py,sha256=6V0pV9Zeh8940mbQbVTCPdTX4Gjq1HGrFCA6E4HLGaM,50
|
||||
black/__pycache__/__init__.cpython-312.pyc,,
|
||||
black/__pycache__/__main__.cpython-312.pyc,,
|
||||
black/__pycache__/_width_table.cpython-312.pyc,,
|
||||
black/__pycache__/brackets.cpython-312.pyc,,
|
||||
black/__pycache__/cache.cpython-312.pyc,,
|
||||
black/__pycache__/comments.cpython-312.pyc,,
|
||||
black/__pycache__/concurrency.cpython-312.pyc,,
|
||||
black/__pycache__/const.cpython-312.pyc,,
|
||||
black/__pycache__/debug.cpython-312.pyc,,
|
||||
black/__pycache__/files.cpython-312.pyc,,
|
||||
black/__pycache__/handle_ipynb_magics.cpython-312.pyc,,
|
||||
black/__pycache__/linegen.cpython-312.pyc,,
|
||||
black/__pycache__/lines.cpython-312.pyc,,
|
||||
black/__pycache__/mode.cpython-312.pyc,,
|
||||
black/__pycache__/nodes.cpython-312.pyc,,
|
||||
black/__pycache__/numerics.cpython-312.pyc,,
|
||||
black/__pycache__/output.cpython-312.pyc,,
|
||||
black/__pycache__/parsing.cpython-312.pyc,,
|
||||
black/__pycache__/ranges.cpython-312.pyc,,
|
||||
black/__pycache__/report.cpython-312.pyc,,
|
||||
black/__pycache__/rusty.cpython-312.pyc,,
|
||||
black/__pycache__/schema.cpython-312.pyc,,
|
||||
black/__pycache__/strings.cpython-312.pyc,,
|
||||
black/__pycache__/trans.cpython-312.pyc,,
|
||||
black/_width_table.cp312-win_amd64.pyd,sha256=uQf35NsYEdr_oQG-fGyJlKMCnuh1FaGy8myJGIjAlUI,10752
|
||||
black/_width_table.py,sha256=nOVdpn2H49kF-h4bPzNAJ_zfb6PJ7a1czEx4N124hq0,3228
|
||||
black/brackets.cp312-win_amd64.pyd,sha256=KbLy50412RL3dv2HL9-rSeBMIPLLLdY-cr0sVOUsGdQ,10752
|
||||
black/brackets.py,sha256=ugO-Aga8aidqXdgqwlHNcBNBcxV1QtEWVPsT5JwXelY,12793
|
||||
black/cache.cp312-win_amd64.pyd,sha256=0uChb_GXnrTrP579KFGlGTH6ucc-6qHJP89vWjxeSG0,10752
|
||||
black/cache.py,sha256=ty9qn9qL7dz7a82dFa8zYFvQprEL4avnJ6zAlDcqwqA,4904
|
||||
black/comments.cp312-win_amd64.pyd,sha256=AbzabdsKc52SGsToK2xNZlAmEVJzzJSBMq829652Ewk,10752
|
||||
black/comments.py,sha256=uUMAUxcA7lmfeo1pGI7cYXPr4DdmAwpVDyHBx4aYM6g,32164
|
||||
black/concurrency.py,sha256=_b74I3Nwo3Dh4OYOW8mLZdATvQgYZnVGSulZPY1MTAg,7794
|
||||
black/const.cp312-win_amd64.pyd,sha256=yeJU-fu_gPvLKffTx9XXR5Pnd-kg8umAvSf4AUUHB7k,10752
|
||||
black/const.py,sha256=FP5YcSxH6Cb0jqSkwF0nI4dHxPyQtL34hoWBfAqnAhI,325
|
||||
black/debug.py,sha256=DJ5Cm2HaJT_J8H5_hqw-gYX1nVX5rh8595e2xagmRKI,1977
|
||||
black/files.py,sha256=-wVRP-4ZOUM8x3ylqiESy6QXa2pTpv_e63b2Sh0w2t0,15090
|
||||
black/handle_ipynb_magics.cp312-win_amd64.pyd,sha256=fyG0fHrOnF1XcIh_p871Ms2BTJlHXfn9iKbNaXuuwN0,10752
|
||||
black/handle_ipynb_magics.py,sha256=4dDae_maXzIXdjK6lwVBi6hzRFzrCG9Bht7IsYy6xdU,16416
|
||||
black/linegen.cp312-win_amd64.pyd,sha256=2YeIymbt-zvIHi-2bIpyYRTQARrPX-sY-SI-dhtmRaY,10752
|
||||
black/linegen.py,sha256=fcknlpYe4XqpVMFNUvUWWwpWGW9lExCa9e-w-2DiJWc,83689
|
||||
black/lines.cp312-win_amd64.pyd,sha256=91Y7UnMKThTGi9k-xqk-MlBcRHP92eNA7u5MFISGy_I,10752
|
||||
black/lines.py,sha256=6Qvi1Dqtum_O7x_jm5WpJ4cv_kQe7TAaIyZdcgZ9JVs,61427
|
||||
black/mode.cp312-win_amd64.pyd,sha256=SqJZ8qQBRJALf03Yg1tiv2Av5RBZ7sJz_8SMUWD8OAk,10752
|
||||
black/mode.py,sha256=H1Cd7GLkmGbV32eZ3rAThibphbKbeEqXrZwLR7o-EPU,11587
|
||||
black/nodes.cp312-win_amd64.pyd,sha256=b4UoAkm1bYdui4deH6NAXJlJnsUfePPsHTBb9lvP1Z4,10752
|
||||
black/nodes.py,sha256=ujH9bVGX4-b2VmNubGtr8sE8EwTB99K-OOhzhLdRSNE,33394
|
||||
black/numerics.cp312-win_amd64.pyd,sha256=z2oS1MtE-fgd3QFenyG3PdkNV-x3Ne-XTlSuNizsARg,10752
|
||||
black/numerics.py,sha256=gB1T1-npxj44Vhex63ov-oGsoIPwx_PZlT-n_qUcwO4,1716
|
||||
black/output.py,sha256=4hEQsqNDlgWZTAJ-n0muusxin2HR8AxSNRmuZrjWoCM,4033
|
||||
black/parsing.cp312-win_amd64.pyd,sha256=vkvYeP_T38Gdc17id4q2VuoiH_WCCUkW0PGN_HLwKIk,10752
|
||||
black/parsing.py,sha256=Fw7KwsA8F4oaRpAI8wEoJINbxQ_OxjJeNJ5E4UhJaxg,9447
|
||||
black/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
black/ranges.cp312-win_amd64.pyd,sha256=NyJZXZd1I881_uy-rlsGe4Eg8vzMuCfGdJ2X4-Qj7m8,10752
|
||||
black/ranges.py,sha256=V6JhiIQCNUhgykRTsgdhqwz0CzHu3VeYk2JuRk5lxRw,21128
|
||||
black/report.py,sha256=8Xies3PseQeTN4gYfHS7RewVQRjDsDBfFDR3sSNytco,3559
|
||||
black/resources/__init__.cp312-win_amd64.pyd,sha256=NYcVsIoxZY-kJKdp0LJkKLChMhXjX7PQ3ivqmEWzOzc,10752
|
||||
black/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
black/resources/__pycache__/__init__.cpython-312.pyc,,
|
||||
black/resources/black.schema.json,sha256=YrLe8-vKGuruDVCtG-v1LA5fj_cao_LD6YnXFy6hV4c,7656
|
||||
black/rusty.cp312-win_amd64.pyd,sha256=0gn4B1LnK70Ss5xNKT8YeapRrYaj3c2NNLkp1V_AJ28,10752
|
||||
black/rusty.py,sha256=RogIomJ1RCLMTOK_RA6U3EMbzWV_ZHxPtrXveXbMjzQ,585
|
||||
black/schema.cp312-win_amd64.pyd,sha256=QYsA-f2eIw8H-8NQikZUU0Zb6qSJqDZ4qABDqpR1n7I,10752
|
||||
black/schema.py,sha256=ZLKjanGVK4bG12GD7mkDzjOtLkv_g86p1-TXrdN9Skc,446
|
||||
black/strings.cp312-win_amd64.pyd,sha256=9Wme-NdDVseJlNTXVxl0ByPWxLFSPNba1-867grBCIQ,10752
|
||||
black/strings.py,sha256=da_BcPrREAbBVURIVPLmKAx7ZdsKYHKZ0PWMtjN63MM,13687
|
||||
black/trans.cp312-win_amd64.pyd,sha256=n0Nzlrzj5H8163oRSudDGI6suQpsz3bnSPQpPb0iUo0,10752
|
||||
black/trans.py,sha256=0mrVI5xJyVbAMh5Ngxhi5ue0zyX1jgAsEfZcdLhEsiM,100499
|
||||
blackd/__init__.py,sha256=cDc5XYBxJJ0zlFPStG-vPF28vxGo3BiRpA2pw6PPSR4,10779
|
||||
blackd/__main__.py,sha256=-2NrSIZ5Es7pTFThp8w5JL9LwmmxtF1akhe7NU1OGvs,40
|
||||
blackd/__pycache__/__init__.cpython-312.pyc,,
|
||||
blackd/__pycache__/__main__.cpython-312.pyc,,
|
||||
blackd/__pycache__/client.cpython-312.pyc,,
|
||||
blackd/__pycache__/middlewares.cpython-312.pyc,,
|
||||
blackd/client.py,sha256=u7mkAPVsONR2Y4ucQNFgCLwK3WOHX7UojGQzXKpJDCI,4078
|
||||
blackd/middlewares.py,sha256=mnIQU5_0sp7sVW5RESY9fSNxBDr9wpRsXdNA1_bYAIQ,1532
|
||||
blib2to3/Grammar.txt,sha256=nf_qqpHNT85EHkGlen7L5A_sJyWPxapelvoUYT8PfWs,12351
|
||||
blib2to3/LICENSE,sha256=D2HM6JsydKABNqFe2-_N4Lf8VxxE1_5DVQtAFzw2_w8,13016
|
||||
blib2to3/PatternGrammar.txt,sha256=tH-u-ZLZsHJGgjZEfXN2S8GVCKXleQwWvOf7sO4hlmo,835
|
||||
blib2to3/README,sha256=G-DiXkC8aKINCNv7smI2q_mz-8k6kC4yYO2OrMb0Nqs,1098
|
||||
blib2to3/__init__.py,sha256=CSR2VOIKJL-JnGG41PcfbQZQEPCw43jfeK_EUisNsFQ,9
|
||||
blib2to3/__pycache__/__init__.cpython-312.pyc,,
|
||||
blib2to3/__pycache__/pygram.cpython-312.pyc,,
|
||||
blib2to3/__pycache__/pytree.cpython-312.pyc,,
|
||||
blib2to3/pgen2/__init__.py,sha256=z8NemtNtAaIBocPMl0aMLgxaQMedsKOS_dOVAy8c3TI,147
|
||||
blib2to3/pgen2/__pycache__/__init__.cpython-312.pyc,,
|
||||
blib2to3/pgen2/__pycache__/conv.cpython-312.pyc,,
|
||||
blib2to3/pgen2/__pycache__/driver.cpython-312.pyc,,
|
||||
blib2to3/pgen2/__pycache__/grammar.cpython-312.pyc,,
|
||||
blib2to3/pgen2/__pycache__/literals.cpython-312.pyc,,
|
||||
blib2to3/pgen2/__pycache__/parse.cpython-312.pyc,,
|
||||
blib2to3/pgen2/__pycache__/pgen.cpython-312.pyc,,
|
||||
blib2to3/pgen2/__pycache__/token.cpython-312.pyc,,
|
||||
blib2to3/pgen2/__pycache__/tokenize.cpython-312.pyc,,
|
||||
blib2to3/pgen2/conv.cp312-win_amd64.pyd,sha256=d9HmRFwm-1eNlev-17RhhefPVUERkkMYoMoxXImO0Mo,10752
|
||||
blib2to3/pgen2/conv.py,sha256=oxdquGP469PeQwFmGM92A9FJEtZlQVb7_v4nFqzzsXY,9852
|
||||
blib2to3/pgen2/driver.cp312-win_amd64.pyd,sha256=-aVEfSKOpwb92TXchJvVeQyI5xNpTWmjtrTGV9Zm4So,10752
|
||||
blib2to3/pgen2/driver.py,sha256=MSY0gFbSqNfvdjD81gLFK5-sTIebRZqPRs90-ZLE47Q,10756
|
||||
blib2to3/pgen2/grammar.cp312-win_amd64.pyd,sha256=lZaPhmcXhfRqM7HE_SaXPV631K8bZ0GyMfBfPl-qHGY,10752
|
||||
blib2to3/pgen2/grammar.py,sha256=kWLJf3bdvHO4g_b3M_EEo98YIoBAwUBtFFYWd7kFH0c,7074
|
||||
blib2to3/pgen2/literals.cp312-win_amd64.pyd,sha256=W-YZ66hb7Qqo51tivpAZtrjURjdx0Hxjy6qXqX9UAS8,10752
|
||||
blib2to3/pgen2/literals.py,sha256=eWQ54eEpsIJ5wohd__6dd0kk960QM5eCgMSgkg7eFZk,1645
|
||||
blib2to3/pgen2/parse.cp312-win_amd64.pyd,sha256=3A3UAWC6dByw__GqB9Ruj-oE0hKJq9GpSR2OuK_BqY0,10752
|
||||
blib2to3/pgen2/parse.py,sha256=EtUNRDQWx_P895Zj-Jb9IOkupgKXPxs2WBYbhtpDXu8,15873
|
||||
blib2to3/pgen2/pgen.cp312-win_amd64.pyd,sha256=DWrxIG_ma2TWWREah2SXv_E8YTm3OQHUrEn7Td9bg0g,10752
|
||||
blib2to3/pgen2/pgen.py,sha256=CHwNcnBZ2FW9EmTP8kmQ6har9mJdCPPSLEC8_C6zfJ4,14519
|
||||
blib2to3/pgen2/token.cp312-win_amd64.pyd,sha256=zKxCLoVRB9ieiTBFKsq61ukMDA4UWf7IsZvRrFS9p9c,10752
|
||||
blib2to3/pgen2/token.py,sha256=NSLaE9zjdundWk3DYgGIqA-yUxgf619ZrMxPHTqoEao,1894
|
||||
blib2to3/pgen2/tokenize.cp312-win_amd64.pyd,sha256=NtFQgKWIJXTtCBeeISHbUOdc5dedFF8aqMsvMWqG8dQ,10752
|
||||
blib2to3/pgen2/tokenize.py,sha256=LoXIebhiZXvMnN4bANlDBo9qfLcMNhHC1Az5ZwAxiiM,9314
|
||||
blib2to3/pygram.cp312-win_amd64.pyd,sha256=ythQVKAdNa9Yr0MsjrMfOha4vAunAr4qdrgBlJnsF5E,10752
|
||||
blib2to3/pygram.py,sha256=U0UgEtRm16EakKHB0Ei7LJYE4WjU9kts4YuhEEDnvJE,5250
|
||||
blib2to3/pytree.cp312-win_amd64.pyd,sha256=_Bk53CrT8Pov0DS1nMVMad8sWko-vOH2D7VXbDUKoB0,10752
|
||||
blib2to3/pytree.py,sha256=Ue3gQZVywQiHlnfvLnbbXpWdYXbf1dncgZNTQD3tCfU,33356
|
||||
@@ -0,0 +1,4 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: hatchling 1.29.0
|
||||
Root-Is-Purelib: false
|
||||
Tag: cp312-cp312-win_amd64
|
||||
@@ -0,0 +1,6 @@
|
||||
[console_scripts]
|
||||
black = black:patched_main
|
||||
blackd = blackd:patched_main [d]
|
||||
|
||||
[validate_pyproject.tool_schema]
|
||||
black = black.schema:get_schema
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Łukasz Langa
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user