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

Mon, 08 Jun 2020 20:08:27 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 08 Jun 2020 20:08:27 +0200
changeset 7613
382f89c11e27
parent 7612
ca1ce1e0fcff
child 7614
646742c260bd

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

eric6.e4p file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCodeSelectionDialog.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCodeSelectionDialog.ui file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleStatisticsDialog.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/__init__.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/assert.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/blackListImports.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/certificateValidation.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/flaskDebug.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/yamlLoad.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/translations.py file | annotate | diff | comparison | revisions
eric6/Plugins/CheckerPlugins/CodeStyleChecker/translations.py file | annotate | diff | comparison | revisions
eric6/Plugins/PluginCodeStyleChecker.py file | annotate | diff | comparison | revisions
--- a/eric6.e4p	Mon Jun 08 08:17:14 2020 +0200
+++ b/eric6.e4p	Mon Jun 08 20:08:27 2020 +0200
@@ -320,7 +320,14 @@
     <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/MiscellaneousChecker.py</Source>
     <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/blackListCalls.py</Source>
+    <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/blackListImports.py</Source>
+    <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/certificateValidation.py</Source>
+    <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/djangoSqlInjection.py</Source>
+    <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/djangoXssVulnerability.py</Source>
+    <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/flaskDebug.py</Source>
+    <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/yamlLoad.py</Source>
     <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityChecker.py</Source>
     <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityContext.py</Source>
     <Source>eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityNodeVisitor.py</Source>
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCodeSelectionDialog.py	Mon Jun 08 08:17:14 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCodeSelectionDialog.py	Mon Jun 08 20:08:27 2020 +0200
@@ -7,12 +7,15 @@
 Module implementing a dialog to select code style message codes.
 """
 
+import textwrap
 
 from PyQt5.QtCore import Qt
 from PyQt5.QtWidgets import QDialog, QTreeWidgetItem
 
 from .Ui_CodeStyleCodeSelectionDialog import Ui_CodeStyleCodeSelectionDialog
 
+from .translations import getMessageCodes, getTranslatedMessage
+
 import UI.PixmapCache
 
 
@@ -37,40 +40,30 @@
         super(CodeStyleCodeSelectionDialog, self).__init__(parent)
         self.setupUi(self)
         
+        textWrapper = textwrap.TextWrapper(width=60)
+        
         self.codeTable.headerItem().setText(self.codeTable.columnCount(), "")
         codeList = [code.strip() for code in codes.split(",") if code.strip()]
         if categories:
             codeList = [code for code in codeList if not code[0] in categories]
         
-        from .translations import _messages, _messages_sample_args
-        from .Security import translations as s_translations
-        
         if showFixCodes:
             from .CodeStyleFixer import FixableCodeStyleIssues
             selectableCodes = FixableCodeStyleIssues
         else:
             selectableCodes = (
-                [x for x in list(_messages.keys())
-                 if not x.startswith('FIX')] +
-                [x for x in list(s_translations._messages.keys())]
+                [x for x in getMessageCodes() if not x.startswith('FIX')]
             )
             if categories:
                 # filter by category
                 selectableCodes = [x for x in selectableCodes
                                    if not x[0] in categories]
         for code in sorted(selectableCodes):
-            if code in _messages_sample_args:
-                message = _messages[code].format(*_messages_sample_args[code])
-            elif code in _messages:
-                message = _messages[code]
-            elif code in s_translations._messages_sample_args:
-                message = s_translations._messages[code].format(
-                    *s_translations._messages_sample_args)
-            elif code in s_translations._messages:
-                message = s_translations._messages[code]
-            else:
+            message = getTranslatedMessage(code, [], example=True)
+            if message is None:
                 continue
-            itm = QTreeWidgetItem(self.codeTable, [code, message])
+            itm = QTreeWidgetItem(self.codeTable, [
+                code, "\n".join(textWrapper.wrap(message))])
             if code.startswith(("W", "C", "M")):
                 itm.setIcon(0, UI.PixmapCache.getIcon("warning"))
             elif code.startswith("E"):
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCodeSelectionDialog.ui	Mon Jun 08 08:17:14 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCodeSelectionDialog.ui	Mon Jun 08 20:08:27 2020 +0200
@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>450</width>
-    <height>350</height>
+    <width>500</width>
+    <height>400</height>
    </rect>
   </property>
   <property name="windowTitle">
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleStatisticsDialog.py	Mon Jun 08 08:17:14 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleStatisticsDialog.py	Mon Jun 08 20:08:27 2020 +0200
@@ -8,12 +8,12 @@
 style checker run.
 """
 
+import textwrap
 
 from PyQt5.QtCore import Qt
 from PyQt5.QtWidgets import QDialog, QTreeWidgetItem
 
-from .translations import _messages, _messages_sample_args
-from .Security import translations as s_translations
+from .translations import getTranslatedMessage
 
 from .Ui_CodeStyleStatisticsDialog import Ui_CodeStyleStatisticsDialog
 
@@ -47,18 +47,15 @@
         
         totalIssues = 0
         
+        textWrapper = textwrap.TextWrapper(width=80)
+        
         for code in sorted(stats.keys()):
-            message = _messages.get(code) or s_translations._messages.get(code)
+            message = getTranslatedMessage(code, [], example=True)
             if message is None:
                 continue
             
