Mon, 08 Jun 2020 08:17:14 +0200
Code Style Checker: started to implement checker for security related issues.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/__init__.py Mon Jun 08 08:17:14 2020 +0200 @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package containing the various security checker modules. +""" + +import collections + +_checkermodules = [ + "blackListCalls", + "blackListImports", +] + + +def generateCheckersDict(): + """ + Function generate the dictionary with checkers. + + @return dictionary containing list of tuples with checker data + @rtype dict + """ + checkersDict = collections.defaultdict(list) + + for checkerModule in _checkermodules: + modName = "Security.Checks.{0}".format(checkerModule) + try: + mod = __import__(modName) + components = modName.split('.') + for comp in components[1:]: + mod = getattr(mod, comp) + except ImportError: + continue + + if not hasattr(mod, "getChecks"): + continue + + modCheckersDict = mod.getChecks() + for checktype, check in modCheckersDict.items(): + checkersDict[checktype].append(check) + + return checkersDict
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/blackListCalls.py Mon Jun 08 08:17:14 2020 +0200 @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing checks for blacklisted methods and functions. +""" + +import ast +import fnmatch + + +_blacklists = { + 'S301': ([ + 'pickle.loads', + 'pickle.load', + 'pickle.Unpickler', + 'cPickle.loads', + 'cPickle.load', + 'cPickle.Unpickler', + 'dill.loads', + 'dill.load', + 'dill.Unpickler', + 'shelve.open', + 'shelve.DbfilenameShelf'], + "M"), + 'S302': ([ + 'marshal.load', + 'marshal.loads'], + "M"), + 'S303': ([ + 'hashlib.md5', + 'hashlib.sha1', + 'Crypto.Hash.MD2.new', + 'Crypto.Hash.MD4.new', + 'Crypto.Hash.MD5.new', + 'Crypto.Hash.SHA.new', + 'Cryptodome.Hash.MD2.new', + 'Cryptodome.Hash.MD4.new', + 'Cryptodome.Hash.MD5.new', + 'Cryptodome.Hash.SHA.new', + 'cryptography.hazmat.primitives.hashes.MD5', + 'cryptography.hazmat.primitives.hashes.SHA1'], + "M"), +} + + +def getChecks(): + """ + Public method to get a dictionary with checks handled by this module. + + @return dictionary containing checker lists containing checker function and + list of codes + @rtype dict + """ + # TODO: should be list of tuples + return { + "Call": (checkBlacklist, tuple(_blacklists.keys())), + } + + +def checkBlacklist(reportError, context, config): + nodeType = context.node.__class__.__name__ + + if nodeType == 'Call': + 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 + else: + name = "UNKNOWN" + else: + name = "" # handle '__import__()' + else: + name = context.callFunctionNameQual + # In the case the Call is an importlib.import, treat the first + # argument name as an actual import module name. + # Will produce None if argument is not a literal or identifier. + if name in ["importlib.import_module", "importlib.__import__"]: + name = context.call_args[0] + + for code in _blacklists: + qualnames, severity = _blacklists[code] + for qualname in qualnames: + if name and fnmatch.fnmatch(name, qualname): + return reportError( + context.node.lineno, + context.node.col_offset, + code, + "M", + "H" + ) + + return None
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityChecker.py Mon Jun 08 08:17:14 2020 +0200 @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the security checker. +""" + +import sys +import ast +import collections + +from . import Checks +from .SecurityNodeVisitor import SecurityNodeVisitor + + +class SecurityChecker(object): + """ + Class implementing a checker for security issues. + """ + def __init__(self, source, filename, select, ignore, expected, repeat, + args): + """ + Constructor + + @param source source code to be checked + @type list of str + @param filename name of the source file + @type str + @param select list of selected codes + @type list of str + @param ignore list of codes to be ignored + @type list of str + @param expected list of expected codes + @type list of str + @param repeat flag indicating to report each occurrence of a code + @type bool + @param args dictionary of arguments for the miscellaneous checks + @type dict + """ + self.__select = tuple(select) + self.__ignore = ('',) if select else tuple(ignore) + self.__expected = expected[:] + self.__repeat = repeat + self.__filename = filename + self.__source = source[:] + self.__args = args + + # statistics counters + self.counters = {} + + # collection of detected errors + self.errors = [] + + checkersWithCodes = Checks.generateCheckersDict() + + self.__checkers = collections.defaultdict(list) + for checkType, checkersList in checkersWithCodes.items(): + for checker, codes in checkersList: + if any(not (code and self.__ignoreCode(code)) + for code in codes): + self.__checkers[checkType].append(checker) + + def __ignoreCode(self, code): + """ + Private method to check if the message code should be ignored. + + @param code message code to check for + @type str + @return flag indicating to ignore the given code + @rtype bool + """ + return (code.startswith(self.__ignore) and + not code.startswith(self.__select)) + + def reportError(self, lineNumber, offset, code, severity, confidence, + *args): + """ + Private method to record an issue. + + @param lineNumber line number of the issue + @type int + @param offset position within line of the issue + @type int + @param code message code + @type str + @param severity severity code (H = high, M = medium, L = low, + U = undefined) + @type str + @param configence confidence code (H = high, M = medium, L = low, + U = undefined) + @type str + @param args arguments for the message + @type list + """ + if self.__ignoreCode(code): + return + + if code in self.counters: + self.counters[code] += 1 + else: + self.counters[code] = 1 + + # Don't care about expected codes + if code in self.__expected: + return + + if code and (self.counters[code] == 1 or self.__repeat): + # record the issue with one based line number + self.errors.append({ + "file": self.__filename, + "line": lineNumber + 1, + "offset": offset, + "code": code, + "args": args, + "severity": severity, + "confidence": confidence, + }) + + def __reportInvalidSyntax(self): + """ + Private method to report a syntax error. + """ + exc_type, exc = sys.exc_info()[:2] + if len(exc.args) > 1: + offset = exc.args[1] + if len(offset) > 2: + offset = offset[1:3] + else: + offset = (1, 0) + self.__error(offset[0] - 1, + offset[1] or 0, + 'S999', + "H", + "H", + exc_type.__name__, exc.args[0]) + + def __generateTree(self): + """ + Private method to generate an AST for our source. + + @return generated AST + @rtype ast.AST + """ + source = "".join(self.__source) + # Check type for py2: if not str it's unicode + if sys.version_info[0] == 2: + try: + source = source.encode('utf-8') + except UnicodeError: + pass + + return compile(source, self.__filename, 'exec', ast.PyCF_ONLY_AST) + + def getConfig(self): + """ + Public method to get the configuration dictionary. + + @return dictionary containing the configuration + @rtype dict + """ + return self.__args + + def run(self): + """ + Public method to check the given source against security related + conditions. + """ + if not self.__filename: + # don't do anything, if essential data is missing + return + + if not self.__checkers: + # don't do anything, if no codes were selected + return + + try: + self.__tree = self.__generateTree() + except (SyntaxError, TypeError): + self.__reportInvalidSyntax() + return + + securityNodeVisitor = SecurityNodeVisitor( + self, self.__checkers, self.__filename) + securityNodeVisitor.generic_visit(self.__tree)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityContext.py Mon Jun 08 08:17:14 2020 +0200 @@ -0,0 +1,393 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a context class for security related checks. +""" + +# +# This code is a modified version of the one in 'bandit'. +# +# Original Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import ast +import sys + +from . import SecurityUtils + + +class SecurityContext(object): + """ + Class implementing a context class for security related checks. + """ + def __init__(self, contextObject=None): + """ + Constructor + + Initialize the class with a context dictionary or an empty + dictionary. + + @param contextObject context dictionary to be used to populate the + class + @type dict + """ + if contextObject is not None: + self.__context = contextObject + else: + self.__context = {} + + def __repr__(self): + """ + Private method to generate representation of object for printing or + interactive use. + + @return string representation of the object + @rtype str + """ + return "<SecurityContext {0}>".formar(self.__context) + + @property + def callArgs(self): + """ + Public method to get a list of function args. + + @return list of function args + @rtype list + """ + args = [] + if ( + 'call' in self.__context and + hasattr(self.__context['call'], 'args') + ): + for arg in self.__context['call'].args: + if hasattr(arg, 'attr'): + args.append(arg.attr) + else: + args.append(self.__getLiteralValue(arg)) + return args + + @property + def callArgsCount(self): + """ + Public method to get the number of args a function call has. + + @return number of args a function call has + @rtype int + """ + if 'call' in self.__context and hasattr(self.__context['call'], 'args'): + return len(self.__context['call'].args) + else: + return None + + @property + def callFunctionName(self): + """ + Public method to get the name (not FQ) of a function call. + + @return name (not FQ) of a function call + @rtype str + """ + return self.__context.get('name') + + @property + def callFunctionNameQual(self): + """ + Public method to get the FQ name of a function call. + + @return FQ name of a function call + @rtype str + """ + return self.__context.get('qualname') + + @property + def callKeywords(self): + """ + Public method to get a dictionary of keyword parameters. + + @return dictionary of keyword parameters + @rtype dict + """ + if ( + 'call' in self.__context and + hasattr(self.__context['call'], 'keywords') + ): + returnDict = {} + for kw in self.__context['call'].keywords: + if hasattr(kw.value, 'attr'): + returnDict[kw.arg] = kw.value.attr + else: + returnDict[kw.arg] = self.__getLiteralValue(kw.value) + return returnDict + + else: + return None + + @property + def node(self): + """ + Public method to get the raw AST node associated with the context. + + @return raw AST node associated with the context + @rtype ast.AST + """ + return self.__context.get('node') + + @property + def stringVal(self): + """ + Public method to get the value of a standalone unicode or string + object. + + @return value of a standalone unicode or string object + @rtype str + """ + return self.__context.get('str') + + @property + def bytesVal(self): + """ + Public method to get the value of a standalone bytes object. + + @return value of a standalone bytes object + @rtype bytes + """ + return self.__context.get('bytes') + + @property + def stringValAsEscapedBytes(self): + r""" + Public method to get the escaped value of the object. + + Turn the value of a string or bytes object into a byte sequence with + unknown, control, and \\ characters escaped. + + This function should be used when looking for a known sequence in a + potentially badly encoded string in the code. + + @return sequence of printable ascii bytes representing original string + @rtype str + """ + val = self.stringVal + if val is not None: + # it's any of str or unicode in py2, or str in py3 + return val.encode('unicode_escape') + + val = self.bytesVal + if val is not None: + return SecurityUtils.escapedBytesRepresentation(val) + + return None + + @property + def statement(self): + """ + Public method to get the raw AST for the current statement. + + @return raw AST for the current statement + @rtype ast.AST + """ + return self.__context.get('statement') + + @property + def functionDefDefaultsQual(self): + """ + Public method to get a list of fully qualified default values in a + function def. + + @return list of fully qualified default values in a function def + @rtype list + """ + defaults = [] + if ( + 'node' in self.__context and + hasattr(self.__context['node'], 'args') and + hasattr(self.__context['node'].args, 'defaults') + ): + for default in self.__context['node'].args.defaults: + defaults.append(SecurityUtils.getQualAttr( + default, + self.__context['import_aliases'])) + + return defaults + + def __getLiteralValue(self, literal): + """ + Private method to turn AST literals into native Python types. + + @param literal AST literal to be converted + @type ast.AST + @return converted Python object + @rtype Any + """ + if isinstance(literal, ast.Num): + literalValue = literal.n + + elif isinstance(literal, ast.Str): + literalValue = literal.s + + elif isinstance(literal, ast.List): + returnList = list() + for li in literal.elts: + returnList.append(self.__getLiteralValue(li)) + literalValue = returnList + + elif isinstance(literal, ast.Tuple): + returnTuple = tuple() + for ti in literal.elts: + returnTuple = returnTuple + (self.__getLiteralValue(ti),) + literalValue = returnTuple + + elif isinstance(literal, ast.Set): + returnSet = set() + for si in literal.elts: + returnSet.add(self.__getLiteralValue(si)) + literalValue = returnSet + + elif isinstance(literal, ast.Dict): + literalValue = dict(zip(literal.keys, literal.values)) + + elif isinstance(literal, ast.Ellipsis): + # what do we want to do with this? + literalValue = None + + elif isinstance(literal, ast.Name): + literalValue = literal.id + + # NameConstants are only part of the AST in Python 3. NameConstants + # tend to refer to things like True and False. This prevents them from + # being re-assigned in Python 3. + elif ( + sys.version_info >= (3, 0, 0) and + isinstance(literal, ast.NameConstant) + ): + literalValue = str(literal.value) + + # Bytes are only part of the AST in Python 3 + elif ( + sys.version_info >= (3, 0, 0) and + isinstance(literal, ast.Bytes) + ): + literalValue = literal.s + + else: + literalValue = None + + return literalValue + + def getCallArgValue(self, argumentName): + """ + Public method to get the value of a named argument in a function call. + + @param argumentName name of the argument to get the value for + @type str + @return value of the named argument + @rtype Any + """ + kwdValues = self.callKeywords + if kwdValues is not None and argumentName in kwdValues: + return kwdValues[argumentName] + + def checkCallArgValue(self, argumentName, argumentValues=None): + """ + Public method to check for a value of a named argument in a function + call. + + @param argumentName name of the argument to be checked + @type str + @param argumentValues value or list of values to test against + @type Any or list of Any + @return True if argument found and matched, False if found and not + matched, None if argument not found at all + @rtype bool or None + """ + argValue = self.getCallArgValue(argumentName) + if argValue is not None: + if not isinstance(argumentValues, list): + # if passed a single value, or a tuple, convert to a list + argumentValues = list((argumentValues,)) + for val in argumentValues: + if argValue == val: + return True + return False + else: + # argument name not found, return None to allow testing for this + # eventuality + return None + + def getLinenoForCallArg(self, argumentName): + """ + Public method to get the line number for a specific named argument. + + @param argumentName name of the argument to get the line number for + @type str + @return line number of the found argument or -1 + @rtype int + """ + if hasattr(self.node, 'keywords'): + for key in self.node.keywords: + if key.arg == argumentName: + return key.value.lineno + + def getCallArgAtPosition(self, positionNum): + """ + Public method to get a positional argument at the specified position + (if it exists). + + @param positionNum index of the argument to get the value for + @type int + @return value of the argument at the specified position if it exists + @rtype Any or None + """ + maxArgs = self.callArgsCount + if maxArgs and positionNum < maxArgs: + return self.__getLiteralValue( + self.__context['call'].args[positionNum] + ) + else: + return None + + def isModuleBeingImported(self, module): + """ + Public method to check for the given module is currently being + imported. + + @param module module name to look for + @type str + @return flag indicating the given module was found + @rtype bool + """ + return self.__context.get('module') == module + + def isModuleImportedExact(self, module): + """ + Public method to check if a given module has been imported; only exact + matches. + + @param module module name to look for + @type str + @return flag indicating the given module was found + @rtype bool + """ + return module in self.__context.get('imports', []) + + def isModuleImportedLike(self, module): + """ + Public method to check if a given module has been imported; given + module exists. + + @param module module name to look for + @type str + @return flag indicating the given module was found + @rtype bool + """ + if 'imports' in self.__context: + for imp in self.__context['imports']: + if module in imp: + return True + + return False
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityNodeVisitor.py Mon Jun 08 08:17:14 2020 +0200 @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing an AST node visitor for security checks. +""" + +import ast + +from . import SecurityUtils +from .SecurityContext import SecurityContext + + +class SecurityNodeVisitor(object): + """ + Class implementing an AST node visitor for security checks. + """ + def __init__(self, checker, secCheckers, filename): + self.__checker = checker + self.__securityCheckers = secCheckers + + self.seen = 0 + self.depth = 0 + self.filename = filename + self.imports = set() + self.import_aliases = {} + + # in some cases we can't determine a qualified name + try: + self.namespace = SecurityUtils.getModuleQualnameFromPath(filename) + except SecurityUtils.InvalidModulePath: + self.namespace = "" + + def __runChecks(self, checkType): + """ + Private method to run all enabled checks for a given check type. + """ + if checkType in self.__securityCheckers: + for check in self.__securityCheckers[checkType]: + check(self.__checker.reportError, + SecurityContext(self.__context), + self.__checker.getConfig()) + + def visit_ClassDef(self, node): + """ + Public method defining a visitor for AST ClassDef nodes. + + Add class name to current namespace for all descendants. + + @param node reference to the node being inspected + @type ast.ClassDef + """ + # For all child nodes, add this class name to current namespace + self.namespace = SecurityUtils.namespacePathJoin( + self.namespace, node.name) + + def visit_FunctionDef(self, node): + """ + Public method defining a visitor for AST FunctionDef nodes. + + Add relevant information about the node to the context for use in tests + which inspect function definitions. Add the function name to the + current namespace for all descendants. + + @param node reference to the node being inspected + @type ast.FunctionDef + """ + self.__context['function'] = node + qualname = SecurityUtils.namespacePathJoin(self.namespace, node.name) + name = qualname.split('.')[-1] + self.__context['qualname'] = qualname + self.__context['name'] = name + + # For all child nodes and any tests run, add this function name to + # current namespace + self.namespace = SecurityUtils.namespacePathJoin( + self.namespace, node.name) + + self.__runChecks("FunctionDef") + + def visit_Call(self, node): + """ + Public method defining a visitor for AST Call nodes. + + Add relevant information about the node to the context for use in tests + which inspect function calls. + + @param node reference to the node being inspected + @type ast.Call + """ + self.__context['call'] = node + qualname = SecurityUtils.getCallName(node, self.import_aliases) + name = qualname.split('.')[-1] + self.__context['qualname'] = qualname + self.__context['name'] = name + self.__runChecks("Call") + + def __preVisit(self, node): + """ + Private method to set up a context for the visit method. + + @param node node to base the context on + @type ast.AST + """ + self.__context = {} + self.__context['imports'] = self.imports + self.__context['import_aliases'] = self.import_aliases + + if hasattr(node, 'lineno'): + self.__context['lineno'] = node.lineno +## +## if node.lineno in self.nosec_lines: +## LOG.debug("skipped, nosec") +## self.metrics.note_nosec() +## return False + + self.__context['node'] = node + self.__context['linerange'] = SecurityUtils.linerange_fix(node) + self.__context['filename'] = self.filename + + self.seen += 1 + self.depth += 1 + + return True + + def visit(self, node): + """ + Public method to inspected an AST node. + + @param node AST node to be inspected + @type ast.AST + """ + name = node.__class__.__name__ + method = 'visit_' + name + visitor = getattr(self, method, None) + if visitor is not None: + visitor(node) + else: + self.__runChecks(name) + + def __postVisit(self, node): + """ + Private method to clean up after a node was visited. + + @param node AST node that was visited + @type ast.AST + """ + self.depth -= 1 + # Clean up post-recursion stuff that gets setup in the visit methods + # for these node types. + if isinstance(node, (ast.FunctionDef, ast.ClassDef)): + self.namespace = SecurityUtils.namespacePathSplit( + self.namespace)[0] + + def generic_visit(self, node): + """ + Public method to drive the node visitor. + + @param node node to be inspected + @type ast.AST + """ + for _, value in ast.iter_fields(node): + if isinstance(value, list): + maxIndex = len(value) - 1 + for index, item in enumerate(value): + if isinstance(item, ast.AST): + if index < maxIndex: + item._securitySibling = value[index + 1] + else: + item._securitySibling = None + item._securityParent = node + + if self.__preVisit(item): + self.visit(item) + self.generic_visit(item) + self.__postVisit(item) + + elif isinstance(value, ast.AST): + value._securitySibling = None + value._securityParent = node + if self.__preVisit(value): + self.visit(value) + self.generic_visit(value) + self.__postVisit(value)
--- /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')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/__init__.py Mon Jun 08 08:17:14 2020 +0200 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the security checker. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/translations.py Mon Jun 08 08:17:14 2020 +0200 @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + + +""" +Module implementing message translations for the code style plugin messages. +""" + + +from PyQt5.QtCore import QCoreApplication + + +__all__ = ["getTranslatedMessage"] + +_messages = { + "S301": QCoreApplication.translate( + "Security", + "Pickle and modules that wrap it can be unsafe when used to " + "deserialize untrusted data, possible security issue."), + "S302": QCoreApplication.translate( + "Security", + "Deserialization with the marshal module is possibly dangerous."), + "S303": QCoreApplication.translate( + "Security", + "Use of insecure MD2, MD4, MD5, or SHA1 hash function."), +} + + +_messages_sample_args = { +} + + +def getTranslatedMessage(messageCode, messageArgs): + """ + Module function to get a translated and formatted message for a + given message ID. + + @param messageCode the message code + @type str + @param messageArgs list of arguments or a single integer value to format + the message + @type list or int + @return translated and formatted message + @rtype str + """ + if messageCode in _messages: + if isinstance(messageArgs, int): + # Retranslate with correct plural form + return _messages[messageCode](messageArgs) + else: + return _messages[messageCode].format(*messageArgs) + else: + return QCoreApplication.translate( + "CodeStyleFixer", " no message defined for code '{0}'" + ).format(messageCode)