Skip to content

Commit

Permalink
Merge branch '3417-add-install-agent-plugin-archive' into develop
Browse files Browse the repository at this point in the history
Issue #3417
PR #3594
  • Loading branch information
mssalvatore committed Aug 21, 2023
2 parents dadf5dd + 36a7cd6 commit e353033
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 2 deletions.
9 changes: 9 additions & 0 deletions envs/monkey_zoo/blackbox/test_blackbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ def test_island__cannot_access_nonisland_endpoints(island):
GET_SECURITY_REPORT_ENDPOINT = "/api/report/security"
GET_ISLAND_VERSION_ENDPOINT = "/api/island/version"
PUT_AGENT_CONFIG_ENDPOINT = "/api/agent-configuration"
INSTALL_AGENT_PLUGIN_ENDPOINT = "/api/install-agent-plugin"


def test_agent__cannot_access_nonagent_endpoints(island):
Expand Down Expand Up @@ -381,6 +382,10 @@ def test_agent__cannot_access_nonagent_endpoints(island):
assert (
agent_requests.put(PUT_AGENT_CONFIG_ENDPOINT, data=None).status_code == HTTPStatus.FORBIDDEN
)
assert (
agent_requests.put(INSTALL_AGENT_PLUGIN_ENDPOINT, data=None).status_code
== HTTPStatus.FORBIDDEN
)


def test_unauthenticated_user_cannot_access_API(island):
Expand Down Expand Up @@ -447,6 +452,10 @@ def test_unauthenticated_user_cannot_access_API(island):
island_requests.put(PUT_AGENT_CONFIG_ENDPOINT, data=None).status_code
== HTTPStatus.UNAUTHORIZED
)
assert (
island_requests.put(INSTALL_AGENT_PLUGIN_ENDPOINT, data=None).status_code
== HTTPStatus.UNAUTHORIZED
)


LOGOUT_AGENT_ID = uuid4()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from common.agent_plugins import AgentPlugin, AgentPluginManifest, AgentPluginType

from . import IAgentPluginService
from .errors import PluginInstallationError
from .i_agent_plugin_repository import IAgentPluginRepository
from .plugin_archive_parser import parse_plugin

