Documentation can be built now

pull/3/head
Thorsten Sick 3 years ago
parent b5fdb52fee
commit d25676032e

@ -0,0 +1,18 @@
# Makefile for standard actions
#
#
.PHONY: test init deinit shipit pylint
test: tox.ini
tox;
coverage html;
coverage report;
shipit: test
cd doc; make html; cd ..
python3 tools/shipit.py
# More detailed pylint tests.
pylint:
pylint --rcfile=pylint.rc *.py app/*.py plugins/base/*.py

@ -0,0 +1,151 @@
#!/usr/bin/env python3
""" Logger for the attack side. Output must be flexible, because we want to be able to feed it into many different processes. From ML to analysts """
import json
import datetime
# TODO: Collect caldera attacks: Source, target, type of attack. Start/Stop. Results. Parameters
# TODO: Collect kali attacks: Source, target, type of attack. Start/Stop. Results. Parameters. Settings
# TODO: Export data
# TODO: Add TTP and similar metadata
def __get_timestamp__():
return datetime.datetime.now().strftime("%H:%M:%S.%f")
def __mitre_fix_ttp__(ttp):
""" enforce some systematic naming scheme for MITRE TTPs """
if ttp is None:
return ""
if ttp.startswith("MITRE_"):
return ttp
else:
return "MITRE_" + ttp
class AttackLog():
""" A specific logger class to log the progress of the attack steps """
def __init__(self):
self.log = []
def start_caldera_attack(self, source, paw, group, ability_id, ttp=None, name=None, description=None): # pylint: disable=too-many-arguments
""" Mark the start of a caldera attack
@param source: source of the attack. Attack IP
@param paw: Caldera oaw of the targets being attacked
@param group: Caldera group of the targets being attacked
@param ability_id: Caldera ability id of the attack
@param ttp: TTP of the attack (as stated by Caldera internal settings)
@param name: Name of the attack. Data source is Caldera internal settings
@param description: Descirption of the attack. Caldera is the source
"""
data = {"timestamp": __get_timestamp__(),
"event": "start",
"type": "attack",
"sub-type": "caldera",
"source": source,
"target_paw": paw,
"target_group": group,
"ability_id": ability_id,
"hunting_tag": __mitre_fix_ttp__(ttp),
"name": name or "",
"description": description or ""
}
self.log.append(data)
# TODO: Add parameter
# TODO: Add config
# TODO: Add results
def stop_caldera_attack(self, source, paw, group, ability_id, ttp=None, name=None, description=None): # pylint: disable=too-many-arguments
""" Mark the end of a caldera attack
@param source: source of the attack. Attack IP
@param paw: Caldera oaw of the targets being attacked
@param group: Caldera group of the targets being attacked
@param ability_id: Caldera ability id of the attack
@param ttp: TTP of the attack (as stated by Caldera internal settings)
@param name: Name of the attack. Data source is Caldera internal settings
@param description: Descirption of the attack. Caldera is the source
"""
data = {"timestamp": __get_timestamp__(),
"event": "stop",
"type": "attack",
"sub-type": "caldera",
"source": source,
"target_paw": paw,
"target_group": group,
"ability_id": ability_id,
"hunting_tag": __mitre_fix_ttp__(ttp),
"name": name or "",
"description": description or ""
}
self.log.append(data)
def start_kali_attack(self, source, target, attack_name, ttp=None):
""" Mark the start of a Kali based attack
@param source: source of the attack. Attack IP
@param target: Target machine of the attack
@param attack_name: Name of the attack. From plugin
@param ttp: TTP of the attack. From plugin
"""
data = {"timestamp": __get_timestamp__(),
"event": "start",
"type": "attack",
"sub-type": "kali",
"source": source,
"target": target,
"kali_name": attack_name,
"hunting_tag": __mitre_fix_ttp__(ttp),
}
self.log.append(data)
# TODO: Add parameter
# TODO: Add config
# TODO: Add results
def stop_kali_attack(self, source, target, attack_name, ttp=None):
""" Mark the end of a Kali based attack
@param source: source of the attack. Attack IP
@param target: Target machine of the attack
@param attack_name: Name of the attack. From plugin
@param ttp: TTP of the attack. From plugin
"""
data = {"timestamp": __get_timestamp__(),
"event": "stop",
"type": "attack",
"sub-type": "kali",
"source": source,
"target": target,
"kali_name": attack_name,
"hunting_tag": __mitre_fix_ttp__(ttp),
}
self.log.append(data)
def write_json(self, filename):
""" Write the json data for this log
@param filename: Name of the json file
"""
with open(filename, "wt") as fh:
json.dump(self.get_dict(), fh)
def get_dict(self):
""" Return logged data in dict format """
return self.log

@ -0,0 +1,547 @@
#!/usr/bin/env python3
""" Remote control a caldera server """
import json
import os
import time
import requests
import simplejson
from app.exceptions import CalderaError
from app.interface_sfx import CommandlineColors
from app.attack_log import AttackLog
# TODO: Ability deserves an own class.
class CalderaControl():
""" Remote control Caldera through REST api """
def __init__(self, server, config=None, apikey=None):
"""
@param server: Caldera server url/ip
@param config: The configuration
"""
# print(server)
self.url = server if server.endswith("/") else server + "/"
self.config = config
if self.config:
self.apikey = self.config.caldera_apikey()
else:
self.apikey = apikey
def fetch_client(self, platform="windows", file="sandcat.go", target_dir=".", extension=""):
""" Downloads the appropriate Caldera client
@param platform: Platform to download the agent for
@param file: file to download from caldera. This defines the agent type
@param target_dir: directory to drop the new file into
@param extension: File extension to add to the downloaded file
"""
header = {"platform": platform,
"file": file}
fullurl = self.url + "file/download"
request = requests.get(fullurl, headers=header)
filename = request.headers["FILENAME"] + extension
open(os.path.join(target_dir, filename), "wb").write(request.content)
# print(r.headers)
return filename
def __contact_server__(self, payload, rest_path="api/rest", method="post"):
"""
@param payload: payload as dict to send to the server
@param rest_path: specific path for this rest api
@param method: http method to use
"""
url = self.url + rest_path
header = {"KEY": self.apikey,
"Content-Type": "application/json"}
if method.lower() == "post":
request = requests.post(url, headers=header, data=json.dumps(payload))
elif method.lower() == "put":
request = requests.put(url, headers=header, data=json.dumps(payload))
elif method.lower() == "get":
request = requests.get(url, headers=header, data=json.dumps(payload))
elif method.lower() == "delete":
request = requests.delete(url, headers=header, data=json.dumps(payload))
else:
raise ValueError
try:
res = request.json()
except simplejson.errors.JSONDecodeError as exception:
print("!!! Error !!!!")
print(payload)
print(request.text)
print("!!! Error !!!!")
raise exception
return res
# ############## List
def list_links(self, opid):
""" List links associated with an operation
@param opid: operation id to list links for
"""
payload = {"index": "link",
"op_id": opid}
return self.__contact_server__(payload)
def list_results(self, linkid):
""" List results for a link
@param linkid: ID of the link
"""
payload = {"index": "result",
"link_id": linkid}
return self.__contact_server__(payload)
def list_operations(self):
""" Return operations """
payload = {"index": "operations"}
return self.__contact_server__(payload)
def list_abilities(self):
""" Return all ablilities """
# curl -H 'KEY: ADMIN123' http://192.168.178.102:8888/api/rest -H 'Content-Type: application/json' -d '{"index":"abilities"}'
payload = {"index": "abilities"}
return self.__contact_server__(payload)
def list_agents(self):
""" List running agents
"""
# TODO: Add filters for specific platforms/executors : , platform_filter=None, executor_filter=None as parameters
# curl -H 'KEY: ADMIN123' http://192.168.178.102:8888/api/rest -H 'Content-Type: application/json' -d '{"index":"agents"}'
payload = {"index": "agents"}
agents = self.__contact_server__(payload)
return agents
def list_adversaries(self):
""" List registered adversaries """
# curl -H 'KEY: ADMIN123' http://192.168.178.102:8888/api/rest -H 'Content-Type: application/json' -d '{"index":"adversaries"}'
payload = {"index": "adversaries"}
return self.__contact_server__(payload)
def list_objectives(self):
""" List registered objectives """
# curl -H 'KEY: ADMIN123' http://192.168.178.102:8888/api/rest -H 'Content-Type: application/json' -d '{"index":"objectives"}'
payload = {"index": "objectives"}
return self.__contact_server__(payload)
# ######### Get one specific item
def get_operation(self, name):
""" Gets an operation by name
@param name: Name of the operation to look for
"""
for operation in self.list_operations():
if operation["name"] == name:
return operation
return None
def get_adversary(self, name):
""" Gets a specific adversary by name
@param name: Name to look for
"""
for adversary in self.list_adversaries():
if adversary["name"] == name:
return adversary
return None
def get_objective(self, name):
""" Returns an objective with a given name
@param name: Name to filter for
"""
for objective in self.list_objectives():
if objective["name"] == name:
return objective
return None
# ######### Get by id
def get_ability(self, abid):
"""" Return an ability by id
@param abid: Ability id
"""
res = []
for ability in self.list_abilities():
if ability["ability_id"] == abid:
res.append(ability)
return res
def get_operation_by_id(self, op_id):
""" Get operation by id
@param op_id: Operation id
"""
payload = {"index": "operations",
"id": op_id}
return self.__contact_server__(payload)
def get_result_by_id(self, linkid):
""" Get the result from a link id
@param linkid: link id
"""
payload = {"index": "result",
"link_id": linkid}
return self.__contact_server__(payload)
def get_linkid(self, op_id, paw, ability_id):
""" Get the id of a link identified by paw and ability_id
@param op_id: Operation id
@param paw: Paw of the agent
@param ability_id: Ability id to filter for
"""
operation = self.get_operation_by_id(op_id)
# print("Check for: {} {}".format(paw, ability_id))
for alink in operation[0]["chain"]:
# print("Lookup: PAW: {} Ability: {}".format(alink["paw"], alink["ability"]["ability_id"]))
# print("In: " + str(alink))
if alink["paw"] == paw and alink["ability"]["ability_id"] == ability_id:
return alink["id"]
return None
# ######### View
def view_operation_report(self, opid):
""" views the operation report
@param opid: Operation id to look for
"""
# let postData = selectedOperationId ? {'index':'operation_report', 'op_id': selectedOperationId, 'agent_output': Number(agentOutput)} : null;
# checking it (from snifffing protocol at the server): POST {'id': 539687}
payload = {"index": "operation_report",
"op_id": opid,
'agent_output': 1
}
return self.__contact_server__(payload)
def view_operation_output(self, opid, paw, ability_id):
""" Gets the output of an executed ability
@param opid: Id of the operation to look for
@param paw: Paw of the agent to look up
@param ability_id: if of the ability to extract the output from
"""
orep = self.view_operation_report(opid)
if paw not in orep["steps"]:
print("Broken operation report:")
print(orep)
print(f"Could not find {paw} in {orep['steps']}")
raise CalderaError
# print("oprep: " + str(orep))
for a_step in orep["steps"][paw]["steps"]:
if a_step["ability_id"] == ability_id:
try:
# TODO There is no output if the state is for example -4 (untrusted). Fix that. Why is the caldera implant untrusted ?
print("oprep: " + str(orep))
return a_step["output"]
except KeyError as exception:
raise CalderaError from exception
# print(f"Did not find ability {ability_id} in caldera operation output")
return None
# ######### Add
def add_operation(self, name, advid, group="red", state="running"):
""" Adds a new operation
@param name: Name of the operation
@param advid: Adversary id
@param group: agent group to attack
@param state: state to initially set
"""
# Add operation: curl -X PUT -H "KEY:$KEY" http://127.0.0.1:8888/api/rest -d '{"index":"operations","name":"testoperation1"}'
# observed from GUI sniffing: PUT {'name': 'schnuffel2', 'group': 'red', 'adversary_id': '0f4c3c67-845e-49a0-927e-90ed33c044e0', 'state': 'running', 'planner': 'atomic', 'autonomous': '1', 'obfuscator': 'plain-text', 'auto_close': '1', 'jitter': '4/8', 'source': 'Alice Filters', 'visibility': '50'}
payload = {"index": "operations",
"name": name,
"state": state,
"autonomous": 1,
'obfuscator': 'plain-text',
'auto_close': '1',
'jitter': '4/8',
'source': 'Alice Filters',
'visibility': '50',
"group": group,
#
"planner": "atomic",
"adversary_id": advid,
}
return self.__contact_server__(payload, method="put")
def add_adversary(self, name, ability, description="created automatically"):
""" Adds a new adversary
@param name: Name of the adversary
@param ability: One ability for this adversary
@param description: Description of this adversary
"""
# Add operation: curl -X PUT -H "KEY:$KEY" http://127.0.0.1:8888/api/rest -d '{"index":"operations","name":"testoperation1"}'
# Sniffed from gui:
# Rest core: PUT adversaries {'name': 'removeme', 'description': 'description', 'atomic_ordering': [{'id': 'bd527b63-9f9e-46e0-9816-b8434d2b8989'}], 'id': '558932cb-3ac6-43d2-b821-2db0fa8ad469', 'objective': ''}
# Returns: [{'name': 'removeme', 'adversary_id': '558932cb-3ac6-43d2-b821-2db0fa8ad469', 'description': 'description', 'tags': [], 'atomic_ordering': ['bd527b63-9f9e-46e0-9816-b8434d2b8989'], 'objective': '495a9828-cab1-44dd-a0ca-66e58177d8cc'}]
payload = {"index": "adversaries",
"name": name,
"description": description,
"atomic_ordering": [{"id": ability}],
#
"objective": '495a9828-cab1-44dd-a0ca-66e58177d8cc' # default objective
}
return self.__contact_server__(payload, method="put")
# ######### Execute
# TODO View the abilities a given agent could execute. curl -H "key:$API_KEY" -X POST localhost:8888/plugin/access/abilities -d '{"paw":"$PAW"}'
def execute_ability(self, paw, ability_id, obfuscator="plain-text"):
""" Executes an ability on a target. This happens outside of the scop of an operation. You will get no result of the ability back
@param paw: Paw of the target
@param ability_id: ability to execute
@param obfuscator: Obfuscator to use
"""
# curl -H "key:ADMIN123" -X POST localhost:8888/plugin/access/exploit -d '{"paw":"$PAW","ability_id":"$ABILITY_ID"}'```
# You can optionally POST an obfuscator and/or a facts dictionary with key/value pairs to fill in any variables the chosen ability requires.
# {"paw":"$PAW","ability_id":"$ABILITY_ID","obfuscator":"base64","facts":[{"trait":"username","value":"admin"},{"trait":"password", "value":"123"}]}
payload = {"paw": paw,
"ability_id": ability_id,
"obfuscator": obfuscator}
return self.__contact_server__(payload, rest_path="plugin/access/exploit_ex")
def execute_operation(self, operation_id, state="running"):
""" Executes an operation on a server
@param operation_id: The operation to modify
@param state: The state to set this operation into
"""
# TODO: Change state of an operation: curl -X POST -H "KEY:ADMIN123" http://localhost:8888/api/rest -d '{"index":"operation", "op_id":123, "state":"finished"}'
# curl -X POST -H "KEY:ADMIN123" http://localhost:8888/api/rest -d '{"index":"operation", "op_id":123, "state":"finished"}'
if state not in ["running", "finished", "paused", "run_one_link", "cleanup"]:
raise ValueError
payload = {"index": "operation",
"op_id": operation_id,
"state": state}
return self.__contact_server__(payload)
# ######### Delete
# TODO: Delete agent
# curl -X DELETE http://localhost:8888/api/rest -d '{"index":"operations","id":"$operation_id"}'
def delete_operation(self, opid):
""" Delete operation by id
@param opid: Operation id
"""
payload = {"index": "operations",
"id": opid}
return self.__contact_server__(payload, method="delete")
def delete_adversary(self, adid):
""" Delete adversary by id
@param adid: Adversary id
"""
payload = {"index": "adversaries",
"adversary_id": [{"adversary_id": adid}]}
return self.__contact_server__(payload, method="delete")
# ######### File access
# TODO: Get uploaded files
#
# Link, chain and stuff
def is_operation_finished(self, opid):
""" Checks if an operation finished - finished is not necessary successful !
@param opid: Operation id to check
"""
# An operation can run several Abilities vs several targets (agents). Each one is a link in the chain (see opperation report).
# Those links can have the states:
# return dict(HIGH_VIZ=-5,
# UNTRUSTED=-4,
# EXECUTE=-3,
# DISCARD=-2,
# PAUSE=-1)
# Plus: 0 as "finished"
#
operation = self.get_operation_by_id(opid)
# print(f"Operation data {operation}")
try:
print(operation[0]["state"])
if operation[0]["state"] == "finished":
return True
except KeyError as exception:
raise CalderaError from exception
except IndexError as exception:
raise CalderaError from exception
return False
# try:
# for alink in operation[0]["chain"]:
# if alink["status"] != 0:
# return False
# if alink["status"] == 0:
# return True
# except Exception as exception:
# raise CalderaError from exception
# return True
def is_operation_finished_multi(self, opid):
""" Checks if an operation finished - finished is not necessary successful ! On several targets.
All links (~ abilities) on all targets must have the status 0 for this to be True.
@param opid: Operation id to check
"""
# An operation can run several Abilities vs several targets (agents). Each one is a link in the chain (see opperation report).
# Those links can have the states:
# return dict(HIGH_VIZ=-5,
# UNTRUSTED=-4,
# EXECUTE=-3,
# DISCARD=-2,
# PAUSE=-1)
# Plus: 0 as "finished"
#
operation = self.get_operation_by_id(opid)
# print(f"Operation data {operation}")
try:
for host_group in operation[0]["host_group"]:
for alink in host_group["links"]:
if alink["status"] != 0:
return False
except Exception as exception:
raise CalderaError from exception
return True
# ######## All inclusive methods
def attack(self, attack_logger: AttackLog = None, paw="kickme", ability_id="bd527b63-9f9e-46e0-9816-b8434d2b8989", group="red"):
""" Attacks a system and returns results
@param attack_logger: An attack logger class to log attacks with
@param paw: Paw to attack
@param group: Group to attack. Paw must be in the group
@param ability_id: Ability to run against the target
"""
adversary_name = "generated_adv__" + str(time.time())
operation_name = "testoperation__" + str(time.time())
self.add_adversary(adversary_name, ability_id)
adid = self.get_adversary(adversary_name)["adversary_id"]
if attack_logger:
attack_logger.start_caldera_attack(source=self.url,
paw=paw, group=group,
ability_id=ability_id,
ttp=self.get_ability(ability_id)[0]["technique_id"],
name=self.get_ability(ability_id)[0]["name"],
description=self.get_ability(ability_id)[0]["description"])
# ##### Create / Run Operation
print(f"New adversary generated. ID: {adid}, ability: {ability_id} group: {group}")
self.add_operation(operation_name, advid=adid, group=group)
opid = self.get_operation(operation_name)["id"]
print("New operation created. OpID: " + str(opid))
self.execute_operation(opid)
retries = 20
print(f"{CommandlineColors.OKGREEN}Executed attack operation{CommandlineColors.ENDC}")
while not self.is_operation_finished(opid) and retries > 0:
print(".... waiting for Caldera to finish")
time.sleep(10)
retries -= 1
# TODO: Handle outout from several clients
retries = 0
output = None
while retries < 10:
try:
output = self.view_operation_output(opid, paw, ability_id)
except CalderaError:
retries += 1
time.sleep(10)
else:
break
if output is None:
output = str(self.get_operation_by_id(opid))
print(f"{CommandlineColors.FAIL}Failed getting operation data. We just have: {output} from get_operation_by_id{CommandlineColors.ENDC}")
else:
print("Output: " + str(output))
# ######## Cleanup
self.execute_operation(opid, "cleanup")
self.delete_adversary(adid)
self.delete_operation(opid)
if attack_logger:
attack_logger.stop_caldera_attack(source=self.url,
paw=paw,
group=group,
ability_id=ability_id,
ttp=self.get_ability(ability_id)[0]["technique_id"],
name=self.get_ability(ability_id)[0]["name"],
description=self.get_ability(ability_id)[0]["description"]
)
def pretty_print_ability(self, abi):
""" Pretty pritns an ability
@param abi: A ability dict
"""
print("""
ID: {technique_id}
Technique name: {technique_name}
Tactic: {tactic}
Name: {name}
ID: {ability_id}
Description: {description}
Platform: {platform}/{executor}
""".format(**abi))

@ -0,0 +1,213 @@
#!/usr/bin/env python3
""" Configuration loader for PurpleDome """
import yaml
from app.exceptions import ConfigurationError
# TODO: Add attack scripts (that will be CACAO in the future !) and plugin config
# So the config being read is distributed into several files and they will have different formats (yaml, CACAO)
# Currently it is a single file and YAML only.
# We want to be independent from file structure or number of config files
# TODO: Attack control also by config class. Used in experimentcontrol. Will change with scripts !
class MachineConfig():
""" Sub config for a specific machine"""
def __init__(self, machinedata):
""" Init machine control config
@param machinedata: dict containing machine data
"""
if machinedata is None:
raise ConfigurationError
self.raw_config = machinedata
self.verify()
def verify(self):
""" Verify essential data is present """
try:
self.vmname()
operating_system = self.os()
vmcontroller = self.vmcontroller()
except KeyError as exception:
raise ConfigurationError from exception
if operating_system not in ["linux", "windows"]:
raise ConfigurationError
# TODO: Verify with plugins
if vmcontroller not in ["vagrant", "running_vm"]:
raise ConfigurationError
def vmname(self):
""" Returns the vmname """
return self.raw_config["vm_name"]
def vmcontroller(self):
""" Returns the vm controller. lowercase """
return self.raw_config["vm_controller"]["type"].lower()
def vm_ip(self):
""" Return the configured ip/domain name (whatever is needed to reach the machine). Returns None if missing """
try:
return self.raw_config["vm_controller"]["ip"]
except KeyError:
return None
def os(self): # pylint: disable=invalid-name
""" returns the os. lowercase """
return self.raw_config["os"].lower()
def use_existing_machine(self):
""" Returns if we want to use the existing machine """
return self.raw_config.get("use_existing_machine", False)
def machinepath(self):
""" Returns the machine path. If not configured it will fall back to the vm_name """
return self.raw_config.get("machinepath", self.vmname())
def get_playground(self):
""" Returns the machine specific playground where all the implants and tools will be installed """
return self.raw_config.get("playground", None)
def caldera_paw(self):
""" Returns the paw (caldera id) of the machine """
return self.raw_config.get("paw", None)
def caldera_group(self):
""" Returns the group (caldera group id) of the machine """
return self.raw_config.get("group", None)
def ssh_keyfile(self):
""" Returns the configured SSH keyfile """
return self.raw_config.get("ssh_keyfile", None)
def ssh_user(self):
""" Returns configured ssh user or "vagrant" as default """
return self.raw_config.get("ssh_user", "vagrant")
def halt_needs_force(self):
""" Returns if halting the machine needs force False as default """
return self.raw_config.get("halt_needs_force", False)
def vagrantfilepath(self):
""" Vagrant specific config: The vagrant file path """
if "vagrantfilepath" not in self.raw_config["vm_controller"]:
raise ConfigurationError("Vagrantfilepath missing")
return self.raw_config["vm_controller"]["vagrantfilepath"]
def sensors(self):
""" Return a list of sensors configured for this machine """
if "sensors" in self.raw_config:
return self.raw_config["sensors"] or []
return []
def vulnerabilities(self):
""" Return a list of vulnerabilities configured for this machine """
if "vulnerabilities" in self.raw_config:
return self.raw_config["vulnerabilities"] or []
return []
def is_active(self):
""" Returns if this machine is set to active. Default is true """
return self.raw_config.get("active", True)
class ExperimentConfig():
""" Configuration class for a whole experiments """
def __init__(self, configfile):
""" Init the config, process the file
@param configfile: The configuration file to process
"""
self.raw_config = None
self._targets = []
self._attackers = []
self.load(configfile)
def load(self, configfile):
""" Loads the configuration file
@param configfile: The configuration file to process
"""
with open(configfile) as fh:
self.raw_config = yaml.safe_load(fh)
# Process targets
for target in self.raw_config["targets"]:
self._targets.append(MachineConfig(self.raw_config["targets"][target]))
# Process attackers
for attacker in self.raw_config["attackers"]:
self._attackers.append(MachineConfig(self.raw_config["attackers"][attacker]))
def targets(self) -> [MachineConfig]:
""" Return config for targets as MachineConfig objects """
return self._targets
def attackers(self) -> [MachineConfig]:
""" Return config for attackers as MachineConfig objects """
return self._attackers
def attacker(self, mid) -> MachineConfig:
""" Return config for attacker as MachineConfig objects
@param mid: id of the attacker, 0 is main attacker
"""
return self.attackers()[mid]
def caldera_apikey(self):
""" Returns the caldera apikey """
return self.raw_config["caldera"]["apikey"]
def loot_dir(self):
""" Returns the loot dir """
return self.raw_config["results"]["loot_dir"]
def kali_conf(self, attack):
""" Get kali config for a specific kali attack
@param attack: Name of the attack to look up config for
"""
try:
res = self.raw_config["kali_conf"][attack]
except KeyError as exception:
raise ConfigurationError from exception
return res
def get_nap_time(self):
""" Returns the attackers nap time between attack steps """
try:
return self.raw_config["attacks"]["nap_time"]
except KeyError:
return 0

@ -0,0 +1,22 @@
#!/usr/bin/env python3
""" A collection of shared exceptions """
class ServerError(Exception):
""" An elemental server is not running """
class ConfigurationError(Exception):
""" An elemental server is not running """
class PluginError(Exception):
""" Some plugin core function is broken """
class CalderaError(Exception):
""" Caldera is broken """
class NetworkError(Exception):
""" Network connection (like ssh) can not be established """

@ -0,0 +1,229 @@
#!/usr/bin/env python3
""" A class to control a whole experiment. From setting up the machines to running the attacks """
import glob
import os
import subprocess
import time
import zipfile
from datetime import datetime
from app.attack_log import AttackLog
from app.config import ExperimentConfig
from app.interface_sfx import CommandlineColors
from caldera_control import CalderaControl
from machine_control import Machine
# TODO: Multi threading at least when starting machines
class Experiment():
""" Class handling experiments """
def __init__(self, configfile):
"""
@param configfile: Path to the configfile to load """
self.attacker_1 = None
self.experiment_control = ExperimentConfig(configfile)
self.attack_logger = AttackLog()
self.__start_attacker()
self.starttime = datetime.now().strftime("%Y_%m_%d___%H_%M_%S")
self.lootdir = os.path.join(self.experiment_control.loot_dir(), self.starttime)
os.makedirs(self.lootdir)
self.targets = []
# start target machines
for target_conf in self.experiment_control.targets():
if not target_conf.is_active():
continue
tname = target_conf.vmname()
print(f"{CommandlineColors.OKBLUE}preparing target {tname} ....{CommandlineColors.ENDC}")
target_1 = Machine(target_conf)
target_1.set_caldera_server(self.attacker_1.getip())
try:
if not target_conf.use_existing_machine():
target_1.destroy()
except subprocess.CalledProcessError:
# Maybe the machine just does not exist yet
pass
target_1.install_caldera_service()
target_1.up()
# TODO prime sensors here
needs_reboot = target_1.prime_sensors()
if needs_reboot:
target_1.reboot()
print(f"{CommandlineColors.OKGREEN}Target is up: {tname} {CommandlineColors.ENDC}")
target_1.start_caldera_client()
print(f"{CommandlineColors.OKGREEN}Initial start of caldera client: {tname} {CommandlineColors.ENDC}")
self.targets.append(target_1)
# TODO: Install vulnerabilities by plugin
print(f"{CommandlineColors.OKBLUE}Contacting caldera agents on all targets ....{CommandlineColors.ENDC}")
time.sleep(20)
# Wait until all targets are registered as Caldera targets
for target_1 in self.targets:
caldera_url = "http://" + self.attacker_1.getip() + ":8888"
caldera_control = CalderaControl(caldera_url, config=self.experiment_control)
running_agents = [i["paw"] for i in caldera_control.list_agents()]
while target_1.get_paw() not in running_agents:
print(f"Connecting to caldera {caldera_url}, running agents are: {running_agents}")
print(f"Missing agent: {target_1.get_paw()} ...")
target_1.start_caldera_client()
print(f"Restarted caldera agent: {target_1.get_paw()} ...")
time.sleep(120) # Was 30, but maybe there are timing issues
running_agents = [i["paw"] for i in caldera_control.list_agents()]
print(f"{CommandlineColors.OKGREEN}Caldera agents reached{CommandlineColors.ENDC}")
# Install vulnerabilities
for a_target in self.targets:
print(f"Installing vulnerabilities on {a_target.get_paw()}")
a_target.install_vulnerabilities()
a_target.start_vulnerabilities()
# Install sensor plugins
for a_target in self.targets:
print(f"Installing sensors on {a_target.get_paw()}")
a_target.install_sensors()
a_target.start_sensors()
# Attack them
print(f"{CommandlineColors.OKBLUE}Running Caldera attacks{CommandlineColors.ENDC}")
for target_1 in self.targets:
# Run caldera attacks
caldera_attacks = self.experiment_control.raw_config["caldera_attacks"][target_1.get_os()]
if caldera_attacks:
for attack in caldera_attacks:
# TODO: Work with snapshots
# TODO: If we have several targets in the same group, it is nonsense to attack each one separately. Make this smarter
print(f"Attacking machine with PAW: {target_1.get_paw()}")
caldera_control = CalderaControl("http://" + self.attacker_1.getip() + ":8888", config=self.experiment_control)
caldera_control.attack(self.attack_logger, target_1.get_paw(), attack, target_1.get_group())
time.sleep(self.experiment_control.get_nap_time())
print(f"{CommandlineColors.OKGREEN}Finished Caldera attacks{CommandlineColors.ENDC}")
# Run Kali attacks
print(f"{CommandlineColors.OKBLUE}Running Kali attacks{CommandlineColors.ENDC}")
for target_1 in self.targets:
for attack in self.experiment_control.raw_config["kali_attacks"][target_1.get_os()]:
# TODO: Work with snapshots
self.attacker_1.kali_attack(attack, target_1.getip(), self.experiment_control)
time.sleep(self.experiment_control.get_nap_time())
print(f"{CommandlineColors.OKGREEN}Finished Kali attacks{CommandlineColors.ENDC}")
# Stop sensor plugins
# Collect data
for a_target in self.targets:
a_target.stop_sensors()
a_target.collect_sensors(self.lootdir)
# Uninstall vulnerabilities
for a_target in self.targets:
print(f"Uninstalling vulnerabilities on {a_target.get_paw()}")
a_target.stop_vulnerabilities()
# TODO: Zip result dir
# Stop target machines
for target_1 in self.targets:
target_1.halt()
self.__stop_attacker()
self.attack_logger.write_json(os.path.join(self.lootdir, "attack.json"))
self.zip_loot()
def zip_loot(self):
""" Zip the loot together """
filename = os.path.join(self.lootdir, self.starttime + ".zip")
globs = ["/**/*.json",
"/**/*.proto",
"/*/**/*.zip",
]
print(f"Creating zip file {filename}")
with zipfile.ZipFile(filename, "w") as zfh:
for a_glob in globs:
a_glob = self.lootdir + a_glob
for a_file in glob.iglob(a_glob, recursive=True):
if a_file != filename:
print(a_file)
zfh.write(a_file)
@staticmethod
def __get_results_files(root):
""" Yields a list of potential result files
@param root: Root dir of the machine to collect data from
"""
# TODO: Properly implement. Get proper root parameter
total = [os.path.join(root, "logstash", "filebeat.json")]
for a_file in total:
if os.path.exists(a_file):
yield a_file
def __clean_result_files(self, root):
""" Deletes result files
@param root: Root dir of the machine to collect data from
"""
# TODO: Properly implement. Get proper root parameter
for a_file in self.__get_results_files(root):
os.remove(a_file)
def __collect_loot(self, root):
""" Collect results into loot dir
@param root: Root dir of the machine to collect data from
"""
try:
os.makedirs(os.path.abspath(self.experiment_control.loot_dir()))
except FileExistsError:
pass
for a_file in self.__get_results_files(root):
print("Copy {} {}".format(a_file, os.path.abspath(self.experiment_control.loot_dir())))
def __start_attacker(self):
""" Start the attacking VM """
# Preparing attacker
self.attacker_1 = Machine(self.experiment_control.attacker(0).raw_config)
if not self.experiment_control.attacker(0).use_existing_machine():
try:
self.attacker_1.destroy()
except subprocess.CalledProcessError:
# Machine does not exist
pass
self.attacker_1.create(reboot=False)
self.attacker_1.up()
self.attacker_1.install_caldera_server(cleanup=False)
else:
self.attacker_1.up()
self.attacker_1.install_caldera_server(cleanup=False)
self.attacker_1.start_caldera_server()
self.attacker_1.set_attack_logger(self.attack_logger)
def __stop_attacker(self):
""" Stop the attacking VM """
self.attacker_1.halt()