-            if code in _messages_sample_args:
-                message = message.format(*_messages_sample_args[code])
-            elif code in s_translations._messages_sample_args:
-                message = message.format(
-                    *s_translations._messages_sample_args[code])
-            
-            self.__createItem(stats[code], code, message)
+            self.__createItem(stats[code], code,
+                              "\n".join(textWrapper.wrap(message)))
             totalIssues += stats[code]
         
         self.totalIssues.setText(
@@ -83,10 +80,8 @@
         @param code of a code style issue message (string)
         @param message code style issue message to be shown (string)
         """
-        itm = QTreeWidgetItem(self.statisticsList)
-        itm.setData(0, Qt.DisplayRole, count)
-        itm.setData(1, Qt.DisplayRole, code)
-        itm.setData(2, Qt.DisplayRole, message)
+        itm = QTreeWidgetItem(self.statisticsList, [
+            "{0:6d}".format(count), code, message])
         if code.startswith(("W", "C", "M")):
             itm.setIcon(1, UI.PixmapCache.getIcon("warning"))
         elif code.startswith("E"):
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/__init__.py	Mon Jun 08 08:17:14 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/__init__.py	Mon Jun 08 20:08:27 2020 +0200
@@ -8,23 +8,29 @@
 """
 
 import collections
-
-_checkermodules = [
-    "blackListCalls",
-    "blackListImports",
-]
+import os
 
 
 def generateCheckersDict():
     """
-    Function generate the dictionary with checkers.
+    Function to generate the dictionary with checkers.
+    
+    Checker modules are searched for inside this package. Each module
+    defining some checks must contain a function 'getChecks()' returning
+    a dictionary containing the check type as key and a list of tuples
+    with the check function and associated message codes.
     
     @return dictionary containing list of tuples with checker data
     @rtype dict
     """
     checkersDict = collections.defaultdict(list)
     
-    for checkerModule in _checkermodules:
+    checkersDirectory = os.path.dirname(__file__)
+    checkerModules = [os.path.splitext(m)[0]
+                      for m in os.listdir(checkersDirectory)
+                      if m != "__init__.py" and m.endswith(".py")]
+    
+    for checkerModule in checkerModules:
         modName = "Security.Checks.{0}".format(checkerModule)
         try:
             mod = __import__(modName)
@@ -38,7 +44,7 @@
             continue
         
         modCheckersDict = mod.getChecks()
-        for checktype, check in modCheckersDict.items():
-            checkersDict[checktype].append(check)
+        for checktype, checks in modCheckersDict.items():
+            checkersDict[checktype].extend(checks)
     
     return checkersDict
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/assert.py	Mon Jun 08 20:08:27 2020 +0200
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a check for the use of 'assert'.
+"""
+
+#
+# This is a modified version of the one found in the bandit package.
+#
+# Original Copyright 2014 Hewlett-Packard Development Company, L.P.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+
+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 {
+        "Assert": [
+            (checkAssertUsed, ("S101",)),
+        ],
+    }
+
+
+def checkAssertUsed(reportError, context, config):
+    """
+    Function to check for the use of 'assert'.
+    
+    @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
+    """
+    reportError(
+        context.node.lineno,
+        context.node.col_offset,
+        "S101",
+        "L",
+        "H"
+    )
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/blackListCalls.py	Mon Jun 08 08:17:14 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/blackListCalls.py	Mon Jun 08 20:08:27 2020 +0200
@@ -7,10 +7,17 @@
 Module implementing checks for blacklisted methods and functions.
 """
 
+#
+# This is a modified version of the one found in the bandit package.
+#
+# Original Copyright 2016 Hewlett-Packard Development Company, L.P.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
 import ast
 import fnmatch
 
-
 _blacklists = {
     'S301': ([
         'pickle.loads',
@@ -43,6 +50,118 @@
         'cryptography.hazmat.primitives.hashes.MD5',
         'cryptography.hazmat.primitives.hashes.SHA1'],
         "M"),
+    'S304': ([
+        'Crypto.Cipher.ARC2.new',
+        'Crypto.Cipher.ARC4.new',
+        'Crypto.Cipher.Blowfish.new',
+        'Crypto.Cipher.DES.new',
+        'Crypto.Cipher.XOR.new',
+        'Cryptodome.Cipher.ARC2.new',
+        'Cryptodome.Cipher.ARC4.new',
+        'Cryptodome.Cipher.Blowfish.new',
+        'Cryptodome.Cipher.DES.new',
+        'Cryptodome.Cipher.XOR.new',
+        'cryptography.hazmat.primitives.ciphers.algorithms.ARC4',
+        'cryptography.hazmat.primitives.ciphers.algorithms.Blowfish',
+        'cryptography.hazmat.primitives.ciphers.algorithms.IDEA'],
+        "H"),
+    'S305': ([
+        'cryptography.hazmat.primitives.ciphers.modes.ECB'],
+        "M"),
+    'S306': ([
+        'tempfile.mktemp'],
+        "M"),
+    'S307': ([
+        'eval'],
+        "M"),
+    'S308': ([
+        'django.utils.safestring.mark_safe'],
+        "M"),
+    'S309': ([
+        'httplib.HTTPSConnection',
+        'http.client.HTTPSConnection',
+        'six.moves.http_client.HTTPSConnection'],
+        "M"),
+    'S310': ([
+        'urllib.urlopen',
+        'urllib.request.urlopen',
+        'urllib.urlretrieve',
+        'urllib.request.urlretrieve',
+        'urllib.URLopener',
+        'urllib.request.URLopener',
+        'urllib.FancyURLopener',
+        'urllib.request.FancyURLopener',
+        'urllib2.urlopen',
+        'urllib2.Request',
+        'six.moves.urllib.request.urlopen',
+        'six.moves.urllib.request.urlretrieve',
+        'six.moves.urllib.request.URLopener',
+        'six.moves.urllib.request.FancyURLopener'],
+        ""),
+    'S311': ([
+        'random.random',
+        'random.randrange',
+        'random.randint',
+        'random.choice',
+        'random.uniform',
+        'random.triangular'],
+        "L"),
+    'S312': ([
+        'telnetlib.*'],
+        "H"),
+    'S313': ([
+        'xml.etree.cElementTree.parse',
+        'xml.etree.cElementTree.iterparse',
+        'xml.etree.cElementTree.fromstring',
+        'xml.etree.cElementTree.XMLParser'],
+        "M"),
+    'S314': ([
+        'xml.etree.ElementTree.parse',
+        'xml.etree.ElementTree.iterparse',
+        'xml.etree.ElementTree.fromstring',
+        'xml.etree.ElementTree.XMLParser'],
+        "M"),
+    'S315': ([
+        'xml.sax.expatreader.create_parser'],
+        "M"),
+    'S316': ([
+        'xml.dom.expatbuilder.parse',
+        'xml.dom.expatbuilder.parseString'],
+        "M"),
+    'S317': ([
+        'xml.sax.parse',
+        'xml.sax.parseString',
+        'xml.sax.make_parser'],
+        "M"),
+    'S318': ([
+        'xml.dom.minidom.parse',
+        'xml.dom.minidom.parseString'],
+        "M"),
+    'S319': ([
+        'xml.dom.pulldom.parse',
+        'xml.dom.pulldom.parseString'],
+        "M"),
+    'S320': ([
+        'lxml.etree.parse',
+        'lxml.etree.fromstring',
+        'lxml.etree.RestrictedElement',
+        'lxml.etree.GlobalParserTLS',
+        'lxml.etree.getDefaultParser',
+        'lxml.etree.check_docinfo'],
+        "M"),
+    'S321': ([
+        'ftplib.*'],
+        "H"),
+    'S322': ([
+        'input'],
+        "H"),
+    'S323': ([
+        'ssl._create_unverified_context'],
+        "M"),
+    'S325': ([
+        'os.tempnam',
+        'os.tmpnam'],
+        "M"),
 }
 
 
@@ -54,13 +173,24 @@
         list of codes
     @rtype dict
     """
-    # TODO: should be list of tuples
     return {
-        "Call": (checkBlacklist, tuple(_blacklists.keys())),
+        "Call": [
+            (checkBlacklist, tuple(_blacklists.keys())),
+        ],
     }
 
 
 def checkBlacklist(reportError, context, config):
+    """
+    Function to check for blacklisted method calls.
+    
+    @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
+    """
     nodeType = context.node.__class__.__name__
     
     if nodeType == 'Call':
@@ -85,12 +215,11 @@
             qualnames, severity = _blacklists[code]
             for qualname in qualnames:
                 if name and fnmatch.fnmatch(name, qualname):
-                    return reportError(
-                        context.node.lineno,
+                    reportError(
+                        context.node.lineno - 1,
                         context.node.col_offset,
                         code,
-                        "M",
-                        "H"
+                        severity,
+                        "H",
+                        name
                     )
-    
-    return None
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/blackListImports.py	Mon Jun 08 20:08:27 2020 +0200
@@ -0,0 +1,126 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing checks for blacklisted imports.
+"""
+
+#
+# This is a modified version of the one found in the bandit package.
+#
+# Original Copyright 2016 Hewlett-Packard Development Company, L.P.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+_blacklists = {
+    "S401": ([
+        'telnetlib'],
+        "H"),
+    "S402": ([
+        'ftplib'],
+        "H"),
+    "S403": ([
+        'pickle',
+        'cPickle',
+        'dill',
+        'shelve'],
+        "L"),
+    "S404": ([
+        'subprocess'],
+        "L"),
+    "S405": ([
+        'xml.etree.cElementTree',
+        'xml.etree.ElementTree'],
+        "L"),
+    "S406": ([
+        'xml.sax'],
+        "L"),
+    "S407": ([
+        'xml.dom.expatbuilder'],
+        "L"),
+    "S408": ([
+        'xml.dom.minidom'],
+        "L"),
+    "S409": ([
+        'xml.dom.pulldom'],
+        "L"),
+    "S410": ([
+        'lxml'],
+        "L"),
+    "S411": ([
+        'xmlrpclib'],
+        "H"),
+    "S412": ([
+        'wsgiref.handlers.CGIHandler',
+        'twisted.web.twcgi.CGIScript',
+        'twisted.web.twcgi.CGIDirectory'],
+        "H"),
+    "S413": ([
+        'Crypto.Cipher',
+        'Crypto.Hash',
+        'Crypto.IO',
+        'Crypto.Protocol',
+        'Crypto.PublicKey',
+        'Crypto.Random',
+        'Crypto.Signature',
+        'Crypto.Util'],
+        "H"),
+}
+
+
+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 {
+        "Import": [
+            (checkBlacklist, tuple(_blacklists.keys())),
+        ],
+        "ImportFrom": [
+            (checkBlacklist, tuple(_blacklists.keys())),
+        ],
+        "Call": [
+            (checkBlacklist, tuple(_blacklists.keys())),
+        ],
+    }
+
+
+def checkBlacklist(reportError, context, config):
+    """
+    Function to check for blacklisted method calls.
+    
+    @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
+    """
+    nodeType = context.node.__class__.__name__
+
+    if nodeType.startswith('Import'):
+        prefix = ""
+        if nodeType == "ImportFrom":
+            if context.node.module is not None:
+                prefix = context.node.module + "."
+
+        for code in _blacklists:
+            qualnames, severity = _blacklists[code]
+            for name in context.node.names:
+                for qualname in qualnames:
+                    if (prefix + name.name).startswith(qualname):
+                        reportError(
+                            context.node.lineno - 1,
+                            context.node.col_offset,
+                            code,
+                            severity,
+                            "H",
+                            name.name
+                        )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/certificateValidation.py	Mon Jun 08 20:08:27 2020 +0200
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing checks for switched off certificate validation.
+"""
+
+#
+# This is a modified version of the one found in the bandit package.
+#
+# Original Copyright 2014 Hewlett-Packard Development Company, L.P.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+
+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 {
+        "Call": [
+            (checkNoCertificateValidation, ("S501",)),
+        ],
+    }
+
+
+def checkNoCertificateValidation(reportError, context, config):
+    """
+    Function to check for switched off certificate validation.
+    
+    @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
+    """
+    http_verbs = ('get', 'options', 'head', 'post', 'put', 'patch', 'delete')
+    if (
+        'requests' in context.callFunctionNameQual and
+        context.callFunctionName in http_verbs
+    ):
+        if context.checkCallArgValue('verify', 'False'):
+            reportError(
+                context.getLinenoForCallArg('verify') - 1,
+                context.getOffsetForCallArg('verify'),
+                "S501",
+                "H",
+                "H"
+            )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/djangoSqlInjection.py	Mon Jun 08 20:08:27 2020 +0200
@@ -0,0 +1,136 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing checks for potential SQL injections risks.
+"""
+
+#
+# This is a modified version of the one found in the bandit package.
+#
+# Original Copyright (C) 2018 [Victor Torre](https://github.com/ehooo)
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+import ast
+
+
+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 {
+        "Call": [
+            (checkDjangoExtraUsed, ("S610",)),
+            (checkDjangoRawSqlUsed, ("S611",)),
+        ],
+    }
+
+
+def keywords2dict(keywords):
+    """
+    Function to extract keywords arguments into a dictionary.
+    
+    @param keywords list of keyword nodes
+    @type list of ast.keyword
+    @return dictionary with keyword name and value
+    @rtype dict
+    """
+    kwargs = {}
+    for node in keywords:
+        if isinstance(node, ast.keyword):
+            kwargs[node.arg] = node.value
+    return kwargs
+
+
+def checkDjangoExtraUsed(reportError, context, config):
+    """
+    Function to check for potential SQL injection on extra function.
+    
+    @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
+    """
+    if context.callFunctionName == 'extra':
+        kwargs = keywords2dict(context.node.keywords)
+        args = context.node.args
+        if args:
+            if len(args) >= 1:
+                kwargs['select'] = args[0]
+            if len(args) >= 2:
+                kwargs['where'] = args[1]
+            if len(args) >= 3:
+                kwargs['params'] = args[2]
+            if len(args) >= 4:
+                kwargs['tables'] = args[3]
+            if len(args) >= 5:
+                kwargs['order_by'] = args[4]
+            if len(args) >= 6:
+                kwargs['select_params'] = args[5]
+        insecure = False
+        for key in ['where', 'tables']:
+            if key in kwargs:
+                if isinstance(kwargs[key], ast.List):
+                    for val in kwargs[key].elts:
+                        if not isinstance(val, ast.Str):
+                            insecure = True
+                            break
+                else:
+                    insecure = True
+                    break
+        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):
+                        insecure = True
+                        break
+                if not insecure:
+                    for v in kwargs['select'].values:
+                        if not isinstance(v, ast.Str):
+                            insecure = True
+                            break
+            else:
+                insecure = True
+        
+        if insecure:
+            reportError(
+                context.node.lineno - 1,
+                context.node.col_offset,
+                "S610",
+                "M",
+                "M"
+            )
+
+
+def checkDjangoRawSqlUsed(reportError, context, config):
+    """
+    Function to check for potential SQL injection on RawSQL function.
+    
+    @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
+    """
+    if context.isModuleImportedLike('django.db.models'):
+        if context.callFunctionName == 'RawSQL':
+            sql = context.node.args[0]
+            if not isinstance(sql, ast.Str):
+                reportError(
+                    context.node.lineno - 1,
+                    context.node.col_offset,
+                    "S611",
+                    "M",
+                    "M"
+                )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/djangoXssVulnerability.py	Mon Jun 08 20:08:27 2020 +0200
@@ -0,0 +1,389 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing checks for potential XSS vulnerability.
+"""
+
+#
+# This is a modified version of the one found in the bandit package.
+#
+# Original Copyright 2018 Victor Torre
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+import ast
+import sys
+
+PY2 = sys.version_info < (3, 0, 0)
+
+
+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 {
+        "Call": [
+            (checkDjangoXssVulnerability, ("S703",)),
+        ],
+    }
+
+
+def checkDjangoXssVulnerability(reportError, context, config):
+    """
+    Function to check for potential XSS vulnerability.
+    
+    @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
+    """
+    if context.isModuleImportedLike('django.utils.safestring'):
+        affectedFunctions = [
+            'mark_safe',
+            'SafeText',
+            'SafeUnicode',
+            'SafeString',
+            'SafeBytes'
+        ]
+        if context.callFunctionName in affectedFunctions:
+            xss = context.node.args[0]
+            if not isinstance(xss, ast.Str):
+                checkPotentialRisk(reportError, context.node)
+
+
+def checkPotentialRisk(reportError, node):
+    """
+    Function to check a given node for a potential XSS vulnerability.
+    
+    @param reportError function to be used to report errors
+    @type func
+    @param node node to be checked
+    @type ast.Call
+    """
+    xssVar = node.args[0]
+    
+    secure = False
+    
+    if isinstance(xssVar, ast.Name):
+        # Check if the var are secure
+        parent = node._securityParent
+        while not isinstance(parent, (ast.Module, ast.FunctionDef)):
+            parent = parent._securityParent
+        
+        isParam = False
+        if isinstance(parent, ast.FunctionDef):
+            for name in parent.args.args:
+                argName = name.id if PY2 else name.arg
+                if argName == xssVar.id:
+                    isParam = True
+                    break
+        
+        if not isParam:
+            secure = evaluateVar(xssVar, parent, node.lineno)
+    elif isinstance(xssVar, ast.Call):
+        parent = node._securityParent
+        while not isinstance(parent, (ast.Module, ast.FunctionDef)):
+            parent = parent._securityParent
+        secure = evaluateCall(xssVar, parent)
+    elif isinstance(xssVar, ast.BinOp):
+        isMod = isinstance(xssVar.op, ast.Mod)
+        isLeftStr = isinstance(xssVar.left, ast.Str)
+        if isMod and isLeftStr:
+            parent = node._securityParent
+            while not isinstance(parent, (ast.Module, ast.FunctionDef)):
+                parent = parent._securityParent
+            newCall = transform2call(xssVar)
+            secure = evaluateCall(newCall, parent)
+    
+    if not secure:
+        reportError(
+            node.lineno - 1,
+            node.col_offset,
+            "S703",
+            "M",
+            "H"
+        )
+
+
+# TODO: carry on from here
+class DeepAssignation(object):
+    """
+    Class to perform a deep analysis of an assign.
+    """
+    def __init__(self, varName, ignoreNodes=None):
+        """
+        Constructor
+        
+        @param varName name of the variable
+        @type str
+        @param ignoreNodes list of nodes to ignore
+        @type list of ast.AST
+        """
+        self.__varName = varName
+        self.__ignoreNodes = ignoreNodes
+    
+    def isAssignedIn(self, items):
+        """
+        Public method to check, if the variable is assigned to.
+        
+        @param items list of nodes to check against
+        @type list of ast.AST
+        @return list of nodes assigned
+        @rtype list of ast.AST
+        """
+        assigned = []
+        for astInst in items:
+            newAssigned = self.isAssigned(astInst)
+            if newAssigned:
+                if isinstance(newAssigned, (list, tuple)):
+                    assigned.extend(newAssigned)
+                else:
+                    assigned.append(newAssigned)
+        
+        return assigned
+    
+    def isAssigned(self, node):
+        """
+        Public method to check assignment against a given node.
+        
+        @param node node to check against
+        @type ast.AST
+        @return flag indicating an assignement
+        @rtype bool
+        """
+        assigned = False
+        if self.__ignoreNodes:
+            if isinstance(self.__ignoreNodes, (list, tuple, object)):
+                if isinstance(node, self.__ignoreNodes):
+                    return assigned
+        
+        if isinstance(node, ast.Expr):
+            assigned = self.isAssigned(node.value)
+        elif isinstance(node, ast.FunctionDef):
+            for name in node.args.args:
+                if isinstance(name, ast.Name):
+                    if name.id == self.var_name.id:
+                        # If is param the assignations are not affected
+                        return assigned
+            
+            assigned = self.isAssignedIn(node.body)
+        elif isinstance(node, ast.With):
+            if PY2:
+                if node.optional_vars.id == self.__varName.id:
+                    assigned = node
+                else:
+                    assigned = self.isAssignedIn(node.body)
+            else:
+                for withitem in node.items:
+                    varId = getattr(withitem.optional_vars, 'id', None)
+                    if varId == self.__varName.id:
+                        assigned = node
+                    else:
+                        assigned = self.isAssignedIn(node.body)
+        elif PY2 and isinstance(node, ast.TryFinally):
+            assigned = []
+            assigned.extend(self.isAssignedIn(node.body))
+            assigned.extend(self.isAssignedIn(node.finalbody))
+        elif PY2 and isinstance(node, ast.TryExcept):
+            assigned = []
+            assigned.extend(self.isAssignedIn(node.body))
+            assigned.extend(self.isAssignedIn(node.handlers))
+            assigned.extend(self.isAssignedIn(node.orelse))
+        elif not PY2 and isinstance(node, ast.Try):
+            assigned = []
+            assigned.extend(self.isAssignedIn(node.body))
+            assigned.extend(self.isAssignedIn(node.handlers))
+            assigned.extend(self.isAssignedIn(node.orelse))
+            assigned.extend(self.isAssignedIn(node.finalbody))
+        elif isinstance(node, ast.ExceptHandler):
+            assigned = []
+            assigned.extend(self.isAssignedIn(node.body))
+        elif isinstance(node, (ast.If, ast.For, ast.While)):
+            assigned = []
+            assigned.extend(self.isAssignedIn(node.body))
+            assigned.extend(self.isAssignedIn(node.orelse))
+        elif isinstance(node, ast.AugAssign):
+            if isinstance(node.target, ast.Name):
+                if node.target.id == self.__varName.id:
+                    assigned = node.value
+        elif isinstance(node, ast.Assign) and node.targets:
+            target = node.targets[0]
+            if isinstance(target, ast.Name):
+                if target.id == self.__varName.id:
+                    assigned = node.value
+            elif isinstance(target, ast.Tuple):
+                pos = 0
+                for name in target.elts:
+                    if name.id == self.__varName.id:
+                        assigned = node.value.elts[pos]
+                        break
+                    pos += 1
+        
+        return assigned
+
+
+def evaluateVar(xssVar, parent, until, ignoreNodes=None):
+    """
+    Function to evaluate a variable node for potential XSS vulnerability.
+    
+    @param xssVar variable node to be checked
+    @type ast.Name
+    @param parent parent node
+    @type ast.AST
+    @param until end line number to evaluate variable against
+    @type int
+    @param ignoreNodes list of nodes to ignore
+    @type list of ast.AST
+    @return flag indicating a secure evaluation
+    @rtype bool
+    """
+    secure = False
+    if isinstance(xssVar, ast.Name):
+        if isinstance(parent, ast.FunctionDef):
+            for name in parent.args.args:
+                argName = name.id if PY2 else name.arg
+                if argName == xssVar.id:
+                    return False  # Params are not secure
+        
+        analyser = DeepAssignation(xssVar, ignoreNodes)
+        for node in parent.body:
+            if node.lineno >= until:
+                break
+            to = analyser.isAssigned(node)
+            if to:
+                if isinstance(to, ast.Str):
+                    secure = True
+                elif isinstance(to, ast.Name):
+                    secure = evaluateVar(
+                        to, parent, to.lineno, ignoreNodes)
+                elif isinstance(to, ast.Call):
+                    secure = evaluateCall(to, parent, ignoreNodes)
+                elif isinstance(to, (list, tuple)):
+                    numSecure = 0
+                    for someTo in to:
+                        if isinstance(someTo, ast.Str):
+                            numSecure += 1
+                        elif isinstance(someTo, ast.Name):
+                            if evaluateVar(someTo, parent,
+                                           node.lineno, ignoreNodes):
+                                numSecure += 1
+                            else:
+                                break
+                        else:
+                            break
+                    if numSecure == len(to):
+                        secure = True
+                    else:
+                        secure = False
+                        break
+                else:
+                    secure = False
+                    break
+    
+    return secure
+
+
+def evaluateCall(call, parent, ignoreNodes=None):
+    """
+    Function to evaluate a call node for potential XSS vulnerability.
+    
+    @param call call node to be checked
+    @type ast.Call
+    @param parent parent node
+    @type ast.AST
+    @param ignoreNodes list of nodes to ignore
+    @type list of ast.AST
+    @return flag indicating a secure evaluation
+    @rtype bool
+    """
+    secure = False
+    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':
+            evaluate = True
+            if call.keywords or (PY2 and call.kwargs):
+                evaluate = False
+    
+    if evaluate:
+        args = list(call.args)
+        if (
+            PY2 and
+            call.starargs and
+            isinstance(call.starargs, (ast.List, ast.Tuple))
+        ):
+            args.extend(call.starargs.elts)
+        
+        numSecure = 0
+        for arg in args:
+            if isinstance(arg, ast.Str):
+                numSecure += 1
+            elif isinstance(arg, ast.Name):
+                if evaluateVar(arg, parent, call.lineno, ignoreNodes):
+                    numSecure += 1
+                else:
+                    break
+            elif isinstance(arg, ast.Call):
+                if evaluateCall(arg, parent, ignoreNodes):
+                    numSecure += 1
+                else:
+                    break
+            elif (
+                not PY2 and
+                isinstance(arg, ast.Starred) and
+                isinstance(arg.value, (ast.List, ast.Tuple))
+            ):
+                args.extend(arg.value.elts)
+                numSecure += 1
+            else:
+                break
+        secure = numSecure == len(args)
+    
+    return secure
+
+
+def transform2call(var):
+    """
+    Function to transform a variable node to a call node.
+    
+    @param var variable node
+    @type ast.BinOp
+    @return call node
+    @rtype ast.Call
+    """
+    if isinstance(var, ast.BinOp):
+        isMod = isinstance(var.op, ast.Mod)
+        isLeftStr = isinstance(var.left, ast.Str)
+        if isMod and isLeftStr:
+            newCall = ast.Call()
+            newCall.args = []
+            newCall.args = []
+            if PY2:
+                newCall.starargs = None
+            newCall.keywords = None
+            if PY2:
+                newCall.kwargs = None
+            newCall.lineno = var.lineno
+            newCall.func = ast.Attribute()
+            newCall.func.value = var.left
+            newCall.func.attr = 'format'
+            if isinstance(var.right, ast.Tuple):
+                newCall.args = var.right.elts
+            elif PY2 and isinstance(var.right, ast.Dict):
+                newCall.kwargs = var.right
+            else:
+                newCall.args = [var.right]
+            
+            return newCall
+    
+    return None
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/flaskDebug.py	Mon Jun 08 20:08:27 2020 +0200
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing checks for running a flask application with enabled debug.
+"""
+
+#
+# This is a modified version of the one found in the bandit package.
+#
+# Original Copyright 2015 Hewlett-Packard Development Company, L.P.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+
+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 {
+        "Call": [
+            (checkFlaskDebug, ("S201",)),
+        ],
+    }
+
+
+def checkFlaskDebug(reportError, context, config):
+    """
+    Function to check for a flask app being run with debug.
+    
+    @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
+    """
+    if context.isModuleImportedLike('flask'):
+        if context.callFunctionNameQual.endswith('.run'):
+            if context.checkCallArgValue('debug', 'True'):
+                reportError(
+                    context.node.lineno - 1,
+                    context.node.col_offset,
+                    "S201",
+                    "L",
+                    "M"
+                )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/Checks/yamlLoad.py	Mon Jun 08 20:08:27 2020 +0200
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing checks for the use of yaml load functions.
+"""
+
+#
+# This is a modified version of the one found in the bandit package.
+#
+# Original Copyright (c) 2016 Rackspace, Inc.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+
+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 {
+        "Call": [
+            (checkYamlLoad, ("S506",)),
+        ],
+    }
+
+
+def checkYamlLoad(reportError, context, config):
+    """
+    Function to check for the use of of yaml load functions.
+    
+    @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
+    """
+    imported = context.isModuleImportedExact('yaml')
+    qualname = context.callFunctionNameQual
+    if not imported and isinstance(qualname, str):
+        return
+    
+    qualnameList = qualname.split('.')
+    func = qualnameList[-1]
+    if all([
+            'yaml' in qualnameList,
+            func == 'load',
+            not context.checkCallArgValue('Loader', 'SafeLoader'),
+            not context.checkCallArgValue('Loader', 'CSafeLoader'),
+    ]):
+        reportError(
+            context.node.lineno - 1,
+            context.node.col_offset,
+            "S506",
+            "M",
+            "H"
+        )
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityChecker.py	Mon Jun 08 08:17:14 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityChecker.py	Mon Jun 08 20:08:27 2020 +0200
@@ -19,6 +19,35 @@
     """
     Class implementing a checker for security issues.
     """
+    Codes = [
+        # assert used
+        "S101",
+        
+        # flask app
+        "S201",
+        
+        # insecure function calls (blacklisted)
+        "S301", "S302", "S303", "S304", "S305", "S306", "S307", "S308", "S309",
+        "S310", "S311", "S312", "S313", "S314", "S315", "S316", "S317", "S318",
+        "S319", "S320", "S321", "S322", "S323", "S325",
+        
+        # insecure imports (blacklisted)
+        "S401", "S402", "S403", "S404", "S405", "S406", "S407", "S408", "S409",
+        "S410", "S411", "S412", "S413",
+        
+        # insecure certificate usage
+        "S501",
+        
+        # Django SQL injection
+        "S610", "S611",
+        
+        # Django XSS vulnerability
+        "S703",
+        
+        # YAML load
+        "S506",
+    ]
+    
     def __init__(self, source, filename, select, ignore, expected, repeat,
                  args):
         """
