Merge pull request #522 from guardicore/feature/521-environment-check
Feature/521 environment check
This commit is contained in:
commit
3550fcf51b
|
@ -0,0 +1,12 @@
|
|||
from typing import List
|
||||
|
||||
from common.cloud.aws.aws_instance import AwsInstance
|
||||
from common.cloud.azure.azure_instance import AzureInstance
|
||||
from common.cloud.gcp.gcp_instance import GcpInstance
|
||||
from common.cloud.instance import CloudInstance
|
||||
|
||||
all_cloud_instances = [AwsInstance(), AzureInstance(), GcpInstance()]
|
||||
|
||||
|
||||
def get_all_cloud_instances() -> List[CloudInstance]:
|
||||
return all_cloud_instances
|
|
@ -6,6 +6,9 @@ import logging
|
|||
|
||||
__author__ = 'itay.mizeretz'
|
||||
|
||||
from common.cloud.environment_names import Environment
|
||||
from common.cloud.instance import CloudInstance
|
||||
|
||||
AWS_INSTANCE_METADATA_LOCAL_IP_ADDRESS = "169.254.169.254"
|
||||
AWS_LATEST_METADATA_URI_PREFIX = 'http://{0}/latest/'.format(AWS_INSTANCE_METADATA_LOCAL_IP_ADDRESS)
|
||||
ACCOUNT_ID_KEY = "accountId"
|
||||
|
@ -13,10 +16,15 @@ ACCOUNT_ID_KEY = "accountId"
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AwsInstance(object):
|
||||
class AwsInstance(CloudInstance):
|
||||
"""
|
||||
Class which gives useful information about the current instance you're on.
|
||||
"""
|
||||
def is_instance(self):
|
||||
return self.instance_id is not None
|
||||
|
||||
def get_cloud_provider_name(self) -> Environment:
|
||||
return Environment.AWS
|
||||
|
||||
def __init__(self):
|
||||
self.instance_id = None
|
||||
|
@ -57,9 +65,6 @@ class AwsInstance(object):
|
|||
def get_region(self):
|
||||
return self.region
|
||||
|
||||
def is_aws_instance(self):
|
||||
return self.instance_id is not None
|
||||
|
||||
@staticmethod
|
||||
def _extract_account_id(instance_identity_document_response):
|
||||
"""
|
|
@ -4,7 +4,7 @@ import boto3
|
|||
import botocore
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
from common.cloud.aws_instance import AwsInstance
|
||||
from common.cloud.aws.aws_instance import AwsInstance
|
||||
|
||||
__author__ = ['itay.mizeretz', 'shay.nehmad']
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import logging
|
||||
import requests
|
||||
|
||||
from common.cloud.environment_names import Environment
|
||||
from common.cloud.instance import CloudInstance
|
||||
|
||||
LATEST_AZURE_METADATA_API_VERSION = "2019-04-30"
|
||||
AZURE_METADATA_SERVICE_URL = "http://169.254.169.254/metadata/instance?api-version=%s" % LATEST_AZURE_METADATA_API_VERSION
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AzureInstance(CloudInstance):
|
||||
"""
|
||||
Access to useful information about the current machine if it's an Azure VM.
|
||||
Based on Azure metadata service: https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service
|
||||
"""
|
||||
def is_instance(self):
|
||||
return self.on_azure
|
||||
|
||||
def get_cloud_provider_name(self) -> Environment:
|
||||
return Environment.AZURE
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Determines if on Azure and if so, gets some basic metadata on this instance.
|
||||
"""
|
||||
self.instance_name = None
|
||||
self.instance_id = None
|
||||
self.location = None
|
||||
self.on_azure = False
|
||||
|
||||
try:
|
||||
response = requests.get(AZURE_METADATA_SERVICE_URL, headers={"Metadata": "true"})
|
||||
self.on_azure = True
|
||||
|
||||
# If not on cloud, the metadata URL is non-routable and the connection will fail.
|
||||
# If on AWS, should get 404 since the metadata service URL is different, so bool(response) will be false.
|
||||
if response:
|
||||
logger.debug("On Azure. Trying to parse metadata.")
|
||||
self.try_parse_response(response)
|
||||
else:
|
||||
logger.warning("On Azure, but metadata response not ok: {}".format(response.status_code))
|
||||
except requests.RequestException:
|
||||
logger.debug("Failed to get response from Azure metadata service: This instance is not on Azure.")
|
||||
self.on_azure = False
|
||||
|
||||
def try_parse_response(self, response):
|
||||
try:
|
||||
response_data = response.json()
|
||||
self.instance_name = response_data["compute"]["name"]
|
||||
self.instance_id = response_data["compute"]["vmId"]
|
||||
self.location = response_data["compute"]["location"]
|
||||
except KeyError:
|
||||
logger.exception("Error while parsing response from Azure metadata service.")
|
|
@ -0,0 +1,15 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class Environment(Enum):
|
||||
UNKNOWN = "Unknown"
|
||||
ON_PREMISE = "On Premise"
|
||||
AZURE = "Azure"
|
||||
AWS = "AWS"
|
||||
GCP = "GCP"
|
||||
ALIBABA = "Alibaba Cloud"
|
||||
IBM = "IBM Cloud"
|
||||
DigitalOcean = "Digital Ocean"
|
||||
|
||||
|
||||
ALL_ENVIRONMENTS_NAMES = [x.value for x in Environment]
|
|
@ -0,0 +1,43 @@
|
|||
import logging
|
||||
import requests
|
||||
|
||||
from common.cloud.environment_names import Environment
|
||||
from common.cloud.instance import CloudInstance
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
GCP_METADATA_SERVICE_URL = "http://metadata.google.internal/"
|
||||
|
||||
|
||||
class GcpInstance(CloudInstance):
|
||||
"""
|
||||
Used to determine if on GCP. See https://cloud.google.com/compute/docs/storing-retrieving-metadata#runninggce
|
||||
"""
|
||||
def is_instance(self):
|
||||
return self.on_gcp
|
||||
|
||||
def get_cloud_provider_name(self) -> Environment:
|
||||
return Environment.GCP
|
||||
|
||||
def __init__(self):
|
||||
self.on_gcp = False
|
||||
|
||||
try:
|
||||
# If not on GCP, this domain shouldn't resolve.
|
||||
response = requests.get(GCP_METADATA_SERVICE_URL)
|
||||
|
||||
if response:
|
||||
logger.debug("Got ok metadata response: on GCP")
|
||||
self.on_gcp = True
|
||||
|
||||
if "Metadata-Flavor" not in response.headers:
|
||||
logger.warning("Got unexpected GCP Metadata format")
|
||||
else:
|
||||
if not response.headers["Metadata-Flavor"] == "Google":
|
||||
logger.warning("Got unexpected Metadata flavor: {}".format(response.headers["Metadata-Flavor"]))
|
||||
else:
|
||||
logger.warning("On GCP, but metadata response not ok: {}".format(response.status_code))
|
||||
except requests.RequestException:
|
||||
logger.debug("Failed to get response from GCP metadata service: This instance is not on GCP")
|
||||
self.on_gcp = False
|
|
@ -0,0 +1,14 @@
|
|||
from common.cloud.environment_names import Environment
|
||||
|
||||
|
||||
class CloudInstance(object):
|
||||
"""
|
||||
This is an abstract class which represents a cloud instance.
|
||||
|
||||
The current machine can be a cloud instance (for example EC2 instance or Azure VM).
|
||||
"""
|
||||
def is_instance(self) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_cloud_provider_name(self) -> Environment:
|
||||
raise NotImplementedError()
|
|
@ -1,6 +1,6 @@
|
|||
import logging
|
||||
|
||||
from common.cloud.aws_service import AwsService
|
||||
from common.cloud.aws.aws_service import AwsService
|
||||
from common.cmd.aws.aws_cmd_result import AwsCmdResult
|
||||
from common.cmd.cmd_runner import CmdRunner
|
||||
from common.cmd.cmd_status import CmdStatus
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
AWS_COLLECTOR = "AwsCollector"
|
||||
HOSTNAME_COLLECTOR = "HostnameCollector"
|
||||
ENVIRONMENT_COLLECTOR = "EnvironmentCollector"
|
||||
PROCESS_LIST_COLLECTOR = "ProcessListCollector"
|
|
@ -1,6 +1,6 @@
|
|||
import hashlib
|
||||
import os
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
from abc import ABCMeta
|
||||
|
@ -125,6 +125,7 @@ class Configuration(object):
|
|||
|
||||
finger_classes = []
|
||||
exploiter_classes = []
|
||||
system_info_collectors_classes = []
|
||||
|
||||
# how many victims to look for in a single scan iteration
|
||||
victims_max_find = 100
|
||||
|
|
|
@ -32,11 +32,17 @@ from infection_monkey.telemetry.attack.t1106_telem import T1106Telem
|
|||
from common.utils.attack_utils import ScanStatus, UsageEnum
|
||||
from infection_monkey.exploit.HostExploiter import HostExploiter
|
||||
|
||||
MAX_DEPTH_REACHED_MESSAGE = "Reached max depth, shutting down"
|
||||
|
||||
__author__ = 'itamar'
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PlannedShutdownException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InfectionMonkey(object):
|
||||
def __init__(self, args):
|
||||
self._keep_running = False
|
||||
|
@ -87,143 +93,158 @@ class InfectionMonkey(object):
|
|||
LOG.debug("Default server: %s is already in command servers list" % self._default_server)
|
||||
|
||||
def start(self):
|
||||
LOG.info("Monkey is running...")
|
||||
try:
|
||||
LOG.info("Monkey is starting...")
|
||||
|
||||
# Sets island's IP and port for monkey to communicate to
|
||||
if not self.set_default_server():
|
||||
return
|
||||
self.set_default_port()
|
||||
LOG.debug("Starting the setup phase.")
|
||||
# Sets island's IP and port for monkey to communicate to
|
||||
self.set_default_server()
|
||||
self.set_default_port()
|
||||
|
||||
# Create a dir for monkey files if there isn't one
|
||||
create_monkey_dir()
|
||||
# Create a dir for monkey files if there isn't one
|
||||
create_monkey_dir()
|
||||
|
||||
if WindowsUpgrader.should_upgrade():
|
||||
self._upgrading_to_64 = True
|
||||
self._singleton.unlock()
|
||||
LOG.info("32bit monkey running on 64bit Windows. Upgrading.")
|
||||
WindowsUpgrader.upgrade(self._opts)
|
||||
return
|
||||
self.upgrade_to_64_if_needed()
|
||||
|
||||
ControlClient.wakeup(parent=self._parent)
|
||||
ControlClient.load_control_config()
|
||||
ControlClient.wakeup(parent=self._parent)
|
||||
ControlClient.load_control_config()
|
||||
|
||||
if is_windows_os():
|
||||
T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send()
|
||||
if is_windows_os():
|
||||
T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send()
|
||||
|
||||
if not WormConfiguration.alive:
|
||||
LOG.info("Marked not alive from configuration")
|
||||
return
|
||||
self.shutdown_by_not_alive_config()
|
||||
|
||||
if firewall.is_enabled():
|
||||
firewall.add_firewall_rule()
|
||||
if firewall.is_enabled():
|
||||
firewall.add_firewall_rule()
|
||||
|
||||
monkey_tunnel = ControlClient.create_control_tunnel()
|
||||
if monkey_tunnel:
|
||||
monkey_tunnel.start()
|
||||
monkey_tunnel = ControlClient.create_control_tunnel()
|
||||
if monkey_tunnel:
|
||||
monkey_tunnel.start()
|
||||
|
||||
StateTelem(is_done=False).send()
|
||||
TunnelTelem().send()
|
||||
StateTelem(is_done=False).send()
|
||||
TunnelTelem().send()
|
||||
|
||||
LOG.debug("Starting the post-breach phase.")
|
||||
self.collect_system_info_if_configured()
|
||||
PostBreach().execute_all_configured()
|
||||
|
||||
LOG.debug("Starting the propagation phase.")
|
||||
self.shutdown_by_max_depth_reached()
|
||||
|
||||
for iteration_index in range(WormConfiguration.max_iterations):
|
||||
ControlClient.keepalive()
|
||||
ControlClient.load_control_config()
|
||||
|
||||
self._network.initialize()
|
||||
|
||||
self._fingerprint = HostFinger.get_instances()
|
||||
|
||||
self._exploiters = HostExploiter.get_classes()
|
||||
|
||||
if not self._keep_running or not WormConfiguration.alive:
|
||||
break
|
||||
|
||||
machines = self._network.get_victim_machines(max_find=WormConfiguration.victims_max_find,
|
||||
stop_callback=ControlClient.check_for_stop)
|
||||
is_empty = True
|
||||
for machine in machines:
|
||||
if ControlClient.check_for_stop():
|
||||
break
|
||||
|
||||
is_empty = False
|
||||
for finger in self._fingerprint:
|
||||
LOG.info("Trying to get OS fingerprint from %r with module %s",
|
||||
machine, finger.__class__.__name__)
|
||||
finger.get_host_fingerprint(machine)
|
||||
|
||||
ScanTelem(machine).send()
|
||||
|
||||
# skip machines that we've already exploited
|
||||
if machine in self._exploited_machines:
|
||||
LOG.debug("Skipping %r - already exploited",
|
||||
machine)
|
||||
continue
|
||||
elif machine in self._fail_exploitation_machines:
|
||||
if WormConfiguration.retry_failed_explotation:
|
||||
LOG.debug("%r - exploitation failed before, trying again", machine)
|
||||
else:
|
||||
LOG.debug("Skipping %r - exploitation failed before", machine)
|
||||
continue
|
||||
|
||||
if monkey_tunnel:
|
||||
monkey_tunnel.set_tunnel_for_host(machine)
|
||||
if self._default_server:
|
||||
if self._network.on_island(self._default_server):
|
||||
machine.set_default_server(get_interface_to_target(machine.ip_addr) +
|
||||
(':' + self._default_server_port if self._default_server_port else ''))
|
||||
else:
|
||||
machine.set_default_server(self._default_server)
|
||||
LOG.debug("Default server for machine: %r set to %s" % (machine, machine.default_server))
|
||||
|
||||
# Order exploits according to their type
|
||||
if WormConfiguration.should_exploit:
|
||||
self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value)
|
||||
host_exploited = False
|
||||
for exploiter in [exploiter(machine) for exploiter in self._exploiters]:
|
||||
if self.try_exploiting(machine, exploiter):
|
||||
host_exploited = True
|
||||
VictimHostTelem('T1210', ScanStatus.USED, machine=machine).send()
|
||||
break
|
||||
if not host_exploited:
|
||||
self._fail_exploitation_machines.add(machine)
|
||||
VictimHostTelem('T1210', ScanStatus.SCANNED, machine=machine).send()
|
||||
if not self._keep_running:
|
||||
break
|
||||
|
||||
if (not is_empty) and (WormConfiguration.max_iterations > iteration_index + 1):
|
||||
time_to_sleep = WormConfiguration.timeout_between_iterations
|
||||
LOG.info("Sleeping %d seconds before next life cycle iteration", time_to_sleep)
|
||||
time.sleep(time_to_sleep)
|
||||
|
||||
if self._keep_running and WormConfiguration.alive:
|
||||
LOG.info("Reached max iterations (%d)", WormConfiguration.max_iterations)
|
||||
elif not WormConfiguration.alive:
|
||||
LOG.info("Marked not alive from configuration")
|
||||
|
||||
# if host was exploited, before continue to closing the tunnel ensure the exploited host had its chance to
|
||||
# connect to the tunnel
|
||||
if len(self._exploited_machines) > 0:
|
||||
time_to_sleep = WormConfiguration.keep_tunnel_open_time
|
||||
LOG.info("Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep)
|
||||
time.sleep(time_to_sleep)
|
||||
|
||||
if monkey_tunnel:
|
||||
monkey_tunnel.stop()
|
||||
monkey_tunnel.join()
|
||||
except PlannedShutdownException:
|
||||
LOG.info("A planned shutdown of the Monkey occurred. Logging the reason and finishing execution.")
|
||||
LOG.exception("Planned shutdown, reason:")
|
||||
|
||||
def shutdown_by_max_depth_reached(self):
|
||||
if 0 == WormConfiguration.depth:
|
||||
TraceTelem(MAX_DEPTH_REACHED_MESSAGE).send()
|
||||
raise PlannedShutdownException(MAX_DEPTH_REACHED_MESSAGE)
|
||||
else:
|
||||
LOG.debug("Running with depth: %d" % WormConfiguration.depth)
|
||||
|
||||
def collect_system_info_if_configured(self):
|
||||
if WormConfiguration.collect_system_info:
|
||||
LOG.debug("Calling system info collection")
|
||||
system_info_collector = SystemInfoCollector()
|
||||
system_info = system_info_collector.get_info()
|
||||
SystemInfoTelem(system_info).send()
|
||||
|
||||
# Executes post breach actions
|
||||
PostBreach().execute()
|
||||
def shutdown_by_not_alive_config(self):
|
||||
if not WormConfiguration.alive:
|
||||
raise PlannedShutdownException("Marked 'not alive' from configuration.")
|
||||
|
||||
if 0 == WormConfiguration.depth:
|
||||
TraceTelem("Reached max depth, shutting down").send()
|
||||
return
|
||||
else:
|
||||
LOG.debug("Running with depth: %d" % WormConfiguration.depth)
|
||||
|
||||
for iteration_index in range(WormConfiguration.max_iterations):
|
||||
ControlClient.keepalive()
|
||||
ControlClient.load_control_config()
|
||||
|
||||
self._network.initialize()
|
||||
|
||||
self._fingerprint = HostFinger.get_instances()
|
||||
|
||||
self._exploiters = HostExploiter.get_classes()
|
||||
|
||||
if not self._keep_running or not WormConfiguration.alive:
|
||||
break
|
||||
|
||||
machines = self._network.get_victim_machines(max_find=WormConfiguration.victims_max_find,
|
||||
stop_callback=ControlClient.check_for_stop)
|
||||
is_empty = True
|
||||
for machine in machines:
|
||||
if ControlClient.check_for_stop():
|
||||
break
|
||||
|
||||
is_empty = False
|
||||
for finger in self._fingerprint:
|
||||
LOG.info("Trying to get OS fingerprint from %r with module %s",
|
||||
machine, finger.__class__.__name__)
|
||||
finger.get_host_fingerprint(machine)
|
||||
|
||||
ScanTelem(machine).send()
|
||||
|
||||
# skip machines that we've already exploited
|
||||
if machine in self._exploited_machines:
|
||||
LOG.debug("Skipping %r - already exploited",
|
||||
machine)
|
||||
continue
|
||||
elif machine in self._fail_exploitation_machines:
|
||||
if WormConfiguration.retry_failed_explotation:
|
||||
LOG.debug("%r - exploitation failed before, trying again", machine)
|
||||
else:
|
||||
LOG.debug("Skipping %r - exploitation failed before", machine)
|
||||
continue
|
||||
|
||||
if monkey_tunnel:
|
||||
monkey_tunnel.set_tunnel_for_host(machine)
|
||||
if self._default_server:
|
||||
if self._network.on_island(self._default_server):
|
||||
machine.set_default_server(get_interface_to_target(machine.ip_addr) +
|
||||
(':' + self._default_server_port if self._default_server_port else ''))
|
||||
else:
|
||||
machine.set_default_server(self._default_server)
|
||||
LOG.debug("Default server for machine: %r set to %s" % (machine, machine.default_server))
|
||||
|
||||
# Order exploits according to their type
|
||||
if WormConfiguration.should_exploit:
|
||||
self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value)
|
||||
host_exploited = False
|
||||
for exploiter in [exploiter(machine) for exploiter in self._exploiters]:
|
||||
if self.try_exploiting(machine, exploiter):
|
||||
host_exploited = True
|
||||
VictimHostTelem('T1210', ScanStatus.USED, machine=machine).send()
|
||||
break
|
||||
if not host_exploited:
|
||||
self._fail_exploitation_machines.add(machine)
|
||||
VictimHostTelem('T1210', ScanStatus.SCANNED, machine=machine).send()
|
||||
if not self._keep_running:
|
||||
break
|
||||
|
||||
if (not is_empty) and (WormConfiguration.max_iterations > iteration_index + 1):
|
||||
time_to_sleep = WormConfiguration.timeout_between_iterations
|
||||
LOG.info("Sleeping %d seconds before next life cycle iteration", time_to_sleep)
|
||||
time.sleep(time_to_sleep)
|
||||
|
||||
if self._keep_running and WormConfiguration.alive:
|
||||
LOG.info("Reached max iterations (%d)", WormConfiguration.max_iterations)
|
||||
elif not WormConfiguration.alive:
|
||||
LOG.info("Marked not alive from configuration")
|
||||
|
||||
# if host was exploited, before continue to closing the tunnel ensure the exploited host had its chance to
|
||||
# connect to the tunnel
|
||||
if len(self._exploited_machines) > 0:
|
||||
time_to_sleep = WormConfiguration.keep_tunnel_open_time
|
||||
LOG.info("Sleeping %d seconds for exploited machines to connect to tunnel", time_to_sleep)
|
||||
time.sleep(time_to_sleep)
|
||||
|
||||
if monkey_tunnel:
|
||||
monkey_tunnel.stop()
|
||||
monkey_tunnel.join()
|
||||
def upgrade_to_64_if_needed(self):
|
||||
if WindowsUpgrader.should_upgrade():
|
||||
self._upgrading_to_64 = True
|
||||
self._singleton.unlock()
|
||||
LOG.info("32bit monkey running on 64bit Windows. Upgrading.")
|
||||
WindowsUpgrader.upgrade(self._opts)
|
||||
raise PlannedShutdownException("Finished upgrading from 32bit to 64bit.")
|
||||
|
||||
def cleanup(self):
|
||||
LOG.info("Monkey cleanup started")
|
||||
|
@ -346,9 +367,11 @@ class InfectionMonkey(object):
|
|||
self._default_server_port = ''
|
||||
|
||||
def set_default_server(self):
|
||||
"""
|
||||
Sets the default server for the Monkey to communicate back to.
|
||||
:raises PlannedShutdownException if couldn't find the server.
|
||||
"""
|
||||
if not ControlClient.find_server(default_tunnel=self._default_tunnel):
|
||||
LOG.info("Monkey couldn't find server. Going down.")
|
||||
return False
|
||||
raise PlannedShutdownException("Monkey couldn't find server with {} default tunnel.".format(self._default_tunnel))
|
||||
self._default_server = WormConfiguration.current_server
|
||||
LOG.debug("default server set to: %s" % self._default_server)
|
||||
return True
|
||||
|
|
|
@ -20,7 +20,7 @@ class PostBreach(object):
|
|||
self.os_is_linux = not is_windows_os()
|
||||
self.pba_list = self.config_to_pba_list()
|
||||
|
||||
def execute(self):
|
||||
def execute_all_configured(self):
|
||||
"""
|
||||
Executes all post breach actions.
|
||||
"""
|
||||
|
|
|
@ -6,9 +6,9 @@ import psutil
|
|||
from enum import IntEnum
|
||||
|
||||
from infection_monkey.network.info import get_host_subnets
|
||||
from infection_monkey.system_info.aws_collector import AwsCollector
|
||||
from infection_monkey.system_info.azure_cred_collector import AzureCollector
|
||||
from infection_monkey.system_info.netstat_collector import NetstatCollector
|
||||
from system_info.system_info_collectors_handler import SystemInfoCollectorsHandler
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
@ -61,50 +61,12 @@ class InfoCollector(object):
|
|||
self.info = {}
|
||||
|
||||
def get_info(self):
|
||||
self.get_hostname()
|
||||
self.get_process_list()
|
||||
# Collect all hardcoded
|
||||
self.get_network_info()
|
||||
self.get_azure_info()
|
||||
self.get_aws_info()
|
||||
|
||||
def get_hostname(self):
|
||||
"""
|
||||
Adds the fully qualified computer hostname to the system information.
|
||||
:return: None. Updates class information
|
||||
"""
|
||||
LOG.debug("Reading hostname")
|
||||
self.info['hostname'] = socket.getfqdn()
|
||||
|
||||
def get_process_list(self):
|
||||
"""
|
||||
Adds process information from the host to the system information.
|
||||
Currently lists process name, ID, parent ID, command line
|
||||
and the full image path of each process.
|
||||
:return: None. Updates class information
|
||||
"""
|
||||
LOG.debug("Reading process list")
|
||||
processes = {}
|
||||
for process in psutil.process_iter():
|
||||
try:
|
||||
processes[process.pid] = {"name": process.name(),
|
||||
"pid": process.pid,
|
||||
"ppid": process.ppid(),
|
||||
"cmdline": " ".join(process.cmdline()),
|
||||
"full_image_path": process.exe(),
|
||||
}
|
||||
except (psutil.AccessDenied, WindowsError):
|
||||
# we may be running as non root
|
||||
# and some processes are impossible to acquire in Windows/Linux
|
||||
# in this case we'll just add what we can
|
||||
processes[process.pid] = {"name": "null",
|
||||
"pid": process.pid,
|
||||
"ppid": process.ppid(),
|
||||
"cmdline": "ACCESS DENIED",
|
||||
"full_image_path": "null",
|
||||
}
|
||||
continue
|
||||
|
||||
self.info['process_list'] = processes
|
||||
# Collect all plugins
|
||||
SystemInfoCollectorsHandler().execute_all_configured()
|
||||
|
||||
def get_network_info(self):
|
||||
"""
|
||||
|
@ -150,11 +112,3 @@ class InfoCollector(object):
|
|||
except Exception:
|
||||
# If we failed to collect azure info, no reason to fail all the collection. Log and continue.
|
||||
LOG.error("Failed collecting Azure info.", exc_info=True)
|
||||
|
||||
def get_aws_info(self):
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
self.info['aws'] = AwsCollector().get_aws_info()
|
||||
except Exception:
|
||||
# If we failed to collect aws info, no reason to fail all the collection. Log and continue.
|
||||
LOG.error("Failed collecting AWS info.", exc_info=True)
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
import logging
|
||||
|
||||
from common.cloud.aws_instance import AwsInstance
|
||||
|
||||
__author__ = 'itay.mizeretz'
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AwsCollector(object):
|
||||
"""
|
||||
Extract info from AWS machines
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_aws_info():
|
||||
LOG.info("Collecting AWS info")
|
||||
aws = AwsInstance()
|
||||
info = {}
|
||||
if aws.is_aws_instance():
|
||||
LOG.info("Machine is an AWS instance")
|
||||
info = \
|
||||
{
|
||||
'instance_id': aws.get_instance_id()
|
||||
}
|
||||
else:
|
||||
LOG.info("Machine is NOT an AWS instance")
|
||||
|
||||
return info
|
|
@ -0,0 +1,3 @@
|
|||
"""
|
||||
This package holds all the dynamic (plugin) collectors
|
||||
"""
|
|
@ -0,0 +1,31 @@
|
|||
import logging
|
||||
|
||||
from common.cloud.aws.aws_instance import AwsInstance
|
||||
from common.data.system_info_collectors_names import AWS_COLLECTOR
|
||||
from infection_monkey.system_info.system_info_collector import SystemInfoCollector
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AwsCollector(SystemInfoCollector):
|
||||
"""
|
||||
Extract info from AWS machines.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__(name=AWS_COLLECTOR)
|
||||
|
||||
def collect(self) -> dict:
|
||||
logger.info("Collecting AWS info")
|
||||
aws = AwsInstance()
|
||||
info = {}
|
||||
if aws.is_instance():
|
||||
logger.info("Machine is an AWS instance")
|
||||
info = \
|
||||
{
|
||||
'instance_id': aws.get_instance_id()
|
||||
}
|
||||
else:
|
||||
logger.info("Machine is NOT an AWS instance")
|
||||
|
||||
return info
|
|
@ -0,0 +1,24 @@
|
|||
from common.cloud.all_instances import get_all_cloud_instances
|
||||
from common.cloud.environment_names import Environment
|
||||
from common.data.system_info_collectors_names import ENVIRONMENT_COLLECTOR
|
||||
from infection_monkey.system_info.system_info_collector import SystemInfoCollector
|
||||
|
||||
|
||||
def get_monkey_environment() -> Environment:
|
||||
"""
|
||||
Get the Monkey's running environment.
|
||||
:return: One of the cloud providers if on cloud; otherwise, assumes "on premise".
|
||||
"""
|
||||
for instance in get_all_cloud_instances():
|
||||
if instance.is_instance():
|
||||
return instance.get_cloud_provider_name()
|
||||
|
||||
return Environment.ON_PREMISE
|
||||
|
||||
|
||||
class EnvironmentCollector(SystemInfoCollector):
|
||||
def __init__(self):
|
||||
super().__init__(name=ENVIRONMENT_COLLECTOR)
|
||||
|
||||
def collect(self) -> dict:
|
||||
return {"environment": get_monkey_environment()}
|
|
@ -0,0 +1,16 @@
|
|||
import logging
|
||||
import socket
|
||||
|
||||
from common.data.system_info_collectors_names import HOSTNAME_COLLECTOR
|
||||
from infection_monkey.system_info.system_info_collector import SystemInfoCollector
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HostnameCollector(SystemInfoCollector):
|
||||
def __init__(self):
|
||||
super().__init__(name=HOSTNAME_COLLECTOR)
|
||||
|
||||
def collect(self) -> dict:
|
||||
return {"hostname": socket.getfqdn()}
|
|
@ -0,0 +1,50 @@
|
|||
import logging
|
||||
import psutil
|
||||
|
||||
from common.data.system_info_collectors_names import PROCESS_LIST_COLLECTOR
|
||||
from infection_monkey.system_info.system_info_collector import SystemInfoCollector
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Linux doesn't have WindowsError
|
||||
try:
|
||||
WindowsError
|
||||
except NameError:
|
||||
# noinspection PyShadowingBuiltins
|
||||
WindowsError = psutil.AccessDenied
|
||||
|
||||
|
||||
class ProcessListCollector(SystemInfoCollector):
|
||||
def __init__(self):
|
||||
super().__init__(name=PROCESS_LIST_COLLECTOR)
|
||||
|
||||
def collect(self) -> dict:
|
||||
"""
|
||||
Adds process information from the host to the system information.
|
||||
Currently lists process name, ID, parent ID, command line
|
||||
and the full image path of each process.
|
||||
"""
|
||||
logger.debug("Reading process list")
|
||||
processes = {}
|
||||
for process in psutil.process_iter():
|
||||
try:
|
||||
processes[process.pid] = {
|
||||
"name": process.name(),
|
||||
"pid": process.pid,
|
||||
"ppid": process.ppid(),
|
||||
"cmdline": " ".join(process.cmdline()),
|
||||
"full_image_path": process.exe(),
|
||||
}
|
||||
except (psutil.AccessDenied, WindowsError):
|
||||
# we may be running as non root and some processes are impossible to acquire in Windows/Linux.
|
||||
# In this case we'll just add what we know.
|
||||
processes[process.pid] = {
|
||||
"name": "null",
|
||||
"pid": process.pid,
|
||||
"ppid": process.ppid(),
|
||||
"cmdline": "ACCESS DENIED",
|
||||
"full_image_path": "null",
|
||||
}
|
||||
continue
|
||||
|
||||
return {'process_list': processes}
|
|
@ -0,0 +1,38 @@
|
|||
from infection_monkey.config import WormConfiguration
|
||||
from infection_monkey.utils.plugins.plugin import Plugin
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
import infection_monkey.system_info.collectors
|
||||
|
||||
|
||||
class SystemInfoCollector(Plugin, metaclass=ABCMeta):
|
||||
"""
|
||||
ABC for system info collection. See system_info_collector_handler for more info. Basically, to implement a new system info
|
||||
collector, inherit from this class in an implementation in the infection_monkey.system_info.collectors class, and override
|
||||
the 'collect' method. Don't forget to parse your results in the Monkey Island and to add the collector to the configuration
|
||||
as well - see monkey_island.cc.services.processing.system_info_collectors for examples.
|
||||
|
||||
See the Wiki page "How to add a new System Info Collector to the Monkey?" for a detailed guide.
|
||||
"""
|
||||
def __init__(self, name="unknown"):
|
||||
self.name = name
|
||||
|
||||
@staticmethod
|
||||
def should_run(class_name) -> bool:
|
||||
return class_name in WormConfiguration.system_info_collectors_classes
|
||||
|
||||
@staticmethod
|
||||
def base_package_file():
|
||||
return infection_monkey.system_info.collectors.__file__
|
||||
|
||||
@staticmethod
|
||||
def base_package_name():
|
||||
return infection_monkey.system_info.collectors.__package__
|
||||
|
||||
@abstractmethod
|
||||
def collect(self) -> dict:
|
||||
"""
|
||||
Collect the relevant information and return it in a dictionary.
|
||||
To be implemented by each collector.
|
||||
"""
|
||||
raise NotImplementedError()
|
|
@ -0,0 +1,33 @@
|
|||
import logging
|
||||
from typing import Sequence
|
||||
|
||||
from infection_monkey.system_info.system_info_collector import SystemInfoCollector
|
||||
from infection_monkey.telemetry.system_info_telem import SystemInfoTelem
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SystemInfoCollectorsHandler(object):
|
||||
def __init__(self):
|
||||
self.collectors_list = self.config_to_collectors_list()
|
||||
|
||||
def execute_all_configured(self):
|
||||
successful_collections = 0
|
||||
system_info_telemetry = {}
|
||||
for collector in self.collectors_list:
|
||||
try:
|
||||
LOG.debug("Executing system info collector: '{}'".format(collector.name))
|
||||
collected_info = collector.collect()
|
||||
system_info_telemetry[collector.name] = collected_info
|
||||
successful_collections += 1
|
||||
except Exception as e:
|
||||
# If we failed one collector, no need to stop execution. Log and continue.
|
||||
LOG.error("Collector {} failed. Error info: {}".format(collector.name, e))
|
||||
LOG.info("All system info collectors executed. Total {} executed, out of which {} collected successfully.".
|
||||
format(len(self.collectors_list), successful_collections))
|
||||
|
||||
SystemInfoTelem({"collectors": system_info_telemetry}).send()
|
||||
|
||||
@staticmethod
|
||||
def config_to_collectors_list() -> Sequence[SystemInfoCollector]:
|
||||
return SystemInfoCollector.get_instances()
|
|
@ -1,6 +1,6 @@
|
|||
import monkey_island.cc.auth
|
||||
from monkey_island.cc.environment import Environment
|
||||
from common.cloud.aws_instance import AwsInstance
|
||||
from common.cloud.aws.aws_instance import AwsInstance
|
||||
|
||||
__author__ = 'itay.mizeretz'
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ from monkey_island.cc.models.monkey_ttl import MonkeyTtl, create_monkey_ttl_docu
|
|||
from monkey_island.cc.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS
|
||||
from monkey_island.cc.models.command_control_channel import CommandControlChannel
|
||||
from monkey_island.cc.utils import local_ip_addresses
|
||||
from common.cloud import environment_names
|
||||
|
||||
MAX_MONKEYS_AMOUNT_TO_CACHE = 100
|
||||
|
||||
|
@ -42,6 +43,9 @@ class Monkey(Document):
|
|||
ttl_ref = ReferenceField(MonkeyTtl)
|
||||
tunnel = ReferenceField("self")
|
||||
command_control_channel = EmbeddedDocumentField(CommandControlChannel)
|
||||
|
||||
# Environment related fields
|
||||
environment = StringField(default=environment_names.Environment.UNKNOWN, choices=environment_names.ALL_ENVIRONMENTS_NAMES)
|
||||
aws_instance_id = StringField(required=False) # This field only exists when the monkey is running on an AWS
|
||||
|
||||
# instance. See https://github.com/guardicore/monkey/issues/426.
|
||||
|
@ -55,7 +59,8 @@ class Monkey(Document):
|
|||
raise MonkeyNotFoundError("info: {0} | id: {1}".format(ex, str(db_id)))
|
||||
|
||||
@staticmethod
|
||||
def get_single_monkey_by_guid(monkey_guid):
|
||||
# See https://www.python.org/dev/peps/pep-0484/#forward-references
|
||||
def get_single_monkey_by_guid(monkey_guid) -> 'Monkey':
|
||||
try:
|
||||
return Monkey.objects.get(guid=monkey_guid)
|
||||
except DoesNotExist as ex:
|
||||
|
|
|
@ -6,7 +6,7 @@ import flask_restful
|
|||
|
||||
from monkey_island.cc.auth import jwt_required
|
||||
from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService
|
||||
from common.cloud.aws_service import AwsService
|
||||
from common.cloud.aws.aws_service import AwsService
|
||||
|
||||
CLIENT_ERROR_FORMAT = "ClientError, error message: '{}'. Probably, the IAM role that has been associated with the " \
|
||||
"instance doesn't permit SSM calls. "
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from common.data.system_info_collectors_names import *
|
||||
|
||||
WARNING_SIGN = " \u26A0"
|
||||
|
||||
SCHEMA = {
|
||||
|
@ -99,6 +101,44 @@ SCHEMA = {
|
|||
}
|
||||
]
|
||||
},
|
||||
"system_info_collectors_classes": {
|
||||
"title": "System Information Collectors",
|
||||
"type": "string",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
ENVIRONMENT_COLLECTOR
|
||||
],
|
||||
"title": "Collect which environment this machine is on (on prem/cloud)",
|
||||
"attack_techniques": []
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
AWS_COLLECTOR
|
||||
],
|
||||
"title": "If on AWS, collect more information about the instance",
|
||||
"attack_techniques": []
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
HOSTNAME_COLLECTOR
|
||||
],
|
||||
"title": "Collect the machine's hostname",
|
||||
"attack_techniques": []
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
PROCESS_LIST_COLLECTOR
|
||||
],
|
||||
"title": "Collect running processes on the machine",
|
||||
"attack_techniques": []
|
||||
},
|
||||
],
|
||||
},
|
||||
"post_breach_acts": {
|
||||
"title": "Post breach actions",
|
||||
"type": "string",
|
||||
|
@ -433,6 +473,21 @@ SCHEMA = {
|
|||
"attack_techniques": ["T1003"],
|
||||
"description": "Determines whether to use Mimikatz"
|
||||
},
|
||||
"system_info_collectors_classes": {
|
||||
"title": "System info collectors",
|
||||
"type": "array",
|
||||
"uniqueItems": True,
|
||||
"items": {
|
||||
"$ref": "#/definitions/system_info_collectors_classes"
|
||||
},
|
||||
"default": [
|
||||
ENVIRONMENT_COLLECTOR,
|
||||
AWS_COLLECTOR,
|
||||
HOSTNAME_COLLECTOR,
|
||||
PROCESS_LIST_COLLECTOR
|
||||
],
|
||||
"description": "Determines which system information collectors will collect information."
|
||||
},
|
||||
}
|
||||
},
|
||||
"life_cycle": {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import logging
|
||||
|
||||
from common.cloud.aws_instance import AwsInstance
|
||||
from common.cloud.aws_service import AwsService
|
||||
from common.cloud.aws.aws_instance import AwsInstance
|
||||
from common.cloud.aws.aws_service import AwsService
|
||||
from common.cmd.aws.aws_cmd_runner import AwsCmdRunner
|
||||
from common.cmd.cmd import Cmd
|
||||
from common.cmd.cmd_runner import CmdRunner
|
||||
|
@ -54,7 +54,7 @@ class RemoteRunAwsService:
|
|||
|
||||
@staticmethod
|
||||
def is_running_on_aws():
|
||||
return RemoteRunAwsService.aws_instance.is_aws_instance()
|
||||
return RemoteRunAwsService.aws_instance.is_instance()
|
||||
|
||||
@staticmethod
|
||||
def update_aws_region_authless():
|
||||
|
|
|
@ -5,7 +5,7 @@ from datetime import datetime
|
|||
import boto3
|
||||
from botocore.exceptions import UnknownServiceError
|
||||
|
||||
from common.cloud.aws_instance import AwsInstance
|
||||
from common.cloud.aws.aws_instance import AwsInstance
|
||||
from monkey_island.cc.environment.environment import load_server_configuration_from_file
|
||||
from monkey_island.cc.services.reporting.exporter import Exporter
|
||||
|
||||
|
|
|
@ -386,10 +386,11 @@ class ReportService:
|
|||
@staticmethod
|
||||
def get_monkey_subnets(monkey_guid):
|
||||
network_info = mongo.db.telemetry.find_one(
|
||||
{'telem_category': 'system_info', 'monkey_guid': monkey_guid},
|
||||
{'telem_category': 'system_info',
|
||||
'monkey_guid': monkey_guid},
|
||||
{'data.network_info.networks': 1}
|
||||
)
|
||||
if network_info is None:
|
||||
if network_info is None or not network_info["data"]:
|
||||
return []
|
||||
|
||||
return \
|
||||
|
|
|
@ -15,28 +15,34 @@ __regular_report_generating_lock = threading.Semaphore()
|
|||
def safe_generate_reports():
|
||||
# Entering the critical section; Wait until report generation is available.
|
||||
__report_generating_lock.acquire()
|
||||
report = safe_generate_regular_report()
|
||||
attack_report = safe_generate_attack_report()
|
||||
# Leaving the critical section.
|
||||
__report_generating_lock.release()
|
||||
try:
|
||||
report = safe_generate_regular_report()
|
||||
attack_report = safe_generate_attack_report()
|
||||
finally:
|
||||
# Leaving the critical section.
|
||||
__report_generating_lock.release()
|
||||
return report, attack_report
|
||||
|
||||
|
||||
def safe_generate_regular_report():
|
||||
# Local import to avoid circular imports
|
||||
from monkey_island.cc.services.reporting.report import ReportService
|
||||
__regular_report_generating_lock.acquire()
|
||||
report = ReportService.generate_report()
|
||||
__regular_report_generating_lock.release()
|
||||
try:
|
||||
__regular_report_generating_lock.acquire()
|
||||
report = ReportService.generate_report()
|
||||
finally:
|
||||
__regular_report_generating_lock.release()
|
||||
return report
|
||||
|
||||
|
||||
def safe_generate_attack_report():
|
||||
# Local import to avoid circular imports
|
||||
from monkey_island.cc.services.attack.attack_report import AttackReportService
|
||||
__attack_report_generating_lock.acquire()
|
||||
attack_report = AttackReportService.generate_new_report()
|
||||
__attack_report_generating_lock.release()
|
||||
try:
|
||||
__attack_report_generating_lock.acquire()
|
||||
attack_report = AttackReportService.generate_new_report()
|
||||
finally:
|
||||
__attack_report_generating_lock.release()
|
||||
return attack_report
|
||||
|
||||
|
||||
|
|
|
@ -1,25 +1,23 @@
|
|||
import logging
|
||||
|
||||
from monkey_island.cc.database import mongo
|
||||
from monkey_island.cc.models import Monkey
|
||||
from monkey_island.cc.services import mimikatz_utils
|
||||
from monkey_island.cc.services.node import NodeService
|
||||
from monkey_island.cc.services.config import ConfigService
|
||||
from monkey_island.cc.services.telemetry.zero_trust_tests.antivirus_existence import test_antivirus_existence
|
||||
from monkey_island.cc.services.wmi_handler import WMIHandler
|
||||
from monkey_island.cc.encryptor import encryptor
|
||||
from monkey_island.cc.services import mimikatz_utils
|
||||
from monkey_island.cc.services.config import ConfigService
|
||||
from monkey_island.cc.services.node import NodeService
|
||||
from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import \
|
||||
SystemInfoTelemetryDispatcher
|
||||
from monkey_island.cc.services.wmi_handler import WMIHandler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def process_system_info_telemetry(telemetry_json):
|
||||
dispatcher = SystemInfoTelemetryDispatcher()
|
||||
telemetry_processing_stages = [
|
||||
process_ssh_info,
|
||||
process_credential_info,
|
||||
process_mimikatz_and_wmi_info,
|
||||
process_aws_data,
|
||||
update_db_with_new_hostname,
|
||||
test_antivirus_existence,
|
||||
dispatcher.dispatch_collector_results_to_relevant_processors
|
||||
]
|
||||
|
||||
# Calling safe_process_telemetry so if one of the stages fail, we log and move on instead of failing the rest of
|
||||
|
@ -34,7 +32,7 @@ def safe_process_telemetry(processing_function, telemetry_json):
|
|||
processing_function(telemetry_json)
|
||||
except Exception as err:
|
||||
logger.error(
|
||||
"Error {} while in {} stage of processing telemetry.".format(str(err), processing_function.func_name),
|
||||
"Error {} while in {} stage of processing telemetry.".format(str(err), processing_function.__name__),
|
||||
exc_info=True)
|
||||
|
||||
|
||||
|
@ -104,14 +102,3 @@ def process_mimikatz_and_wmi_info(telemetry_json):
|
|||
wmi_handler = WMIHandler(monkey_id, telemetry_json['data']['wmi'], users_secrets)
|
||||
wmi_handler.process_and_handle_wmi_info()
|
||||
|
||||
|
||||
def process_aws_data(telemetry_json):
|
||||
if 'aws' in telemetry_json['data']:
|
||||
if 'instance_id' in telemetry_json['data']['aws']:
|
||||
monkey_id = NodeService.get_monkey_by_guid(telemetry_json['monkey_guid']).get('_id')
|
||||
mongo.db.monkey.update_one({'_id': monkey_id},
|
||||
{'$set': {'aws_instance_id': telemetry_json['data']['aws']['instance_id']}})
|
||||
|
||||
|
||||
def update_db_with_new_hostname(telemetry_json):
|
||||
Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid']).set_hostname(telemetry_json['data']['hostname'])
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import logging
|
||||
|
||||
from monkey_island.cc.models.monkey import Monkey
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def process_aws_telemetry(collector_results, monkey_guid):
|
||||
relevant_monkey = Monkey.get_single_monkey_by_guid(monkey_guid)
|
||||
|
||||
if "instance_id" in collector_results:
|
||||
instance_id = collector_results["instance_id"]
|
||||
relevant_monkey.aws_instance_id = instance_id
|
||||
relevant_monkey.save()
|
||||
logger.debug("Updated Monkey {} with aws instance id {}".format(str(relevant_monkey), instance_id))
|
|
@ -0,0 +1,12 @@
|
|||
import logging
|
||||
|
||||
from monkey_island.cc.models.monkey import Monkey
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def process_environment_telemetry(collector_results, monkey_guid):
|
||||
relevant_monkey = Monkey.get_single_monkey_by_guid(monkey_guid)
|
||||
relevant_monkey.environment = collector_results["environment"]
|
||||
relevant_monkey.save()
|
||||
logger.debug("Updated Monkey {} with env {}".format(str(relevant_monkey), collector_results))
|
|
@ -0,0 +1,9 @@
|
|||
import logging
|
||||
|
||||
from monkey_island.cc.models.monkey import Monkey
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def process_hostname_telemetry(collector_results, monkey_guid):
|
||||
Monkey.get_single_monkey_by_guid(monkey_guid).set_hostname(collector_results["hostname"])
|
|
@ -0,0 +1,63 @@
|
|||
import logging
|
||||
import typing
|
||||
|
||||
from common.data.system_info_collectors_names import *
|
||||
from monkey_island.cc.services.telemetry.processing.system_info_collectors.aws import process_aws_telemetry
|
||||
from monkey_island.cc.services.telemetry.processing.system_info_collectors.environment import process_environment_telemetry
|
||||
from monkey_island.cc.services.telemetry.processing.system_info_collectors.hostname import process_hostname_telemetry
|
||||
from monkey_island.cc.services.telemetry.zero_trust_tests.antivirus_existence import test_antivirus_existence
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SYSTEM_INFO_COLLECTOR_TO_TELEMETRY_PROCESSORS = {
|
||||
AWS_COLLECTOR: [process_aws_telemetry],
|
||||
ENVIRONMENT_COLLECTOR: [process_environment_telemetry],
|
||||
HOSTNAME_COLLECTOR: [process_hostname_telemetry],
|
||||
PROCESS_LIST_COLLECTOR: [test_antivirus_existence]
|
||||
}
|
||||
|
||||
|
||||
class SystemInfoTelemetryDispatcher(object):
|
||||
def __init__(self, collector_to_parsing_functions: typing.Mapping[str, typing.List[typing.Callable]] = None):
|
||||
"""
|
||||
:param collector_to_parsing_functions: Map between collector names and a list of functions that process the output of
|
||||
that collector. If `None` is supplied, uses the default one; This should be the normal flow, overriding the
|
||||
collector->functions mapping is useful mostly for testing.
|
||||
"""
|
||||
if collector_to_parsing_functions is None:
|
||||
collector_to_parsing_functions = SYSTEM_INFO_COLLECTOR_TO_TELEMETRY_PROCESSORS
|
||||
self.collector_to_processing_functions = collector_to_parsing_functions
|
||||
|
||||
def dispatch_collector_results_to_relevant_processors(self, telemetry_json):
|
||||
"""
|
||||
If the telemetry has collectors' results, dispatches the results to the relevant processing functions.
|
||||
:param telemetry_json: Telemetry sent from the Monkey
|
||||
"""
|
||||
if "collectors" in telemetry_json["data"]:
|
||||
self.dispatch_single_result_to_relevant_processor(telemetry_json)
|
||||
|
||||
def dispatch_single_result_to_relevant_processor(self, telemetry_json):
|
||||
relevant_monkey_guid = telemetry_json['monkey_guid']
|
||||
|
||||
for collector_name, collector_results in telemetry_json["data"]["collectors"].items():
|
||||
self.dispatch_result_of_single_collector_to_processing_functions(
|
||||
collector_name,
|
||||
collector_results,
|
||||
relevant_monkey_guid)
|
||||
|
||||
def dispatch_result_of_single_collector_to_processing_functions(
|
||||
self,
|
||||
collector_name,
|
||||
collector_results,
|
||||
relevant_monkey_guid):
|
||||
if collector_name in self.collector_to_processing_functions:
|
||||
for processing_function in self.collector_to_processing_functions[collector_name]:
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
processing_function(collector_results, relevant_monkey_guid)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error {} while processing {} system info telemetry".format(str(e), collector_name),
|
||||
exc_info=True)
|
||||
else:
|
||||
logger.warning("Unknown system info collector name: {}".format(collector_name))
|
|
@ -0,0 +1,57 @@
|
|||
import uuid
|
||||
|
||||
from monkey_island.cc.models import Monkey
|
||||
from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import \
|
||||
SystemInfoTelemetryDispatcher
|
||||
from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import \
|
||||
process_aws_telemetry
|
||||
from monkey_island.cc.testing.IslandTestCase import IslandTestCase
|
||||
|
||||
TEST_SYS_INFO_TO_PROCESSING = {
|
||||
"AwsCollector": [process_aws_telemetry],
|
||||
}
|
||||
|
||||
|
||||
class SystemInfoTelemetryDispatcherTest(IslandTestCase):
|
||||
def test_dispatch_to_relevant_collector_bad_inputs(self):
|
||||
self.fail_if_not_testing_env()
|
||||
|
||||
dispatcher = SystemInfoTelemetryDispatcher(TEST_SYS_INFO_TO_PROCESSING)
|
||||
|
||||
# Bad format telem JSONs - throws
|
||||
bad_empty_telem_json = {}
|
||||
self.assertRaises(KeyError, dispatcher.dispatch_collector_results_to_relevant_processors, bad_empty_telem_json)
|
||||
bad_no_data_telem_json = {"monkey_guid": "bla"}
|
||||
self.assertRaises(KeyError, dispatcher.dispatch_collector_results_to_relevant_processors, bad_no_data_telem_json)
|
||||
bad_no_monkey_telem_json = {"data": {"collectors": {"AwsCollector": "Bla"}}}
|
||||
self.assertRaises(KeyError, dispatcher.dispatch_collector_results_to_relevant_processors, bad_no_monkey_telem_json)
|
||||
|
||||
# Telem JSON with no collectors - nothing gets dispatched
|
||||
good_telem_no_collectors = {"monkey_guid": "bla", "data": {"bla": "bla"}}
|
||||
good_telem_empty_collectors = {"monkey_guid": "bla", "data": {"bla": "bla", "collectors": {}}}
|
||||
|
||||
dispatcher.dispatch_collector_results_to_relevant_processors(good_telem_no_collectors)
|
||||
dispatcher.dispatch_collector_results_to_relevant_processors(good_telem_empty_collectors)
|
||||
|
||||
def test_dispatch_to_relevant_collector(self):
|
||||
self.fail_if_not_testing_env()
|
||||
self.clean_monkey_db()
|
||||
|
||||
a_monkey = Monkey(guid=str(uuid.uuid4()))
|
||||
a_monkey.save()
|
||||
|
||||
dispatcher = SystemInfoTelemetryDispatcher()
|
||||
|
||||
# JSON with results - make sure functions are called
|
||||
instance_id = "i-0bd2c14bd4c7d703f"
|
||||
telem_json = {
|
||||
"data": {
|
||||
"collectors": {
|
||||
"AwsCollector": {"instance_id": instance_id},
|
||||
}
|
||||
},
|
||||
"monkey_guid": a_monkey.guid
|
||||
}
|
||||
dispatcher.dispatch_collector_results_to_relevant_processors(telem_json)
|
||||
|
||||
self.assertEquals(Monkey.get_single_monkey_by_guid(a_monkey.guid).aws_instance_id, instance_id)
|
|
@ -7,36 +7,36 @@ from monkey_island.cc.models.zero_trust.event import Event
|
|||
from monkey_island.cc.services.telemetry.zero_trust_tests.known_anti_viruses import ANTI_VIRUS_KNOWN_PROCESS_NAMES
|
||||
|
||||
|
||||
def test_antivirus_existence(telemetry_json):
|
||||
current_monkey = Monkey.get_single_monkey_by_guid(telemetry_json['monkey_guid'])
|
||||
if 'process_list' in telemetry_json['data']:
|
||||
process_list_event = Event.create_event(
|
||||
title="Process list",
|
||||
message="Monkey on {} scanned the process list".format(current_monkey.hostname),
|
||||
event_type=zero_trust_consts.EVENT_TYPE_MONKEY_LOCAL)
|
||||
events = [process_list_event]
|
||||
def test_antivirus_existence(process_list_json, monkey_guid):
|
||||
current_monkey = Monkey.get_single_monkey_by_guid(monkey_guid)
|
||||
|
||||
av_processes = filter_av_processes(telemetry_json)
|
||||
process_list_event = Event.create_event(
|
||||
title="Process list",
|
||||
message="Monkey on {} scanned the process list".format(current_monkey.hostname),
|
||||
event_type=zero_trust_consts.EVENT_TYPE_MONKEY_LOCAL)
|
||||
events = [process_list_event]
|
||||
|
||||
for process in av_processes:
|
||||
events.append(Event.create_event(
|
||||
title="Found AV process",
|
||||
message="The process '{}' was recognized as an Anti Virus process. Process "
|
||||
"details: {}".format(process[1]['name'], json.dumps(process[1])),
|
||||
event_type=zero_trust_consts.EVENT_TYPE_MONKEY_LOCAL
|
||||
))
|
||||
av_processes = filter_av_processes(process_list_json["process_list"])
|
||||
|
||||
if len(av_processes) > 0:
|
||||
test_status = zero_trust_consts.STATUS_PASSED
|
||||
else:
|
||||
test_status = zero_trust_consts.STATUS_FAILED
|
||||
AggregateFinding.create_or_add_to_existing(
|
||||
test=zero_trust_consts.TEST_ENDPOINT_SECURITY_EXISTS, status=test_status, events=events
|
||||
)
|
||||
for process in av_processes:
|
||||
events.append(Event.create_event(
|
||||
title="Found AV process",
|
||||
message="The process '{}' was recognized as an Anti Virus process. Process "
|
||||
"details: {}".format(process[1]['name'], json.dumps(process[1])),
|
||||
event_type=zero_trust_consts.EVENT_TYPE_MONKEY_LOCAL
|
||||
))
|
||||
|
||||
if len(av_processes) > 0:
|
||||
test_status = zero_trust_consts.STATUS_PASSED
|
||||
else:
|
||||
test_status = zero_trust_consts.STATUS_FAILED
|
||||
AggregateFinding.create_or_add_to_existing(
|
||||
test=zero_trust_consts.TEST_ENDPOINT_SECURITY_EXISTS, status=test_status, events=events
|
||||
)
|
||||
|
||||
|
||||
def filter_av_processes(telemetry_json):
|
||||
all_processes = list(telemetry_json['data']['process_list'].items())
|
||||
def filter_av_processes(process_list):
|
||||
all_processes = list(process_list.items())
|
||||
av_processes = []
|
||||
for process in all_processes:
|
||||
process_name = process[1]['name']
|
||||
|
|
Loading…
Reference in New Issue