@ -0,0 +1,25 @@
#!/usr/bin/env python3
""" Helper functions to improve the command line experiments """
# Colors to be used when printing text to the terminal
# print(f"{CommandlineColors.WARNING}Warning {CommandlineColors.ENDC}")
class CommandlineColors:
""" A collection of command line colors """
# pylint: disable=too-few-public-methods
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
ATTACK = '\033[93m' # An attack is running
MACHINE_CREATED = '\033[92m'
MACHINE_STOPPED = '\033[96m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'

@ -0,0 +1,563 @@
#!/usr/bin/env python3
""" (Virtual) machine handling. Start, stop, create and destroy. Starting remote commands on them. """
import os
import time
from glob import glob
import requests
import straight.plugin
from app.config import MachineConfig, ExperimentConfig
from app.exceptions import ServerError, ConfigurationError
from app.calderacontrol import CalderaControl
from app.interface_sfx import CommandlineColors
from plugins.base.kali import KaliPlugin
from plugins.base.machinery import MachineryPlugin
from plugins.base.sensor import SensorPlugin
from plugins.base.vulnerability_plugin import VulnerabilityPlugin
class Machine():
""" A virtual machine. Attacker or target. Abstracting stuff away. """
def __init__(self, config, calderakey="ADMIN123"):
"""
@param config: The machine configuration as dict
@param calderakey: Key to the caldera controller
"""
self.vm_manager = None
self.attack_logger = None
if isinstance(config, MachineConfig):
self.config = config
else:
self.config = MachineConfig(config)
# TODO: Read config from plugin
if self.config.vmcontroller() == "vagrant":
self.__parse_vagrant_config__()
if self.config.vmcontroller() == "running_vm":
self.__parse_running_vm_config__()
self.caldera_server = None
self.abs_machinepath_external = None
self.abs_machinepath_external = os.path.join(self.vagrantfilepath, self.config.machinepath())
# TODO Add internal machinepath path for within the VM (/vagrant/machinepath) for non-linux machines
self.abs_machinepath_internal = os.path.join("/vagrant/", self.config.machinepath())
if not os.path.exists(self.abs_machinepath_external):
raise ConfigurationError(f"machinepath does not exist: {self.abs_machinepath_external}")
self.load_machine_plugin()
self.caldera_basedir = self.vm_manager.get_playground()
self.calderakey = calderakey
self.sensors = [] # Sensor plugins
self.vulnerabilities = [] # Vulnerability plugins
def __parse_vagrant_config__(self):
""" Check if a file configured in the config is present """
self.vagrantfilepath = os.path.abspath(self.config.vagrantfilepath())
self.vagrantfile = os.path.join(self.vagrantfilepath, "Vagrantfile")
if not os.path.isfile(self.vagrantfile):
raise ConfigurationError(f"Vagrantfile not existing: {self.vagrantfile}")
def __parse_running_vm_config__(self):
""" Check if a file configured in the config is present """
self.vagrantfilepath = os.path.abspath(self.config.vagrantfilepath())
self.vagrantfile = os.path.join(self.vagrantfilepath, "Vagrantfile")
def get_paw(self):
""" Returns the paw of the current machine """
return self.config.caldera_paw()
def get_group(self):
""" Returns the group of the current machine """
return self.config.caldera_group()
def destroy(self):
""" Destroys the current machine """
self.vm_manager.__call_destroy__()
def create(self, reboot=True):
""" Create a VM
@param reboot: Reboot the VM during installation. Required if you want to install software
"""
self.vm_manager.__call_create__(reboot)
def reboot(self):
""" Reboot a machine """
if self.get_os() == "windows":
self.vm_manager.remote_run("shutdown /r")
self.vm_manager.__call_disconnect__()
time.sleep(60) # Shutdown can be slow....
if self.get_os() == "linux":
self.vm_manager.remote_run("reboot")
self.vm_manager.__call_disconnect__()
res = None
while not res:
time.sleep(5)
res = self.vm_manager.__call_connect__()
print("Re-connecting....")
def up(self): # pylint: disable=invalid-name
""" Starts a VM. Creates it if not already created """
self.vm_manager.__call_up__()
def halt(self):
""" Halts a VM """
self.vm_manager.__call_halt__()
def getuser(self):
""" Gets the user of the current VM """
return "Result " + str(self.vm_manager.__call_remote_run__("echo $USER"))
def connect(self):
""" command connection. establish it """
return self.vm_manager.__call_connect__()
def disconnect(self, connection):
""" Command connection dis-connect """
self.vm_manager.__call_disconnect__(connection)
def remote_run(self, cmd, disown=False):
""" Simplifies connect and run
@param cmd: Command to run as shell command
@param disown: run in background
"""
return self.vm_manager.__call_remote_run__(cmd, disown)
def kali_attack(self, attack, target, config: ExperimentConfig):
""" Pick a Kali attack and run it
@param attack: Name of the attack to run
@param target: IP address of the target
@param config: A full experiment config object that has the methog "kali_conf" (just in case I want to split the config later)
@returns: The output of the cmdline attacking tool
"""
def get_handlers(plugin) -> [KaliPlugin]:
return plugin.produce()
base = "plugins/**/*.py"
plugin_dirs = set()
for a_glob in glob(base, recursive=True):
plugin_dirs.add(os.path.dirname(a_glob))
for a_directory in plugin_dirs:
plugins = straight.plugin.load(a_directory, subclasses=KaliPlugin)
handlers = get_handlers(plugins)
for plugin in handlers:
name = plugin.get_name()
if name == attack:
print(f"{CommandlineColors.OKBLUE}Running Kali plugin {name}{CommandlineColors.ENDC}")
syscon = {"abs_machinepath_internal": self.abs_machinepath_internal,
"abs_machinepath_external": self.abs_machinepath_external}
plugin.set_sysconf(syscon)
plugin.set_machine_plugin(self.vm_manager)
plugin.__set_logger__(self.attack_logger)
plugin.__execute__([target], config.kali_conf(name))
def load_machine_plugin(self):
""" Loads the matching machine plugin """
def get_handlers(a_plugin) -> [MachineryPlugin]:
return a_plugin.produce()
base = "plugins/**/*.py"
plugin_dirs = set()
for a_glob in glob(base, recursive=True):
plugin_dirs.add(os.path.dirname(a_glob))
for a_dir in plugin_dirs:
plugins = straight.plugin.load(a_dir, subclasses=MachineryPlugin)
handlers = get_handlers(plugins)
for plugin in handlers:
name = plugin.get_name()
if name == self.config.vmcontroller():
print(f"{CommandlineColors.OKBLUE}Installing sensor: {name}{CommandlineColors.ENDC}")
syscon = {"abs_machinepath_internal": self.abs_machinepath_internal,
"abs_machinepath_external": self.abs_machinepath_external}
plugin.set_sysconf(syscon)
plugin.__call_process_config__(self.config)
self.vm_manager = plugin
break
def prime_sensors(self):
""" Prime sensors from plugins (hard core installs that could require a reboot)
A machine can have several sensors running. Those are defined in a list in the config. This primes the sensors
"""
def get_handlers(a_plugin) -> [SensorPlugin]:
return a_plugin.produce()
base = "plugins/**/*.py"
reboot = False
plugin_dirs = set()
for a_glob in glob(base, recursive=True):
plugin_dirs.add(os.path.dirname(a_glob))
for a_dir in plugin_dirs:
plugins = straight.plugin.load(a_dir, subclasses=SensorPlugin)
handlers = get_handlers(plugins)
for plugin in handlers:
name = plugin.get_name()
if name in self.config.sensors():
print(f"{CommandlineColors.OKBLUE}Priming sensor: {name}{CommandlineColors.ENDC}")
syscon = {"abs_machinepath_internal": self.abs_machinepath_internal,
"abs_machinepath_external": self.abs_machinepath_external,
"sensor_specific": self.config.raw_config.get(name, {})
}
plugin.set_sysconf(syscon)
plugin.set_machine_plugin(self.vm_manager)
plugin.setup()
reboot |= plugin.prime()
self.sensors.append(plugin)
print(f"{CommandlineColors.OKGREEN}Primed sensor: {name}{CommandlineColors.ENDC}")
return reboot
def install_sensors(self):
""" Install sensors from plugins
A machine can have several sensors running. Those are defined in a list in the config. This installs the sensors
"""
for plugin in self.get_sensors():
name = plugin.get_name()
print(f"{CommandlineColors.OKBLUE}Installing sensor: {name}{CommandlineColors.ENDC}")
syscon = {"abs_machinepath_internal": self.abs_machinepath_internal,
"abs_machinepath_external": self.abs_machinepath_external,
"sensor_specific": self.config.raw_config.get(name, {})}
plugin.set_sysconf(syscon)
plugin.set_machine_plugin(self.vm_manager)
plugin.setup()
plugin.install()
print(f"{CommandlineColors.OKGREEN}Installed sensor: {name}{CommandlineColors.ENDC}")
# self.sensors.append(plugin)
def get_sensors(self) -> [SensorPlugin]:
""" Returns a list of running sensors """
return self.sensors
def start_sensors(self):
""" Start sensors
A machine can have several sensors running. Those are defined in a list in the config. This starts the sensors
"""
for plugin in self.get_sensors():
print(f"{CommandlineColors.OKBLUE}Starting sensor: {plugin.get_name()}{CommandlineColors.ENDC}")
plugin.set_machine_plugin(self.vm_manager)
plugin.start()
print(f"{CommandlineColors.OKGREEN}Started sensor: {plugin.get_name()}{CommandlineColors.ENDC}")
def stop_sensors(self):
""" Stop sensors
A machine can have several sensors running. Those are defined in a list in the config. This stops the sensors
"""
for plugin in self.get_sensors():
print(f"{CommandlineColors.OKBLUE}Stopping sensor: {plugin.get_name()}{CommandlineColors.ENDC}")
plugin.set_machine_plugin(self.vm_manager)
plugin.stop()
print(f"{CommandlineColors.OKGREEN}Stopped sensor: {plugin.get_name()}{CommandlineColors.ENDC}")
def collect_sensors(self, lootdir):
""" Collect data from sensors
A machine can have several sensors running. Those are defined in a list in the config. This collects the data from the sensors
@param lootdir: Fresh created directory for loot
"""
machine_specific_path = os.path.join(lootdir, self.config.vmname())
os.mkdir(machine_specific_path)
for plugin in self.get_sensors():
print(f"{CommandlineColors.OKBLUE}Collecting sensor: {plugin.get_name()}{CommandlineColors.ENDC}")
plugin.set_machine_plugin(self.vm_manager)
plugin.__call_collect__(machine_specific_path)
print(f"{CommandlineColors.OKGREEN}Collected sensor: {plugin.get_name()}{CommandlineColors.ENDC}")
############
def install_vulnerabilities(self):
""" Install vulnerabilities from plugins: The machine is not yet modified ! For that call start_vulnerabilities next
A machine can have several vulnerabilities. Those are defined in a list in the config. This installs the vulnerabilities
"""
def get_handlers(a_plugin) -> [SensorPlugin]:
return a_plugin.produce()
base = "plugins/**/*.py"
plugin_dirs = set()
for a_glob in glob(base, recursive=True):
plugin_dirs.add(os.path.dirname(a_glob))
for a_dir in plugin_dirs:
plugins = straight.plugin.load(a_dir, subclasses=VulnerabilityPlugin)
handlers = get_handlers(plugins)
for plugin in handlers:
name = plugin.get_name()
print(f"Configured vulnerabilities: {self.config.vulnerabilities()}")
if name in self.config.vulnerabilities():
print(f"{CommandlineColors.OKBLUE}Installing vulnerability: {name}{CommandlineColors.ENDC}")
syscon = {"abs_machinepath_internal": self.abs_machinepath_internal,
"abs_machinepath_external": self.abs_machinepath_external}
plugin.set_sysconf(syscon)
plugin.set_machine_plugin(self.vm_manager)
plugin.setup()
plugin.install(self.vm_manager)
self.vulnerabilities.append(plugin)
def get_vulnerabilities(self) -> [SensorPlugin]:
""" Returns a list of installed vulnerabilities """
return self.vulnerabilities
def start_vulnerabilities(self):
""" Really install the vulnerabilities on the machine
A machine can have vulnerabilities installed. Those are defined in a list in the config. This starts the vulnerabilities
"""
for plugin in self.get_vulnerabilities():
print(f"{CommandlineColors.OKBLUE}Activating vulnerability: {plugin.get_name()}{CommandlineColors.ENDC}")
plugin.set_machine_plugin(self.vm_manager)
plugin.start()
def stop_vulnerabilities(self):
""" Un-install the vulnerabilities on the machine
A machine can have vulnerabilities installed. Those are defined in a list in the config. This stops the vulnerabilities
"""
for plugin in self.get_vulnerabilities():
print(f"{CommandlineColors.OKBLUE}Uninstalling vulnerability: {plugin.get_name()}{CommandlineColors.ENDC}")
plugin.set_machine_plugin(self.vm_manager)
plugin.stop()
############
def getip(self):
""" Returns the IP of the main ethernet interface of this machine """
# TODO: Create special code to extract windows IPs
# TODO: Find a smarter way to get the ip
return self.vm_manager.get_ip()
def install_caldera_server(self, cleanup=False, version="2.8.1"):
""" Installs the caldera server on the VM
@param cleanup: Remove the old caldera version. Slow but reduces side effects
@param version: Caldera version to use. Check Caldera git for potential branches to use
"""
# https://github.com/mitre/caldera.git
print(f"{CommandlineColors.OKBLUE}Installing Caldera server {CommandlineColors.ENDC}")
if cleanup:
cleanupcmd = "rm -rf caldera;"
else:
cleanupcmd = ""
cmd = f"cd {self.caldera_basedir}; {cleanupcmd} git clone https://github.com/mitre/caldera.git --recursive --branch {version}; cd caldera; pip3 install -r requirements.txt"
print(f"{CommandlineColors.OKGREEN}Caldera server installed {CommandlineColors.ENDC}")
res = self.vm_manager.__call_remote_run__(cmd)
return "Result installing caldera server " + str(res)
def wait_for_caldera_server(self, timeout=6):
""" Ping caldera server. return as soon as it is responding
@param timeout: timeout in seconds
"""
for i in range(timeout):
time.sleep(10)
caldera_url = "http://" + self.getip() + ":8888"
caldera_control = CalderaControl(caldera_url, apikey=self.calderakey)
print(f"{i} Trying to connect to {caldera_url} Caldera API")
try:
caldera_control.list_adversaries()
except requests.exceptions.ConnectionError:
pass
else:
print("Caldera: All systems nominal")
return True
raise ServerError
def start_caldera_server(self):
""" Start the caldera server on the VM. Required for an attacker VM """
# https://github.com/mitre/caldera.git
print(f"{CommandlineColors.OKBLUE}Starting Caldera server {CommandlineColors.ENDC}")
cmd = f"cd {self.caldera_basedir}; cd caldera ; nohup python3 server.py --insecure &"
self.vm_manager.__call_remote_run__(cmd, disown=True)
self.wait_for_caldera_server()
print(f"{CommandlineColors.OKGREEN}Caldera server started. Confirmed it is running. {CommandlineColors.ENDC}")
def create_start_caldera_client_cmd(self):
""" Creates a command to start the caldera client """
playground = self.vm_manager.get_playground()
if self.get_os() == "linux":
cmd = f"""
nohup {playground}/caldera_agent.sh start &
"""
elif self.get_os() == "windows":
if playground:
playground = playground + "\\" # Workaround for Windows: Can not set target dir for fabric-put in Windows. Only default (none=user) dir available.
else:
playground = ""
# playground = self.vm_manager.get_playground()
cmd = f"""
{playground}caldera_agent.bat
"""
return cmd
def start_caldera_client(self):
""" Install caldera client. Required on targets """
name = self.vm_manager.get_vm_name()
print(f"{CommandlineColors.OKBLUE}Starting Caldera client {name} {CommandlineColors.ENDC}")
if self.get_os() == "windows":
# TODO: Do not mount but use ssh to copy
url = "http://" + self.caldera_server + ":8888"
caldera_control = CalderaControl(url, apikey=self.calderakey)
caldera_control.fetch_client(platform="windows",
file="sandcat.go",
target_dir=self.abs_machinepath_external,
extension=".go")
dst = self.vm_manager.get_playground()
src = os.path.join(self.abs_machinepath_external, "caldera_agent.bat")
self.vm_manager.put(src, dst)
src = os.path.join(self.abs_machinepath_external, "splunkd.go") # sandcat.go local name
self.vm_manager.put(src, dst)
cmd = self.__install_caldera_service_cmd().strip()
print(cmd)
self.vm_manager.remote_run(cmd, disown=False)
if self.get_os() == "linux":
dst = self.vm_manager.get_playground()
src = os.path.join(self.abs_machinepath_external, "caldera_agent.sh")
self.vm_manager.put(src, dst)
cmd = self.create_start_caldera_client_cmd().strip()
print(cmd)
self.vm_manager.remote_run(cmd, disown=True)
print(f"{CommandlineColors.OKGREEN}Caldera client started {CommandlineColors.ENDC}")
def get_os(self):
""" Returns the OS of the machine """
return self.config.os()
def __install_caldera_service_cmd(self):
playground = self.vm_manager.get_playground()
if self.get_os() == "linux":
return f"""
#!/bin/bash
# Installs and runs the caldera agent
# TODO: Respect start/stop commands
cd {playground}
server="http://{self.caldera_server}:8888";
curl -s -X POST -H "file:sandcat.go" -H "platform:linux" $server/file/download > sandcat.go;
chmod +x sandcat.go;
nohup ./sandcat.go -server $server -group {self.config.caldera_group()} -v -paw {self.config.caldera_paw()} &
"""
if self.get_os() == "windows":
if playground: # Workaround for Windows: Can not set target dir for fabric-put in Windows. Only default (none=user) dir available.
playground = playground + "\\"
else:
playground = ""
url = "http://" + self.caldera_server + ":8888"
caldera_control = CalderaControl(url, apikey=self.calderakey)
filename = caldera_control.fetch_client(platform="windows",
file="sandcat.go",
target_dir=self.abs_machinepath_external,
extension=".go")
return f"""
START {playground}{filename} -server {url} -group {self.config.caldera_group()} -paw {self.config.caldera_paw()}
""".strip()
raise Exception # System type unknown
def install_caldera_service(self):
""" Install the caldera client as a service. For linux targets """
# print("DELETEME ! " + sys._getframe().f_code.co_name)
content = self.__install_caldera_service_cmd()
print(f"{CommandlineColors.OKBLUE}Installing Caldera service {CommandlineColors.ENDC}")
if self.get_os() == "linux":
filename = os.path.join(self.abs_machinepath_external, "caldera_agent.sh")
elif self.get_os() == "windows":
filename = os.path.join(self.abs_machinepath_external, "caldera_agent.bat")
with open(filename, "wt") as fh:
fh.write(content)
print(f"{CommandlineColors.OKGREEN}Installed Caldera service {CommandlineColors.ENDC}")
def set_caldera_server(self, server):
""" Set the local caldera server config """
self.caldera_server = server
def set_attack_logger(self, attack_logger):
""" Configure the attack logger for this server
@param attack_logger: The attack logger to set
"""
self.attack_logger = attack_logger

