# -*- coding: utf-8 -*-

# Copyright (c) 2020 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the security checker.
"""

import sys
import ast
import collections

from . import Checks
from .SecurityNodeVisitor import SecurityNodeVisitor


class SecurityChecker(object):
    """
    Class implementing a checker for security issues.
    """
    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",
        
        # Syntax error
        "S999",
    ]
    
    def __init__(self, source, filename, select, ignore, expected, repeat,
                 args):
        """
        Constructor
        
        @param source source code to be checked
        @type list of str
        @param filename name of the source file
        @type str
        @param select list of selected codes
        @type list of str
        @param ignore list of codes to be ignored
        @type list of str
        @param expected list of expected codes
        @type list of str
        @param repeat flag indicating to report each occurrence of a code
        @type bool
        @param args dictionary of arguments for the 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.__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 __reportInvalidSyntax(self):
        """
        Private method to report a syntax error.
        """
        exc_type, exc = sys.exc_info()[:2]
        if len(exc.args) > 1:
            offset = exc.args[1]
            if len(offset) > 2:
                offset = offset[1:3]
        else:
            offset = (1, 0)
        self.reportError(offset[0] - 1,
                         offset[1] or 0,
                         'S999',
                         "H",
                         "H",
                         exc_type.__name__, exc.args[0])
    
    def __generateTree(self):
        """
        Private method to generate an AST for our source.
        
        @return generated AST
        @rtype ast.AST
        """
        source = "".join(self.__source)
        return compile(source, self.__filename, 'exec', ast.PyCF_ONLY_AST)
    
    def getConfig(self):
        """
        Public method to get the configuration dictionary.
        
        @return dictionary containing the configuration
        @rtype dict
        """
        return self.__args
    
    def run(self):
        """
        Public method to check the given source against security related
        conditions.
        """
        if not self.__filename:
            # don't do anything, if essential data is missing
            return
        
        if not self.__checkers:
            # don't do anything, if no codes were selected
            return
        
        try:
            self.__tree = self.__generateTree()
        except (SyntaxError, TypeError):
            self.__reportInvalidSyntax()
            return
        
        securityNodeVisitor = SecurityNodeVisitor(
            self, self.__checkers, self.__filename)
        securityNodeVisitor.generic_visit(self.__tree)