@@ -77,7 +106,7 @@
     def reportError(self, lineNumber, offset, code, severity, confidence,
                     *args):
         """
-        Private method to record an issue.
+        Public method to record an issue.
         
         @param lineNumber line number of the issue
         @type int
@@ -88,7 +117,7 @@
         @param severity severity code (H = high, M = medium, L = low,
             U = undefined)
         @type str
-        @param configence confidence code (H = high, M = medium, L = low,
+        @param confidence confidence code (H = high, M = medium, L = low,
             U = undefined)
         @type str
         @param args arguments for the message
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityContext.py	Mon Jun 08 08:17:14 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityContext.py	Mon Jun 08 20:08:27 2020 +0200
@@ -16,6 +16,7 @@
 #
 
 import ast
+import copy
 import sys
 
 from . import SecurityUtils
@@ -37,13 +38,13 @@
         @type dict
         """
         if contextObject is not None:
-            self.__context = contextObject
+            self.__context = copy.copy(contextObject)
         else:
             self.__context = {}
     
     def __repr__(self):
         """
-        Private method to generate representation of object for printing or
+        Special method to generate representation of object for printing or
         interactive use.
         
         @return string representation of the object
@@ -79,7 +80,10 @@
         @return number of args a function call has
         @rtype int
         """