@ -0,0 +1,97 @@
#!/usr/bin/env python3
""" A command line tool to control a caldera server """
import argparse
from app.calderacontrol import CalderaControl
# https://caldera.readthedocs.io/en/latest/The-REST-API.html
# Arpgparse handling
def list_agents(calcontrol, arguments): # pylint: disable=unused-argument
""" Call list agents in caldera control
@param calcontrol: Connection to the caldera server
@param arguments: Parser command line arguments
"""
# TODO: calcontrol.list_agents(arguments)
pass # pylint: disable=unnecessary-pass
def list_abilities(calcontrol, arguments):
""" Call list abilities in caldera control
@param calcontrol: Connection to the caldera server
@param arguments: Parser command line arguments
"""
abilities = arguments.ability_ids
if arguments.all:
abilities = [aid["ability_id"] for aid in calcontrol.list_abilities()]
for aid in abilities:
for ability in calcontrol.get_ability(aid):
calcontrol.pretty_print_ability(ability)
def attack(calcontrol, arguments):
""" Calling attack
@param calcontrol: Connection to the caldera server
@param arguments: Parser command line arguments
"""
print("Running attack")
print(arguments.paw)
print(arguments.group)
print(arguments.ability_id)
calcontrol.attack(paw=arguments.paw, group=arguments.group, ability_id=arguments.ability_id)
def create_parser():
""" Creates the parser for the command line arguments"""
main_parser = argparse.ArgumentParser("Controls a Caldera server to attack other systems")
subparsers = main_parser.add_subparsers(help="sub-commands")
# Sub parser for attacks
parser_attack = subparsers.add_parser("attack", help="attack system")
parser_attack.set_defaults(func=attack)
parser_attack.add_argument("--paw", default="kickme", help="paw to attack and get specific results for")
parser_attack.add_argument("--group", default="red", help="target group to attack")
parser_attack.add_argument("--ability_id", default="bd527b63-9f9e-46e0-9816-b8434d2b8989",
help="The ability to use for the attack")
# Sub parser to list abilities
parser_abilities = subparsers.add_parser("abilities", help="abilities")
# parser_abilities.add_argument("--abilityid", default=None, help="Id of the ability to list")
parser_abilities.set_defaults(func=list_abilities)
parser_abilities.add_argument("--ability_ids", default=[], nargs="+",
help="The abilities to look up. One or more ids")
parser_abilities.add_argument("--all", default=False, action="store_true",
help="List all abilities")
# TODO: Add sub parser to list agents
parser_agents = subparsers.add_parser("agents", help="agents")
parser_agents.set_defaults(func=list_agents)
# For all parsers
main_parser.add_argument("--caldera_url", help="caldera url, including port", default="http://192.168.178.97:8888/")
main_parser.add_argument("--apikey", help="caldera api key", default="ADMIN123")
return main_parser
if __name__ == "__main__":
parser = create_parser()
args = parser.parse_args()
print(args.caldera_url)
caldera_control = CalderaControl(args.caldera_url, config=None, apikey=args.apikey)
print("Caldera Control ready")
str(args.func(caldera_control, args))

