Tue, 16 Jun 2020 17:45:12 +0200
Code Style Checker: continued to implement checker for security related issues.
--- 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"], }