forked from p15670423/monkey
Merge pull request #2061 from guardicore/file-repository-decorators
File repository decorators
This commit is contained in:
commit
568a10e2f9
|
@ -30,6 +30,7 @@ pywin32 = {version = "*", sys_platform = "== 'win32'"} # Lock file is not create
|
||||||
pefile = {version = "*", sys_platform = "== 'win32'"} # Pyinstaller requirement on windows
|
pefile = {version = "*", sys_platform = "== 'win32'"} # Pyinstaller requirement on windows
|
||||||
marshmallow = "*"
|
marshmallow = "*"
|
||||||
marshmallow-enum = "*"
|
marshmallow-enum = "*"
|
||||||
|
readerwriterlock = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
virtualenv = ">=20.0.26"
|
virtualenv = ">=20.0.26"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "04efa1f593acdfcdc48b7089108921a46421acbacec80d8a664ec674b221dd4b"
|
"sha256": "91b8cfcf1408b3709300f47d420c550fe355df76ad396e455049fef1cceca3ad"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
@ -338,7 +338,7 @@
|
||||||
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
|
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
|
||||||
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
|
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
|
||||||
],
|
],
|
||||||
"markers": "python_full_version >= '3.5.0'",
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==3.3"
|
"version": "==3.3"
|
||||||
},
|
},
|
||||||
"importlib-metadata": {
|
"importlib-metadata": {
|
||||||
|
@ -792,6 +792,14 @@
|
||||||
"markers": "sys_platform == 'win32'",
|
"markers": "sys_platform == 'win32'",
|
||||||
"version": "==0.2.0"
|
"version": "==0.2.0"
|
||||||
},
|
},
|
||||||
|
"readerwriterlock": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:8c4b704e60d15991462081a27ef46762fea49b478aa4426644f2146754759ca7",
|
||||||
|
"sha256:b7c4cc003435d7a8ff15b312b0a62a88d9800ba6164af88991f87f8b748f9bea"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.0.9"
|
||||||
|
},
|
||||||
"requests": {
|
"requests": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f",
|
"sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f",
|
||||||
|
@ -1117,7 +1125,7 @@
|
||||||
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
|
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
|
||||||
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
|
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
|
||||||
],
|
],
|
||||||
"markers": "python_full_version >= '3.5.0'",
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==3.3"
|
"version": "==3.3"
|
||||||
},
|
},
|
||||||
"imagesize": {
|
"imagesize": {
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
from .errors import RemovalError, RetrievalError, StorageError
|
from .errors import RemovalError, RetrievalError, StorageError
|
||||||
from .i_file_repository import FileNotFoundError, IFileRepository
|
from .i_file_repository import FileNotFoundError, IFileRepository
|
||||||
from .local_storage_file_repository import LocalStorageFileRepository
|
from .local_storage_file_repository import LocalStorageFileRepository
|
||||||
|
from .file_repository_caching_decorator import FileRepositoryCachingDecorator
|
||||||
|
from .file_repository_locking_decorator import FileRepositoryLockingDecorator
|
||||||
|
from .file_repository_logging_decorator import FileRepositoryLoggingDecorator
|
||||||
from .i_agent_binary_repository import IAgentBinaryRepository
|
from .i_agent_binary_repository import IAgentBinaryRepository
|
||||||
from .agent_binary_repository import AgentBinaryRepository
|
from .agent_binary_repository import AgentBinaryRepository
|
||||||
from .i_agent_configuration_repository import IAgentConfigurationRepository
|
from .i_agent_configuration_repository import IAgentConfigurationRepository
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import io
|
||||||
|
import shutil
|
||||||
|
from functools import lru_cache
|
||||||
|
from typing import BinaryIO
|
||||||
|
|
||||||
|
from . import IFileRepository
|
||||||
|
|
||||||
|
|
||||||
|
class FileRepositoryCachingDecorator(IFileRepository):
|
||||||
|
"""
|
||||||
|
An IFileRepository decorator that provides caching for other IFileRepositories.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, file_repository: IFileRepository):
|
||||||
|
self._file_repository = file_repository
|
||||||
|
|
||||||
|
def save_file(self, unsafe_file_name: str, file_contents: BinaryIO):
|
||||||
|
self._open_file.cache_clear()
|
||||||
|
return self._file_repository.save_file(unsafe_file_name, file_contents)
|
||||||
|
|
||||||
|
def open_file(self, unsafe_file_name: str) -> BinaryIO:
|
||||||
|
original_file = self._open_file(unsafe_file_name)
|
||||||
|
file_copy = io.BytesIO()
|
||||||
|
|
||||||
|
shutil.copyfileobj(original_file, file_copy)
|
||||||
|
original_file.seek(0)
|
||||||
|
file_copy.seek(0)
|
||||||
|
|
||||||
|
return file_copy
|
||||||
|
|
||||||
|
@lru_cache(maxsize=16)
|
||||||
|
def _open_file(self, unsafe_file_name: str) -> BinaryIO:
|
||||||
|
return self._file_repository.open_file(unsafe_file_name)
|
||||||
|
|
||||||
|
def delete_file(self, unsafe_file_name: str):
|
||||||
|
self._open_file.cache_clear()
|
||||||
|
return self._file_repository.delete_file(unsafe_file_name)
|
||||||
|
|
||||||
|
def delete_all_files(self):
|
||||||
|
self._open_file.cache_clear()
|
||||||
|
return self._file_repository.delete_all_files()
|
|
@ -0,0 +1,31 @@
|
||||||
|
from typing import BinaryIO
|
||||||
|
|
||||||
|
from readerwriterlock import rwlock
|
||||||
|
|
||||||
|
from . import IFileRepository
|
||||||
|
|
||||||
|
|
||||||
|
class FileRepositoryLockingDecorator(IFileRepository):
|
||||||
|
"""
|
||||||
|
An IFileRepository decorator that makes other IFileRepositories thread-safe.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, file_repository: IFileRepository):
|
||||||
|
self._file_repository = file_repository
|
||||||
|
self._rwlock = rwlock.RWLockFair()
|
||||||
|
|
||||||
|
def save_file(self, unsafe_file_name: str, file_contents: BinaryIO):
|
||||||
|
with self._rwlock.gen_wlock():
|
||||||
|
return self._file_repository.save_file(unsafe_file_name, file_contents)
|
||||||
|
|
||||||
|
def open_file(self, unsafe_file_name: str) -> BinaryIO:
|
||||||
|
with self._rwlock.gen_rlock():
|
||||||
|
return self._file_repository.open_file(unsafe_file_name)
|
||||||
|
|
||||||
|
def delete_file(self, unsafe_file_name: str):
|
||||||
|
with self._rwlock.gen_wlock():
|
||||||
|
return self._file_repository.delete_file(unsafe_file_name)
|
||||||
|
|
||||||
|
def delete_all_files(self):
|
||||||
|
with self._rwlock.gen_wlock():
|
||||||
|
return self._file_repository.delete_all_files()
|
|
@ -0,0 +1,31 @@
|
||||||
|
import logging
|
||||||
|
from typing import BinaryIO
|
||||||
|
|
||||||
|
from . import IFileRepository
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FileRepositoryLoggingDecorator(IFileRepository):
|
||||||
|
"""
|
||||||
|
An IFileRepository decorator that provides debug logging for other IFileRepositories.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, file_repository: IFileRepository):
|
||||||
|
self._file_repository = file_repository
|
||||||
|
|
||||||
|
def save_file(self, unsafe_file_name: str, file_contents: BinaryIO):
|
||||||
|
logger.debug(f"Saving file {unsafe_file_name}")
|
||||||
|
return self._file_repository.save_file(unsafe_file_name, file_contents)
|
||||||
|
|
||||||
|
def open_file(self, unsafe_file_name: str) -> BinaryIO:
|
||||||
|
logger.debug(f"Opening file {unsafe_file_name}")
|
||||||
|
return self._file_repository.open_file(unsafe_file_name)
|
||||||
|
|
||||||
|
def delete_file(self, unsafe_file_name: str):
|
||||||
|
logger.debug(f"Deleting file {unsafe_file_name}")
|
||||||
|
return self._file_repository.delete_file(unsafe_file_name)
|
||||||
|
|
||||||
|
def delete_all_files(self):
|
||||||
|
logger.debug("Deleting all files in the repository")
|
||||||
|
return self._file_repository.delete_all_files()
|
|
@ -27,7 +27,7 @@ class IFileRepository(metaclass=abc.ABCMeta):
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def open_file(self, unsafe_file_name: str) -> BinaryIO:
|
def open_file(self, unsafe_file_name: str) -> BinaryIO:
|
||||||
"""
|
"""
|
||||||
Open a file and return a file-like object
|
Open a file and return a read-only file-like object
|
||||||
|
|
||||||
:param unsafe_file_name: An unsanitized file name that identifies the file to be opened
|
:param unsafe_file_name: An unsanitized file name that identifies the file to be opened
|
||||||
:return: A file-like object providing access to the file's contents
|
:return: A file-like object providing access to the file's contents
|
||||||
|
|
|
@ -4,10 +4,11 @@ from pathlib import Path
|
||||||
from typing import BinaryIO
|
from typing import BinaryIO
|
||||||
|
|
||||||
from common.utils.file_utils import get_all_regular_files_in_directory
|
from common.utils.file_utils import get_all_regular_files_in_directory
|
||||||
|
from monkey_island.cc import repository
|
||||||
from monkey_island.cc.repository import RemovalError, RetrievalError, StorageError
|
from monkey_island.cc.repository import RemovalError, RetrievalError, StorageError
|
||||||
from monkey_island.cc.server_utils.file_utils import create_secure_directory
|
from monkey_island.cc.server_utils.file_utils import create_secure_directory
|
||||||
|
|
||||||
from . import IFileRepository, i_file_repository
|
from . import IFileRepository
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -35,7 +36,6 @@ class LocalStorageFileRepository(IFileRepository):
|
||||||
try:
|
try:
|
||||||
safe_file_path = self._get_safe_file_path(unsafe_file_name)
|
safe_file_path = self._get_safe_file_path(unsafe_file_name)
|
||||||
|
|
||||||
logger.debug(f"Saving file contents to {safe_file_path}")
|
|
||||||
with open(safe_file_path, "wb") as dest:
|
with open(safe_file_path, "wb") as dest:
|
||||||
shutil.copyfileobj(file_contents, dest)
|
shutil.copyfileobj(file_contents, dest)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
|
@ -44,12 +44,10 @@ class LocalStorageFileRepository(IFileRepository):
|
||||||
def open_file(self, unsafe_file_name: str) -> BinaryIO:
|
def open_file(self, unsafe_file_name: str) -> BinaryIO:
|
||||||
try:
|
try:
|
||||||
safe_file_path = self._get_safe_file_path(unsafe_file_name)
|
safe_file_path = self._get_safe_file_path(unsafe_file_name)
|
||||||
|
|
||||||
logger.debug(f"Opening {safe_file_path}")
|
|
||||||
return open(safe_file_path, "rb")
|
return open(safe_file_path, "rb")
|
||||||
except FileNotFoundError as err:
|
except FileNotFoundError as err:
|
||||||
# Wrap Python's FileNotFound error, which is-an OSError, in repository.FileNotFoundError
|
# Wrap Python's FileNotFound error, which is-an OSError, in repository.FileNotFoundError
|
||||||
raise i_file_repository.FileNotFoundError(
|
raise repository.FileNotFoundError(
|
||||||
f'The requested file "{unsafe_file_name}" does not exist: {err}'
|
f'The requested file "{unsafe_file_name}" does not exist: {err}'
|
||||||
)
|
)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
|
@ -60,8 +58,6 @@ class LocalStorageFileRepository(IFileRepository):
|
||||||
def delete_file(self, unsafe_file_name: str):
|
def delete_file(self, unsafe_file_name: str):
|
||||||
try:
|
try:
|
||||||
safe_file_path = self._get_safe_file_path(unsafe_file_name)
|
safe_file_path = self._get_safe_file_path(unsafe_file_name)
|
||||||
|
|
||||||
logger.debug(f"Deleting {safe_file_path}")
|
|
||||||
safe_file_path.unlink()
|
safe_file_path.unlink()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
# This method is idempotent.
|
# This method is idempotent.
|
||||||
|
|
|
@ -12,6 +12,9 @@ from common.utils.file_utils import get_binary_io_sha256_hash
|
||||||
from monkey_island.cc.repository import (
|
from monkey_island.cc.repository import (
|
||||||
AgentBinaryRepository,
|
AgentBinaryRepository,
|
||||||
FileAgentConfigurationRepository,
|
FileAgentConfigurationRepository,
|
||||||
|
FileRepositoryCachingDecorator,
|
||||||
|
FileRepositoryLockingDecorator,
|
||||||
|
FileRepositoryLoggingDecorator,
|
||||||
FileSimulationRepository,
|
FileSimulationRepository,
|
||||||
IAgentBinaryRepository,
|
IAgentBinaryRepository,
|
||||||
IAgentConfigurationRepository,
|
IAgentConfigurationRepository,
|
||||||
|
@ -62,7 +65,8 @@ def _register_conventions(container: DIContainer, data_dir: Path):
|
||||||
|
|
||||||
def _register_repositories(container: DIContainer, data_dir: Path):
|
def _register_repositories(container: DIContainer, data_dir: Path):
|
||||||
container.register_instance(
|
container.register_instance(
|
||||||
IFileRepository, LocalStorageFileRepository(data_dir / "runtime_data")
|
IFileRepository,
|
||||||
|
_decorate_file_repository(LocalStorageFileRepository(data_dir / "runtime_data")),
|
||||||
)
|
)
|
||||||
container.register_instance(IAgentBinaryRepository, _build_agent_binary_repository())
|
container.register_instance(IAgentBinaryRepository, _build_agent_binary_repository())
|
||||||
container.register_instance(
|
container.register_instance(
|
||||||
|
@ -71,8 +75,14 @@ def _register_repositories(container: DIContainer, data_dir: Path):
|
||||||
container.register_instance(ISimulationRepository, container.resolve(FileSimulationRepository))
|
container.register_instance(ISimulationRepository, container.resolve(FileSimulationRepository))
|
||||||
|
|
||||||
|
|
||||||
|
def _decorate_file_repository(file_repository: IFileRepository) -> IFileRepository:
|
||||||
|
return FileRepositoryLockingDecorator(
|
||||||
|
FileRepositoryLoggingDecorator(FileRepositoryCachingDecorator(file_repository))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _build_agent_binary_repository():
|
def _build_agent_binary_repository():
|
||||||
file_repository = LocalStorageFileRepository(AGENT_BINARIES_PATH)
|
file_repository = _decorate_file_repository(LocalStorageFileRepository(AGENT_BINARIES_PATH))
|
||||||
agent_binary_repository = AgentBinaryRepository(file_repository)
|
agent_binary_repository = AgentBinaryRepository(file_repository)
|
||||||
|
|
||||||
_log_agent_binary_hashes(agent_binary_repository)
|
_log_agent_binary_hashes(agent_binary_repository)
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
import io
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from tests.monkey_island import SingleFileRepository
|
||||||
|
|
||||||
|
from monkey_island.cc import repository
|
||||||
|
from monkey_island.cc.repository import FileRepositoryCachingDecorator
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def file_repository():
|
||||||
|
return FileRepositoryCachingDecorator(SingleFileRepository())
|
||||||
|
|
||||||
|
|
||||||
|
def test_open_cache_file(file_repository):
|
||||||
|
file_name = "test.txt"
|
||||||
|
file_contents = b"Hello World!"
|
||||||
|
|
||||||
|
file_repository.save_file(file_name, io.BytesIO(file_contents))
|
||||||
|
assert file_repository.open_file(file_name).read() == file_contents
|
||||||
|
assert file_repository.open_file(file_name).read() == file_contents
|
||||||
|
|
||||||
|
|
||||||
|
def test_overwrite_file(file_repository):
|
||||||
|
file_name = "test.txt"
|
||||||
|
file_contents_1 = b"Hello World!"
|
||||||
|
file_contents_2 = b"Goodbye World!"
|
||||||
|
|
||||||
|
file_repository.save_file(file_name, io.BytesIO(file_contents_1))
|
||||||
|
assert file_repository.open_file(file_name).read() == file_contents_1
|
||||||
|
|
||||||
|
file_repository.save_file(file_name, io.BytesIO(file_contents_2))
|
||||||
|
assert file_repository.open_file(file_name).read() == file_contents_2
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_file(file_repository):
|
||||||
|
file_name = "test.txt"
|
||||||
|
file_contents = b"Hello World!"
|
||||||
|
|
||||||
|
file_repository.save_file(file_name, io.BytesIO(file_contents))
|
||||||
|
file_repository.delete_file(file_name)
|
||||||
|
|
||||||
|
with pytest.raises(repository.FileNotFoundError):
|
||||||
|
file_repository.open_file(file_name)
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_all_files(file_repository):
|
||||||
|
file_name = "test.txt"
|
||||||
|
file_contents = b"Hello World!"
|
||||||
|
|
||||||
|
file_repository.save_file(file_name, io.BytesIO(file_contents))
|
||||||
|
file_repository.delete_all_files()
|
||||||
|
|
||||||
|
with pytest.raises(repository.FileNotFoundError):
|
||||||
|
file_repository.open_file(file_name)
|
Loading…
Reference in New Issue