diff --git a/bandit/plugins/logging_sensitive_info.py b/bandit/plugins/logging_sensitive_info.py new file mode 100644 index 000000000..d9cd9f88b --- /dev/null +++ b/bandit/plugins/logging_sensitive_info.py @@ -0,0 +1,182 @@ +# +# Copyright 2026 RaccoonLabs +# SPDX-License-Identifier: Apache-2.0 +""" +Bandit plugin to detect logging/printing of sensitive information. + +Flags calls to logging.*, print(), pprint.*, and f-string output where +the arguments contain variable names associated with secrets: +password, secret, token, api_key, private_key, credential, auth_token, etc. +""" +import ast +import re + +import bandit +from bandit.core import issue +from bandit.core import test_properties as test + +# Sensitive variable name patterns +RE_SENSITIVE = re.compile( + r"(pas+wo?r?d|pass(phrase)?|pwd|secret|token|api_key|apikey|" + r"private_key|privatekey|access_key|accesskey|secret_key|secretkey|" + r"credential|auth_token|auth_token|bearer|api_secret|client_secret|" + r"database_url|db_password|db_pass|encryption_key|signing_key)", + re.IGNORECASE, +) + +# Logging modules and functions to check +LOGGING_MODULES = {"logging", "logger"} +PRINT_FUNCTIONS = { + "print", + "pprint", + "pprint.pprint", + "debug", + "info", + "warning", + "warn", + "error", + "critical", + "exception", + "log", +} +LOGGING_METHODS = { + "debug", + "info", + "warning", + "warn", + "error", + "critical", + "exception", + "log", + "fatal", +} + + +def _is_sensitive_name(name: str) -> bool: + """Check if a variable name looks like it holds sensitive data.""" + return bool(RE_SENSITIVE.search(name)) + + +def _check_node_for_sensitive(node) -> list: + """Recursively check an AST node for references to sensitive variable names.""" + found = [] + + if isinstance(node, ast.Name): + if _is_sensitive_name(node.id): + found.append(node.id) + elif isinstance(node, ast.Attribute): + if _is_sensitive_name(node.attr): + found.append(node.attr) + elif isinstance(node, ast.Subscript): + found.extend(_check_node_for_sensitive(node.value)) + elif isinstance(node, ast.FormattedValue): + found.extend(_check_node_for_sensitive(node.value)) + elif isinstance(node, ast.JoinedStr): + for value in node.values: + found.extend(_check_node_for_sensitive(value)) + elif isinstance(node, ast.Call): + # Check keyring.get_password() and similar + if isinstance(node.func, ast.Attribute): + if node.func.attr in ( + "get_password", + "get_credential", + "get_secret", + ): + found.append(node.func.attr) + elif isinstance(node, ast.BinOp): + # f-string style: "Password: " + password + found.extend(_check_node_for_sensitive(node.left)) + found.extend(_check_node_for_sensitive(node.right)) + elif isinstance(node, (ast.Tuple, ast.List)): + for elt in node.elts: + found.extend(_check_node_for_sensitive(elt)) + + return found + + +def _is_logging_call(node: ast.Call) -> bool: + """Check if a Call node is a logging or print call.""" + func = node.func + + # print(), pprint() + if isinstance(func, ast.Name) and func.id in PRINT_FUNCTIONS: + return True + + # logging.debug(), logger.info(), etc. + if isinstance(func, ast.Attribute): + if func.attr in LOGGING_METHODS: + # Check if the object is a logger + if isinstance(func.value, ast.Name): + # Could be any logger instance or the logging module + return True + if isinstance(func.value, ast.Attribute): + return True + + return False + + +@test.checks("Call") +@test.test_id("B622") +def logging_sensitive_info(context): + """**B622: Test for logging of sensitive information** + + This plugin detects when potentially sensitive information is passed + to logging or print calls. Sensitive variable names include: + password, secret, token, api_key, private_key, credential, etc. + + **Config Options:** + + None + + :Example: + + .. code-block:: none + + >> Issue: [B622] Possible sensitive information logged: 'password' + Severity: Medium Confidence: Medium + CWE: CWE-532 (https://cwe.mitre.org/data/definitions/532.html) + Location: ./examples/sensitive_logging.py:5 + 4 def login(user, password): + 5 logging.debug("Password: %s", password) + + .. seealso:: + + - https://cwe.mitre.org/data/definitions/532.html + - https://owasp.org/www-community/vulnerabilities/Information_exposure_through_query_parameters_in_url + + .. versionadded:: 1.9.0 + """ + node = context.node + + if not isinstance(node, ast.Call): + return None + + if not _is_logging_call(node): + return None + + # Check all arguments for sensitive variable references + sensitive_found = [] + for arg in node.args: + sensitive_found.extend(_check_node_for_sensitive(arg)) + + for kw in node.keywords: + if kw.arg and _is_sensitive_name(kw.arg): + sensitive_found.append(kw.arg) + sensitive_found.extend(_check_node_for_sensitive(kw.value)) + + if not sensitive_found: + return None + + # Deduplicate + unique_sensitive = list(dict.fromkeys(sensitive_found)) + + return bandit.Issue( + severity=bandit.MEDIUM, + confidence=bandit.MEDIUM, + cwe=issue.Cwe.CLEARTEXT_TRANSMISSION, + text=( + f"Possible sensitive information in logging/print call: " + f"'{', '.join(unique_sensitive)}'. " + f"Avoid logging passwords, tokens, API keys, or other secrets." + ), + ) diff --git a/examples/sensitive_logging.py b/examples/sensitive_logging.py new file mode 100644 index 000000000..2025ad258 --- /dev/null +++ b/examples/sensitive_logging.py @@ -0,0 +1,47 @@ +# Test cases for B622: logging of sensitive information + +import logging + +# B622: logging password +logging.debug("Password: %s", password) + +# B622: logging secret +logging.info("Secret is %s", api_secret) + +# B622: print token +print(f"Token: {auth_token}") + +# B622: logging with keyword arg +logging.warning("Credentials: %s", credentials=credentials) + +# B622: print api_key +print("The API key is", api_key) + +# B622: logging private_key +logger.error("Key: %s", private_key) + +# B622: logging access_key +logging.info("Access: %s" % access_key) + +# No issue - safe logging +logging.debug("User logged in: %s", username) +print("Hello world") +logging.info("Request completed in %s seconds", elapsed) + +# No issue - safe variable names +logging.debug("Count: %s", total_count) +print(f"Name: {user_name}") + +# B622: pprint sensitive info +import pprint +pprint.pprint({"token": token}) + +# B622: logging database URL with password +logging.debug("DB URL: %s", database_url) + +# B622: f-string with sensitive +print(f"Connecting with {db_password}") + +# B622: keyring retrieval then logged +password = keyring.get_password("service", "user") +logging.debug("Got password: %s", password) diff --git a/setup.cfg b/setup.cfg index fe4c74746..14ec07a1d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -87,6 +87,7 @@ bandit.plugins = hardcoded_password_string = bandit.plugins.general_hardcoded_password:hardcoded_password_string hardcoded_password_funcarg = bandit.plugins.general_hardcoded_password:hardcoded_password_funcarg hardcoded_password_default = bandit.plugins.general_hardcoded_password:hardcoded_password_default + logging_sensitive_info = bandit.plugins.logging_sensitive_info:logging_sensitive_info # bandit/plugins/general_hardcoded_tmp.py hardcoded_tmp_directory = bandit.plugins.general_hardcoded_tmp:hardcoded_tmp_directory