-        if 'call' in self.__context and hasattr(self.__context['call'], 'args'):
+        if (
+            'call' in self.__context and
+            hasattr(self.__context['call'], 'args')
+        ):
             return len(self.__context['call'].args)
         else:
             return None
@@ -231,13 +235,13 @@
             literalValue = literal.s
         
         elif isinstance(literal, ast.List):
-            returnList = list()
+            returnList = []
             for li in literal.elts:
                 returnList.append(self.__getLiteralValue(li))
             literalValue = returnList
         
         elif isinstance(literal, ast.Tuple):
-            returnTuple = tuple()
+            returnTuple = ()
             for ti in literal.elts:
                 returnTuple = returnTuple + (self.__getLiteralValue(ti),)
             literalValue = returnTuple
@@ -291,6 +295,8 @@
         kwdValues = self.callKeywords
         if kwdValues is not None and argumentName in kwdValues:
             return kwdValues[argumentName]
+        
+        return None
     
     def checkCallArgValue(self, argumentName, argumentValues=None):
         """
@@ -309,7 +315,7 @@
         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,))
+                argumentValues = [argumentValues]
             for val in argumentValues:
                 if argValue == val:
                     return True
@@ -332,6 +338,24 @@
             for key in self.node.keywords:
                 if key.arg == argumentName:
                     return key.value.lineno
+        
+        return -1
+    
+    def getOffsetForCallArg(self, argumentName):
+        """
+        Public method to get the offset for a specific named argument.
+        
+        @param argumentName name of the argument to get the line number for
+        @type str
+        @return offset 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.col_offset
+        
+        return -1
     
     def getCallArgAtPosition(self, positionNum):
         """
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityNodeVisitor.py	Mon Jun 08 08:17:14 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityNodeVisitor.py	Mon Jun 08 20:08:27 2020 +0200
@@ -18,6 +18,16 @@
     Class implementing an AST node visitor for security checks.
     """
     def __init__(self, checker, secCheckers, filename):