@ -0,0 +1,232 @@
###
# Caldera configuration
caldera:
###
# API key for caldera. See caldera configuration. Default is ADMIN123
apikey: ADMIN123
###
# Attacks configuration
attackers:
###
# Configuration for the first attacker. One should normally be enough
attacker:
###
# Defining VM controller settings for this machine
vm_controller:
###
# Type of the VM controller, Options are "vagrant"
type: vagrant
###
# # path where the vagrantfile is in
vagrantfilepath: systems
###
# Name of machine in Vagrantfile
vm_name: attacker
###
# machinepath is a path where the machine specific files and logs are stored. Relative to the Vagrantfile path
# and will be mounted internally as /vagrant/<name>
# If machinepath is not set PurpleDome will try "vm_name"
machinepath: attacker1
###
# OS of the VM guest. Options are so far "windows", "linux"
os: linux
###
# Do not destroy/create the machine: Set this to "yes".
use_existing_machine: yes
###
# List of targets
targets:
###
# Specific target
target1:
vm_controller:
type: vagrant
vagrantfilepath: systems
###
# simple switch if targets is used in attack simulation. Default is true. If set to false the machine will not be started
active: no
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
###
# The folder all the implants will be installed into
playground: /home/vagrant
# Sensors to run on this machine
sensors:
# - linux_idp
target2:
#root: systems/target1
vm_controller:
type: vagrant
vagrantfilepath: systems
###
# simple switch if targets is used in attack simulation. Default is true. If set to false the machine will not be started
active: yes
vm_name: target2
os: windows
paw: target2w
group: red
machinepath: target2w
# Do not destroy/create the machine: Set this to "yes".
use_existing_machine: yes
###
# Optional setting to activate force when halting the machine. Windows guests sometime get stuck
halt_needs_force: yes
###
# If SSH without vagrant support is used (Windows !) we need a user name (uppercase)
ssh_user: PURPLEDOME
###
# For non-vagrant ssh connections a ssh keyfile stored in the machinepath is required.
ssh_keyfile: id_rsa.3
###
# The folder all the implants will be installed into
# Windows can only use default playground at the moment !
# playground: C:\\Users\\PurpleDome
# Sensors to run on this machine
sensors:
- windows_idp
vulnerabilities:
- weak_user_passwords
- rdp_config_vul
# Ubuntu 20.10 (Groovy)
target3:
vm_controller:
type: vagrant
vagrantfilepath: systems
###
# simple switch if targets is used in attack simulation. Default is true. If set to false the machine will not be started
active: no
vm_name: target3
os: linux
###
# Targets need a unique PAW name for caldera
paw: target3
###
# Targets need to be in a group for caldera
group: red
machinepath: target3
# Do not destroy/create the machine: Set this to "yes".
use_existing_machine: no
###
# The folder all the implants will be installed into
playground: /home/vagrant
# Sensors to run on this machine
sensors:
- linux_idp
vulnerabilities:
- sshd_config_vul
- weak_user_passwords
###
# General sensor config config
sensors:
###
# Windows IDP plugin configuration
windows_idp:
###
# Name of the dll to use. Must match AV version
dll_name: aswidptestdll.dll
###
# Folder where the IDP tool is located
idp_tool_folder: C:\\capture
###
# General attack config
attacks:
###
# configure the seconds the system idles between the attacks. Makes it slower. But attack and defense logs will be simpler to match
nap_time: 5
###
# A list of caldera attacks to run against the targets.
caldera_attacks:
###
# Linux specific attacks. A list of caldera ability IDs
linux:
- "bd527b63-9f9e-46e0-9816-b8434d2b8989"
###
# Windows specific attacks. A list of caldera ability IDs
windows:
- "bd527b63-9f9e-46e0-9816-b8434d2b8989"
###
# Kali tool based attacks. Will result in kali commandline tools to be called. Currently supported are: "hydra"
kali_attacks:
###
# Linux specific attacks, a list
linux:
- hydra
- nmap
###
# Windows specific attacks, a list
windows:
- hydra
- nmap
###
# Configuration for the kali attack tools
kali_conf:
###
# Hydra configuration
hydra:
###
# A list of protocols to brute force against. Supported: "ssh"
protocols:
- ssh
- rdp
#- ftps
###
# A file containing potential user names
userfile: users.txt
###
# A file containing potential passwords
pwdfile: passwords.txt
nmap:
###
# Settings for the results being harvested
results:
###
# The directory the loot will be in
loot_dir: loot

