From 378baa7139c1e5d94380d57e9e49eb5cb0f3531e Mon Sep 17 00:00:00 2001
From: Itay Mizeretz <itay.mizeretz@guardicore.com>
Date: Sun, 3 Feb 2019 14:18:08 +0200
Subject: [PATCH] Add most infrastrucure for running AWS commands

---
 .../common/cloud/{aws.py => aws_instance.py}  |  2 +-
 monkey/common/cloud/aws_service.py            | 41 +++++++++++++++++++
 monkey/common/cmd/__init__.py                 |  0
 monkey/common/cmd/aws_cmd_result.py           | 20 +++++++++
 monkey/common/cmd/aws_cmd_runner.py           | 39 ++++++++++++++++++
 monkey/common/cmd/cmd_result.py               | 12 ++++++
 monkey/common/cmd/cmd_runner.py               | 22 ++++++++++
 .../system_info/aws_collector.py              |  4 +-
 monkey/monkey_island/cc/environment/aws.py    |  4 +-
 .../cc/resources/aws_exporter.py              |  4 +-
 .../monkey_island/cc/resources/remote_run.py  | 20 +++++++++
 11 files changed, 161 insertions(+), 7 deletions(-)
 rename monkey/common/cloud/{aws.py => aws_instance.py} (95%)
 create mode 100644 monkey/common/cloud/aws_service.py
 create mode 100644 monkey/common/cmd/__init__.py
 create mode 100644 monkey/common/cmd/aws_cmd_result.py
 create mode 100644 monkey/common/cmd/aws_cmd_runner.py
 create mode 100644 monkey/common/cmd/cmd_result.py
 create mode 100644 monkey/common/cmd/cmd_runner.py
 create mode 100644 monkey/monkey_island/cc/resources/remote_run.py

diff --git a/monkey/common/cloud/aws.py b/monkey/common/cloud/aws_instance.py
similarity index 95%
rename from monkey/common/cloud/aws.py
rename to monkey/common/cloud/aws_instance.py
index 7937815ef..86b8d1a34 100644
--- a/monkey/common/cloud/aws.py
+++ b/monkey/common/cloud/aws_instance.py
@@ -3,7 +3,7 @@ import urllib2
 __author__ = 'itay.mizeretz'
 
 
-class AWS(object):
+class AwsInstance(object):
     def __init__(self):
         try:
             self.instance_id = urllib2.urlopen('http://169.254.169.254/latest/meta-data/instance-id').read()
