Agent: Refactor ElasticSearchFingerprinter

* Rename ElasticFinger -> ElasticSearchFingerprinter
* Don't scan port if port is closed or not configured
* Refactor code to conform to the IFingerprinter interface
* Add unit tests
This commit is contained in:
Mike Salvatore 2022-02-08 12:43:10 -05:00
parent 3451724641
commit cc3be599d7
2 changed files with 128 additions and 29 deletions

View File

@ -1,48 +1,64 @@
import json
import logging
from contextlib import closing
from typing import Any, Dict
import requests
from requests.exceptions import ConnectionError, Timeout
import infection_monkey.config
from common.common_consts.network_consts import ES_SERVICE
from infection_monkey.network.HostFinger import HostFinger
from infection_monkey.i_puppet import (
FingerprintData,
IFingerprinter,
PingScanData,
PortScanData,
PortStatus,
)
ES_PORT = 9200
ES_HTTP_TIMEOUT = 5
logger = logging.getLogger(__name__)
class ElasticFinger(HostFinger):
class ElasticSearchFingerprinter(IFingerprinter):
"""
Fingerprints elastic search clusters, only on port 9200
"""
_SCANNED_SERVICE = "Elastic search"
def get_host_fingerprint(
self,
host: str,
_ping_scan_data: PingScanData,
port_scan_data: Dict[int, PortScanData],
_options: Dict,
) -> FingerprintData:
services = {}
def __init__(self):
self._config = infection_monkey.config.WormConfiguration
if (ES_PORT not in port_scan_data) or (port_scan_data[ES_PORT].status != PortStatus.OPEN):
return FingerprintData(None, None, services)
def get_host_fingerprint(self, host):
"""
Returns elasticsearch metadata
:param host:
:return: Success/failure, data is saved in the host struct
"""
try:
url = "http://%s:%s/" % (host.ip_addr, ES_PORT)
with closing(requests.get(url, timeout=ES_HTTP_TIMEOUT)) as req:
data = json.loads(req.text)
self.init_service(host.services, ES_SERVICE, ES_PORT)
host.services[ES_SERVICE]["cluster_name"] = data["cluster_name"]
host.services[ES_SERVICE]["name"] = data["name"]
host.services[ES_SERVICE]["version"] = data["version"]["number"]
return True
except Timeout:
logger.debug("Got timeout while trying to read header information")
except ConnectionError: # Someone doesn't like us
logger.debug("Unknown connection error")
except KeyError:
logger.debug("Failed parsing the ElasticSearch JSOn response")
return False
elasticsearch_info = _query_elasticsearch(host)
services[ES_SERVICE] = _get_service_from_query_info(elasticsearch_info)
except Exception as ex:
logger.debug(f"Did not detect an ElasticSearch cluster: {ex}")
return FingerprintData(None, None, services)
def _query_elasticsearch(host: str) -> Dict[str, Any]:
url = "http://%s:%s/" % (host, ES_PORT)
logger.debug(f"Sending request to {url}")
with closing(requests.get(url, timeout=ES_HTTP_TIMEOUT)) as response:
return response.json()
def _get_service_from_query_info(elasticsearch_info: Dict[str, Any]) -> Dict[str, Any]:
try:
return {
"display_name": "ElasticSearch",
"port": ES_PORT,
"cluster_name": elasticsearch_info["cluster_name"],
"name": elasticsearch_info["name"],
"version": elasticsearch_info["version"]["number"],
}
except KeyError as ke:
raise Exception(f"Unable to find the key {ke} in the server's response") from ke

View File

@ -0,0 +1,83 @@
from unittest.mock import MagicMock
import pytest
from common.common_consts.network_consts import ES_SERVICE
from infection_monkey.i_puppet import PortScanData, PortStatus
from infection_monkey.network.elasticsearch_fingerprinter import ES_PORT, ElasticSearchFingerprinter
PORT_SCAN_DATA_OPEN = {ES_PORT: PortScanData(ES_PORT, PortStatus.OPEN, "", f"tcp-{ES_PORT}")}
PORT_SCAN_DATA_CLOSED = {ES_PORT: PortScanData(ES_PORT, PortStatus.CLOSED, "", f"tcp-{ES_PORT}")}
PORT_SCAN_DATA_MISSING = {
80: PortScanData(80, PortStatus.OPEN, "", "tcp-80"),
8080: PortScanData(8080, PortStatus.OPEN, "", "tcp-8080"),
}
@pytest.fixture
def fingerprinter():
return ElasticSearchFingerprinter()
def test_successful(monkeypatch, fingerprinter):
successful_server_response = {
"cluster_name": "test cluster",
"name": "test name",
"version": {"number": "1.0.0"},
}
monkeypatch.setattr(
"infection_monkey.network.elasticsearch_fingerprinter._query_elasticsearch",
lambda _: successful_server_response,
)
fingerprint_data = fingerprinter.get_host_fingerprint(
"127.0.0.1", None, PORT_SCAN_DATA_OPEN, {}
)
assert fingerprint_data.os_type is None
assert fingerprint_data.os_version is None
assert len(fingerprint_data.services.keys()) == 1
es_service = fingerprint_data.services[ES_SERVICE]
assert es_service["cluster_name"] == successful_server_response["cluster_name"]
assert es_service["version"] == successful_server_response["version"]["number"]
assert es_service["name"] == successful_server_response["name"]
@pytest.mark.parametrize("port_scan_data", [PORT_SCAN_DATA_CLOSED, PORT_SCAN_DATA_MISSING])
def test_fingerprinting_skipped_if_port_closed(monkeypatch, fingerprinter, port_scan_data):
mock_query_elasticsearch = MagicMock()
monkeypatch.setattr(
"infection_monkey.network.elasticsearch_fingerprinter._query_elasticsearch",
mock_query_elasticsearch,
)
fingerprint_data = fingerprinter.get_host_fingerprint("127.0.0.1", None, port_scan_data, {})
assert not mock_query_elasticsearch.called
assert fingerprint_data.os_type is None
assert fingerprint_data.os_version is None
assert len(fingerprint_data.services.keys()) == 0
@pytest.mark.parametrize(
"mock_query_function",
[
MagicMock(side_effect=Exception("test exception")),
MagicMock(return_value={"unexpected_key": "unexpected_value"}),
],
)
def test_no_response_from_server(monkeypatch, fingerprinter, mock_query_function):
monkeypatch.setattr(
"infection_monkey.network.elasticsearch_fingerprinter._query_elasticsearch",
mock_query_function,
)
fingerprint_data = fingerprinter.get_host_fingerprint(
"127.0.0.1", None, PORT_SCAN_DATA_OPEN, {}
)
assert fingerprint_data.os_type is None
assert fingerprint_data.os_version is None
assert len(fingerprint_data.services.keys()) == 0