+        """
+        Constructor
+        
+        @param checker reference to the main security checker object
+        @type SecurityChecker
+        @param secCheckers dictionary containing the available checker routines
+        @type dict
+        @param filename name of the checked file
+        @type str
+        """
         self.__checker = checker
         self.__securityCheckers = secCheckers
         
@@ -36,6 +46,9 @@
     def __runChecks(self, checkType):
         """
         Private method to run all enabled checks for a given check type.
+        
+        @param checkType type of checks to be run
+        @type str
         """
         if checkType in self.__securityCheckers:
             for check in self.__securityCheckers[checkType]:
@@ -97,12 +110,108 @@
         self.__context['name'] = name
         self.__runChecks("Call")
     
+    def visit_Import(self, node):
+        """
+        Public method defining a visitor for AST Import nodes.
+        
+        @param node reference to the node being inspected
+        @type ast.Import
+        """
+        for nodename in node.names:
+            if nodename.asname:
+                self.import_aliases[nodename.asname] = nodename.name
+            self.imports.add(nodename.name)
+            self.__context['module'] = nodename.name
+        self.__runChecks("Import")
+    
+    def visit_ImportFrom(self, node):
+        """
+        Public method defining a visitor for AST Import nodes.
+        
+        This adds relevant information about the node to
+        the context for use in tests which inspect imports.
+        
+        @param node reference to the node being inspected
+        @type ast.ImportFrom
+        """
+        module = node.module
+        if module is None:
+            self.visit_Import(node)
+            return
+        
+        for nodename in node.names:
+            if nodename.asname:
+                self.import_aliases[nodename.asname] = (
+                    module + "." + nodename.name
+                )
+            else:
+                # Even if import is not aliased we need an entry that maps
+                # name to module.name.  For example, with 'from a import b'
+                # b should be aliased to the qualified name a.b
+                self.import_aliases[nodename.name] = (
+                    module + '.' + nodename.name)
+            self.imports.add(module + "." + nodename.name)
+            self.__context['module'] = module
+            self.__context['name'] = nodename.name
+        self.__runChecks("ImportFrom")
+    
+    def visit_Constant(self, node):
+        """
+        Public method defining a visitor for Constant nodes.
+        
+        This calls the appropriate method for the node type.
+        It maintains compatibility with <3.6 and 3.8+
+        
+        @param node reference to the node being inspected
+        @type ast.Constant
+        """
+        if isinstance(node.value, str):
+            self.visit_Str(node)
+        elif isinstance(node.value, bytes):
+            self.visit_Bytes(node)
+
+    def visit_Str(self, node):
+        """
+        Public method defining a visitor for String nodes.
+        
+        This adds relevant information about node to
+        the context for use in tests which inspect strings.
+        
+        @param node reference to the node being inspected
+        @type ast.Str
+        """
+        self.__context['str'] = node.s
+        if not isinstance(node._securityParent, ast.Expr):  # docstring
+            self.__context['linerange'] = SecurityUtils.linerange_fix(
+                node._securityParent
+            )
+            self.__runChecks("Str")
+
+    def visit_Bytes(self, node):
+        """
+        Public method defining a visitor for Bytes nodes.
+        
+        This adds relevant information about node to
+        the context for use in tests which inspect strings.
+        
+        @param node reference to the node being inspected
+        @type ast.Bytes
+        """
+        self.__context['bytes'] = node.s
+        if not isinstance(node._securityParent, ast.Expr):  # docstring
+            self.__context['linerange'] = SecurityUtils.linerange_fix(
+                node._securityParent
+            )
+            self.__runChecks("Bytes")
+    
     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