@ -0,0 +1,40 @@
#!/usr/bin/env python3
""" The main tool to run experiments """
import argparse
from app.experimentcontrol import Experiment
def explain(args): # pylint: disable=unused-argument
""" Explain the tool"""
print("Please specify a command to execute. For a list see <help>")
def run(args):
""" Run experiments
@param args: arguments from the argparse parser
"""
Experiment(args.configfile)
def create_parser():
""" Creates the parser for the command line arguments"""
parser = argparse.ArgumentParser("Controls an experiment on the configured systems")
subparsers = parser.add_subparsers(help="sub-commands")
parser.set_defaults(func=explain)
# Sub parser for machine creation
parser_run = subparsers.add_parser("run", help="run experiments")
parser_run.set_defaults(func=run)
parser_run.add_argument("--configfile", default="experiment.yaml", help="Config file to create from")
return parser
if __name__ == "__main__":
arguments = create_parser().parse_args()
arguments.func(arguments)

@ -0,0 +1,8 @@
#!/bin/bash
# Init the system
sudo apt-get -y install python3-venv
python3 -m venv venv
source venv/bin/activate
pip3 install -r requirements.txt

@ -0,0 +1,91 @@
#!/usr/bin/env python3
""" Demo program to set up and control the machines """
import argparse
import yaml
from app.calderacontrol import CalderaControl
from app.machinecontrol import Machine
def create_machines(arguments):
"""
@param arguments: The arguments from argparse
"""
# TODO: Add argparse and make it flexible
with open(arguments.configfile) as fh:
config = yaml.safe_load(fh)
target_ = Machine(config["targets"]["target1"])
attacker_1 = Machine(config["attackers"]["attacker"])
print("Got them")
# TODO Automatically create all machines defined in config file
# attacker_1.destroy()
print("destroyed")
attacker_1.create(reboot=False)
print("Attacker up")
attacker_1.up()
print(attacker_1.install_caldera_server())
attacker_1.start_caldera_server()
print("Attacker done")
target_.destroy()
target_.set_caldera_server(attacker_1.getip())
target_.install_caldera_service()
target_.create()
print("Target up")
target_.up()
target_.start_caldera_client()
print("Target done")
print("Caldera server running at: http://{}:8888/".format(attacker_1.getip()))
# target_.install_caldera_client(attacker_1.getip(), "target1elf")
def download_caldera_client(arguments):
""" Downloads the caldera client
@param arguments: The arguments from argparse
"""
caldera_control = CalderaControl(args.ip, None)
caldera_control.fetch_client(platform=arguments.platform,
file=arguments.file,
target_dir=arguments.target_dir,
extension=".go")
def create_parser():
""" Creates the parser for the command line arguments"""
main_parser = argparse.ArgumentParser("Controls a Caldera server to attack other systems")
subparsers = main_parser.add_subparsers(help="sub-commands")
# Sub parser for machine creation
parser_create = subparsers.add_parser("create", help="create systems")
parser_create.set_defaults(func=create_machines)
parser_create.add_argument("--configfile", default="experiment.yaml", help="Config file to create from")
parser_download_caldera_client = subparsers.add_parser("fetch_client", help="download the caldera client")
parser_download_caldera_client.set_defaults(func=download_caldera_client)
parser_download_caldera_client.add_argument("--ip", default="192.168.178.189", help="Ip of Caldera to connect to")
parser_download_caldera_client.add_argument("--platform", default="windows", help="platform to download the client for")
parser_download_caldera_client.add_argument("--file", default="sandcat.go", help="The agent to download")
parser_download_caldera_client.add_argument("--target_dir", default=".", help="The target dir to download the file to")
return main_parser
if __name__ == "__main__":
parser = create_parser()
args = parser.parse_args()
args.func(args)

