Merge pull request #28 from avast/caldera_4_ver2

Moving to Caldera 4 Alpha API
pull/29/head
Thorsten Sick 2 years ago committed by GitHub
commit 78eaa0cff8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,272 @@
#!/usr/bin/env python3
""" Direct API to the caldera server. Not abstract simplification methods. Compatible with Caldera 2.8.1 """
import json
import requests
import simplejson
class CalderaAPI:
""" API to Caldera 2.8.1 """
def __init__(self, server: str, attack_logger, config=None, apikey=None):
"""
@param server: Caldera server url/ip
@param attack_logger: The attack logger to use
@param config: The configuration
"""
# print(server)
self.url = server if server.endswith("/") else server + "/"
self.attack_logger = attack_logger
self.config = config
if self.config:
self.apikey = self.config.caldera_apikey()
else:
self.apikey = apikey
def __contact_server__(self, payload, rest_path: str = "api/rest", method: str = "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: # type: ignore
print("!!! Error !!!!")
print(payload)
print(request.text)
print("!!! Error !!!!")
raise exception
return res
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_sources(self):
""" List stored facts
"""
# 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": "sources"}
facts = self.__contact_server__(payload)
return facts
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)
def add_sources(self, name: str, parameters):
""" Adds a data source and seeds it with facts """
payload = {"index": "sources",
"name": name,
# "id": "123456-1234-1234-1234-12345678",
"rules": [],
"relationships": []
}
facts = []
if parameters is not None:
for key, value in parameters.items():
facts.append({"trait": key, "value": value})
# TODO: We need something better than a dict here as payload to have strong typing
payload["facts"] = facts # type: ignore
print(payload)
return self.__contact_server__(payload, method="put")
def add_operation(self, **kwargs):
""" 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
@param obfuscator: obfuscator to use for the attack
@param jitter: jitter to use for the attack
@param parameters: parameters to pass to the ability
"""
# name: str, advid: str, group: str = "red", state: str = "running", obfuscator: str = "plain-text", jitter: str = '4/8', parameters=None
name: str = kwargs.get("name")
advid: str = kwargs.get("adversary_id")
group: str = kwargs.get("group", "red")
state: str = kwargs.get("state", "running")
obfuscator: str = kwargs.get("obfuscator", "plain-text")
jitter: str = kwargs.get("jitter", "4/8")
parameters = kwargs.get("parameters", None)
# 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'}
sources_name = "source_" + name
self.add_sources(sources_name, parameters)
# To verify:
# print(self.get_source(sources_name))
payload = {"index": "operations",
"name": name,
"state": state,
"autonomous": 1,
'obfuscator': obfuscator,
'auto_close': '1',
'jitter': jitter,
'source': sources_name,
'visibility': '50',
"group": group,
#
"planner": "atomic",
"adversary_id": advid,
}
return self.__contact_server__(payload, method="put")
def view_operation_report(self, opid: str):
""" 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 set_operation_state(self, operation_id: str, state: str = "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)
def add_adversary(self, name: str, ability: str, description: str = "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
# "objective": ''
}
return self.__contact_server__(payload, method="put")
# curl -X DELETE http://localhost:8888/api/rest -d '{"index":"operations","id":"$operation_id"}'
def delete_operation(self, opid: str):
""" 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: str):
""" Delete adversary by id
@param adid: Adversary id
"""
payload = {"index": "adversaries",
"adversary_id": [{"adversary_id": adid}]}
return self.__contact_server__(payload, method="delete")
def delete_agent(self, paw: str):
""" Delete a specific agent from the kali db. implant may still be running and reconnect
@param paw: The Id of the agent to delete
"""
payload = {"index": "adversaries",
"paw": paw}
return self.__contact_server__(payload, method="delete")
def kill_agent(self, paw: str):
""" Send a message to an agent to kill itself
@param paw: The Id of the agent to delete
"""
payload = {"index": "agents",
"paw": paw,
"watchdog": 1,
"sleep_min": 3,
"sleep_max": 3}
return self.__contact_server__(payload, method="put")