+        @return flag indicating to visit the node
+        @rtype bool
         """
         self.__context = {}
         self.__context['imports'] = self.imports
@@ -110,7 +219,7 @@
         
         if hasattr(node, 'lineno'):
             self.__context['lineno'] = node.lineno
-##            
+##
 ##            if node.lineno in self.nosec_lines:
 ##                LOG.debug("skipped, nosec")
 ##                self.metrics.note_nosec()
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityUtils.py	Mon Jun 08 08:17:14 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityUtils.py	Mon Jun 08 20:08:27 2020 +0200
@@ -36,6 +36,7 @@
     @type str
     @return qualified name of the module
     @rtype str
+    @exception InvalidModulePath raised to indicate an invalid module path
     """
     (head, tail) = os.path.split(path)
     if head == '' or tail == '':
@@ -171,6 +172,8 @@
     """
     Function to recurs through an attribute chain to get the ultimate value.
     
+    @param obj reference to the object to be recursed
+    @type ast.Name or ast.Attribute
     @param attr attribute chain to be parsed
     @type ast.Attribute
     @return ultimate value
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/translations.py	Mon Jun 08 08:17:14 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/translations.py	Mon Jun 08 20:08:27 2020 +0200
@@ -5,16 +5,26 @@
 
 
 """
