Code Style Checker: started to implement checker for security related issues.

Mon, 08 Jun 2020 08:17:14 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 08 Jun 2020 08:17:14 +0200
changeset 7612
ca1ce1e0fcff
parent 7611
d546c4e72f52
child 7613
382f89c11e27

Code Style Checker: started to implement checker for security related issues.

eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/__init__.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/blackListCalls.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityChecker.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityContext.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityNodeVisitor.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityUtils.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/__init__.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/translations.py file | annotate | diff | comparison | revisions
--- /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)

eric ide

mercurial