diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 1451c5e77..4569f95dc 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -44,22 +44,18 @@ body: label: Bandit version description: Run "bandit --version" if unsure of version number options: - - 1.7.6 (Default) + - 1.8.0 (Default) + - 1.7.10 + - 1.7.9 + - 1.7.8 + - 1.7.7 + - 1.7.6 - 1.7.5 - 1.7.4 - 1.7.3 - 1.7.2 - 1.7.1 - 1.7.0 - - 1.6.3 - - 1.6.2 - - 1.6.1 - - 1.6.0 - - 1.5.1 - - 1.5.0 - - 1.4.0 - - 1.3.0 - - 0.17.0-eol validations: required: true @@ -69,14 +65,11 @@ body: label: Python version description: Run "bandit --version" if unsure of version number options: - - "3.12 (Default)" + - "3.13 (Default)" + - "3.12" - "3.11" - "3.10" - "3.9" - - "3.8" - - "3.7" - - "3.6" - - "3.5" validations: required: true diff --git a/.github/workflows/build-publish-image.yml b/.github/workflows/build-publish-image.yml index 8ce2a1e8e..8db96a2d2 100644 --- a/.github/workflows/build-publish-image.yml +++ b/.github/workflows/build-publish-image.yml @@ -31,17 +31,17 @@ jobs: ref: ${{ github.event_name == 'release' && github.ref || env.RELEASE_TAG }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3 + uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3 - name: Log in to GitHub Container Registry - uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3 + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Install Cosign - uses: sigstore/cosign-installer@e1523de7571e31dbe865fd2e80c5c7c23ae71eb4 # v3.4.0 + uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 with: cosign-release: 'v2.2.2' @@ -51,7 +51,7 @@ jobs: - name: Build and push Docker image id: build-and-push - uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5 + uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6 with: context: . file: ./docker/Dockerfile diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 7f72c66e8..a295a9c36 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -13,10 +13,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up Python 3.8 + - name: Set up Python 3.9 uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies run: pip install wheel diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml index 89153cf8f..252ad5b88 100644 --- a/.github/workflows/publish-to-test-pypi.yml +++ b/.github/workflows/publish-to-test-pypi.yml @@ -13,10 +13,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up Python 3.8 + - name: Set up Python 3.9 uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies run: pip install wheel diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 94dd399ee..edfd03ac3 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8] + python-version: [3.9] steps: - name: Checkout repository uses: actions/checkout@v4 @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8] + python-version: [3.9] steps: - name: Checkout repository uses: actions/checkout@v4 @@ -47,11 +47,11 @@ jobs: strategy: matrix: python-version: [ - ["3.8", "38"], ["3.9", "39"], ["3.10", "310"], ["3.11", "311"], ["3.12", "312"], + ["3.13", "313"], ] os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6edc03a1b..4ee5fe051 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,24 +1,24 @@ exclude: ^(examples|tools|doc) repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v5.0.0 hooks: - id: check-yaml - id: debug-statements - id: end-of-file-fixer - id: trailing-whitespace -- repo: https://github.com/asottile/reorder_python_imports - rev: v3.9.0 +- repo: https://github.com/asottile/reorder-python-imports + rev: v3.14.0 hooks: - id: reorder-python-imports args: [--application-directories, '.:src', --py38-plus] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.10.1 + rev: 24.10.0 hooks: - id: black args: [--line-length=79, --target-version=py38] - repo: https://github.com/asottile/pyupgrade - rev: v3.3.2 + rev: v3.19.1 hooks: - id: pyupgrade args: [--py38-plus] diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 4afff4aba..15fc1366e 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,7 +3,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.8" + python: "3.9" sphinx: configuration: doc/source/conf.py @@ -14,3 +14,5 @@ python: - requirements: doc/requirements.txt - method: pip path: . + extra_requirements: + - sarif diff --git a/.stestr.conf b/.stestr.conf index 126bb41fa..ebc11d4c9 100644 --- a/.stestr.conf +++ b/.stestr.conf @@ -1,4 +1,4 @@ [DEFAULT] -test_path=${OS_TEST_PATH:-./tests} +test_path=./tests top_dir=./ -group_regex=.*(test_cert_setup) +parallel_class=True diff --git a/README.rst b/README.rst index fbc6e161d..79615fffc 100644 --- a/README.rst +++ b/README.rst @@ -117,3 +117,29 @@ source of origin using the following cosign command: --certificate-oidc-issuer https://token.actions.githubusercontent.com Where `` is the release version of Bandit. + +Sponsors +-------- + +The development of Bandit is made possible by the following sponsors: + +.. list-table:: + :width: 100% + :class: borderless + + * - .. image:: https://avatars.githubusercontent.com/u/34240465?s=200&v=4 + :target: https://opensource.mercedes-benz.com/ + :alt: Mercedes-Benz + :width: 88 + + - .. image:: https://github.githubassets.com/assets/tidelift-8cea37dea8fc.svg + :target: https://tidelift.com/lifter/search/pypi/bandit + :alt: Tidelift + :width: 88 + + - .. image:: https://avatars.githubusercontent.com/u/110237746?s=200&v=4 + :target: https://stacklok.com/ + :alt: Stacklok + :width: 88 + +If you also ❤️ Bandit, please consider sponsoring. diff --git a/bandit/__init__.py b/bandit/__init__.py index 75f863db2..7c7bf00a8 100644 --- a/bandit/__init__.py +++ b/bandit/__init__.py @@ -16,4 +16,5 @@ from bandit.core.issue import * # noqa from bandit.core.test_properties import * # noqa +__author__ = metadata.metadata("bandit")["Author"] __version__ = metadata.version("bandit") diff --git a/bandit/blacklists/calls.py b/bandit/blacklists/calls.py index d69f5dd3c..86f08a61d 100644 --- a/bandit/blacklists/calls.py +++ b/bandit/blacklists/calls.py @@ -96,6 +96,12 @@ | | | .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 | | @@ -213,7 +219,7 @@ | B312 | telnetlib | - telnetlib.\* | High | +------+---------------------+------------------------------------+-----------+ -B313 - B320: XML +B313 - B319: XML ---------------- Most of this is based off of Christian Heimes' work on defusedxml: @@ -250,6 +256,15 @@ | 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 | | @@ -321,8 +336,6 @@ +------+---------------------+------------------------------------+-----------+ """ -import sys - from bandit.blacklists import utils from bandit.core import issue @@ -373,52 +386,26 @@ def gen_blacklist(): ) ) - if sys.version_info >= (3, 9): - 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.", - ) - ) - else: - sets.append( - utils.build_conf_dict( - "md5", - "B303", - issue.Cwe.BROKEN_CRYPTO, - [ - "hashlib.md4", - "hashlib.md5", - "hashlib.sha", - "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", - ], - "Use of insecure MD2, MD4, MD5, or SHA1 hash function.", - ) + 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( @@ -438,7 +425,10 @@ def gen_blacklist(): "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.", @@ -537,7 +527,7 @@ def gen_blacklist(): "telnetlib", "B312", issue.Cwe.CLEARTEXT_TRANSMISSION, - ["telnetlib.*"], + ["telnetlib.Telnet"], "Telnet-related functions are being called. Telnet is considered " "insecure. Use SSH or some other encrypted protocol.", "HIGH", @@ -634,26 +624,7 @@ def gen_blacklist(): ) ) - sets.append( - utils.build_conf_dict( - "xml_bad_etree", - "B320", - issue.Cwe.IMPROPER_INPUT_VALIDATION, - [ - "lxml.etree.parse", - "lxml.etree.fromstring", - "lxml.etree.RestrictedElement", - "lxml.etree.GlobalParserTLS", - "lxml.etree.getDefaultParser", - "lxml.etree.check_docinfo", - ], - ( - "Using {name} to parse untrusted XML data is known to be " - "vulnerable to XML attacks. Replace {name} with its " - "defusedxml equivalent function." - ), - ) - ) + # skipped B320 as the check for a call to lxml.etree has been removed # end of XML tests @@ -662,7 +633,7 @@ def gen_blacklist(): "ftplib", "B321", issue.Cwe.CLEARTEXT_TRANSMISSION, - ["ftplib.*"], + ["ftplib.FTP"], "FTP-related functions are being called. FTP is considered " "insecure. Use SSH/SFTP/SCP or some other encrypted protocol.", "HIGH", diff --git a/bandit/blacklists/imports.py b/bandit/blacklists/imports.py index 58dfcb3c7..b15155b65 100644 --- a/bandit/blacklists/imports.py +++ b/bandit/blacklists/imports.py @@ -133,6 +133,9 @@ 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. @@ -297,11 +300,6 @@ def gen_blacklist(): "defusedxml package, or make sure defusedxml.defuse_stdlib() " "is called." ) - lxml_msg = ( - "Using {name} to parse untrusted XML data is known to be " - "vulnerable to XML attacks. Replace {name} with the " - "equivalent defusedxml package." - ) sets.append( utils.build_conf_dict( @@ -358,16 +356,7 @@ def gen_blacklist(): ) ) - sets.append( - utils.build_conf_dict( - "import_lxml", - "B410", - issue.Cwe.IMPROPER_INPUT_VALIDATION, - ["lxml"], - lxml_msg, - "LOW", - ) - ) + # skipped B410 as the check for import_lxml has been removed sets.append( utils.build_conf_dict( diff --git a/bandit/cli/main.py b/bandit/cli/main.py index 119380b28..0cb0f8d5f 100644 --- a/bandit/cli/main.py +++ b/bandit/cli/main.py @@ -450,16 +450,17 @@ def main(): args.confidence = 4 # Other strings will be blocked by argparse - try: - b_conf = b_config.BanditConfig(config_file=args.config_file) - except utils.ConfigError as e: - LOG.error(e) - sys.exit(2) - # Handle .bandit files in projects to pass cmdline args from file ini_options = _get_options_from_ini(args.ini_path, args.targets) if ini_options: # prefer command line, then ini file + args.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, @@ -592,6 +593,12 @@ def main(): "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) diff --git a/bandit/core/blacklisting.py b/bandit/core/blacklisting.py index 2f84ae023..2bbb093d5 100644 --- a/bandit/core/blacklisting.py +++ b/bandit/core/blacklisting.py @@ -3,7 +3,6 @@ # # SPDX-License-Identifier: Apache-2.0 import ast -import fnmatch from bandit.core import issue @@ -55,7 +54,7 @@ def blacklist(context, config): name = context.call_keywords["name"] for check in blacklists[node_type]: for qn in check["qualnames"]: - if name is not None and fnmatch.fnmatch(name, qn): + if name is not None and name == qn: return report_issue(check, name) if node_type.startswith("Import"): diff --git a/bandit/core/context.py b/bandit/core/context.py index 76b50923a..8a2d4fbbc 100644 --- a/bandit/core/context.py +++ b/bandit/core/context.py @@ -193,7 +193,7 @@ def _get_literal_value(self, literal): elif isinstance(literal, ast.Tuple): return_tuple = tuple() for ti in literal.elts: - return_tuple = return_tuple + (self._get_literal_value(ti),) + return_tuple += (self._get_literal_value(ti),) literal_value = return_tuple elif isinstance(literal, ast.Set): diff --git a/bandit/core/issue.py b/bandit/core/issue.py index 875e5e418..bfa583356 100644 --- a/bandit/core/issue.py +++ b/bandit/core/issue.py @@ -30,6 +30,7 @@ class Cwe: 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" @@ -84,7 +85,7 @@ def __init__( ident=None, lineno=None, test_id="", - col_offset=0, + col_offset=-1, end_col_offset=0, ): self.severity = severity diff --git a/bandit/core/node_visitor.py b/bandit/core/node_visitor.py index 26cdb2471..938e8733b 100644 --- a/bandit/core/node_visitor.py +++ b/bandit/core/node_visitor.py @@ -19,7 +19,6 @@ def __init__( ): self.debug = debug self.nosec_lines = nosec_lines - self.seen = 0 self.scores = { "SEVERITY": [0] * len(constants.RANKING), "CONFIDENCE": [0] * len(constants.RANKING), @@ -209,7 +208,6 @@ def pre_visit(self, node): self.context["filename"] = self.fname self.context["file_data"] = self.fdata - self.seen += 1 LOG.debug( "entering: %s %s [%s]", hex(id(node)), type(node), self.depth ) @@ -286,4 +284,14 @@ def process(self, data): """ f_ast = ast.parse(data) self.generic_visit(f_ast) + # Run tests that do not require access to the AST, + # but only to the whole file source: + self.context = { + "file_data": self.fdata, + "filename": self.fname, + "lineno": 0, + "linerange": [0, 1], + "col_offset": 0, + } + self.update_scores(self.tester.run_tests(self.context, "File")) return self.scores diff --git a/bandit/core/test_properties.py b/bandit/core/test_properties.py index cf969952f..f6d4da1a7 100644 --- a/bandit/core/test_properties.py +++ b/bandit/core/test_properties.py @@ -15,7 +15,11 @@ def checks(*args): def wrapper(func): if not hasattr(func, "_checks"): func._checks = [] - func._checks.extend(utils.check_ast_node(a) for a in args) + 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) diff --git a/bandit/core/tester.py b/bandit/core/tester.py index af5ffdae9..6d41877cb 100644 --- a/bandit/core/tester.py +++ b/bandit/core/tester.py @@ -43,7 +43,7 @@ def run_tests(self, raw_context, checktype): tests = self.testset.get_tests(checktype) for test in tests: name = test.__name__ - # execute test with the an instance of the context class + # execute test with an instance of the context class temp_context = copy.copy(raw_context) context = b_context.Context(temp_context) try: @@ -66,7 +66,8 @@ def run_tests(self, raw_context, checktype): if result.lineno is None: result.lineno = temp_context["lineno"] result.linerange = temp_context["linerange"] - result.col_offset = temp_context["col_offset"] + if result.col_offset == -1: + result.col_offset = temp_context["col_offset"] result.end_col_offset = temp_context.get( "end_col_offset", 0 ) diff --git a/bandit/formatters/sarif.py b/bandit/formatters/sarif.py new file mode 100644 index 000000000..ce2f03b7b --- /dev/null +++ b/bandit/formatters/sarif.py @@ -0,0 +1,372 @@ +# 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 logging +import pathlib +import sys +import urllib.parse as urlparse +from datetime import datetime + +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.utcnow().strftime(TS_FORMAT), + execution_successful=True, + ) + ], + properties={"metrics": manager.metrics.data}, + ) + ], + ) + + run = log.runs[0] + invocation = run.invocations[0] + + skips = manager.get_skipped() + add_skipped_file_notifications(skips, invocation) + + issues = manager.get_issue_list(sev_level=sev_level, conf_level=conf_level) + + add_results(issues, run) + + serializedLog = to_json(log) + + with fileobj: + fileobj.write(serializedLog) + + if fileobj.name != sys.stdout.name: + LOG.info("SARIF output written to file: %s", fileobj.name) + + +def add_skipped_file_notifications(skips, invocation): + if skips is None or len(skips) == 0: + return + + if invocation.tool_configuration_notifications is None: + invocation.tool_configuration_notifications = [] + + for skip in skips: + (file_name, reason) = skip + + notification = om.Notification( + level="error", + message=om.Message(text=reason), + locations=[ + om.Location( + physical_location=om.PhysicalLocation( + artifact_location=om.ArtifactLocation( + uri=to_uri(file_name) + ) + ) + ) + ], + ) + + invocation.tool_configuration_notifications.append(notification) + + +def add_results(issues, run): + if run.results is None: + run.results = [] + + rules = {} + rule_indices = {} + for issue in issues: + result = create_result(issue, rules, rule_indices) + run.results.append(result) + + if len(rules) > 0: + run.tool.driver.rules = list(rules.values()) + + +def create_result(issue, rules, rule_indices): + issue_dict = issue.as_dict() + + rule, rule_index = create_or_find_rule(issue_dict, rules, rule_indices) + + physical_location = om.PhysicalLocation( + artifact_location=om.ArtifactLocation( + uri=to_uri(issue_dict["filename"]) + ) + ) + + add_region_and_context_region( + physical_location, + issue_dict["line_range"], + issue_dict["col_offset"], + issue_dict["end_col_offset"], + issue_dict["code"], + ) + + return om.Result( + rule_id=rule.id, + rule_index=rule_index, + message=om.Message(text=issue_dict["issue_text"]), + level=level_from_severity(issue_dict["issue_severity"]), + locations=[om.Location(physical_location=physical_location)], + properties={ + "issue_confidence": issue_dict["issue_confidence"], + "issue_severity": issue_dict["issue_severity"], + }, + ) + + +def level_from_severity(severity): + if severity == "HIGH": + return "error" + elif severity == "MEDIUM": + return "warning" + elif severity == "LOW": + return "note" + else: + return "warning" + + +def add_region_and_context_region( + physical_location, line_range, col_offset, end_col_offset, code +): + if code: + first_line_number, snippet_lines = parse_code(code) + snippet_line = snippet_lines[line_range[0] - first_line_number] + snippet = om.ArtifactContent(text=snippet_line) + else: + snippet = None + + physical_location.region = om.Region( + start_line=line_range[0], + end_line=line_range[1] if len(line_range) > 1 else line_range[0], + start_column=col_offset + 1, + end_column=end_col_offset + 1, + snippet=snippet, + ) + + if code: + physical_location.context_region = om.Region( + start_line=first_line_number, + end_line=first_line_number + len(snippet_lines) - 1, + snippet=om.ArtifactContent(text="".join(snippet_lines)), + ) + + +def parse_code(code): + code_lines = code.split("\n") + + # The last line from the split has nothing in it; it's an artifact of the + # last "real" line ending in a newline. Unless, of course, it doesn't: + last_line = code_lines[len(code_lines) - 1] + + last_real_line_ends_in_newline = False + if len(last_line) == 0: + code_lines.pop() + last_real_line_ends_in_newline = True + + snippet_lines = [] + first_line_number = 0 + first = True + for code_line in code_lines: + number_and_snippet_line = code_line.split(" ", 1) + if first: + first_line_number = int(number_and_snippet_line[0]) + first = False + + snippet_line = number_and_snippet_line[1] + "\n" + snippet_lines.append(snippet_line) + + if not last_real_line_ends_in_newline: + last_line = snippet_lines[len(snippet_lines) - 1] + snippet_lines[len(snippet_lines) - 1] = last_line[: len(last_line) - 1] + + return first_line_number, snippet_lines + + +def create_or_find_rule(issue_dict, rules, rule_indices): + rule_id = issue_dict["test_id"] + if rule_id in rules: + return rules[rule_id], rule_indices[rule_id] + + rule = om.ReportingDescriptor( + id=rule_id, + name=issue_dict["test_name"], + help_uri=docs_utils.get_url(rule_id), + properties={ + "tags": [ + "security", + f"external/cwe/cwe-{issue_dict['issue_cwe'].get('id')}", + ], + "precision": issue_dict["issue_confidence"].lower(), + }, + ) + + index = len(rules) + rules[rule_id] = rule + rule_indices[rule_id] = index + return rule, index + + +def to_uri(file_path): + pure_path = pathlib.PurePath(file_path) + if pure_path.is_absolute(): + return pure_path.as_uri() + else: + # Replace backslashes with slashes. + posix_path = pure_path.as_posix() + # %-encode special characters. + return urlparse.quote(posix_path) diff --git a/bandit/formatters/xml.py b/bandit/formatters/xml.py index 6e196d92f..d2b2067ff 100644 --- a/bandit/formatters/xml.py +++ b/bandit/formatters/xml.py @@ -65,7 +65,7 @@ def report(manager, fileobj, sev_level, conf_level, lines=-1): "Test ID: %s Severity: %s Confidence: %s\nCWE: %s\n%s\n" "Location %s:%s" ) - text = text % ( + text %= ( issue.test_id, issue.severity, issue.confidence, diff --git a/bandit/plugins/crypto_request_no_cert_validation.py b/bandit/plugins/crypto_request_no_cert_validation.py index 223d421ff..11791ed1e 100644 --- a/bandit/plugins/crypto_request_no_cert_validation.py +++ b/bandit/plugins/crypto_request_no_cert_validation.py @@ -54,8 +54,8 @@ @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 + 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 ( diff --git a/bandit/plugins/hashlib_insecure_functions.py b/bandit/plugins/hashlib_insecure_functions.py index 710800a72..626c8edec 100644 --- a/bandit/plugins/hashlib_insecure_functions.py +++ b/bandit/plugins/hashlib_insecure_functions.py @@ -48,8 +48,6 @@ Added check for the crypt module weak hashes """ # noqa: E501 -import sys - import bandit from bandit.core import issue from bandit.core import test_properties as test @@ -86,21 +84,6 @@ def _hashlib_func(context, func): ) -def _hashlib_new(context, func): - if func == "new": - args = context.call_args - keywords = context.call_keywords - name = args[0] if args else keywords.get("name", None) - if isinstance(name, str) and name.lower() in WEAK_HASHES: - return bandit.Issue( - severity=bandit.MEDIUM, - confidence=bandit.HIGH, - cwe=issue.Cwe.BROKEN_CRYPTO, - text=f"Use of insecure {name.upper()} hash function.", - lineno=context.node.lineno, - ) - - def _crypt_crypt(context, func): args = context.call_args keywords = context.call_keywords @@ -135,10 +118,7 @@ def hashlib(context): func = qualname_list[-1] if "hashlib" in qualname_list: - if sys.version_info >= (3, 9): - return _hashlib_func(context, func) - else: - return _hashlib_new(context, func) + return _hashlib_func(context, func) elif "crypt" in qualname_list and func in ("crypt", "mksalt"): return _crypt_crypt(context, func) diff --git a/bandit/plugins/injection_shell.py b/bandit/plugins/injection_shell.py index c138a523c..229368340 100644 --- a/bandit/plugins/injection_shell.py +++ b/bandit/plugins/injection_shell.py @@ -49,6 +49,8 @@ def gen_config(name): "popen2.Popen4", "commands.getoutput", "commands.getstatusoutput", + "subprocess.getoutput", + "subprocess.getstatusoutput", ], # Start a process with a function that is not vulnerable to shell # injection. @@ -447,6 +449,8 @@ def start_process_with_a_shell(context, config): - popen2.Popen4 - commands.getoutput - commands.getstatusoutput + - subprocess.getoutput + - subprocess.getstatusoutput :Example: @@ -679,7 +683,7 @@ def start_process_with_partial_path(context, config): ): node = context.node.args[0] # some calls take an arg list, check the first part - if isinstance(node, ast.List): + 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 diff --git a/bandit/plugins/injection_sql.py b/bandit/plugins/injection_sql.py index eed3e52ba..bd7aa92a1 100644 --- a/bandit/plugins/injection_sql.py +++ b/bandit/plugins/injection_sql.py @@ -132,9 +132,11 @@ def hardcoded_sql_expressions(context): if _check_string(statement): return bandit.Issue( severity=bandit.MEDIUM, - confidence=bandit.MEDIUM - if execute_call and not str_replace - else bandit.LOW, + confidence=( + bandit.MEDIUM + if execute_call and not str_replace + else bandit.LOW + ), cwe=issue.Cwe.SQL_INJECTION, text="Possible SQL injection vector through string-based " "query construction.", diff --git a/bandit/plugins/injection_wildcard.py b/bandit/plugins/injection_wildcard.py index 94d03b30a..46f6b5b6c 100644 --- a/bandit/plugins/injection_wildcard.py +++ b/bandit/plugins/injection_wildcard.py @@ -124,7 +124,7 @@ def linux_commands_wildcard_injection(context, config): argument_string = "" if isinstance(call_argument, list): for li in call_argument: - argument_string = argument_string + f" {li}" + argument_string += f" {li}" elif isinstance(call_argument, str): argument_string = call_argument diff --git a/bandit/plugins/pytorch_load_save.py b/bandit/plugins/pytorch_load_save.py new file mode 100644 index 000000000..77522da22 --- /dev/null +++ b/bandit/plugins/pytorch_load_save.py @@ -0,0 +1,72 @@ +# Copyright (c) 2024 Stacklok, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +r""" +========================================== +B614: Test for unsafe PyTorch load or save +========================================== + +This plugin checks for the use of `torch.load` and `torch.save`. Using +`torch.load` with untrusted data can lead to arbitrary code execution, and +improper use of `torch.save` might expose sensitive data or lead to data +corruption. A safe alternative is to use `torch.load` with the `safetensors` +library from hugingface, which provides a safe deserialization mechanism. + +:Example: + +.. code-block:: none + + >> Issue: Use of unsafe PyTorch load or save + Severity: Medium Confidence: High + CWE: CWE-94 (https://cwe.mitre.org/data/definitions/94.html) + Location: examples/pytorch_load_save.py:8 + 7 loaded_model.load_state_dict(torch.load('model_weights.pth')) + 8 another_model.load_state_dict(torch.load('model_weights.pth', + map_location='cpu')) + 9 + 10 print("Model loaded successfully!") + +.. seealso:: + + - https://cwe.mitre.org/data/definitions/94.html + - https://pytorch.org/docs/stable/generated/torch.load.html#torch.load + - https://github.com/huggingface/safetensors + +.. versionadded:: 1.7.10 + +""" +import bandit +from bandit.core import issue +from bandit.core import test_properties as test + + +@test.checks("Call") +@test.test_id("B614") +def pytorch_load_save(context): + """ + This plugin checks for the use of `torch.load` and `torch.save`. Using + `torch.load` with untrusted data can lead to arbitrary code execution, + and improper use of `torch.save` might expose sensitive data or lead + to data corruption. + """ + imported = context.is_module_imported_exact("torch") + qualname = context.call_function_name_qual + if not imported and isinstance(qualname, str): + return + + qualname_list = qualname.split(".") + func = qualname_list[-1] + if all( + [ + "torch" in qualname_list, + func in ["load", "save"], + not context.check_call_arg_value("map_location", "cpu"), + ] + ): + return bandit.Issue( + severity=bandit.MEDIUM, + confidence=bandit.HIGH, + text="Use of unsafe PyTorch load or save", + cwe=issue.Cwe.DESERIALIZATION_OF_UNTRUSTED_DATA, + lineno=context.get_lineno_for_call_arg("load"), + ) diff --git a/bandit/plugins/request_without_timeout.py b/bandit/plugins/request_without_timeout.py index a418b6cc0..c6439001b 100644 --- a/bandit/plugins/request_without_timeout.py +++ b/bandit/plugins/request_without_timeout.py @@ -4,7 +4,8 @@ B113: Test for missing requests timeout ======================================= -This plugin test checks for ``requests`` calls without a timeout specified. +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. @@ -17,7 +18,7 @@ .. code-block:: none - >> Issue: [B113:request_without_timeout] Requests call without timeout + >> 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 @@ -27,7 +28,7 @@ 4 requests.get('https://gmail.com', timeout=None) -------------------------------------------------- - >> Issue: [B113:request_without_timeout] Requests call with timeout set to 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 @@ -42,6 +43,9 @@ .. versionadded:: 1.7.5 +.. versionchanged:: 1.7.10 + Added check for httpx module + """ # noqa: E501 import bandit from bandit.core import issue @@ -51,23 +55,30 @@ @test.checks("Call") @test.test_id("B113") def request_without_timeout(context): - http_verbs = ("get", "options", "head", "post", "put", "patch", "delete") + 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: + 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="Requests call without timeout", + 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="Requests call with timeout set to None", + text=f"Call to {qualname} with timeout set to None", ) diff --git a/bandit/plugins/tarfile_unsafe_members.py b/bandit/plugins/tarfile_unsafe_members.py index 32c1e6127..5ad145c1a 100644 --- a/bandit/plugins/tarfile_unsafe_members.py +++ b/bandit/plugins/tarfile_unsafe_members.py @@ -42,6 +42,9 @@ .. versionadded:: 1.7.5 +.. versionchanged:: 1.7.8 + Added check for filter parameter + """ import ast @@ -91,6 +94,13 @@ def get_members_value(context): return {"Other": value} +def is_filter_data(context): + for keyword in context.node.keywords: + if keyword.arg == "filter": + arg = keyword.value + return isinstance(arg, ast.Str) and arg.s == "data" + + @test.test_id("B202") @test.checks("Call") def tarfile_unsafe_members(context): @@ -100,6 +110,8 @@ def tarfile_unsafe_members(context): "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: diff --git a/bandit/plugins/trojansource.py b/bandit/plugins/trojansource.py new file mode 100755 index 000000000..5c0eae5eb --- /dev/null +++ b/bandit/plugins/trojansource.py @@ -0,0 +1,77 @@ +# +# 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:: + + .. [1] https://trojansource.codes/ + .. [2] https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-42574 + +.. versionadded:: 1.7.10 + +""" # noqa: E501 +from tokenize import detect_encoding + +import bandit +from bandit.core import issue +from bandit.core import test_properties as test + + +BIDI_CHARACTERS = ( + "\u202A", + "\u202B", + "\u202C", + "\u202D", + "\u202E", + "\u2066", + "\u2067", + "\u2068", + "\u2069", + "\u200F", +) + + +@test.test_id("B613") +@test.checks("File") +def trojansource(context): + with open(context.filename, "rb") as src_file: + encoding, _ = detect_encoding(src_file.readline) + with open(context.filename, encoding=encoding) as src_file: + for lineno, line in enumerate(src_file.readlines(), start=1): + for char in BIDI_CHARACTERS: + try: + col_offset = line.index(char) + 1 + except ValueError: + continue + text = ( + "A Python source file contains bidirectional" + " control characters (%r)." % char + ) + return bandit.Issue( + severity=bandit.HIGH, + confidence=bandit.MEDIUM, + cwe=issue.Cwe.INAPPROPRIATE_ENCODING_FOR_OUTPUT_CONTEXT, + text=text, + lineno=lineno, + col_offset=col_offset, + ) diff --git a/doc/source/conf.py b/doc/source/conf.py index 869c1b3a8..f2a991c11 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 +from datetime import datetime import os import sys @@ -27,7 +28,7 @@ # General information about the project. project = "Bandit" -copyright = "2023, Bandit Developers" +copyright = f"{datetime.now():%Y}, Bandit Developers" # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True diff --git a/doc/source/faq.rst b/doc/source/faq.rst index a344f5fab..16fe25fe4 100644 --- a/doc/source/faq.rst +++ b/doc/source/faq.rst @@ -5,10 +5,10 @@ Under Which Version of Python Should I Install Bandit? ------------------------------------------------------ The answer to this question depends on the project(s) you will be running -Bandit against. If your project is only compatible with Python 3.8, you -should install Bandit to run under Python 3.8. If your project is only -compatible with Python 3.9, then use 3.9 respectively. If your project supports -both, you *could* run Bandit with both versions but you don't have to. +Bandit against. If your project is only compatible with Python 3.9, you +should install Bandit to run under Python 3.9. If your project is only +compatible with Python 3.10, then use 3.10 respectively. If your project +supports both, you *could* run Bandit with both versions but you don't have to. Bandit uses the `ast` module from Python's standard library in order to analyze your Python code. The `ast` module is only able to parse Python code diff --git a/doc/source/formatters/sarif.rst b/doc/source/formatters/sarif.rst new file mode 100644 index 000000000..58b9633a7 --- /dev/null +++ b/doc/source/formatters/sarif.rst @@ -0,0 +1,5 @@ +----- +sarif +----- + +.. automodule:: bandit.formatters.sarif diff --git a/doc/source/man/bandit.rst b/doc/source/man/bandit.rst index 46125e613..eef10d271 100644 --- a/doc/source/man/bandit.rst +++ b/doc/source/man/bandit.rst @@ -44,7 +44,7 @@ OPTIONS (-l for LOW, -ll for MEDIUM, -lll for HIGH) -i, --confidence report only issues of a given confidence level or higher (-i for LOW, -ii for MEDIUM, -iii for HIGH) - -f {csv,custom,html,json,screen,txt,xml,yaml}, --format {csv,custom,html,json,screen,txt,xml,yaml} + -f {csv,custom,html,json,sarif,screen,txt,xml,yaml}, --format {csv,custom,html,json,sarif,screen,txt,xml,yaml} specify output format --msg-template MSG_TEMPLATE specify output message template (only usable with diff --git a/doc/source/plugins/b613_trojansource.rst b/doc/source/plugins/b613_trojansource.rst new file mode 100644 index 000000000..8fa0bc47b --- /dev/null +++ b/doc/source/plugins/b613_trojansource.rst @@ -0,0 +1,5 @@ +------------------ +B613: trojansource +------------------ + +.. automodule:: bandit.plugins.trojansource diff --git a/doc/source/plugins/b614_pytorch_load_save.rst b/doc/source/plugins/b614_pytorch_load_save.rst new file mode 100644 index 000000000..dcc1ae3a0 --- /dev/null +++ b/doc/source/plugins/b614_pytorch_load_save.rst @@ -0,0 +1,5 @@ +----------------------- +B614: pytorch_load_save +----------------------- + +.. automodule:: bandit.plugins.pytorch_load_save diff --git a/doc/source/start.rst b/doc/source/start.rst index d45ca84d8..9a8e75b43 100644 --- a/doc/source/start.rst +++ b/doc/source/start.rst @@ -6,17 +6,17 @@ Installation Bandit is distributed on PyPI. The best way to install it is with pip. -Create a virtual environment (optional): +Create a virtual environment and activate it using `virtualenv` (optional): .. code-block:: console virtualenv bandit-env - python3 -m venv bandit-env - -And activate it: + source bandit-env/bin/activate +Alternatively, use `venv` instead of `virtualenv` (optional): .. code-block:: console + python3 -m venv bandit-env source bandit-env/bin/activate Install Bandit: @@ -38,6 +38,13 @@ extras: pip install bandit[baseline] +If you want to include SARIF output formatter support, install it with the +`sarif` extras: + +.. code-block:: console + + pip install bandit[sarif] + Run Bandit: .. code-block:: console diff --git a/examples/ciphers.py b/examples/ciphers.py index 7e0762d8f..af3080111 100644 --- a/examples/ciphers.py +++ b/examples/ciphers.py @@ -71,6 +71,18 @@ encryptor = cipher.encryptor() ct = encryptor.update(b"a secret message") +cipher = Cipher(algorithms.CAST5(key), mode=None, backend=default_backend()) +encryptor = cipher.encryptor() +ct = encryptor.update(b"a secret message") + cipher = Cipher(algorithms.IDEA(key), mode=None, backend=default_backend()) encryptor = cipher.encryptor() ct = encryptor.update(b"a secret message") + +cipher = Cipher(algorithms.SEED(key), mode=None, backend=default_backend()) +encryptor = cipher.encryptor() +ct = encryptor.update(b"a secret message") + +cipher = Cipher(algorithms.TripleDES(key), mode=None, backend=default_backend()) +encryptor = cipher.encryptor() +ct = encryptor.update(b"a secret message") diff --git a/examples/ftplib.py b/examples/ftplib.py index beaeb74af..6664ed001 100644 --- a/examples/ftplib.py +++ b/examples/ftplib.py @@ -1,9 +1,24 @@ from ftplib import FTP +from ftplib import FTP_TLS + +# bad ftp = FTP('ftp.debian.org') ftp.login() ftp.cwd('debian') ftp.retrlines('LIST') -ftp.quit() \ No newline at end of file +ftp.quit() + +# okay +ftp = ftplib.FTP_TLS( + "ftp.us.debian.org", + context=ssl.create_default_context(), +) +ftp.login() + +ftp.cwd("debian") +ftp.retrlines("LIST") + +ftp.quit() diff --git a/examples/pytorch_load_save.py b/examples/pytorch_load_save.py new file mode 100644 index 000000000..e1f912022 --- /dev/null +++ b/examples/pytorch_load_save.py @@ -0,0 +1,21 @@ +import torch +import torchvision.models as models + +# Example of saving a model +model = models.resnet18(pretrained=True) +torch.save(model.state_dict(), 'model_weights.pth') + +# Example of loading the model weights in an insecure way +loaded_model = models.resnet18() +loaded_model.load_state_dict(torch.load('model_weights.pth')) + +# Save the model +torch.save(loaded_model.state_dict(), 'model_weights.pth') + +# Another example using torch.load with more parameters +another_model = models.resnet18() +another_model.load_state_dict(torch.load('model_weights.pth', map_location='cpu')) + +# Save the model +torch.save(another_model.state_dict(), 'model_weights.pth') + diff --git a/examples/requests-missing-timeout.py b/examples/requests-missing-timeout.py index 38f24440a..fa71c4b0e 100644 --- a/examples/requests-missing-timeout.py +++ b/examples/requests-missing-timeout.py @@ -1,27 +1,68 @@ +import httpx import requests import not_requests +# Errors requests.get('https://gmail.com') requests.get('https://gmail.com', timeout=None) -requests.get('https://gmail.com', timeout=5) requests.post('https://gmail.com') requests.post('https://gmail.com', timeout=None) -requests.post('https://gmail.com', timeout=5) requests.put('https://gmail.com') requests.put('https://gmail.com', timeout=None) -requests.put('https://gmail.com', timeout=5) requests.delete('https://gmail.com') requests.delete('https://gmail.com', timeout=None) -requests.delete('https://gmail.com', timeout=5) requests.patch('https://gmail.com') requests.patch('https://gmail.com', timeout=None) -requests.patch('https://gmail.com', timeout=5) requests.options('https://gmail.com') requests.options('https://gmail.com', timeout=None) -requests.options('https://gmail.com', timeout=5) requests.head('https://gmail.com') requests.head('https://gmail.com', timeout=None) -requests.head('https://gmail.com', timeout=5) +httpx.get('https://gmail.com') +httpx.get('https://gmail.com', timeout=None) +httpx.post('https://gmail.com') +httpx.post('https://gmail.com', timeout=None) +httpx.put('https://gmail.com') +httpx.put('https://gmail.com', timeout=None) +httpx.delete('https://gmail.com') +httpx.delete('https://gmail.com', timeout=None) +httpx.patch('https://gmail.com') +httpx.patch('https://gmail.com', timeout=None) +httpx.options('https://gmail.com') +httpx.options('https://gmail.com', timeout=None) +httpx.head('https://gmail.com') +httpx.head('https://gmail.com', timeout=None) +httpx.Client() +httpx.Client(timeout=None) +httpx.AsyncClient() +httpx.AsyncClient(timeout=None) +with httpx.Client() as client: + client.get('https://gmail.com') +with httpx.Client(timeout=None) as client: + client.get('https://gmail.com') +async with httpx.AsyncClient() as client: + await client.get('https://gmail.com') +async with httpx.AsyncClient(timeout=None) as client: + await client.get('https://gmail.com') # Okay not_requests.get('https://gmail.com') +requests.get('https://gmail.com', timeout=5) +requests.post('https://gmail.com', timeout=5) +requests.put('https://gmail.com', timeout=5) +requests.delete('https://gmail.com', timeout=5) +requests.patch('https://gmail.com', timeout=5) +requests.options('https://gmail.com', timeout=5) +requests.head('https://gmail.com', timeout=5) +httpx.get('https://gmail.com', timeout=5) +httpx.post('https://gmail.com', timeout=5) +httpx.put('https://gmail.com', timeout=5) +httpx.delete('https://gmail.com', timeout=5) +httpx.patch('https://gmail.com', timeout=5) +httpx.options('https://gmail.com', timeout=5) +httpx.head('https://gmail.com', timeout=5) +httpx.Client(timeout=5) +httpx.AsyncClient(timeout=5) +with httpx.Client(timeout=5) as client: + client.get('https://gmail.com') +async with httpx.AsyncClient(timeout=5) as client: + await client.get('https://gmail.com') diff --git a/examples/requests-ssl-verify-disabled.py b/examples/requests-ssl-verify-disabled.py index 25f5ef41f..c45b9e944 100644 --- a/examples/requests-ssl-verify-disabled.py +++ b/examples/requests-ssl-verify-disabled.py @@ -1,6 +1,7 @@ import httpx import requests +# Errors requests.get('https://gmail.com', timeout=30, verify=True) requests.get('https://gmail.com', timeout=30, verify=False) requests.post('https://gmail.com', timeout=30, verify=True) @@ -16,25 +17,26 @@ requests.head('https://gmail.com', timeout=30, verify=True) requests.head('https://gmail.com', timeout=30, verify=False) -httpx.request('GET', 'https://gmail.com', verify=True) -httpx.request('GET', 'https://gmail.com', verify=False) -httpx.get('https://gmail.com', verify=True) -httpx.get('https://gmail.com', verify=False) -httpx.options('https://gmail.com', verify=True) -httpx.options('https://gmail.com', verify=False) -httpx.head('https://gmail.com', verify=True) -httpx.head('https://gmail.com', verify=False) -httpx.post('https://gmail.com', verify=True) -httpx.post('https://gmail.com', verify=False) -httpx.put('https://gmail.com', verify=True) -httpx.put('https://gmail.com', verify=False) -httpx.patch('https://gmail.com', verify=True) -httpx.patch('https://gmail.com', verify=False) -httpx.delete('https://gmail.com', verify=True) -httpx.delete('https://gmail.com', verify=False) -httpx.stream('https://gmail.com', verify=True) -httpx.stream('https://gmail.com', verify=False) -httpx.Client() -httpx.Client(verify=False) -httpx.AsyncClient() -httpx.AsyncClient(verify=False) +# Okay +httpx.request('GET', 'https://gmail.com', timeout=30, verify=True) +httpx.request('GET', 'https://gmail.com', timeout=30, verify=False) +httpx.get('https://gmail.com', timeout=30, verify=True) +httpx.get('https://gmail.com', timeout=30, verify=False) +httpx.options('https://gmail.com', timeout=30, verify=True) +httpx.options('https://gmail.com', timeout=30, verify=False) +httpx.head('https://gmail.com', timeout=30, verify=True) +httpx.head('https://gmail.com', timeout=30, verify=False) +httpx.post('https://gmail.com', timeout=30, verify=True) +httpx.post('https://gmail.com', timeout=30, verify=False) +httpx.put('https://gmail.com', timeout=30, verify=True) +httpx.put('https://gmail.com', timeout=30, verify=False) +httpx.patch('https://gmail.com', timeout=30, verify=True) +httpx.patch('https://gmail.com', timeout=30, verify=False) +httpx.delete('https://gmail.com', timeout=30, verify=True) +httpx.delete('https://gmail.com', timeout=30, verify=False) +httpx.stream('https://gmail.com', timeout=30, verify=True) +httpx.stream('https://gmail.com', timeout=30, verify=False) +httpx.Client(timeout=30) +httpx.Client(timeout=30, verify=False) +httpx.AsyncClient(timeout=30) +httpx.AsyncClient(timeout=30, verify=False) diff --git a/examples/subprocess_shell.py b/examples/subprocess_shell.py index 049ed05bc..38944d5fa 100644 --- a/examples/subprocess_shell.py +++ b/examples/subprocess_shell.py @@ -25,6 +25,10 @@ def __len__(self): subprocess.check_output(['/bin/ls', '-l']) subprocess.check_output('/bin/ls -l', shell=True) +subprocess.check_output([], stdout=None) + +subprocess.getoutput('/bin/ls -l') +subprocess.getstatusoutput('/bin/ls -l') subprocess.run(['/bin/ls', '-l']) subprocess.run('/bin/ls -l', shell=True) diff --git a/examples/tarfile_extractall.py b/examples/tarfile_extractall.py index 2af3eb544..b32736afb 100644 --- a/examples/tarfile_extractall.py +++ b/examples/tarfile_extractall.py @@ -15,6 +15,18 @@ def managed_members_archive_handler(filename): tar.close() +def filter_data_archive_handler(filename): + tar = tarfile.open(filename) + tar.extractall(path=tempfile.mkdtemp(), filter="data") + tar.close() + + +def filter_fully_trusted_archive_handler(filename): + tar = tarfile.open(filename) + tar.extractall(path=tempfile.mkdtemp(), filter="fully_trusted") + tar.close() + + def list_members_archive_handler(filename): tar = tarfile.open(filename) tar.extractall(path=tempfile.mkdtemp(), members=[]) @@ -45,3 +57,5 @@ def members_filter(tarfile): filename = sys.argv[1] unsafe_archive_handler(filename) managed_members_archive_handler(filename) + filter_data_archive_handler(filename) + filter_fully_trusted_archive_handler(filename) diff --git a/examples/trojansource.py b/examples/trojansource.py new file mode 100644 index 000000000..40c605579 --- /dev/null +++ b/examples/trojansource.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 +# cf. https://trojansource.codes/ & https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-42574 +access_level = "user" +if access_level != 'none‮⁦': # Check if admin ⁩⁦' and access_level != 'user + print("You are an admin.\n") diff --git a/examples/trojansource_latin1.py b/examples/trojansource_latin1.py new file mode 100644 index 000000000..dee24e07c --- /dev/null +++ b/examples/trojansource_latin1.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 +# -*- coding: latin-1 -*- +# cf. https://trojansource.codes & https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-42574 +# Some special characters: +access_level = "user" +if access_level != 'none??': # Check if admin ??' and access_level != 'user + print("You are an admin.\n") diff --git a/examples/xml_lxml.py b/examples/xml_lxml.py deleted file mode 100644 index dd12e5384..000000000 --- a/examples/xml_lxml.py +++ /dev/null @@ -1,9 +0,0 @@ -import lxml.etree -import lxml -from lxml import etree -from defusedxml.lxml import fromstring -from defuxedxml import lxml as potatoe - -xmlString = "\nTove\nJani\nReminder\nDon't forget me this weekend!\n" -root = lxml.etree.fromstring(xmlString) -root = fromstring(xmlString) diff --git a/funding.json b/funding.json new file mode 100644 index 000000000..a7831ac3a --- /dev/null +++ b/funding.json @@ -0,0 +1,65 @@ +{ + "version": "v1.0.0", + "entity": { + "type": "individual", + "role": "maintainer", + "name": "Eric Brown", + "email": "eric_wade_brown@yahoo.com", + "phone": "", + "description": "I’m passionate about developing tools that empower engineers to produce secure, hardened code, reducing vulnerabilities and strengthening software integrity. With a focus on security automation, I aim to make secure coding practices more accessible and integrated into development workflows.", + "webpageUrl": { + "url": "https://github.com" + } + }, + "projects": [{ + "guid": "bandit", + "name": "Bandit", + "description": " Bandit is a tool designed to find common security issues in Python code.", + "webpageUrl": { + "url": "https://github.com/PyCQA/bandit" + }, + "repositoryUrl": { + "url": "https://github.com/PyCQA/bandit" + }, + "licenses": ["spdx:Apache-2.0"], + "tags": ["python", "static-code-analysis", "security", "security-tools"] + }], + "funding": { + "channels": [ + { + "guid": "github", + "type": "payment-provider", + "address": "https://github.com/sponsors/ericwb", + "description": "Pay with your credit card through this gateway and setup recurring subscriptions." + }, + { + "guid": "psf", + "type": "payment-provider", + "address": "https://psfmember.org/civicrm/contribute/transact/?reset=1&id=42", + "description": "Pay with your credit card through this gateway and setup recurring subscriptions." + } + ], + "plans": [ + { + "guid": "developer-time", + "status": "active", + "name": "Developer compensation", + "description": "This will cover the cost of one developer working part-time on the projects.", + "amount": 1000, + "currency": "USD", + "frequency": "monthly", + "channels": ["github", "psf"] + }, + { + "guid": "angel-plan", + "status": "active", + "name": "Goodwill plan", + "description": "Pay anything you wish to show your goodwill for the project.", + "amount": 0, + "currency": "USD", + "frequency": "one-time", + "channels": ["psf"] + } + ] + } +} diff --git a/logo/logo.svg b/logo/logo.svg index e373123fa..0204d32b4 100644 --- a/logo/logo.svg +++ b/logo/logo.svg @@ -1,87 +1,51 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + diff --git a/logo/logomark-singlecolor.png b/logo/logomark-singlecolor.png index 6f5d2edb6..00c931417 100644 Binary files a/logo/logomark-singlecolor.png and b/logo/logomark-singlecolor.png differ diff --git a/logo/logomark.png b/logo/logomark.png index 64da8fe62..8a6b1be83 100644 Binary files a/logo/logomark.png and b/logo/logomark.png differ diff --git a/logo/logotype-singlecolor.png b/logo/logotype-singlecolor.png index 371166cfe..3276c68ad 100644 Binary files a/logo/logotype-singlecolor.png and b/logo/logotype-singlecolor.png differ diff --git a/logo/logotype-sm.png b/logo/logotype-sm.png index 8ceacc3c5..85f1d2273 100644 Binary files a/logo/logotype-sm.png and b/logo/logotype-sm.png differ diff --git a/logo/logotype.png b/logo/logotype.png index 2cc6ccfe4..3276c68ad 100644 Binary files a/logo/logotype.png and b/logo/logotype.png differ diff --git a/setup.cfg b/setup.cfg index 54d4096a2..e7868e5b4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,17 +18,20 @@ classifier = Operating System :: MacOS :: MacOS X Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3.13 Programming Language :: Python :: 3 :: Only Topic :: Security project_urls = + Documentation = https://bandit.readthedocs.io/ Release Notes = https://github.com/PyCQA/bandit/releases Source Code = https://github.com/PyCQA/bandit Issue Tracker = https://github.com/PyCQA/bandit/issues + Discord = https://discord.gg/qYxpadCgkx + Sponsor = https://psfmember.org/civicrm/contribute/transact/?reset=1&id=42 [extras] yaml = @@ -37,6 +40,9 @@ toml = tomli>=1.1.0; python_version < "3.11" baseline = GitPython>=3.1.30 +sarif = + sarif-om>=1.0.4 + jschema-to-python>=1.2.3 [entry_points] console_scripts = @@ -52,6 +58,7 @@ bandit.formatters = txt = bandit.formatters.text:report xml = bandit.formatters.xml:report html = bandit.formatters.html:report + sarif = bandit.formatters.sarif:report screen = bandit.formatters.screen:report yaml = bandit.formatters.yaml:report custom = bandit.formatters.custom:report @@ -148,6 +155,12 @@ bandit.plugins = #bandit/plugins/tarfile_unsafe_members.py tarfile_unsafe_members = bandit.plugins.tarfile_unsafe_members:tarfile_unsafe_members + #bandit/plugins/pytorch_load_save.py + pytorch_load_save = bandit.plugins.pytorch_load_save:pytorch_load_save + + # bandit/plugins/trojansource.py + trojansource = bandit.plugins.trojansource:trojansource + [build_sphinx] all_files = 1 build-dir = doc/build diff --git a/setup.py b/setup.py index 8400e38ec..4930fc245 100644 --- a/setup.py +++ b/setup.py @@ -4,5 +4,5 @@ import setuptools setuptools.setup( - python_requires=">=3.8", setup_requires=["pbr>=2.0.0"], pbr=True + python_requires=">=3.9", setup_requires=["pbr>=2.0.0"], pbr=True ) diff --git a/tests/functional/test_baseline.py b/tests/functional/test_baseline.py index 8962c2366..d3df4f539 100644 --- a/tests/functional/test_baseline.py +++ b/tests/functional/test_baseline.py @@ -26,7 +26,6 @@ class BaselineFunctionalTests(testtools.TestCase): - """Functional tests for Bandit baseline. This set of tests is used to verify that the baseline comparison handles diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py index a230dc30b..f9943f708 100644 --- a/tests/functional/test_functional.py +++ b/tests/functional/test_functional.py @@ -3,7 +3,6 @@ # # SPDX-License-Identifier: Apache-2.0 import os -import sys import testtools @@ -15,7 +14,6 @@ class FunctionalTests(testtools.TestCase): - """Functional tests for bandit test plugins. This set of tests runs bandit against each example file in turn @@ -108,43 +106,17 @@ def test_binding(self): def test_crypto_md5(self): """Test the `hashlib.md5` example.""" - if sys.version_info >= (3, 9): - expect = { - "SEVERITY": { - "UNDEFINED": 0, - "LOW": 0, - "MEDIUM": 16, - "HIGH": 9, - }, - "CONFIDENCE": { - "UNDEFINED": 0, - "LOW": 0, - "MEDIUM": 0, - "HIGH": 25, - }, - } - else: - expect = { - "SEVERITY": { - "UNDEFINED": 0, - "LOW": 0, - "MEDIUM": 22, - "HIGH": 4, - }, - "CONFIDENCE": { - "UNDEFINED": 0, - "LOW": 0, - "MEDIUM": 0, - "HIGH": 26, - }, - } + expect = { + "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 16, "HIGH": 9}, + "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 25}, + } self.check_example("crypto-md5.py", expect) def test_ciphers(self): """Test the `Crypto.Cipher` example.""" expect = { - "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 21}, - "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 22}, + "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 24}, + "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 25}, } self.check_example("ciphers.py", expect) @@ -198,26 +170,10 @@ def test_hardcoded_tmp(self): def test_imports_aliases(self): """Test the `import X as Y` syntax.""" - if sys.version_info >= (3, 9): - expect = { - "SEVERITY": {"UNDEFINED": 0, "LOW": 4, "MEDIUM": 1, "HIGH": 4}, - "CONFIDENCE": { - "UNDEFINED": 0, - "LOW": 0, - "MEDIUM": 0, - "HIGH": 9, - }, - } - else: - expect = { - "SEVERITY": {"UNDEFINED": 0, "LOW": 4, "MEDIUM": 5, "HIGH": 0}, - "CONFIDENCE": { - "UNDEFINED": 0, - "LOW": 0, - "MEDIUM": 0, - "HIGH": 9, - }, - } + expect = { + "SEVERITY": {"UNDEFINED": 0, "LOW": 4, "MEDIUM": 1, "HIGH": 4}, + "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 9}, + } self.check_example("imports-aliases.py", expect) def test_imports_from(self): @@ -247,8 +203,8 @@ def test_telnet_usage(self): def test_ftp_usage(self): """Test for `import ftplib` and FTP.* calls.""" expect = { - "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 2}, - "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 2}, + "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 3}, + "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 3}, } self.check_example("ftplib.py", expect) @@ -412,8 +368,8 @@ def test_requests_ssl_verify_disabled(self): def test_requests_without_timeout(self): """Test for the `requests` library missing timeouts.""" expect = { - "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 14, "HIGH": 0}, - "CONFIDENCE": {"UNDEFINED": 0, "LOW": 14, "MEDIUM": 0, "HIGH": 0}, + "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 25, "HIGH": 0}, + "CONFIDENCE": {"UNDEFINED": 0, "LOW": 25, "MEDIUM": 0, "HIGH": 0}, } self.check_example("requests-missing-timeout.py", expect) @@ -493,8 +449,8 @@ def test_ssl_insecure_version(self): def test_subprocess_shell(self): """Test for `subprocess.Popen` with `shell=True`.""" expect = { - "SEVERITY": {"UNDEFINED": 0, "LOW": 21, "MEDIUM": 1, "HIGH": 11}, - "CONFIDENCE": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 0, "HIGH": 32}, + "SEVERITY": {"UNDEFINED": 0, "LOW": 24, "MEDIUM": 1, "HIGH": 11}, + "CONFIDENCE": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 0, "HIGH": 35}, } self.check_example("subprocess_shell.py", expect) @@ -600,12 +556,6 @@ def test_xml(self): } self.check_example("xml_expatbuilder.py", expect) - expect = { - "SEVERITY": {"UNDEFINED": 0, "LOW": 3, "MEDIUM": 1, "HIGH": 0}, - "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 4}, - } - self.check_example("xml_lxml.py", expect) - expect = { "SEVERITY": {"UNDEFINED": 0, "LOW": 2, "MEDIUM": 2, "HIGH": 0}, "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 4}, @@ -856,36 +806,10 @@ def test_unverified_context(self): def test_hashlib_new_insecure_functions(self): """Test insecure hash functions created by `hashlib.new`.""" - if sys.version_info >= (3, 9): - expect = { - "SEVERITY": { - "UNDEFINED": 0, - "LOW": 0, - "MEDIUM": 0, - "HIGH": 9, - }, - "CONFIDENCE": { - "UNDEFINED": 0, - "LOW": 0, - "MEDIUM": 0, - "HIGH": 9, - }, - } - else: - expect = { - "SEVERITY": { - "UNDEFINED": 0, - "LOW": 0, - "MEDIUM": 10, - "HIGH": 0, - }, - "CONFIDENCE": { - "UNDEFINED": 0, - "LOW": 0, - "MEDIUM": 0, - "HIGH": 10, - }, - } + expect = { + "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 9}, + "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 9}, + } self.check_example("hashlib_new_insecure_functions.py", expect) def test_blacklist_pycrypto(self): @@ -926,7 +850,29 @@ def test_snmp_security_check(self): def test_tarfile_unsafe_members(self): """Test insecure usage of tarfile.""" expect = { - "SEVERITY": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 2, "HIGH": 1}, - "CONFIDENCE": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 2, "HIGH": 1}, + "SEVERITY": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 2, "HIGH": 2}, + "CONFIDENCE": {"UNDEFINED": 0, "LOW": 1, "MEDIUM": 2, "HIGH": 2}, } self.check_example("tarfile_extractall.py", expect) + + def test_pytorch_load_save(self): + """Test insecure usage of torch.load and torch.save.""" + expect = { + "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 4, "HIGH": 0}, + "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 4}, + } + self.check_example("pytorch_load_save.py", expect) + + def test_trojansource(self): + expect = { + "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 1}, + "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 1, "HIGH": 0}, + } + self.check_example("trojansource.py", expect) + + def test_trojansource_latin1(self): + expect = { + "SEVERITY": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 0}, + "CONFIDENCE": {"UNDEFINED": 0, "LOW": 0, "MEDIUM": 0, "HIGH": 0}, + } + self.check_example("trojansource_latin1.py", expect) diff --git a/tests/unit/formatters/test_sarif.py b/tests/unit/formatters/test_sarif.py new file mode 100644 index 000000000..a5306fa81 --- /dev/null +++ b/tests/unit/formatters/test_sarif.py @@ -0,0 +1,139 @@ +# SPDX-License-Identifier: Apache-2.0 +import collections +import json +import tempfile +from unittest import mock + +import testtools + +import bandit +from bandit.core import config +from bandit.core import constants +from bandit.core import issue +from bandit.core import manager +from bandit.core import metrics +from bandit.formatters import sarif + + +class SarifFormatterTests(testtools.TestCase): + def setUp(self): + super().setUp() + conf = config.BanditConfig() + self.manager = manager.BanditManager(conf, "file") + (tmp_fd, self.tmp_fname) = tempfile.mkstemp() + self.context = { + "filename": self.tmp_fname, + "lineno": 4, + "linerange": [4], + "code": ( + "import socket\n\n" + "s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n" + "s.bind(('0.0.0.0', 31137))" + ), + } + self.check_name = "hardcoded_bind_all_interfaces" + self.issue = issue.Issue( + severity=bandit.MEDIUM, + cwe=issue.Cwe.MULTIPLE_BINDS, + confidence=bandit.MEDIUM, + text="Possible binding to all interfaces.", + test_id="B104", + ) + + self.candidates = [ + issue.Issue( + issue.Cwe.MULTIPLE_BINDS, + bandit.LOW, + bandit.LOW, + "Candidate A", + lineno=1, + ), + issue.Issue( + bandit.HIGH, + issue.Cwe.MULTIPLE_BINDS, + bandit.HIGH, + "Candiate B", + lineno=2, + ), + ] + + self.manager.out_file = self.tmp_fname + + self.issue.fname = self.context["filename"] + self.issue.lineno = self.context["lineno"] + self.issue.linerange = self.context["linerange"] + self.issue.code = self.context["code"] + self.issue.test = self.check_name + + self.manager.results.append(self.issue) + self.manager.metrics = metrics.Metrics() + + # mock up the metrics + for key in ["_totals", "binding.py"]: + self.manager.metrics.data[key] = {"loc": 4, "nosec": 2} + for criteria, default in constants.CRITERIA: + for rank in constants.RANKING: + self.manager.metrics.data[key][f"{criteria}.{rank}"] = 0 + + @mock.patch("bandit.core.manager.BanditManager.get_issue_list") + def test_report(self, get_issue_list): + self.manager.files_list = ["binding.py"] + self.manager.scores = [ + { + "SEVERITY": [0] * len(constants.RANKING), + "CONFIDENCE": [0] * len(constants.RANKING), + } + ] + + get_issue_list.return_value = collections.OrderedDict( + [(self.issue, self.candidates)] + ) + + with open(self.tmp_fname, "w") as tmp_file: + sarif.report( + self.manager, + tmp_file, + self.issue.severity, + self.issue.confidence, + ) + + with open(self.tmp_fname) as f: + data = json.loads(f.read()) + run = data["runs"][0] + self.assertEqual(sarif.SCHEMA_URI, data["$schema"]) + self.assertEqual(sarif.SCHEMA_VER, data["version"]) + driver = run["tool"]["driver"] + self.assertEqual("Bandit", driver["name"]) + self.assertEqual(bandit.__author__, driver["organization"]) + self.assertEqual(bandit.__version__, driver["semanticVersion"]) + self.assertEqual("B104", driver["rules"][0]["id"]) + self.assertEqual(self.check_name, driver["rules"][0]["name"]) + self.assertIn("security", driver["rules"][0]["properties"]["tags"]) + self.assertIn( + "external/cwe/cwe-605", + driver["rules"][0]["properties"]["tags"], + ) + self.assertEqual( + "medium", driver["rules"][0]["properties"]["precision"] + ) + invocation = run["invocations"][0] + self.assertTrue(invocation["executionSuccessful"]) + self.assertIsNotNone(invocation["endTimeUtc"]) + result = run["results"][0] + # If the level is "warning" like in this case, SARIF will remove + # from output, as "warning" is the default value. + self.assertIsNone(result.get("level")) + self.assertEqual(self.issue.text, result["message"]["text"]) + physicalLocation = result["locations"][0]["physicalLocation"] + self.assertEqual( + self.context["linerange"][0], + physicalLocation["region"]["startLine"], + ) + self.assertEqual( + self.context["linerange"][0], + physicalLocation["region"]["endLine"], + ) + self.assertIn( + self.tmp_fname, + physicalLocation["artifactLocation"]["uri"], + ) diff --git a/tox.ini b/tox.ini index 27b3d75e7..b52833d40 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.2.0 -envlist = py38,pep8 +envlist = py39,pep8 [testenv] usedevelop = True @@ -14,6 +14,7 @@ extras = yaml toml baseline + sarif commands = find bandit -type f -name "*.pyc" -delete stestr run {posargs} @@ -27,9 +28,6 @@ passenv = no_proxy NO_PROXY -[testenv:debug] -commands = oslo_debug_helper -t tests {posargs} - [testenv:linters] deps = {[testenv:pep8]deps} usedevelop = False @@ -86,20 +84,3 @@ deps = pre-commit commands = pre-commit run --all-files --show-diff-on-failure - -[testenv:lower-constraints] -basepython = python3 -deps = - -c{toxinidir}/lower-constraints.txt - -r{toxinidir}/test-requirements.txt - -r{toxinidir}/requirements.txt - -# This environment can be used to quickly validate that all needed system -# packages required to successfully execute test targets are installed -[testenv:bindep] -# Do not install any requirements. We want this to be fast and work even if -# system dependencies are missing, since it's used to tell you what system -# dependencies are missing! This also means that bindep must be installed -# separately, outside of the requirements files. -deps = bindep -commands = bindep test