diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..78fa337 --- /dev/null +++ b/.gitignore @@ -0,0 +1,109 @@ +__pycache__ + +# Created by https://www.toptal.com/developers/gitignore/api/pycharm +# Edit at https://www.toptal.com/developers/gitignore?templates=pycharm + +### PyCharm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# End of https://www.toptal.com/developers/gitignore/api/pycharm \ No newline at end of file diff --git a/scan_output_parser/.idea/.gitignore b/scan_output_parser/.idea/.gitignore new file mode 100644 index 0000000..ef4e4c6 --- /dev/null +++ b/scan_output_parser/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/../../../../../:\Git-Repos\it-security-2\scan_output_parser\.idea/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/scan_output_parser/.idea/inspectionProfiles/profiles_settings.xml b/scan_output_parser/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/scan_output_parser/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/scan_output_parser/.idea/misc.xml b/scan_output_parser/.idea/misc.xml new file mode 100644 index 0000000..d56657a --- /dev/null +++ b/scan_output_parser/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/scan_output_parser/.idea/modules.xml b/scan_output_parser/.idea/modules.xml new file mode 100644 index 0000000..dff304b --- /dev/null +++ b/scan_output_parser/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/scan_output_parser/.idea/scan_output_parser.iml b/scan_output_parser/.idea/scan_output_parser.iml new file mode 100644 index 0000000..d0876a7 --- /dev/null +++ b/scan_output_parser/.idea/scan_output_parser.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/scan_output_parser/.idea/vcs.xml b/scan_output_parser/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/scan_output_parser/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/scan_output_parser/lynis.py b/scan_output_parser/lynis.py new file mode 100644 index 0000000..a4a98a4 --- /dev/null +++ b/scan_output_parser/lynis.py @@ -0,0 +1,108 @@ +import re + +LYNIS_REGEX = { + "green": r"\[1;32m", # more advanced "green": r"\[ .*\[1;32m([\w\d ]*).*", + "yellow": r"\[1;33m", + "red": r"\[1;31m", + "white": r"\[1;37m", + "heading": r".*\[1;33m([\w\d ,:-]*).*" +} +LYNIS_BLOCKS = { + "Boot and services", + "Kernel", + "Memory and Processes", + "Users, Groups and Authentication", + "Shells", + "File systems", + "USB Devices", + "Storage", + "NFS", + "Name services", + "Ports and packages", + "Networking", + "Printers and Spools", + "Software: e-mail and messaging", + "Software: firewalls", + "Software: webserver", + "SSH Support", + "Databases", + "PHP", + "Logging and files", + "Insecure services", + "Scheduled tasks", + "Accounting", + "Time and Synchronization", + "Cryptography", + "Security frameworks", + "Software: Malware", + "File Permissions", + "Home directories", + "Kernel Hardening", + "Hardening" +} + + +def lynis_get_base(path_to_log): + with open(path_to_log) as handle: + text = handle.read() + blocks = text.split("[+]") + interesting_blocks = {} + for block in blocks: + heading = re.findall(LYNIS_REGEX["heading"], block.splitlines()[0]) + if heading and heading[0] in LYNIS_BLOCKS: + block_text = "".join(block.splitlines()[2:]) + interesting_blocks[heading[0]] = { + "text": block_text, + "counts": { + "green": len(re.findall(LYNIS_REGEX['green'], block_text)), + "yellow": len(re.findall(LYNIS_REGEX['yellow'], block_text)), + "red": len(re.findall(LYNIS_REGEX['red'], block_text)), + "white": len(re.findall(LYNIS_REGEX['white'], block_text)), + } + } + interesting_blocks["GENERAL"] = {} + if warning_count := re.findall(r".*Warnings.* \((\d+)\)", text): + interesting_blocks["GENERAL"]["warningCount"] = int(warning_count[0]) + + if suggestion_count := re.findall(r".*Suggestions.* \((\d+)\)", text): + interesting_blocks["GENERAL"]["suggestionCount"] = int(suggestion_count[0]) + + if hardening_index := re.findall(r".*Hardening index.*m(\d+)", text): + interesting_blocks["GENERAL"]["hardeningIndex"] = int(hardening_index[0]) + + if tests_performed := re.findall(r".*Tests performed.*m(\d+)", text): + interesting_blocks["GENERAL"]["testsPerformed"] = int(tests_performed[0]) + + return interesting_blocks + + +def lynis_add_special_properties(lynis_base): + def find_prop(category, key, regex, as_int=False): + if category in lynis_base: + if match := re.findall(regex, lynis_base[category]["text"]): + lynis_base[category][key] = int(match[0]) if as_int else match[0] + + find_prop("Boot and services", "runningServices", r"found (\d+) running services", as_int=True) + find_prop("Boot and services", "enabledServices", r"found (\d+) enabled services", as_int=True) + find_prop("Kernel", "defaultRunLevel", r"RUNLEVEL (\d+)", as_int=True) + find_prop("Kernel", "activeModules", r"Found (\d+) active modules", as_int=True) + find_prop("Kernel", "kernelUpdate", r"Checking for available kernel update.*? \[ .*?m([\w ]*).*? \]") + find_prop("Shells", "numShells", r"Result: found (\d*) shells", as_int=True) + find_prop("Networking", "ipv6Enabled", r"Checking IPv6 configuration.*? \[ .*?m([\w ]*).*? \]") + find_prop("Time and Synchronization", "lastSync", r"Last time synchronization.*? \[ .*?m([\w ]*).*? \]") + find_prop("Security frameworks", "unconfinedProcesses", r"Found (\d+) unconfined processes") + + +def lynis_remove_text_from_blocks(lynis_base): + for block in lynis_base: + try: + del lynis_base[block]["text"] + except KeyError: + pass + + +def lynis_parse(path): + lynis_base = lynis_get_base(path) + lynis_add_special_properties(lynis_base) + lynis_remove_text_from_blocks(lynis_base) + return lynis_base diff --git a/scan_output_parser/main.py b/scan_output_parser/main.py new file mode 100644 index 0000000..8eb2a9a --- /dev/null +++ b/scan_output_parser/main.py @@ -0,0 +1,57 @@ +import glob +import os.path +import re +from dataclasses import dataclass +from typing import List + +from lynis import lynis_parse +from otseca import otseca_parse +from testssl import testssl_parse + +BASE_SCAN_PATH = os.path.join("..", "raw_scans") + + +@dataclass +class Result: + path: str + run_nr: int + result: dict + + +@dataclass +class Run: + id: int + platform: str + system: str + version: str + path: str + otseca_results: List[Result] + lynis_results: List[Result] + testssl_results: List[Result] + + +def main(): + list_of_all = [] + all_scans = glob.glob(os.path.join(BASE_SCAN_PATH, "*", "")) + for scan in all_scans: + findings = re.findall(r"(\d+)_(.*)_(.*)_(.*)", os.path.dirname(scan)) + findings = findings[0] + list_of_all.append( + Run(findings[0], findings[1], findings[2], findings[3], scan, [], [], []) + ) + for run in list_of_all: + for otseca_path in glob.glob(os.path.join(run.path, "otseca*", "report*")): + nr = re.findall(r"otseca-(\d+)", otseca_path)[0] + run.otseca_results.append(Result(otseca_path, nr, otseca_parse(otseca_path))) + for log_file in os.listdir(run.path): + path = os.path.join(run.path, log_file) + nr = re.findall(r"(\d+)", log_file)[0] + if "lynis-console-" in log_file: + run.lynis_results.append(Result(path, nr, lynis_parse(path))) + if "testssl-" in log_file: + run.testssl_results.append(Result(path, nr, testssl_parse(path))) + [print(run) for run in list_of_all] + + +if __name__ == '__main__': + main() diff --git a/scan_output_parser/otseca.py b/scan_output_parser/otseca.py new file mode 100644 index 0000000..383aaaa --- /dev/null +++ b/scan_output_parser/otseca.py @@ -0,0 +1,49 @@ +import os.path +import re + +OTSECA_FILES = { + "distro": "distro.all.log.html", + "kernel": "kernel.all.log.html", + "network": "network.all.log.html", + "permission": "permissions.all.log.html", + "services": "services.all.log.html", + "system": "system.all.log.html" +} + + +def otseca_box_counts(path_to_report): + counts = {} + for (name, curr_file) in OTSECA_FILES.items(): + curr_path = os.path.join(path_to_report, curr_file) + with open(curr_path, encoding="UTF-8") as handle: + text = handle.read() + counts[name] = { + "green": (len(re.findall("background-color: #1F9D55", text))), + "yellow": (len(re.findall("background-color: #F2D024", text))), + "red": (len(re.findall("background-color: #CC1F1A", text))), + "total": (len(re.findall("background-color:", text))) + } + return counts + + +def otseca_distro_info(path_to_report): + with open(os.path.join(path_to_report, OTSECA_FILES["distro"])) as handle: + text = handle.read() + + pkg_count = len(re.findall("ii {2}", text)) + upgrades_count = re.findall(r"(\d+) .*? (\d+) .*? (\d+) .*? (\d+) .*", text).pop() + + return { + "pkgCount": pkg_count, + "upgraded": upgrades_count[0], + "newlyInstalled": upgrades_count[1], + "remove": upgrades_count[2], + "notUpgraded": upgrades_count[3] + } + + +def otseca_parse(path): + return { + "boxes": otseca_box_counts(path), + "general": otseca_distro_info(path) + } diff --git a/scan_output_parser/testssl.py b/scan_output_parser/testssl.py new file mode 100644 index 0000000..7f1984d --- /dev/null +++ b/scan_output_parser/testssl.py @@ -0,0 +1,32 @@ +import re + + +def testssl_parse(path): + with open(path) as handle: + text = handle.read() + + testssl_overall = { + "443": {"open": False, "ssl": False}, + "21": {"open": False, "ssl": False}, + "465": {"open": False, "ssl": False}, + "587": {"open": False, "ssl": False}, + "110": {"open": False, "ssl": False}, + "995": {"open": False, "ssl": False}, + "993": {"open": False, "ssl": False}, + "5432": {"open": False, "ssl": False}, + "3306": {"open": False, "ssl": False} + } + + tests = text.split("## Scan started as: ") + for test in tests: + port = re.findall("Start .*? -->> 127\.0\.0\.1:(\d*) \(localhost\) <<--", test) + if not port: + continue + port = port[0] + if "Overall Grade" in test: + testssl_overall[port]["ssl"] = True + testssl_overall[port]["open"] = True + if "firewall" not in test: + testssl_overall[port]["open"] = True + continue + return testssl_overall