-Module implementing message translations for the code style plugin messages.
+Module implementing message translations for the code style plugin messages
+(security part).
 """
 
-
 from PyQt5.QtCore import QCoreApplication
 
-
-__all__ = ["getTranslatedMessage"]
-
-_messages = {
+_securityMessages = {
+    # assert used
+    "S101": QCoreApplication.translate(
+        "Security",
+        "Use of assert detected. The enclosed code will be removed when"
+        " compiling to optimised byte code."),
+    
+    # flask app
+    "S201": QCoreApplication.translate(
+        "Security",
+        "A Flask app appears to be run with debug=True, which exposes the"
+        " Werkzeug debugger and allows the execution of arbitrary code."),
+    
+    # blacklisted calls
     "S301": QCoreApplication.translate(
         "Security",
         "Pickle and modules that wrap it can be unsafe when used to "
@@ -25,33 +35,216 @@
     "S303": QCoreApplication.translate(
         "Security",
         "Use of insecure MD2, MD4, MD5, or SHA1 hash function."),
-}
-
-
-_messages_sample_args = {
+    "S304": QCoreApplication.translate(
+        "Security",
+        "Use of insecure cipher '{0}'. Replace with a known secure cipher"
+        " such as AES."),
+    "S305": QCoreApplication.translate(
+        "Security",
+        "Use of insecure cipher mode '{0}'."),
+    "S306": QCoreApplication.translate(
+        "Security",
+        "Use of insecure and deprecated function (mktemp)."),
+    "S307": QCoreApplication.translate(
+        "Security",
+        "Use of possibly insecure function - consider using safer"
+        " ast.literal_eval."),
+    "S308": QCoreApplication.translate(
+        "Security",
+        "Use of mark_safe() may expose cross-site scripting vulnerabilities"
+        " and should be reviewed."),
+    "S309": QCoreApplication.translate(
+        "Security",
+        "Use of HTTPSConnection on older versions of Python prior to 2.7.9"
+        " and 3.4.3 do not provide security, see"
+        " https://wiki.openstack.org/wiki/OSSN/OSSN-0033"),
+    "S310": QCoreApplication.translate(
+        "Security",
+        "Audit url open for permitted schemes. Allowing use of file:/ or"
+        " custom schemes is often unexpected."),
+    "S311": QCoreApplication.translate(
+        "Security",
+        "Standard pseudo-random generators are not suitable for"
+        " security/cryptographic purposes."),
+    "S312": QCoreApplication.translate(
+        "Security",
+        "Telnet-related functions are being called. Telnet is considered"
+        " insecure. Use SSH or some other encrypted protocol."),
+    "S313": QCoreApplication.translate(
+        "Security",
+        "Using '{0}' to parse untrusted XML data is known to be vulnerable to"
+        " XML attacks. Replace '{0}' with its defusedxml equivalent function"
+        " or make sure defusedxml.defuse_stdlib() is called."),
+    "S314": QCoreApplication.translate(
+        "Security",
+        "Using '{0}' to parse untrusted XML data is known to be vulnerable to"
+        " XML attacks. Replace '{0}' with its defusedxml equivalent function"
+        " or make sure defusedxml.defuse_stdlib() is called."),
+    "S315": QCoreApplication.translate(
+        "Security",
+        "Using '{0}' to parse untrusted XML data is known to be vulnerable to"
+        " XML attacks. Replace '{0}' with its defusedxml equivalent function"
+        " or make sure defusedxml.defuse_stdlib() is called."),
+    "S316": QCoreApplication.translate(
+        "Security",
+        "Using '{0}' to parse untrusted XML data is known to be vulnerable to"
+        " XML attacks. Replace '{0}' with its defusedxml equivalent function"
+        " or make sure defusedxml.defuse_stdlib() is called."),
+    "S317": QCoreApplication.translate(
+        "Security",
+        "Using '{0}' to parse untrusted XML data is known to be vulnerable to"
+        " XML attacks. Replace '{0}' with its defusedxml equivalent function"
+        " or make sure defusedxml.defuse_stdlib() is called."),
+    "S318": QCoreApplication.translate(
+        "Security",
+        "Using '{0}' to parse untrusted XML data is known to be vulnerable to"
+        " XML attacks. Replace '{0}' with its defusedxml equivalent function"
+        " or make sure defusedxml.defuse_stdlib() is called."),
+    "S319": QCoreApplication.translate(
+        "Security",
+        "Using '{0}' to parse untrusted XML data is known to be vulnerable to"
+        " XML attacks. Replace '{0}' with its defusedxml equivalent function"
+        " or make sure defusedxml.defuse_stdlib() is called."),
+    "S320": QCoreApplication.translate(
+        "Security",
+        "Using '{0}' to parse untrusted XML data is known to be vulnerable to"
+        " XML attacks. Replace '{0}' with its defusedxml equivalent"
+        " function."),
+    "S321": QCoreApplication.translate(
+        "Security",
+        "FTP-related functions are being called. FTP is considered insecure."
+        " Use SSH/SFTP/SCP or some other encrypted protocol."),
+    "S322": QCoreApplication.translate(
+        "Security",
+        "The input method in Python 2 will read from standard input, evaluate"
+        " and run the resulting string as Python source code. This is"
+        " similar, though in many ways worse, than using eval. On Python 2,"
+        " use raw_input instead, input is safe in Python 3."),
+    "S323": QCoreApplication.translate(
+        "Security",
+        "By default, Python will create a secure, verified SSL context for"
+        " use in such classes as HTTPSConnection. However, it still allows"
+        " using an insecure context via the _create_unverified_context that"
+        " reverts to the previous behavior that does not validate"
+        " certificates or perform hostname checks."),
+    "S325": QCoreApplication.translate(
+        "Security",
+        "Use of os.tempnam() and os.tmpnam() is vulnerable to symlink"
+        " attacks. Consider using tmpfile() instead."),
+    
+    # blacklisted imports
+    "S401": QCoreApplication.translate(
+        "Security",
+        "A telnet-related module is being imported.  Telnet is considered"
+        " insecure. Use SSH or some other encrypted protocol."),
+    "S402": QCoreApplication.translate(
+        "Security",
+        "A FTP-related module is being imported.  FTP is considered"
+        " insecure. Use SSH/SFTP/SCP or some other encrypted protocol."),
+    "S403": QCoreApplication.translate(
+        "Security",
+        "Consider possible security implications associated with '{0}'"
+        " module."),
+    "S404": QCoreApplication.translate(
+        "Security",
+        "Consider possible security implications associated with '{0}'"
+        " module."),
+    "S405": QCoreApplication.translate(
+        "Security",
+        "Using '{0}' to parse untrusted XML data is known to be vulnerable"
+        " to XML attacks. Replace '{0}' with the equivalent defusedxml"
+        " package, or make sure defusedxml.defuse_stdlib() is called."),
+    "S406": QCoreApplication.translate(
+        "Security",
+        "Using '{0}' to parse untrusted XML data is known to be vulnerable"
+        " to XML attacks. Replace '{0}' with the equivalent defusedxml"
+        " package, or make sure defusedxml.defuse_stdlib() is called."),
+    "S407": QCoreApplication.translate(
+        "Security",
+        "Using '{0}' to parse untrusted XML data is known to be vulnerable"
+        " to XML attacks. Replace '{0}' with the equivalent defusedxml"
+        " package, or make sure defusedxml.defuse_stdlib() is called."),
+    "S408": QCoreApplication.translate(
+        "Security",
+        "Using '{0}' to parse untrusted XML data is known to be vulnerable"
+        " to XML attacks. Replace '{0}' with the equivalent defusedxml"
+        " package, or make sure defusedxml.defuse_stdlib() is called."),
+    "S409": QCoreApplication.translate(
+        "Security",
+        "Using '{0}' to parse untrusted XML data is known to be vulnerable"
+        " to XML attacks. Replace '{0}' with the equivalent defusedxml"
+        " package, or make sure defusedxml.defuse_stdlib() is called."),
+    "S410": QCoreApplication.translate(
+        "Security",
+        "Using '{0}' to parse untrusted XML data is known to be vulnerable"
+        " to XML attacks. Replace '{0}' with the equivalent defusedxml"
+        " package."),
+    "S411": QCoreApplication.translate(
+        "Security",
+        "Using '{0}' to parse untrusted XML data is known to be vulnerable"
+        " to XML attacks. Use defused.xmlrpc.monkey_patch() function to"
+        " monkey-patch xmlrpclib and mitigate XML vulnerabilities."),
+    "S412": QCoreApplication.translate(
+        "Security",
+        "Consider possible security implications associated with '{0}'"
+        " module."),
+    "S413": QCoreApplication.translate(
+        "Security",
+        "The pyCrypto library and its module '{0}' are no longer actively"
+        " maintained and have been deprecated. Consider using"
+        " pyca/cryptography library."),
+    
+    # insecure certificate usage
+    "S501": QCoreApplication.translate(
+        "Security",
+        "Requests call with verify=False disabling SSL certificate checks,"
+        " security issue."),
+    
+    # YAML load
+    "S506": QCoreApplication.translate(
+        "Security",
+        "Use of unsafe yaml load. Allows instantiation of arbitrary objects."
+        " Consider yaml.safe_load()."),
+    
+    # Django SQL injection
+    "S610": QCoreApplication.translate(
+        "Security",
+        "Use of extra potential SQL attack vector."),
+    "S611": QCoreApplication.translate(
+        "Security",
+        "Use of RawSQL potential SQL attack vector."),
+    
+    # Django XSS vulnerability
+    "S703": QCoreApplication.translate(
+        "Security",
+        "Potential XSS on mark_safe() function."),
+    
+##    "S": QCoreApplication.translate(
+##        "Security",
+##        ""),
 }
 
-
-def getTranslatedMessage(messageCode, messageArgs):
-    """
-    Module function to get a translated and formatted message for a
-    given message ID.
+_securityMessagesSampleArgs = {
+    "S304": ["Crypto.Cipher.DES"],
+    "S305": ["cryptography.hazmat.primitives.ciphers.modes.ECB"],
+    "S313": ["xml.etree.cElementTree.parse"],
+    "S314": ["xml.etree.ElementTree.parse"],
+    "S315": ["xml.sax.expatreader.create_parser"],
+    "S316": ["xml.dom.expatbuilder.parse"],
+    "S317": ["xml.sax.parse"],
+    "S318": ["xml.dom.minidom.parse"],
+    "S319": ["xml.dom.pulldom.parse"],
+    "S320": ["lxml.etree.parse"],
     
-    @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)
+    "S403": ["pickle"],
+    "S404": ["subprocess"],
+    "S405": ["xml.etree.ElementTree"],
+    "S406": ["xml.sax"],
+    "S407": ["xml.dom.expatbuilder"],
+    "S408": ["xml.dom.minidom"],
+    "S409": ["xml.dom.pulldom"],
+    "S410": ["lxml"],
+    "S411": ["xmlrpclib"],
+    "S412": ["wsgiref.handlers.CGIHandler"],
+    "S413": ["Crypto.Cipher"],
+}
--- a/eric6/Plugins/CheckerPlugins/CodeStyleChecker/translations.py	Mon Jun 08 08:17:14 2020 +0200
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/translations.py	Mon Jun 08 20:08:27 2020 +0200
@@ -12,8 +12,11 @@
 
 from Globals import translate
 