@ -0,0 +1,77 @@
#!/usr/bin/env python3
""" Base class for Kali plugins """
from plugins.base.plugin_base import BasePlugin
class KaliPlugin(BasePlugin):
""" Class to execute a command on a kali system targeting another system """
# Boilerplate
name = None
description = None
ttp = None
references = None
required_files = []
# TODO: parse results
def __init__(self):
super().__init__()
self.conf = {} # Plugin specific configuration
self.sysconf = {} # System configuration. common for all plugins
self.attack_logger = None
def teardown(self):
""" Cleanup afterwards """
pass # pylint: disable=unnecessary-pass
def run(self, targets, config):
""" Run the command
@param targets: A list of targets, ip addresses will do
@param config: dict with command specific configuration
"""
raise NotImplementedError
def __execute__(self, targets, config):
""" Execute the plugin. This is called by the code
@param targets: A list of targets, ip addresses will do
@param config: dict with command specific configuration
"""
self.attack_logger.start_kali_attack(self.machine_plugin.config.vmname(), targets, self.name, ttp=self.get_ttp())
self.setup()
res = self.run(targets, config)
self.teardown()
self.attack_logger.stop_kali_attack(self.machine_plugin.config.vmname(), targets, self.name, ttp=self.get_ttp())
return res
def command(self, targets, config):
""" Generate command
@param targets: A list of targets, ip addresses will do
@param config: dict with command specific configuration
"""
raise NotImplementedError
def __set_logger__(self, attack_logger):
""" Set the attack logger for this machine """
self.attack_logger = attack_logger
def get_ttp(self):
""" Returns the ttp of the plugin, please set in boilerplate """
if self.ttp:
return self.ttp
raise NotImplementedError
def get_references(self):
""" Returns the references of the plugin, please set in boilerplate """
if self.references:
return self.references
raise NotImplementedError

