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

Tue, 16 Jun 2020 17:45:12 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Tue, 16 Jun 2020 17:45:12 +0200
changeset 7622
384e2aa5c073
parent 7621
ffd1f00ca376
child 7623
ddea119f0b1d

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

eric6.e4p file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/AnnotationsChecker.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/awsHardcodedPassword.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/blackListCalls.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/djangoSqlInjection.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/djangoXssVulnerability.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalHardcodedPassword.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionShell.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/SecurityUtils.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/translations.py file | annotate | diff | comparison | revisions
--- a/eric6.e4p	Tue Jun 16 17:44:28 2020 +0200
+++ b/eric6.e4p	Tue Jun 16 17:45:12 2020 +0200
@@ -321,6 +321,7 @@
     <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/NamingStyleChecker.py</Source>
     <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/__init__.py</Source>
     <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/assert.py</Source>
+    <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/awsHardcodedPassword.py</Source>
     <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/blackListCalls.py</Source>
     <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/blackListImports.py</Source>
     <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/certificateValidation.py</Source>
@@ -1235,6 +1236,7 @@
     <Source>eric6/ThirdParty/asttokens/asttokens/line_numbers.py</Source>
     <Source>eric6/ThirdParty/asttokens/asttokens/mark_tokens.py</Source>
     <Source>eric6/ThirdParty/asttokens/asttokens/util.py</Source>
+    <Source>eric6/ThirdParty/asttokens/asttokens/version.py</Source>
     <Source>eric6/ThirdParty/enum/__init__.py</Source>
     <Source>eric6/Toolbox/SingleApplication.py</Source>
     <Source>eric6/Toolbox/Startup.py</Source>
@@ -2112,9 +2114,6 @@
     <Other>eric6/APIs/MicroPython/circuitpython.api</Other>
     <Other>eric6/APIs/MicroPython/microbit.api</Other>
     <Other>eric6/APIs/MicroPython/micropython.api</Other>
-    <Other>eric6/APIs/Python/zope-2.10.7.api</Other>
-    <Other>eric6/APIs/Python/zope-2.11.2.api</Other>
-    <Other>eric6/APIs/Python/zope-3.3.1.api</Other>
     <Other>eric6/APIs/Python3/PyQt4.bas</Other>
     <Other>eric6/APIs/Python3/PyQt5.bas</Other>
     <Other>eric6/APIs/Python3/PyQtChart.bas</Other>
@@ -2122,6 +2121,9 @@
     <Other>eric6/APIs/Python3/QScintilla2.bas</Other>
     <Other>eric6/APIs/Python3/eric6.api</Other>
     <Other>eric6/APIs/Python3/eric6.bas</Other>
+    <Other>eric6/APIs/Python/zope-2.10.7.api</Other>
+    <Other>eric6/APIs/Python/zope-2.11.2.api</Other>
+    <Other>eric6/APIs/Python/zope-3.3.1.api</Other>
     <Other>eric6/APIs/QSS/qss.api</Other>
     <Other>eric6/APIs/Ruby/Ruby-1.8.7.api</Other>
     <Other>eric6/APIs/Ruby/Ruby-1.8.7.bas</Other>
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/AnnotationsChecker.py	Tue Jun 16 17:44:28 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/AnnotationsChecker.py	Tue Jun 16 17:45:12 2020 +0200
@@ -10,6 +10,8 @@
 import sys
 import ast
 
+import AstUtilities
+
 
 class AnnotationsChecker(object):
     """
@@ -461,7 +463,7 @@
     @return annotation complexity
     @rtype = int
     """
-    if isinstance(annotationNode, ast.Str):
+    if AstUtilities.isString(annotationNode):
         annotationNode = ast.parse(annotationNode.s).body[0].value
     if isinstance(annotationNode, ast.Subscript):
         return 1 + getAnnotationComplexity(annotationNode.slice.value)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/awsHardcodedPassword.py	Tue Jun 16 17:45:12 2020 +0200