Expand Down Expand Up @@ -37,7 +38,10 @@ def get_all_plugin_manifests(self) -> Dict[AgentPluginType, Dict[str, AgentPlugi

def install_agent_plugin_archive(self, agent_plugin_archive: bytes):
with self._lock:
os_agent_plugins = parse_plugin(io.BytesIO(agent_plugin_archive))
try:
os_agent_plugins = parse_plugin(io.BytesIO(agent_plugin_archive))
except ValueError as err:
raise PluginInstallationError("Failed to install the plugin") from err

plugin = next(iter(os_agent_plugins.values()))
self._agent_plugin_repository.remove_agent_plugin(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class PluginInstallationError(ValueError):
"""
Raised when a service encounters an error while attempting to install a plugin
"""
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import logging
from http import HTTPStatus

from flask import make_response, request
from flask_security import auth_token_required, roles_accepted

from monkey_island.cc.flask_utils import AbstractResource
from monkey_island.cc.services.authentication_service import AccountRole

from .. import IAgentPluginService
from ..errors import PluginInstallationError

logger = logging.getLogger(__name__)


class InstallAgentPlugin(AbstractResource):
urls = ["/api/install-agent-plugin"]

def __init__(self, agent_plugin_service: IAgentPluginService):
self._agent_plugin_service = agent_plugin_service

@auth_token_required
@roles_accepted(AccountRole.ISLAND_INTERFACE.name)
def put(self):
"""
Install the plugin archive.
"""
try:
self._agent_plugin_service.install_agent_plugin_archive(request.data)
return make_response({}, HTTPStatus.OK)
except PluginInstallationError as err:
return make_response(str(err), HTTPStatus.UNPROCESSABLE_ENTITY)
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from .agent_plugins import AgentPlugins
from .agent_plugins_manifest import AgentPluginsManifest
from .install_agent_plugin import InstallAgentPlugin


def register_resources(api: FlaskDIWrapper):
api.add_resource(AgentPlugins)
api.add_resource(AgentPluginsManifest)
api.add_resource(InstallAgentPlugin)
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def install_agent_plugin_archive(self, agent_plugin_archive: bytes):
:param agent_plugin_archive: The archive of the plugin
:raises RemovalError: If an error occus while attempting to uninstall a previous
version of the plugin
:raises StorageError: If an error occurs while attempting to install the plugin
:raises StorageError: If an error occurs while attempting to store the plugin
:raises PluginInstallationError: If an error occurs while attempting to install the plugin
"""
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from http import HTTPStatus
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 monkey_island.cc.repositories import RetrievalError, StorageError
from monkey_island.cc.services import IAgentPluginService
from monkey_island.cc.services.agent_plugin_service.errors import PluginInstallationError
from monkey_island.cc.services.agent_plugin_service.flask_resources.install_agent_plugin import ( # noqa: E501
InstallAgentPlugin,
)

AGENT_PLUGIN = b"SomePlugin"


@pytest.fixture
def agent_plugin_service() -> IAgentPluginService:
return MagicMock(spec=IAgentPluginService)


@pytest.fixture
def flask_client(build_flask_client, agent_plugin_service):
container = StubDIContainer()
container.register_instance(IAgentPluginService, agent_plugin_service)

with build_flask_client(container) as flask_client:
yield flask_client


def test_install_plugin(agent_plugin_service, flask_client):
resp = flask_client.put(
get_url_for_resource(InstallAgentPlugin),
data=AGENT_PLUGIN,
follow_redirects=True,
)

assert resp.status_code == HTTPStatus.OK
assert agent_plugin_service.install_agent_plugin_archive.call_count == 1
assert agent_plugin_service.install_agent_plugin_archive.call_args[0][0] == AGENT_PLUGIN


def test_install_plugin__install_error(agent_plugin_service, flask_client):
agent_plugin_service.install_agent_plugin_archive = MagicMock(
side_effect=PluginInstallationError
)
resp = flask_client.put(
get_url_for_resource(InstallAgentPlugin),
data=AGENT_PLUGIN,
follow_redirects=True,
)

assert resp.status_code == HTTPStatus.UNPROCESSABLE_ENTITY


@pytest.mark.parametrize("error", [RetrievalError, StorageError, Exception])
def test_install_plugin__internal_server_error(agent_plugin_service, flask_client, error):
agent_plugin_service.install_agent_plugin_archive = MagicMock(side_effect=error)
resp = flask_client.put(
get_url_for_resource(InstallAgentPlugin),
data=AGENT_PLUGIN,
follow_redirects=True,
)

assert resp.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
from unittest.mock import MagicMock

import pytest
from tests.unit_tests.monkey_island.cc.services.agent_plugin_service.conftest import (
build_agent_plugin_tar,
)

from common import OperatingSystem
from monkey_island.cc.services.agent_plugin_service.agent_plugin_service import AgentPluginService
from monkey_island.cc.services.agent_plugin_service.errors import PluginInstallationError
from monkey_island.cc.services.agent_plugin_service.i_agent_plugin_repository import (
IAgentPluginRepository,
)
Expand Down Expand Up @@ -64,3 +68,16 @@ def test_agent_plugin_service__install_agent_plugin_archive_multi(

assert agent_plugin_repository.remove_agent_plugin.call_count == 1
assert agent_plugin_repository.store_agent_plugin.call_count == 2


def test_agent_plugin_service__plugin_install_error(
simple_agent_plugin,
plugin_data_dir: Path,
agent_plugin_service: IAgentPluginService,
build_agent_plugin_tar_with_source_tar: Callable[[Path], BinaryIO],
):
agent_plugin_tar = build_agent_plugin_tar(
simple_agent_plugin, manifest_file_name="manifest.idk"
)
with pytest.raises(PluginInstallationError):
agent_plugin_service.install_agent_plugin_archive(agent_plugin_tar.getvalue())

0 comments on commit e353033

Please sign in to comment.