diff --git a/bandit/core/blacklisting.py b/bandit/core/blacklisting.py index 2bbb093d5..8d8ed02ca 100644 --- a/bandit/core/blacklisting.py +++ b/bandit/core/blacklisting.py @@ -35,8 +35,8 @@ def blacklist(context, config): func = context.node.func if isinstance(func, ast.Name) and func.id == "__import__": if len(context.node.args): - if isinstance(context.node.args[0], ast.Str): - name = context.node.args[0].s + if isinstance(context.node.args[0], ast.Constant): + name = context.node.args[0].value else: # TODO(??): import through a variable, need symbol tab name = "UNKNOWN" diff --git a/bandit/core/context.py b/bandit/core/context.py index 8a2d4fbbc..67a26e7fd 100644 --- a/bandit/core/context.py +++ b/bandit/core/context.py @@ -178,11 +178,13 @@ def _get_literal_value(self, literal): :param literal: The AST literal to convert :return: The value of the AST literal """ - if isinstance(literal, ast.Num): - literal_value = literal.n - - elif isinstance(literal, ast.Str): - literal_value = literal.s + 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() @@ -205,19 +207,9 @@ def _get_literal_value(self, literal): elif isinstance(literal, ast.Dict): literal_value = dict(zip(literal.keys, literal.values)) - elif isinstance(literal, ast.Ellipsis): - # what do we want to do with this? - literal_value = None - elif isinstance(literal, ast.Name): literal_value = literal.id - elif isinstance(literal, ast.NameConstant): - literal_value = str(literal.value) - - elif isinstance(literal, ast.Bytes): - literal_value = literal.s - else: literal_value = None diff --git a/bandit/core/node_visitor.py b/bandit/core/node_visitor.py index 938e8733b..3696e49bc 100644 --- a/bandit/core/node_visitor.py +++ b/bandit/core/node_visitor.py @@ -5,6 +5,7 @@ import ast import logging import operator +import sys from bandit.core import constants from bandit.core import tester as b_tester @@ -168,7 +169,7 @@ def visit_Str(self, node): :param node: The node that is being inspected :return: - """ - self.context["str"] = node.s + 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")) @@ -181,7 +182,7 @@ def visit_Bytes(self, node): :param node: The node that is being inspected :return: - """ - self.context["bytes"] = node.s + 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")) diff --git a/bandit/core/utils.py b/bandit/core/utils.py index 7fb775305..ce987c3bd 100644 --- a/bandit/core/utils.py +++ b/bandit/core/utils.py @@ -273,12 +273,12 @@ def linerange(node): def concat_string(node, stop=None): """Builds a string from a ast.BinOp chain. - This will build a string from a series of ast.Str nodes wrapped in + 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.Str or ast.BinOp) The node to process - :param stop: (ast.Str or ast.BinOp) Optional base node to stop at + :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 """ @@ -300,7 +300,10 @@ def _get(node, bits, stop=None): node = node._bandit_parent if isinstance(node, ast.BinOp): _get(node, bits, stop) - return (node, " ".join([x.s for x in bits if isinstance(x, ast.Str)])) + return ( + node, + " ".join([x.value for x in bits if isinstance(x, ast.Constant)]), + ) def get_called_name(node): @@ -361,6 +364,17 @@ def parse_ini_file(f_loc): def check_ast_node(name): "Check if the given name is that of a valid AST node." try: + # These ast Node types don't exist in Python 3.14, but plugins may + # still check on them. + if sys.version_info >= (3, 14) and name in ( + "Num", + "Str", + "Ellipsis", + "NameConstant", + "Bytes", + ): + return name + node = getattr(ast, name) if issubclass(node, ast.AST): return name diff --git a/bandit/plugins/django_sql_injection.py b/bandit/plugins/django_sql_injection.py index a57ff46a3..96f2ea963 100644 --- a/bandit/plugins/django_sql_injection.py +++ b/bandit/plugins/django_sql_injection.py @@ -68,7 +68,7 @@ def django_extra_used(context): if key in kwargs: if isinstance(kwargs[key], ast.List): for val in kwargs[key].elts: - if not isinstance(val, ast.Str): + if not isinstance(val, ast.Constant): insecure = True break else: @@ -77,12 +77,12 @@ def django_extra_used(context): if not insecure and "select" in kwargs: if isinstance(kwargs["select"], ast.Dict): for k in kwargs["select"].keys: - if not isinstance(k, ast.Str): + if not isinstance(k, ast.Constant): insecure = True break if not insecure: for v in kwargs["select"].values: - if not isinstance(v, ast.Str): + if not isinstance(v, ast.Constant): insecure = True break else: @@ -135,7 +135,7 @@ def django_rawsql_used(context): kwargs = keywords2dict(context.node.keywords) sql = kwargs["sql"] - if not isinstance(sql, ast.Str): + if not isinstance(sql, ast.Constant): return bandit.Issue( severity=bandit.MEDIUM, confidence=bandit.MEDIUM, diff --git a/bandit/plugins/django_xss.py b/bandit/plugins/django_xss.py index e96522a55..6ed0a3975 100644 --- a/bandit/plugins/django_xss.py +++ b/bandit/plugins/django_xss.py @@ -96,7 +96,7 @@ def evaluate_var(xss_var, parent, until, ignore_nodes=None): break to = analyser.is_assigned(node) if to: - if isinstance(to, ast.Str): + if isinstance(to, ast.Constant): secure = True elif isinstance(to, ast.Name): secure = evaluate_var(to, parent, to.lineno, ignore_nodes) @@ -105,7 +105,7 @@ def evaluate_var(xss_var, parent, until, ignore_nodes=None): elif isinstance(to, (list, tuple)): num_secure = 0 for some_to in to: - if isinstance(some_to, ast.Str): + if isinstance(some_to, ast.Constant): num_secure += 1 elif isinstance(some_to, ast.Name): if evaluate_var( @@ -131,7 +131,10 @@ def evaluate_call(call, parent, ignore_nodes=None): secure = False evaluate = False if isinstance(call, ast.Call) and isinstance(call.func, ast.Attribute): - if isinstance(call.func.value, ast.Str) and call.func.attr == "format": + if ( + isinstance(call.func.value, ast.Constant) + and call.func.attr == "format" + ): evaluate = True if call.keywords: evaluate = False # TODO(??) get support for this @@ -140,7 +143,7 @@ def evaluate_call(call, parent, ignore_nodes=None): args = list(call.args) num_secure = 0 for arg in args: - if isinstance(arg, ast.Str): + if isinstance(arg, ast.Constant): num_secure += 1 elif isinstance(arg, ast.Name): if evaluate_var(arg, parent, call.lineno, ignore_nodes): @@ -167,7 +170,7 @@ def evaluate_call(call, parent, ignore_nodes=None): def transform2call(var): if isinstance(var, ast.BinOp): is_mod = isinstance(var.op, ast.Mod) - is_left_str = isinstance(var.left, ast.Str) + is_left_str = isinstance(var.left, ast.Constant) if is_mod and is_left_str: new_call = ast.Call() new_call.args = [] @@ -212,7 +215,7 @@ def check_risk(node): secure = evaluate_call(xss_var, parent) elif isinstance(xss_var, ast.BinOp): is_mod = isinstance(xss_var.op, ast.Mod) - is_left_str = isinstance(xss_var.left, ast.Str) + is_left_str = isinstance(xss_var.left, ast.Constant) if is_mod and is_left_str: parent = node._bandit_parent while not isinstance(parent, (ast.Module, ast.FunctionDef)): @@ -272,5 +275,5 @@ def django_mark_safe(context): ] if context.call_function_name in affected_functions: xss = context.node.args[0] - if not isinstance(xss, ast.Str): + if not isinstance(xss, ast.Constant): return check_risk(context.node) diff --git a/bandit/plugins/general_hardcoded_password.py b/bandit/plugins/general_hardcoded_password.py index cc3e7d09d..594c23fd0 100644 --- a/bandit/plugins/general_hardcoded_password.py +++ b/bandit/plugins/general_hardcoded_password.py @@ -83,45 +83,45 @@ def hardcoded_password_string(context): # looks for "candidate='some_string'" for targ in node._bandit_parent.targets: if isinstance(targ, ast.Name) and RE_CANDIDATES.search(targ.id): - return _report(node.s) + return _report(node.value) elif isinstance(targ, ast.Attribute) and RE_CANDIDATES.search( targ.attr ): - return _report(node.s) + return _report(node.value) elif isinstance( node._bandit_parent, ast.Subscript - ) and RE_CANDIDATES.search(node.s): + ) 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.Str + assign.value, ast.Constant ): - return _report(assign.value.s) + return _report(assign.value.value) elif isinstance(node._bandit_parent, ast.Index) and RE_CANDIDATES.search( - node.s + 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.Str + assign.value, ast.Constant ): - return _report(assign.value.s) + 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.Str): - return _report(comp.comparators[0].s) + if isinstance(comp.comparators[0], ast.Constant): + 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.Str): - return _report(comp.comparators[0].s) + if isinstance(comp.comparators[0], ast.Constant): + return _report(comp.comparators[0].value) @test.checks("Call") @@ -176,8 +176,8 @@ def hardcoded_password_funcarg(context): """ # looks for "function(candidate='some_string')" for kw in context.node.keywords: - if isinstance(kw.value, ast.Str) and RE_CANDIDATES.search(kw.arg): - return _report(kw.value.s) + if isinstance(kw.value, ast.Constant) and RE_CANDIDATES.search(kw.arg): + return _report(kw.value.value) @test.checks("FunctionDef") @@ -242,5 +242,5 @@ def hardcoded_password_default(context): # 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)): - if isinstance(val, ast.Str) and RE_CANDIDATES.search(key.arg): - return _report(val.s) + if isinstance(val, ast.Constant) and RE_CANDIDATES.search(key.arg): + return _report(val.value) diff --git a/bandit/plugins/injection_shell.py b/bandit/plugins/injection_shell.py index 229368340..f48545507 100644 --- a/bandit/plugins/injection_shell.py +++ b/bandit/plugins/injection_shell.py @@ -15,7 +15,7 @@ def _evaluate_shell_call(context): - no_formatting = isinstance(context.node.args[0], ast.Str) + no_formatting = isinstance(context.node.args[0], ast.Constant) if no_formatting: return bandit.LOW @@ -83,16 +83,14 @@ def has_shell(context): for key in keywords: if key.arg == "shell": val = key.value - if isinstance(val, ast.Num): - result = bool(val.n) + if isinstance(val, ast.Constant): + 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.NameConstant): - result = val.value else: result = True return result @@ -687,7 +685,9 @@ def start_process_with_partial_path(context, config): node = node.elts[0] # make sure the param is a string literal and not a var name - if isinstance(node, ast.Str) and not full_path_match.match(node.s): + if isinstance(node, ast.Constant) and not full_path_match.match( + node.value + ): return bandit.Issue( severity=bandit.LOW, confidence=bandit.HIGH, diff --git a/bandit/plugins/injection_sql.py b/bandit/plugins/injection_sql.py index bd7aa92a1..5071be177 100644 --- a/bandit/plugins/injection_sql.py +++ b/bandit/plugins/injection_sql.py @@ -96,7 +96,7 @@ def _evaluate_ast(node): elif isinstance( node._bandit_parent, ast.Attribute ) and node._bandit_parent.attr in ("format", "replace"): - statement = node.s + 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": @@ -107,14 +107,14 @@ def _evaluate_ast(node): substrings = [ child for child in node._bandit_parent.values - if isinstance(child, ast.Str) + if isinstance(child, ast.Constant) ] # JoinedStr consists of list of Constant and FormattedValue # instances. Let's perform one test for the whole string # and abandon all parts except the first one to raise one # failed test instead of many for the same SQL statement. if substrings and node == substrings[0]: - statement = "".join([str(child.s) for child in substrings]) + statement = "".join([str(child.value) for child in substrings]) wrapper = node._bandit_parent._bandit_parent if isinstance(wrapper, ast.Call): # wrapped in "execute" call? diff --git a/bandit/plugins/tarfile_unsafe_members.py b/bandit/plugins/tarfile_unsafe_members.py index 5ad145c1a..d47a241e6 100644 --- a/bandit/plugins/tarfile_unsafe_members.py +++ b/bandit/plugins/tarfile_unsafe_members.py @@ -98,7 +98,7 @@ 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" + return isinstance(arg, ast.Constant) and arg.value == "data" @test.test_id("B202") diff --git a/tests/unit/core/test_context.py b/tests/unit/core/test_context.py index 23b3436da..32dff63df 100644 --- a/tests/unit/core/test_context.py +++ b/tests/unit/core/test_context.py @@ -132,39 +132,36 @@ def test_function_def_defaults_qual(self, get_qual_attr): def test__get_literal_value(self): new_context = context.Context() - value = ast.Num(42) - expected = value.n + value = ast.Constant(42) + expected = value.value self.assertEqual(expected, new_context._get_literal_value(value)) - value = ast.Str("spam") - expected = value.s + value = ast.Constant("spam") + expected = value.value self.assertEqual(expected, new_context._get_literal_value(value)) - value = ast.List([ast.Str("spam"), ast.Num(42)], ast.Load()) - expected = [ast.Str("spam").s, ast.Num(42).n] + value = ast.List([ast.Constant("spam"), ast.Constant(42)], ast.Load()) + expected = [ast.Constant("spam").value, ast.Constant(42).value] self.assertListEqual(expected, new_context._get_literal_value(value)) - value = ast.Tuple([ast.Str("spam"), ast.Num(42)], ast.Load()) - expected = (ast.Str("spam").s, ast.Num(42).n) + value = ast.Tuple([ast.Constant("spam"), ast.Constant(42)], ast.Load()) + expected = (ast.Constant("spam").value, ast.Constant(42).value) self.assertTupleEqual(expected, new_context._get_literal_value(value)) - value = ast.Set([ast.Str("spam"), ast.Num(42)]) - expected = {ast.Str("spam").s, ast.Num(42).n} + value = ast.Set([ast.Constant("spam"), ast.Constant(42)]) + expected = {ast.Constant("spam").value, ast.Constant(42).value} self.assertSetEqual(expected, new_context._get_literal_value(value)) value = ast.Dict(["spam", "eggs"], [42, "foo"]) expected = dict(spam=42, eggs="foo") self.assertDictEqual(expected, new_context._get_literal_value(value)) - value = ast.Ellipsis() - self.assertIsNone(new_context._get_literal_value(value)) - value = ast.Name("spam", ast.Load()) expected = value.id self.assertEqual(expected, new_context._get_literal_value(value)) - value = ast.Bytes(b"spam") - expected = value.s + value = ast.Constant(b"spam") + expected = value.value self.assertEqual(expected, new_context._get_literal_value(value)) self.assertIsNone(new_context._get_literal_value(None)) @@ -207,7 +204,7 @@ def test_get_lineno_for_call_arg(self, node): def test_get_call_arg_at_position(self): expected_arg = "spam" ref_call = mock.Mock() - ref_call.args = [ast.Str(expected_arg)] + ref_call.args = [ast.Constant(expected_arg)] ref_context = dict(call=ref_call) new_context = context.Context(context_object=ref_context) self.assertEqual(expected_arg, new_context.get_call_arg_at_position(0))