--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityUtils.py Mon Jun 08 08:17:14 2020 +0200 @@ -0,0 +1,257 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing utility functions used by the security checks. +""" + +import ast +import os + + +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 + """ + (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): + try: + val = deepgetattr(node, 'value.id') + if val in aliases: + prefix = aliases[val] + else: + prefix = deepgetattr(node, 'value.id') + except Exception: + # We can't get the fully qualified name for an attr, just return + # its base name. + pass + + 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 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.keys(): + 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.keys(): + 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')