#!/usr/bin/env python3 """ Direct API to the caldera server. Not abstract simplification methods. Compatible with Caldera 2.8.1 """ import json from typing import Optional, Any import requests import simplejson from app.attack_log import AttackLog from app.config import ExperimentConfig from app.exceptions import ConfigurationError class CalderaAPI: """ API to Caldera 2.8.1 """ def __init__(self, server: str, attack_logger: AttackLog, config: Optional[ExperimentConfig] = None, apikey: str = None) -> 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 self.apikey: str = "" if self.config is not None: self.apikey = self.config.caldera_apikey() else: if apikey is None: raise ConfigurationError("No APIKEY configured") self.apikey = apikey def __contact_server__(self, payload, rest_path: str = "api/rest", method: str = "post") -> Any: """ @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) -> Any: """ 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")