-__all__ = ["getTranslatedMessage"]
+from .Security.translations import (
+    _securityMessages, _securityMessagesSampleArgs
+)
 
+# TODO: separate this huge dict into separate translations per checker type
 _messages = {
     ##################################################################
     ## pycodestyle messages
@@ -1077,8 +1080,18 @@
     "FIXWRITE_ERROR": ["IOError"],
 }
 
+messageCatalogs = (
+    _messages,
+    _securityMessages,
+)
 
-def getTranslatedMessage(messageCode, messageArgs):
+messageSampleArgsCatalog = (
+    _messages_sample_args,
+    _securityMessagesSampleArgs,
+)
+
+
+def getTranslatedMessage(messageCode, messageArgs, example=False):
     """
     Module function to get a translated and formatted message for a
     given message ID.
@@ -1088,24 +1101,52 @@
     @param messageArgs list of arguments or a single integer value to format
         the message
     @type list or int
+    @param example flag indicating a translated message filled with example
+        data is requested (messageArgs is ignored if given)
+    @type bool
     @return translated and formatted message
     @rtype str
     """
-    if messageCode.startswith("S"):
-        from .Security import translations as s_translations
-        return s_translations.getTranslatedMessage(messageCode, messageArgs)
+    if example:
+        for argsCatalog in messageSampleArgsCatalog:
+            if messageCode in argsCatalog:
+                args = argsCatalog[messageCode]
+                break
+        else:
+            args = None
+    else:
+        args = messageArgs
     
-    elif messageCode in _messages:
-        if isinstance(messageArgs, int):
-            # Retranslate with correct plural form
-            return _messages[messageCode](messageArgs)
+    for catalog in messageCatalogs:
+        if messageCode in catalog:
+            if args is None:
+                return catalog[messageCode]
+            elif isinstance(args, int):
+                # Retranslate with correct plural form
+                return catalog[messageCode](args)
+            else:
+                return catalog[messageCode].format(*args)
+    else:
+        if example:
+            return None
         else:
-            return _messages[messageCode].format(*messageArgs)
+            return QCoreApplication.translate(
+                "CodeStyleChecker",
+                "No message defined for code '{0}'."
+            ).format(messageCode)
+
+
+def getMessageCodes():
+    """
+    Module function to get a list of known message codes.
     
-    else:
-        return QCoreApplication.translate(
-            "CodeStyleFixer", " no message defined for code '{0}'"
-        ).format(messageCode)
+    @return list of known message codes
+    @rtype set of str
+    """
+    knownCodes = []
+    for catalog in messageCatalogs:
+        knownCodes += list(catalog.keys())
+    return {c.split(".", 1)[0] for c in knownCodes}
 
 #
 # eflag: noqa = M201
--- a/eric6/Plugins/PluginCodeStyleChecker.py	Mon Jun 08 08:17:14 2020 +0200
+++ b/eric6/Plugins/PluginCodeStyleChecker.py	Mon Jun 08 20:08:27 2020 +0200
@@ -7,8 +7,8 @@
 Module implementing the code style checker plug-in.
 """
 
-
 import os
+import textwrap
 
 from PyQt5.QtCore import QObject, pyqtSignal, QCoreApplication
 
@@ -84,6 +84,8 @@
         
         self.queuedBatches = []
         self.batchesFinished = True
+        
+        self.__wrapper = textwrap.TextWrapper(width=80)
     
     def __serviceError(self, fn, msg):
         """
@@ -246,7 +248,7 @@
                 msg += "\n" + QCoreApplication.translate(
                     'CodeStyleCheckerDialog', "Fix: {0}").format(trFixedMsg)
             
-            result["display"] = msg
+            result["display"] = "\n".join(self.__wrapper.wrap(msg))
         self.styleChecked.emit(fn, codeStyleCheckerStats, fixes, results)
 
     def activate(self):

eric ide

mercurial