@ -0,0 +1,710 @@
#!/usr/bin/env python3
""" Remote control a caldera 4 server. Starting compatible to the old control 2.8 calderacontrol. Maybe it will stop being compatible if refactoring is an option """
import json
from pprint import pformat
from typing import Optional, Union
import requests
import simplejson
from pydantic.dataclasses import dataclass
from pydantic import conlist # pylint: disable=no-name-in-module
# from app.exceptions import CalderaError
# from app.interface_sfx import CommandlineColors
# TODO: Ability deserves an own class.
# TODO: Support all Caldera agents: "Sandcat (GoLang)","Elasticat (Blue Python/ Elasticsearch)","Manx (Reverse Shell TCP)","Ragdoll (Python/HTML)"
@dataclass
class Variation:
description: str
command: str
@dataclass
class ParserConfig:
source: str
edge: str
target: str
custom_parser_vals: dict # undocumented ! Needs improvement ! TODO
@dataclass
class Parser:
module: str
relationships: list[ParserConfig] # undocumented ! Needs improvement ! TODO
parserconfigs: Optional[list[ParserConfig]] = None
@dataclass
class Requirement:
module: str
relationship_match: list[dict]
@dataclass
class AdditionalInfo:
additionalProp1: Optional[str] = None # pylint: disable=invalid-name
additionalProp2: Optional[str] = None # pylint: disable=invalid-name
additionalProp3: Optional[str] = None # pylint: disable=invalid-name
@dataclass
class Executor:
build_target: Optional[str] # Why can this be None ?
language: Optional[str] # Why can this be None ?
payloads: list[str]
variations: list[Variation]
additional_info: Optional[AdditionalInfo]
parsers: list[Parser]
cleanup: list[str]
name: str
timeout: int
code: Optional[str] # Why can this be None ?
uploads: list[str]
platform: str
command: Optional[str]
def get(self, akey, default=None):
""" Get a specific element out of the internal data representation, behaves like the well know 'get' """
if akey in self.__dict__:
return self.__dict__[akey]
return default
@dataclass
class Ability:
""" An ability is an exploit, a TTP, an attack step ...more or less... """
description: str
plugin: str
technique_name: str
requirements: list[Requirement]
additional_info: AdditionalInfo
singleton: bool
buckets: list[str]
access: dict
executors: list[Executor]
name: str
technique_id: str
tactic: str
repeatable: str
ability_id: str
privilege: Optional[str] = None
def get(self, akey, default=None):
""" Get a specific element out of the internal data representation, behaves like the well know 'get' """
if akey in self.__dict__:
return self.__dict__[akey]
return default
@dataclass
class AbilityList:
""" A list of exploits """
abilities: conlist(Ability, min_items=1)
def get_data(self):
return self.abilities
@dataclass
class Obfuscator:
""" An obfuscator hides the attack by encryption/encoding """
description: str
name: str
module: Optional[str] = None # Documentation error !!!
@dataclass
class ObfuscatorList:
""" A list of obfuscators """
obfuscators: conlist(Obfuscator, min_items=1)
def get_data(self):
return self.obfuscators
@dataclass
class Adversary:
""" An adversary is a defined attacker """
has_repeatable_abilities: bool
adversary_id: str
description: str
name: str
atomic_ordering: list[str]
objective: str
tags: list[str]
plugin: Optional[str] = None
def get(self, akey, default=None):
""" Get a specific element out of the internal data representation, behaves like the well know 'get' """
if akey in self.__dict__:
return self.__dict__[akey]
return default
@dataclass
class AdversaryList:
""" A list of adversary """
adversaries: conlist(Adversary, min_items=1)
def get_data(self):
return self.adversaries
@dataclass
class Fact:
unique: str
name: str
score: int
limit_count: int
relationships: list[str]
source: str
trait: str
links: list[str]
created: str
origin_type: Optional[str] = None
value: Optional[str] = None
technique_id: Optional[str] = None
collected_by: Optional[str] = None
def get(self, akey, default=None):
""" Get a specific element out of the internal data representation, behaves like the well know 'get' """
if akey in self.__dict__:
return self.__dict__[akey]
return default
@dataclass
class Relationship:
target: Fact
unique: str
score: int
edge: str
origin: str
source: Fact
@dataclass
class Visibility:
score: int
adjustments: list[int]
@dataclass
class Link:
pin: int
ability: Ability
paw: str
status: int
finish: str
decide: str
output: str
visibility: Visibility
pid: str
host: str
executor: Executor
unique: str
score: int
used: list[Fact]
facts: list[Fact]
agent_reported_time: str
id: str # pylint: disable=invalid-name
collect: str
command: str
cleanup: int
relationships: list[Relationship]
jitter: int
deadman: bool
@dataclass
class Agent:
""" A representation of an agent on the target (agent = implant) """
paw: str
location: str
platform: str
last_seen: str # Error in document
host_ip_addrs: list[str]
group: str
architecture: str
pid: int
server: str
trusted: bool
username: str
host: str
ppid: int
created: str
links: list[Link]
sleep_max: int
exe_name: str
display_name: str
sleep_min: int
contact: str
deadman_enabled: bool
proxy_receivers: AdditionalInfo
origin_link_id: str
executors: list[str]
watchdog: int
proxy_chain: list[list[str]]
available_contacts: list[str]
upstream_dest: str
pending_contact: str
privilege: Optional[str] = None # Error, not documented
def get(self, akey, default=None):
""" Get a specific element out of the internal data representation, behaves like the well know 'get' """
if akey in self.__dict__:
return self.__dict__[akey]
return default
@dataclass
class AgentList:
""" A list of agents """
agents: list[Agent]
def get_data(self):
return self.agents
@dataclass
class Rule:
match: str
trait: str
action: Optional[str] = None
@dataclass
class Adjustment:
offset: int
trait: str
value: str
ability_id: str
@dataclass
class Source:
name: str
plugin: str
facts: list[Fact]
rules: list[Rule]
relationships: list[Relationship]
id: str # pylint: disable=invalid-name
adjustments: Optional[list[Adjustment]] = None
def get(self, akey, default=None):
""" Get a specific element out of the internal data representation, behaves like the well know 'get' """
if akey in self.__dict__:
return self.__dict__[akey]
return default
@dataclass
class SourceList:
sources: list[Source]
def get_data(self):
return self.sources
@dataclass
class Planner:
""" A logic defining the order in which attack steps are executed """
name: str
plugin: str
id: str # pylint: disable=invalid-name
stopping_conditions: list[Fact]
params: dict
description: str
allow_repeatable_abilities: bool
module: Optional[str] = None
ignore_enforcement_module: Optional[list[str]] = None
ignore_enforcement_modules: Optional[list[str]] = None # Maybe error in Caldera 4 ?
@dataclass
class PlannerList:
planners: list[Planner]
def get_data(self):
return self.planners
@dataclass
class Goal:
target: str
count: int
achieved: bool
operator: str
value: str
@dataclass
class Objective:
percentage: int
name: str
goals: list[Goal]
description: str
id: str # pylint: disable=invalid-name
def get(self, akey, default=None):
""" Get a specific element out of the internal data representation, behaves like the well know 'get' """
if akey in self.__dict__:
return self.__dict__[akey]
return default
@dataclass
class Operation:
""" An attack operation collecting all the relevant items (obfuscator, adversary, planner) """
obfuscator: str
state: str
jitter: str
autonomous: int
name: str
source: Source
adversary: Adversary
objective: Union[Objective, str] # Maybe Error in caldera 4: Creating a Operation returns a objective ID, not an objective object
host_group: list[Agent]
start: str
group: str
use_learning_parsers: bool
planner: Planner
visibility: int
id: str # pylint: disable=invalid-name
auto_close: bool
chain: Optional[list] = None
def get(self, akey, default=None):
""" Get a specific element out of the internal data representation, behaves like the well know 'get' """
if akey in self.__dict__:
return self.__dict__[akey]
return default
@dataclass
class OperationList:
operations: conlist(Operation)
def get_data(self):
return self.operations
@dataclass
class ObjectiveList:
objectives: conlist(Objective)
def get_data(self):
return self.objectives
class CalderaAPI():
""" Remote control Caldera through REST api """
def __init__(self, server: str, attack_logger, config=None, apikey=None):
"""
@param server: Caldera server url/ip
@param attack_logger: The attack logger to use
@param config: The configuration
"""
self.url = server if server.endswith("/") else server + "/"
self.attack_logger = attack_logger
self.config = config
if self.config:
self.apikey = self.config.caldera_apikey()
else:
self.apikey = apikey
def __contact_server__(self, payload, rest_path: str = "api/v2/abilities", method: str = "get"):
"""
@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": "ADMIN123",
"accept": "application/json",
"Content-Type": "application/json"}
if method.lower() == "post":
j = json.dumps(payload)
request = requests.post(url, headers=header, data=j)
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() == "head":
request = requests.head(url, headers=header, data=json.dumps(payload))
elif method.lower() == "delete":
request = requests.delete(url, headers=header, data=json.dumps(payload))
elif method.lower() == "patch":
request = requests.patch(url, headers=header, data=json.dumps(payload))
else:
raise ValueError
try:
if request.status_code == 200:
res = request.json()
# Comment: Sometimes we get a 204: succcess, but not content in response
elif request.status_code == 204:
res = {"result": "ok",
"http_status_code": 204}
else:
print(f"Status code: {request.status_code} {request.json()}")
res = request.json()
except simplejson.errors.JSONDecodeError as exception: # type: ignore
print("!!! Error !!!!")
print(payload)
print(request.text)
print("!!! Error !!!!")
raise exception
return res
def list_abilities(self):
""" Return all ablilities """
payload = None
data = {"abilities": self.__contact_server__(payload, method="get", rest_path="api/v2/abilities")}
abilities = AbilityList(**data)
return abilities.get_data()
def list_obfuscators(self):
""" Return all obfuscators """
payload = None
data = {"obfuscators": self.__contact_server__(payload, method="get", rest_path="api/v2/obfuscators")}
obfuscators = ObfuscatorList(**data)
return obfuscators.get_data()
def list_adversaries(self):
""" Return all adversaries """
payload = None
data = {"adversaries": self.__contact_server__(payload, method="get", rest_path="api/v2/adversaries")}
adversaries = AdversaryList(**data)
return adversaries.get_data()
def list_sources(self):
""" Return all sources """
payload = None
data = {"sources": self.__contact_server__(payload, method="get", rest_path="api/v2/sources")}
sources = SourceList(**data)
return sources.get_data()
def list_planners(self):
""" Return all planners """
payload = None
data = {"planners": self.__contact_server__(payload, method="get", rest_path="api/v2/planners")}
planners = PlannerList(**data)
return planners.get_data()
def list_operations(self):
""" Return all operations """
payload = None
data = {"operations": self.__contact_server__(payload, method="get", rest_path="api/v2/operations")}
operations = OperationList(**data)
return operations.get_data()
def set_operation_state(self, operation_id: str, state: str = "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 = {"state": state}
return self.__contact_server__(payload, method="patch", rest_path=f"api/v2/operations/{operation_id}")
def list_agents(self):
""" Return all agents """
payload = None
data = {"agents": self.__contact_server__(payload, method="get", rest_path="api/v2/agents")}
agents = AgentList(**data)
return agents.get_data()
def list_objectives(self):
""" Return all objectivs """
payload = None
data = {"objectives": self.__contact_server__(payload, method="get", rest_path="api/v2/objectives")}
objectives = ObjectiveList(**data)
return objectives.get_data()
def add_adversary(self, name: str, ability: str, description: str = "created automatically"):
""" Adds a new adversary
:param name: Name of the adversary
:param ability: Ability ID to add
:param description: Human readable description
:return:
"""
payload = {
# "adversary_id": "string",
"atomic_ordering": [
ability
],
"name": name,
# "plugin": "string",
"objective": '495a9828-cab1-44dd-a0ca-66e58177d8cc', # default objective
# "tags": [
# "string"
# ],
"description": description
}
data = {"agents": self.__contact_server__(payload, method="post", rest_path="api/v2/adversaries")}
# agents = AgentList(**data)
return data
def delete_adversary(self, adversary_id: str):
""" Deletes an adversary
:param adversary_id: The id of this adversary
:return:
"""
payload = None
data = {"agents": self.__contact_server__(payload, method="delete", rest_path=f"api/v2/adversaries/{adversary_id}")}
return data
def delete_agent(self, agent_paw: str):
""" Deletes an agent
:param agent_paw: the paw to delete
:return:
"""
payload = None
data = {"agents": self.__contact_server__(payload, method="delete", rest_path=f"api/v2/agents/{agent_paw}")}
return data
def kill_agent(self, agent_paw: str):
""" Kills an agent on the target
:param agent_paw: The paw identifying this agent
:return:
"""
payload = {"watchdog": 1,
"sleep_min": 3,
"sleep_max": 3}
data = self.__contact_server__(payload, method="patch", rest_path=f"api/v2/agents/{agent_paw}")
return data
def add_operation(self, **kwargs):
""" Adds a new operation
:param kwargs:
:return:
"""
# name, adversary_id, source_id = "basic", planner_id = "atomic", group = "", state: str = "running", obfuscator: str = "plain-text", jitter: str = '4/8'
name: str = kwargs.get("name")
adversary_id: str = kwargs.get("adversary_id")
source_id: str = kwargs.get("source_id", "basic")
planner_id: str = kwargs.get("planner_id", "atomic")
group: str = kwargs.get("group", "")
state: str = kwargs.get("state", "running")
obfuscator: str = kwargs.get("obfuscator", "plain-text")
jitter: str = kwargs.get("jitter", "4/8")
payload = {"name": name,
"group": group,
"adversary": {"adversary_id": adversary_id},
"auto_close": False,
"state": state,
"autonomous": 1,
"planner": {"id": planner_id},
"source": {"id": source_id},
"use_learning_parsers": True,
"obfuscator": obfuscator,
"jitter": jitter,
"visibility": "51"}
data = {"operations": [self.__contact_server__(payload, method="post", rest_path="api/v2/operations")]}
operations = OperationList(**data)
return operations
def delete_operation(self, operation_id):
""" Deletes an operation
:param operation_id: The Id of the operation to delete
:return:
"""
payload = {}
data = self.__contact_server__(payload, method="delete", rest_path=f"api/v2/operations/{operation_id}")
return data
def view_operation_report(self, operation_id):
""" Views the report of a finished operation
:param operation_id: The id of this operation
:return:
"""
payload = {
"enable_agent_output": True
}
data = self.__contact_server__(payload, method="post", rest_path=f"api/v2/operations/{operation_id}/report")
return data
def get_ability(self, abid: str):
"""" Return an ability by id
@param abid: Ability id
"""
res = []
print(f"Number of abilities: {len(self.list_abilities())}")
with open("debug_removeme.txt", "wt") as fh:
fh.write(pformat(self.list_abilities()))
for ability in self.list_abilities()["abilities"]:
if ability.get("ability_id", None) == abid or ability.get("auto_generated_guid", None) == abid:
res.append(ability)
return res
def pretty_print_ability(self, abi):
""" Pretty pritns an ability
@param abi: A ability dict
"""
print("""
TTP: {technique_id}
Technique name: {technique_name}
Tactic: {tactic}
Name: {name}
ID: {ability_id}
Description: {description}
""".format(**abi))

@ -2,43 +2,26 @@
""" Remote control a caldera server """
import json
import os
import time
from pprint import pprint, pformat
from typing import Optional
import requests
import simplejson
from app.exceptions import CalderaError
from app.interface_sfx import CommandlineColors
# from app.calderaapi_2 import CalderaAPI
from app.calderaapi_4 import CalderaAPI
# TODO: Ability deserves an own class.
# TODO: Support all Caldera agents: "Sandcat (GoLang)","Elasticat (Blue Python/ Elasticsearch)","Manx (Reverse Shell TCP)","Ragdoll (Python/HTML)"
class CalderaControl():
class CalderaControl(CalderaAPI):
""" Remote control Caldera through REST api """
def __init__(self, server: str, attack_logger, config=None, apikey=None):
"""
@param server: Caldera server url/ip
@param attack_logger: The attack logger to use
@param config: The configuration
"""
# print(server)
self.url = server if server.endswith("/") else server + "/"
self.attack_logger = attack_logger
self.config = config
if self.config:
self.apikey = self.config.caldera_apikey()
else:
self.apikey = apikey
def fetch_client(self, platform: str = "windows", file: str = "sandcat.go", target_dir: str = ".", extension: str = ""):
""" Downloads the appropriate Caldera client
@ -57,98 +40,11 @@ class CalderaControl():
# print(r.headers)
return filename
def __contact_server__(self, payload, rest_path: str = "api/rest", method: str = "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: # type: ignore
print("!!! Error !!!!")
print(payload)
print(request.text)
print("!!! Error !!!!")
raise exception
return res
# ############## List
def list_links(self, opid: str):
""" 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: str):
""" 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_sources(self):
""" List stored facts
"""
# 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": "sources"}
facts = self.__contact_server__(payload)
return facts
def list_sources_for_name(self, name: str):
""" List facts in a source pool with a specific name """
for i in self.list_sources():
if i["name"] == name:
if i.get("name") == name:
return i
return None
@ -164,31 +60,19 @@ class CalderaControl():
return {}
res = {}
for i in source["facts"]:
res[i["trait"]] = {"value": i["value"],
"technique_id": i["technique_id"],
"collected_by": i["collected_by"]
}
for i in source.get("facts"):
res[i.get("trait")] = {"value": i.get("value"),
"technique_id": i.get("technique_id"),
"collected_by": i.get("collected_by")
}
return res
def list_paws_of_running_agents(self):
""" Returns a list of all paws of running agents """
return [i["paw"] for i in self.list_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)
return [i.get("paw") for i in self.list_agents()] # 2.8.1 version
# return [i.paw for i in self.list_agents()] # 4* version
# ######### Get one specific item
def get_operation(self, name: str):
""" Gets an operation by name
@ -196,7 +80,7 @@ class CalderaControl():
"""
for operation in self.list_operations():
if operation["name"] == name:
if operation.get("name") == name:
return operation
return None
@ -206,7 +90,7 @@ class CalderaControl():
@param name: Name to look for
"""
for adversary in self.list_adversaries():
if adversary["name"] == name:
if adversary.get("name") == name:
return adversary
return None
@ -216,22 +100,10 @@ class CalderaControl():
@param name: Name to filter for
"""
for objective in self.list_objectives():
if objective["name"] == name:
if objective.get("name") == name:
return objective
return None
# ######### Get by id
def get_source(self, source_name: str):
""" Retrieves data source and detailed facts
@param: The name of the source
"""
payload = {"index": "sources",
"name": source_name}
return self.__contact_server__(payload)
def get_ability(self, abid: str):
"""" Return an ability by id
@ -262,12 +134,17 @@ class CalderaControl():
abilities = self.get_ability(abid)
for ability in abilities:
if ability["platform"] == platform:
if ability.get("platform") == platform:
return True
if platform in ability.get("supported_platforms", []):
return True
if platform in ability.get("platforms", []):
return True
executors = ability.get("executors") # For Caldera 4.*
if executors is not None:
for executor in executors:
if executor.get("platform") == platform:
return True
print(self.get_ability(abid))
return False
@ -276,18 +153,13 @@ class CalderaControl():
@param op_id: Operation id
"""
payload = {"index": "operations",
"id": op_id}
return self.__contact_server__(payload)
def get_result_by_id(self, linkid: str):
""" Get the result from a link id
operations = self.list_operations()
@param linkid: link id
"""
payload = {"index": "result",
"link_id": linkid}
return self.__contact_server__(payload)
if operations is not None:
for an_operation in operations:
if an_operation.get("id") == op_id:
return [an_operation]
return []
def get_linkid(self, op_id: str, paw: str, ability_id: str):
""" Get the id of a link identified by paw and ability_id
@ -309,20 +181,6 @@ class CalderaControl():
# ######### View
def view_operation_report(self, opid: str):
""" 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: str, paw: str, ability_id: str):
""" Gets the output of an executed ability
@ -332,200 +190,24 @@ class CalderaControl():
"""
orep = self.view_operation_report(opid)
# print(orep)
if paw not in orep["steps"]:
print("Broken operation report:")
pprint(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:
for a_step in orep.get("steps").get(paw).get("steps"):
if a_step.get("ability_id") == ability_id:
try:
return a_step["output"]
return a_step.get("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_sources(self, name: str, parameters):
""" Adds a data source and seeds it with facts """
payload = {"index": "sources",
"name": name,
# "id": "123456-1234-1234-1234-12345678",
"rules": [],
"relationships": []
}
facts = []
if parameters is not None:
for key, value in parameters.items():
facts.append({"trait": key, "value": value})
# TODO: We need something better than a dict here as payload to have strong typing
payload["facts"] = facts # type: ignore
print(payload)
return self.__contact_server__(payload, method="put")
def add_operation(self, name: str, advid: str, group: str = "red", state: str = "running", obfuscator: str = "plain-text", jitter: str = '4/8', parameters=None):
""" 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
@param obfuscator: obfuscator to use for the attack
@param jitter: jitter to use for the attack
@param parameters: parameters to pass to the ability
"""
# 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'}
sources_name = "source_" + name
self.add_sources(sources_name, parameters)
# To verify:
# print(self.get_source(sources_name))
payload = {"index": "operations",
"name": name,
"state": state,
"autonomous": 1,
'obfuscator': obfuscator,
'auto_close': '1',
'jitter': jitter,
'source': sources_name,
'visibility': '50',
"group": group,
#
"planner": "atomic",
"adversary_id": advid,
}
return self.__contact_server__(payload, method="put")
def add_adversary(self, name: str, ability: str, description: str = "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
# "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: str, ability_id: str, obfuscator: str = "plain-text", parameters=None):
""" 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
@param parameters: parameters to pass to the ability
"""
# 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}
facts = []
if parameters is not None:
for key, value in parameters.items():
facts.append({"trait": key, "value": value})
# TODO. We need something better than a dict here for strong typing
payload["facts"] = facts # type: ignore
# print(payload)
return self.__contact_server__(payload, rest_path="plugin/access/exploit_ex")
def execute_operation(self, operation_id: str, state: str = "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
# curl -X DELETE http://localhost:8888/api/rest -d '{"index":"operations","id":"$operation_id"}'
def delete_operation(self, opid: str):
""" 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: str):
""" Delete adversary by id
@param adid: Adversary id
"""
payload = {"index": "adversaries",
"adversary_id": [{"adversary_id": adid}]}
return self.__contact_server__(payload, method="delete")
def delete_agent(self, paw: str):
""" Delete a specific agent from the kali db. implant may still be running and reconnect
@param paw: The Id of the agent to delete
"""
payload = {"index": "adversaries",
"paw": paw}
return self.__contact_server__(payload, method="delete")
def kill_agent(self, paw: str):
""" Send a message to an agent to kill itself
@param paw: The Id of the agent to delete
"""
payload = {"index": "agents",
"paw": paw,
"watchdog": 1,
"sleep_min": 3,
"sleep_max": 3}
return self.__contact_server__(payload, method="put")
def delete_all_agents(self):
""" Delete all agents from kali db """
@ -562,12 +244,14 @@ class CalderaControl():
# Plus: 0 as "finished"
#
# TODO: Maybe try to get the report and continue until we have it. Could be done in addition.
operation = self.get_operation_by_id(opid)
if debug:
print(f"Operation data {operation}")
try:
# print(operation[0]["state"])
if operation[0]["state"] == "finished":
if operation[0].get("state") == "finished":
return True
except KeyError as exception:
raise CalderaError from exception
@ -641,15 +325,15 @@ class CalderaControl():
return False
self.add_adversary(adversary_name, ability_id)
adid = self.get_adversary(adversary_name)["adversary_id"]
adid = self.get_adversary(adversary_name).get("adversary_id")
logid = self.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"],
ttp=self.get_ability(ability_id)[0].get("technique_id"),
name=self.get_ability(ability_id)[0].get("name"),
description=self.get_ability(ability_id)[0].get("description"),
obfuscator=obfuscator,
jitter=jitter,
**kwargs
@ -658,8 +342,8 @@ class CalderaControl():
# ##### Create / Run Operation
self.attack_logger.vprint(f"New adversary generated. ID: {adid}, ability: {ability_id} group: {group}", 2)
res = self.add_operation(operation_name,
advid=adid,
res = self.add_operation(name=operation_name,
adversary_id=adid,
group=group,
obfuscator=obfuscator,
jitter=jitter,
@ -667,14 +351,14 @@ class CalderaControl():
)
self.attack_logger.vprint(pformat(res), 3)
opid = self.get_operation(operation_name)["id"]
opid = self.get_operation(operation_name).get("id")
self.attack_logger.vprint("New operation created. OpID: " + str(opid), 3)
self.execute_operation(opid)
self.set_operation_state(opid)
self.attack_logger.vprint("Execute operation", 3)
retries = 30
ability_name = self.get_ability(ability_id)[0]["name"]
ability_description = self.get_ability(ability_id)[0]["description"]
ability_name = self.get_ability(ability_id)[0].get("name")
ability_description = self.get_ability(ability_id)[0].get("description")
self.attack_logger.vprint(f"{CommandlineColors.OKBLUE}Executed attack operation{CommandlineColors.ENDC}", 1)
self.attack_logger.vprint(f"{CommandlineColors.BACKGROUND_BLUE} PAW: {paw} Group: {group} Ability: {ability_id} {CommandlineColors.ENDC}", 1)
self.attack_logger.vprint(f"{CommandlineColors.BACKGROUND_BLUE} {ability_name}: {ability_description} {CommandlineColors.ENDC}", 1)
@ -713,16 +397,16 @@ class CalderaControl():
self.attack_logger.vprint(self.list_facts_for_name("source_" + operation_name), 2)
# ######## Cleanup
self.execute_operation(opid, "cleanup")
self.set_operation_state(opid, "cleanup")
self.delete_adversary(adid)
self.delete_operation(opid)
self.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"],
ttp=self.get_ability(ability_id)[0].get("technique_id"),
name=self.get_ability(ability_id)[0].get("name"),
description=self.get_ability(ability_id)[0].get("description"),
obfuscator=obfuscator,
jitter=jitter,
logid=logid,

@ -16,8 +16,8 @@ from app.interface_sfx import CommandlineColors
from app.exceptions import ServerError
from app.pluginmanager import PluginManager
from app.doc_generator import DocGenerator
from caldera_control import CalderaControl
from machine_control import Machine
from app.calderacontrol import CalderaControl
from app.machinecontrol import Machine
from plugins.base.attack import AttackPlugin

@ -390,7 +390,8 @@ class Machine():
# TODO: Caldera implant
# TODO: Metasploit implant
def install_caldera_server(self, cleanup=False, version="2.8.1"):
# options for version: 4.0.0-alpha.2 2.8.1
def install_caldera_server(self, cleanup=False, version="4.0.0-alpha.2"):
""" Installs the caldera server on the VM
@param cleanup: Remove the old caldera version. Slow but reduces side effects

@ -3,25 +3,40 @@
""" A command line tool to control a caldera server """
import argparse
from pprint import pprint
# from app.calderacontrol import CalderaControl
from app.calderaapi_4 import CalderaAPI
from app.calderacontrol import CalderaControl
from app.attack_log import AttackLog
class CmdlineArgumentException(Exception):
""" An error in the user supplied command line """
# https://caldera.readthedocs.io/en/latest/The-REST-API.html
# TODO: Check if attack is finished
# TODO: Get results of a specific attack
# Arpgparse handling
def list_agents(calcontrol, arguments): # pylint: disable=unused-argument
""" Call list agents in caldera control
def agents(calcontrol, arguments): # pylint: disable=unused-argument
""" Agents in caldera control
@param calcontrol: Connection to the caldera server
@param arguments: Parser command line arguments
"""
print(f"Running agents: {calcontrol.list_agents()}")
if arguments.list:
print(calcontrol.list_agents())
print([i["paw"] for i in calcontrol.list_agents()])
if arguments.delete:
print(calcontrol.delete_agent(arguments.paw))
if arguments.kill:
print(calcontrol.kill_agent(arguments.paw))
def list_facts(calcontrol, arguments): # pylint: disable=unused-argument
@ -53,38 +68,136 @@ def add_facts(calcontrol, arguments): # pylint: disable=unused-argument
print(f'Created fact: {calcontrol.add_sources(name, data)}')
def delete_agents(calcontrol, arguments): # pylint: disable=unused-argument
""" Call list agents in caldera control
def list_abilities(calcontrol, arguments):
""" Call list abilities in caldera control
@param calcontrol: Connection to the caldera server
@param arguments: Parser command line arguments
"""
print(calcontrol.list_paws_of_running_agents())
if arguments.paw:
print(calcontrol.kill_agent(paw=arguments.paw))
print(calcontrol.delete_agent(paw=arguments.paw))
if arguments.list:
abilities = calcontrol.list_abilities()
abi_ids = [aid.ability_id for aid in abilities]
print(abi_ids)
else:
print(calcontrol.kill_all_agents())
print(calcontrol.delete_all_agents())
for abi in abilities:
for executor in abi.executors:
for a_parser in executor.parsers:
pprint(a_parser.relationships)
def list_abilities(calcontrol, arguments):
""" Call list abilities in caldera control
def obfuscators(calcontrol, arguments):
""" Manage obfuscators caldera control
@param calcontrol: Connection to the caldera server
@param arguments: Parser command line arguments
"""
if arguments.list:
obfs = calcontrol.list_obfuscators()
# ob_ids = [aid.ability_id for aid in obfuscators]
# print(ob_ids)
for obfuscator in obfs:
print(obfuscator)
def objectives(calcontrol, arguments):
""" Manage objectives caldera control
@param calcontrol: Connection to the caldera server
@param arguments: Parser command line arguments
"""
abilities = arguments.ability_ids
if arguments.list:
for objective in calcontrol.list_objectives():
print(objective)
def adversaries(calcontrol, arguments):
""" Manage adversaries caldera control
@param calcontrol: Connection to the caldera server
@param arguments: Parser command line arguments
"""
if arguments.all:
abilities = [aid["ability_id"] for aid in calcontrol.list_abilities()]
if arguments.list:
for adversary in calcontrol.list_adversaries():
print(adversary)
if arguments.add:
if arguments.ability_id is None:
raise CmdlineArgumentException("Creating an adversary requires an ability id")
if arguments.name is None:
raise CmdlineArgumentException("Creating an adversary requires an adversary name")
calcontrol.add_adversary(arguments.name, arguments.ability_id)
if arguments.delete:
if arguments.adversary_id is None:
raise CmdlineArgumentException("Deleting an adversary requires an adversary id")
calcontrol.delete_adversary(arguments.adversary_id)
for aid in abilities:
for ability in calcontrol.get_ability(aid):
calcontrol.pretty_print_ability(ability)
def sources(calcontrol, arguments):
""" Manage sources caldera control
@param calcontrol: Connection to the caldera server
@param arguments: Parser command line arguments
"""
if arguments.list:
for a_source in calcontrol.list_sources():
print(a_source)
def planners(calcontrol, arguments):
""" Manage planners caldera control
@param calcontrol: Connection to the caldera server
@param arguments: Parser command line arguments
"""
if arguments.list:
for a_planner in calcontrol.list_planners():
print(a_planner)
def operations(calcontrol, arguments):
""" Manage operations caldera control
@param calcontrol: Connection to the caldera server
@param arguments: Parser command line arguments
"""
if arguments.list:
for an_operation in calcontrol.list_operations():
print(an_operation)
if arguments.add:
if arguments.adversary_id is None:
raise CmdlineArgumentException("Adding an operation requires an adversary id")
if arguments.name is None:
raise CmdlineArgumentException("Adding an operation requires a name for it")
ops = calcontrol.add_operation(name=arguments.name,
adversary_id=arguments.adversary_id,
source_id=arguments.source_id,
planner_id=arguments.planner_id,
group=arguments.group,
state=arguments.state,
obfuscator=arguments.obfuscator,
jitter=arguments.jitter)
print(ops)
if arguments.delete:
if arguments.id is None:
raise CmdlineArgumentException("Deleting an operation requires its id")
ops = calcontrol.delete_operation(arguments.id)
print(ops)
if arguments.view_report:
if arguments.id is None:
raise CmdlineArgumentException("Viewing an operation report requires an operation id")
report = calcontrol.view_operation_report(arguments.id)
print(report)
def attack(calcontrol, arguments):
@ -122,15 +235,15 @@ def create_parser():
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",
parser_abilities.add_argument("--list", default=False, action="store_true",
help="List all abilities")
parser_agents = subparsers.add_parser("agents", help="agents")
parser_agents.set_defaults(func=list_agents)
parser_delete_agents = subparsers.add_parser("delete_agents", help="agents")
parser_delete_agents.add_argument("--paw", default=None, help="PAW to delete. if not set it will delete all agents")
parser_delete_agents.set_defaults(func=delete_agents)
parser_agents.set_defaults(func=agents)
parser_agents.add_argument("--list", default=False, action="store_true", help="List all agents")
parser_agents.add_argument("--delete", default=False, action="store_true", help="Delete agent")
parser_agents.add_argument("--kill", default=False, action="store_true", help="Delete agent")
parser_agents.add_argument("--paw", default=None, help="PAW to delete. if not set it will delete all agents")
parser_facts = subparsers.add_parser("facts", help="facts")
parser_facts.set_defaults(func=list_facts)
@ -139,8 +252,66 @@ def create_parser():
parser_facts = subparsers.add_parser("add_facts", help="facts")
parser_facts.set_defaults(func=add_facts)
# Sub parser for obfuscators
parser_obfuscators = subparsers.add_parser("obfuscators", help="obfuscators")
parser_obfuscators.set_defaults(func=obfuscators)
parser_obfuscators.add_argument("--list", default=False, action="store_true",
help="List all obfuscators")
# Sub parser for objectives
parser_objectives = subparsers.add_parser("objectives", help="objectives")
parser_objectives.set_defaults(func=objectives)
parser_objectives.add_argument("--list", default=False, action="store_true",
help="List all objectives")
# Sub parser for adversaries
parser_adversaries = subparsers.add_parser("adversaries", help="adversaries")
parser_adversaries.set_defaults(func=adversaries)
parser_adversaries.add_argument("--list", default=False, action="store_true",
help="List all adversaries")
parser_adversaries.add_argument("--add", default=False, action="store_true",
help="Add a new adversary")
parser_adversaries.add_argument("--ability_id", "--abid", default=None, help="Ability ID")
parser_adversaries.add_argument("--ability_name", default=None, help="Adversary name")
parser_adversaries.add_argument("--delete", default=False, action="store_true",
help="Delete adversary")
parser_adversaries.add_argument("--adversary_id", "--advid", default=None, help="Adversary ID")
# Sub parser for operations
parser_operations = subparsers.add_parser("operations", help="operations")
parser_operations.set_defaults(func=operations)
parser_operations.add_argument("--list", default=False, action="store_true",
help="List all operations")
parser_operations.add_argument("--add", default=False, action="store_true",
help="Add a new operations")
parser_operations.add_argument("--delete", default=False, action="store_true",
help="Delete an operation")
parser_operations.add_argument("--view_report", default=False, action="store_true",
help="View the report of a finished operation")
parser_operations.add_argument("--name", default=None, help="Name of the operation")
parser_operations.add_argument("--adversary_id", "--advid", default=None, help="Adversary ID")
parser_operations.add_argument("--source_id", "--sourceid", default="basic", help="'Source' ID")
parser_operations.add_argument("--planner_id", "--planid", default="atomic", help="Planner ID")
parser_operations.add_argument("--group", default="", help="Caldera group to run the operation on (we are targeting groups, not PAWs)")
parser_operations.add_argument("--state", default="running", help="State to start the operation in")
parser_operations.add_argument("--obfuscator", default="plain-text", help="Obfuscator to use for this attack")
parser_operations.add_argument("--jitter", default="4/8", help="Jitter to use")
parser_operations.add_argument("--id", default=None, help="ID of operation to delete")
# Sub parser for sources
parser_sources = subparsers.add_parser("sources", help="sources")
parser_sources.set_defaults(func=sources)
parser_sources.add_argument("--list", default=False, action="store_true",
help="List all sources")
# Sub parser for planners
parser_sources = subparsers.add_parser("planners", help="planners")
parser_sources.set_defaults(func=planners)
parser_sources.add_argument("--list", default=False, action="store_true",
help="List all planners")
# For all parsers
main_parser.add_argument("--caldera_url", help="caldera url, including port", default="http://192.168.178.125:8888/")
main_parser.add_argument("--caldera_url", help="caldera url, including port", default="http://localhost:8888/")
main_parser.add_argument("--apikey", help="caldera api key", default="ADMIN123")
return main_parser
@ -153,7 +324,10 @@ if __name__ == "__main__":
print(args.caldera_url)
attack_logger = AttackLog(args.verbose)
caldera_control = CalderaControl(args.caldera_url, attack_logger, config=None, apikey=args.apikey)
caldera_control = CalderaAPI(args.caldera_url, attack_logger, config=None, apikey=args.apikey)
print("Caldera Control ready")
str(args.func(caldera_control, args))
try:
str(args.func(caldera_control, args))
except CmdlineArgumentException as ex:
parser.print_help()
print(f"\nCommandline error: {ex}")

@ -4,6 +4,7 @@ from app.calderacontrol import CalderaControl
from simplejson.errors import JSONDecodeError
from app.exceptions import CalderaError
from app.attack_log import AttackLog
import pydantic
# https://docs.python.org/3/library/unittest.html
@ -17,35 +18,14 @@ class TestExample(unittest.TestCase):
def tearDown(self) -> None:
pass
# List links sends the right commands and post
def test_list_links(self):
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
self.cc.list_links("asd")
mock_method.assert_called_once_with({"index": "link", "op_id": "asd"})
# List links gets an Exception and does not handle it (as expected)
def test_list_links_with_exception(self):
with self.assertRaises(JSONDecodeError):
with patch.object(self.cc, "__contact_server__", side_effect=JSONDecodeError("foo", "bar", 2)):
self.cc.list_links("asd")
# list results sends the right commands and post
def test_list_results(self):
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
self.cc.list_results("asd")
mock_method.assert_called_once_with({"index": "result", "link_id": "asd"})
# List results gets an Exception and does not handle it (as expected)
def test_list_results_with_exception(self):
with self.assertRaises(JSONDecodeError):
with patch.object(self.cc, "__contact_server__", side_effect=JSONDecodeError("foo", "bar", 2)):
self.cc.list_results("asd")
# list_operations
def test_list_operations(self):
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
self.cc.list_operations()
mock_method.assert_called_once_with({"index": "operations"})
try:
self.cc.list_operations()
except pydantic.error_wrappers.ValidationError:
pass
mock_method.assert_called_once_with(None, method='get', rest_path='api/v2/operations')
# list operations gets the expected exception
def test_list_operations_with_exception(self):
@ -56,8 +36,11 @@ class TestExample(unittest.TestCase):
# list_abilities
def test_list_abilities(self):
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
self.cc.list_abilities()
mock_method.assert_called_once_with({"index": "abilities"})
try:
self.cc.list_abilities()
except pydantic.error_wrappers.ValidationError:
pass
mock_method.assert_called_once_with(None, method='get', rest_path='api/v2/abilities')
# list abilities gets the expected exception
def test_list_abilities_with_exception(self):
@ -68,8 +51,11 @@ class TestExample(unittest.TestCase):
# list_agents
def test_list_agents(self):
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
self.cc.list_agents()
mock_method.assert_called_once_with({"index": "agents"})
try:
self.cc.list_agents()
except pydantic.error_wrappers.ValidationError:
pass
mock_method.assert_called_once_with(None, method='get', rest_path='api/v2/agents')
# list agents gets the expected exception
def test_list_agents_with_exception(self):
@ -80,8 +66,11 @@ class TestExample(unittest.TestCase):
# list_adversaries
def test_list_adversaries(self):
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
self.cc.list_adversaries()
mock_method.assert_called_once_with({"index": "adversaries"})
try:
self.cc.list_adversaries()
except pydantic.error_wrappers.ValidationError:
pass
mock_method.assert_called_once_with(None, method='get', rest_path='api/v2/adversaries')
# list adversaries gets the expected exception
def test_list_adversaries_with_exception(self):
@ -92,8 +81,11 @@ class TestExample(unittest.TestCase):
# list_objectives
def test_list_objectives(self):
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
self.cc.list_objectives()
mock_method.assert_called_once_with({"index": "objectives"})
try:
self.cc.list_objectives()
except pydantic.error_wrappers.ValidationError:
pass
mock_method.assert_called_once_with(None, method='get', rest_path='api/v2/objectives')
# list objectives gets the expected exception
def test_list_objectives_with_exception(self):
@ -161,27 +153,11 @@ class TestExample(unittest.TestCase):
def test_get_operation_by_id(self):
opid = "FooBar"
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
self.cc.get_operation_by_id(opid)
mock_method.assert_called_once_with({"index": "operations", "id": opid})
# get_operation_by_id gets the expected exception
def test_get_operation_by_id_with_exception(self):
with self.assertRaises(JSONDecodeError):
with patch.object(self.cc, "__contact_server__", side_effect=JSONDecodeError("foo", "bar", 2)):
self.cc.get_result_by_id("FooBar")
# get_result_by_id
def test_get_result_by_id(self):
opid = "FooBar"
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
self.cc.get_result_by_id(opid)
mock_method.assert_called_once_with({"index": "result", "link_id": opid})
# get_result_by_id gets the expected exception
def test_get_result_by_id_with_exception(self):
with self.assertRaises(JSONDecodeError):
with patch.object(self.cc, "__contact_server__", side_effect=JSONDecodeError("foo", "bar", 2)):
self.cc.get_result_by_id("FooBar")
try:
self.cc.get_operation_by_id(opid)
except pydantic.error_wrappers.ValidationError:
pass
mock_method.assert_called_once_with(None, method='get', rest_path='api/v2/operations')
# get_linkid
def test_get_linkid(self):
@ -218,7 +194,7 @@ class TestExample(unittest.TestCase):
opid = "FooBar"
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
self.cc.view_operation_report(opid)
mock_method.assert_called_once_with({"index": "operation_report", "op_id": opid, "agent_output": 1})
mock_method.assert_called_once_with({"enable_agent_output": True}, method="post", rest_path="api/v2/operations/FooBar/report")
# get_result_by_id gets the expected exception
def test_view_operation_report_with_exception(self):
@ -263,57 +239,32 @@ class TestExample(unittest.TestCase):
group = "test_group"
advid = "test_id"
exp1 = {"index": "sources",
"name": "source_test_name",
"rules": [],
"relationships": [],
"facts": []
}
exp3 = {"index": "operations",
"name": name,
"state": state,
"autonomous": 1,
'obfuscator': 'plain-text',
'auto_close': '1',
'jitter': '4/8',
'source': 'source_test_name',
'visibility': '50',
"group": group,
"planner": "atomic",
"adversary_id": advid,
}
exp1 = {'name': 'test_name', 'group': 'test_group', 'adversary': {'adversary_id': None}, 'auto_close': False, 'state': 'test_state', 'autonomous': 1, 'planner': {'id': 'atomic'}, 'source': {'id': 'basic'}, 'use_learning_parsers': True, 'obfuscator': 'plain-text', 'jitter': '4/8', 'visibility': '51'}
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
self.cc.add_operation(name, advid, group, state)
# mock_method.assert_called_once_with(exp, method="put")
mock_method.assert_has_calls([call(exp1, method="put"), call(exp3, method="put")])
try:
self.cc.add_operation(name=name,
advid=advid,
group=group,
state=state)
except pydantic.error_wrappers.ValidationError:
pass
mock_method.assert_has_calls([call(exp1, method='post', rest_path='api/v2/operations')])
# add_operation defaults
def test_add_operation_defaults(self):
name = "test_name"
advid = "test_id"
exp1 = {"index": "sources",
"name": "source_test_name",
"rules": [],
"relationships": [],
"facts": []
}
exp3 = {"index": "operations",
"name": name,
"state": "running", # default
"autonomous": 1,
'obfuscator': 'plain-text',
'auto_close': '1',
'jitter': '4/8',
'source': 'source_test_name',
'visibility': '50',
"group": "red", # default
"planner": "atomic",
"adversary_id": advid,
}
exp1 = {'name': 'test_name', 'group': '', 'adversary': {'adversary_id': None}, 'auto_close': False, 'state': 'running', 'autonomous': 1, 'planner': {'id': 'atomic'}, 'source': {'id': 'basic'}, 'use_learning_parsers': True, 'obfuscator': 'plain-text', 'jitter': '4/8', 'visibility': '51'}
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
self.cc.add_operation(name, advid)
mock_method.assert_has_calls([call(exp1, method="put"), call(exp3, method="put")])
try:
self.cc.add_operation(name=name,
advid=advid)
except pydantic.error_wrappers.ValidationError:
pass
mock_method.assert_has_calls([call(exp1, method='post', rest_path='api/v2/operations')])
# add_adversary
def test_add_adversary(self):
@ -321,109 +272,74 @@ class TestExample(unittest.TestCase):
ability = "test_ability"
description = "test_descritption"
exp = {"index": "adversaries",
"name": name,
"description": description,
"atomic_ordering": [{"id": ability}],
#
"objective": '495a9828-cab1-44dd-a0ca-66e58177d8cc' # default objective
}
# Caldera 4
exp_4 = {
"name": name,
"description": description,
"atomic_ordering": ["test_ability"],
#
"objective": '495a9828-cab1-44dd-a0ca-66e58177d8cc' # default objective
}
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
self.cc.add_adversary(name, ability, description)
mock_method.assert_called_once_with(exp, method="put")
mock_method.assert_called_once_with(exp_4, method="post", rest_path="api/v2/adversaries")
def test_add_adversary_default(self):
name = "test_name"
ability = "test_ability"
exp = {"index": "adversaries",
"name": name,
"description": "created automatically",
"atomic_ordering": [{"id": ability}],
#
"objective": '495a9828-cab1-44dd-a0ca-66e58177d8cc' # default objective
}
# Caldera 4
exp_4 = {
"name": name,
"description": "created automatically",
"atomic_ordering": ["test_ability"],
#
"objective": '495a9828-cab1-44dd-a0ca-66e58177d8cc' # default objective
}
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
self.cc.add_adversary(name, ability)
mock_method.assert_called_once_with(exp, method="put")
# execute_ability
def test_execute_ability(self):
paw = "test_paw"
ability_id = "test_ability"
obfuscator = "plain-text"
exp = {"paw": paw,
"ability_id": ability_id,
"obfuscator": obfuscator,
"facts": []}
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
self.cc.execute_ability(paw, ability_id, obfuscator)
mock_method.assert_called_once_with(exp, rest_path="plugin/access/exploit_ex")
mock_method.assert_called_once_with(exp_4, method="post", rest_path="api/v2/adversaries")
def test_execute_ability_default(self):
paw = "test_paw"
ability_id = "test_ability"
exp = {"paw": paw,
"ability_id": ability_id,
"obfuscator": "plain-text",
"facts": []}
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
self.cc.execute_ability(paw, ability_id)
mock_method.assert_called_once_with(exp, rest_path="plugin/access/exploit_ex")
# execute_operation
def test_execute_operation(self):
# set_operation_state
def test_set_operation_state(self):
operation_id = "test_opid"
state = "paused"
exp = {"index": "operation",
"op_id": operation_id,
"state": state}
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
self.cc.execute_operation(operation_id, state)
mock_method.assert_called_once_with(exp)
self.cc.set_operation_state(operation_id, state)
mock_method.assert_called_once_with({'state': 'paused'}, method='patch', rest_path='api/v2/operations/test_opid')
# not supported state
def test_execute_operation_not_supported(self):
def test_set_operation_state_not_supported(self):
operation_id = "test_opid"
state = "not supported"
with self.assertRaises(ValueError):
with patch.object(self.cc, "__contact_server__", return_value=None):
self.cc.execute_operation(operation_id, state)
self.cc.set_operation_state(operation_id, state)
def test_execute_operation_default(self):
def test_set_operation_state_default(self):
operation_id = "test_opid"
exp = {"index": "operation",
"op_id": operation_id,
"state": "running" # default
}
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
self.cc.execute_operation(operation_id)
mock_method.assert_called_once_with(exp)
self.cc.set_operation_state(operation_id)
mock_method.assert_called_once_with({'state': 'running'}, method='patch', rest_path='api/v2/operations/test_opid')
# delete_operation
def test_delete_operation(self):
opid = "test_opid"
exp = {"index": "operations",
"id": opid}
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
self.cc.delete_operation(opid)
mock_method.assert_called_once_with(exp, method="delete")
mock_method.assert_called_once_with({}, method="delete", rest_path="api/v2/operations/test_opid")
# delete_adversary
def test_delete_adversary(self):
adid = "test_adid"
exp = {"index": "adversaries",
"adversary_id": [{"adversary_id": adid}]}
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
self.cc.delete_adversary(adid)
mock_method.assert_called_once_with(exp, method="delete")
mock_method.assert_called_once_with(None, method="delete", rest_path="api/v2/adversaries/test_adid")
# is_operation_finished
def test_is_operation_finished_true(self):
@ -442,14 +358,6 @@ class TestExample(unittest.TestCase):
res = self.cc.is_operation_finished(opid)
self.assertEqual(res, False)
def test_is_operation_finished_exception(self):
opdata = [{"chain": [{"statusa": 1}]}]
opid = "does not matter"
with self.assertRaises(CalderaError):
with patch.object(self.cc, "get_operation_by_id", return_value=opdata):
self.cc.is_operation_finished(opid)
def test_is_operation_finished_exception2(self):
opdata = []
opid = "does not matter"

Loading…
Cancel
Save