diff -r 3fc8dfeb6ebe -r b99e7fd55fd3 src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityChecker.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityChecker.py Thu Jul 07 11:23:56 2022 +0200 @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the security checker. +""" + +import copy +import collections + +from . import Checks +from .SecurityNodeVisitor import SecurityNodeVisitor + + +class SecurityChecker: + """ + Class implementing a checker for security issues. + """ + Codes = [ + # assert used + "S101", + + # exec used + "S102", + + # bad file permissions + "S103", + + # bind to all interfaces + "S104", + + # hardcoded passwords + "S105", "S106", "S107" + + # hardcoded tmp directory + "S108", + + # try-except + "S110", "S112", + + # 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", "S324", + + # hashlib.new + "S331", + + # insecure imports (blacklisted) + "S401", "S402", "S403", "S404", "S405", "S406", "S407", "S408", "S409", + "S410", "S411", "S412", "S413", + + # insecure certificate usage + "S501", + + # insecure SSL/TLS protocol version + "S502", "S503", "S504", + + # weak cryptographic keys + "S505", + + # YAML load + "S506", + + # SSH host key verification + "S507", + + # Shell injection + "S601", "S602", "S603", "S604", "S605", "S606", "S607", + + # SQL injection + "S608", + + # Wildcard injection + "S609", + + # Django SQL injection + "S610", "S611", + + # Jinja2 templates + "S701", + + # Mako templates + "S702", + + # Django XSS vulnerability + "S703", + + # hardcoded AWS passwords + "S801", "S802", + ] + + def __init__(self, source, filename, tree, select, ignore, expected, + repeat, args): + """ + Constructor + + @param source source code to be checked + @type list of str + @param filename name of the source file + @type str + @param tree AST tree of the source code + @type ast.Module + @param select list of selected codes + @type list of str + @param ignore list of codes to be ignored + @type list of str + @param expected list of expected codes + @type list of str + @param repeat flag indicating to report each occurrence of a code + @type bool + @param args dictionary of arguments for the security checks + @type dict + """ + self.__select = tuple(select) + self.__ignore = ('',) if select else tuple(ignore) + self.__expected = expected[:] + self.__repeat = repeat + self.__filename = filename + self.__source = source[:] + self.__tree = copy.deepcopy(tree) + self.__args = args + + # statistics counters + self.counters = {} + + # collection of detected errors + self.errors = [] + + checkersWithCodes = Checks.generateCheckersDict() + + self.__checkers = collections.defaultdict(list) + for checkType, checkersList in checkersWithCodes.items(): + for checker, codes in checkersList: + if any(not (code and self.__ignoreCode(code)) + for code in codes): + self.__checkers[checkType].append(checker) + + def __ignoreCode(self, code): + """ + Private method to check if the message code should be ignored. + + @param code message code to check for + @type str + @return flag indicating to ignore the given code + @rtype bool + """ + return (code.startswith(self.__ignore) and + not code.startswith(self.__select)) + + def reportError(self, lineNumber, offset, code, severity, confidence, + *args): + """ + Public method to record an issue. + + @param lineNumber line number of the issue + @type int + @param offset position within line of the issue + @type int + @param code message code + @type str + @param severity severity code (H = high, M = medium, L = low, + U = undefined) + @type str + @param confidence confidence code (H = high, M = medium, L = low, + U = undefined) + @type str + @param args arguments for the message + @type list + """ + if self.__ignoreCode(code): + return + + if code in self.counters: + self.counters[code] += 1 + else: + self.counters[code] = 1 + + # Don't care about expected codes + if code in self.__expected: + return + + if code and (self.counters[code] == 1 or self.__repeat): + # record the issue with one based line number + self.errors.append({ + "file": self.__filename, + "line": lineNumber + 1, + "offset": offset, + "code": code, + "args": args, + "severity": severity, + "confidence": confidence, + }) + + def getConfig(self): + """ + Public method to get the configuration dictionary. + + @return dictionary containing the configuration + @rtype dict + """ + return self.__args + + def run(self): + """ + Public method to check the given source against security related + conditions. + """ + if not self.__filename: + # don't do anything, if essential data is missing + return + + if not self.__checkers: + # don't do anything, if no codes were selected + return + + securityNodeVisitor = SecurityNodeVisitor( + self, self.__checkers, self.__filename) + securityNodeVisitor.generic_visit(self.__tree)