diff --git a/CHANGELOG.md b/CHANGELOG.md index b637ed441..0e7a7a0d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - The "/api/netmap/nodeStates" endpoint to "/api/netmap/node-states". #1888 - All "/api/monkey_control" endpoints to "/api/monkey-control". #1888 - All "/api/monkey" endpoints to "/api/agent". #1888 +- Analytics and version update queries are sent separately instead of just one query. #2165 - Update MongoDB version to 4.4.x. #1924 - Endpoint to get agent binaries from "/api/agent/download/" to "/api/agent-binaries/". #1978 diff --git a/docs/content/FAQ/_index.md b/docs/content/FAQ/_index.md index f05f65a31..7bf848cf2 100644 --- a/docs/content/FAQ/_index.md +++ b/docs/content/FAQ/_index.md @@ -81,7 +81,6 @@ Monkey in the newly created folder. ## Reset the Monkey Island password - {{% notice warning %}} If you reset the credentials, the database will be cleared. Any findings of the Infection Monkey from previous runs will be lost.

However, you can save the Monkey's existing configuration by logging in with your current credentials and clicking on the **Export config** button on the configuration page. @@ -160,8 +159,25 @@ If internet access is available, the Infection Monkey will use the internet for The Monkey performs queries out to the Internet on two separate occasions: -1. The Infection Monkey agent checks if it has internet access by performing requests to pre-configured domains. By default, these domains are `monkey.guardicore.com` and `www.google.com`, which can be changed. The request doesn't include any extra information - it's a GET request with no extra parameters. Since the Infection Monkey is 100% open-source, you can find the domains in the configuration [here](https://github.com/guardicore/monkey/blob/85c70a3e7125217c45c751d89205e95985b279eb/monkey/infection_monkey/config.py#L152) and the code that performs the internet check [here](https://github.com/guardicore/monkey/blob/85c70a3e7125217c45c751d89205e95985b279eb/monkey/infection_monkey/network/info.py#L123). This **IS NOT** used for statistics collection. -1. After installing the Monkey Island, it sends a request to check for updates on `updates.infectionmonkey.com`. The request doesn't include any PII other than the IP address of the request. It also includes the server's deployment type (e.g., Windows Server, AppImage, Docker) and the server's version (e.g., "1.6.3"), so we can check if we have an update available for this type of deployment. Since the Infection Monkey is 100% open-source, you can inspect the code that performs this [here](https://github.com/guardicore/monkey/blob/85c70a3e7125217c45c751d89205e95985b279eb/monkey/monkey_island/cc/services/version_update.py#L37). This **IS** used for statistics collection. However, due to this data's anonymous nature, we use this to get an aggregate assumption of how many deployments we see over a specific time period - it's not used for "personal" tracking. +1. The Infection Monkey agent checks if it has internet access by performing + requests to pre-configured domains. By default, these domains are + `monkey.guardicore.com` and `www.google.com`, which can be changed. The + request doesn't include any extra information - it's a GET request with no + extra parameters. Since the Infection Monkey is 100% open-source, you can + find the domains in the configuration + [here](https://github.com/guardicore/monkey/blob/85c70a3e7125217c45c751d89205e95985b279eb/monkey/infection_monkey/config.py#L152) + and the code that performs the internet check + [here](https://github.com/guardicore/monkey/blob/85c70a3e7125217c45c751d89205e95985b279eb/monkey/infection_monkey/network/info.py#L123). + This **IS NOT** used for statistics collection. +1. After the Monkey Island starts it sends a GET request with current + deployment type to the update server to fetch the latest version and a + download link for it. This information is used by the Monkey Island to + suggest an update if one is available. No information gets collected during + this process. +1. After the Monkey Island starts it sends a GET request to the analytics + server with your deployment type and a version number. This information gets + collected on the analytics server. It is used to understand which deployment + types/versions are no longer used and can be deprecated. ## Logging and how to find logs diff --git a/monkey/monkey_island/cc/server_setup.py b/monkey/monkey_island/cc/server_setup.py index eed949a02..418074557 100644 --- a/monkey/monkey_island/cc/server_setup.py +++ b/monkey/monkey_island/cc/server_setup.py @@ -5,8 +5,11 @@ import sys from pathlib import Path import gevent.hub +import requests from gevent.pywsgi import WSGIServer +from monkey_island.cc import Version +from monkey_island.cc.deployment import Deployment from monkey_island.cc.server_utils.consts import ISLAND_PORT from monkey_island.cc.setup.config_setup import get_server_config @@ -150,6 +153,7 @@ def _start_island_server( error_log=logger, ) _log_init_info() + _send_analytics(container) http_server.serve_forever() @@ -173,3 +177,25 @@ def _log_web_interface_access_urls(): "To access the web interface, navigate to one of the the following URLs using your " f"browser: {web_interface_urls}" ) + + +ANALYTICS_URL = ( + "https://m15mjynko3.execute-api.us-east-1.amazonaws.com/default?version={" + "version}&deployment={deployment}" +) + + +def _send_analytics(di_container): + version = di_container.resolve(Version) + deployment = di_container.resolve(Deployment) + url = ANALYTICS_URL.format(deployment=deployment.value, version=version.version_number) + try: + response = requests.get(url).json() + logger.info( + f"Version number and deployment type was sent to analytics server. " + f"The response is: {response}" + ) + except requests.exceptions.ConnectionError as err: + logger.info( + f"Failed to send deployment type and version number to the analytics server: {err}" + ) diff --git a/monkey/monkey_island/cc/version.py b/monkey/monkey_island/cc/version.py index 09c3fefa1..093549b1c 100644 --- a/monkey/monkey_island/cc/version.py +++ b/monkey/monkey_island/cc/version.py @@ -1,15 +1,13 @@ import logging from threading import Event, Thread +from typing import Optional, Tuple import requests -import semantic_version from .deployment import Deployment -VERSION_SERVER_URL_PREF = "https://updates.infectionmonkey.com" -VERSION_SERVER_CHECK_NEW_URL = VERSION_SERVER_URL_PREF + "?deployment=%s&monkey_version=%s" -VERSION_SERVER_DOWNLOAD_URL = VERSION_SERVER_CHECK_NEW_URL + "&is_download=true" - +# TODO get redirects instead of using direct links to AWS +LATEST_VERSION_URL = "https://njf01cuupf.execute-api.us-east-1.amazonaws.com/default?deployment={}" LATEST_VERSION_TIMEOUT = 7 logger = logging.getLogger(__name__) @@ -55,29 +53,23 @@ class Version: return self._download_url def _set_version_metadata(self): - self._latest_version = self._get_latest_version() - self._download_url = self._get_download_link() + self._latest_version, self._download_url = self._get_version_info() self._initialization_complete.set() - def _get_latest_version(self) -> str: - url = VERSION_SERVER_CHECK_NEW_URL % (self._deployment.value, self._version_number) + def _get_version_info(self) -> Tuple[str, Optional[str]]: + url = LATEST_VERSION_URL.format(self._deployment.value) try: - reply = requests.get(url, timeout=LATEST_VERSION_TIMEOUT) + response = requests.get(url, timeout=LATEST_VERSION_TIMEOUT).json() except requests.exceptions.RequestException as err: - logger.warning(f"Failed to connect to {VERSION_SERVER_URL_PREF}: {err}") - return self._version_number + logger.warning(f"Failed to fetch version information from {url}: {err}") + return self._version_number, None - res = reply.json().get("newer_version", None) + try: + download_link = response["download_link"] + latest_version = response["version"] + except KeyError: + logger.error(f"Failed to fetch version information from {url}: {response}") + return self._version_number, None - if res is False: - return self._version_number - - if not semantic_version.validate(res): - logger.warning(f"Recieved invalid version {res} from {VERSION_SERVER_URL_PREF}") - return self._version_number - - return res.strip() - - def _get_download_link(self): - return VERSION_SERVER_DOWNLOAD_URL % (self._deployment.value, self._version_number) + return latest_version, download_link diff --git a/monkey/tests/unit_tests/monkey_island/cc/test_version.py b/monkey/tests/unit_tests/monkey_island/cc/test_version.py new file mode 100644 index 000000000..51c5def58 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/test_version.py @@ -0,0 +1,43 @@ +from unittest.mock import MagicMock + +import pytest +import requests + +from monkey_island.cc import Version +from monkey_island.cc.deployment import Deployment + +failed_response = MagicMock() +failed_response.return_value.json.return_value = {"message": "Internal server error"} + +successful_response = MagicMock() +SUCCESS_VERSION = "1.1.1" +SUCCESS_URL = "http://be_free.gov" +successful_response.return_value.json.return_value = { + "version": SUCCESS_VERSION, + "download_link": SUCCESS_URL, +} + + +@pytest.mark.parametrize( + "request_mock", + [ + failed_response, + MagicMock(side_effect=requests.exceptions.RequestException("Timeout or something")), + ], +) +def test_version__request_failed(monkeypatch, request_mock): + monkeypatch.setattr("requests.get", request_mock) + + version = Version(version_number="1.0.0", deployment=Deployment.DEVELOP) + + assert version.latest_version == "1.0.0" + assert version.download_url is None + + +def test_version__request_successful(monkeypatch): + monkeypatch.setattr("requests.get", successful_response) + + version = Version(version_number="1.0.0", deployment=Deployment.DEVELOP) + + assert version.latest_version == SUCCESS_VERSION + assert version.download_url == SUCCESS_URL