@ -0,0 +1,172 @@
#!/usr/bin/env python3
""" Base class for classes to control any kind of machine: vm, bare metal, cloudified """
from enum import Enum
from app.config import MachineConfig
from app.interface_sfx import CommandlineColors
from plugins.base.plugin_base import BasePlugin
class MachineStates(Enum):
""" Potential machine states """
# TODO: maybe move state handling functions in here like "is running", "is broken"
RUNNING = 1
NOT_CREATED = 2
POWEROFF = 3
ABORTED = 4
SAVED = 5
STOPPED = 6
FROZEN = 7
SHUTOFF = 8
class MachineryPlugin(BasePlugin):
""" Class to control virtual machines, vagrant, .... """
# Boilerplate
name = None
required_files = []
###############
# This is stuff you might want to implement
def __init__(self):
super().__init__()
self.connection = None # Connection
self.config = None
def process_config(self, config: MachineConfig):
""" Machine specific processing of configuration
@param config: configuration to do additional processing on
"""
raise NotImplementedError
def create(self, reboot=True):
""" Create a machine
@param reboot: Optionally reboot the machine after creation
"""
raise NotImplementedError
def up(self): # pylint: disable=invalid-name
""" Start a machine, create it if it does not exist """
raise NotImplementedError
def halt(self):
""" Halt a machine """
raise NotImplementedError
def destroy(self):
""" Destroy a machine """
raise NotImplementedError
def connect(self):
""" Connect to a machine """
raise NotImplementedError
def remote_run(self, cmd, disown=False):
""" Connects to the machine and runs a command there
@param cmd: command to run int he machine's shell
@param disown: Send the connection into background
"""
raise NotImplementedError
def disconnect(self):
""" Disconnect from a machine """
raise NotImplementedError
def put(self, src, dst):
""" Send a file to a machine
@param src: source dir
@param dst: destination
"""
raise NotImplementedError
def get(self, src, dst):
""" Get a file to a machine
@param src: source dir
@param dst: destination
"""
raise NotImplementedError
def is_running(self):
""" Returns if the machine is running """
return self.get_state() == MachineStates.RUNNING
def get_state(self):
""" Get detailed state of a machine """
raise NotImplementedError
def get_ip(self):
""" Return the IP of the machine. If there are several it should be the one accepting ssh or similar. If a resolver is running, a domain is also ok. """
raise NotImplementedError
def get_playground(self):
""" path where all the attack tools will be copied to on a client. Your specific machine plugin can overwrite it. """
return self.config.get_playground()
def get_vm_name(self):
""" Get the name of the machine """
return self.config.vmname()
###############
# This is the interface from the main code to the plugin system. Do not touch
def __call_halt__(self):
""" Wrapper around halt """
print(f"{CommandlineColors.OKBLUE}Stopping machine: {self.config.vmname()} {CommandlineColors.ENDC}")
self.halt()
print(f"{CommandlineColors.OKGREEN}Machine stopped: {self.config.vmname()}{CommandlineColors.ENDC}")
def __call_process_config__(self, config: MachineConfig):
""" Wrapper around process_config """
# print("===========> Processing config")
self.config = config
self.process_config(config)
def __call_remote_run__(self, cmd, disown=False):
""" Simplifies connect and run
@param cmd: Command to run as shell command
@param disown: run in background
"""
return self.remote_run(cmd, disown)
def __call_disconnect__(self):
""" Command connection dis-connect """
self.disconnect()
def __call_connect__(self):
""" command connection. establish it """
return self.connect()
def __call_up__(self):
""" Starts a VM. Creates it if not already created """
self.up()
def __call_create__(self, reboot=True):
""" Create a VM
@param reboot: Reboot the VM during installation. Required if you want to install software
"""
self.create(reboot)
def __call_destroy__(self):
""" Destroys the current machine """
self.destroy()

