--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityUtils.py Thu Jul 07 11:23:56 2022 +0200 @@ -0,0 +1,329 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing utility functions used by the security checks. +""" + +import ast +import os +import contextlib + +import AstUtilities + + +class InvalidModulePath(Exception): + """ + Class defining an exception for invalid module paths. + """ + pass + + +def getModuleQualnameFromPath(path): + """ + Function to get the module's qualified name by analysis of the + path. + + Resolve the absolute pathname and eliminate symlinks. This could result + in an incorrect name if symlinks are used to restructure the python lib + directory. + + Starting from the right-most directory component look for __init__.py + in the directory component. If it exists then the directory name is + part of the module name. Move left to the subsequent directory + components until a directory is found without __init__.py. + + @param path path of the module to be analyzed + @type str + @return qualified name of the module + @rtype str + @exception InvalidModulePath raised to indicate an invalid module path + """ + (head, tail) = os.path.split(path) + if head == '' or tail == '': + raise InvalidModulePath('Invalid python file path: "{0}"' + ' Missing path or file name'.format(path)) + + qname = [os.path.splitext(tail)[0]] + while head not in ['/', '.', '']: + if os.path.isfile(os.path.join(head, '__init__.py')): + (head, tail) = os.path.split(head) + qname.insert(0, tail) + else: + break + + qualname = '.'.join(qname) + return qualname + + +def namespacePathJoin(namespace, name): + """ + Function to extend a given namespace path. + + @param namespace namespace to be extended + @type str + @param name node name to be appended + @type str + @return extended namespace + @rtype str + """ + return "{0}.{1}".format(namespace, name) + + +def namespacePathSplit(path): + """ + Function to split a namespace path into a head and tail. + + Tail will be the last namespace path component and head will + be everything leading up to that in the path. This is similar to + os.path.split. + + @param path namespace path to be split + @type str + @return tuple containing the namespace path head and tail + @rtype tuple of (str, str) + """ + return tuple(path.rsplit('.', 1)) + + +def getAttrQualName(node, aliases): + """ + Function to get a the full name for the attribute node. + + This will resolve a pseudo-qualified name for the attribute + rooted at node as long as all the deeper nodes are Names or + Attributes. This will give you how the code referenced the name but + will not tell you what the name actually refers to. If we + encounter a node without a static name we punt with an + empty string. If this encounters something more complex, such as + foo.mylist[0](a,b) we just return empty string. + + @param node attribute node to be treated + @type ast.Attribute + @param aliases dictionary of import aliases + @type dict + @return qualified name of the attribute + @rtype str + """ + if isinstance(node, ast.Name): + if node.id in aliases: + return aliases[node.id] + return node.id + elif isinstance(node, ast.Attribute): + name = "{0}.{1}".format(getAttrQualName(node.value, aliases), + node.attr) + if name in aliases: + return aliases[name] + return name + else: + return "" + + +def getCallName(node, aliases): + """ + Function to extract the call name from an ast.Call node. + + @param node node to extract information from + @type ast.Call + @param aliases dictionary of import aliases + @type dict + @return name of the ast.Call node + @rtype str + """ + if isinstance(node.func, ast.Name): + if deepgetattr(node, 'func.id') in aliases: + return aliases[deepgetattr(node, 'func.id')] + return deepgetattr(node, 'func.id') + elif isinstance(node.func, ast.Attribute): + return getAttrQualName(node.func, aliases) + else: + return "" + + +def getQualAttr(node, aliases): + """ + Function to extract the qualified name from an ast.Attribute node. + + @param node node to extract information from + @type ast.Attribute + @param aliases dictionary of import aliases + @type dict + @return qualified attribute name + @rtype str + """ + prefix = "" + if isinstance(node, ast.Attribute): + with contextlib.suppress(Exception): + val = deepgetattr(node, 'value.id') + prefix = ( + aliases[val] if val in aliases + else deepgetattr(node, 'value.id') + ) + # Id we can't get the fully qualified name for an attr, just return + # its base name. + + return "{0}.{1}".format(prefix, node.attr) + else: + return "" + + +def deepgetattr(obj, attr): + """ + Function to recurs through an attribute chain to get the ultimate value. + + @param obj reference to the object to be recursed + @type ast.Name or ast.Attribute + @param attr attribute chain to be parsed + @type ast.Attribute + @return ultimate value + @rtype ast.AST + """ + for key in attr.split('.'): + obj = getattr(obj, key) + return obj + + +def linerange(node): + """ + Function to get line number range from a node. + + @param node node to extract a line range from + @type ast.AST + @return list containing the line number range + @rtype list of int + """ + strip = { + "body": None, + "orelse": None, + "handlers": None, + "finalbody": None + } + for key in strip: + if hasattr(node, key): + strip[key] = getattr(node, key) + node.key = [] + + lines_min = 9999999999 + lines_max = -1 + for n in ast.walk(node): + if hasattr(n, 'lineno'): + lines_min = min(lines_min, n.lineno) + lines_max = max(lines_max, n.lineno) + + for key in strip: + if strip[key] is not None: + node.key = strip[key] + + if lines_max > -1: + return list(range(lines_min, lines_max + 1)) + + return [0, 1] + + +def linerange_fix(node): + """ + Function to get a line number range working around a known Python bug + with multi-line strings. + + @param node node to extract a line range from + @type ast.AST + @return list containing the line number range + @rtype list of int + """ + # deal with multiline strings lineno behavior (Python issue #16806) + lines = linerange(node) + if ( + hasattr(node, '_securitySibling') and + hasattr(node._securitySibling, 'lineno') + ): + start = min(lines) + delta = node._securitySibling.lineno - start + if delta > 1: + return list(range(start, node._securitySibling.lineno)) + + return lines + + +def escapedBytesRepresentation(b): + """ + Function to escape bytes for comparison with other strings. + + In practice it turns control characters into acceptable codepoints then + encodes them into bytes again to turn unprintable bytes into printable + escape sequences. + + This is safe to do for the whole range 0..255 and result matches + unicode_escape on a unicode string. + + @param b bytes object to be escaped + @type bytes + @return escaped bytes object + @rtype bytes + """ + return b.decode('unicode_escape').encode('unicode_escape') + + +def concatString(node, stop=None): + """ + Function to build a string from an ast.BinOp chain. + + This will build a string from a series of ast.Str/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 node to be processed + @type ast.BinOp or ast.Str/ast.Constant + @param stop base node to stop at + @type ast.BinOp or ast.Str/ast.Constant + @return tuple containing the root node of the expression and the string + value + @rtype tuple of (ast.AST, str) + """ + def _get(node, bits, stop=None): + if node != stop: + bits.append( + _get(node.left, bits, stop) + if isinstance(node.left, ast.BinOp) + else node.left + ) + bits.append( + _get(node.right, bits, stop) + if isinstance(node.right, ast.BinOp) + else node.right + ) + + bits = [node] + while isinstance(node._securityParent, ast.BinOp): + node = node._securityParent + if isinstance(node, ast.BinOp): + _get(node, bits, stop) + + return ( + node, + " ".join([x.s for x in bits if AstUtilities.isString(x)]) + ) + + +def getCalledName(node): + """ + Function to get the function name from an ast.Call node. + + An ast.Call node representing a method call will present differently to one + wrapping a function call: thing.call() vs call(). This helper will grab the + unqualified call name correctly in either case. + + @param node reference to the call node + @type ast.Call + @return function name of the node + @rtype str + """ + func = node.func + try: + return func.attr if isinstance(func, ast.Attribute) else func.id + except AttributeError: + return "" + +# +# eflag: noqa = M601