@@ -0,0 +1,110 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing checks for potentially hardcoded AWS passwords.
+"""
+
+#
+# This is a modified version of the one found at
+# https://pypi.org/project/bandit-aws/.
+#
+# Original Copyright 2020 CMCRC (devcdt@cmcrc.com)
+#
+# License: GPLv3
+#
+
+from collections import Counter
+import math
+import re
+import string
+
+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
+    """
+    return {
+        "Str": [
+            (checkHardcodedAwsKey, ("S801", "S802")),
+        ],
+    }
+
+AWS_ACCESS_KEY_ID_SYMBOLS = string.ascii_uppercase + string.digits
+AWS_ACCESS_KEY_ID_REGEX = re.compile(
+    '[' + AWS_ACCESS_KEY_ID_SYMBOLS + ']{20}'
+)
+AWS_ACCESS_KEY_ID_MAX_ENTROPY = 3
+
+AWS_SECRET_ACCESS_KEY_SYMBOLS = string.ascii_letters + string.digits + '/+='
+AWS_SECRET_ACCESS_KEY_REGEX = re.compile(
+    '[' + AWS_SECRET_ACCESS_KEY_SYMBOLS + ']{40}'
+)
+AWS_SECRET_ACCESS_KEY_MAX_ENTROPY = 4.5
+
+
+def shannonEntropy(data, symbols):
+    """
+    Function to caclculate the Shannon entropy of some given data.
+    
+    Source:
+    http://blog.dkbza.org/2007/05/scanning-data-for-entropy-anomalies.html
+    
+    @param data data to calculate the entropy for
+    @type str
+    @param symbols allowed symbols
+    @type str
+    @return Shannon entropy of the given data
+    @rtype float
+    """
+    if not data:
+        return 0
+    entropy = 0
+    counts = Counter(data)
+    for x in symbols:
+        p_x = float(counts[x]) / len(data)
+        if p_x > 0:
+            entropy += - p_x * math.log(p_x, 2)
+    return entropy
+
+
+def checkHardcodedAwsKey(reportError, context, config):
+    """
+    Function to check for potentially hardcoded AWS passwords.
+    
+    @param reportError function to be used to report errors
+    @type func
+    @param context security context object
+    @type SecurityContext
+    @param config dictionary with configuration data
+    @type dict
+    """
+    node = context.node
+    if AWS_ACCESS_KEY_ID_REGEX.fullmatch(node.s):
+        entropy = shannonEntropy(node.s, AWS_ACCESS_KEY_ID_SYMBOLS)
+        if entropy > AWS_ACCESS_KEY_ID_MAX_ENTROPY:
+            reportError(
+                context.node.lineno - 1,
+                context.node.col_offset,
+                "S801",
+                "L",
+                "M",
+                node.s
+            )
+    
+    elif AWS_SECRET_ACCESS_KEY_REGEX.fullmatch(node.s):
+        entropy = shannonEntropy(node.s, AWS_SECRET_ACCESS_KEY_SYMBOLS)
+        if entropy > AWS_SECRET_ACCESS_KEY_MAX_ENTROPY:
+            reportError(
+                context.node.lineno - 1,
+                context.node.col_offset,
+                "S802",
+                "M",
+                "M",
+                node.s
+            )
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/blackListCalls.py	Tue Jun 16 17:44:28 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/blackListCalls.py	Tue Jun 16 17:45:12 2020 +0200
@@ -18,6 +18,8 @@
 import ast
 import fnmatch
 
