def has_no_password_protection(path: str) -> bool: """ Check if .jks files are password protected. :param path: path to check """ if not os.path.exists(path): show_unknown('Path does not exist', details=dict(path=path)) return False jks_with_password: list = [] jks_without_password: list = [] for full_path in full_paths_in_dir(path): if not full_path.endswith('.jks'): continue try: jks.KeyStore.load(full_path, '') except jks.util.KeystoreSignatureException: # has password jks_with_password.append(dict(path=full_path, sha256=get_sha256(full_path))) else: # has not password jks_without_password.append(dict(path=full_path, sha256=get_sha256(full_path))) if jks_without_password: show_open('JKS is/are not password protected', details=dict(jks_without_password=jks_without_password)) return True show_close('JKS is/are password protected', details=dict(jks_with_password=jks_with_password)) return False
def file_does_not_exist(code_file: str) -> bool: """ Check if the given file does'nt exist. :param code_file: Path to the file to be tested. """ if os.path.exists(code_file): show_close('File exists', details=dict(path=code_file, fingerprint=get_sha256(code_file))) return False show_open('File does not exist', details=dict(path=code_file, fingerprint=get_sha256(code_file))) return True
def _check_grammar_in_file_re(grammar: str, code_dest: str, lang_spec: dict) -> Dict[str, List[str]]: """ Check grammar in file. :param grammar: Pyparsing grammar against which file will be checked. :param code_dest: File or directory to check. :param lang_spec: Contains language-specific syntax elements, such as acceptable file extensions and comment delimiters. :param exclude: Exclude files or directories with given strings :return: Maps files to their found vulnerabilites. """ vulns = {} lines = [] lang_extensions = lang_spec.get('extensions') if lang_extensions: if _path_match_extension(code_dest, lang_extensions): lines = _get_match_lines_re(grammar, code_dest, lang_spec) else: lines = _get_match_lines_re(grammar, code_dest, lang_spec) if lines: vulns[code_dest] = { 'lines': str(lines)[1:-1], 'sha256': get_sha256(code_dest), } return vulns
def _path_contains_grammar(grammar: ParserElement, path: str) -> dict: """ Return a dict mapping the path to the lines where the grammar matched. :param grammar: Grammar to be searched for in path. :param path: Path to the destination file. """ with open(path, encoding='latin-1') as file_d: lines = file_d.read().splitlines() lines_length = tuple(map(lambda x: len(x) + 1, lines)) file_as_string = '\n'.join(lines) # Given scanString expands tabs to 'n' number of spaces # And we count tabs as '1' char width # And scanString reports the match column relative to the expanded version # When a file contains tabs # Then the line numbers will get an offset # Given we force to parse without expanding tabs grammar.parseWithTabs() # Then the line numbers are reported correctly matched_lines = [ _get_line_number(start, lines_length) for _, start, _ in grammar.scanString(file_as_string) ] if matched_lines: return { path: { 'lines': str(matched_lines)[1:-1], 'sha256': get_sha256(path), } } return {}
def _use_passwords(path: str, passwords: list) -> bool: """ Check if a JKS file has been protected by any of ``passwords``. :param path: path to check :param passwords: passwords to test """ if not os.path.exists(path): show_unknown('Path does not exist', details=dict(path=path)) return False opened_jks: list = [] closed_jks: list = [] passwords = ['', *(p for p in set(passwords))] for full_path in full_paths_in_dir(path): if not full_path.endswith('.jks'): continue success: bool = False for password in passwords: try: jks.KeyStore.load(full_path, password) except jks.util.KeystoreSignatureException: # wrong password continue else: # correct password success = True break if success: opened_jks.append(dict(path=full_path, password=password, sha256=get_sha256(full_path))) else: closed_jks.append(dict(path=full_path, sha256=get_sha256(full_path))) if opened_jks: show_open('JKS is/are protected by a password from the list', details=dict(opened_jks=opened_jks, tested_passwords=passwords)) return True show_close('JKS is/are protected by a password from the list', details=dict(closed_jks=closed_jks, tested_passwords=passwords)) return False
def has_if_without_else(java_dest: str, exclude: list = None) -> bool: r""" Check if all ``if``\ s have an ``else`` clause. See `REQ.161 <https://fluidattacks.com/web/rules/161/>`_. :param java_dest: Path to a Java source file or package. :param exclude: Paths that contains any string from this list are ignored. """ args = nestedExpr(opener='(', closer=')') if_ = Keyword('if') + args if_line = Optional('}') + if_ + Optional('{') try: conds = lang.check_grammar(if_line, java_dest, LANGUAGE_SPECS, exclude) except FileNotFoundError: show_unknown('File does not exist', details=dict(code_dest=java_dest)) return False else: if not conds: show_close('Code does not use "if" statements', details=dict(code_dest=java_dest)) return False block = nestedExpr(opener='{', closer='}') if_block = if_ + block else_if_block = Keyword('else') + Keyword('if') + args + block else_block = Keyword('else') + block cond_block = \ Suppress(if_block + ZeroOrMore(else_if_block)) + Optional(else_block) cond_block.ignore(javaStyleComment) cond_block.ignore(L_CHAR) cond_block.ignore(L_STRING) vulns = {} for code_file, val in conds.items(): vulns.update(lang.block_contains_empty_grammar(cond_block, code_file, val['lines'], _get_block)) if not vulns: show_close('Code has "if" with "else" clause', details=dict(file=java_dest, fingerprint=get_sha256(java_dest))) else: show_open('Code has "if" without "else" clause', details=dict(matched=vulns, total_vulns=len(vulns))) return True return False
def has_switch_without_default(csharp_dest: str, exclude: list = None) -> bool: r""" Check if all ``switch``\ es have a ``default`` clause. See `REQ.161 <https://fluidattacks.com/web/rules/161/>`_. See `CWE-478 <https://cwe.mitre.org/data/definitions/478.html>`_. :param csharp_dest: Path to a C# source file or package. :param exclude: Paths that contains any string from this list are ignored. """ switch = Keyword('switch') + nestedExpr(opener='(', closer=')') switch_line = Optional('}') + switch + Optional('{') result = False try: switches = lang.check_grammar(switch_line, csharp_dest, LANGUAGE_SPECS, exclude) except FileNotFoundError: show_unknown('File does not exist', details=dict(code_dest=csharp_dest)) return False else: if not switches: show_close('Code does not have switches', details=dict(code_dest=csharp_dest)) return False switch_block = Suppress(switch) + nestedExpr(opener='{', closer='}') switch_block.ignore(javaStyleComment) switch_block.ignore(L_CHAR) switch_block.ignore(L_STRING) vulns = {} for code_file, val in switches.items(): vulns.update( lang.block_contains_grammar(switch_block, code_file, val['lines'], _get_block, should_not_have=r'(?:default\s*:)')) if not vulns: show_close('Code has "switch" with "default" clause', details=dict(file=csharp_dest, fingerprint=get_sha256(csharp_dest))) else: show_open('Code does not have "switch" with "default" clause', details=dict(matched=vulns, total_vulns=len(vulns))) result = True return result
def block_contains_grammar(grammar: ParserElement, code_dest: str, lines: List[str], get_block_fn: Callable, should_have: str = '', should_not_have: str = '', search_for_empty: bool = False) -> List[str]: """ Check block grammar. :param grammar: Pyparsing grammar against which file will be checked. :param code_dest: Source code file to check. :param lines: List of starting lines. :param get_block_fn: Function that gives block code starting at line. :param should_have: A string to search for in the match results. :param should_not_have: A string to search for in the match results. """ vulns = {} vuln_lines = [] if should_have: should_have_re = re.compile(should_have, flags=re.M) if should_not_have: should_not_have_re = re.compile(should_not_have, flags=re.M) with open(code_dest, encoding='latin-1') as code_f: file_lines = code_f.read().splitlines() for line in map(int, lines.split(',')): txt = get_block_fn(file_lines, line) results = grammar.searchString(txt, maxMatches=1) results_str = str(results) is_vulnerable = not search_for_empty if _is_empty_result(results): is_vulnerable = search_for_empty elif should_have and should_have_re.search(results_str): is_vulnerable = search_for_empty elif should_not_have and should_not_have_re.search(results_str): is_vulnerable = search_for_empty if is_vulnerable: vuln_lines.append(line) if vuln_lines: vulns = { code_dest: { 'lines': str(vuln_lines)[1:-1], 'sha256': get_sha256(code_dest), } } return vulns
def has_ssl_disabled(apphostconf_dest: str, exclude: list = None) -> bool: """ Check if SSL is disabled in ``ApplicationHost.config``. Search for access tag in security section in an ``ApplicationHost.config`` source file or package. :param apphostconf_dest: Path to an ``ApplicationHost.config`` source file or package. :param exclude: Paths that contains any string from this list are ignored. """ tk_tag_s, _ = makeXMLTags('security') tk_access, _ = makeXMLTags('access') tag_no_comm = tk_access.ignore(htmlComment) tk_access_none = copy(tag_no_comm) tk_access_none.setParseAction(withAttribute(sslFlags='None')) result = False try: sec_tag = lang.check_grammar(tk_tag_s, apphostconf_dest, LANGUAGE_SPECS, exclude) if not sec_tag: show_unknown('Not files matched', details=dict(code_dest=apphostconf_dest)) return False except FileNotFoundError: show_unknown('File does not exist', details=dict(code_dest=apphostconf_dest)) return False access_tags = {} none_sslflags = {} for code_file, val in sec_tag.items(): access_tags.update( lang.block_contains_grammar(tk_access, code_file, val['lines'], _get_block)) none_sslflags.update( lang.block_contains_grammar(tk_access_none, code_file, val['lines'], _get_block)) if not access_tags or none_sslflags: show_open('SSL is disabled', details=dict( matched=access_tags if access_tags else none_sslflags)) result = True else: show_close('SSL is enabled', details=dict(file=apphostconf_dest, fingerprint=get_sha256(apphostconf_dest))) return result
def swallows_exceptions(py_dest: str, exclude: list = None) -> bool: """ Search for swallowed exceptions. Identifies ``except`` blocks that are either empty or only contain comments or the ``pass`` statement. :param py_dest: Path to a Python script or package. :param exclude: Paths that contains any string from this list are ignored. """ tk_except = CaselessKeyword('except') tk_word = Word(alphas) + Optional('.') tk_pass = Literal('pass') tk_exc_obj = tk_word + Optional(Literal('as') + tk_word) parser_exception = tk_except + \ Optional('(') + \ Optional(delimitedList(tk_exc_obj)) + \ Optional(')') + Literal(':') empty_exception = (Suppress(parser_exception) + tk_pass).ignore(pythonStyleComment) result = False try: matches = lang.check_grammar(parser_exception, py_dest, LANGUAGE_SPECS, exclude) if not matches: show_close('Code does not have excepts', details=dict(code_dest=py_dest)) return False except FileNotFoundError: show_unknown('File does not exist', details=dict(code_dest=py_dest)) return False vulns = {} for code_file, val in matches.items(): vulns.update( lang.block_contains_grammar(empty_exception, code_file, val['lines'], _get_block)) if not vulns: show_close('Code does not have empty "catches"', details=dict(file=py_dest, fingerprint=get_sha256(py_dest))) else: show_open('Code has empty "catches"', details=dict(matched=vulns, total_vulns=len(vulns))) result = True return result
def is_header_x_powered_by_present(webconf_dest: str, exclude: list = None) -> bool: """ Search for X-Powered-By headers in a Web.config source file or package. :param webconf_dest: Path to a Web.config source file or package. :param exclude: Paths that contains any string from this list are ignored. """ tk_tag_s, _ = makeXMLTags('customHeaders') tk_add_tag, _ = makeXMLTags('add') tk_clear_tag, _ = makeXMLTags('clear') tk_remove_tag, _ = makeXMLTags('remove') tk_remove_tag.setParseAction(withAttribute(name='X-Powered-By')) tk_child_tag = MatchFirst( [Suppress(tk_add_tag), Suppress(tk_clear_tag), tk_remove_tag]) result = False try: custom_headers = lang.check_grammar(tk_tag_s, webconf_dest, LANGUAGE_SPECS, exclude) if not custom_headers: show_unknown('Not files matched', details=dict(code_dest=webconf_dest)) return False except FileNotFoundError: show_unknown('File does not exist', details=dict(code_dest=webconf_dest)) return False tk_rem = Suppress(tk_tag_s) + OneOrMore(tk_child_tag) vulns = {} for code_file, val in custom_headers.items(): vulns.update( lang.block_contains_empty_grammar(tk_rem, code_file, val['lines'], _get_block)) if vulns: show_open('Header "X-Powered-By" is present', details=dict(matched=vulns, total_lines=len(custom_headers))) result = True else: show_close('Header "X-Powered-By" is not present', details=dict(file=webconf_dest, fingerprint=get_sha256(webconf_dest))) return result
def swallows_exceptions(csharp_dest: str, exclude: list = None) -> bool: """ Search for ``catch`` blocks that are empty or only have comments. See `REQ.161 <https://fluidattacks.com/web/rules/161/>`_. :param csharp_dest: Path to a C# source file or package. :param exclude: Paths that contains any string from this list are ignored. """ tk_catch = CaselessKeyword('catch') tk_word = Word(alphas) parser_catch = (Optional(Literal('}')) + tk_catch + Literal('(') + tk_word + Optional(Literal('(') + tk_word + Literal(')')) + Optional(tk_word) + Literal(')')) empty_catch = (Suppress(parser_catch) + nestedExpr(opener='{', closer='}')).ignore(cppStyleComment) result = False try: catches = lang.check_grammar(parser_catch, csharp_dest, LANGUAGE_SPECS, exclude) if not catches: show_close('Code does not have catches', details=dict(code_dest=csharp_dest)) return False except FileNotFoundError: show_unknown('File does not exist', details=dict(code_dest=csharp_dest)) return False vulns = {} for code_file, val in catches.items(): vulns.update( lang.block_contains_empty_grammar(empty_catch, code_file, val['lines'], _get_block_as_one_liner)) if not vulns: show_close('Code does not have empty catches', details=dict(file=csharp_dest, fingerprint=get_sha256(csharp_dest))) else: show_open('Code has empty catches', details=dict(matches=vulns, total_vulns=len(vulns))) result = True return result
def has_if_without_else(csharp_dest: str, exclude: list = None) -> bool: r""" Check if all ``if``\ s have an ``else`` clause. See `REQ.161 <https://fluidattacks.com/web/rules/161/>`_. :param csharp_dest: Path to a C# source file or package. :param exclude: Paths that contains any string from this list are ignored. """ tk_if = CaselessKeyword('if') tk_else = CaselessKeyword('else') block = nestedExpr(opener='{', closer='}') prsr_if = tk_if + nestedExpr() + block prsr_else = Suppress(tk_else) + (prsr_if | block) if_head = tk_if + nestedExpr() + Optional(Literal('{')) if_wout_else = (Suppress(prsr_if) + prsr_else).ignore(cppStyleComment) result = False try: conds = lang.check_grammar(if_head, csharp_dest, LANGUAGE_SPECS, exclude) if not conds: show_close('Code does not have conditionals', details=dict(code_dest=csharp_dest)) return False except FileNotFoundError: show_unknown('File does not exist', details=dict(code_dest=csharp_dest)) return False vulns = {} for code_file, val in conds.items(): vulns.update( lang.block_contains_empty_grammar(if_wout_else, code_file, val['lines'], _get_block_as_one_liner)) if not vulns: show_close('Code has "if" with "else" clauses', details=dict(file=csharp_dest, fingerprint=get_sha256(csharp_dest))) else: show_open('Code does not have "if" with "else" clauses', details=dict(matched=vulns, total_vulns=len(vulns))) result = True return result
def has_debug_enabled(webconf_dest: str, exclude: list = None) -> bool: """ Check if debug flag is enabled in Web.config. Search for debug tag in compilation section in a Web.config source file or package. :param webconf_dest: Path to a Web.config source file or package. :param exclude: Paths that contains any string from this list are ignored. """ tk_tag_s, _ = makeXMLTags('system.web') tk_compilation, _ = makeXMLTags('compilation') tag_no_comm = tk_compilation.ignore(htmlComment) tk_comp_debug = copy(tag_no_comm) tk_comp_debug.setParseAction(withAttribute(debug='true')) result = False try: sysweb_tag = lang.check_grammar(tk_tag_s, webconf_dest, LANGUAGE_SPECS, exclude) if not sysweb_tag: show_unknown('Not files matched', details=dict(code_dest=webconf_dest)) return False except FileNotFoundError: show_unknown('File does not exist', details=dict(code_dest=webconf_dest)) return False debug_tags = {} for code_file, val in sysweb_tag.items(): debug_tags.update( lang.block_contains_grammar(tk_comp_debug, code_file, val['lines'], _get_block)) if debug_tags: show_open('Debug is enabled', details=dict(matched=debug_tags, total_lines=len(sysweb_tag))) result = True else: show_close('Debug is disabled', details=dict(file=webconf_dest, fingerprint=get_sha256(webconf_dest))) return result
def swallows_exceptions(rpg_dest: str, exclude: list = None) -> bool: """ Search for on-error without code. See `REQ.075 <https://fluidattacks.com/web/rules/075>`_. :param rpg_dest: Path to a RPG source or directory. :param exclude: Paths that contains any string from this list are ignored. """ tk_on = CaselessKeyword('on') tk_error = CaselessKeyword('error') tk_code = Word(nums) tk_monitor = tk_on + Literal('-') + tk_error + Optional(tk_code) + \ Literal(';') tk_end_mon = CaselessKeyword('endmon') + Literal(';') prs_sw = (tk_monitor + tk_end_mon).ignore(cppStyleComment) result = False try: matches = lang.check_grammar(tk_monitor, rpg_dest, LANGUAGE_SPECS, exclude) if not matches: show_unknown('Code does not have error handling', details=dict(code_dest=rpg_dest)) return False except FileNotFoundError: show_unknown('File does not exist', details=dict(code_dest=rpg_dest)) return False vulns = {} for code_file, val in matches.items(): vulns.update( lang.block_contains_grammar(prs_sw, code_file, val['lines'], _get_block)) if vulns: show_open('Code swallows exceptions', details=dict(matched=vulns, total_vulns=len(vulns))) result = True else: show_close('Code does not swallow exceptions', details=dict(file=rpg_dest, fingerprint=get_sha256(rpg_dest))) return result
def not_custom_errors(webconf_dest: str, exclude: list = None) -> bool: """ Check if customErrors flag is set to off in Web.config. CWE-12: ASP.NET Misconfiguration: Missing Custom Error Page :param webconf_dest: Path to a Web.config source file or package. :param exclude: Paths that contains any string from this list are ignored. """ tk_tag_s, _ = makeXMLTags('system.web') tk_custom_errors, _ = makeXMLTags('customErrors') tag_no_comm = tk_custom_errors.ignore(htmlComment) tk_comp_custom_errors = copy(tag_no_comm) tk_comp_custom_errors.setParseAction(withAttribute(mode='Off')) result = False try: sysweb_tag = lang.check_grammar(tk_tag_s, webconf_dest, LANGUAGE_SPECS, exclude) if not sysweb_tag: show_unknown('Not files matched', details=dict(code_dest=webconf_dest)) return False except FileNotFoundError: show_unknown('File does not exist', details=dict(code_dest=webconf_dest)) return False vulns = {} for code_file, val in sysweb_tag.items(): vulns.update( lang.block_contains_grammar(tk_comp_custom_errors, code_file, val['lines'], _get_block)) if vulns: show_open('Custom errors are not enabled', details=dict(matches=vulns, total_lines=len(sysweb_tag))) result = True else: show_close('Custom errors are enabled', details=dict(file=webconf_dest, fingerprint=get_sha256(webconf_dest))) return result
def test_get_sha256(): """Test add_info.""" expected_sha256: str = \ 'e988f5d769a5fc3b32031fa46c75256f5c60647c0d958e1ca59816ba58643ecb' assert expected_sha256 == generic.get_sha256( 'test/static/format/jks/open/1.jks')