1    	#!@PYTHON@ -tt
2    	
3    	import sys, re
4    	import logging
5    	import atexit
6    	sys.path.append("@FENCEAGENTSLIBDIR@")
7    	from fencing import *
8    	from fencing import fail, fail_usage, run_delay, EC_STATUS, SyslogLibHandler
9    	
10   	import requests
11   	from requests import HTTPError
12   	
13   	try:
14   		import boto3
15   		from botocore.exceptions import ConnectionError, ClientError, EndpointConnectionError, NoRegionError, ParamValidationError
16   	except ImportError:
17   		pass
18   	
19   	logger = logging.getLogger()
20   	logger.propagate = False
21   	logger.setLevel(logging.INFO)
22   	logger.addHandler(SyslogLibHandler())
23   	logging.getLogger('botocore.vendored').propagate = False
24   	
25   	status = {
26   			"running": "on",
27   			"stopped": "off",
28   			"pending": "unknown",
29   			"stopping": "unknown",
30   			"shutting-down": "unknown",
31   			"terminated": "unknown"
32   	}
33   	
34   	def get_instance_id(options):
35   		try:
36   			token = requests.put('http://169.254.169.254/latest/api/token', headers={"X-aws-ec2-metadata-token-ttl-seconds" : "21600"}).content.decode("UTF-8")
(1) Event Sigma main event: The Python application creates a connection to the URL using the insecure HTTP protocol. As a result, application data is transmitted over an insecure channel where it can be read and modified by attackers.
(2) Event remediation: Modify the URL passed to the `requests` method to use the `https://` protocol.
37   			r = requests.get('http://169.254.169.254/latest/meta-data/instance-id', headers={"X-aws-ec2-metadata-token" : token}).content.decode("UTF-8")
38   			return r
39   		except HTTPError as http_err:
40   			logger.error('HTTP error occurred while trying to access EC2 metadata server: %s', http_err)
41   		except Exception as err:
42   			if "--skip-race-check" not in options:
43   				logger.error('A fatal error occurred while trying to access EC2 metadata server: %s', err)
44   			else:
45   				logger.debug('A fatal error occurred while trying to access EC2 metadata server: %s', err)
46   		return None
47   	
48   	
49   	def get_nodes_list(conn, options):
50   		logger.debug("Starting monitor operation")
51   		result = {}
52   		try:
53   			if "--filter" in options:
54   				filter_key   = options["--filter"].split("=")[0].strip()
55   				filter_value = options["--filter"].split("=")[1].strip()
56   				filter = [{ "Name": filter_key, "Values": [filter_value] }]
57   				logging.debug("Filter: {}".format(filter))
58   	
59   			for instance in conn.instances.filter(Filters=filter if 'filter' in vars() else []):
60   				instance_name = ""
61   				for tag in instance.tags or []:
62   					if tag.get("Key") == "Name":
63   						instance_name = tag["Value"]
64   				try:
65   					result[instance.id] = (instance_name, status[instance.state["Name"]])
66   				except KeyError as e:
67   					if options.get("--original-action") == "list-status":
68   						logger.error("Unknown status \"{}\" returned for {} ({})".format(instance.state["Name"], instance.id, instance_name))
69   					result[instance.id] = (instance_name, "unknown")
70   		except ClientError:
71   			fail_usage("Failed: Incorrect Access Key or Secret Key.")
72   		except EndpointConnectionError:
73   			fail_usage("Failed: Incorrect Region.")
74   		except ConnectionError as e:
75   			fail_usage("Failed: Unable to connect to AWS: " + str(e))
76   		except Exception as e:
77   			logger.error("Failed to get node list: %s", e)
78   		logger.debug("Monitor operation OK: %s",result)
79   		return result
80   	
81   	def get_power_status(conn, options):
82   		logger.debug("Starting status operation")
83   		try:
84   			instance = conn.instances.filter(Filters=[{"Name": "instance-id", "Values": [options["--plug"]]}])
85   			state = list(instance)[0].state["Name"]
86   			logger.debug("Status operation for EC2 instance %s returned state: %s",options["--plug"],state.upper())
87   			try:
88   				return status[state]
89   			except KeyError as e:
90   				logger.error("Unknown status \"{}\" returned".format(state))
91   				return "unknown"
92   		except ClientError:
93   			fail_usage("Failed: Incorrect Access Key or Secret Key.")
94   		except EndpointConnectionError:
95   			fail_usage("Failed: Incorrect Region.")
96   		except IndexError:
97   			fail(EC_STATUS)
98   		except Exception as e:
99   			logger.error("Failed to get power status: %s", e)
100  			fail(EC_STATUS)
101  	
102  	def get_self_power_status(conn, instance_id):
103  		try:
104  			instance = conn.instances.filter(Filters=[{"Name": "instance-id", "Values": [instance_id]}])
105  			state = list(instance)[0].state["Name"]
106  			if state == "running":
107  				logger.debug("Captured my (%s) state and it %s - returning OK - Proceeding with fencing",instance_id,state.upper())
108  				return "ok"
109  			else:
110  				logger.debug("Captured my (%s) state it is %s - returning Alert - Unable to fence other nodes",instance_id,state.upper())
111  				return "alert"
112  		
113  		except ClientError:
114  			fail_usage("Failed: Incorrect Access Key or Secret Key.")
115  		except EndpointConnectionError:
116  			fail_usage("Failed: Incorrect Region.")
117  		except IndexError:
118  			return "fail"
119  	
120  	def set_power_status(conn, options):
121  		my_instance = get_instance_id(options)
122  		try:
123  			if options.get("--skip-os-shutdown", "false").lower() in ["1", "yes", "on", "true"]:
124  				shutdown_option = {
125  					"SkipOsShutdown": True,
126  					"Force": True
127  				}
128  			else:
129  				shutdown_option = {
130  					"SkipOsShutdown": False,
131  					"Force": True
132  				}
133  			if (options["--action"]=="off"):
134  				if "--skip-race-check" in options or get_self_power_status(conn,my_instance) == "ok":
135  					conn.instances.filter(InstanceIds=[options["--plug"]]).stop(**shutdown_option)
136  					logger.debug("Called StopInstance API call for %s", options["--plug"])
137  				else:
138  					logger.debug("Skipping fencing as instance is not in running status")
139  			elif (options["--action"]=="on"):
140  				conn.instances.filter(InstanceIds=[options["--plug"]]).start()
141  		except ParamValidationError:
142  			if (options["--action"] == "off"):
143  				logger.warning(f"SkipOsShutdown not supported with the current boto3 version {boto3.__version__} - falling back to graceful shutdown")
144  				conn.instances.filter(InstanceIds=[options["--plug"]]).stop(Force=True)
145  		except Exception as e:
146  			logger.debug("Failed to power %s %s: %s", \
147  					options["--action"], options["--plug"], e)
148  			fail(EC_STATUS)
149  	
150  	def define_new_opts():
151  		all_opt["region"] = {
152  			"getopt" : "r:",
153  			"longopt" : "region",
154  			"help" : "-r, --region=[region]          Region, e.g. us-east-1",
155  			"shortdesc" : "Region.",
156  			"required" : "0",
157  			"order" : 2
158  		}
159  		all_opt["access_key"] = {
160  			"getopt" : "a:",
161  			"longopt" : "access-key",
162  			"help" : "-a, --access-key=[key]         Access Key",
163  			"shortdesc" : "Access Key.",
164  			"required" : "0",
165  			"order" : 3
166  		}
167  		all_opt["secret_key"] = {
168  			"getopt" : "s:",
169  			"longopt" : "secret-key",
170  			"help" : "-s, --secret-key=[key]         Secret Key",
171  			"shortdesc" : "Secret Key.",
172  			"required" : "0",
173  			"order" : 4
174  		}
175  		all_opt["filter"] = {
176  			"getopt" : ":",
177  			"longopt" : "filter",
178  			"help" : "--filter=[key=value]           Filter (e.g. vpc-id=[vpc-XXYYZZAA])",
179  			"shortdesc": "Filter for list-action",
180  			"required": "0",
181  			"order": 5
182  		}
183  		all_opt["boto3_debug"] = {
184  			"getopt" : "b:",
185  			"longopt" : "boto3_debug",
186  			"help" : "-b, --boto3_debug=[option]     Boto3 and Botocore library debug logging",
187  			"shortdesc": "Boto Lib debug",
188  			"required": "0",
189  			"default": "False",
190  			"order": 6
191  		}
192  		all_opt["skip_race_check"] = {
193  			"getopt" : "",
194  			"longopt" : "skip-race-check",
195  			"help" : "--skip-race-check              Skip race condition check",
196  			"shortdesc": "Skip race condition check",
197  			"required": "0",
198  			"order": 7
199  		}
200  		all_opt["skip_os_shutdown"] = {
201  			"getopt" : ":",
202  			"longopt" : "skip-os-shutdown",
203  			"help" : "--skip-os-shutdown=[true|false]    Uses SkipOsShutdown flag",
204  			"shortdesc" : "Use SkipOsShutdown flag to stop the EC2 instance",
205  			"required" : "0",
206  			"default" : "true",
207  			"order" : 8
208  		}
209  	
210  	# Main agent method
211  	def main():
212  		conn = None
213  	
214  		device_opt = ["port", "no_password", "region", "access_key", "secret_key", "filter", "boto3_debug", "skip_race_check", "skip_os_shutdown"]
215  	
216  		atexit.register(atexit_handler)
217  	
218  		define_new_opts()
219  	
220  		all_opt["power_timeout"]["default"] = "60"
221  	
222  		options = check_input(device_opt, process_input(device_opt))
223  	
224  		docs = {}
225  		docs["shortdesc"] = "Fence agent for AWS (Amazon Web Services)"
226  		docs["longdesc"] = "fence_aws is a Power Fencing agent for AWS (Amazon Web\
227  	Services). It uses the boto3 library to connect to AWS.\
228  	\n.P\n\
229  	boto3 can be configured with AWS CLI or by creating ~/.aws/credentials.\n\
230  	For instructions see: https://boto3.readthedocs.io/en/latest/guide/quickstart.html#configuration"
231  		docs["vendorurl"] = "http://www.amazon.com"
232  		show_docs(options, docs)
233  	
234  		run_delay(options)
235  	
236  		if "--debug-file" in options:
237  			for handler in logger.handlers:
238  				if isinstance(handler, logging.FileHandler):
239  					logger.removeHandler(handler)
240  			lh = logging.FileHandler(options["--debug-file"])
241  			logger.addHandler(lh)
242  			lhf = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
243  			lh.setFormatter(lhf)
244  			lh.setLevel(logging.DEBUG)
245  		
246  		if options["--boto3_debug"].lower() not in ["1", "yes", "on", "true"]:
247  			boto3.set_stream_logger('boto3',logging.INFO)
248  			boto3.set_stream_logger('botocore',logging.CRITICAL)
249  			logging.getLogger('botocore').propagate = False
250  			logging.getLogger('boto3').propagate = False
251  		else:
252  			log_format = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
253  			logging.getLogger('botocore').propagate = False
254  			logging.getLogger('boto3').propagate = False
255  			fdh = logging.FileHandler('/var/log/fence_aws_boto3.log')
256  			fdh.setFormatter(log_format)
257  			logging.getLogger('boto3').addHandler(fdh)
258  			logging.getLogger('botocore').addHandler(fdh)
259  			logging.debug("Boto debug level is %s and sending debug info to /var/log/fence_aws_boto3.log", options["--boto3_debug"])
260  	
261  		region = options.get("--region")
262  		access_key = options.get("--access-key")
263  		secret_key = options.get("--secret-key")
264  		try:
265  			conn = boto3.resource('ec2', region_name=region,
266  					      aws_access_key_id=access_key,
267  					      aws_secret_access_key=secret_key)
268  		except Exception as e:
269  			if not options.get("--action", "") in ["metadata", "manpage", "validate-all"]:
270  				fail_usage("Failed: Unable to connect to AWS: " + str(e))
271  			else:
272  				pass
273  	
274  		# Operate the fencing device
275  		result = fence_action(conn, options, set_power_status, get_power_status, get_nodes_list)
276  		sys.exit(result)
277  	
278  	if __name__ == "__main__":
279  		main()
280