diff --git a/CHANGELOG.md b/CHANGELOG.md index 72782b3e4..57c7dbe75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). encrypting files. #1242 - `/api/agents` endpoint. - `/api/agent-signals` endpoint. #2261 +- `/api/agent-logs/` endpoint. #2274 ### Changed - Reset workflow. Now it's possible to delete data gathered by agents without diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 494f48dd0..5e0aaadb2 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -14,6 +14,7 @@ from monkey_island.cc.resources import ( AgentBinaries, AgentConfiguration, AgentEvents, + AgentLogs, Agents, AgentSignals, ClearSimulationData, @@ -185,6 +186,7 @@ def init_restful_endpoints(api: FlaskDIWrapper): api.add_resource(ZeroTrustFindingEvent) api.add_resource(TelemetryFeed) api.add_resource(Log) + api.add_resource(AgentLogs) api.add_resource(IslandLog) api.add_resource(IPAddresses) diff --git a/monkey/monkey_island/cc/resources/__init__.py b/monkey/monkey_island/cc/resources/__init__.py index 937766e2b..e6c2db648 100644 --- a/monkey/monkey_island/cc/resources/__init__.py +++ b/monkey/monkey_island/cc/resources/__init__.py @@ -11,3 +11,4 @@ from .pba_file_download import PBAFileDownload from .agent_events import AgentEvents from .agents import Agents from .agent_signals import AgentSignals, TerminateAllAgents +from .agent_logs import AgentLogs diff --git a/monkey/monkey_island/cc/resources/agent_logs.py b/monkey/monkey_island/cc/resources/agent_logs.py new file mode 100644 index 000000000..40152c0fb --- /dev/null +++ b/monkey/monkey_island/cc/resources/agent_logs.py @@ -0,0 +1,35 @@ +import logging +from http import HTTPStatus + +from flask import request + +from common.types import AgentID +from monkey_island.cc.repository import IAgentLogRepository, UnknownRecordError +from monkey_island.cc.resources.AbstractResource import AbstractResource +from monkey_island.cc.resources.request_authentication import jwt_required + +logger = logging.getLogger(__name__) + + +class AgentLogs(AbstractResource): + urls = ["/api/agent-logs/"] + + def __init__(self, agent_log_repository: IAgentLogRepository): + self._agent_log_repository = agent_log_repository + + @jwt_required + def get(self, agent_id: AgentID): + try: + log_contents = self._agent_log_repository.get_agent_log(agent_id) + except UnknownRecordError as err: + logger.error(f"Error occurred while getting agent log: {err}") + return {}, HTTPStatus.NOT_FOUND + + return log_contents, HTTPStatus.OK + + def put(self, agent_id: AgentID): + log_contents = request.json + + self._agent_log_repository.upsert_agent_log(agent_id, log_contents) + + return {}, HTTPStatus.NO_CONTENT diff --git a/monkey/monkey_island/cc/resources/agent_signals/agent_signals.py b/monkey/monkey_island/cc/resources/agent_signals/agent_signals.py index f6ce22f71..1103722b5 100644 --- a/monkey/monkey_island/cc/resources/agent_signals/agent_signals.py +++ b/monkey/monkey_island/cc/resources/agent_signals/agent_signals.py @@ -1,6 +1,7 @@ import logging from http import HTTPStatus +from common.types import AgentID from monkey_island.cc.resources.AbstractResource import AbstractResource from monkey_island.cc.services import AgentSignalsService @@ -8,7 +9,7 @@ logger = logging.getLogger(__name__) class AgentSignals(AbstractResource): - urls = ["/api/agent-signals/"] + urls = ["/api/agent-signals/"] def __init__( self, @@ -16,6 +17,6 @@ class AgentSignals(AbstractResource): ): self._agent_signals_service = agent_signals_service - def get(self, agent_id: str): + def get(self, agent_id: AgentID): agent_signals = self._agent_signals_service.get_signals(agent_id) return agent_signals.dict(simplify=True), HTTPStatus.OK diff --git a/monkey/tests/monkey_island/__init__.py b/monkey/tests/monkey_island/__init__.py index 9dd1996f9..230c1d7b0 100644 --- a/monkey/tests/monkey_island/__init__.py +++ b/monkey/tests/monkey_island/__init__.py @@ -4,4 +4,5 @@ from .open_error_file_repository import OpenErrorFileRepository from .in_memory_agent_configuration_repository import InMemoryAgentConfigurationRepository from .in_memory_simulation_configuration import InMemorySimulationRepository from .in_memory_credentials_repository import InMemoryCredentialsRepository +from .in_memory_agent_log_repository import InMemoryAgentLogRepository from .in_memory_file_repository import InMemoryFileRepository diff --git a/monkey/tests/monkey_island/in_memory_agent_log_repository.py b/monkey/tests/monkey_island/in_memory_agent_log_repository.py new file mode 100644 index 000000000..fb75ad7a7 --- /dev/null +++ b/monkey/tests/monkey_island/in_memory_agent_log_repository.py @@ -0,0 +1,20 @@ +from common.types import AgentID +from monkey_island.cc.repository import IAgentLogRepository, UnknownRecordError + + +class InMemoryAgentLogRepository(IAgentLogRepository): + def __init__(self): + self._agent_logs = {} + + def upsert_agent_log(self, agent_id: AgentID, log_contents: str): + if agent_id not in self._agent_logs.keys(): + self._agent_logs[agent_id] = log_contents + + def get_agent_log(self, agent_id: AgentID) -> str: + if agent_id not in self._agent_logs: + raise UnknownRecordError(f"Unknown agent {agent_id}") + + return self._agent_logs[agent_id] + + def reset(self): + self._agent_logs = {} diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_logs.py b/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_logs.py new file mode 100644 index 000000000..0f7e86b53 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/test_agent_logs.py @@ -0,0 +1,39 @@ +from http import HTTPStatus +from uuid import UUID + +import pytest +from tests.common import StubDIContainer +from tests.monkey_island import InMemoryAgentLogRepository + +from monkey_island.cc.repository import IAgentLogRepository + +AGENT_ID_1 = UUID("c0dd10b3-e21a-4da9-9d96-a99c19ebd7c5") +AGENT_ID_2 = UUID("f811ad00-5a68-4437-bd51-7b5cc1768ad5") + +AGENT_LOGS_URL_1 = f"/api/agent-logs/{AGENT_ID_1}" +AGENT_LOGS_URL_2 = f"/api/agent-logs/{AGENT_ID_2}" + + +@pytest.fixture +def flask_client(build_flask_client): + container = StubDIContainer() + container.register_instance(IAgentLogRepository, InMemoryAgentLogRepository()) + + with build_flask_client(container) as flask_client: + yield flask_client + + +def test_agent_logs_endpoint__get_empty(flask_client): + resp = flask_client.get(AGENT_LOGS_URL_1, follow_redirects=True) + assert resp.status_code == HTTPStatus.NOT_FOUND + assert resp.json == {} + + +@pytest.mark.parametrize( + "url,log", [(AGENT_LOGS_URL_1, "LoremIpsum1"), (AGENT_LOGS_URL_2, "SecondLoremIpsum")] +) +def test_agent_logs_endpoint(flask_client, url, log): + flask_client.put(url, json=log, follow_redirects=True) + resp = flask_client.get(url, follow_redirects=True) + assert resp.status_code == HTTPStatus.OK + assert resp.json == log