Wed, 30 Dec 2020 11:00:44 +0100

Detlev Offenbach <>
Wed, 30 Dec 2020 11:00:44 +0100
changeset 7924
parent 7628
child 8205

Updated copyright for 2021.

# -*- coding: utf-8 -*-

# Copyright (c) 2020 - 2021 Detlev Offenbach <>

Module implementing utility functions used by the security checks.

import ast
import os

import AstUtilities

class InvalidModulePath(Exception):
    Class defining an exception for invalid module paths.

def getModuleQualnameFromPath(path):
    Function to get the module's qualified name by analysis of the
    Resolve the absolute pathname and eliminate symlinks. This could result
    in an incorrect name if symlinks are used to restructure the python lib
    Starting from the right-most directory component look for
    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
    @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, '')):
            (head, tail) = os.path.split(head)
            qname.insert(0, tail)

    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
    @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 in aliases:
            return aliases[]
    elif isinstance(node, ast.Attribute):
        name = "{0}.{1}".format(getAttrQualName(node.value, aliases),
        if name in aliases:
            return aliases[name]
        return name
        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, '') in aliases:
            return aliases[deepgetattr(node, '')]
        return deepgetattr(node, '')
    elif isinstance(node.func, ast.Attribute):
        return getAttrQualName(node.func, aliases)
        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):
            val = deepgetattr(node, '')
            if val in aliases:
                prefix = aliases[val]
                prefix = deepgetattr(node, '')
        except Exception:           # secok
            # We can't get the fully qualified name for an attr, just return
            # its base name.
        return "{0}.{1}".format(prefix, node.attr)
        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.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')

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
    @rtype tuple of (ast.AST, str)
    def _get(node, bits, stop=None):
        if node != stop:
                _get(node.left, bits, stop)
                if isinstance(node.left, ast.BinOp)
                else node.left
                _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 (
        " ".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: 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
        return func.attr if isinstance(func, ast.Attribute) else
    except AttributeError:
        return ""

# eflag: noqa = M601

eric ide