forked from p34709852/monkey
Merge pull request #2381 from guardicore/2362-add-machines-endpoint
2362 add machines endpoint
This commit is contained in:
commit
f9e74d4f03
|
@ -26,6 +26,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- `/api/agents` endpoint. #2362
|
- `/api/agents` endpoint. #2362
|
||||||
- `/api/agent-signals` endpoint. #2261
|
- `/api/agent-signals` endpoint. #2261
|
||||||
- `/api/agent-logs/<uuid:agent_id>` endpoint. #2274
|
- `/api/agent-logs/<uuid:agent_id>` endpoint. #2274
|
||||||
|
- `/api/machines` endpoint. #2362
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Reset workflow. Now it's possible to delete data gathered by agents without
|
- Reset workflow. Now it's possible to delete data gathered by agents without
|
||||||
|
|
|
@ -20,6 +20,7 @@ from monkey_island.cc.resources import (
|
||||||
ClearSimulationData,
|
ClearSimulationData,
|
||||||
IPAddresses,
|
IPAddresses,
|
||||||
IslandLog,
|
IslandLog,
|
||||||
|
Machines,
|
||||||
PBAFileDownload,
|
PBAFileDownload,
|
||||||
PBAFileUpload,
|
PBAFileUpload,
|
||||||
PropagationCredentials,
|
PropagationCredentials,
|
||||||
|
@ -173,6 +174,7 @@ def init_restful_endpoints(api: FlaskDIWrapper):
|
||||||
api.add_resource(AgentBinaries)
|
api.add_resource(AgentBinaries)
|
||||||
api.add_resource(NetMap)
|
api.add_resource(NetMap)
|
||||||
api.add_resource(Edge)
|
api.add_resource(Edge)
|
||||||
|
api.add_resource(Machines)
|
||||||
api.add_resource(Node)
|
api.add_resource(Node)
|
||||||
api.add_resource(NodeStates)
|
api.add_resource(NodeStates)
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,15 @@ class IMachineRepository(ABC):
|
||||||
:raises RetrievalError: If an error occurs while attempting to retrieve the `Machine`
|
:raises RetrievalError: If an error occurs while attempting to retrieve the `Machine`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_machines(self) -> Sequence[Machine]:
|
||||||
|
"""
|
||||||
|
Get all machines in the repository
|
||||||
|
|
||||||
|
:return: A sequence of all stored `Machine`s
|
||||||
|
:raises RetrievalError: If an error occurs while attempting to retrieve the `Machine`s
|
||||||
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_machines_by_ip(self, ip: IPv4Address) -> Sequence[Machine]:
|
def get_machines_by_ip(self, ip: IPv4Address) -> Sequence[Machine]:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -69,6 +69,14 @@ class MongoMachineRepository(IMachineRepository):
|
||||||
|
|
||||||
return Machine(**machine_dict)
|
return Machine(**machine_dict)
|
||||||
|
|
||||||
|
def get_machines(self) -> Sequence[Machine]:
|
||||||
|
try:
|
||||||
|
cursor = self._machines_collection.find({}, {MONGO_OBJECT_ID_KEY: False})
|
||||||
|
except Exception as err:
|
||||||
|
raise RetrievalError(f"Error retrieving machines: {err}")
|
||||||
|
|
||||||
|
return [Machine(**m) for m in cursor]
|
||||||
|
|
||||||
def get_machines_by_ip(self, ip: IPv4Address) -> Sequence[Machine]:
|
def get_machines_by_ip(self, ip: IPv4Address) -> Sequence[Machine]:
|
||||||
ip_regex = "^" + str(ip).replace(".", "\\.") + "\\/.*$"
|
ip_regex = "^" + str(ip).replace(".", "\\.") + "\\/.*$"
|
||||||
query = {"network_interfaces": {"$elemMatch": {"$regex": ip_regex}}}
|
query = {"network_interfaces": {"$elemMatch": {"$regex": ip_regex}}}
|
||||||
|
@ -78,7 +86,7 @@ class MongoMachineRepository(IMachineRepository):
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
raise RetrievalError(f'Error retrieving machines with ip "{ip}": {err}')
|
raise RetrievalError(f'Error retrieving machines with ip "{ip}": {err}')
|
||||||
|
|
||||||
machines = list(map(lambda m: Machine(**m), cursor))
|
machines = [Machine(**m) for m in cursor]
|
||||||
|
|
||||||
if len(machines) == 0:
|
if len(machines) == 0:
|
||||||
raise UnknownRecordError(f'No machines found with IP "{ip}"')
|
raise UnknownRecordError(f'No machines found with IP "{ip}"')
|
||||||
|
|
|
@ -12,3 +12,4 @@ from .agent_events import AgentEvents
|
||||||
from .agents import Agents
|
from .agents import Agents
|
||||||
from .agent_signals import AgentSignals, TerminateAllAgents
|
from .agent_signals import AgentSignals, TerminateAllAgents
|
||||||
from .agent_logs import AgentLogs
|
from .agent_logs import AgentLogs
|
||||||
|
from .machines import Machines
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from monkey_island.cc.repository import IMachineRepository
|
||||||
|
from monkey_island.cc.resources.AbstractResource import AbstractResource
|
||||||
|
from monkey_island.cc.resources.request_authentication import jwt_required
|
||||||
|
|
||||||
|
|
||||||
|
class Machines(AbstractResource):
|
||||||
|
urls = ["/api/machines"]
|
||||||
|
|
||||||
|
def __init__(self, machine_repository: IMachineRepository):
|
||||||
|
self._machine_repository = machine_repository
|
||||||
|
|
||||||
|
@jwt_required
|
||||||
|
def get(self):
|
||||||
|
return self._machine_repository.get_machines(), HTTPStatus.OK
|
|
@ -93,6 +93,11 @@ def machine_repository(mongo_client) -> IMachineRepository:
|
||||||
return MongoMachineRepository(mongo_client)
|
return MongoMachineRepository(mongo_client)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def empty_machine_repository() -> IMachineRepository:
|
||||||
|
return MongoMachineRepository(mongomock.MongoClient())
|
||||||
|
|
||||||
|
|
||||||
def test_get_new_id__unique_id(machine_repository):
|
def test_get_new_id__unique_id(machine_repository):
|
||||||
new_machine_id = machine_repository.get_new_id()
|
new_machine_id = machine_repository.get_new_id()
|
||||||
|
|
||||||
|
@ -107,8 +112,7 @@ def test_get_new_id__multiple_unique_ids(machine_repository):
|
||||||
assert id_1 != id_2
|
assert id_1 != id_2
|
||||||
|
|
||||||
|
|
||||||
def test_get_new_id__new_id_for_empty_repo(machine_repository):
|
def test_get_new_id__new_id_for_empty_repo(empty_machine_repository):
|
||||||
empty_machine_repository = MongoMachineRepository(mongomock.MongoClient())
|
|
||||||
id_1 = empty_machine_repository.get_new_id()
|
id_1 = empty_machine_repository.get_new_id()
|
||||||
id_2 = empty_machine_repository.get_new_id()
|
id_2 = empty_machine_repository.get_new_id()
|
||||||
|
|
||||||
|
@ -202,7 +206,7 @@ def test_get_machine_by_hardware_id__retrieval_error(error_raising_machine_repos
|
||||||
error_raising_machine_repository.get_machine_by_hardware_id(1)
|
error_raising_machine_repository.get_machine_by_hardware_id(1)
|
||||||
|
|
||||||
|
|
||||||
def test_get_machine_by_ip(machine_repository):
|
def test_get_machines_by_ip(machine_repository):
|
||||||
expected_machine = MACHINES[0]
|
expected_machine = MACHINES[0]
|
||||||
expected_machine_ip = expected_machine.network_interfaces[0].ip
|
expected_machine_ip = expected_machine.network_interfaces[0].ip
|
||||||
|
|
||||||
|
@ -212,7 +216,7 @@ def test_get_machine_by_ip(machine_repository):
|
||||||
assert retrieved_machines[0] == expected_machine
|
assert retrieved_machines[0] == expected_machine
|
||||||
|
|
||||||
|
|
||||||
def test_get_machine_by_ip__multiple_results(machine_repository):
|
def test_get_machines_by_ip__multiple_results(machine_repository):
|
||||||
search_ip = MACHINES[3].network_interfaces[0].ip
|
search_ip = MACHINES[3].network_interfaces[0].ip
|
||||||
|
|
||||||
retrieved_machines = machine_repository.get_machines_by_ip(search_ip)
|
retrieved_machines = machine_repository.get_machines_by_ip(search_ip)
|
||||||
|
@ -222,16 +226,35 @@ def test_get_machine_by_ip__multiple_results(machine_repository):
|
||||||
assert MACHINES[3] in retrieved_machines
|
assert MACHINES[3] in retrieved_machines
|
||||||
|
|
||||||
|
|
||||||
def test_get_machine_by_ip__not_found(machine_repository):
|
def test_get_machines_by_ip__not_found(machine_repository):
|
||||||
with pytest.raises(UnknownRecordError):
|
with pytest.raises(UnknownRecordError):
|
||||||
machine_repository.get_machines_by_ip("1.1.1.1")
|
machine_repository.get_machines_by_ip("1.1.1.1")
|
||||||
|
|
||||||
|
|
||||||
def test_get_machine_by_ip__retrieval_error(error_raising_machine_repository):
|
def test_get_machines_by_ip__retrieval_error(error_raising_machine_repository):
|
||||||
with pytest.raises(RetrievalError):
|
with pytest.raises(RetrievalError):
|
||||||
error_raising_machine_repository.get_machines_by_ip("1.1.1.1")
|
error_raising_machine_repository.get_machines_by_ip("1.1.1.1")
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_machines(machine_repository):
|
||||||
|
retrieved_machines = machine_repository.get_machines()
|
||||||
|
|
||||||
|
assert len(retrieved_machines) == len(MACHINES)
|
||||||
|
for machine in MACHINES:
|
||||||
|
assert machine in retrieved_machines
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_machines__empty_repository(empty_machine_repository):
|
||||||
|
retrieved_machines = empty_machine_repository.get_machines()
|
||||||
|
|
||||||
|
assert len(retrieved_machines) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_machines__retrieval_error(error_raising_machine_repository):
|
||||||
|
with pytest.raises(RetrievalError):
|
||||||
|
error_raising_machine_repository.get_machines()
|
||||||
|
|
||||||
|
|
||||||
def test_reset(machine_repository):
|
def test_reset(machine_repository):
|
||||||
# Ensure the repository is not empty
|
# Ensure the repository is not empty
|
||||||
preexisting_machine = machine_repository.get_machine_by_id(MACHINES[0].id)
|
preexisting_machine = machine_repository.get_machine_by_id(MACHINES[0].id)
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
from ipaddress import IPv4Interface
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from tests.common import StubDIContainer
|
||||||
|
from tests.unit_tests.monkey_island.conftest import get_url_for_resource
|
||||||
|
|
||||||
|
from common import OperatingSystem
|
||||||
|
from monkey_island.cc.models import Machine
|
||||||
|
from monkey_island.cc.repository import IMachineRepository, RetrievalError
|
||||||
|
from monkey_island.cc.resources import Machines
|
||||||
|
|
||||||
|
MACHINES_URL = get_url_for_resource(Machines)
|
||||||
|
MACHINES = [
|
||||||
|
Machine(
|
||||||
|
id=1,
|
||||||
|
hardware_id=101,
|
||||||
|
island=True,
|
||||||
|
network_interfaces=[IPv4Interface("10.10.10.1")],
|
||||||
|
operating_system=OperatingSystem.WINDOWS,
|
||||||
|
),
|
||||||
|
Machine(
|
||||||
|
id=2,
|
||||||
|
hardware_id=102,
|
||||||
|
island=False,
|
||||||
|
network_interfaces=[IPv4Interface("10.10.10.2/24")],
|
||||||
|
operating_system=OperatingSystem.LINUX,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_machine_repository() -> IMachineRepository:
|
||||||
|
machine_repository = MagicMock(spec=IMachineRepository)
|
||||||
|
return machine_repository
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def flask_client(build_flask_client, mock_machine_repository):
|
||||||
|
container = StubDIContainer()
|
||||||
|
|
||||||
|
container.register_instance(IMachineRepository, mock_machine_repository)
|
||||||
|
|
||||||
|
with build_flask_client(container) as flask_client:
|
||||||
|
yield flask_client
|
||||||
|
|
||||||
|
|
||||||
|
def test_machines_get__status_ok(flask_client, mock_machine_repository):
|
||||||
|
mock_machine_repository.get_machines = MagicMock(return_value=MACHINES)
|
||||||
|
|
||||||
|
resp = flask_client.get(MACHINES_URL)
|
||||||
|
|
||||||
|
assert resp.status_code == HTTPStatus.OK
|
||||||
|
|
||||||
|
|
||||||
|
def test_machines_get__gets_machines(flask_client, mock_machine_repository):
|
||||||
|
mock_machine_repository.get_machines = MagicMock(return_value=MACHINES)
|
||||||
|
|
||||||
|
resp = flask_client.get(MACHINES_URL)
|
||||||
|
|
||||||
|
response_machines = [Machine(**m) for m in resp.json]
|
||||||
|
for machine in response_machines:
|
||||||
|
assert machine in MACHINES
|
||||||
|
|
||||||
|
|
||||||
|
def test_machines_get__gets_no_machines(flask_client, mock_machine_repository):
|
||||||
|
mock_machine_repository.get_machines = MagicMock(return_value=[])
|
||||||
|
|
||||||
|
resp = flask_client.get(MACHINES_URL)
|
||||||
|
|
||||||
|
assert resp.json == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_machines_get__retrieval_error(flask_client, mock_machine_repository):
|
||||||
|
mock_machine_repository.get_machines = MagicMock(side_effect=RetrievalError)
|
||||||
|
|
||||||
|
resp = flask_client.get(MACHINES_URL)
|
||||||
|
|
||||||
|
assert resp.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
|
|
@ -1,7 +1,7 @@
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import Set
|
from typing import Set, Type
|
||||||
|
|
||||||
import flask_restful
|
import flask_restful
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -47,7 +47,7 @@ def mock_flask_resource_manager(container):
|
||||||
return flask_resource_manager
|
return flask_resource_manager
|
||||||
|
|
||||||
|
|
||||||
def get_url_for_resource(resource: AbstractResource, **kwargs):
|
def get_url_for_resource(resource: Type[AbstractResource], **kwargs):
|
||||||
chosen_url = None
|
chosen_url = None
|
||||||
for url in resource.urls:
|
for url in resource.urls:
|
||||||
if _get_url_keywords(url) == set(kwargs.keys()):
|
if _get_url_keywords(url) == set(kwargs.keys()):
|
||||||
|
|
Loading…
Reference in New Issue