diff --git a/monkey/common/cloud/aws_service.py b/monkey/common/cloud/aws_service.py
new file mode 100644
index 000000000..0cc641356
--- /dev/null
+++ b/monkey/common/cloud/aws_service.py
@@ -0,0 +1,41 @@
+import boto3
+
+__author__ = 'itay.mizeretz'
+
+
+class AwsService(object):
+    """
+    Supplies various AWS services
+    """
+
+    # TODO: consider changing from static to singleton, and generally change design
+    access_key_id = None
+    secret_access_key = None
+    region = None
+
+    @staticmethod
+    def set_auth_params(access_key_id, secret_access_key):
+        AwsService.access_key_id = access_key_id
+        AwsService.secret_access_key = secret_access_key
+
+    @staticmethod
+    def set_region(region):
+        AwsService.region = region
+
+    @staticmethod
+    def get_client(client_type, region=None):
+        return boto3.client(
+            client_type,
+            aws_access_key_id=AwsService.access_key_id,
+            aws_secret_access_key=AwsService.secret_access_key,
+            region_name=region if region is not None else AwsService.region)
+
+    @staticmethod
+    def get_session():
+        return boto3.session.Session(
+            aws_access_key_id=AwsService.access_key_id,
+            aws_secret_access_key=AwsService.secret_access_key)
+
+    @staticmethod
+    def get_regions():
+        return AwsService.get_session().get_available_regions('ssm')
diff --git a/monkey/common/cmd/__init__.py b/monkey/common/cmd/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/monkey/common/cmd/aws_cmd_result.py b/monkey/common/cmd/aws_cmd_result.py
new file mode 100644
index 000000000..5c9057a61
--- /dev/null
+++ b/monkey/common/cmd/aws_cmd_result.py
@@ -0,0 +1,20 @@
+from common.cmd.cmd_result import CmdResult
+
+
+__author__ = 'itay.mizeretz'
+
+
+class AwsCmdResult(CmdResult):
+    """
+    Class representing an AWS command result
+    """
+
+    def __init__(self, command_info):
+        super(AwsCmdResult, self).__init__(
+            self.is_successful(command_info), command_info[u'ResponseCode'], command_info[u'StandardOutputContent'],
+            command_info[u'StandardErrorContent'])
+        self.command_info = command_info
+
+    @staticmethod
+    def is_successful(command_info):
+        return command_info[u'Status'] == u'Success'
diff --git a/monkey/common/cmd/aws_cmd_runner.py b/monkey/common/cmd/aws_cmd_runner.py
new file mode 100644
index 000000000..0f5032b9d
--- /dev/null
+++ b/monkey/common/cmd/aws_cmd_runner.py
@@ -0,0 +1,39 @@
+import time
+
+from common.cloud.aws_service import AwsService
+from common.cmd.aws_cmd_result import AwsCmdResult
+
+__author__ = 'itay.mizeretz'
+
+
+class AwsCmdRunner(object):
+    """
+    Class for running a command on a remote AWS machine
+    """
+
+    def __init__(self, instance_id, region, is_powershell=False):
+        self.instance_id = instance_id
+        self.region = region
+        self.is_powershell = is_powershell
+        self.ssm = AwsService.get_client('ssm', region)
+
+    def run_command(self, command, timeout):
+        command_id = self._send_command(command)
+        init_time = time.time()
+        curr_time = init_time
+        command_info = None
+        while curr_time - init_time < timeout:
+            command_info = self.ssm.get_command_invocation(CommandId=command_id, InstanceId=self.instance_id)
+            if AwsCmdResult.is_successful(command_info):
+                break
+            else:
+                time.sleep(0.5)
+                curr_time = time.time()
+
+        return AwsCmdResult(command_info)
+
+    def _send_command(self, command):
+        doc_name = "AWS-RunPowerShellScript" if self.is_powershell else "AWS-RunShellScript"
+        command_res = self.ssm.send_command(DocumentName=doc_name, Parameters={'commands': [command]},
+                                            InstanceIds=[self.instance_id])
+        return command_res['Command']['CommandId']
diff --git a/monkey/common/cmd/cmd_result.py b/monkey/common/cmd/cmd_result.py
new file mode 100644
index 000000000..40eca2c85
--- /dev/null
+++ b/monkey/common/cmd/cmd_result.py
@@ -0,0 +1,12 @@
+
+
+class CmdResult(object):
+    """
+    Class representing a command result
+    """
+
+    def __init__(self, is_success, status_code=None, stdout=None, stderr=None):
+        self.is_success = is_success
+        self.status_code = status_code
+        self.stdout = stdout
+        self.stderr = stderr
diff --git a/monkey/common/cmd/cmd_runner.py b/monkey/common/cmd/cmd_runner.py
new file mode 100644
index 000000000..cf4afa289
--- /dev/null
+++ b/monkey/common/cmd/cmd_runner.py
@@ -0,0 +1,22 @@
+from abc import abstractmethod
+
+__author__ = 'itay.mizeretz'
+
+
+class CmdRunner(object):
+    """
+    Interface for running a command on a remote machine
+    """
+
+    # Default command timeout in seconds
+    DEFAULT_TIMEOUT = 5
+
+    @abstractmethod
+    def run_command(self, command, timeout=DEFAULT_TIMEOUT):
+        """
+        Runs the given command on the remote machine
+        :param command: The command to run
+        :param timeout: Timeout in seconds for command.
+        :return: Command result
+        """
+        raise NotImplementedError()
diff --git a/monkey/infection_monkey/system_info/aws_collector.py b/monkey/infection_monkey/system_info/aws_collector.py
index 699339ae8..df90e5913 100644
--- a/monkey/infection_monkey/system_info/aws_collector.py
+++ b/monkey/infection_monkey/system_info/aws_collector.py
@@ -1,6 +1,6 @@
 import logging
 