+import AstUtilities
+
 _blacklists = {
     'S301': ([
         'pickle.loads',
@@ -197,7 +199,7 @@
         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):
+                if AstUtilities.isString(context.node.args[0]):
                     name = context.node.args[0].s
                 else:
                     name = "UNKNOWN"
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/djangoSqlInjection.py	Tue Jun 16 17:44:28 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/djangoSqlInjection.py	Tue Jun 16 17:45:12 2020 +0200
@@ -17,6 +17,8 @@
 
 import ast
 
+import AstUtilities
+
 
 def getChecks():
     """
@@ -82,7 +84,7 @@
             if key in kwargs:
                 if isinstance(kwargs[key], ast.List):
                     for val in kwargs[key].elts:
-                        if not isinstance(val, ast.Str):
+                        if not AstUtilities.isString(val):
                             insecure = True
                             break
                 else:
@@ -91,12 +93,12 @@
         if not insecure and 'select' in kwargs:
             if isinstance(kwargs['select'], ast.Dict):
                 for k in kwargs['select'].keys:
-                    if not isinstance(k, ast.Str):
+                    if not AstUtilities.isString(k):
                         insecure = True
                         break
                 if not insecure:
                     for v in kwargs['select'].values:
-                        if not isinstance(v, ast.Str):
+                        if not AstUtilities.isString(v):
                             insecure = True
                             break
             else:
@@ -126,7 +128,7 @@
     if context.isModuleImportedLike('django.db.models'):
         if context.callFunctionName == 'RawSQL':
             sql = context.node.args[0]
-            if not isinstance(sql, ast.Str):
+            if not AstUtilities.isString(sql):
                 reportError(
                     context.node.lineno - 1,
                     context.node.col_offset,
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/djangoXssVulnerability.py	Tue Jun 16 17:44:28 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/djangoXssVulnerability.py	Tue Jun 16 17:45:12 2020 +0200
@@ -18,6 +18,8 @@
 import ast
 import sys
 
+import AstUtilities
+
 PY2 = sys.version_info[0] == 2
 
 
@@ -57,7 +59,7 @@
         ]
         if context.callFunctionName in affectedFunctions:
             xss = context.node.args[0]
-            if not isinstance(xss, ast.Str):
+            if not AstUtilities.isString(xss):
                 checkPotentialRisk(reportError, context.node)
 
 
@@ -97,7 +99,7 @@
         secure = evaluateCall(xssVar, parent)
     elif isinstance(xssVar, ast.BinOp):
         isMod = isinstance(xssVar.op, ast.Mod)
-        isLeftStr = isinstance(xssVar.left, ast.Str)
+        isLeftStr = AstUtilities.isString(xssVar.left)
         if isMod and isLeftStr:
             parent = node._securityParent
             while not isinstance(parent, (ast.Module, ast.FunctionDef)):
@@ -260,7 +262,7 @@
                 break
             to = analyser.isAssigned(node)
             if to:
-                if isinstance(to, ast.Str):
+                if AstUtilities.isString(to):
                     secure = True
                 elif isinstance(to, ast.Name):
                     secure = evaluateVar(
@@ -270,7 +272,7 @@
                 elif isinstance(to, (list, tuple)):
                     numSecure = 0
                     for someTo in to:
-                        if isinstance(someTo, ast.Str):
+                        if AstUtilities.isString(someTo):
                             numSecure += 1
                         elif isinstance(someTo, ast.Name):
                             if evaluateVar(someTo, parent,
@@ -309,7 +311,10 @@
     evaluate = False
     
     if isinstance(call, ast.Call) and isinstance(call.func, ast.Attribute):
-        if isinstance(call.func.value, ast.Str) and call.func.attr == 'format':
+        if (
+            AstUtilities.isString(call.func.value) and
+            call.func.attr == 'format'
+        ):
             evaluate = True
             if call.keywords or (PY2 and call.kwargs):
                 evaluate = False
@@ -325,7 +330,7 @@
         
         numSecure = 0
         for arg in args:
-            if isinstance(arg, ast.Str):
+            if AstUtilities.isString(arg):
                 numSecure += 1
             elif isinstance(arg, ast.Name):
                 if evaluateVar(arg, parent, call.lineno, ignoreNodes):
@@ -362,7 +367,7 @@
     """
     if isinstance(var, ast.BinOp):
         isMod = isinstance(var.op, ast.Mod)
-        isLeftStr = isinstance(var.left, ast.Str)
+        isLeftStr = AstUtilities.isString(var.left)
         if isMod and isLeftStr:
             newCall = ast.Call()
             newCall.args = []
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalHardcodedPassword.py	Tue Jun 16 17:44:28 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/generalHardcodedPassword.py	Tue Jun 16 17:45:12 2020 +0200
@@ -19,6 +19,8 @@
 import re
 import sys
 
+import AstUtilities
+
 RE_WORDS = "(pas+wo?r?d|pass(phrase)?|pwd|token|secrete?|ken+wort|geheim)"
 RE_CANDIDATES = re.compile(
     '(^{0}$|_{0}_|^{0}_|_{0}$)'.format(RE_WORDS),
@@ -81,7 +83,7 @@
         assign = node._securityParent._securityParent._securityParent
         if (
             isinstance(assign, ast.Assign) and
-            isinstance(assign.value, ast.Str)
+            AstUtilities.isString(assign.value)
         ):
             reportError(
                 context.node.lineno - 1,
@@ -97,7 +99,7 @@
         comp = node._securityParent
         if isinstance(comp.left, ast.Name):
             if RE_CANDIDATES.search(comp.left.id):
-                if isinstance(comp.comparators[0], ast.Str):
+                if AstUtilities.isString(comp.comparators[0]):
                     reportError(
                         context.node.lineno - 1,
                         context.node.col_offset,
@@ -121,7 +123,7 @@
     """
     # looks for "function(candidate='some_string')"
     for kw in context.node.keywords:
-        if isinstance(kw.value, ast.Str) and RE_CANDIDATES.search(kw.arg):
+        if AstUtilities.isString(kw.value) and RE_CANDIDATES.search(kw.arg):
             reportError(
                 context.node.lineno - 1,
                 context.node.col_offset,
@@ -157,7 +159,7 @@
             isPy3Arg = isinstance(key, ast.arg)
         if isinstance(key, ast.Name) or isPy3Arg:
             check = key.arg if sys.version_info[0] > 2 else key.id  # Py3
-            if isinstance(val, ast.Str) and RE_CANDIDATES.search(check):
+            if AstUtilities.isString(val) and RE_CANDIDATES.search(check):
                 reportError(
                     context.node.lineno - 1,
                     context.node.col_offset,
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionShell.py	Tue Jun 16 17:44:28 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/injectionShell.py	Tue Jun 16 17:45:12 2020 +0200
@@ -19,6 +19,8 @@
 import re
 import sys
 
+import AstUtilities
+
 from Security.SecurityDefaults import SecurityDefaults
 
 # This regex starts with a windows drive letter (eg C:)
@@ -55,7 +57,7 @@
     @return severity level (L, M or H)
     @rtype str
     """
-    noFormatting = isinstance(context.node.args[0], ast.Str)
+    noFormatting = AstUtilities.isString(context.node.args[0])
 
     if noFormatting:
         return "L"
@@ -81,7 +83,7 @@
         for key in keywords:
             if key.arg == 'shell':
                 val = key.value
-                if isinstance(val, ast.Num):
+                if AstUtilities.isNumber(val):
                     result = bool(val.n)
                 elif isinstance(val, ast.List):
                     result = bool(val.elts)
@@ -90,8 +92,8 @@
                 elif isinstance(val, ast.Name) and val.id in ['False', 'None']:
                     result = False
                 elif (
-                    sys.version_info[0] > 2 and
-                    isinstance(val, ast.NameConstant)
+                    sys.version_info[0] >= 3 and
+                    AstUtilities.isNameConstant(val)
                 ):
                     result = val.value
                 else:
@@ -292,7 +294,10 @@
                 node = node.elts[0]
             
             # make sure the param is a string literal and not a var name
-            if isinstance(node, ast.Str) and not fullPathMatchRe.match(node.s):
+            if (
+                AstUtilities.isString(node) and
+                not fullPathMatchRe.match(node.s)
+            ):
                 reportError(
                     context.node.lineno - 1,
                     context.node.col_offset,
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityChecker.py	Tue Jun 16 17:44:28 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityChecker.py	Tue Jun 16 17:45:12 2020 +0200
@@ -92,6 +92,9 @@
         # Django XSS vulnerability
         "S703",
         
+        # hardcoded AWS passwords
+        "S801", "S802",
+        
         # Syntax error
         "S999",
     ]
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityContext.py	Tue Jun 16 17:44:28 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityContext.py	Tue Jun 16 17:45:12 2020 +0200
@@ -19,6 +19,8 @@
 import copy
 import sys
 
+import AstUtilities
+
 from . import SecurityUtils
 
 
@@ -228,10 +230,10 @@
         @return converted Python object
         @rtype Any
         """
-        if isinstance(literal, ast.Num):
+        if AstUtilities.isNumber(literal):
             literalValue = literal.n
         
-        elif isinstance(literal, ast.Str):
+        elif AstUtilities.isString(literal):
             literalValue = literal.s
         
         elif isinstance(literal, ast.List):
@@ -255,7 +257,10 @@
         elif isinstance(literal, ast.Dict):
             literalValue = dict(zip(literal.keys, literal.values))
         
-        elif isinstance(literal, ast.Ellipsis):
+        elif (
+            sys.version_info <= (3, 8, 0) and
+            isinstance(literal, ast.Ellipsis)
+        ):
             # what do we want to do with this?
             literalValue = None
         
@@ -267,14 +272,14 @@
         # being re-assigned in Python 3.
         elif (
             sys.version_info[0] >= 3 and
-            isinstance(literal, ast.NameConstant)
+            AstUtilities.isNameConstant(literal)
         ):
             literalValue = str(literal.value)
         
         # Bytes are only part of the AST in Python 3
         elif (
             sys.version_info[0] >= 3 and
-            isinstance(literal, ast.Bytes)
+            AstUtilities.isBytes(literal)
         ):
             literalValue = literal.s
         
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityUtils.py	Tue Jun 16 17:44:28 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityUtils.py	Tue Jun 16 17:45:12 2020 +0200
@@ -10,6 +10,8 @@
 import ast
 import os
 
+import AstUtilities
+
 
 class InvalidModulePath(Exception):
     """
@@ -264,14 +266,14 @@
     """
     Function to build a string from an ast.BinOp chain.
 
-    This will build a string from a series of ast.Str 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.
+    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
+    @type ast.BinOp or ast.Str/ast.Constant
     @param stop base node to stop at
-    @type ast.BinOp or ast.Str
+    @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)
@@ -297,7 +299,7 @@
     
     return (
         node,
-        " ".join([x.s for x in bits if isinstance(x, ast.Str)])
+        " ".join([x.s for x in bits if AstUtilities.isString(x)])
     )
 
 
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/translations.py	Tue Jun 16 17:44:28 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/translations.py	Tue Jun 16 17:45:12 2020 +0200
@@ -361,6 +361,14 @@
         "Security",
         "Potential XSS on 'mark_safe()' function."),
     
+    # hardcoded AWS passwords
+    "S801": QCoreApplication.translate(
+        "Security",
+        "Possible hardcoded AWS access key ID: {0:r}"),
+    "S802": QCoreApplication.translate(
+        "Security",
+        "Possible hardcoded AWS secret access key: {0:r}"),
+    
     # Syntax error
     "S999": QCoreApplication.translate(
         "Security",
@@ -402,5 +410,8 @@
     
     "S609": ["os.system"],
     
+    "S801": ["A1B2C3D4E5F6G7H8I9J0"],
+    "S802": ["aA1bB2cC3dD4/eE5fF6gG7+hH8iI9jJ0=kKlLM+="],
+    
     "S999": ["SyntaxError", "Invalid Syntax"],
 }

eric ide

mercurial