|
|
|
import unittest
|
|
|
|
from unittest.mock import patch, call
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
class TestExample(unittest.TestCase):
|
|
|
|
|
|
|
|
def setUp(self) -> None:
|
|
|
|
self.attack_logger = AttackLog(0)
|
|
|
|
self.cc = CalderaControl("https://localhost", attack_logger=self.attack_logger, apikey="123")
|
|
|
|
|
|
|
|
def tearDown(self) -> None:
|
|
|
|
pass
|
|
|
|
|
|
|
|
# list_operations
|
|
|
|
def test_list_operations(self):
|
|
|
|
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
|
|
|
|
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):
|
|
|
|
with self.assertRaises(JSONDecodeError):
|
|
|
|
with patch.object(self.cc, "__contact_server__", side_effect=JSONDecodeError("foo", "bar", 2)):
|
|
|
|
self.cc.list_operations()
|
|
|
|
|
|
|
|
# list_abilities
|
|
|
|
def test_list_abilities(self):
|
|
|
|
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
|
|
|
|
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):
|
|
|
|
with self.assertRaises(JSONDecodeError):
|
|
|
|
with patch.object(self.cc, "__contact_server__", side_effect=JSONDecodeError("foo", "bar", 2)):
|
|
|
|
self.cc.list_abilities()
|
|
|
|
|
|
|
|
# list_agents
|
|
|
|
def test_list_agents(self):
|
|
|
|
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
|
|
|
|
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):
|
|
|
|
with self.assertRaises(JSONDecodeError):
|
|
|
|
with patch.object(self.cc, "__contact_server__", side_effect=JSONDecodeError("foo", "bar", 2)):
|
|
|
|
self.cc.list_agents()
|
|
|
|
|
|
|
|
# list_adversaries
|
|
|
|
def test_list_adversaries(self):
|
|
|
|
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
|
|
|
|
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):
|
|
|
|
with self.assertRaises(JSONDecodeError):
|
|
|
|
with patch.object(self.cc, "__contact_server__", side_effect=JSONDecodeError("foo", "bar", 2)):
|
|
|
|
self.cc.list_adversaries()
|
|
|
|
|
|
|
|
# list_objectives
|
|
|
|
def test_list_objectives(self):
|
|
|
|
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
|
|
|
|
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):
|
|
|
|
with self.assertRaises(JSONDecodeError):
|
|
|
|
with patch.object(self.cc, "__contact_server__", side_effect=JSONDecodeError("foo", "bar", 2)):
|
|
|
|
self.cc.list_objectives()
|
|
|
|
|
|
|
|
# Get operation (working)
|
|
|
|
def test_get_operation(self):
|
|
|
|
a = {"name": "bar", "bar": "baz"}
|
|
|
|
with patch.object(self.cc, "list_operations", return_value=[a, {"name": "zigg", "bar": "baz"}]):
|
|
|
|
op = self.cc.get_operation("bar")
|
|
|
|
self.assertEqual(op, a)
|
|
|
|
|
|
|
|
# Get operation (no matching name)
|
|
|
|
def test_get_operation_not_available(self):
|
|
|
|
a = {"name": "bar", "bar": "baz"}
|
|
|
|
with patch.object(self.cc, "list_operations", return_value=[a, {"name": "zigg", "bar": "baz"}]):
|
|
|
|
op = self.cc.get_operation("baaaaar")
|
|
|
|
self.assertEqual(op, None)
|
|
|
|
|
|
|
|
# get_adversary
|
|
|
|
def test_get_adversary(self):
|
|
|
|
a = {"name": "bar", "bar": "baz"}
|
|
|
|
with patch.object(self.cc, "list_adversaries", return_value=[a, {"name": "zigg", "bar": "baz"}]):
|
|
|
|
op = self.cc.get_adversary("bar")
|
|
|
|
self.assertEqual(op, a)
|
|
|
|
|
|
|
|
# get_adversary (no matching name)
|
|
|
|
def test_get_adversary_not_available(self):
|
|
|
|
a = {"name": "bar", "bar": "baz"}
|
|
|
|
with patch.object(self.cc, "list_adversaries", return_value=[a, {"name": "zigg", "bar": "baz"}]):
|
|
|
|
op = self.cc.get_adversary("baaaar")
|
|
|
|
self.assertEqual(op, None)
|
|
|
|
|
|
|
|
# get_objective
|
|
|
|
def test_get_objective(self):
|
|
|
|
a = {"name": "bar", "bar": "baz"}
|
|
|
|
with patch.object(self.cc, "list_objectives", return_value=[a, {"name": "zigg", "bar": "baz"}]):
|
|
|
|
op = self.cc.get_objective("bar")
|
|
|
|
self.assertEqual(op, a)
|
|
|
|
|
|
|
|
# get_objective (no matching name)
|
|
|
|
def test_get_objective_not_available(self):
|
|
|
|
a = {"name": "bar", "bar": "baz"}
|
|
|
|
with patch.object(self.cc, "list_objectives", return_value=[a, {"name": "zigg", "bar": "baz"}]):
|
|
|
|
op = self.cc.get_objective("baaaar")
|
|
|
|
self.assertEqual(op, None)
|
|
|
|
|
|
|
|
# get_ability
|
|
|
|
def test_get_ability(self):
|
|
|
|
a = {"ability_id": "bar", "bar": "baz"}
|
|
|
|
with patch.object(self.cc, "list_abilities", return_value=[a, {"ability_id": "zigg", "bar": "baz"}]):
|
|
|
|
op = self.cc.get_ability("bar")
|
|
|
|
self.assertEqual(op, [a])
|
|
|
|
|
|
|
|
# get_ability (no matching name)
|
|
|
|
def test_get_ability_not_available(self):
|
|
|
|
a = {"ability_id": "bar", "bar": "baz"}
|
|
|
|
with patch.object(self.cc, "list_abilities", return_value=[a, {"ability_id": "zigg", "bar": "baz"}]):
|
|
|
|
op = self.cc.get_ability("baaaar")
|
|
|
|
self.assertEqual(op, [])
|
|
|
|
|
|
|
|
# get_operation_by_id
|
|
|
|
def test_get_operation_by_id(self):
|
|
|
|
opid = "FooBar"
|
|
|
|
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
|
|
|
|
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):
|
|
|
|
paw = "FooBar"
|
|
|
|
ability_id = "AID"
|
|
|
|
|
|
|
|
alink = {"paw": paw,
|
|
|
|
"ability": {"ability_id": ability_id},
|
|
|
|
"id": "Getme"}
|
|
|
|
|
|
|
|
op = [{"chain": [alink]}]
|
|
|
|
|
|
|
|
with patch.object(self.cc, "get_operation_by_id", return_value=op):
|
|
|
|
res = self.cc.get_linkid("Foo", paw, ability_id)
|
|
|
|
self.assertEqual(res, "Getme")
|
|
|
|
|
|
|
|
# get missing link id
|
|
|
|
def test_get_linkid_missing(self):
|
|
|
|
paw = "FooBar"
|
|
|
|
ability_id = "AID"
|
|
|
|
|
|
|
|
alink = {"paw": paw,
|
|
|
|
"ability": {"ability_id": ability_id},
|
|
|
|
"id": "Getme"}
|
|
|
|
|
|
|
|
op = [{"chain": [alink]}]
|
|
|
|
|
|
|
|
with patch.object(self.cc, "get_operation_by_id", return_value=op):
|
|
|
|
res = self.cc.get_linkid("Foo", "Bar", ability_id)
|
|
|
|
self.assertEqual(res, None)
|
|
|
|
|
|
|
|
# view_operation_report
|
|
|
|
def test_view_operation_report(self):
|
|
|
|
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({"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):
|
|
|
|
with self.assertRaises(JSONDecodeError):
|
|
|
|
with patch.object(self.cc, "__contact_server__", side_effect=JSONDecodeError("foo", "bar", 2)):
|
|
|
|
self.cc.view_operation_report("FooBar")
|
|
|
|
|
|
|
|
# view_operation_output
|
|
|
|
def test_view_operation_output(self):
|
|
|
|
opid = "OPID_123"
|
|
|
|
paw = "PAW_123"
|
|
|
|
ability_id = "AID_123"
|
|
|
|
or_ret = {"steps": {paw: {"steps": [{"ability_id": ability_id, "output": "red"}]}}}
|
|
|
|
with patch.object(self.cc, "view_operation_report", return_value=or_ret):
|
|
|
|
res = self.cc.view_operation_output(opid, paw, ability_id)
|
|
|
|
self.assertEqual(res, "red")
|
|
|
|
|
|
|
|
# view_operation_output paw missing
|
|
|
|
def test_view_operation_output_paw_missing(self):
|
|
|
|
opid = "OPID_123"
|
|
|
|
paw = "PAW_123"
|
|
|
|
ability_id = "AID_123"
|
|
|
|
or_ret = {"steps": {paw: {"steps": [{"ability_id": ability_id, "output": "red"}]}}}
|
|
|
|
with self.assertRaises(CalderaError):
|
|
|
|
with patch.object(self.cc, "view_operation_report", return_value=or_ret):
|
|
|
|
self.cc.view_operation_output(opid, "missing", ability_id)
|
|
|
|
|
|
|
|
# view_operation_output ability_id missing
|
|
|
|
def test_view_operation_output_ability_id_missing(self):
|
|
|
|
opid = "OPID_123"
|
|
|
|
paw = "PAW_123"
|
|
|
|
ability_id = "AID_123"
|
|
|
|
or_ret = {"steps": {paw: {"steps": [{"ability_id": ability_id, "output": "red"}]}}}
|
|
|
|
with patch.object(self.cc, "view_operation_report", return_value=or_ret):
|
|
|
|
res = self.cc.view_operation_output(opid, paw, "missing")
|
|
|
|
self.assertEqual(res, None)
|
|
|
|
|
|
|
|
# add_operation
|
|
|
|
def test_add_operation(self):
|
|
|
|
name = "test_name"
|
|
|
|
state = "test_state"
|
|
|
|
group = "test_group"
|
|
|
|
advid = "test_id"
|
|
|
|
|
|
|
|
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:
|
|
|
|
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 = {'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:
|
|
|
|
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):
|
|
|
|
name = "test_name"
|
|
|
|
ability = "test_ability"
|
|
|
|
description = "test_descritption"
|
|
|
|
|
|
|
|
# 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_4, method="post", rest_path="api/v2/adversaries")
|
|
|
|
|
|
|
|
def test_add_adversary_default(self):
|
|
|
|
name = "test_name"
|
|
|
|
ability = "test_ability"
|
|
|
|
|
|
|
|
# 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_4, method="post", rest_path="api/v2/adversaries")
|
|
|
|
|
|
|
|
# set_operation_state
|
|
|
|
def test_set_operation_state(self):
|
|
|
|
operation_id = "test_opid"
|
|
|
|
state = "paused"
|
|
|
|
|
|
|
|
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
|
|
|
|
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_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.set_operation_state(operation_id, state)
|
|
|
|
|
|
|
|
def test_set_operation_state_default(self):
|
|
|
|
operation_id = "test_opid"
|
|
|
|
|
|
|
|
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
|
|
|
|
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"
|
|
|
|
|
|
|
|
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
|
|
|
|
self.cc.delete_operation(opid)
|
|
|
|
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"
|
|
|
|
|
|
|
|
with patch.object(self.cc, "__contact_server__", return_value=None) as mock_method:
|
|
|
|
self.cc.delete_adversary(adid)
|
|
|
|
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):
|
|
|
|
opdata = [{"state": "finished", "chain": [{"status": 0}]}]
|
|
|
|
opid = "does not matter"
|
|
|
|
|
|
|
|
with patch.object(self.cc, "get_operation_by_id", return_value=opdata):
|
|
|
|
res = self.cc.is_operation_finished(opid)
|
|
|
|
self.assertEqual(res, True)
|
|
|
|
|
|
|
|
def test_is_operation_finished_false(self):
|
|
|
|
opdata = [{"state": "running", "chain": [{"status": 1}]}]
|
|
|
|
opid = "does not matter"
|
|
|
|
|
|
|
|
with patch.object(self.cc, "get_operation_by_id", return_value=opdata):
|
|
|
|
res = self.cc.is_operation_finished(opid)
|
|
|
|
self.assertEqual(res, False)
|
|
|
|
|
|
|
|
def test_is_operation_finished_exception2(self):
|
|
|
|
opdata = []
|
|
|
|
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)
|
|
|
|
|
|
|
|
# TODO attack (lots of complexity !)
|
|
|
|
|
|
|
|
# TODO test fetch_client
|
|
|
|
|
|
|
|
# TODO test __contact_server__
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
unittest.main()
|