-from common.cloud.aws import AWS
+from common.cloud.aws_instance import AwsInstance
 
 __author__ = 'itay.mizeretz'
 
@@ -15,7 +15,7 @@ class AwsCollector(object):
     @staticmethod
     def get_aws_info():
         LOG.info("Collecting AWS info")
-        aws = AWS()
+        aws = AwsInstance()
         info = {}
         if aws.is_aws_instance():
             LOG.info("Machine is an AWS instance")
diff --git a/monkey/monkey_island/cc/environment/aws.py b/monkey/monkey_island/cc/environment/aws.py
index a004a2540..d80157806 100644
--- a/monkey/monkey_island/cc/environment/aws.py
+++ b/monkey/monkey_island/cc/environment/aws.py
@@ -1,6 +1,6 @@
 import cc.auth
 from cc.environment import Environment
-from common.cloud.aws import AWS
+from common.cloud.aws_instance import AwsInstance
 
 __author__ = 'itay.mizeretz'
 
@@ -8,7 +8,7 @@ __author__ = 'itay.mizeretz'
 class AwsEnvironment(Environment):
     def __init__(self):
         super(AwsEnvironment, self).__init__()
-        self.aws_info = AWS()
+        self.aws_info = AwsInstance()
         self._instance_id = self._get_instance_id()
         self.region = self._get_region()
 
diff --git a/monkey/monkey_island/cc/resources/aws_exporter.py b/monkey/monkey_island/cc/resources/aws_exporter.py
index 735de6584..d8a01e909 100644
--- a/monkey/monkey_island/cc/resources/aws_exporter.py
+++ b/monkey/monkey_island/cc/resources/aws_exporter.py
@@ -7,7 +7,7 @@ from botocore.exceptions import UnknownServiceError
 from cc.resources.exporter import Exporter
 from cc.services.config import ConfigService
 from cc.environment.environment import load_server_configuration_from_file
-from common.cloud.aws import AWS
+from common.cloud.aws_instance import AwsInstance
 
 logger = logging.getLogger(__name__)
 
@@ -20,7 +20,7 @@ class AWSExporter(Exporter):
 
     @staticmethod
     def handle_report(report_json):
-        aws = AWS()
+        aws = AwsInstance()
         findings_list = []
         issues_list = report_json['recommendations']['issues']
         if not issues_list:
diff --git a/monkey/monkey_island/cc/resources/remote_run.py b/monkey/monkey_island/cc/resources/remote_run.py
new file mode 100644
index 000000000..6df5aee02
--- /dev/null
+++ b/monkey/monkey_island/cc/resources/remote_run.py
@@ -0,0 +1,20 @@
+import json
+from flask import request, jsonify, make_response
+import flask_restful
+
+
+class RemoteRun(flask_restful.Resource):
+    def run_aws_monkey(self, request_body):
+        instance_id = request_body.get('instance_id')
+        region = request_body.get('region')
+        os = request_body.get('os')  # TODO: consider getting this from instance
+        island_ip = request_body.get('island_ip')  # TODO: Consider getting this another way. Not easy to determine target interface
+
+    def post(self):
+        body = json.loads(request.data)
+        if body.get('type') == 'aws':
+            local_run = self.run_aws_monkey(body)
+            return jsonify(is_running=local_run[0], error_text=local_run[1])
+
+        # default action
+        return make_response({'error': 'Invalid action'}, 500)
\ No newline at end of file