@ -0,0 +1,86 @@
#!/usr/bin/env python3
""" Base class for all plugin types """
import os
# from shutil import copy
class BasePlugin():
""" Base class for plugins """
required_files = None # a list of files shipped with the plugin to be installed
name = None # The name of the plugin
description = None # The description of this plugin
def __init__(self):
# self.machine = None
self.plugin_path = None
self.machine_plugin = None
self.sysconf = {}
def setup(self):
""" Prepare everything for the plugin """
for a_file in self.required_files:
src = os.path.join(os.path.dirname(self.plugin_path), a_file)
print(src)
self.copy_to_machine(src)
def set_machine_plugin(self, machine_plugin):
""" Set the machine plugin class to communicate with
@param machine_plugin: Machine plugin to communicate with
"""
self.machine_plugin = machine_plugin
def set_sysconf(self, config):
""" Set system config
@param config: A dict with system configuration relevant for all plugins
"""
self.sysconf["abs_machinepath_internal"] = config["abs_machinepath_internal"]
self.sysconf["abs_machinepath_external"] = config["abs_machinepath_external"]
def copy_to_machine(self, filename):
""" Copies a file shipped with the plugin to the machine share folder
@param filename: File from the plugin folder to copy to the machine share.
"""
self.machine_plugin.put(filename, self.machine_plugin.get_playground())
# plugin_folder = os.path.dirname(os.path.realpath(self.plugin_path))
# src = os.path.join(plugin_folder, filename)
# if os.path.commonprefix((os.path.realpath(src), plugin_folder)) != plugin_folder:
# raise PluginError
# copy(src, self.sysconf["abs_machinepath_external"])
def run_cmd(self, command, warn=True, disown=False):
""" Execute a command on the vm using the connection
@param command: Command to execute
@param disown: Run in background
"""
print(f" Plugin running command {command}")
res = self.machine_plugin.__call_remote_run__(command, disown=disown)
return res
def get_name(self):
""" Returns the name of the plugin, please set in boilerplate """
if self.name:
return self.name
raise NotImplementedError
def get_description(self):
""" Returns the description of the plugin, please set in boilerplate """
if self.description:
return self.description
raise NotImplementedError

@ -0,0 +1,102 @@
#!/usr/bin/env python3
""" A base plugin class for sensors. Anything installed on the target to collect system information and identify the attack """
import os
from plugins.base.plugin_base import BasePlugin
class SensorPlugin(BasePlugin):
""" A sensor will be running on the target machine and monitor attacks. To remote control those sensors
there are sensor plugins. This is the base class for them
"""
# Boilerplate
name = None
required_files = []
def __init__(self):
super().__init__() # pylint:disable=useless-super-delegation
self.debugit = False
# self.machine = None
def set_sysconf(self, config):
""" Set system config
@param config: A dict with system configuration relevant for all plugins
"""
super().set_sysconf(config)
self.sysconf["sensor_specific"] = config["sensor_specific"]
def prime(self):
""" prime sets hard core configs in the target. You can use it to call everything that permanently alters the OS by settings.
If your prime function returns True the machine will be rebooted after prime-ing it. This is very likely what you want. Only use prime if install is not sufficient.
"""
return False
def install_command(self):
""" Generate the install command. Put everything you need that does not require a reboot in here. If you want to hard core alter the OS of the target, use the prime method """
raise NotImplementedError
def install(self):
""" Install the sensor. Executed on the target. Take the sensor from the share and (maybe) copy it to its destination. Do some setup
"""
cmd = self.install_command()
if cmd:
self.machine_plugin.__call_remote_run__(cmd)
def start_command(self):
""" Generate the start command """
raise NotImplementedError
def start(self, disown=None):
""" Start the sensor. The connection to the client is disowned here. = Sent to background. This keeps the process running.
@param disown: Send async into background
"""
if disown is None:
disown = not self.debugit
cmd = self.start_command()
if cmd:
# self.run_cmd(cmd, disown=not self.debugit)
self.machine_plugin.__call_remote_run__(cmd, disown=disown)
def stop_command(self):
""" Generate the stop command """
raise NotImplementedError
def stop(self):
""" Stop the sensor """
cmd = self.stop_command()
if cmd:
# self.run_cmd(cmd)
self.machine_plugin.__call_remote_run__(cmd)
def __call_collect__(self, machine_path):
""" Generate the data collect command
@param machine_path: Machine specific path to collect data into
"""
path = os.path.join(machine_path, "sensors", self.name)
os.makedirs(path)
self.collect(path)
def collect_command(self, path):
""" Generate the data collect command
@param path: Path to put the data into
"""
def collect(self, path):
""" Collect data from sensor. Copy it from sensor collection dir on target OS to the share
@param path: The path to copy the data into
"""
raise NotImplementedError

@ -0,0 +1,58 @@
#!/usr/bin/env python3
""" This is a specific plugin type that installs a vulnerability into a VM. This can be a vulnerable application or a configuration setting """
from plugins.base.plugin_base import BasePlugin
class VulnerabilityPlugin(BasePlugin):
""" A plugin that installs a vulnerable application or does vulnerable configuration changes on the target VM
"""
# Boilerplate
name = None
description = None
ttp = None
references = None
required_files = []
def __init__(self):
super().__init__() # pylint:disable=useless-super-delegation
self.debugit = False
def install(self, machine_plugin=None):
""" This is setting up everything up to the point where the machine itself would be modified. But system
modification is done by start
@param machine_plugin: Optional: you can already set the machine to use
"""
if machine_plugin:
self.machine_plugin = machine_plugin
def start(self):
""" Modifying the target machine and add the vulnerability """
# It is ok if install is empty. But this function here is the core. So implement it !
raise NotImplementedError
def stop(self):
""" Modifying the target machine and remove the vulnerability """
# Must be implemented. If you want to leave a mess create an empty function and be honest :-)
raise NotImplementedError
def get_ttp(self):
""" Returns the ttp of the plugin, please set in boilerplate """
if self.ttp:
return self.ttp
raise NotImplementedError
def get_references(self):
""" Returns the references of the plugin, please set in boilerplate """
if self.references:
return self.references
raise NotImplementedError

@ -0,0 +1,13 @@
python-vagrant==0.5.15
fabric==2.6.0
requests==2.25.1
simplejson==3.17.2
tox==3.22.0
sphinx-argparse==0.2.5
sphinxcontrib-autoyaml==0.6.1
sphinx-pyreverse==0.0.13
coverage==5.4
PyYAML==5.4.1
straight.plugin==1.5.0
sphinxcontrib.asciinema==0.3.1
paramiko

@ -0,0 +1,13 @@
python-vagrant>=0.5.15
fabric>=2.6.0
requests>=2.25.1
simplejson>=3.17.2
tox>=3.22.0
sphinx-argparse>=0.2.5
sphinxcontrib-autoyaml>=0.6.1
sphinx-pyreverse>=0.0.13
coverage>=5.4
PyYAML>=5.4.1
straight.plugin>=1.5.0
sphinxcontrib.asciinema>=0.3.1
paramiko
Loading…
Cancel
Save